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
-
+
- + + - - - - 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, }