diff --git a/apps/design-system/README.md b/apps/design-system/README.md index 127060277616e..959e363180250 100644 --- a/apps/design-system/README.md +++ b/apps/design-system/README.md @@ -58,13 +58,13 @@ The design system _references_ components rather than housing them. That’s an With that out of the way, there are several parts of this design system that need to be manually updated after components have been added or removed (from documentation). These include: - `config/docs.ts`: list of components in the sidebar -- `content/docs`: where the actual component documentation `.mdx` file lives +- `content/docs`: the actual component documentation - `registry/examples.ts`: list of example components -- `registry/default/example`: where the actual example component(s) live -- `registry/charts.ts`: Chart components -- `registry/fragments.ts`: Fragment components +- `registry/default/example`: the actual example components +- `registry/charts.ts`: chart components +- `registry/fragments.ts`: fragment components -You will need to rebuild the design system’s registry each time a new example component is added. In other words: whenever a new file enters `registry`, it needs to be rebuilt. You can do that via: +You may need to rebuild the design system’s registry. You can do that via: ```bash cd apps/design-system diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index 005d827880186..cead6b766f8e2 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -2293,6 +2293,39 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "empty-state-missing-route": { + name: "empty-state-missing-route", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/empty-state-missing-route")), + source: "", + files: ["registry/default/example/empty-state-missing-route.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "empty-state-zero-items-table": { + name: "empty-state-zero-items-table", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/empty-state-zero-items-table")), + source: "", + files: ["registry/default/example/empty-state-zero-items-table.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "empty-state-initial-state": { + name: "empty-state-initial-state", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/empty-state-initial-state")), + source: "", + files: ["registry/default/example/empty-state-initial-state.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "chart-area-axes": { name: "chart-area-axes", type: "components:block", diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index 35407e0b4333c..ec78bcd99caff 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -6,28 +6,6 @@ interface DocsConfig { } export const docsConfig: DocsConfig = { - // mainNav: [ - // { - // title: 'Documentation', - // href: '/docs', - // }, - // { - // title: 'Components', - // href: '/docs/components/accordion', - // }, - // { - // title: 'Themes', - // href: '/themes', - // }, - // { - // title: 'Examples', - // href: '/examples', - // }, - // { - // title: 'Blocks', - // href: '/blocks', - // }, - // ], sidebarNav: [ { title: 'Getting Started', @@ -77,6 +55,11 @@ export const docsConfig: DocsConfig = { href: '/docs/ui-patterns/navigation', items: [], }, + { + title: 'Empty States', + href: '/docs/ui-patterns/empty-states', + items: [], + }, ], }, { diff --git a/apps/design-system/content/docs/ui-patterns/empty-states.mdx b/apps/design-system/content/docs/ui-patterns/empty-states.mdx new file mode 100644 index 0000000000000..876f5020a0eee --- /dev/null +++ b/apps/design-system/content/docs/ui-patterns/empty-states.mdx @@ -0,0 +1,38 @@ +--- +title: Empty states +description: Convey the absence of data and provide clear instruction for what to do about it. +--- + +At a minimum, empty states convey the fact that there is nothing to list, perform, or display on the current page. They should also provide a clear call to action for the user to take. + +## Missing route + +Users may accidentally navigate to a non-existent dynamic route, such as a non-existent bucket in [Storage](https://supabase.com/dashboard/project/_/storage) or a non-existent table in the [Table Editor](https://supabase.com/dashboard/project/_/editor). In these cases, follow the pattern of a centered [Admonition](../fragments/admonition) as shown below.. + + + +## Zero results + +Tabular information without results—or perhaps no data to begin with—should have an empty state that matches the larger presentation. + +For instance, a [Table](../components/table) may just display a single row just like it would if it had data. Dulling the TableHead text color and removing the TableCell hover state can further reinforce the lack of usable data. + + + +The treatment for other layouts, such as the list of users in [Authentication](https://supabase.com/dashboard/project/_/auth/users), should match their own general styling. + +## Initial state + +Perhaps the user has not yet created any data yet. They might be a feature for the first time. In these cases, the empty state should provide the briefest information about the lack of data, putting more focus on the value proposition and primary action. + + + +Keep in mind that this empty state will likely appear after a visual loading state. Consider layout shift and button placement during and after the transition. + +## Components + +There is not yet a shared empty state UI component. The context and needs for each placement differ enough to warrant custom components for each placement. That said, we should aim to make these as consistent as possible over time. See the below examples that might share common logic in a future centralized component. + +## External references + +- [_Empty States_ on GitHub Primer](https://primer.style/product/ui-patterns/empty-states/) diff --git a/apps/design-system/next.config.mjs b/apps/design-system/next.config.mjs index 2280b564d2924..2c3beb53dca3e 100644 --- a/apps/design-system/next.config.mjs +++ b/apps/design-system/next.config.mjs @@ -26,16 +26,27 @@ const nextConfig = { return [ ...(BASE_PATH.length ? [ - { - source: '/', - destination: BASE_PATH, - basePath: false, - permanent: false, - }, - ] + { + source: '/', + destination: BASE_PATH, + basePath: false, + permanent: false, + }, + ] : []), ] }, + // Turbopack configuration to handle .md files with raw-loader + // This mirrors the webpack configuration added by withContentlayer + // and ensures both bundlers can process content files properly + turbopack: { + rules: { + '*.md': { + loaders: ['raw-loader'], + as: '*.js', + }, + }, + }, } export default withContentlayer(nextConfig) diff --git a/apps/design-system/registry/default/example/empty-state-initial-state.tsx b/apps/design-system/registry/default/example/empty-state-initial-state.tsx new file mode 100644 index 0000000000000..0c0db65716615 --- /dev/null +++ b/apps/design-system/registry/default/example/empty-state-initial-state.tsx @@ -0,0 +1,22 @@ +import { Button } from 'ui' +import { Plus } from 'lucide-react' +import { BucketAdd } from 'icons' + +export default function EmptyStateInitialState() { + return ( + + ) +} diff --git a/apps/design-system/registry/default/example/empty-state-missing-route.tsx b/apps/design-system/registry/default/example/empty-state-missing-route.tsx new file mode 100644 index 0000000000000..fca36ca0fd0da --- /dev/null +++ b/apps/design-system/registry/default/example/empty-state-missing-route.tsx @@ -0,0 +1,22 @@ +import { Admonition } from 'ui-patterns/admonition' +import Link from 'next/link' +import { Button } from 'ui' + +const bucketId = 'user_avatars' + +export default function EmptyStateMissingRoute() { + return ( +
+ + + +
+ ) +} diff --git a/apps/design-system/registry/default/example/empty-state-zero-items-table.tsx b/apps/design-system/registry/default/example/empty-state-zero-items-table.tsx new file mode 100644 index 0000000000000..67949539b132a --- /dev/null +++ b/apps/design-system/registry/default/example/empty-state-zero-items-table.tsx @@ -0,0 +1,25 @@ +import { Card, Table, TableBody, TableHead, TableHeader, TableRow, TableCell } from 'ui' + +export default function EmptyStateZeroItemsTable() { + return ( + + + + + Table name + Date created + + + + + + +

No tables yet

+

Connect a table from your database

+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index b47de24fed577..622605d689644 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -1263,4 +1263,19 @@ export const examples: Registry = [ type: 'components:example', files: ['example/logs-bar-chart.tsx'], }, + { + name: 'empty-state-missing-route', + type: 'components:example', + files: ['example/empty-state-missing-route.tsx'], + }, + { + name: 'empty-state-zero-items-table', + type: 'components:example', + files: ['example/empty-state-zero-items-table.tsx'], + }, + { + name: 'empty-state-initial-state', + type: 'components:example', + files: ['example/empty-state-initial-state.tsx'], + }, ] diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index 023aa782b0071..fea520c5c2d26 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -443,13 +443,13 @@ export default OneTapComponent -Unlike the OAuth flow which requires the use of a web browser, the native Sign in with Google flow on Android uses the [operating system's built-in functionalities](https://developers.google.com/android/reference/com/google/android/gms/auth/api/identity/package-summary) to prompt the user for consent. Note that native sign-in has been rebranded as _One Tap sign-in on Android_ by Google, which you should not confuse with _One Tap sign in for web_, as mentioned below. +Unlike the OAuth flow which requires the use of a web browser, the native Sign in with Google flow on Android uses the [Credential Manager library](https://developer.android.com/identity/sign-in/credential-manager-siwg) to prompt the user for consent. -When the user provides consent, Google issues an identity token (commonly abbreviated as ID token) that is then sent to your project's Supabase Auth server. When valid, a new user session is started by issuing an access and refresh token from Supabase Auth. +When the user provides consent, Google issues an identity token (commonly abbreviated as ID token) that you then send to your project's Supabase Auth server. When valid, a new user session is started by issuing an access and refresh token from Supabase Auth. By default, Supabase Auth implements nonce validation during the authentication flow. This can be disabled in production under `Authentication > Providers > Google > Skip Nonce Check` in the Dashboard, or when developing locally by setting `auth.external..skip_nonce_check`. Only disable this if your client libraries cannot properly handle nonce verification. -When working with Expo, you can use the [`react-native-google-signin/google-signin` library](https://github.com/react-native-google-signin/google-signin#expo-installation) library to obtain an ID token that you can pass to supabase-js [`signInWithIdToken` method](/docs/reference/javascript/auth-signinwithidtoken). +When working with Expo, you can use the [`@react-native-google-signin/google-signin` library](https://github.com/react-native-google-signin/google-signin#expo-installation) to obtain an ID token that you can pass to supabase-js [`signInWithIdToken` method](/docs/reference/javascript/auth-signinwithidtoken). Follow the [Expo installation docs](https://react-native-google-signin.github.io/docs/setting-up/expo) for installation and configuration instructions. See the [supabase-js reference](/docs/reference/javascript/initializing?example=react-native-options-async-storage) for instructions on initializing the supabase-js client in React Native. @@ -463,7 +463,6 @@ import { supabase } from '../utils/supabase' export default function () { GoogleSignin.configure({ - scopes: ['https://www.googleapis.com/auth/drive.readonly'], webClientId: 'YOUR CLIENT ID FROM GOOGLE CONSOLE', }) @@ -474,20 +473,16 @@ export default function () { onPress={async () => { try { await GoogleSignin.hasPlayServices() - const userInfo = await GoogleSignin.signIn() - if (userInfo.data.idToken) { + const response = await GoogleSignin.signIn() + if (isSuccessResponse(response)) { const { data, error } = await supabase.auth.signInWithIdToken({ provider: 'google', - token: userInfo.data.idToken, + token: response.data.idToken, }) console.log(error, data) - } else { - throw new Error('no ID token present!') } } catch (error: any) { - if (error.code === statusCodes.SIGN_IN_CANCELLED) { - // user cancelled the login flow - } else if (error.code === statusCodes.IN_PROGRESS) { + if (error.code === statusCodes.IN_PROGRESS) { // operation (e.g. sign in) is in progress already } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) { // play services not available or outdated diff --git a/apps/docs/content/guides/platform/regions.mdx b/apps/docs/content/guides/platform/regions.mdx index 594e3f80cbf23..fc46be89e6bf5 100644 --- a/apps/docs/content/guides/platform/regions.mdx +++ b/apps/docs/content/guides/platform/regions.mdx @@ -1,18 +1,19 @@ --- title: Available regions -subtitle: Spin up Supabase projects in our global regions --- -The following regions are available for your Supabase projects. +Each Supabase project is deployed to one primary region. Choose the location closest to your users for the best performance. -## Smart region selection +## General regions -Smart Region Selection is a feature that allows you to select from a list of broader regions. Optimizations are made to ensure that the selected region has compute capacity availability. +For most projects, we recommend choosing a general region. Supabase will deploy your project to an available AWS region within that area based on current infrastructure capacity. -Currently, smart region selection is not available for read replicas and project-related operations via the management API. +Note: General regions aren’t yet supported for read replicas or management via the API. -## AWS +## Specific regions + +If you prefer, you can choose an exact AWS region for your project. diff --git a/apps/docs/content/troubleshooting/edge-function-shutdown-reasons-explained.mdx b/apps/docs/content/troubleshooting/edge-function-shutdown-reasons-explained.mdx index 67816d99b72ba..9b7a4a16cf670 100644 --- a/apps/docs/content/troubleshooting/edge-function-shutdown-reasons-explained.mdx +++ b/apps/docs/content/troubleshooting/edge-function-shutdown-reasons-explained.mdx @@ -2,7 +2,6 @@ title = "Edge Function shutdown reasons explained" topics = [ "functions" ] keywords = [ "shutdown", "termination", "event loop", "wall clock", "cpu time", "memory", "early drop" ] -database_id = "" [[errors]] http_status_code = 546 diff --git a/apps/docs/content/troubleshooting/email-template-not-updating.mdx b/apps/docs/content/troubleshooting/email-template-not-updating.mdx new file mode 100644 index 0000000000000..c0929a289d42a --- /dev/null +++ b/apps/docs/content/troubleshooting/email-template-not-updating.mdx @@ -0,0 +1,31 @@ +--- +title = "Email template not updating" +topics = [ "auth", "studio" ] +keywords = [ "email", "template" ] + +[[errors]] +code = "templatemailer_template_body_parse_error" +--- + +If your email templates are not updated, check [Auth Logs](/dashboard/project/_/logs/auth-logs) for any errors when the email is sent. When there are errors, we will use a fallback template with valid syntax so it will not show any of your customizations. + +Typical errors relate to invalid syntax involving variables. Refer to [the Email Templates docs](/docs/guides/auth/auth-email-templates#terminology) for the supported variables and syntax. + +**Example error message** + +``` +"event": "templatemailer_template_body_parse_error", +"msg": "templatemailer: template type \"recovery\": template: https://api.supabase.com/platform/auth/PROJECT_ID/templates/recovery:2: function \"default\" not defined", +``` + +The error shows an issue with `default` which is used in the invalid syntax below. + +``` +{{ .Data.display_name | default: "test" }} +``` + +The final correct syntax after removing invalid syntax. + +``` +{{ .Data.display_name }} +``` diff --git a/apps/docs/docs/ref/api/api.mdx b/apps/docs/docs/ref/api/api.mdx index 74db1446683d1..c1764683dd31a 100644 --- a/apps/docs/docs/ref/api/api.mdx +++ b/apps/docs/docs/ref/api/api.mdx @@ -23,7 +23,7 @@ All API requests must be authenticated and made over HTTPS. ## Rate limits -The rate limit for Management API is 60 requests per one minute per user, and applies cumulatively across all requests made with your personal access tokens. +The rate limit for Management API is 120 requests per one minute per user, and applies cumulatively across all requests made with your personal access tokens. If you exceed this limit, all Management API calls for the next minute will be blocked, resulting in a HTTP 429 response. diff --git a/apps/docs/docs/ref/api/introduction.mdx b/apps/docs/docs/ref/api/introduction.mdx index a5cd9b0057e44..638f39b6d53f1 100644 --- a/apps/docs/docs/ref/api/introduction.mdx +++ b/apps/docs/docs/ref/api/introduction.mdx @@ -40,7 +40,7 @@ hideTitle: true ## Rate limits - The rate limit for Management API is 60 requests per one minute per user, and applies cumulatively across all requests made with your personal access tokens. + The rate limit for Management API is 120 requests per one minute per user, and applies cumulatively across all requests made with your personal access tokens. If you exceed this limit, all Management API calls for the next minute will be blocked, resulting in a HTTP 429 response. diff --git a/apps/docs/docs/ref/self-hosting-analytics/introduction.mdx b/apps/docs/docs/ref/self-hosting-analytics/introduction.mdx index 45db31b53e095..cbe7d39d8d813 100644 --- a/apps/docs/docs/ref/self-hosting-analytics/introduction.mdx +++ b/apps/docs/docs/ref/self-hosting-analytics/introduction.mdx @@ -173,6 +173,14 @@ The current Postgres Ingestion backend isn't optimized for production usage. We **We recommend using the BigQuery backend for production environments as it offers better scaling and querying/debugging experiences.** +### Rotate Encryption Keys Regularly + +The Logflare server uses the a Base64 encryption key set on the `LOGFLARE_DB_ENCRYPTION_KEY` environment variable to perform encryption at rest for sensitive database columns. + +To perform encryption key rotation, move the retired key to the `LOGFLARE_DB_ENCRYPTION_KEY_RETIRED` environment variable, and replace the `LOGFLARE_DB_ENCRYPTION_KEY` environement variable with the new key. Perform a server restart and check `info` logs for the migration to be detected and performed. + +Once migration is complete, you can safely remove the retired key. + diff --git a/apps/docs/features/docs/Troubleshooting.script.mjs b/apps/docs/features/docs/Troubleshooting.script.mjs index e3cd431c50833..ff6e9eda6c785 100644 --- a/apps/docs/features/docs/Troubleshooting.script.mjs +++ b/apps/docs/features/docs/Troubleshooting.script.mjs @@ -258,37 +258,37 @@ async function updateChecksumIfNeeded(entry) { /** * Converts relative links to absolute URLs for GitHub discussions using MDAST - * @param {string} content - The markdown content to process + * @param {string} content - The markdown content to process (already stripped of JSX) */ function rewriteRelativeLinks(content) { const baseUrl = 'https://supabase.com' - + // Parse the markdown to AST const mdast = fromMarkdown(content, { - extensions: [gfm(), mdxjs()], - mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()], + extensions: [gfm()], + mdastExtensions: [gfmFromMarkdown()], }) - + // Walk the tree and modify link nodes - /** - * @param {import('mdast').Root|import('mdast').Content} node - */ + /** + * @param {import('mdast').Root|import('mdast').Content} node + */ function visitNode(node) { if (node.type === 'link' && node.url && node.url.startsWith('/')) { // Convert relative URL to absolute node.url = `${baseUrl}${node.url}` } - + // Recursively visit children if ('children' in node) { node.children.forEach(visitNode) } } - + visitNode(mdast) - + // Convert back to markdown - return toMarkdown(mdast, { extensions: [gfmToMarkdown(), mdxToMarkdown()] }) + return toMarkdown(mdast, { extensions: [gfmToMarkdown()] }) } /** diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx index 03d791ca8914e..ce8d859e7852c 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx @@ -8,13 +8,6 @@ export const FEATURE_PREVIEWS = [ isNew: true, isPlatformOnly: true, }, - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_NEW_STORAGE_UI, - name: 'New Storage interface', - discussionsUrl: undefined, - isNew: true, - isPlatformOnly: false, - }, { key: LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, name: 'New Logs interface', diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index bb2b449c4d512..248d731e5a705 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -110,11 +110,6 @@ export const useIsAdvisorRulesEnabled = () => { return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES] } -export const useIsNewStorageUIEnabled = () => { - const { flags } = useFeaturePreviewContext() - return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_NEW_STORAGE_UI] -} - export const useIsSecurityNotificationsEnabled = () => { const { flags } = useFeaturePreviewContext() return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SECURITY_NOTIFICATIONS] @@ -126,7 +121,6 @@ export const useFeaturePreviewModal = () => { const gitlessBranchingEnabled = useFlag('gitlessBranching') const advisorRulesEnabled = useFlag('advisorRules') const isUnifiedLogsPreviewAvailable = useFlag('unifiedLogs') - const isNewStorageUIAvailable = useFlag('storageAnalyticsVector') const isSecurityNotificationsAvailable = useFlag('securityNotifications') const selectedFeatureKeyFromQuery = featurePreviewModal?.trim() ?? null @@ -142,8 +136,6 @@ export const useFeaturePreviewModal = () => { return advisorRulesEnabled case 'supabase-ui-preview-unified-logs': return isUnifiedLogsPreviewAvailable - case 'new-storage-ui': - return isNewStorageUIAvailable case 'security-notifications': return isSecurityNotificationsAvailable default: @@ -154,7 +146,6 @@ export const useFeaturePreviewModal = () => { gitlessBranchingEnabled, advisorRulesEnabled, isUnifiedLogsPreviewAvailable, - isNewStorageUIAvailable, isSecurityNotificationsAvailable, ] ) diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 3fa262e4b123e..524e7cd28e5af 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -14,9 +14,8 @@ import { CLSPreview } from './CLSPreview' import { FEATURE_PREVIEWS } from './FeaturePreview.constants' import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext' import { InlineEditorPreview } from './InlineEditorPreview' -import { NewStorageUIPreview } from './NewStorageUIPreview' -import { UnifiedLogsPreview } from './UnifiedLogsPreview' import { SecurityNotificationsPreview } from './SecurityNotificationsPreview' +import { UnifiedLogsPreview } from './UnifiedLogsPreview' const FEATURE_PREVIEW_KEY_TO_CONTENT: { [key: string]: ReactNode @@ -27,7 +26,6 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: { [LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]: , - [LOCAL_STORAGE_KEYS.UI_PREVIEW_NEW_STORAGE_UI]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_SECURITY_NOTIFICATIONS]: , } diff --git a/apps/studio/components/interfaces/App/FeaturePreview/NewStorageUIPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/NewStorageUIPreview.tsx deleted file mode 100644 index 6225b04d5237c..0000000000000 --- a/apps/studio/components/interfaces/App/FeaturePreview/NewStorageUIPreview.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import Image from 'next/image' - -import { useParams } from 'common' -import { InlineLink } from 'components/ui/InlineLink' -import { BASE_PATH } from 'lib/constants' -import { useIsNewStorageUIEnabled } from './FeaturePreviewContext' - -export const NewStorageUIPreview = () => { - const { ref } = useParams() - const isStorageV2 = useIsNewStorageUIEnabled() - - return ( -
-

- Experience our enhanced{' '} - - Storage interface - {' '} - with support for analytics and vector bucket types. -

- new-storage-preview -
-

Enabling this preview will:

-
    -
  • Move Storage buckets from the sidebar into the main content area
  • -
  • Change the role of the sidebar to a bucket type selector
  • -
  • Nest settings and policies under their respective bucket types
  • -
-

- These changes are necessary to support incoming analytics and vector bucket types. File - storage will remain the default, and be shown by default when entering Storage. -

-
-
- ) -} diff --git a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx index 6b8c96bee7d86..55285d3368d81 100644 --- a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx +++ b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx @@ -6,7 +6,7 @@ import { PropsWithChildren, ReactNode } from 'react' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import type { Branch } from 'data/branches/branches-query' -import { BASE_PATH } from 'lib/constants' +import Link from 'next/link' import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { WorkflowLogs } from './WorkflowLogs' @@ -25,9 +25,7 @@ export const BranchManagementSection = ({
{typeof header === 'string' ? {header} : header}
-
- {children} -
+
{children}
{footer !== undefined &&
{footer}
} ) @@ -86,14 +84,6 @@ export const BranchRow = ({ const navigateUrl = rowLink ?? `/project/${branch.project_ref}` - const handleRowClick = () => { - if (external) { - window.open(`${BASE_PATH}/${navigateUrl}`, '_blank', 'noopener noreferrer') - } else { - router.push(navigateUrl) - } - } - return (
@@ -114,10 +104,15 @@ export const BranchRow = ({ )} - -
+ + {label || branch.name} -
+
{((page === 'branches' && !branch.is_default) || page === 'merge-requests') && ( diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx index 283c180190b69..54f871f94b224 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx @@ -3,7 +3,6 @@ import { useMemo, useState } from 'react' import type { UseFormReturn } from 'react-hook-form' import { useParams } from 'common' -import { useIsNewStorageUIEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' 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' @@ -124,7 +123,6 @@ export const AnalyticsBucketFields = ({ const [showSecretAccessKey, setShowSecretAccessKey] = useState(false) const { ref: projectRef } = useParams() - const isStorageV2 = useIsNewStorageUIEnabled() const { data: project } = useSelectedProjectQuery() const { data: apiKeys } = useAPIKeysQuery({ projectRef, reveal: true }) @@ -447,13 +445,7 @@ export const AnalyticsBucketFields = ({

Please select another key or create a new set, as this destination will not work otherwise. S3 access keys can be managed in your{' '} - + storage settings

@@ -466,13 +458,7 @@ export const AnalyticsBucketFields = ({

S3 access keys can be managed in your{' '} - + storage settings . diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx index d7578686bc49c..e793ecfc5a948 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx @@ -38,6 +38,14 @@ interface RegionSelectorProps { // I tried using https://flagpack.xyz/docs/development/react/ but couldn't get it to render // ^ can try again next time +// Map backend region names to user-friendly display names +const getDisplayNameForSmartRegion = (name: string): string => { + if (name === 'APAC') { + return 'Asia-Pacific' + } + return name +} + export const RegionSelector = ({ form, instanceSize, @@ -149,7 +157,9 @@ export const RegionSelector = ({ /> )} - {selectedRegion?.name ?? field.value} + {selectedRegion?.name + ? getDisplayNameForSmartRegion(selectedRegion.name) + : field.value}

)} @@ -159,7 +169,7 @@ export const RegionSelector = ({ {smartRegionEnabled && ( <> - Smart Region Selection + General regions {smartRegions.map((value) => { return ( - {value.name} + + {getDisplayNameForSmartRegion(value.name)} +
@@ -194,7 +206,7 @@ export const RegionSelector = ({ )} - All Regions + Specific regions {regionOptions.map((value) => { return ( { const { mutate: sendEvent } = useSendEventMutation() const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled() - const isStorageV2 = useIsNewStorageUIEnabled() const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview() const activeRoute = router.pathname.split('/')[3] @@ -257,7 +254,6 @@ const ProjectLinks = () => { storage: storageEnabled, realtime: realtimeEnabled, authOverviewPage: authOverviewPageEnabled, - isStorageV2, }) const otherRoutes = generateOtherRoutes(ref, project, { unifiedLogs: isUnifiedLogsEnabled, diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBucketDetails/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBucketDetails/index.tsx index 5685cc1affe00..de7b2a1d9c0be 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBucketDetails/index.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBucketDetails/index.tsx @@ -3,7 +3,6 @@ import { SquarePlus } from 'lucide-react' import Link from 'next/link' import { useMemo, useState } from 'react' -import { useIsNewStorageUIEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants' import { WrapperMeta } from 'components/interfaces/Integrations/Wrappers/Wrappers.types' import { @@ -31,17 +30,7 @@ import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-cr import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' -import { - Button, - Card, - CardContent, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from 'ui' +import { Button, Card, CardContent } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { DeleteBucketModal } from '../DeleteBucketModal' @@ -49,7 +38,6 @@ import { ConnectTablesDialog } from './ConnectTablesDialog' import { DESCRIPTIONS, LABELS, OPTION_ORDER } from './constants' import { CopyEnvButton } from './CopyEnvButton' import { DecryptedReadOnlyInput } from './DecryptedReadOnlyInput' -import { NamespaceRow } from './NamespaceRow' import { NamespaceWithTables } from './NamespaceWithTables' import { SimpleConfigurationDetails } from './SimpleConfigurationDetails' import { useAnalyticsBucketWrapperInstance } from './useAnalyticsBucketWrapperInstance' @@ -58,7 +46,6 @@ import { useIcebergWrapperExtension } from './useIcebergWrapper' export const AnalyticBucketDetails = ({ bucket }: { bucket: AnalyticsBucket }) => { const config = BUCKET_TYPES.analytics const [modal, setModal] = useState<'delete' | null>(null) - const isStorageV2 = useIsNewStorageUIEnabled() const { data: project } = useSelectedProjectQuery() const { state: extensionState } = useIcebergWrapperExtension() @@ -128,16 +115,12 @@ export const AnalyticBucketDetails = ({ bucket }: { bucket: AnalyticsBucket }) = <> ] : []} > @@ -165,117 +148,49 @@ export const AnalyticBucketDetails = ({ bucket }: { bucket: AnalyticsBucket }) = {state === 'added' && wrapperInstance && ( <> - {isStorageV2 ? ( - - -
- Tables - - Analytics tables stored in this bucket - -
- {namespaces.length > 0 && } -
- - {isLoadingNamespaces || isLoading ? ( - - ) : namespaces.length === 0 ? ( - - ) : ( -
- {namespaces.map(({ namespace, schema, tables }) => ( - - ))} -
- )} -
- ) : ( - - - Namespaces + + +
+ Tables - Connected namespaces and tables. + Analytics tables stored in this bucket - +
+ {namespaces.length > 0 && } +
- {isLoadingNamespaces || isLoading ? ( - - ) : 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 }) => ( - - ))} - -
-
- )} -
- )} + {isLoadingNamespaces || isLoading ? ( + + ) : namespaces.length === 0 ? ( + + ) : ( +
+ {namespaces.map(({ namespace, schema, tables }) => ( + + ))} +
+ )} +
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBucketDetails/useAnalyticsBucketAssociatedEntities.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBucketDetails/useAnalyticsBucketAssociatedEntities.tsx index e75b00d6117b1..8aa21db79fbd8 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBucketDetails/useAnalyticsBucketAssociatedEntities.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBucketDetails/useAnalyticsBucketAssociatedEntities.tsx @@ -1,6 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useIsNewStorageUIEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { WrapperMeta } from 'components/interfaces/Integrations/Wrappers/Wrappers.types' import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation' import { FDW } from 'data/fdw/fdws-query' @@ -26,10 +25,6 @@ export const useAnalyticsBucketAssociatedEntities = ( { projectRef, bucketId }: { projectRef?: string; bucketId: string }, options: { enabled: boolean } = { enabled: true } ) => { - // [Joshen] Opting to skip cleaning up ETL related entities within old UI - // Also to prevent an unnecessary call to /sources for existing UI - const isStorageV2 = useIsNewStorageUIEnabled() - const { can: canReadS3Credentials } = useAsyncCheckPermissions( PermissionAction.STORAGE_ADMIN_READ, '*' @@ -50,7 +45,7 @@ export const useAnalyticsBucketAssociatedEntities = ( const { data: sourcesData } = useReplicationSourcesQuery( { projectRef }, - { enabled: isStorageV2 && options.enabled } + { enabled: options.enabled } ) const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets.tsx index 84abf828ce690..b95c0daba94d8 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets.tsx @@ -1,6 +1,5 @@ -import { MoreVertical, Search, Trash2 } from 'lucide-react' +import { ExternalLink, MoreVertical, Search, Trash2 } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/router' import { useState } from 'react' import { useParams } from 'common' @@ -21,14 +20,13 @@ import { TableHeader, TableRow, } from 'ui' -import { TimestampInfo } from 'ui-patterns' +import { Admonition, TimestampInfo } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' import { CreateSpecializedBucketModal } from './CreateSpecializedBucketModal' import { DeleteBucketModal } from './DeleteBucketModal' import { EmptyBucketState } from './EmptyBucketState' export const AnalyticsBuckets = () => { - const router = useRouter() const { ref } = useParams() const [filterString, setFilterString] = useState('') @@ -44,14 +42,36 @@ export const AnalyticsBuckets = () => { ) return ( - <> + + }> + + Leave feedback + + + } + > +

+ Expect rapid changes, limited features, and possible breaking updates as we expand access. +

+

Please share feedback as we refine the experience!

+
+ {!isLoadingBuckets && buckets.filter((bucket) => !('type' in bucket) || bucket.type === 'ANALYTICS').length === 0 ? ( ) : ( - // Override the default first:pt-12 to match other storage types - +
Buckets @@ -143,7 +163,7 @@ export const AnalyticsBuckets = () => { )} - +
)} {selectedBucket && ( @@ -153,6 +173,6 @@ export const AnalyticsBuckets = () => { onClose={() => setModal(null)} /> )} - +
) } diff --git a/apps/studio/components/interfaces/Storage/BucketsComingSoon.tsx b/apps/studio/components/interfaces/Storage/BucketsComingSoon.tsx new file mode 100644 index 0000000000000..30416d60e81ba --- /dev/null +++ b/apps/studio/components/interfaces/Storage/BucketsComingSoon.tsx @@ -0,0 +1,36 @@ +import { ScaffoldSection } from 'components/layouts/Scaffold' +import { Bucket } from 'icons' +import { ExternalLink } from 'lucide-react' +import Link from 'next/link' +import { Button } from 'ui' + +export const BucketsComingSoon = ({ type }: { type: 'analytics' | 'vector' }) => { + return ( + + + + ) +} diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx index c9579551809b8..f816ff023922e 100644 --- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx @@ -1,30 +1,22 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { snakeCase } from 'lodash' import { Plus } from 'lucide-react' -import { useRouter } from 'next/router' import { useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import z from 'zod' import { useParams } from 'common' -import { useIcebergWrapperExtension } from 'components/interfaces/Storage/AnalyticsBucketDetails/useIcebergWrapper' 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 { useAnalyticsBucketCreateMutation } from 'data/storage/analytics-bucket-create-mutation' import { useBucketCreateMutation } from 'data/storage/bucket-create-mutation' -import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-create-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { BASE_PATH, IS_PLATFORM } from 'lib/constants' +import { IS_PLATFORM } from 'lib/constants' import { - Alert_Shadcn_, - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, Button, Dialog, DialogContent, @@ -39,22 +31,16 @@ import { FormField_Shadcn_, FormMessage_Shadcn_, Input_Shadcn_, - Label_Shadcn_, - RadioGroupStacked, - RadioGroupStackedItem, Select_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, Switch, - WarningIcon, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { useIsNewStorageUIEnabled } from '../App/FeaturePreview/FeaturePreviewContext' import { inverseValidBucketNameRegex, validBucketNameRegex } from './CreateBucketModal.utils' -import { BUCKET_TYPES } from './Storage.constants' import { convertFromBytes, convertToBytes } from './StorageSettings/StorageSettings.utils' const FormSchema = z @@ -72,7 +58,6 @@ const FormSchema = z (value) => value !== 'public', '"public" is a reserved name. Please choose another name' ), - type: z.enum(['STANDARD', 'ANALYTICS']).default('STANDARD'), public: z.boolean().default(false), has_file_size_limit: z.boolean().default(false), formatted_size_limit: z.coerce @@ -111,66 +96,42 @@ export const CreateBucketModal = ({ buttonClassName, label = 'New bucket', }: CreateBucketModalProps) => { - const router = useRouter() const { ref } = useParams() const { data: org } = useSelectedOrganizationQuery() - const isStorageV2 = useIsNewStorageUIEnabled() const [visible, setVisible] = useState(false) 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}` + const { mutate: sendEvent } = useSendEventMutation() const { mutateAsync: createBucket, isLoading: isCreatingBucket } = useBucketCreateMutation({ // [Joshen] Silencing the error here as it's being handled in onSubmit onError: () => {}, }) - const { mutateAsync: createAnalyticsBucket, isLoading: isCreatingAnalyticsBucket } = - useAnalyticsBucketCreateMutation({ - // [Joshen] Silencing the error here as it's being handled in onSubmit - onError: () => {}, - }) - const { mutateAsync: createIcebergWrapper, isLoading: isCreatingIcebergWrapper } = - useIcebergWrapperCreateMutation() - - const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM }) - const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0) - const formattedGlobalUploadLimit = `${value} ${unit}` - - const config = BUCKET_TYPES['files'] - const isCreating = isCreatingBucket || isCreatingAnalyticsBucket const form = useForm({ resolver: zodResolver(FormSchema), defaultValues: { name: '', public: false, - type: 'STANDARD', has_file_size_limit: false, formatted_size_limit: undefined, allowed_mime_types: '', }, }) const { formatted_size_limit: formattedSizeLimitError } = form.formState.errors - - const bucketName = snakeCase(form.watch('name')) const isPublicBucket = form.watch('public') - const isStandardBucket = form.watch('type') === 'STANDARD' const hasFileSizeLimit = form.watch('has_file_size_limit') - const [hasAllowedMimeTypes, setHasAllowedMimeTypes] = useState(false) - const { state: icebergWrapperExtensionState } = useIcebergWrapperExtension() - const icebergCatalogEnabled = data?.features?.icebergCatalog?.enabled const onSubmit: SubmitHandler = async (values) => { if (!ref) return console.error('Project ref is required') - if (values.type === 'ANALYTICS' && !icebergCatalogEnabled) { - return toast.error( - 'The Analytics catalog feature is not enabled for your project. Please contact support to enable it.' - ) - } - // [Joshen] Should shift this into superRefine in the form schema try { const fileSizeLimit = @@ -190,37 +151,24 @@ export const CreateBucketModal = ({ }) } - if (values.type === 'STANDARD') { - await createBucket({ - projectRef: ref, - id: values.name, - type: 'STANDARD', - isPublic: values.public, - file_size_limit: fileSizeLimit, - allowed_mime_types: allowedMimeTypes, - }) - } else if (values.type === 'ANALYTICS') { - await createAnalyticsBucket({ - projectRef: ref, - bucketName: values.name, - }) - if (icebergWrapperExtensionState === 'installed') { - await createIcebergWrapper({ bucketName: values.name }) - } - } - + await createBucket({ + projectRef: ref, + id: values.name, + type: 'STANDARD', + isPublic: values.public, + file_size_limit: fileSizeLimit, + allowed_mime_types: allowedMimeTypes, + }) sendEvent({ action: 'storage_bucket_created', - properties: { bucketType: values.type }, + properties: { bucketType: 'STANDARD' }, groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) toast.success(`Successfully created bucket ${values.name}`) form.reset() - setSelectedUnit(StorageSizeUnits.MB) setVisible(false) - if (!isStorageV2) router.push(`/project/${ref}/storage/buckets/${values.name}`) } catch (error: any) { // Handle specific error cases for inline display const errorMessage = error.message?.toLowerCase() || '' @@ -281,7 +229,7 @@ export const CreateBucketModal = ({ - Create a {isStorageV2 ? config.singularName : 'storage'} bucket + Create a storage bucket @@ -313,344 +261,197 @@ export const CreateBucketModal = ({ )} /> + - {!isStorageV2 && ( - ( - - - field.onChange(v)} - > - - {IS_PLATFORM && ( - - <> -

- Stores Iceberg files and is optimized for analytical workloads. -

- - {icebergCatalogEnabled ? null : ( -
- - - This feature is currently in alpha and not yet enabled for - your project. Sign up{' '} - - here - - . - -
- )} - -
- )} -
-
-
- )} + + + + ( + + + + + + )} + /> + {isPublicBucket && ( + )} - {isStandardBucket ? ( - <> - + + ( + + + + + + )} + /> + + {hasFileSizeLimit && ( +
( - - - +
+
+ + + +
+
+ + + {selectedUnit} + + + {Object.values(StorageSizeUnits).map((unit: string) => ( + + {unit} + + ))} + + +
+
)} /> - {isPublicBucket && ( - + {formattedSizeLimitError?.message === 'exceed_global_limit' && ( + + Exceeds global limit of {formattedGlobalUploadLimit}. Increase limit in{' '} + setVisible(false)} + > + Storage Settings + {' '} + first. + )} - - - - - ( - + This project has a{' '} + setVisible(false)} > - - - - - )} - /> - - {hasFileSizeLimit && ( -
- ( - -
-
- - - -
-
- - - {selectedUnit} - - - {Object.values(StorageSizeUnits).map((unit: string) => ( - - {unit} - - ))} - - -
-
-
- )} - /> - {formattedSizeLimitError?.message === 'exceed_global_limit' && ( - - Exceeds global limit of {formattedGlobalUploadLimit}. Increase limit in{' '} - setVisible(false)} - > - Storage Settings - {' '} - first. - - )} - - {IS_PLATFORM && ( -

- This project has a{' '} - setVisible(false)} - > - global file size limit - {' '} - of {formattedGlobalUploadLimit}. -

- )} -
+ global file size limit + {' '} + of {formattedGlobalUploadLimit}. +

)} -
+
+ )} +
- + - - - - - - - {hasAllowedMimeTypes && ( - + + + + + + {hasAllowedMimeTypes && ( + ( + ( - - - - - - )} - /> + label="Allowed MIME types" + labelOptional="Comma separated values" + description="Wildcards are allowed, e.g. image/*." + > + + + + )} - - - ) : ( - <> - {icebergWrapperExtensionState === 'installed' ? ( - - -

- Supabase will setup a - - foreign data wrapper - {bucketName && {`${bucketName}_fdw`}} - - - {' '} - 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 - bucket to your database. - - -

- You need to install the wrappers{' '} - extension (with the minimum version of 0.5.3) if you want to - connect your Analytics bucket to your database. -

-
-
- )} - - )} + /> + )} +
- diff --git a/apps/studio/components/interfaces/Storage/CreateSpecializedBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateSpecializedBucketModal.tsx index 3208dfedd624d..7783ebe2a57c8 100644 --- a/apps/studio/components/interfaces/Storage/CreateSpecializedBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/CreateSpecializedBucketModal.tsx @@ -10,7 +10,7 @@ import z from 'zod' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { InlineLink } from 'components/ui/InlineLink' -import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' +import { useIsAnalyticsBucketsEnabled } from 'data/config/project-storage-config-query' import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation' import { useAnalyticsBucketCreateMutation } from 'data/storage/analytics-bucket-create-mutation' import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-create-mutation' @@ -18,7 +18,7 @@ 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, IS_PLATFORM } from 'lib/constants' +import { DOCS_URL } from 'lib/constants' import { Button, cn, @@ -106,8 +106,8 @@ export const CreateSpecializedBucketModal = ({ const [visible, setVisible] = useState(false) - const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM }) - const icebergCatalogEnabled = data?.features?.icebergCatalog?.enabled + const icebergCatalogEnabled = useIsAnalyticsBucketsEnabled({ projectRef: ref }) + const wrappersExtenstionNeedsUpgrading = wrappersExtensionState === 'needs-upgrade' const { mutate: sendEvent } = useSendEventMutation() @@ -136,21 +136,6 @@ export const CreateSpecializedBucketModal = ({ if (!project) return console.error('Project details is required') if (!wrappersExtension) return console.error('Unable to find wrappers extension') - if (wrappersExtensionState === 'needs-upgrade') { - // [Joshen] Double check if this is the right CTA - return toast.error( -

- Wrappers extensions needs to be updated to create an Iceberg Wrapper. Update the extension - by disabling and enabling the wrappers extension first in - the{' '} - - database extensions page - {' '} - before creating an Analytics bucket. -

- ) - } - try { if (bucketType === 'analytics') { await createAnalyticsBucket({ @@ -227,7 +212,7 @@ export const CreateSpecializedBucketModal = ({ - + Create {config.singularName} bucket @@ -264,17 +249,43 @@ export const CreateSpecializedBucketModal = ({ /> {bucketType === 'analytics' && ( - -

- Supabase will install the{' '} - {wrappersExtensionState !== 'installed' ? 'Wrappers extension and ' : ''} - Iceberg Wrapper integration on your behalf.{' '} - - Learn more - - . -

-
+ <> + {wrappersExtenstionNeedsUpgrading ? ( + +

+ Update the wrappers extension by disabling + and enabling it in{' '} + + database extensions + {' '} + before creating an Analytics bucket.{' '} + + Learn more + + . +

+
+ ) : ( + +

+ Supabase will install the{' '} + {wrappersExtensionState !== 'installed' ? 'Wrappers extension and ' : ''} + Iceberg Wrapper integration on your behalf.{' '} + + Learn more + + . +

+
+ )} + )} @@ -284,8 +295,13 @@ export const CreateSpecializedBucketModal = ({ -
diff --git a/apps/studio/components/interfaces/Storage/DeleteBucketModal.tsx b/apps/studio/components/interfaces/Storage/DeleteBucketModal.tsx index 495981485df2f..d4a723180b757 100644 --- a/apps/studio/components/interfaces/Storage/DeleteBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/DeleteBucketModal.tsx @@ -29,7 +29,6 @@ import { } from 'ui' import { Admonition } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { useIsNewStorageUIEnabled } from '../App/FeaturePreview/FeaturePreviewContext' import { useAnalyticsBucketAssociatedEntities, useAnalyticsBucketDeleteCleanUp, @@ -46,9 +45,8 @@ const formId = `delete-storage-bucket-form` export const DeleteBucketModal = ({ visible, bucket, onClose }: DeleteBucketModalProps) => { const router = useRouter() - const { ref: projectRef } = useParams() + const { ref: projectRef, bucketId } = useParams() const { data: project } = useSelectedProjectQuery() - const isStorageV2 = useIsNewStorageUIEnabled() const isStandardBucketSelected = 'type' in bucket && bucket.type === 'STANDARD' @@ -107,11 +105,7 @@ export const DeleteBucketModal = ({ visible, bucket, onClose }: DeleteBucketModa ) toast.success(`Successfully deleted bucket ${bucket.id}`) - if (isStorageV2) { - router.push(`/project/${projectRef}/storage/files`) - } else { - router.push(`/project/${projectRef}/storage/buckets`) - } + if (!!bucketId) router.push(`/project/${projectRef}/storage/files`) onClose() } catch (error) { toast.success( @@ -139,11 +133,7 @@ export const DeleteBucketModal = ({ visible, bucket, onClose }: DeleteBucketModa }) } toast.success(`Successfully deleted analytics bucket ${bucket.id}`) - if (isStorageV2) { - router.push(`/project/${projectRef}/storage/analytics`) - } else { - router.push(`/project/${projectRef}/storage/buckets`) - } + if (!!bucketId) router.push(`/project/${projectRef}/storage/analytics`) onClose() }, }) diff --git a/apps/studio/components/interfaces/Storage/EmptyBucketState.tsx b/apps/studio/components/interfaces/Storage/EmptyBucketState.tsx index dde4c6e3166a0..56ca9f1759294 100644 --- a/apps/studio/components/interfaces/Storage/EmptyBucketState.tsx +++ b/apps/studio/components/interfaces/Storage/EmptyBucketState.tsx @@ -1,18 +1,26 @@ import { BucketAdd } from 'icons' +import { cn } from 'ui' import { CreateBucketModal } from './CreateBucketModal' import { CreateSpecializedBucketModal } from './CreateSpecializedBucketModal' import { BUCKET_TYPES } from './Storage.constants' interface EmptyBucketStateProps { bucketType: keyof typeof BUCKET_TYPES + className?: string } -export const EmptyBucketState = ({ bucketType }: EmptyBucketStateProps) => { +export const EmptyBucketState = ({ bucketType, className }: EmptyBucketStateProps) => { const config = BUCKET_TYPES[bucketType] return ( -