diff --git a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx index 922bd07e577..a4aa58474a6 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx @@ -2,7 +2,7 @@ import { Copy } from 'lucide-react' import { useCallback, useState } from 'react' import { toast } from 'sonner' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { Button } from 'ui' import { getDecryptedValue } from 'data/vault/vault-secret-decrypted-value-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { copyToClipboard } from 'ui' @@ -34,29 +34,14 @@ export const CopyEnvButton = ({ ).then((values) => values.join('\n')) copyToClipboard(envFile, () => { - toast.success('Copied to clipboard') + toast.success('Copied to clipboard as .env file') setIsLoading(false) }) }, [serverOptions, values]) return ( - } - onClick={onCopy} - tooltip={{ - content: { - text: ( - - Copies an .env file with the configuration details - to your clipboard. - - ), - }, - }} - > - Copy env values - + ) } diff --git a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx index 7b9bae83c09..ad4d675eaa4 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx @@ -4,7 +4,9 @@ import { useState } from 'react' import { useParams } from 'common' import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Button, Input, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Button, CardContent, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' export const DecryptedReadOnlyInput = ({ value, @@ -41,52 +43,64 @@ export const DecryptedReadOnlyInput = ({ : value return ( - - {label} - - - - - - - View parameter in Vault - - - } - value={renderedValue} - type={secureEntry ? (isLoading ? 'text' : showHidden ? 'text' : 'password') : 'text'} - descriptionText={descriptionText} - layout="horizontal" - actions={ - secureEntry ? ( - isLoading ? ( -
-
- ) : ( -
-
- ) - ) : null - } - /> + + + {label} + + + + + + + Open in Vault + + + } + description={descriptionText} + isReactForm={false} + > + + + + + + + + + Name + Schema + Created at + + + + + +

No tables yet

+

+ Create an analytics table to get started +

+
+
+
+
+
+ + ) : ( + + + Namespaces + + Connected namespaces and tables. + + + + {isLoadingNamespaces || isFDWsLoading ? ( + + ) : namespaces.length === 0 ? ( + + + + + Namespace + Schema + Tables + + + + + + +

+ No namespaces in this bucket +

+

+ Create a namespace and add some data +

+
+
+
+
+
+ ) : ( + + + + + Namespace + Schema + Tables + + + + + {namespaces.map(({ namespace, schema, tables }) => ( + + ))} + +
+
+ )} +
+ )} + + + +
+ Configuration + + Connect to this bucket from an Iceberg client.{' '} + + Learn more + + +
!option.hidden && wrapperValues[option.name] )} values={wrapperValues} /> - +
+ + + {wrapperMeta.server.options + .filter((option) => !option.hidden && wrapperValues[option.name]) + .sort((a, b) => OPTION_ORDER.indexOf(a.name) - OPTION_ORDER.indexOf(b.name)) + .map((option) => { + return ( + + ) + })} + +
+ + )} + {state === 'missing' && } + + +
+ Manage +
+ + +
+

Delete bucket

+

+ This will also delete any data in your bucket. Make sure you have a backup if + you want to keep your data. +

- - - {wrapperMeta.server.options - .filter((option) => !option.hidden && wrapperValues[option.name]) - .sort((a, b) => OPTION_ORDER.indexOf(a.name) - OPTION_ORDER.indexOf(b.name)) - .map((option) => { - return ( - - ) - })} - - - - )} - {state === 'missing' && } - - + +
+
+
+ + + + setModal(null)} + /> + ) } @@ -284,24 +386,22 @@ const ExtensionNotInstalled = ({ return ( <> - - - - You need to install the wrappers extension to connect this Analytics bucket to the - database. - - + +

- The {wrapperMeta.label} wrapper requires the Wrappers extension to be installed. You can - install version {wrappersExtension?.installed_version} + The Wrappers extension is required in order to query analytics tables.{' '} {databaseNeedsUpgrading && - ' which is below the minimum version that supports Iceberg wrapper'} - . Please {databaseNeedsUpgrading && 'upgrade your database then '}install the{' '} - wrappers extension to create this wrapper. + 'Please first upgrade your database and then install the extension.'}{' '} + + Learn more +

-
- - - -
+ + ) @@ -337,26 +437,19 @@ const ExtensionNeedsUpgrade = ({ return ( <> - - - - Your extension version is outdated for this wrapper. - - + +

The {wrapperMeta.label} wrapper requires a minimum extension version of{' '} {wrapperMeta.minimumExtensionVersion}. You have version{' '} {wrappersExtension?.installed_version} installed. Please{' '} - {databaseNeedsUpgrading && 'upgrade your database then '}update the extension by - disabling and enabling the wrappers extension to create - this wrapper. + {databaseNeedsUpgrading && 'first upgrade your database, and then '}update the extension + by disabling and enabling the Wrappers extension.

-

- Warning: Before reinstalling the wrapper extension, you must first remove all existing - wrappers. Afterward, you can recreate the wrappers. +

+ Before reinstalling the wrapper extension, you must first remove all existing wrappers. + Afterward, you can recreate the wrappers.

-
- - -
+ + ) @@ -385,20 +478,14 @@ const WrapperMissing = ({ bucketName }: { bucketName: string }) => { return ( <> - - - - This Analytics bucket does not have a foreign data wrapper setup. - - -

You need to setup a wrapper to connect this bucket to the database.

-
- + + +

The Iceberg Wrapper integration is required in order to query analytics tables.

-
-
+ + ) diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx index 994707ac6c7..6f5171d26be 100644 --- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx @@ -550,59 +550,61 @@ export const CreateBucketModal = ({ ) : ( <> {icebergWrapperExtensionState === 'installed' ? ( - -

- Supabase will setup a - - foreign data wrapper - {bucketName && {`${bucketName}_fdw`}} - - - {' '} - for easier access to the data. This action will also create{' '} + + +

+ Supabase will setup a - S3 Access Keys - {bucketName && ( - <> - {' '} - named {`${bucketName}_keys`} - - )} + foreign data wrapper + {bucketName && {`${bucketName}_fdw`}} - and - - four Vault Secrets - {bucketName && ( - <> - {' '} - prefixed with{' '} - {`${bucketName}_vault_`} - - )} - - . - -

-

- As a final step, you'll need to create an{' '} - Iceberg namespace before you - connect the Iceberg data to your database. -

-
+ + {' '} + for easier access to the data. This action will also create{' '} + + S3 Access Keys + {bucketName && ( + <> + {' '} + named {`${bucketName}_keys`} + + )} + + and + + four Vault Secrets + {bucketName && ( + <> + {' '} + prefixed with{' '} + {`${bucketName}_vault_`} + + )} + + . + +

+

+ As a final step, you'll need to create an{' '} + Iceberg namespace before you + connect the Iceberg data to your database. +

+ + ) : ( - + You need to install the Iceberg wrapper extension to connect your Analytic diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts b/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts index 2023b7c0446..ad340ad84d2 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts @@ -1,11 +1,19 @@ import { useParams } from 'common' import { useBucketsQuery } from 'data/storage/buckets-query' +import { useStorageV2Page } from '../Storage.utils' export const useSelectedBucket = () => { + const page = useStorageV2Page() const { ref, bucketId } = useParams() const { data: buckets = [], isSuccess, isError, error } = useBucketsQuery({ projectRef: ref }) - const bucket = buckets.find((b) => b.id === bucketId) + const bucketsByType = + page === 'files' + ? buckets.filter((b) => b.type === 'STANDARD') + : page === 'analytics' + ? buckets.filter((b) => b.type === 'ANALYTICS') + : buckets + const bucket = bucketsByType.find((b) => b.id === bucketId) return { bucket, isSuccess, isError, error } } diff --git a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx index aebbc62f7ec..17c85bcc69a 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx @@ -26,6 +26,7 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { const suffix = !!featurePreviewModal ? `?featurePreviewModal=${featurePreviewModal}` : '' if (isStorageV2) { + // From old UI to new UI if (pathname.endsWith('/storage/settings')) { router.push(`/project/${ref}/storage/files/settings${suffix}`) } else if (pathname.endsWith('/storage/policies')) { @@ -38,6 +39,7 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { } } } else { + // From new UI to old UI if (pathname.endsWith('/files/settings')) { router.push(`/project/${ref}/storage/settings${suffix}`) } else if (pathname.endsWith('/files/policies')) { @@ -47,6 +49,12 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { pathname.endsWith('/analytics/buckets/[bucketId]') ) { router.push(`/project/${ref}/storage/buckets/${bucketId}${suffix}`) + } else if ( + pathname.endsWith('/storage/files') || + pathname.endsWith('/storage/analytics') || + pathname.endsWith('/storage/vectors') + ) { + router.push(`/project/${ref}/storage/buckets`) } } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/studio/components/ui/InlineLink.tsx b/apps/studio/components/ui/InlineLink.tsx index 1d69d11e68c..d82ba4eefa0 100644 --- a/apps/studio/components/ui/InlineLink.tsx +++ b/apps/studio/components/ui/InlineLink.tsx @@ -11,7 +11,7 @@ interface InlineLinkProps { } export const InlineLinkClassName = - 'underline transition underline-offset-2 decoration-foreground-lighter hover:decoration-foreground text-foreground' + 'underline transition underline-offset-2 decoration-foreground-lighter hover:decoration-foreground text-inherit hover:text-foreground' export const InlineLink = ({ href, diff --git a/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx new file mode 100644 index 00000000000..19e8e676817 --- /dev/null +++ b/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx @@ -0,0 +1,48 @@ +import { useParams } from 'common' + +import { AnalyticBucketDetails } from 'components/interfaces/Storage/AnalyticBucketDetails' +import StorageBucketsError from 'components/interfaces/Storage/StorageBucketsError' +import { useSelectedBucket } from 'components/interfaces/Storage/StorageExplorer/useSelectedBucket' +import DefaultLayout from 'components/layouts/DefaultLayout' +import StorageLayout from 'components/layouts/StorageLayout/StorageLayout' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' +import type { NextPageWithLayout } from 'types' + +const AnalyticsBucketPage: NextPageWithLayout = () => { + const { bucketId } = useParams() + const { data: project } = useSelectedProjectQuery() + const { projectRef } = useStorageExplorerStateSnapshot() + const { bucket, error, isSuccess, isError } = useSelectedBucket() + + // [Joshen] Checking against projectRef from storage explorer to check if the store has initialized + if (!project || !projectRef) return null + + return ( +
+ {isError && } + + {isSuccess ? ( + !bucket ? ( +
+

Bucket {bucketId} cannot be found

+
+ ) : bucket.type === 'ANALYTICS' ? ( + + ) : ( +
+

This bucket is not an analytics bucket

+
+ ) + ) : null} +
+ ) +} + +AnalyticsBucketPage.getLayout = (page) => ( + + {page} + +) + +export default AnalyticsBucketPage