From 316d26cf992ae37306ba08f9f26176940f33f153 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Tue, 2 Dec 2025 11:30:09 +0100 Subject: [PATCH 01/22] feat(design-system): add TableHeadSort to table (#40900) * feat(design-system): add TableHeadSort to table * feat(studio): sort oauth apps columns from header --- .../content/docs/components/table.mdx | 18 +++ .../Auth/OAuthApps/OAuthAppsList.tsx | 118 +++++++++++++++--- .../ui/src/components/shadcn/ui/table.tsx | 85 ++++++++++++- 3 files changed, 204 insertions(+), 17 deletions(-) diff --git a/apps/design-system/content/docs/components/table.mdx b/apps/design-system/content/docs/components/table.mdx index ab2cc6bf5dc..6dcafe2b601 100644 --- a/apps/design-system/content/docs/components/table.mdx +++ b/apps/design-system/content/docs/components/table.mdx @@ -48,6 +48,24 @@ import { ``` +## Sortable Columns + +Use `TableHeadSort` inside `TableHead` to enable column sorting. + +| Prop | Type | Description | +| -------------- | -------------------------- | ------------------------------------------------------------------ | +| `column` | `string` | Unique identifier for the column | +| `currentSort` | `string` | Current sort state in format `"column:order"` (e.g., `"name:asc"`) | +| `onSortChange` | `(column: string) => void` | Callback fired when column header is clicked | + +```tsx + + + Name + + +``` + ## Data Table You can use the `` component to build more complex data tables. Combine it with [@tanstack/react-table](https://tanstack.com/table/v8) to create tables with sorting, filtering and pagination. diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx index 8e77b34ab23..9db1788627d 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx @@ -1,8 +1,8 @@ import type { OAuthClient } from '@supabase/supabase-js' import { Edit, MoreVertical, Plus, RotateCw, Search, Trash, X } from 'lucide-react' import Link from 'next/link' -import { parseAsBoolean, useQueryState } from 'nuqs' -import { useRef, useState } from 'react' +import { parseAsBoolean, parseAsStringLiteral, useQueryState } from 'nuqs' +import { useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' @@ -30,6 +30,7 @@ import { TableCell, TableHead, TableHeader, + TableHeadSort, TableRow, } from 'ui' import { Admonition } from 'ui-patterns/admonition' @@ -44,6 +45,21 @@ import { OAUTH_APP_REGISTRATION_TYPE_OPTIONS, } from './oauthApps.utils' +const OAUTH_APPS_SORT_VALUES = [ + 'name:asc', + 'name:desc', + 'client_type:asc', + 'client_type:desc', + 'registration_type:asc', + 'registration_type:desc', + 'created_at:asc', + 'created_at:desc', +] as const + +type OAuthAppsSort = (typeof OAUTH_APPS_SORT_VALUES)[number] +type OAuthAppsSortColumn = OAuthAppsSort extends `${infer Column}:${string}` ? Column : unknown +type OAuthAppsSortOrder = OAuthAppsSort extends `${string}:${infer Order}` ? Order : unknown + export const OAuthAppsList = () => { const { ref: projectRef } = useParams() const { data: authConfig, isLoading: isAuthConfigLoading } = useAuthConfigQuery({ projectRef }) @@ -108,12 +124,40 @@ export const OAuthAppsList = () => { const [filterString, setFilterString] = useState('') - const filteredOAuthApps = filterOAuthApps({ - apps: oAuthApps, - searchString: filterString, - registrationTypes: filteredRegistrationTypes, - clientTypes: filteredClientTypes, - }) + const [sort, setSort] = useQueryState( + 'sort', + parseAsStringLiteral(OAUTH_APPS_SORT_VALUES).withDefault('name:asc') + ) + + const filteredAndSortedOAuthApps = useMemo(() => { + const filtered = filterOAuthApps({ + apps: oAuthApps, + searchString: filterString, + registrationTypes: filteredRegistrationTypes, + clientTypes: filteredClientTypes, + }) + + const [sortCol, sortOrder] = sort.split(':') as [OAuthAppsSortColumn, OAuthAppsSortOrder] + const orderMultiplier = sortOrder === 'asc' ? 1 : -1 + + return filtered.sort((a, b) => { + if (sortCol === 'name') { + return a.client_name.localeCompare(b.client_name) * orderMultiplier + } + if (sortCol === 'client_type') { + return a.client_type.localeCompare(b.client_type) * orderMultiplier + } + if (sortCol === 'registration_type') { + return a.registration_type.localeCompare(b.registration_type) * orderMultiplier + } + if (sortCol === 'created_at') { + return ( + (new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) * orderMultiplier + ) + } + return 0 + }) + }, [oAuthApps, filterString, filteredRegistrationTypes, filteredClientTypes, sort]) const hasActiveFilters = filterString.length > 0 || @@ -126,6 +170,22 @@ export const OAuthAppsList = () => { setFilteredClientTypes([]) } + const handleSortChange = (column: OAuthAppsSortColumn) => { + const [currentCol, currentOrder] = sort.split(':') as [OAuthAppsSortColumn, OAuthAppsSortOrder] + if (currentCol === column) { + // Cycle through: asc -> desc -> no sort (default) + if (currentOrder === 'asc') { + setSort(`${column}:desc` as OAuthAppsSort) + } else { + // Reset to default sort (name:asc) + setSort('name:asc') + } + } else { + // New column, start with asc + setSort(`${column}:asc` as OAuthAppsSort) + } + } + if (isAuthConfigLoading || (isOAuthServerEnabled && isLoading)) { return } @@ -223,26 +283,54 @@ export const OAuthAppsList = () => {
- Name + + + Name + + Client ID - Client Type - Registration Type - Created + + + Client Type + + + + + Registration Type + + + + + Created + +
- {filteredOAuthApps.length === 0 && ( + {filteredAndSortedOAuthApps.length === 0 && (

No OAuth apps found

)} - {filteredOAuthApps.length > 0 && - filteredOAuthApps.map((app) => ( + {filteredAndSortedOAuthApps.length > 0 && + filteredAndSortedOAuthApps.map((app) => (
+
) } @@ -76,6 +81,72 @@ const TableHead = React.forwardRef< )) TableHead.displayName = 'TableHead' +interface TableHeadSortProps { + column: TColumn + currentSort: string + onSortChange: (column: TColumn) => void + children: React.ReactNode + className?: string +} + +function TableHeadSort({ + column, + currentSort, + onSortChange, + children, + className, +}: TableHeadSortProps) { + const [currentCol, currentOrder] = currentSort.split(':') + const isActive = currentCol === column + const isAsc = isActive && currentOrder === 'asc' + const isDesc = isActive && currentOrder === 'desc' + + const getSortIcon = () => { + const baseIconClass = 'w-3 h-3 absolute inset-0' + + return ( + <> + + + + + ) + } + + return ( + + ) +} +TableHeadSort.displayName = 'TableHeadSort' + const TableCell = React.forwardRef< HTMLTableCellElement, React.TdHTMLAttributes @@ -96,4 +167,14 @@ const TableCaption = React.forwardRef< )) TableCaption.displayName = 'TableCaption' -export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } +export { + Table, + TableBody, + TableCaption, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableHeadSort, + TableRow, +} From bcb39c4bc5ad3d0414506f235534605e90583f4b Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 2 Dec 2025 19:44:58 +0800 Subject: [PATCH 02/22] Add config cat custom attribute is_staff (#40865) * Add config cat custom attribute is_staff * Update based on feedback --- packages/common/configcat.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/common/configcat.ts b/packages/common/configcat.ts index c70d83a695c..0316badb37f 100644 --- a/packages/common/configcat.ts +++ b/packages/common/configcat.ts @@ -66,16 +66,20 @@ async function getClient() { export async function getFlags(userEmail: string = '', customAttributes?: Record) { const client = await getClient() + const _customAttributes = { + ...customAttributes, + is_staff: !!userEmail ? userEmail.includes('@supabase.').toString() : 'false', + } if (!client) { return [] } else if (userEmail) { return client.getAllValuesAsync( - new configcat.User(userEmail, undefined, undefined, customAttributes) + new configcat.User(userEmail, undefined, undefined, _customAttributes) ) } else { return client.getAllValuesAsync( - new configcat.User('anonymous', undefined, undefined, customAttributes) + new configcat.User('anonymous', undefined, undefined, _customAttributes) ) } } From 8367d7fd2db0f5c0357a1d07227f3e03411ac6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Tue, 2 Dec 2025 20:08:05 +0800 Subject: [PATCH 03/22] fix: compute upgrades for v3 (#40975) --- .../DiskManagement/DiskManagement.schema.ts | 3 ++- .../DiskManagement/DiskManagementForm.tsx | 2 ++ .../DiskManagementReviewAndSubmitDialog.tsx | 24 +++++++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts index 789a42f1dbc..a93062840e8 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts @@ -50,8 +50,9 @@ export const CreateDiskStorageSchema = ({ }) => { const isFlyProject = cloudProvider === 'FLY' const isAwsNimbusProject = cloudProvider === 'AWS_NIMBUS' + const isAwsK8sProject = cloudProvider === 'AWS_K8S' - const validateDiskConfiguration = !isFlyProject && !isAwsNimbusProject + const validateDiskConfiguration = !isFlyProject && !isAwsNimbusProject && !isAwsK8sProject const schema = baseSchema.superRefine((data, ctx) => { const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb } = data diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 4de34f0e855..49bc1820a70 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -256,6 +256,7 @@ export function DiskManagementForm() { // [Joshen] Skip disk configuration related stuff for AWS Nimbus try { if ( + !isAwsK8s && !isAwsNimbus && (payload.storageType !== form.formState.defaultValues?.storageType || payload.provisionedIOPS !== form.formState.defaultValues?.provisionedIOPS || @@ -274,6 +275,7 @@ export function DiskManagementForm() { } if ( + !isAwsK8s && !isAwsNimbus && (payload.growthPercent !== form.formState.defaultValues?.growthPercent || payload.minIncrementGb !== form.formState.defaultValues?.minIncrementGb || diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx index 5845431ffb2..161057c7995 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx @@ -152,6 +152,8 @@ export const DiskManagementReviewAndSubmitDialog = ({ const { data: org } = useSelectedOrganizationQuery() const isAwsNimbus = useIsAwsNimbusCloudProvider() + const isAwsK8sProject = project?.cloud_provider === 'AWS_K8S' + const { formState, getValues } = form const { can: canUpdateDiskConfiguration } = useAsyncCheckPermissions( @@ -214,22 +216,34 @@ export const DiskManagementReviewAndSubmitDialog = ({ const hasComputeChanges = form.formState.defaultValues?.computeSize !== form.getValues('computeSize') const hasTotalSizeChanges = - !isAwsNimbus && form.formState.defaultValues?.totalSize !== form.getValues('totalSize') + !isAwsK8sProject && + !isAwsNimbus && + form.formState.defaultValues?.totalSize !== form.getValues('totalSize') const hasStorageTypeChanges = - !isAwsNimbus && form.formState.defaultValues?.storageType !== form.getValues('storageType') + !isAwsK8sProject && + !isAwsNimbus && + form.formState.defaultValues?.storageType !== form.getValues('storageType') const hasThroughputChanges = - !isAwsNimbus && form.formState.defaultValues?.throughput !== form.getValues('throughput') + !isAwsK8sProject && + !isAwsNimbus && + form.formState.defaultValues?.throughput !== form.getValues('throughput') const hasIOPSChanges = + !isAwsK8sProject && !isAwsNimbus && form.formState.defaultValues?.provisionedIOPS !== form.getValues('provisionedIOPS') const hasGrowthPercentChanges = - !isAwsNimbus && form.formState.defaultValues?.growthPercent !== form.getValues('growthPercent') + !isAwsK8sProject && + !isAwsNimbus && + form.formState.defaultValues?.growthPercent !== form.getValues('growthPercent') const hasMinIncrementChanges = + !isAwsK8sProject && !isAwsNimbus && form.formState.defaultValues?.minIncrementGb !== form.getValues('minIncrementGb') const hasMaxSizeChanges = - !isAwsNimbus && form.formState.defaultValues?.maxSizeGb !== form.getValues('maxSizeGb') + !isAwsK8sProject && + !isAwsNimbus && + form.formState.defaultValues?.maxSizeGb !== form.getValues('maxSizeGb') const hasDiskConfigChanges = hasIOPSChanges || From 7860dad8413c35db3d9140ff294ad542f56af660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Tue, 2 Dec 2025 14:37:24 +0100 Subject: [PATCH 04/22] fix: organization usage page (#40956) * fix: organization usage page Fixes https://linear.app/supabase/issue/API-759/egress-graph-does-not-change-when-filtering-by-project * test: org usage --- .../Organization/Usage/Activity.tsx | 2 - .../interfaces/Organization/Usage/Egress.tsx | 6 + .../Organization/Usage/SizeAndCounts.tsx | 6 + .../interfaces/Organization/Usage/Usage.tsx | 8 +- .../Usage/UsageSection/UsageSection.tsx | 11 +- .../analytics/org-daily-stats-query.test.ts | 125 ++++++++++++++++++ .../data/analytics/org-daily-stats-query.ts | 2 +- .../studio/data/usage/org-usage-query.test.ts | 125 ++++++++++++++++++ 8 files changed, 279 insertions(+), 6 deletions(-) create mode 100644 apps/studio/data/analytics/org-daily-stats-query.test.ts create mode 100644 apps/studio/data/usage/org-usage-query.test.ts diff --git a/apps/studio/components/interfaces/Organization/Usage/Activity.tsx b/apps/studio/components/interfaces/Organization/Usage/Activity.tsx index 394694da3a6..4ee8fc90366 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Activity.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Activity.tsx @@ -19,8 +19,6 @@ const Activity = ({ orgSlug, projectRef, subscription, - startDate, - endDate, currentBillingCycleSelected, orgDailyStats, isLoadingOrgDailyStats, diff --git a/apps/studio/components/interfaces/Organization/Usage/Egress.tsx b/apps/studio/components/interfaces/Organization/Usage/Egress.tsx index d9988db578c..8a20726ccc1 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Egress.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Egress.tsx @@ -11,6 +11,8 @@ export interface EgressProps { currentBillingCycleSelected: boolean orgDailyStats: OrgDailyUsageResponse | undefined isLoadingOrgDailyStats: boolean + startDate: string | undefined + endDate: string | undefined } const Egress = ({ @@ -20,6 +22,8 @@ const Egress = ({ currentBillingCycleSelected, orgDailyStats, isLoadingOrgDailyStats, + startDate, + endDate, }: EgressProps) => { const chartMeta: { [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean } @@ -47,6 +51,8 @@ const Egress = ({ chartMeta={chartMeta} subscription={subscription} currentBillingCycleSelected={currentBillingCycleSelected} + startDate={startDate} + endDate={endDate} /> ) } diff --git a/apps/studio/components/interfaces/Organization/Usage/SizeAndCounts.tsx b/apps/studio/components/interfaces/Organization/Usage/SizeAndCounts.tsx index ebaa53b1837..0887860f75a 100644 --- a/apps/studio/components/interfaces/Organization/Usage/SizeAndCounts.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/SizeAndCounts.tsx @@ -15,6 +15,8 @@ export interface SizeAndCountsProps { currentBillingCycleSelected: boolean orgDailyStats: OrgDailyUsageResponse | undefined isLoadingOrgDailyStats: boolean + startDate: string | undefined + endDate: string | undefined } const SizeAndCounts = ({ @@ -24,6 +26,8 @@ const SizeAndCounts = ({ currentBillingCycleSelected, orgDailyStats, isLoadingOrgDailyStats, + startDate, + endDate, }: SizeAndCountsProps) => { const chartMeta: { [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean } @@ -46,6 +50,8 @@ const SizeAndCounts = ({ chartMeta={chartMeta} subscription={subscription} currentBillingCycleSelected={currentBillingCycleSelected} + startDate={startDate} + endDate={endDate} /> ) } diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index 9e5bf136e9a..ec443ebbdd7 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -32,7 +32,7 @@ import SizeAndCounts from './SizeAndCounts' import { TotalUsage } from './TotalUsage' export const Usage = () => { - const { slug, projectRef } = useParams() + const { slug } = useParams() const [dateRange, setDateRange] = useState() @@ -106,7 +106,7 @@ export const Usage = () => { isError: isErrorOrgDailyStats, } = useOrgDailyStatsQuery({ orgSlug: slug, - projectRef, + projectRef: selectedProjectRef ?? undefined, startDate, endDate, }) @@ -299,6 +299,8 @@ export const Usage = () => { currentBillingCycleSelected={currentBillingCycleSelected} orgDailyStats={orgDailyStats} isLoadingOrgDailyStats={isLoadingOrgDailyStats} + startDate={startDate} + endDate={endDate} /> { currentBillingCycleSelected={currentBillingCycleSelected} orgDailyStats={orgDailyStats} isLoadingOrgDailyStats={isLoadingOrgDailyStats} + startDate={startDate} + endDate={endDate} /> { const { data: usage, @@ -36,7 +40,12 @@ const UsageSection = ({ isLoading: isLoadingUsage, isError: isErrorUsage, isSuccess: isSuccessUsage, - } = useOrgUsageQuery({ orgSlug }) + } = useOrgUsageQuery({ + orgSlug, + projectRef, + start: !currentBillingCycleSelected && startDate ? new Date(startDate) : undefined, + end: !currentBillingCycleSelected && endDate ? new Date(endDate) : undefined, + }) const categoryMeta = USAGE_CATEGORIES(subscription).find( (category) => category.key === categoryKey diff --git a/apps/studio/data/analytics/org-daily-stats-query.test.ts b/apps/studio/data/analytics/org-daily-stats-query.test.ts new file mode 100644 index 00000000000..a3636d90758 --- /dev/null +++ b/apps/studio/data/analytics/org-daily-stats-query.test.ts @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getOrgDailyStats } from './org-daily-stats-query' + +vi.mock('data/fetchers', () => ({ + get: vi.fn(), + handleError: vi.fn((error) => { + throw error + }), +})) + +describe('org-daily-stats-query', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('getOrgDailyStats', () => { + it('throws error when orgSlug is not provided', async () => { + await expect( + getOrgDailyStats({ + orgSlug: undefined, + startDate: '2025-01-01', + endDate: '2025-01-31', + }) + ).rejects.toThrow('Org slug is required') + }) + + it('throws error when startDate is not provided', async () => { + await expect( + getOrgDailyStats({ + orgSlug: 'test-org', + startDate: undefined, + endDate: '2025-01-31', + }) + ).rejects.toThrow('Start date is required') + }) + + it('throws error when endDate is not provided', async () => { + await expect( + getOrgDailyStats({ + orgSlug: 'test-org', + startDate: '2025-01-01', + endDate: undefined, + }) + ).rejects.toThrow('Start date is required') + }) + + it('calls API with correct parameters including project_ref', async () => { + const { get } = await import('data/fetchers') + const mockGet = get as unknown as ReturnType + + const mockResponse = { usages: [] } + mockGet.mockResolvedValueOnce({ data: mockResponse, error: null }) + + const result = await getOrgDailyStats({ + orgSlug: 'test-org', + startDate: '2025-01-01', + endDate: '2025-01-31', + projectRef: 'test-project-ref', + }) + + expect(mockGet).toHaveBeenCalledWith( + '/platform/organizations/{slug}/usage/daily', + expect.objectContaining({ + params: { + path: { slug: 'test-org' }, + query: { + start: '2025-01-01', + end: '2025-01-31', + project_ref: 'test-project-ref', + }, + }, + }) + ) + expect(result).toEqual(mockResponse) + }) + + it('calls API without project_ref when not provided', async () => { + const { get } = await import('data/fetchers') + const mockGet = get as unknown as ReturnType + + const mockResponse = { usages: [] } + mockGet.mockResolvedValueOnce({ data: mockResponse, error: null }) + + await getOrgDailyStats({ + orgSlug: 'test-org', + startDate: '2025-01-01', + endDate: '2025-01-31', + }) + + expect(mockGet).toHaveBeenCalledWith( + '/platform/organizations/{slug}/usage/daily', + expect.objectContaining({ + params: { + path: { slug: 'test-org' }, + query: { + start: '2025-01-01', + end: '2025-01-31', + project_ref: undefined, + }, + }, + }) + ) + }) + + it('handles API errors correctly', async () => { + const { get, handleError } = await import('data/fetchers') + const mockGet = get as unknown as ReturnType + const mockHandleError = handleError as unknown as ReturnType + + const mockError = { message: 'API Error' } + mockGet.mockResolvedValueOnce({ data: null, error: mockError }) + mockHandleError.mockImplementation((error) => { + throw new Error(error.message) + }) + + await expect( + getOrgDailyStats({ + orgSlug: 'test-org', + startDate: '2025-01-01', + endDate: '2025-01-31', + }) + ).rejects.toThrow('API Error') + }) + }) +}) diff --git a/apps/studio/data/analytics/org-daily-stats-query.ts b/apps/studio/data/analytics/org-daily-stats-query.ts index 1a3c05ac6e2..d2742f603d6 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/usage/org-usage-query.test.ts b/apps/studio/data/usage/org-usage-query.test.ts new file mode 100644 index 00000000000..e694790dffc --- /dev/null +++ b/apps/studio/data/usage/org-usage-query.test.ts @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getOrgUsage } from './org-usage-query' + +vi.mock('data/fetchers', () => ({ + get: vi.fn(), + handleError: vi.fn((error) => { + throw error + }), +})) + +describe('org-usage-query', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('getOrgUsage', () => { + it('throws error when orgSlug is not provided', async () => { + await expect(getOrgUsage({ orgSlug: undefined })).rejects.toThrow('orgSlug is required') + }) + + it('calls API with correct parameters including project_ref', async () => { + const { get } = await import('data/fetchers') + const mockGet = get as unknown as ReturnType + + const mockResponse = { usages: [] } + mockGet.mockResolvedValueOnce({ data: mockResponse, error: null }) + + const startDate = new Date('2025-01-01T00:00:00.000Z') + const endDate = new Date('2025-01-31T23:59:59.000Z') + + const result = await getOrgUsage({ + orgSlug: 'test-org', + projectRef: 'test-project-ref', + start: startDate, + end: endDate, + }) + + expect(mockGet).toHaveBeenCalledWith( + '/platform/organizations/{slug}/usage', + expect.objectContaining({ + params: { + path: { slug: 'test-org' }, + query: { + project_ref: 'test-project-ref', + start: '2025-01-01T00:00:00.000Z', + end: '2025-01-31T23:59:59.000Z', + }, + }, + }) + ) + expect(result).toEqual(mockResponse) + }) + + it('calls API without project_ref when projectRef is null', async () => { + const { get } = await import('data/fetchers') + const mockGet = get as unknown as ReturnType + + const mockResponse = { usages: [] } + mockGet.mockResolvedValueOnce({ data: mockResponse, error: null }) + + await getOrgUsage({ + orgSlug: 'test-org', + projectRef: null, + }) + + expect(mockGet).toHaveBeenCalledWith( + '/platform/organizations/{slug}/usage', + expect.objectContaining({ + params: { + path: { slug: 'test-org' }, + query: { + project_ref: undefined, + start: undefined, + end: undefined, + }, + }, + }) + ) + }) + + it('calls API without date params when start and end are undefined', async () => { + const { get } = await import('data/fetchers') + const mockGet = get as unknown as ReturnType + + const mockResponse = { usages: [] } + mockGet.mockResolvedValueOnce({ data: mockResponse, error: null }) + + await getOrgUsage({ + orgSlug: 'test-org', + }) + + expect(mockGet).toHaveBeenCalledWith( + '/platform/organizations/{slug}/usage', + expect.objectContaining({ + params: { + path: { slug: 'test-org' }, + query: { + project_ref: undefined, + start: undefined, + end: undefined, + }, + }, + }) + ) + }) + + it('handles API errors correctly', async () => { + const { get, handleError } = await import('data/fetchers') + const mockGet = get as unknown as ReturnType + const mockHandleError = handleError as unknown as ReturnType + + const mockError = { message: 'API Error' } + mockGet.mockResolvedValueOnce({ data: null, error: mockError }) + mockHandleError.mockImplementation((error) => { + throw new Error(error.message) + }) + + await expect( + getOrgUsage({ + orgSlug: 'test-org', + }) + ).rejects.toThrow('API Error') + }) + }) +}) From 27e89ed64e59bc5219a2db0a01a0dc3d3cbc4698 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 2 Dec 2025 22:17:13 +0800 Subject: [PATCH 05/22] Analytics bucket support creating bucket tables in dashboard (#40858) * Midway * Hook up create/delete table, and update all iceberg RQs with mgmt API routes * Add insert data CTA, update create table logic for decimal and fixed types * Clean up * Nit * Fix test --- .../DestinationPanel/DestinationPanel.tsx | 12 +- .../DestinationPanelFields.tsx | 18 +- .../AnalyticsBucketDetails/BucketHeader.tsx | 16 +- .../CreateTableInstructions.constants.ts | 0 .../CreateTableInstructions.tsx} | 2 +- .../CreateTableInstructionsDialog.tsx | 71 ++++ .../CreateTable/CreateTableSheet.constants.ts | 26 ++ .../CreateTable/CreateTableSheet.schema.ts | 82 ++++ .../CreateTable/CreateTableSheet.tsx | 372 ++++++++++++++++++ .../CreateTableInstructionsDialog.tsx | 32 -- .../NamespaceWithTables/InsertDataDialog.tsx | 87 ++++ .../NamespaceWithTables/TableRowComponent.tsx | 14 +- .../NamespaceWithTables/index.tsx | 10 +- .../AnalyticsBucketDetails/index.tsx | 3 +- .../CreateAnalyticsBucketModal.tsx | 75 +--- .../Storage/AnalyticsBuckets/index.tsx | 262 ++++++------ .../interfaces/Storage/CreateBucketModal.tsx | 61 +-- .../interfaces/Storage/EmptyBucketState.tsx | 16 +- .../interfaces/Storage/FilesBuckets/index.tsx | 158 ++++---- .../interfaces/Storage/NewBucketButton.tsx | 34 ++ .../CreateVectorBucketDialog.tsx | 34 +- .../Storage/VectorBuckets/index.tsx | 228 +++++------ .../__tests__/CreateBucketModal.test.tsx | 4 +- .../iceberg-namespace-create-mutation.ts | 51 +-- .../iceberg-namespace-delete-mutation.ts | 41 +- ...iceberg-namespace-table-create-mutation.ts | 81 ++++ ...iceberg-namespace-table-delete-mutation.ts | 42 +- .../storage/iceberg-namespace-tables-query.ts | 64 +-- .../data/storage/iceberg-namespaces-query.ts | 53 +-- apps/studio/data/storage/keys.ts | 33 +- 30 files changed, 1211 insertions(+), 771 deletions(-) rename apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/{CreateTableInstructions => CreateTable}/CreateTableInstructions.constants.ts (100%) rename apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/{CreateTableInstructions/index.tsx => CreateTable/CreateTableInstructions.tsx} (100%) create mode 100644 apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx create mode 100644 apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.constants.ts create mode 100644 apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.schema.ts create mode 100644 apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.tsx delete mode 100644 apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/CreateTableInstructionsDialog.tsx create mode 100644 apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/InsertDataDialog.tsx create mode 100644 apps/studio/components/interfaces/Storage/NewBucketButton.tsx create mode 100644 apps/studio/data/storage/iceberg-namespace-table-create-mutation.ts diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx index 6c5d9346cca..cee10b749d2 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx @@ -7,7 +7,6 @@ import * as z from 'zod' import { useFlag, useParams } from 'common' import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility' -import { getCatalogURI } from 'components/interfaces/Storage/StorageSettings/StorageSettings.utils' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPrimaryKeysExists } from 'data/database/primary-keys-exists-query' @@ -221,19 +220,10 @@ export const DestinationPanel = ({ // Helper function to handle namespace creation if needed const resolveNamespace = async (data: z.infer) => { if (data.namespace === CREATE_NEW_NAMESPACE) { - if (!data.newNamespaceName) { - throw new Error('New namespace name is required') - } - - // Construct catalog URI for namespace creation - const protocol = projectSettings?.app_config?.protocol ?? 'https' - const endpoint = - projectSettings?.app_config?.storage_endpoint || projectSettings?.app_config?.endpoint - const catalogUri = getCatalogURI(project?.ref ?? '', protocol, endpoint) + if (!data.newNamespaceName) throw new Error('New namespace name is required') await createNamespace({ projectRef, - catalogUri, warehouse: data.warehouseName!, namespace: data.newNamespaceName, }) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx index 992311dd4ca..99aaf046f8b 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx @@ -1,10 +1,9 @@ import { Eye, EyeOff, Loader2 } from 'lucide-react' -import { useMemo, useState } from 'react' +import { useState } from 'react' import type { UseFormReturn } from 'react-hook-form' import { useParams } from 'common' import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility' -import { getCatalogURI } from 'components/interfaces/Storage/StorageSettings/StorageSettings.utils' import { InlineLink } from 'components/ui/InlineLink' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' @@ -154,30 +153,19 @@ export const AnalyticsBucketFields = ({ isError: isErrorBuckets, } = useAnalyticsBucketsQuery({ projectRef }) - // Construct catalog URI for iceberg namespaces query - const catalogUri = useMemo(() => { - if (!project?.ref || !projectSettings) return '' - const protocol = projectSettings.app_config?.protocol ?? 'https' - const endpoint = - projectSettings.app_config?.storage_endpoint || projectSettings.app_config?.endpoint - return getCatalogURI(project.ref, protocol, endpoint) - }, [project?.ref, projectSettings]) - const canSelectNamespace = !!warehouseName && !!serviceApiKey const { data: namespaces = [], isLoading: isLoadingNamespaces, isError: isErrorNamespaces, - refetch: refetchNamespaces, } = useIcebergNamespacesQuery( { projectRef, - catalogUri, - warehouse: warehouseName || '', + warehouse: warehouseName, }, { - enabled: type === 'Analytics Bucket' && !!catalogUri && !!warehouseName && !!serviceApiKey, + enabled: type === 'Analytics Bucket' && !!serviceApiKey, } ) diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx index d2802ff3212..033fd0c409d 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx @@ -8,7 +8,7 @@ import { } from 'components/layouts/Scaffold' import { HIDE_REPLICATION_USER_FLOW } from './AnalyticsBucketDetails.constants' import { ConnectTablesDialog } from './ConnectTablesDialog' -import { CreateTableInstructionsDialog } from './CreateTableInstructions/CreateTableInstructionsDialog' +import { CreateTableInstructionsDialog } from './CreateTable/CreateTableInstructionsDialog' interface BucketHeaderProps { showActions?: boolean @@ -35,14 +35,12 @@ export const BucketHeader = ({ {showActions && (
- {namespaces.length > 0 && ( - <> - {HIDE_REPLICATION_USER_FLOW ? ( - - ) : ( - - )} - + {HIDE_REPLICATION_USER_FLOW ? ( + + ) : ( + namespaces.length > 0 && ( + + ) )}
)} diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/CreateTableInstructions.constants.ts b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.constants.ts similarity index 100% rename from apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/CreateTableInstructions.constants.ts rename to apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.constants.ts diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx similarity index 100% rename from apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/index.tsx rename to apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx index d09baff9804..67d0494b2cc 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/index.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx @@ -1,3 +1,4 @@ +import { Eye, EyeOff } from 'lucide-react' import { useMemo, useState } from 'react' import { useParams } from 'common' @@ -13,7 +14,6 @@ import { } from 'data/vault/vault-secret-decrypted-value-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' -import { Eye, EyeOff } from 'lucide-react' import { Accordion_Shadcn_, AccordionContent_Shadcn_, diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx new file mode 100644 index 00000000000..9f5d0d1b61f --- /dev/null +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx @@ -0,0 +1,71 @@ +import { useFlag } from 'common' +import { ChevronDown, Plus } from 'lucide-react' +import { useState } from 'react' +import { + Button, + cn, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' +import { CreateTableInstructions } from './CreateTableInstructions' +import { CreateTableSheet } from './CreateTableSheet' + +export const CreateTableInstructionsDialog = () => { + const enableCreationOfTablesFromDashboard = useFlag('analyticsBucketsTableCreation') + + const [showModal, setShowModal] = useState(false) + const [showSheet, setShowSheet] = useState(false) + + return ( + <> +
+ + {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 00000000000..63389dbc20d --- /dev/null +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.constants.ts @@ -0,0 +1,26 @@ +export const NEW_NAMESPACE_MARKER = 'new-namespace' + +export const COLUMN_TYPES = [ + 'boolean', + 'int', + 'long', + 'float', + 'double', + 'string', + 'timestamp', + 'date', + 'time', + 'timestamptz', + 'uuid', + 'binary', + 'decimal', + 'fixed', +] + +export const COLUMN_TYPE_FIELDS = { + decimal: [ + { name: 'precision', type: 'number' }, + { name: 'scale', type: 'number' }, + ], + fixed: [{ name: 'length', type: 'number' }], +} diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.schema.ts b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.schema.ts new file mode 100644 index 00000000000..d9aac58c843 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.schema.ts @@ -0,0 +1,82 @@ +import { z } from 'zod' +import { NEW_NAMESPACE_MARKER } from './CreateTableSheet.constants' + +const getValidRegex = (type: 'namespace' | 'table') => + type === 'namespace' + ? /^(?!aws)[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/ + : /^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/ +const getErrorRegex = (type: 'namespace' | 'table') => + type === 'namespace' + ? /^(?:(?aws.*)|(?[^a-z0-9].*)|(?.*[^a-z0-9_].*)|(?.*[^a-z0-9]))$/ + : /^(?:(?[^a-z0-9].*)|(?.*[^a-z0-9_].*)|(?.*[^a-z0-9]))$/ + +const validateName = ({ name, type }: { name: string; type: 'namespace' | 'table' }) => { + const validRe = getValidRegex(type) + if (validRe.test(name)) return undefined + + const errorRe = getErrorRegex(type) + const match = name.match(errorRe)?.groups || {} + if (match.starts_with_reserved) return "Namespace must not start with 'aws'" + if (match.invalid_start) return 'Name must begin with a lowercase letter or number' + if (match.invalid_end) return 'Name must end with a lowercase letter or number' + if (match.invalid_char) return 'Name may only contain lowercase letters, numbers, and underscores' + + return 'Invalid name' +} + +export const createFormSchema = () => + z + .object({ + namespace: z.string().min(1, 'Please select a namespace'), + newNamespace: z.string().max(255, 'Name must be within 255 characters').optional(), + name: z + .string() + .min(1, 'Provide a name for your table') + .max(255, 'Name must be within 255 characters'), + columns: z + .object({ + name: z.string().min(1, 'Provide a name for your column'), + type: z.string().min(1, 'Select a type for your column'), + // For decimal type + precision: z.number().optional(), + scale: z.number().int().optional(), + // For fixed type + length: z.number().int().optional(), + }) + .array() + .default([]), + }) + .superRefine((data, ctx) => { + if (data.namespace === NEW_NAMESPACE_MARKER) { + if (data.newNamespace) { + const newNamespaceError = validateName({ + name: data.newNamespace, + type: 'namespace', + }) + if (newNamespaceError) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: newNamespaceError, + path: ['newNamespace'], + }) + } + } else { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Provide a name for your new namespace', + path: ['newNamespace'], + }) + } + } + + if (data.name) { + const newTableError = validateName({ name: data.name, type: 'table' }) + if (newTableError) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: newTableError, + path: ['name'], + }) + } + } + }) diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.tsx new file mode 100644 index 00000000000..fea88e742e1 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.tsx @@ -0,0 +1,372 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { Plus, X } from 'lucide-react' +import { Fragment, useState } from 'react' +import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { z } from 'zod' + +import { useParams } from 'common' +import { useIcebergNamespaceCreateMutation } from 'data/storage/iceberg-namespace-create-mutation' +import { + NamespaceTableFields, + useIcebergNamespaceTableCreateMutation, +} from 'data/storage/iceberg-namespace-table-create-mutation' +import { useIcebergNamespaceTablesQuery } from 'data/storage/iceberg-namespace-tables-query' +import { useIcebergNamespacesQuery } from 'data/storage/iceberg-namespaces-query' +import { + Button, + DialogSectionSeparator, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectSeparator_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetSection, + SheetTitle, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { COLUMN_TYPE_FIELDS, COLUMN_TYPES } from './CreateTableSheet.constants' +import { createFormSchema } from './CreateTableSheet.schema' + +const formId = 'create-namespace-table' +const NEW_NAMESPACE_MARKER = 'new-namespace' + +interface CreateTableSheetProps { + open: boolean + onOpenChange: (value: boolean) => void +} + +export const CreateTableSheet = ({ open, onOpenChange }: CreateTableSheetProps) => { + const { ref: projectRef, bucketId } = useParams() + const [isCreating, setIsCreating] = useState(false) + + const FormSchema = createFormSchema() + const defaultValues = { + namespace: '', + newNamespace: undefined, + name: '', + columns: [{ name: '', type: 'string' as any }], + } + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues, + mode: 'onChange', + }) + const { namespace } = form.watch() + const { + fields: columns, + append: appendColumn, + remove: removeColumn, + } = useFieldArray({ control: form.control, name: 'columns' }) + + const { data: namespaces = [] } = useIcebergNamespacesQuery({ projectRef, warehouse: bucketId }) + const { data: tables = [] } = useIcebergNamespaceTablesQuery( + { + projectRef, + warehouse: bucketId, + namespace, + }, + { enabled: namespace !== NEW_NAMESPACE_MARKER } + ) + + const { mutateAsync: createNamespace } = useIcebergNamespaceCreateMutation() + const { mutateAsync: createTable } = useIcebergNamespaceTableCreateMutation() + + const onSubmit: SubmitHandler> = async (values) => { + if (!bucketId) return console.error('Bucket ID is missing') + if (namespaces.includes(values.newNamespace ?? '')) { + return form.setError('newNamespace', { message: 'Namespace name already exists' }) + } + if (tables.includes(values.name ?? '')) { + return form.setError('name', { message: 'Table name already exists' }) + } + + const isCreatingNewNamespace = + values.namespace === NEW_NAMESPACE_MARKER && !!values.newNamespace + + try { + setIsCreating(true) + if (isCreatingNewNamespace) { + await createNamespace({ + projectRef, + warehouse: bucketId, + namespace: values.newNamespace as string, + }) + } + + const fields = values.columns.map((column, idx) => { + return { + id: idx + 1, + name: column.name, + type: + column.type === 'decimal' + ? `decimal(${column.precision}, ${column.scale})` + : column.type === 'fixed' + ? `fixed[${column.length}]` + : column.type, + required: false, + } + }) as NamespaceTableFields + + await createTable({ + projectRef, + warehouse: bucketId, + namespace: isCreatingNewNamespace ? (values.newNamespace as string) : values.namespace, + name: values.name, + fields, + }) + + toast.success(`Successfully created table in ${values.newNamespace ?? values.namespace}!`) + onOpenChange(false) + form.reset(defaultValues) + } catch (error) { + } finally { + setIsCreating(false) + } + } + + return ( + + +
+ + + Create a new table + + + +
+ ( + + + { + field.onChange(value) + form.resetField('newNamespace') + }} + > + + + + + {namespaces.map((x) => ( + + {x} + + ))} + {namespaces.length > 0 && } + +
+ +

Create a new namespace

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

Columns

+ +
+ {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 19c08f08015..00000000000 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/CreateTableInstructionsDialog.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Plus } from 'lucide-react' -import { - Button, - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from 'ui' -import { CreateTableInstructions } from '.' - -export const CreateTableInstructionsDialog = () => { - return ( - - - - - - - Adding tables to your Analytics Bucket - - Tables can be created or added to your bucket via Pyiceberg - - - - - - ) -} diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/InsertDataDialog.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/InsertDataDialog.tsx new file mode 100644 index 00000000000..451d30b25d9 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/InsertDataDialog.tsx @@ -0,0 +1,87 @@ +import { useParams } from 'common' +import { DocsButton } from 'components/ui/DocsButton' +import { FDWTable } from 'data/fdw/fdws-query' +import { SqlEditor } from 'icons' +import { DOCS_URL } from 'lib/constants' +import Link from 'next/link' +import { + Button, + cn, + CodeBlock, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, +} from 'ui' + +interface InsertDataDialogProps { + table: string + fdwTable: FDWTable +} + +export const InsertDataDialog = ({ table, fdwTable }: InsertDataDialogProps) => { + const { ref } = useParams() + + const sql = /* SQL */ ` +insert into ${fdwTable.schema}.${fdwTable.name} ( + -- specify columns +) +values ( + -- specify values for each column +); +`.trim() + + return ( + + + + + + + + Insert data 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 cb66723ac10..0bfc7b9fc9d 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx @@ -12,12 +12,12 @@ import { import { getDecryptedParameters } from 'components/interfaces/Storage/ImportForeignSchemaDialog.utils' import { DotPing } from 'components/ui/DotPing' import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' +import { useFDWDropForeignTableMutation } from 'data/fdw/fdw-drop-foreign-table-mutation' +import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation' import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query' import { useUpdatePublicationMutation } from 'data/replication/publication-update-mutation' import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation' import { useReplicationTablesQuery } from 'data/replication/tables-query' -import { useFDWDropForeignTableMutation } from 'data/fdw/fdw-drop-foreign-table-mutation' -import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation' import { useIcebergNamespaceTableDeleteMutation } from 'data/storage/iceberg-namespace-table-delete-mutation' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { SqlEditor, TableEditor } from 'icons' @@ -43,6 +43,7 @@ import { } from '../AnalyticsBucketDetails.utils' import { useAnalyticsBucketAssociatedEntities } from '../useAnalyticsBucketAssociatedEntities' import { useAnalyticsBucketWrapperInstance } from '../useAnalyticsBucketWrapperInstance' +import { InsertDataDialog } from './InsertDataDialog' import { inferPostgresTableFromNamespaceTable } from './NamespaceWithTables.utils' interface TableRowComponentProps { @@ -78,7 +79,7 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen const { mutateAsync: updateFDW } = useFDWUpdateMutation() const { mutateAsync: dropForeignTable } = useFDWDropForeignTableMutation() - const { mutateAsync: deleteNamespaceTable, isLoading: isDeletingNamespaceTable } = + const { mutateAsync: deleteNamespaceTable, isPending: isDeletingNamespaceTable } = useIcebergNamespaceTableDeleteMutation({ onError: () => {} }) const { mutateAsync: updatePublication } = useUpdatePublicationMutation() const { mutateAsync: startPipeline } = useStartPipelineMutation() @@ -227,7 +228,6 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? []) await deleteNamespaceTable({ projectRef, - catalogUri: wrapperValues.catalog_uri, warehouse: wrapperValues.warehouse, namespace: namespace, table: table.name, @@ -257,7 +257,6 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? []) await deleteNamespaceTable({ projectRef, - catalogUri: wrapperValues.catalog_uri, warehouse: wrapperValues.warehouse, namespace: namespace, table: table.name, @@ -419,7 +418,7 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen
-
+
- + +
+ + + {analyticsBuckets.length > 0 && ( + + Icon + + )} + Name + Created at + + Actions + + + + + {analyticsBuckets.length === 0 && filterString.length > 0 && ( + + +

No results found

+

+ Your search for "{filterString}" did not return any results +

+
+
+ )} + {analyticsBuckets.map((bucket) => ( + handleBucketNavigation(bucket.name, event)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + handleBucketNavigation(bucket.name, event) + } + }} + tabIndex={0} + > + + + + +

+ {bucket.name} +

+
+ + +

+ +

+
+ + +
+ +
+
+
+ ))} +
+
+ )} -
- setFilterString(e.target.value)} - icon={} - /> - -
- - {isLoadingBuckets ? ( - - ) : ( - - - - - {analyticsBuckets.length > 0 && ( - - Icon - - )} - Name - Created at - - Actions - - - - - {analyticsBuckets.length === 0 && filterString.length > 0 && ( - - -

No results found

-

- Your search for "{filterString}" did not return any results -

-
-
- )} - {analyticsBuckets.map((bucket) => ( - handleBucketNavigation(bucket.name, event)} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault() - handleBucketNavigation(bucket.name, event) - } - }} - tabIndex={0} - > - - - - -

- {bucket.name} -

-
- - -

- -

-
- - -
- -
-
-
- ))} -
-
-
- )} - - )} - - )} - - - + )} + + )} + + + + + ) } diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx index 0650510f289..aea27e3fc7a 100644 --- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx @@ -1,20 +1,15 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Plus } from 'lucide-react' import { useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import z from 'zod' -import { parseAsBoolean, useQueryState } from 'nuqs' import { useParams } from 'common' import { StorageSizeUnits } from 'components/interfaces/Storage/StorageSettings/StorageSettings.constants' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { InlineLink } from 'components/ui/InlineLink' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useBucketCreateMutation } from 'data/storage/bucket-create-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' import { @@ -26,7 +21,6 @@ import { DialogSection, DialogSectionSeparator, DialogTitle, - DialogTrigger, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, @@ -85,30 +79,17 @@ const formId = 'create-storage-bucket-form' export type CreateBucketForm = z.infer interface CreateBucketModalProps { - buttonSize?: 'tiny' | 'small' - buttonType?: 'default' | 'primary' - buttonClassName?: string - label?: string + open: boolean + onOpenChange: (value: boolean) => void } -export const CreateBucketModal = ({ - buttonSize = 'tiny', - buttonType = 'default', - buttonClassName, - label = 'New bucket', -}: CreateBucketModalProps) => { +export const CreateBucketModal = ({ open, onOpenChange }: CreateBucketModalProps) => { const { ref } = useParams() const { data: org } = useSelectedOrganizationQuery() - const [visible, setVisible] = useQueryState( - 'new', - parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) - ) const [selectedUnit, setSelectedUnit] = useState(StorageSizeUnits.MB) const [hasAllowedMimeTypes, setHasAllowedMimeTypes] = useState(false) - const { can: canCreateBuckets } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*') - const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM }) const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0) const formattedGlobalUploadLimit = `${value} ${unit}` @@ -172,7 +153,7 @@ export const CreateBucketModal = ({ toast.success(`Successfully created bucket ${values.name}`) form.reset() setSelectedUnit(StorageSizeUnits.MB) - setVisible(false) + onOpenChange(false) } catch (error: any) { // Handle specific error cases for inline display const errorMessage = error.message?.toLowerCase() || '' @@ -196,42 +177,18 @@ export const CreateBucketModal = ({ const handleClose = () => { form.reset() setSelectedUnit(StorageSizeUnits.MB) - setVisible(false) + onOpenChange(false) } return ( { if (!open) { handleClose() } }} > - - } - disabled={!canCreateBuckets} - style={{ justifyContent: 'start' }} - onClick={() => setVisible(true)} - tabIndex={!canCreateBuckets ? -1 : 0} - tooltip={{ - content: { - side: 'bottom', - text: !canCreateBuckets - ? 'You need additional permissions to create buckets' - : undefined, - }, - }} - > - {label} - - - Create file bucket @@ -378,7 +335,7 @@ export const CreateBucketModal = ({ setVisible(false)} + onClick={() => onOpenChange(false)} > Storage Settings {' '} @@ -392,7 +349,7 @@ export const CreateBucketModal = ({ setVisible(false)} + onClick={() => onOpenChange(false)} > global file size limit {' '} @@ -449,7 +406,7 @@ export const CreateBucketModal = ({ - - - - - 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 00000000000..f356f155c7b --- /dev/null +++ b/apps/studio/components/interfaces/Storage/NewBucketButton.tsx @@ -0,0 +1,34 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { Plus } from 'lucide-react' +import { MouseEventHandler } from 'react' + +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' + +export const CreateBucketButton = ({ + onClick, +}: { + onClick?: MouseEventHandler +}) => { + const { can: canCreateBuckets } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + + return ( + } + disabled={!canCreateBuckets} + onClick={onClick} + tooltip={{ + content: { + side: 'bottom', + text: !canCreateBuckets ? 'You need additional permissions to create buckets' : undefined, + }, + }} + > + New bucket + + ) +} diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx index 2ab963bcf53..b5d9bf56a09 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx @@ -1,13 +1,10 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Plus } from 'lucide-react' -import { MouseEventHandler, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import z from 'zod' import { useParams } from 'common' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { InlineLink } from 'components/ui/InlineLink' import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation' import { useSchemaCreateMutation } from 'data/database/schema-create-mutation' @@ -15,7 +12,6 @@ import { useS3VectorsWrapperCreateMutation } from 'data/storage/s3-vectors-wrapp import { useVectorBucketCreateMutation } from 'data/storage/vector-bucket-create-mutation' import { useVectorBucketsQuery } from 'data/storage/vector-buckets-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' @@ -87,34 +83,6 @@ const formId = 'create-storage-bucket-form' export type CreateBucketForm = z.infer -export const CreateVectorBucketButton = ({ - onClick, -}: { - onClick?: MouseEventHandler -}) => { - const { can: canCreateBuckets } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*') - - return ( - } - disabled={!canCreateBuckets} - onClick={onClick} - tooltip={{ - content: { - side: 'bottom', - text: !canCreateBuckets ? 'You need additional permissions to create buckets' : undefined, - }, - }} - > - New bucket - - ) -} - export const CreateVectorBucketDialog = ({ visible, setVisible, diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx index 14e1fee38eb..b8585792eff 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx @@ -16,7 +16,8 @@ import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent, PageSectionTitle } from 'ui-patterns/PageSection' import { TimestampInfo } from 'ui-patterns/TimestampInfo' import { EmptyBucketState } from '../EmptyBucketState' -import { CreateVectorBucketButton, CreateVectorBucketDialog } from './CreateVectorBucketDialog' +import { CreateBucketButton } from '../NewBucketButton' +import { CreateVectorBucketDialog } from './CreateVectorBucketDialog' /** * [Joshen] Low-priority refactor: We should use a virtualized table here as per how we do it @@ -59,124 +60,131 @@ export const VectorsBuckets = () => { } return ( - - - - + <> + + + + - {isLoadingBuckets && } + {isLoadingBuckets && } - {isErrorBuckets && ( - - )} + {isErrorBuckets && ( + + )} - {isSuccessBuckets && ( - <> - {bucketsList.length === 0 ? ( - setVisible(true)} /> - ) : ( -
-
- 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 1527dac75c2..3f1ca183d7f 100644 --- a/apps/studio/components/interfaces/Storage/__tests__/CreateBucketModal.test.tsx +++ b/apps/studio/components/interfaces/Storage/__tests__/CreateBucketModal.test.tsx @@ -3,8 +3,8 @@ import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext' -import { addAPIMock } from 'tests/lib/msw' import { customRender } from 'tests/lib/custom-render' +import { addAPIMock } from 'tests/lib/msw' import { routerMock } from 'tests/lib/route-mock' import { CreateBucketModal } from '../CreateBucketModal' @@ -42,7 +42,7 @@ describe(`CreateBucketModal`, () => { it(`renders a dialog with a form`, async () => { customRender( - + {}} /> , { nuqs: { diff --git a/apps/studio/data/storage/iceberg-namespace-create-mutation.ts b/apps/studio/data/storage/iceberg-namespace-create-mutation.ts index fc638a387ff..86a1bd89784 100644 --- a/apps/studio/data/storage/iceberg-namespace-create-mutation.ts +++ b/apps/studio/data/storage/iceberg-namespace-create-mutation.ts @@ -1,56 +1,30 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils' -import { constructHeaders, fetchHandler, handleError } from 'data/fetchers' +import { handleError, post } from 'data/fetchers' import type { ResponseError, UseCustomMutationOptions } from 'types' import { storageKeys } from './keys' type CreateIcebergNamespaceVariables = { projectRef?: string - catalogUri: string warehouse: string namespace: string } -const errorPrefix = 'Failed to create Iceberg namespace' - async function createIcebergNamespace({ projectRef, - catalogUri, warehouse, namespace, }: CreateIcebergNamespaceVariables) { - try { - if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`) + if (!projectRef) throw new Error('projectRef is required') - const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef) - const tempApiKey = tempApiKeyObj.apiKey + const { error } = await post('/platform/storage/{ref}/analytics-buckets/{id}/namespaces', { + params: { path: { ref: projectRef, id: warehouse } }, + body: { namespace }, + }) - let headers = new Headers() - headers = await constructHeaders({ - 'Content-Type': 'application/json', - apikey: tempApiKey, - }) - headers.delete('Authorization') - - const url = `${catalogUri}/v1/${warehouse}/namespaces`.replaceAll(/(?> @@ -72,18 +46,9 @@ export const useIcebergNamespaceCreateMutation = ({ return useMutation({ mutationFn: (vars) => createIcebergNamespace({ ...vars }), async onSuccess(data, variables, context) { - await queryClient.invalidateQueries({ - queryKey: storageKeys.icebergNamespace({ - projectRef: variables.projectRef, - catalog: variables.catalogUri, - warehouse: variables.warehouse, - namespace: variables.namespace, - }), - }) await queryClient.invalidateQueries({ queryKey: storageKeys.icebergNamespaces({ projectRef: variables.projectRef, - catalog: variables.catalogUri, warehouse: variables.warehouse, }), }) diff --git a/apps/studio/data/storage/iceberg-namespace-delete-mutation.ts b/apps/studio/data/storage/iceberg-namespace-delete-mutation.ts index 86bbc58faf2..43d3e55b4ff 100644 --- a/apps/studio/data/storage/iceberg-namespace-delete-mutation.ts +++ b/apps/studio/data/storage/iceberg-namespace-delete-mutation.ts @@ -1,52 +1,32 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils' -import { constructHeaders, fetchHandler, handleError } from 'data/fetchers' +import { del, handleError } from 'data/fetchers' import type { ResponseError, UseCustomMutationOptions } from 'types' import { storageKeys } from './keys' type DeleteIcebergNamespaceVariables = { projectRef?: string - catalogUri: string warehouse: string namespace: string } -const errorPrefix = 'Failed to delete Iceberg namespace' - async function deleteIcebergNamespace({ projectRef, - catalogUri, warehouse, namespace, }: DeleteIcebergNamespaceVariables) { - try { - if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`) + if (!projectRef) throw new Error('projectRef is required') - const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef) - const tempApiKey = tempApiKeyObj.apiKey + const { error } = await del( + '/platform/storage/{ref}/analytics-buckets/{id}/namespaces/{namespace}', + { + params: { path: { ref: projectRef, id: warehouse, namespace } }, + } + ) - let headers = new Headers() - headers = await constructHeaders({ - 'Content-Type': 'application/json', - apikey: tempApiKey, - }) - headers.delete('Authorization') - - const url = `${catalogUri}/v1/${warehouse}/namespaces/${namespace}`.replaceAll( - /(?> @@ -71,7 +51,6 @@ export const useIcebergNamespaceDeleteMutation = ({ await queryClient.invalidateQueries({ queryKey: storageKeys.icebergNamespaces({ projectRef: variables.projectRef, - catalog: variables.catalogUri, warehouse: variables.warehouse, }), }) diff --git a/apps/studio/data/storage/iceberg-namespace-table-create-mutation.ts b/apps/studio/data/storage/iceberg-namespace-table-create-mutation.ts new file mode 100644 index 00000000000..4b5ae9df424 --- /dev/null +++ b/apps/studio/data/storage/iceberg-namespace-table-create-mutation.ts @@ -0,0 +1,81 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { components } from 'api-types' +import { handleError, post } from 'data/fetchers' +import type { ResponseError, UseCustomMutationOptions } from 'types' +import { storageKeys } from './keys' + +export type NamespaceTableFields = components['schemas']['CreateNamespaceTableBody']['fields'] + +type CreateIcebergNamespaceTableVariables = { + projectRef?: string + warehouse: string + namespace: string + name: string + fields: NamespaceTableFields +} + +async function createIcebergNamespaceTable({ + projectRef, + warehouse, + namespace, + name, + fields, +}: CreateIcebergNamespaceTableVariables) { + if (!projectRef) throw new Error('projectRef is required') + + const { data, error } = await post( + '/platform/storage/{ref}/analytics-buckets/{id}/namespaces/{namespace}/tables', + { + params: { path: { ref: projectRef, id: warehouse, namespace } }, + body: { name, fields }, + } + ) + + if (error) handleError(error) + return data +} + +type IcebergNamespaceTableCreateData = Awaited> + +export const useIcebergNamespaceTableCreateMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseCustomMutationOptions< + IcebergNamespaceTableCreateData, + ResponseError, + CreateIcebergNamespaceTableVariables + >, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation< + IcebergNamespaceTableCreateData, + ResponseError, + CreateIcebergNamespaceTableVariables + >({ + mutationFn: (vars) => createIcebergNamespaceTable({ ...vars }), + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries({ + queryKey: storageKeys.icebergNamespace({ + projectRef: variables.projectRef, + warehouse: variables.warehouse, + namespace: variables.namespace, + }), + }) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to create Iceberg namespace table: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/storage/iceberg-namespace-table-delete-mutation.ts b/apps/studio/data/storage/iceberg-namespace-table-delete-mutation.ts index 23ebca37862..cd16fb305d5 100644 --- a/apps/studio/data/storage/iceberg-namespace-table-delete-mutation.ts +++ b/apps/studio/data/storage/iceberg-namespace-table-delete-mutation.ts @@ -1,52 +1,37 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils' -import { constructHeaders, fetchHandler, handleError } from 'data/fetchers' +import { del, handleError } from 'data/fetchers' import type { ResponseError, UseCustomMutationOptions } from 'types' import { storageKeys } from './keys' type DeleteIcebergNamespaceTableVariables = { - catalogUri: string warehouse: string namespace: string table: string projectRef?: string } -const errorPrefix = 'Failed to delete Iceberg namespace table' - async function deleteIcebergNamespaceTable({ projectRef, - catalogUri, warehouse, namespace, table, }: DeleteIcebergNamespaceTableVariables) { - try { - if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`) + if (!projectRef) throw new Error('projectRef is required') - const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef) - const tempApiKey = tempApiKeyObj.apiKey + const { error } = await del( + '/platform/storage/{ref}/analytics-buckets/{id}/namespaces/{namespace}/tables/{table}', + { + params: { + path: { ref: projectRef, id: warehouse, namespace, table }, + query: { purge: true }, + }, + } + ) - let headers = new Headers() - headers = await constructHeaders({ - 'Content-Type': 'application/json', - apikey: tempApiKey, - }) - headers.delete('Authorization') - - const url = - `${catalogUri}/v1/${warehouse}/namespaces/${namespace}/tables/${table}?purgeRequested=true`.replaceAll( - /(?> @@ -75,7 +60,6 @@ export const useIcebergNamespaceTableDeleteMutation = ({ await queryClient.invalidateQueries({ queryKey: storageKeys.icebergNamespace({ projectRef: variables.projectRef, - catalog: variables.catalogUri, warehouse: variables.warehouse, namespace: variables.namespace, }), diff --git a/apps/studio/data/storage/iceberg-namespace-tables-query.ts b/apps/studio/data/storage/iceberg-namespace-tables-query.ts index e5edcf0bdca..fd699bb6674 100644 --- a/apps/studio/data/storage/iceberg-namespace-tables-query.ts +++ b/apps/studio/data/storage/iceberg-namespace-tables-query.ts @@ -1,55 +1,33 @@ import { useQuery } from '@tanstack/react-query' -import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils' -import { constructHeaders, fetchHandler, handleError } from 'data/fetchers' +import { get, handleError } from 'data/fetchers' import type { ResponseError, UseCustomQueryOptions } from 'types' import { storageKeys } from './keys' type GetNamespaceTablesVariables = { - catalogUri: string - warehouse: string - namespace: string + warehouse?: string + namespace?: string projectRef?: string } -const errorPrefix = 'Failed to retrieve Iceberg namespace tables' +async function getNamespaceTables( + { projectRef, warehouse, namespace }: GetNamespaceTablesVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + if (!namespace) throw new Error('namespace is required') + if (!warehouse) throw new Error('warehouse is required') -async function getNamespaceTables({ - projectRef, - catalogUri, - warehouse, - namespace, -}: GetNamespaceTablesVariables) { - try { - if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`) - - const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef) - const tempApiKey = tempApiKeyObj.apiKey - - let headers = new Headers() - headers = await constructHeaders({ - 'Content-Type': 'application/json', - apikey: tempApiKey, - }) - headers.delete('Authorization') - - const url = `${catalogUri}/v1/${warehouse}/namespaces/${namespace}/tables`.replaceAll( - /(? i.name) - } catch (error) { - handleError(error) - } + if (error) handleError(error) + return data.data.map((x) => x.name) } type IcebergNamespaceTablesData = Awaited> @@ -63,22 +41,20 @@ export const useIcebergNamespaceTablesQuery = = {} ) => { - const { projectRef, catalogUri, warehouse, namespace } = params + const { projectRef, warehouse, namespace } = params return useQuery({ queryKey: storageKeys.icebergNamespaceTables({ projectRef, warehouse, namespace, - catalog: catalogUri, }), queryFn: () => getNamespaceTables({ ...params }), enabled: enabled && typeof projectRef !== 'undefined' && typeof warehouse !== 'undefined' && - typeof namespace !== 'undefined' && - typeof catalogUri !== 'undefined', + typeof namespace !== 'undefined', ...options, }) } diff --git a/apps/studio/data/storage/iceberg-namespaces-query.ts b/apps/studio/data/storage/iceberg-namespaces-query.ts index be5e4057388..651efe03054 100644 --- a/apps/studio/data/storage/iceberg-namespaces-query.ts +++ b/apps/studio/data/storage/iceberg-namespaces-query.ts @@ -1,46 +1,28 @@ import { useQuery } from '@tanstack/react-query' -import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils' -import { constructHeaders, fetchHandler, handleError } from 'data/fetchers' +import { get, handleError } from 'data/fetchers' import type { ResponseError, UseCustomQueryOptions } from 'types' import { storageKeys } from './keys' type GetNamespacesVariables = { - catalogUri: string - warehouse: string + warehouse?: string projectRef?: string } -const errorPrefix = 'Failed to retrieve Iceberg namespaces' +async function getNamespaces( + { projectRef, warehouse }: GetNamespacesVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + if (!warehouse) throw new Error('warehouse is required') -async function getNamespaces({ projectRef, catalogUri, warehouse }: GetNamespacesVariables) { - try { - if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`) + const { data, error } = await get('/platform/storage/{ref}/analytics-buckets/{id}/namespaces', { + params: { path: { ref: projectRef, id: warehouse } }, + signal, + }) - const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef) - const tempApiKey = tempApiKeyObj.apiKey - - let headers = new Headers() - headers = await constructHeaders({ - 'Content-Type': 'application/json', - apikey: tempApiKey, - }) - headers.delete('Authorization') - - const url = `${catalogUri}/v1/${warehouse}/namespaces`.replaceAll(/(? x.namespace).flat() } type IcebergNamespacesData = Awaited> @@ -54,20 +36,17 @@ export const useIcebergNamespacesQuery = ( ...options }: UseCustomQueryOptions = {} ) => { - const { projectRef, catalogUri, warehouse } = params + const { projectRef, warehouse } = params return useQuery({ queryKey: storageKeys.icebergNamespaces({ projectRef, warehouse, - catalog: catalogUri, }), - queryFn: () => getNamespaces({ ...params }), + queryFn: ({ signal }) => getNamespaces({ projectRef, warehouse }, signal), enabled: options && typeof projectRef !== 'undefined' && - typeof catalogUri !== 'undefined' && - catalogUri.length > 0 && typeof warehouse !== 'undefined' && warehouse.length > 0, ...options, diff --git a/apps/studio/data/storage/keys.ts b/apps/studio/data/storage/keys.ts index 952d0a59059..fb9bf4a9315 100644 --- a/apps/studio/data/storage/keys.ts +++ b/apps/studio/data/storage/keys.ts @@ -9,45 +9,24 @@ export const storageKeys = { vectorBucketsIndexes: (projectRef: string | undefined, vectorBucketName: string | undefined) => ['projects', projectRef, 'vector-buckets', vectorBucketName, 'indexes'] as const, archive: (projectRef: string | undefined) => ['projects', projectRef, 'archive'] as const, - icebergNamespaces: ({ - projectRef, - catalog, - warehouse, - }: { - projectRef?: string - catalog: string - warehouse: string - }) => [projectRef, 'catalog', catalog, 'warehouse', warehouse, 'namespaces'] as const, + icebergNamespaces: ({ projectRef, warehouse }: { projectRef?: string; warehouse?: string }) => + [projectRef, 'warehouse', warehouse, 'namespaces'] as const, icebergNamespace: ({ projectRef, - catalog, warehouse, namespace, }: { projectRef?: string - catalog: string warehouse: string namespace: string - }) => [projectRef, 'catalog', catalog, 'warehouse', warehouse, 'namespaces', namespace] as const, + }) => [projectRef, 'warehouse', warehouse, 'namespaces', namespace] as const, icebergNamespaceTables: ({ projectRef, - catalog, warehouse, namespace, }: { projectRef?: string - catalog: string - warehouse: string - namespace: string - }) => - [ - projectRef, - 'catalog', - catalog, - 'warehouse', - warehouse, - 'namespaces', - namespace, - 'tables', - ] as const, + warehouse?: string + namespace?: string + }) => [projectRef, 'warehouse', warehouse, 'namespaces', namespace, 'tables'] as const, } From db8b88a890784f1c6991b33e3056d6bea73c5a73 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Tue, 2 Dec 2025 07:18:21 -0700 Subject: [PATCH 06/22] update: Add TS any checks to ratchet rules to further stop slipping (#40877) * add ts any checks * fix linting issues * updated more file rules * updated runtime files * updated turbo to remove lints * removed eslint and reference from next * Revert the pnpm lock to the one from master. --------- Co-authored-by: Ivan Vasilov --- .../registry/default/example/dialog-demo.tsx | 2 +- .../default/example/page-layout-detail.tsx | 4 +- .../registry/default/example/toc-demo.tsx | 2 +- .../default/example/toc-single-demo.tsx | 2 +- .../default/example/typography-blockquote.tsx | 4 +- .../default/example/typography-demo.tsx | 29 +- .../default/example/typography-table.tsx | 4 +- apps/docs/instrumentation.ts | 2 + .../studio/.github/eslint-rule-baselines.json | 585 +++++++++++++++++- apps/studio/package.json | 2 +- .../app/routes/sign-up.tsx | 4 +- .../components/sign-up-form.tsx | 4 +- .../routes/sign-up-success.tsx | 2 +- .../app/api/supabase-proxy/[...path]/route.ts | 2 + .../supabase-manager/suggestions.tsx | 2 +- packages/eslint-config-supabase/next.js | 22 + turbo.json | 7 +- 17 files changed, 638 insertions(+), 41 deletions(-) diff --git a/apps/design-system/registry/default/example/dialog-demo.tsx b/apps/design-system/registry/default/example/dialog-demo.tsx index 68e81be28d5..3316817aea9 100644 --- a/apps/design-system/registry/default/example/dialog-demo.tsx +++ b/apps/design-system/registry/default/example/dialog-demo.tsx @@ -21,7 +21,7 @@ export default function DialogDemo() { Edit profile - Make changes to your profile here. Click save when you're done. + Make changes to your profile here. Click save when you're done. diff --git a/apps/design-system/registry/default/example/page-layout-detail.tsx b/apps/design-system/registry/default/example/page-layout-detail.tsx index 6553522b8ed..a5036cca81c 100644 --- a/apps/design-system/registry/default/example/page-layout-detail.tsx +++ b/apps/design-system/registry/default/example/page-layout-detail.tsx @@ -24,7 +24,7 @@ export default function PageLayoutDetail() { Billing - Manage your organization's billing and subscription settings. + Manage your organization's billing and subscription settings. @@ -72,7 +72,7 @@ export default function PageLayoutDetail() { Cost control - Set spending limits and alerts to manage your organization's costs. + Set spending limits and alerts to manage your organization's costs. diff --git a/apps/design-system/registry/default/example/toc-demo.tsx b/apps/design-system/registry/default/example/toc-demo.tsx index 2de1b7fdfeb..86c2d700664 100644 --- a/apps/design-system/registry/default/example/toc-demo.tsx +++ b/apps/design-system/registry/default/example/toc-demo.tsx @@ -39,7 +39,7 @@ export default function MultiSelectDemo() {

- Before diving deep into cloud services, it's important to understand the basic + Before diving deep into cloud services, it's important to understand the basic building blocks that make cloud computing possible.

diff --git a/apps/design-system/registry/default/example/toc-single-demo.tsx b/apps/design-system/registry/default/example/toc-single-demo.tsx index d7fe367d0ce..d5f24f153b0 100644 --- a/apps/design-system/registry/default/example/toc-single-demo.tsx +++ b/apps/design-system/registry/default/example/toc-single-demo.tsx @@ -39,7 +39,7 @@ export default function MultiSelectDemo() {

- Before diving deep into cloud services, it's important to understand the basic + Before diving deep into cloud services, it's important to understand the basic building blocks that make cloud computing possible.

diff --git a/apps/design-system/registry/default/example/typography-blockquote.tsx b/apps/design-system/registry/default/example/typography-blockquote.tsx index 15c95473fd2..4d0ae0e529e 100644 --- a/apps/design-system/registry/default/example/typography-blockquote.tsx +++ b/apps/design-system/registry/default/example/typography-blockquote.tsx @@ -1,8 +1,8 @@ export default function TypographyBlockquote() { return (
- "After all," he said, "everyone enjoys a good joke, so it's only fair that they should pay for - the privilege." + "After all," he said, "everyone enjoys a good joke, so it's only fair that + they should pay for the privilege."
) } diff --git a/apps/design-system/registry/default/example/typography-demo.tsx b/apps/design-system/registry/default/example/typography-demo.tsx index 60ec59526bc..3ea54c3a470 100644 --- a/apps/design-system/registry/default/example/typography-demo.tsx +++ b/apps/design-system/registry/default/example/typography-demo.tsx @@ -10,7 +10,7 @@ export default function TypographyDemo() { of money.

- The King's Plan + The King's Plan

The king thought long and hard, and finally came up with{' '} @@ -20,12 +20,13 @@ export default function TypographyDemo() { : he would tax the jokes in the kingdom.

- "After all," he said, "everyone enjoys a good joke, so it's only fair that they should pay - for the privilege." + "After all," he said, "everyone enjoys a good joke, so it's only fair + that they should pay for the privilege."

The Joke Tax

- The king's subjects were not amused. They grumbled and complained, but the king was firm: + The king's subjects were not amused. They grumbled and complained, but the king was + firm:

  • 1st level of puns: 5 gold coins
  • @@ -34,22 +35,24 @@ export default function TypographyDemo() {

As a result, people stopped telling jokes, and the kingdom fell into a gloom. But there was - one person who refused to let the king's foolishness get him down: a court jester named + one person who refused to let the king's foolishness get him down: a court jester named Jokester.

-

Jokester's Revolt

+

+ Jokester's Revolt +

Jokester began sneaking into the castle in the middle of the night and leaving jokes all - over the place: under the king's pillow, in his soup, even in the royal toilet. The king was - furious, but he couldn't seem to stop Jokester. + over the place: under the king's pillow, in his soup, even in the royal toilet. The + king was furious, but he couldn't seem to stop Jokester.

And then, one day, the people of the kingdom discovered that the jokes left by Jokester were - so funny that they couldn't help but laugh. And once they started laughing, they couldn't - stop. + so funny that they couldn't help but laugh. And once they started laughing, they + couldn't stop.

- The People's Rebellion + The People's Rebellion

The people of the kingdom, feeling uplifted by the laughter, started to tell jokes and puns @@ -60,10 +63,10 @@ export default function TypographyDemo() { - King's Treasury + King's Treasury - People's happiness + People's happiness diff --git a/apps/design-system/registry/default/example/typography-table.tsx b/apps/design-system/registry/default/example/typography-table.tsx index 0698c475194..7a91dfda844 100644 --- a/apps/design-system/registry/default/example/typography-table.tsx +++ b/apps/design-system/registry/default/example/typography-table.tsx @@ -5,10 +5,10 @@ export default function TypographyTable() { - King's Treasury + King's Treasury - People's happiness + People's happiness diff --git a/apps/docs/instrumentation.ts b/apps/docs/instrumentation.ts index 3063091e693..b1d3d20b222 100644 --- a/apps/docs/instrumentation.ts +++ b/apps/docs/instrumentation.ts @@ -1,10 +1,12 @@ import * as Sentry from '@sentry/nextjs' export async function register() { + // eslint-disable-next-line turbo/no-undeclared-env-vars if (process.env.NEXT_RUNTIME === 'nodejs') { await import('./sentry.server.config') } + // eslint-disable-next-line turbo/no-undeclared-env-vars if (process.env.NEXT_RUNTIME === 'edge') { await import('./sentry.edge.config') } diff --git a/apps/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json index a2729485e45..016422c7cf9 100644 --- a/apps/studio/.github/eslint-rule-baselines.json +++ b/apps/studio/.github/eslint-rule-baselines.json @@ -1,9 +1,10 @@ { "rules": { - "react-hooks/exhaustive-deps": 230, + "react-hooks/exhaustive-deps": 226, "import/no-anonymous-default-export": 62, - "@tanstack/query/exhaustive-deps": 19, - "@tanstack/query/no-deprecated-options": 0 + "@tanstack/query/exhaustive-deps": 18, + "@tanstack/query/no-deprecated-options": 0, + "@typescript-eslint/no-explicit-any": 1452 }, "ruleFiles": { "react-hooks/exhaustive-deps": { @@ -24,7 +25,6 @@ "components/interfaces/Auth/BasicAuthSettingsForm.tsx": 1, "components/interfaces/Auth/Hooks/CreateHookSheet.tsx": 1, "components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx": 1, - "components/interfaces/Auth/OAuthApps/CreateOAuthAppSheet.tsx": 1, "components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx": 1, "components/interfaces/Auth/Policies/PolicyEditor/PolicyDefinition.tsx": 1, "components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyDetailsV2.tsx": 1, @@ -101,14 +101,12 @@ "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CopyEnvButton.tsx": 1, "components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx": 1, "components/interfaces/Storage/StorageSettings/StorageSettings.tsx": 1, - "components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx": 1, "components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx": 1, "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx": 1, "components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx": 2, "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx": 2, "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx": 1, "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx": 2, - "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx": 1, "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/QuickstartAIWidget.tsx": 1, "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/useAITableGeneration.ts": 2, "components/interfaces/TableGridEditor/TableDefinition.tsx": 1, @@ -168,14 +166,13 @@ "pages/project/[ref]/editor/index.tsx": 1, "pages/project/[ref]/integrations/[id]/[pageId]/index.tsx": 1, "pages/project/[ref]/logs/explorer/index.tsx": 1, - "pages/project/[ref]/settings/jwt/index.tsx": 1, + "pages/project/[ref]/settings/jwt/legacy.tsx": 1, "pages/project/[ref]/sql/quickstarts.tsx": 1, "pages/project/[ref]/sql/templates.tsx": 1, "pages/sign-in-fly-tos.tsx": 1, "pages/sign-in-mfa.tsx": 1, "state/role-impersonation-state.tsx": 1, "state/sidebar-manager-state.tsx": 1, - "state/storage-explorer.tsx": 1, "state/table-editor-table.tsx": 1 }, "import/no-anonymous-default-export": { @@ -245,7 +242,7 @@ "@tanstack/query/exhaustive-deps": { "components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants.ts": 1, "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx": 1, - "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 3, + "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 2, "data/branches/branch-diff-query.ts": 1, "data/database/foreign-key-constraints-query.ts": 1, "data/database/schemas-query.ts": 1, @@ -258,6 +255,574 @@ "hooks/analytics/useProjectUsageStats.tsx": 1, "hooks/analytics/useSingleLog.tsx": 1 }, - "@tanstack/query/no-deprecated-options": {} + "@tanstack/query/no-deprecated-options": {}, + "@typescript-eslint/no-explicit-any": { + "components/grid/SupabaseGrid.tsx": 1, + "components/grid/SupabaseGrid.utils.ts": 4, + "components/grid/components/common/Hooks.tsx": 2, + "components/grid/components/common/MonacoEditor.tsx": 3, + "components/grid/components/editor/BooleanEditor.tsx": 1, + "components/grid/components/editor/JsonEditor.tsx": 2, + "components/grid/components/editor/SelectEditor.tsx": 1, + "components/grid/components/editor/TextEditor.tsx": 2, + "components/grid/components/formatter/ReferenceRecordPeek.tsx": 4, + "components/grid/components/grid/AddColumn.tsx": 2, + "components/grid/components/grid/Grid.tsx": 10, + "components/grid/components/grid/Grid.utils.tsx": 2, + "components/grid/components/grid/GridError.tsx": 4, + "components/grid/components/grid/SelectColumn.tsx": 3, + "components/grid/components/header/Header.utils.ts": 1, + "components/grid/components/header/sort/SortRow.tsx": 1, + "components/grid/components/menu/ColumnMenu.tsx": 1, + "components/grid/types/base.ts": 2, + "components/grid/types/table.ts": 1, + "components/grid/utils/column.ts": 2, + "components/grid/utils/common.ts": 1, + "components/grid/utils/gridColumns.tsx": 10, + "components/interfaces/Account/AccessTokens/NewAccessTokenButton.tsx": 1, + "components/interfaces/Account/AccessTokens/NewAccessTokenDialog.tsx": 1, + "components/interfaces/Auth/AdvancedAuthSettingsForm.tsx": 2, + "components/interfaces/Auth/AuditLogsForm.tsx": 1, + "components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.types.ts": 1, + "components/interfaces/Auth/AuthProvidersForm/FormField.tsx": 4, + "components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx": 6, + "components/interfaces/Auth/AuthProvidersForm/index.tsx": 5, + "components/interfaces/Auth/AuthProvidersFormValidation.tsx": 2, + "components/interfaces/Auth/BasicAuthSettingsForm.tsx": 1, + "components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx": 1, + "components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx": 1, + "components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx": 3, + "components/interfaces/Auth/Overview/OverviewTable.tsx": 1, + "components/interfaces/Auth/Overview/OverviewUsage.constants.ts": 1, + "components/interfaces/Auth/Policies/Policies.tsx": 1, + "components/interfaces/Auth/Policies/Policies.utils.ts": 1, + "components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx": 1, + "components/interfaces/Auth/Policies/PolicyEditor/index.tsx": 2, + "components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx": 2, + "components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx": 1, + "components/interfaces/Auth/Policies/PolicyEditorPanel/RLSCodeEditor.tsx": 2, + "components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx": 2, + "components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx": 1, + "components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx": 1, + "components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx": 2, + "components/interfaces/Auth/SiteUrl/SiteUrl.tsx": 1, + "components/interfaces/Auth/SmtpForm/SmtpForm.tsx": 4, + "components/interfaces/Auth/Users/CreateUserModal.tsx": 1, + "components/interfaces/Auth/Users/InviteUserModal.tsx": 3, + "components/interfaces/Auth/Users/UserLogs.tsx": 1, + "components/interfaces/Auth/Users/UserPanel.tsx": 2, + "components/interfaces/Auth/Users/Users.utils.tsx": 5, + "components/interfaces/Auth/Users/UsersGridComponents.tsx": 2, + "components/interfaces/Auth/Users/UsersV2.tsx": 6, + "components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx": 3, + "components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx": 3, + "components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx": 3, + "components/interfaces/BranchManagement/Branch.Commands.tsx": 1, + "components/interfaces/BranchManagement/DatabaseDiffPanel.tsx": 1, + "components/interfaces/BranchManagement/EmptyStates.tsx": 1, + "components/interfaces/BranchManagement/ReviewWithAI.tsx": 1, + "components/interfaces/Connect/ConnectTabs.tsx": 1, + "components/interfaces/Database/Backups/PITR/PITR.utils.ts": 1, + "components/interfaces/Database/Backups/RestoreToNewProject/BackupsList.tsx": 1, + "components/interfaces/Database/EnumeratedTypes/CreateEnumeratedTypeSidePanel.tsx": 2, + "components/interfaces/Database/EnumeratedTypes/DeleteEnumeratedTypeModal.tsx": 1, + "components/interfaces/Database/EnumeratedTypes/EditEnumeratedTypeSidePanel.tsx": 2, + "components/interfaces/Database/EnumeratedTypes/EnumeratedTypeValueRow.tsx": 1, + "components/interfaces/Database/Extensions/EnableExtensionModal.tsx": 4, + "components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx": 1, + "components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx": 3, + "components/interfaces/Database/Hooks/EditHookPanel.tsx": 9, + "components/interfaces/Database/Hooks/FormContents.tsx": 5, + "components/interfaces/Database/Hooks/HTTPRequestFields.tsx": 6, + "components/interfaces/Database/Hooks/HooksList/HooksList.tsx": 2, + "components/interfaces/Database/Indexes/Indexes.tsx": 1, + "components/interfaces/Database/Migrations/Migrations.tsx": 2, + "components/interfaces/Database/Privileges/PrivilegesTable.tsx": 1, + "components/interfaces/Database/Publications/PublicationsList.tsx": 5, + "components/interfaces/Database/Publications/PublicationsTableItem.tsx": 5, + "components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx": 8, + "components/interfaces/Database/Replication/PublicationsComboBox.tsx": 1, + "components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx": 1, + "components/interfaces/Database/Replication/RetryOptionsDropdown.tsx": 1, + "components/interfaces/Database/Replication/RowMenu.tsx": 2, + "components/interfaces/Database/Replication/UpdateVersionModal.tsx": 2, + "components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx": 1, + "components/interfaces/Database/Roles/RoleRow.tsx": 4, + "components/interfaces/Database/Roles/RolesList.tsx": 1, + "components/interfaces/Database/Schemas/SchemaGraph.tsx": 1, + "components/interfaces/Database/Tables/ColumnList.tsx": 2, + "components/interfaces/DiskManagement/DiskManagement.utils.ts": 3, + "components/interfaces/Docs/Description.tsx": 2, + "components/interfaces/Docs/GeneratingTypes.tsx": 1, + "components/interfaces/Docs/Param.tsx": 1, + "components/interfaces/Docs/ResourceContent.tsx": 1, + "components/interfaces/Docs/RpcContent.tsx": 4, + "components/interfaces/Docs/Snippets.ts": 1, + "components/interfaces/Functions/CommandRender.tsx": 4, + "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx": 1, + "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx": 3, + "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx": 1, + "components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx": 1, + "components/interfaces/Functions/EdgeFunctionsListItem.tsx": 2, + "components/interfaces/Functions/FunctionsNav.tsx": 1, + "components/interfaces/Home/Home.tsx": 6, + "components/interfaces/Home/NewProjectPanel/APIKeys.tsx": 1, + "components/interfaces/Home/ProjectList/ProjectList.tsx": 1, + "components/interfaces/Home/ProjectUsage.tsx": 1, + "components/interfaces/Home/ServiceStatus.tsx": 1, + "components/interfaces/HomeNew/ProjectUsageSection.tsx": 1, + "components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx": 1, + "components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx": 2, + "components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx": 1, + "components/interfaces/Integrations/CronJobs/CronJobsTab.tsx": 1, + "components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx": 2, + "components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx": 1, + "components/interfaces/Integrations/Landing/Integrations.constants.tsx": 1, + "components/interfaces/Integrations/Queues/QueuesSettings.tsx": 1, + "components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx": 1, + "components/interfaces/Integrations/Queues/SingleQueue/QueueFilters.tsx": 1, + "components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx": 2, + "components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx": 6, + "components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx": 1, + "components/interfaces/Integrations/Wrappers/CreateIcebergWrapperSheet.tsx": 3, + "components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx": 6, + "components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx": 7, + "components/interfaces/Integrations/Wrappers/InputField.tsx": 1, + "components/interfaces/Integrations/Wrappers/WrapperDynamicColumns.tsx": 1, + "components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx": 7, + "components/interfaces/Integrations/Wrappers/Wrappers.utils.ts": 2, + "components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx": 2, + "components/interfaces/JwtSecrets/jwt-settings.tsx": 5, + "components/interfaces/Linter/LinterDataGrid.tsx": 4, + "components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx": 2, + "components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx": 2, + "components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils.ts": 2, + "components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx": 2, + "components/interfaces/Organization/BillingSettings/CreditTopUp.tsx": 2, + "components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx": 1, + "components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx": 2, + "components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx": 1, + "components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx": 6, + "components/interfaces/Organization/BillingSettings/Subscription/UpgradeModal.tsx": 3, + "components/interfaces/Organization/Documents/SOC2.tsx": 1, + "components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx": 1, + "components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx": 3, + "components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx": 1, + "components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx": 1, + "components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx": 1, + "components/interfaces/Organization/OAuthApps/PublishAppSidePanel/index.tsx": 9, + "components/interfaces/Organization/ProjectClaim/confirm.tsx": 1, + "components/interfaces/Organization/TeamSettings/MemberRow.tsx": 2, + "components/interfaces/Organization/TeamSettings/TeamSettings.tsx": 1, + "components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx": 2, + "components/interfaces/Organization/Usage/Usage.tsx": 1, + "components/interfaces/Organization/Usage/Usage.utils.ts": 1, + "components/interfaces/Organization/Usage/UsageChartTooltips.tsx": 7, + "components/interfaces/ProjectAPIDocs/Content/Entities.tsx": 1, + "components/interfaces/ProjectAPIDocs/Content/Entity.tsx": 1, + "components/interfaces/ProjectAPIDocs/Content/RPC.tsx": 1, + "components/interfaces/ProjectAPIDocs/ProjectAPIDocs.constants.ts": 2, + "components/interfaces/ProjectAPIDocs/ResourceContent.tsx": 1, + "components/interfaces/ProjectAPIDocs/SecondLevelNav.tsx": 1, + "components/interfaces/ProjectCreation/DatabasePasswordInput.tsx": 1, + "components/interfaces/ProjectCreation/PostgresVersionSelector.tsx": 2, + "components/interfaces/ProjectCreation/SchemaGenerator.tsx": 1, + "components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton.tsx": 1, + "components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorDisabledState.tsx": 1, + "components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts": 2, + "components/interfaces/QueryPerformance/QueryDetail.tsx": 1, + "components/interfaces/QueryPerformance/QueryIndexes.tsx": 1, + "components/interfaces/QueryPerformance/QueryPerformance.tsx": 1, + "components/interfaces/QueryPerformance/QueryPerformanceChart.tsx": 3, + "components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx": 3, + "components/interfaces/QueryPerformance/WithMonitor/WithMonitor.utils.ts": 2, + "components/interfaces/QueryPerformance/WithStatements/WithStatements.tsx": 2, + "components/interfaces/QueryPerformance/WithStatements/WithStatements.utils.ts": 1, + "components/interfaces/Realtime/Inspector/Messages.types.ts": 1, + "components/interfaces/Realtime/Inspector/RealtimeFilterPopover/TableSelector.tsx": 1, + "components/interfaces/Realtime/Inspector/useRealtimeMessages.ts": 10, + "components/interfaces/Realtime/RealtimeSettings.tsx": 2, + "components/interfaces/Reports/GridResize.tsx": 2, + "components/interfaces/Reports/MetricOptions.tsx": 1, + "components/interfaces/Reports/ReportBlock/ChartBlock.tsx": 1, + "components/interfaces/Reports/ReportWidget.tsx": 2, + "components/interfaces/Reports/Reports.tsx": 3, + "components/interfaces/Reports/Reports.types.ts": 1, + "components/interfaces/Reports/Reports.utils.tsx": 10, + "components/interfaces/Reports/SharedAPIReport/SharedAPIReport.tsx": 3, + "components/interfaces/Reports/v2/ReportChartUpsell.tsx": 1, + "components/interfaces/Reports/v2/ReportChartV2.test.tsx": 4, + "components/interfaces/Reports/v2/ReportChartV2.tsx": 8, + "components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx": 1, + "components/interfaces/SQLEditor/MonacoEditor.tsx": 2, + "components/interfaces/SQLEditor/MoveQueryModal.tsx": 2, + "components/interfaces/SQLEditor/RenameQueryModal.tsx": 5, + "components/interfaces/SQLEditor/SQLEditor.tsx": 4, + "components/interfaces/SQLEditor/SQLEditor.utils.ts": 1, + "components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx": 1, + "components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx": 1, + "components/interfaces/SQLEditor/UtilityPanel/CellDetailPanel.tsx": 1, + "components/interfaces/SQLEditor/UtilityPanel/ChartConfig.tsx": 2, + "components/interfaces/SQLEditor/UtilityPanel/Results.tsx": 7, + "components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx": 1, + "components/interfaces/SQLEditor/hooks.ts": 1, + "components/interfaces/SQLEditor/useAddDefinitions.ts": 1, + "components/interfaces/SchemaVisualizer/index.tsx": 1, + "components/interfaces/Settings/Addons/Addons.tsx": 1, + "components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx": 1, + "components/interfaces/Settings/Addons/IPv4SidePanel.tsx": 1, + "components/interfaces/Settings/Addons/PITRSidePanel.tsx": 1, + "components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx": 2, + "components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx": 1, + "components/interfaces/Settings/Database/NetworkRestrictions/AddRestrictionModal.tsx": 5, + "components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.utils.ts": 2, + "components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx": 1, + "components/interfaces/Settings/General/General.tsx": 3, + "components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx": 1, + "components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx": 1, + "components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx": 1, + "components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx": 1, + "components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/RestartReplicaConfirmationModal.tsx": 1, + "components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx": 1, + "components/interfaces/Settings/Logs/LogTable.tsx": 3, + "components/interfaces/Settings/Logs/Logs.constants.ts": 1, + "components/interfaces/Settings/Logs/Logs.types.ts": 1, + "components/interfaces/Settings/Logs/Logs.utils.ts": 11, + "components/interfaces/Settings/Logs/LogsQueryPanel.tsx": 2, + "components/interfaces/Settings/Logs/PreviewFilterPanel.tsx": 1, + "components/interfaces/Settings/Logs/PreviewFilterPanelWithUniversal.tsx": 3, + "components/interfaces/Settings/Logs/SidebarV2/SidebarItem.tsx": 1, + "components/interfaces/Sidebar.tsx": 3, + "components/interfaces/SignIn/SignInForm.tsx": 1, + "components/interfaces/SignIn/SignInWithCustom.tsx": 1, + "components/interfaces/SignIn/SignInWithGitHub.tsx": 1, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/ConnectTablesDialog.tsx": 2, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/InitializeForeignSchemaDialog.tsx": 1, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx": 4, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/index.tsx": 1, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/UpdateForeignSchemaDialog.tsx": 1, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx": 1, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/useAnalyticsBucketAssociatedEntities.tsx": 4, + "components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx": 1, + "components/interfaces/Storage/CreateBucketModal.tsx": 1, + "components/interfaces/Storage/DeleteBucketModal.tsx": 1, + "components/interfaces/Storage/ImportForeignSchemaDialog.tsx": 1, + "components/interfaces/Storage/Storage.utils.ts": 7, + "components/interfaces/Storage/StorageBucketsError.tsx": 1, + "components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx": 5, + "components/interfaces/Storage/StorageExplorer/FileExplorer.tsx": 2, + "components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx": 6, + "components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx": 6, + "components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx": 1, + "components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx": 2, + "components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx": 1, + "components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx": 1, + "components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx": 1, + "components/interfaces/Storage/StorageExplorer/StorageExplorer.utils.tsx": 2, + "components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx": 8, + "components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx": 11, + "components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx": 2, + "components/interfaces/Storage/StoragePolicies/StoragePoliciesReview.tsx": 3, + "components/interfaces/Storage/StorageSettings/StorageSettings.tsx": 2, + "components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx": 2, + "components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx": 3, + "components/interfaces/Support/AttachmentUpload.tsx": 3, + "components/interfaces/Support/LinkSupportTicketForm.tsx": 6, + "components/interfaces/Support/SupportFormV2.tsx": 1, + "components/interfaces/Support/__tests__/SupportFormPage.test.tsx": 19, + "components/interfaces/TableGridEditor/SidePanelEditor/ActionBar.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnDefaultValue.tsx": 2, + "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx": 5, + "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts": 2, + "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx": 2, + "components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx": 9, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/InputField.tsx": 9, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/DrilldownViewer/DrilldownPane.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/DrilldownViewer/DrilldownViewer.tsx": 3, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx": 3, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx": 9, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types.ts": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.test.ts": 8, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts": 9, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx": 2, + "components/interfaces/TableGridEditor/SidePanelEditor/SchemaEditor.tsx": 2, + "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx": 14, + "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.types.ts": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.test.ts": 6, + "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 14, + "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetFileUpload.tsx": 3, + "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetTextInput.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx": 4, + "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.types.ts": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.tsx": 18, + "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx": 2, + "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetPreviewGrid.tsx": 2, + "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 3, + "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx": 3, + "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeyRow.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx": 2, + "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts": 1, + "components/interfaces/TableGridEditor/TableDefinition.tsx": 3, + "components/interfaces/TableGridEditor/TableEntity.utils.ts": 1, + "components/interfaces/UnifiedLogs/ServiceFlow/components/ServiceFlowHeader.tsx": 1, + "components/interfaces/UnifiedLogs/ServiceFlow/components/blocks/ResponseCompletedBlock.tsx": 2, + "components/interfaces/UnifiedLogs/ServiceFlow/components/shared/Block.tsx": 1, + "components/interfaces/UnifiedLogs/ServiceFlow/components/shared/CollapsibleSection.tsx": 4, + "components/interfaces/UnifiedLogs/ServiceFlow/components/shared/FieldWithSeeMore.tsx": 4, + "components/interfaces/UnifiedLogs/ServiceFlow/components/shared/TimelineStep.tsx": 1, + "components/interfaces/UnifiedLogs/ServiceFlow/config/fieldHelpers.ts": 3, + "components/interfaces/UnifiedLogs/ServiceFlow/types.ts": 10, + "components/interfaces/UnifiedLogs/ServiceFlow/utils/storageUtils.ts": 3, + "components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts": 5, + "components/interfaces/UnifiedLogs/UnifiedLogs.tsx": 8, + "components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts": 1, + "components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx": 1, + "components/interfaces/UnifiedLogs/components/LogsListPanel.tsx": 1, + "components/layouts/AccountLayout/WithSidebar.tsx": 3, + "components/layouts/DocsLayout/DocsLayout.tsx": 2, + "components/layouts/IntegrationsLayout/Integrations.utils.ts": 2, + "components/layouts/LogsLayout/LogsSidebarMenuV2.tsx": 1, + "components/layouts/ObservabilityLayout/ObservabilityMenu.tsx": 1, + "components/layouts/ProjectLayout/BuildingState.tsx": 1, + "components/layouts/ProjectLayout/LayoutHeader/BreadcrumbsView.tsx": 2, + "components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx": 3, + "components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx": 1, + "components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx": 1, + "components/layouts/ReportsLayout/ReportsMenu.tsx": 1, + "components/layouts/SQLEditorLayout/SQLEditorMenu.tsx": 2, + "components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx": 3, + "components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.utils.ts": 2, + "components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx": 2, + "components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx": 1, + "components/layouts/TableEditorLayout/EntityListItem.tsx": 3, + "components/layouts/TableEditorLayout/TableEditorMenu.tsx": 2, + "components/layouts/Tabs/NewTab.tsx": 1, + "components/to-be-cleaned/KeyMap.tsx": 3, + "components/ui/AIAssistantPanel/AIAssistant.utils.ts": 1, + "components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx": 1, + "components/ui/AIAssistantPanel/Message.Parts.tsx": 1, + "components/ui/AIAssistantPanel/MessageMarkdown.tsx": 1, + "components/ui/AIEditor/index.tsx": 2, + "components/ui/CardButton.tsx": 2, + "components/ui/Charts/AreaChart.tsx": 1, + "components/ui/Charts/BarChart.tsx": 2, + "components/ui/Charts/ChartHandler.tsx": 4, + "components/ui/Charts/ChartHeader.tsx": 5, + "components/ui/Charts/Charts.constants.ts": 1, + "components/ui/Charts/Charts.utils.tsx": 3, + "components/ui/Charts/ComposedChart.tsx": 10, + "components/ui/Charts/ComposedChart.utils.tsx": 13, + "components/ui/Charts/ComposedChartHandler.tsx": 9, + "components/ui/Charts/StackedBarChart.tsx": 3, + "components/ui/Charts/useChartHighlight.tsx": 9, + "components/ui/Charts/useChartHoverState.test.tsx": 1, + "components/ui/CodeEditor/CodeEditor.tsx": 8, + "components/ui/CodeEditor/CodeEditor.utils.ts": 2, + "components/ui/CodeEditor/Providers/BackwardIterator.ts": 1, + "components/ui/CodeEditor/Providers/PgSQLCompletionProvider.ts": 21, + "components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider.ts": 4, + "components/ui/DataTable/DataTable.utils.ts": 8, + "components/ui/DataTable/DataTableFilters/DataTableFilterCommand.tsx": 1, + "components/ui/DataTable/DataTableFilters/DataTableFilters.utils.ts": 1, + "components/ui/DataTable/DataTableInfinite.tsx": 6, + "components/ui/DataTable/DataTableViewOptions.tsx": 1, + "components/ui/DataTable/LiveButton.tsx": 1, + "components/ui/DataTable/providers/DataTableProvider.tsx": 3, + "components/ui/DatePicker/DatePicker.types.ts": 2, + "components/ui/DatePicker/index.tsx": 2, + "components/ui/DateRangePicker.tsx": 1, + "components/ui/DebouncedComponent.tsx": 1, + "components/ui/DownloadResultsButton.tsx": 2, + "components/ui/Error.tsx": 1, + "components/ui/ErrorBoundary/ErrorBoundary.tsx": 2, + "components/ui/ErrorBoundary/GlobalErrorBoundaryState.tsx": 1, + "components/ui/FilterPopover.tsx": 1, + "components/ui/Forms/Form.constants.ts": 1, + "components/ui/InfiniteList.tsx": 1, + "components/ui/Logs/LogsExplorerHeader.tsx": 2, + "components/ui/PasswordStrengthBar.tsx": 4, + "components/ui/ProductMenu/ProductMenu.types.ts": 1, + "components/ui/ProjectSettings/DisplayApiSettings.tsx": 1, + "components/ui/QueryBlock/QueryBlock.tsx": 4, + "components/ui/QueryBlock/QueryBlock.utils.ts": 1, + "components/ui/SchemaComboBox.tsx": 1, + "components/ui/SchemaSelector.tsx": 1, + "components/ui/SqlEditor.tsx": 6, + "components/ui/TwoOptionToggle.tsx": 4, + "components/ui/ui.types.ts": 1, + "data/ai/check-api-key-query.ts": 1, + "data/ai/rate-message-mutation.ts": 1, + "data/ai/sql-cron-mutation.ts": 1, + "data/ai/sql-title-mutation.ts": 1, + "data/analytics/functions-combined-stats-query.ts": 1, + "data/analytics/functions-req-stats-query.ts": 1, + "data/analytics/functions-resource-usage-query.ts": 1, + "data/analytics/infra-monitoring-queries.ts": 1, + "data/analytics/infra-monitoring-query.ts": 1, + "data/analytics/project-daily-stats-queries.ts": 1, + "data/analytics/project-metrics-query.ts": 3, + "data/api-settings/create-and-expose-api-schema-mutation.ts": 1, + "data/auth/auth-overview-query.ts": 1, + "data/auth/session-access-token-query.ts": 1, + "data/config/project-disk-resize-mutation.ts": 1, + "data/config/project-storage-config-query.ts": 2, + "data/content/content-insert-mutation.ts": 1, + "data/custom-domains/custom-domains-query.ts": 1, + "data/database-columns/database-column-create-mutation.ts": 1, + "data/database-queues/database-queues-metrics-query.ts": 1, + "data/database-triggers/database-triggers-query.ts": 2, + "data/database/table-columns-query.ts": 1, + "data/docs/project-json-schema-query.ts": 1, + "data/documents/dpa-request-mutation.ts": 1, + "data/edge-functions/edge-function-test-mutation.ts": 1, + "data/edge-functions/edge-functions-deploy-mutation.ts": 2, + "data/fdw/fdw-create-mutation.ts": 1, + "data/fdw/fdw-update-mutation.ts": 1, + "data/fetchers.ts": 12, + "data/foreign-tables/foreign-tables-query.ts": 1, + "data/graphql/fragment-masking.ts": 29, + "data/graphql/gql.ts": 1, + "data/graphql/graphql.ts": 2, + "data/integrations/integrations.types.ts": 1, + "data/invoices/invoices-count-query.ts": 1, + "data/jwt-signing-keys/jwt-signing-key-create-mutation.ts": 1, + "data/log-drains/create-log-drain-mutation.ts": 1, + "data/log-drains/update-log-drain-mutation.ts": 1, + "data/logs/get-unified-logs.ts": 1, + "data/logs/unified-log-inspection-query.ts": 2, + "data/logs/unified-logs-chart-query.ts": 1, + "data/logs/unified-logs-count-query.ts": 1, + "data/logs/unified-logs-infinite-query.ts": 2, + "data/materialized-views/materialized-views-query.ts": 1, + "data/network-restrictions/network-restrictions-query.ts": 3, + "data/notifications/keys.ts": 1, + "data/notifications/notifications-v2-query.ts": 2, + "data/oauth-secrets/client-secret-create-mutation.ts": 2, + "data/open-api/api-spec-query.ts": 8, + "data/organization-members/organization-roles-query.ts": 1, + "data/organizations/organization-create-mutation.ts": 2, + "data/organizations/organization-customer-profile-update-mutation.ts": 2, + "data/organizations/organization-payment-method-default-mutation.ts": 2, + "data/organizations/organization-update-mutation.ts": 1, + "data/platform/platform-status-query.ts": 1, + "data/profile/profile-identities-query.ts": 1, + "data/profile/profile-unlink-identity-mutation.ts": 2, + "data/projects/project-create-mutation.ts": 1, + "data/projects/project-detail-query.ts": 2, + "data/replication/restart-pipeline-helper.ts": 2, + "data/replication/rollback-table-mutation.ts": 1, + "data/replication/use-table-reset.ts": 1, + "data/reports/database-charts.ts": 5, + "data/reports/v2/auth.config.ts": 16, + "data/reports/v2/edge-functions.config.ts": 9, + "data/reports/v2/edge-functions.test.tsx": 2, + "data/reports/v2/reports.types.ts": 2, + "data/sql/execute-sql-query.ts": 5, + "data/ssl-enforcement/ssl-enforcement-query.ts": 2, + "data/ssl-enforcement/ssl-enforcement-update-mutation.ts": 1, + "data/sso/sso-config-query.ts": 2, + "data/storage/analytics-bucket-delete-mutation.ts": 1, + "data/storage/bucket-delete-mutation.ts": 1, + "data/storage/bucket-update-mutation.ts": 3, + "data/subscriptions/org-subscription-confirm-pending-create.ts": 1, + "data/subscriptions/org-subscription-update-mutation.ts": 2, + "data/support/generate-attachment-urls-mutation.ts": 1, + "data/table-rows/get-cell-value-mutation.ts": 1, + "data/table-rows/keys.ts": 2, + "data/table-rows/table-row-create-mutation.ts": 1, + "data/table-rows/table-row-update-mutation.ts": 2, + "data/table-rows/table-rows-query.ts": 4, + "data/tables/tables-query.ts": 1, + "data/vault/vault-secret-decrypted-value-query.ts": 2, + "data/views/views-query.ts": 1, + "hooks/analytics/useDbQuery.tsx": 1, + "hooks/analytics/useFillTimeseriesSorted.ts": 3, + "hooks/analytics/useLogsPreview.tsx": 1, + "hooks/analytics/useLogsQuery.tsx": 1, + "hooks/analytics/useLogsUrlState.ts": 1, + "hooks/analytics/useSingleLog.tsx": 1, + "hooks/analytics/useTimeseriesUnixToIso.ts": 1, + "hooks/branches/useBranchMergeDiff.ts": 3, + "hooks/misc/withAuth.tsx": 1, + "hooks/storage/useStoragePolicyCounts.ts": 2, + "hooks/ui/useClickedOutside.ts": 2, + "hooks/ui/useFlag.ts": 1, + "hooks/ui/useHotKey.ts": 1, + "instrumentation-client.ts": 5, + "lib/ai/model.ts": 2, + "lib/ai/model.utils.ts": 4, + "lib/ai/test-fixtures.ts": 1, + "lib/ai/tool-filter.test.ts": 5, + "lib/ai/tool-filter.ts": 4, + "lib/api/apiAuthenticate.test.ts": 2, + "lib/api/apiHelpers.test.ts": 1, + "lib/api/apiHelpers.ts": 2, + "lib/api/apiWrappers.test.ts": 2, + "lib/api/generate-v4.test.ts": 2, + "lib/api/rate.test.ts": 2, + "lib/api/self-hosted/logs.ts": 2, + "lib/gotrue.ts": 1, + "lib/helpers.test.ts": 10, + "lib/helpers.ts": 12, + "lib/integration-utils.test.ts": 1, + "lib/integration-utils.ts": 2, + "lib/password-strength.ts": 1, + "lib/pg-format.ts": 6, + "lib/pingPostgrest.test.ts": 1, + "lib/posthog.test.ts": 4, + "lib/profile.tsx": 1, + "lib/role-impersonation.test.ts": 5, + "lib/role-impersonation.ts": 1, + "lib/sanitize.test.ts": 10, + "lib/semver.test.ts": 3, + "lib/telemetry/track.ts": 1, + "lib/upload.test.ts": 1, + "pages/_app.tsx": 1, + "pages/account/tokens.tsx": 1, + "pages/api/ai/feedback/rate.ts": 2, + "pages/api/ai/sql/generate-v4.ts": 3, + "pages/api/check-cname.ts": 1, + "pages/api/edge-functions/test.ts": 1, + "pages/api/platform/auth/[ref]/users/[id]/factors.ts": 1, + "pages/api/platform/database/[ref]/pooling.ts": 1, + "pages/api/platform/integrations/github/authorization.ts": 1, + "pages/api/platform/projects/[ref]/analytics/log-drains.ts": 1, + "pages/api/platform/projects/[ref]/content/index.ts": 1, + "pages/api/platform/projects/[ref]/content/item/[id].ts": 1, + "pages/api/platform/projects/[ref]/databases.ts": 1, + "pages/cli/login.tsx": 1, + "pages/integrations/vercel/[slug]/deploy-button/new-project.tsx": 1, + "pages/integrations/vercel/[slug]/marketplace/choose-project.tsx": 1, + "pages/new/index.tsx": 1, + "pages/project/[ref]/api/index.tsx": 3, + "pages/project/[ref]/auth/templates/[templateId].tsx": 1, + "pages/project/[ref]/functions/[functionSlug]/index.tsx": 3, + "pages/project/[ref]/logs/explorer/index.tsx": 3, + "pages/project/[ref]/merge.tsx": 1, + "pages/project/[ref]/observability/database.tsx": 2, + "pages/project/[ref]/settings/log-drains.tsx": 1, + "pages/project/[ref]/storage/files/buckets/[bucketId].tsx": 1, + "scripts/__tests__/ratchet-eslint-rules.test.ts": 1, + "state/ai-assistant-state.tsx": 6, + "state/editor-panel-state.tsx": 4, + "state/role-impersonation-state.tsx": 1, + "state/sql-editor-v2.ts": 5, + "state/storage-explorer.tsx": 16, + "state/table-editor-table.tsx": 9, + "state/table-editor.tsx": 5, + "state/tabs.tsx": 1, + "tests/components/Editor/SpreadsheetImport.utils.test.ts": 1, + "tests/components/ui/Charts/Charts.utils.test.ts": 1, + "tests/features/logs/LogsPreviewer.test.tsx": 2, + "tests/features/logs/logs-query.test.tsx": 1, + "tests/lib/custom-render.tsx": 1, + "tests/setup/polyfills.ts": 3, + "tests/vitestSetup.ts": 1, + "types/form.ts": 1, + "types/next.ts": 2, + "types/ui.ts": 2 + } } } diff --git a/apps/studio/package.json b/apps/studio/package.json index 649d785f98e..70a96386ac1 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -8,7 +8,7 @@ "build": "next build && if [ \"$SKIP_ASSET_UPLOAD\" != \"1\" ]; then ./../../scripts/upload-static-assets.sh; fi", "start": "next start", "lint": "eslint .", - "lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rule react-hooks/exhaustive-deps --rule import/no-anonymous-default-export --rule @tanstack/query/exhaustive-deps --rule @tanstack/query/no-deprecated-options", + "lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rule react-hooks/exhaustive-deps --rule import/no-anonymous-default-export --rule @tanstack/query/exhaustive-deps --rule @tanstack/query/no-deprecated-options --rule @typescript-eslint/no-explicit-any", "clean": "rimraf node_modules tsconfig.tsbuildinfo .next .turbo", "test": "vitest --run --coverage", "test:watch": "vitest watch", diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx index dea6dd62212..9d287ed00d1 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx +++ b/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx @@ -68,8 +68,8 @@ export default function SignUp() {

- You've successfully signed up. Please check your email to confirm your account - before signing in. + You've successfully signed up. Please check your email to confirm your + account before signing in.

diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx index 056ada53d79..9b5d77b1187 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx +++ b/apps/ui-library/registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx @@ -55,8 +55,8 @@ export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutR

- You've successfully signed up. Please check your email to confirm your account before - signing in. + You've successfully signed up. Please check your email to confirm your account + before signing in.

diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx index 5a59b834113..8765ff067b9 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx @@ -23,7 +23,7 @@ function SignUpSuccess() {

- You've successfully signed up. Please check your email to confirm your account + You've successfully signed up. Please check your email to confirm your account before signing in.

diff --git a/apps/ui-library/registry/default/platform/platform-kit-nextjs/app/api/supabase-proxy/[...path]/route.ts b/apps/ui-library/registry/default/platform/platform-kit-nextjs/app/api/supabase-proxy/[...path]/route.ts index 9c8de65bc50..4727e7231e9 100644 --- a/apps/ui-library/registry/default/platform/platform-kit-nextjs/app/api/supabase-proxy/[...path]/route.ts +++ b/apps/ui-library/registry/default/platform/platform-kit-nextjs/app/api/supabase-proxy/[...path]/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server' async function forwardToSupabaseAPI(request: Request, method: string, params: { path: string[] }) { + // eslint-disable-next-line turbo/no-undeclared-env-vars if (!process.env.SUPABASE_MANAGEMENT_API_TOKEN) { console.error('Supabase Management API token is not configured.') return NextResponse.json({ message: 'Server configuration error.' }, { status: 500 }) @@ -30,6 +31,7 @@ async function forwardToSupabaseAPI(request: Request, method: string, params: { try { const forwardHeaders: HeadersInit = { + // eslint-disable-next-line turbo/no-undeclared-env-vars Authorization: `Bearer ${process.env.SUPABASE_MANAGEMENT_API_TOKEN}`, } diff --git a/apps/ui-library/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions.tsx b/apps/ui-library/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions.tsx index 070c6069804..a6b5135fbea 100644 --- a/apps/ui-library/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions.tsx +++ b/apps/ui-library/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions.tsx @@ -37,7 +37,7 @@ export function SuggestionsManager({ projectRef }: { projectRef: string }) {

Suggestions

- Improve your project's security and performance. + Improve your project's security and performance.

{isLoading && (
diff --git a/packages/eslint-config-supabase/next.js b/packages/eslint-config-supabase/next.js index 667ffba631d..b59b28777b2 100644 --- a/packages/eslint-config-supabase/next.js +++ b/packages/eslint-config-supabase/next.js @@ -5,6 +5,8 @@ const prettierConfig = require('eslint-config-prettier/flat') const { default: turboConfig } = require('eslint-config-turbo/flat') const { fixupPluginRules } = require('@eslint/compat') const tanstackQuery = require('@tanstack/eslint-plugin-query') +const tseslint = require('@typescript-eslint/eslint-plugin') +const tsparser = require('@typescript-eslint/parser') const compat = new FlatCompat({ baseDirectory: __dirname, @@ -25,12 +27,32 @@ const tanstackQueryConfig = { }, } +// TypeScript ESLint config for TypeScript files +const typescriptConfig = { + name: 'typescript', + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + }, +} + module.exports = defineConfig([ // Global ignore for the .next folder { ignores: ['.next', 'public'] }, turboConfig, prettierConfig, tanstackQueryConfig, + typescriptConfig, { extends: compat.extends('next/core-web-vitals'), linterOptions: { diff --git a/turbo.json b/turbo.json index 1888dac5b6e..66455bb6507 100644 --- a/turbo.json +++ b/turbo.json @@ -197,7 +197,8 @@ "NOTION_SUPASQUAD_APPLICATIONS_DB_ID", "CUSTOMERIO_SITE_ID", "CUSTOMERIO_API_KEY", - "CUSTOMERIO_APP_API_KEY" + "CUSTOMERIO_APP_API_KEY", + "NEXT_RUNTIME" ], "outputs": [".next/**", "!.next/cache/**", ".contentlayer/**"] }, @@ -220,7 +221,9 @@ "NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID", "NEXT_PUBLIC_GOTRUE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY", - "NEXT_PUBLIC_USERCENTRICS_RULESET_ID" + "NEXT_PUBLIC_USERCENTRICS_RULESET_ID", + "SUPABASE_MANAGEMENT_API_TOKEN", + "OPENAI_API_KEY" ], "outputs": [".next/**", "!.next/cache/**", ".contentlayer/**"] }, From 23998da5c3cfb6fa740a02cb058cf789eefd6821 Mon Sep 17 00:00:00 2001 From: Prashant Sridharan <914007+CoolAssPuppy@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:55:03 +0000 Subject: [PATCH 07/22] Staging blog posts for Tuesday (#40962) * Staging blog posts for Tuesday * ci: Autofix updates from GitHub workflow * Removed Open Warehouse Architecture post * Added images * Improve content * Improe * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Changed author of Analytics Buckets post --------- Co-authored-by: Riccardo Busetti --- apps/www/_blog/2025-12-01-vector-buckets.mdx | 1 + ...25-12-02-introducing-analytics-buckets.mdx | 210 ++++++++++++++++++ .../2025-12-02-introducing-supabase-etl.mdx | 165 ++++++++++++++ apps/www/components/Blog/BlogListItem.tsx | 14 +- apps/www/components/Blog/FeaturedThumb.tsx | 13 +- apps/www/lib/authors.json | 8 + .../og.png | Bin 0 -> 131489 bytes .../thumb.png | Bin 0 -> 150205 bytes .../og.png | Bin 0 -> 138414 bytes .../thumb.png | Bin 0 -> 190872 bytes apps/www/public/rss.xml | 16 +- 11 files changed, 414 insertions(+), 13 deletions(-) create mode 100644 apps/www/_blog/2025-12-02-introducing-analytics-buckets.mdx create mode 100644 apps/www/_blog/2025-12-02-introducing-supabase-etl.mdx create mode 100644 apps/www/public/images/blog/2025-12-02-introducing-analytics-buckets/og.png create mode 100644 apps/www/public/images/blog/2025-12-02-introducing-analytics-buckets/thumb.png create mode 100644 apps/www/public/images/blog/2025-12-02-introducing-supabase-etl/og.png create mode 100644 apps/www/public/images/blog/2025-12-02-introducing-supabase-etl/thumb.png diff --git a/apps/www/_blog/2025-12-01-vector-buckets.mdx b/apps/www/_blog/2025-12-01-vector-buckets.mdx index fee362e2b8b..8140126b186 100644 --- a/apps/www/_blog/2025-12-01-vector-buckets.mdx +++ b/apps/www/_blog/2025-12-01-vector-buckets.mdx @@ -6,6 +6,7 @@ image: 2025-12-01-vector-buckets/og.png?v=3 thumb: 2025-12-01-vector-buckets/thumb.png?v=3 categories: - product + - launch-week date: '2025-12-01' toc_depth: 2 --- diff --git a/apps/www/_blog/2025-12-02-introducing-analytics-buckets.mdx b/apps/www/_blog/2025-12-02-introducing-analytics-buckets.mdx new file mode 100644 index 00000000000..04eef79ac36 --- /dev/null +++ b/apps/www/_blog/2025-12-02-introducing-analytics-buckets.mdx @@ -0,0 +1,210 @@ +--- +title: 'Introducing Analytics Buckets' +description: 'Use Analytics Buckets to store huge datasets in Supabase Storage with Apache Iceberg and columnar Parquet format, optimized for analytical workloads.' +author: fabrizio +image: 2025-12-02-introducing-analytics-buckets/og.png +thumb: 2025-12-02-introducing-analytics-buckets/thumb.png +categories: + - product + - launch-week +date: '2025-12-02:10:00' +toc_depth: 2 +--- + +Today Supabase is introducing Analytics Buckets, which you can use to store huge sets of data in Supabase Storage. Postgres is great for your app. But Postgres isn't designed for analytical workloads. + +Analytics Buckets are a specialized storage type in Supabase designed for analytical workloads and built on [Apache Iceberg](https://iceberg.apache.org) and Amazon S3. They store data in columnar Parquet format, which is optimized for scans, aggregations, and time-series queries. + +Think of them as cold storage for your data, with a query engine attached. + +Your hot transactional data stays in Postgres. Your historical data and analytical workloads live in Analytics Buckets. You query both using familiar tools. + +## What do they do? + +Analytics Buckets give you: + +- **Cost-effective storage.** S3 pricing instead of database storage. Documented savings of 30-90% on storage costs for large datasets. + +- **Open table format.** Apache Iceberg means no vendor lock-in. Query your data from any compatible tool. + +- **Schema evolution.** Change your table schema without rewriting data. + +- **Time travel.** Query historical snapshots of your data. See what a table looked like at any point in time. + +- **Full audit history.** Every change is preserved. Track what changed, when, and how. + +## When to use Analytics Buckets vs Postgres + +Analytics Buckets and Postgres are complementary. They serve different workloads. + +### Keep data in Postgres when: + +- You need low-latency reads for your application + +- Data changes frequently and consistency matters + +- Your dataset is small to medium size + +- You need real-time access from your app + +### Use Analytics Buckets when: + +- You are storing millions or billions of rows + +- You run heavy analytical queries that scan large tables + +- You need long-term retention at low cost + +- You want to query data from multiple tools + +- You need complete audit history and time travel + +Many teams use both. Keep the last 90 days in Postgres. Archive everything to Analytics Buckets. Query historical data when needed. + +## How they work + +Analytics Buckets use [Apache Iceberg](https://iceberg.apache.org), an open table format created for large analytical datasets. Here is what happens under the hood: + +1. Data is stored in Parquet files on S3 + +2. Iceberg manages metadata including schema, partitions, and snapshots + +3. An Iceberg REST Catalog provides the interface for querying + +4. You connect using any Iceberg-compatible tool + +The separation of compute and storage means you can scale each independently. Store petabytes of data and query only what you need. + +## Creating an Analytics Bucket + +You can create an Analytics Bucket from the Dashboard or using the SDK. + +### Using the Dashboard + +1. Navigate to Storage in your Supabase Dashboard + +2. Click Create Bucket + +3. Enter a name for your bucket + +4. Select Analytics Bucket as the bucket type + +5. Click Create + +You can use the Supabase Dashboard to define columns and set data types, including complex types like decimal with precision and scale. The foreign data wrapper schema will automatically be configured for you. Once your table is created, you can manage Analytics Buckets in the same way that you manage your Postgres tables. + +### Using the SDK + +```jsx +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient('https://your-project.supabase.co', 'your-service-key') + +await supabase.storage.createBucket('my-analytics-bucket', { + type: 'ANALYTICS', +}) +``` + +## Connecting to Analytics Buckets + +Analytics Buckets require authentication with two services: + +**Iceberg REST Catalog** manages metadata for your tables. It handles schema, partitions, and snapshots. + +**S3-Compatible Storage** stores the actual data in Parquet format. + +You authenticate using your Supabase service key for the catalog and S3 credentials for storage. + +## Streaming data with Supabase ETL + +Analytics Buckets work hand in hand with [Supabase ETL](/blog/introducing-supabase-etl). ETL captures changes from your Postgres database and streams them to Analytics Buckets in near real time. + +This gives you: + +- Automatic replication of your Postgres tables + +- Near real-time data in your analytics bucket + +- Complete changelog with every insert, update, and delete + +- No manual data movement or scheduled jobs + +To set up replication, create a Postgres publication for the tables you want to replicate, then add an Analytics Buckets destination in the Replication section of the Dashboard. + +## Querying from Postgres + +You can query Analytics Buckets directly from Postgres using Foreign Data Wrappers. This lets you join hot data in Postgres with historical data in Analytics Buckets. + +```sql +-- Create a foreign server for your Iceberg data +create server iceberg_server +foreign data wrapper iceberg_wrapper +options ( + aws_access_key_id 'your-access-key', + aws_secret_access_key 'your-secret-key', + region_name 'us-east-1' +); + +-- Import tables from your analytics bucket +import foreign schema "analytics" +from server iceberg_server +into iceberg; + +-- Query historical data +select * from iceberg.events +where event_timestamp > '2024-01-01'; +``` + +## Data tiering pattern + +A common architecture is data tiering: keeping recent data in Postgres and archiving history to Analytics Buckets. + +1. Partition tables by time in Postgres, keeping a rolling window like the last 90 days + +2. Stream all data to Analytics Buckets using Supabase ETL + +3. Drop old partitions from Postgres + +4. Query recent data from Postgres, historical data from Analytics Buckets + +This keeps your Postgres database small and fast. Storage costs drop. Analytics queries run on data optimized for scans. + +## Compatible tools + +Analytics Buckets work with any tool that supports the Iceberg REST Catalog API: + +- PyIceberg + +- Apache Spark + +- DuckDB + +- Amazon Athena + +- Trino + +- Apache Flink + +- Snowflake (via external tables) + +- BigQuery (via BigLake) + +## Pricing and availability + +Analytics Buckets are free during the Private Alpha. Standard egress charges apply when you move data out of the region. + +To request access, fill out the form at [forms.supabase.com/analytics-buckets](http://forms.supabase.com/analytics-buckets). + +## Get started + +1. Request access to the Private Alpha + +2. Create an Analytics Bucket in the Dashboard + +3. Connect using PyIceberg, Spark, or your tool of choice + +4. Set up ETL to stream data from Postgres automatically + +Separate your transactional and analytical workloads. Keep Postgres fast. Store history at S3 prices. Query from any tool. + +We are excited to see what you build. diff --git a/apps/www/_blog/2025-12-02-introducing-supabase-etl.mdx b/apps/www/_blog/2025-12-02-introducing-supabase-etl.mdx new file mode 100644 index 00000000000..dec76d78f64 --- /dev/null +++ b/apps/www/_blog/2025-12-02-introducing-supabase-etl.mdx @@ -0,0 +1,165 @@ +--- +title: 'Introducing Supabase ETL' +description: 'A change-data-capture pipeline that replicates your Postgres tables to analytical destinations like Analytics Buckets and BigQuery in near real time.' +author: riccardo_busetti +image: 2025-12-02-introducing-supabase-etl/og.png +thumb: 2025-12-02-introducing-supabase-etl/thumb.png +categories: + - product + - launch-week +date: '2025-12-02:11:00' +toc_depth: 2 +--- + +Today we're introducing **Supabase ETL**: a change-data-capture pipeline that replicates your Postgres tables to analytical destinations in near real time. + +Supabase ETL reads changes from your Postgres database and writes them to external destinations. It uses logical replication to capture inserts, updates, deletes and truncates as they happen. **Setup takes minutes in the Supabase Dashboard.** + +The first supported destinations are [Analytics Buckets](/blog/introducing-analytics-buckets) (powered by Iceberg) and BigQuery. + +Supabase ETL is open source. You can find the code on GitHub at [github.com/supabase/etl](http://github.com/supabase/etl). + +## Why separate OLTP and OLAP? + +Postgres is excellent for transactional workloads like reading a single user record or inserting an order. But when you need to scan millions of rows for analytics, Postgres slows down. + +Column-oriented systems like BigQuery, or those built on open formats like Apache Iceberg, are designed for this. They can aggregate massive datasets **orders of magnitude faster**, compress data more efficiently, and handle complex analytical queries that would choke a transactional database. + +**Supabase ETL gives you the best of both worlds**: keep your app fast on Postgres while unlocking powerful analytics on purpose-built systems. + +## How it works + +Supabase ETL captures every change in your Postgres database and delivers it to your analytics destination in near real time. + +Here's how: + +1. You create a Postgres publication that defines which tables to replicate + +2. You configure ETL to connect a publication to a destination + +3. ETL reads changes from the publication through a logical replication slot + +4. Changes are batched and written to your destination + +5. Your data is available for querying in the destination + +The pipeline starts with an initial copy of your selected tables, then switches to streaming mode. **Your analytics stay fresh with latency measured in milliseconds to seconds.** + +## Setting up ETL + +You configure ETL entirely through the Supabase Dashboard. **No code required.** + +### Step 1: Create a publication + +A publication defines which tables to replicate. You create it with SQL or via the UI: + +```sql +-- Replicate specific tables +create publication analytics_pub +for table events, orders, users; + +-- Or replicate all tables in a schema +create publication analytics_pub +for tables in schema public; +``` + +### Step 2: Enable replication + +Navigate to `Database` in your Supabase Dashboard. Select the `Replication` tab and click `Enable Replication`. + +### Step 3: Add a destination + +Click `Add Destination` and choose your destination type. + +**Note:** For Analytics Buckets, you will need to create an analytics bucket first in the Storage section. + +Configure the destination with your bucket credentials and select your publication. Click Create and `Start` to begin replication. + +### Step 4: Monitor your pipeline + +The Dashboard shows pipeline status and lag. You can start, stop, restart, or delete pipelines from the actions menu. + +## Available destinations + +Our goal with Supabase ETL is to let you connect your existing data systems to Supabase. We're actively expanding the list of supported destinations. Right now, the official destinations are Analytics Buckets and BigQuery. + +### Analytics Buckets + +[Analytics Buckets](https://supabase.com/docs/guides/storage/analytics/introduction) are specialized storage buckets built on Apache Iceberg, an open table format designed for large analytical datasets. Your data is stored in Parquet files on S3. + +When you replicate to Analytics Buckets, your tables are created with a changelog structure. Each row includes a `cdc_operation` column indicating whether the change was an `INSERT`, `UPDATE`, or `DELETE`. **This append-only format preserves the complete history of all changes.** + +You can query Analytics Buckets from PyIceberg, Apache Spark, DuckDB, Amazon Athena, or any tool that supports the Iceberg REST Catalog API. + +### BigQuery + +BigQuery is Google's serverless data warehouse, built for large-scale analytics. It handles petabytes of data and integrates well with existing BI tools and data pipelines. + +When you replicate to BigQuery, Supabase ETL creates a view for each table and uses an underlying versioned table to support all operations efficiently. You query the view, and ETL handles the rest. + +## Adding and removing tables + +You can modify which tables are replicated after your pipeline is running. + +To add a table: + +```sql +alter publication analytics_pub add table products; +``` + +To remove a table: + +```sql +alter publication analytics_pub drop table orders; +``` + +After changing your publication, restart the pipeline from the Dashboard actions menu for the changes to take effect. + +**Note:** ETL does not remove data from your destination when you remove a table from a publication. This is by design to prevent accidental data loss. + +## When to use ETL vs read replicas + +Read replicas and ETL solve different problems. + +Read replicas help when you need to scale concurrent queries, but they're still Postgres. They don't make analytics faster. + +ETL moves your data to systems built for analytics. You get **faster queries on large datasets**, **lower storage costs** through compression, and **complete separation** between your production workload and analytics. + +You can use both: read replicas for application read scaling, ETL for analytics. + +## Things to know + +Replication with Supabase ETL has a few constraints to be aware of: + +- Tables must have primary keys (this is a Postgres logical replication requirement) +- Generated columns are not supported +- Custom data types are replicated as strings +- Schema changes are not automatically propagated to destinations +- Data is replicated as-is, without transformation +- During the initial copy phase, changes accumulate in the WAL and are replayed once streaming begins + +We're working on schema change support and additional destinations, and evaluating different streaming techniques to improve flexibility and performance. + +## Pricing + +Supabase ETL is usage-based: + +- **$25 per connector per month** +- **$15 per GB** of change data processed after the initial sync +- **Initial copy is free** + +## Get started + +**Supabase ETL is in private alpha.** To request access, contact your account manager or fill out the form in the Dashboard. + +If you want to dive into the code, **the ETL framework is open source** and written in Rust. Check out the repository at [github.com/supabase/etl](http://github.com/supabase/etl). + +
+