diff --git a/apps/design-system/content/docs/accessibility.mdx b/apps/design-system/content/docs/accessibility.mdx index a549d1eb7b67c..0d71788abacc9 100644 --- a/apps/design-system/content/docs/accessibility.mdx +++ b/apps/design-system/content/docs/accessibility.mdx @@ -25,14 +25,68 @@ All interactive page elements should be reachable by keyboard. Given the below i Chromium-based browsers and Firefox handle this automatically via the Tab key. Safari, by default, requires the Option key to also be held down. Enabling _Keyboard navigation_ on macOS Settings [removes this requirement](https://mayank.co/blog/safari-focus/#keyboard-navigation) but makes links non-tabbable as a result. -Interactive page elements should also provide visual feedback upon selection via a `focus-visible` state. We use consistent focus styles such as `inset-focus` so users recognize this state instantly. +Interactive page elements should also provide visual feedback upon selection via a `focus-visible` state. We use one shared focus ring so users recognize this state instantly. -[Button](components/button) has all of the above built-in. The same explicit `tabIndex` default is also baked into Checkbox, Switch, Select Trigger, Toggle, Accordion Trigger, Collapsible Trigger, Dropdown Menu Trigger, Popover Trigger, Dialog Trigger, Sheet Trigger, Alert Dialog Trigger, and the Sidebar Menu and action buttons. Bespoke interactive elements however, such as the below interactive [Table Row](components/table#examples), require these props to be added manually: +### Focus ring recipe + +Prefer the shared utilities over inventing local styles: + +| Utility | Use when | +| ------------- | -------------------------------------------------------------------------- | +| `focus-ring` | Buttons, inputs, and most controls (offset **ring**) | +| `focus-inset` | Dense or flush surfaces such as interactive table rows (inset **outline**) | + +```tsx +className = 'focus-ring' +// or +className = 'relative cursor-pointer focus-inset' +``` + +These expand to: + +**`focus-ring`** + +```txt +outline-hidden +focus-visible:ring-2 +focus-visible:ring-ring +focus-visible:ring-offset-2 +focus-visible:ring-offset-background +``` + +**`focus-inset`** + +Uses `outline` (not `ring`) so it paints reliably on interactive ``s. Tailwind `ring` is `box-shadow`, which browsers often skip on `display: table-row` (notably Safari). Do not put `focus-ring` or raw `ring-*` on a ``, and do not add `outline-hidden` alongside `focus-inset`. `outline-hidden` sets `outline-style: none` and will hide the indicator. + +```txt +&:focus-visible { + outline-style: solid + outline-width: 2px + outline-offset: -2px + outline-color: var(--ring) + border-radius: var(--radius-md) +} +``` + +`outline-hidden` is always on (not `focus-visible:`-prefixed) so mouse click does not show the browser’s default outline; the focus indicator replaces it for keyboard focus only. + +Rules: + +- Prefer `:focus-visible` over `:focus` so click/tap does not show a focus indicator +- Never use `outline-none` / `outline-hidden` without a ring or outline replacement +- Always use the shared color (`ring-ring` / `outline-ring`). Variants (primary, danger, warning) do not change focus colour +- Do not animate the focus indicator; avoid `transition-all` / `transition` on controls that show one (prefer `transition-colors`) +- Prefer `focus-ring` / `focus-inset` over copy-pasting the class stack +- On interactive ``s, use `focus-inset` only. `focus-ring` will look fine in some browsers and invisible in others + +When the focused element is not the thing that should show the ring (e.g. a wrapping `Link` with `group`, or an `InputGroup` parent using `:has()`), keep the explicit `group-focus-visible:ring-*` / `has-[…]:focus-visible:ring-*` stack. The utilities bake in `:focus-visible` on the same element and do not compose as `group-focus-visible:focus-ring`. + +[Button](components/button) has focus, `tabIndex`, and the shared ring built-in. The same explicit `tabIndex` default is also baked into Checkbox, Switch, Select Trigger, Toggle, Accordion Trigger, Collapsible Trigger, Dropdown Menu Trigger, Popover Trigger, Dialog Trigger, Sheet Trigger, Alert Dialog Trigger, and the Sidebar Menu and action buttons. Bespoke interactive elements however, such as the below interactive [Table Row](components/table#examples), require these props to be added manually: ```tsx showLineNumbers {4-14} { if (event.currentTarget !== event.target) return handleBucketNavigation(bucket.id, event) diff --git a/apps/design-system/content/docs/components/button.mdx b/apps/design-system/content/docs/components/button.mdx index 43afbfad43fa6..ffa96da53df9f 100644 --- a/apps/design-system/content/docs/components/button.mdx +++ b/apps/design-system/content/docs/components/button.mdx @@ -144,5 +144,6 @@ Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions, - Enabled buttons default to `tabIndex={0}` (keyboard accessible) - Disabled buttons default to `tabIndex={-1}` (removed from tab order) - You can still override with an explicit `tabIndex` prop when needed +- Keyboard focus uses the shared `focus-ring` utility; variants do not change ring colour You therefore don't need to manually set `tabIndex`, as Button handles it automatically based on its `disabled` state. diff --git a/apps/design-system/content/docs/components/table.mdx b/apps/design-system/content/docs/components/table.mdx index f25a80fe12f45..d0ab5ccb53606 100644 --- a/apps/design-system/content/docs/components/table.mdx +++ b/apps/design-system/content/docs/components/table.mdx @@ -221,7 +221,7 @@ Avoid adding other actions when using row-level navigation, as multiple interact When implementing row-level navigation, pay close attention to [Accessibility](/accessibility#focus-management) requirements. The row must be keyboard accessible with proper focus management. Also consider these affordances: - Handle `Enter` and `Space` key presses for activation -- Provide visual focus indicators using classes like `inset-focus` +- Provide visual focus indicators using classes like `focus-inset` - Support modifier keys (`Ctrl`/`Cmd`, middle-click) for opening links in new tabs - Consider using the shared `createNavigationHandler` function to handle modifier keys - Avoid bubbling up action events from _within_ the row diff --git a/apps/design-system/registry/default/example/table-row-link-actions.tsx b/apps/design-system/registry/default/example/table-row-link-actions.tsx index 7e744df1b893e..5e366aec4460d 100644 --- a/apps/design-system/registry/default/example/table-row-link-actions.tsx +++ b/apps/design-system/registry/default/example/table-row-link-actions.tsx @@ -70,7 +70,7 @@ export default function TableRowLinkActions() { {policies.map((policy) => ( { if (event.currentTarget !== event.target) return handlePolicyNavigation(policy.id, event) diff --git a/apps/design-system/registry/default/example/table-row-link.tsx b/apps/design-system/registry/default/example/table-row-link.tsx index be4716eff2494..25850a882031d 100644 --- a/apps/design-system/registry/default/example/table-row-link.tsx +++ b/apps/design-system/registry/default/example/table-row-link.tsx @@ -54,7 +54,7 @@ export default function TableRowLink() { {buckets.map((bucket) => ( { if (event.currentTarget !== event.target) return handleBucketNavigation(bucket.id, event) diff --git a/apps/docs/package.json b/apps/docs/package.json index 87f158579da73..e343ae6e65c51 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -37,7 +37,7 @@ "lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml", "postbuild": "pnpm run build:sitemap && ./../../scripts/upload-static-assets.sh", "prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples && pnpm build:federated-content && pnpm run build:markdown && pnpm run build:gz-archive", - "predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples && pnpm build:federated-content && pnpm run build:markdown", + "predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples", "preembeddings": "pnpm run codegen:references", "preinstall": "npx only-allow pnpm", "presync": "pnpm run codegen:graphql", @@ -175,7 +175,7 @@ "slugify": "^1.6.6", "smol-toml": "^1.3.1", "tailwindcss": "catalog:", - "tar": "^7.5.13", + "tar": "^7.5.19", "tsconfig": "workspace:*", "tsx": "catalog:", "twoslash": "^0.3.1", diff --git a/apps/docs/scripts/generate-dart-reference.ts b/apps/docs/scripts/generate-dart-reference.ts index ae0d51361fdc2..6171c1b47f9b6 100644 --- a/apps/docs/scripts/generate-dart-reference.ts +++ b/apps/docs/scripts/generate-dart-reference.ts @@ -67,6 +67,8 @@ const HEADER_IDS = new Set([ 'database-api', 'realtime-api', 'file-buckets', + 'analytics-buckets', + 'vector-buckets', 'using-modifiers', 'using-filters', ]) diff --git a/apps/docs/spec/reference/dart/v2/partials/analytics-buckets.json b/apps/docs/spec/reference/dart/v2/partials/analytics-buckets.json new file mode 100644 index 0000000000000..9cd32a13fa629 --- /dev/null +++ b/apps/docs/spec/reference/dart/v2/partials/analytics-buckets.json @@ -0,0 +1,5 @@ +{ + "id": "analytics-buckets", + "title": "Analytics Buckets", + "notes": "This section contains methods for working with analytics buckets backed by Apache Iceberg.\n" +} diff --git a/apps/docs/spec/reference/dart/v2/partials/oauth-server.json b/apps/docs/spec/reference/dart/v2/partials/oauth-server.json index 4d8ea90b2b2f8..07dec49d2d035 100644 --- a/apps/docs/spec/reference/dart/v2/partials/oauth-server.json +++ b/apps/docs/spec/reference/dart/v2/partials/oauth-server.json @@ -1,5 +1,5 @@ { "id": "oauth-server", "title": "OAuth Server", - "notes": "Methods under the `supabase.auth.oauth` namespace are used when your Supabase project acts as an OAuth 2.1 server. They drive the user-facing consent flow and require a signed-in user. The OAuth 2.1 server feature must be enabled in your Supabase Auth configuration.\n" + "notes": "Methods under the `supabase.auth.oauth` namespace are used when your Supabase project acts as an OAuth 2.1 server. They drive the user-facing consent flow, let users manage the grants they have issued to third-party clients, and require a signed-in user. The OAuth 2.1 server feature must be enabled in your Supabase Auth configuration.\n" } diff --git a/apps/docs/spec/reference/dart/v2/partials/vector-buckets.json b/apps/docs/spec/reference/dart/v2/partials/vector-buckets.json new file mode 100644 index 0000000000000..96fc64fa098ae --- /dev/null +++ b/apps/docs/spec/reference/dart/v2/partials/vector-buckets.json @@ -0,0 +1,5 @@ +{ + "id": "vector-buckets", + "title": "Vector Buckets", + "notes": "This section contains methods for working with Vector Buckets, invoked behind the `supabase.storage.vectors` namespace.\n" +} diff --git a/apps/docs/spec/supabase_dart_v2.yml b/apps/docs/spec/supabase_dart_v2.yml index 33db2972de40b..c418a59d7e44a 100644 --- a/apps/docs/spec/supabase_dart_v2.yml +++ b/apps/docs/spec/supabase_dart_v2.yml @@ -886,6 +886,60 @@ functions: providerId: '21648a9d-8d5a-4555-a9d1-d6375dc14e92', ); ``` + - id: sign-in-with-web3 + title: 'signInWithWeb3()' + description: | + Signs in a user by verifying a message signed with their Web3 wallet. + notes: | + - Supports Ethereum (Sign-In with Ethereum) and Solana (Sign-In with Solana), both of which derive from the [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) standard. + - Handle the wallet interaction and message signing yourself with the wallet library of your choice, then provide the signed `message` together with its `signature`. + - For `Web3Chain.ethereum` the signature is a hex encoded string. For `Web3Chain.solana` it is a base64url encoded string. + - On success, it signs the user in and returns a session. On failure, it throws an `AuthException`. + params: + - name: chain + isOptional: false + type: Web3Chain + description: The blockchain used to sign in. One of `Web3Chain.ethereum` or `Web3Chain.solana`. + - name: message + isOptional: false + type: String + description: The EIP-4361 message that was signed by the user's wallet. + - name: signature + isOptional: false + type: String + description: The signature produced by the wallet. Hex encoded for Ethereum, base64url encoded for Solana. + - name: captchaToken + isOptional: true + type: String + description: The verification token received when the user completes the captcha on the app. + examples: + - id: sign-in-with-ethereum + name: Sign in with an Ethereum wallet + isSpotlight: true + description: | + Sign the EIP-4361 message with the user's Ethereum wallet, then pass the message and its hex encoded signature. + code: | + ```dart + final response = await supabase.auth.signInWithWeb3( + chain: Web3Chain.ethereum, + message: message, // The EIP-4361 message signed by the wallet. + signature: signature, // Hex encoded signature. + ); + final session = response.session; + ``` + - id: sign-in-with-solana + name: Sign in with a Solana wallet + description: | + Sign the message with the user's Solana wallet, then pass the message and its base64url encoded signature. + code: | + ```dart + final response = await supabase.auth.signInWithWeb3( + chain: Web3Chain.solana, + message: message, // The message signed by the wallet. + signature: signature, // base64url encoded signature. + ); + final session = response.session; + ``` - id: sign-in-with-passkey title: 'signInWithPasskey()' notes: | @@ -935,6 +989,10 @@ functions: isOptional: false type: PasskeyAuthenticatorInterface description: Performs the platform passkey ceremony (FaceID/TouchID/security key). For example, a `PasskeyAuthenticator` from the `passkeys` package. + - name: friendlyName + isOptional: true + type: String + description: Human readable name for the passkey. Used as a fallback for the WebAuthn `user.name`/`displayName` when the server omits them, and stored as the passkey's friendly name. Defaults to `Passkey` when not provided. examples: - id: register-passkey name: Register a passkey for the current user @@ -945,7 +1003,10 @@ functions: final authenticator = PasskeyAuthenticator(); - final Passkey passkey = await supabase.auth.registerPasskey(authenticator); + final Passkey passkey = await supabase.auth.registerPasskey( + authenticator, + friendlyName: 'Work laptop', + ); ``` - id: sign-out title: 'signOut()' @@ -1108,6 +1169,9 @@ functions: title: 'currentSession' description: | Returns the session data, if there is an active session. + notes: | + - `currentSession` is a synchronous getter that returns whatever session is stored, even one whose access token has already expired. + - `getSession()` is an asynchronous alternative that guarantees a valid access token when it resolves: a still-valid session is returned as-is, while an expired one is refreshed on demand first. It returns `null` when there is no session and throws an `AuthException` when an expired session cannot be refreshed. examples: - id: get-the-session-data name: Get the session data @@ -1158,6 +1222,14 @@ functions: ), ); ``` + - id: get-the-session-data-async + name: Get the session data, refreshing if needed + description: | + Use the asynchronous `getSession()` to get a session whose access token is guaranteed to be valid, refreshing it on demand when it has expired. + code: | + ```dart + final session = await supabase.auth.getSession(); + ``` - id: get-user title: 'currentUser' description: | @@ -1239,6 +1311,10 @@ functions: isOptional: true type: String description: The nonce sent for reauthentication if the user's password is to be updated. + - name: currentPassword + isOptional: true + type: String + description: The user's current password. Serialized as `current_password`. Required when changing the password and the auth server enforces the current password on password changes. - name: emailRedirectTo isOptional: true type: String @@ -1307,6 +1383,21 @@ functions: ); final User? updatedUser = res.user; ``` + - id: update-the-password-with-current-password + name: Update the password with the current password + description: | + When the auth server is configured to require the current password on password changes, pass it in `currentPassword`. It is serialized as `current_password` and used to verify the change. + isSpotlight: false + code: | + ```dart + final UserResponse res = await supabase.auth.updateUser( + UserAttributes( + password: 'new password', + currentPassword: 'current password', + ), + ); + final User? updatedUser = res.user; + ``` - id: update-the-users-metadata name: Update the user's metadata isSpotlight: true @@ -1941,6 +2032,10 @@ functions: isOptional: false type: String description: System assigned identifier for authenticator device as returned by enroll + - name: channel + isOptional: true + type: OtpChannel + description: Messaging channel to use for phone factors (e.g. `OtpChannel.whatsapp` or `OtpChannel.sms`). Defaults to the server's behavior (SMS) when omitted. Ignored for TOTP factors. examples: - id: create-mfa-challenge name: Create a challenge for a factor @@ -1958,6 +2053,17 @@ functions: expiresAt: DateTime.fromMillisecondsSinceEpoch(1700000000), ); ``` + - id: create-mfa-challenge-with-channel + name: Create a challenge for a phone factor over WhatsApp + description: | + For phone factors you can choose the messaging channel used to deliver the verification code. + code: | + ```dart + final res = await supabase.auth.mfa.challenge( + factorId: '34e770dd-9ff9-416c-87fa-43b31d7ef225', + channel: OtpChannel.whatsapp, + ); + ``` - id: mfa-verify title: 'mfa.verify()' notes: | @@ -2251,6 +2357,12 @@ functions: Starts the registration of a new passkey for the signed in user. - Requires a signed in (non-anonymous) user. - Pass the returned `options` to the platform's passkey API to create the credential, then call [`passkey.verifyRegistration()`](/docs/reference/dart/auth-passkey-verifyregistration) with the result. + - When the server omits `user.name`/`displayName` in the registration options, they are backfilled with `friendlyName` (or a generic `Passkey` default) before the platform ceremony. + params: + - name: friendlyName + isOptional: true + type: String + description: Human readable name used as a fallback for the WebAuthn `user.name`/`displayName` when the server omits them. Defaults to `Passkey` when not provided. examples: - id: start-passkey-registration name: Start a passkey registration @@ -2258,7 +2370,9 @@ functions: code: | ```dart final PasskeyRegistrationOptionsResponse registration = - await supabase.auth.passkey.startRegistration(); + await supabase.auth.passkey.startRegistration( + friendlyName: 'Work laptop', + ); // Hand registration.options to the platform passkey API. ``` @@ -2410,6 +2524,41 @@ functions: await supabase.auth.oauth.denyAuthorization(authorizationId); // Redirect the user to consent.redirectUrl ``` + - id: oauth-list-grants + title: 'oauth.listGrants()' + description: | + Lists the OAuth grants the signed-in user has issued to third-party OAuth clients. + notes: | + - Requires an authenticated user. Returns the grants issued by the current user. + examples: + - id: list-oauth-grants + name: List OAuth grants + isSpotlight: true + code: | + ```dart + final List grants = await supabase.auth.oauth.listGrants(); + + for (final grant in grants) { + print('${grant.client.clientId}: ${grant.scopes}'); + } + ``` + - id: oauth-revoke-grant + title: 'oauth.revokeGrant()' + description: | + Revokes a grant the signed-in user previously issued to a third-party OAuth client. + params: + - name: clientId + isOptional: false + type: String + description: The identifier of the OAuth client whose grant should be revoked. + examples: + - id: revoke-oauth-grant + name: Revoke an OAuth grant + isSpotlight: true + code: | + ```dart + await supabase.auth.oauth.revokeGrant('client-id'); + ``` - id: admin-api title: 'Overview' category: Auth @@ -3104,6 +3253,10 @@ functions: isOptional: true type: HttpMethod description: HTTP method of the request. Defaults to POST. + - name: abortSignal + isOptional: true + type: Future + description: Cancels the in-flight request when the provided `Future` completes. It must not complete with an error. On abort, an `http.RequestAbortedException` (from `package:http`) is thrown. Useful for cancelling a request in response to an event or for setting a request timeout. examples: - id: basic-invocation name: Basic invocation. @@ -3128,6 +3281,25 @@ functions: }, ); ``` + - id: aborting-a-request + name: Aborting a request + description: | + Pass an `abortSignal` to cancel the in-flight request. When the `Future` completes the request is aborted and an `http.RequestAbortedException` (from `package:http`) is thrown. A `Future.delayed` gives you a per-invocation timeout. + code: | + ```dart + import 'package:http/http.dart' as http; + + try { + final res = await supabase.functions.invoke( + 'hello', + body: {'foo': 'baa'}, + abortSignal: Future.delayed(const Duration(seconds: 5)), + ); + final data = res.data; + } on http.RequestAbortedException catch (error) { + print('Request was aborted: $error'); + } + ``` - id: database-api title: 'Database' category: Database @@ -4496,14 +4668,6 @@ functions: subcategory: File Buckets notes: | This section contains methods for working with File Buckets. - # - id: analytics-buckets - # title: 'Overview' - # notes: | - # This section contains methods for working with Analytics Buckets. - # - id: vector-buckets - # title: 'Overview' - # notes: | - # This section contains methods for working with Vector Buckets. - id: list-buckets description: | @@ -4514,6 +4678,32 @@ functions: - `buckets` permissions: `select` - `objects` permissions: none - Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works + params: + - name: options + isOptional: true + type: ListBucketsOptions + description: Optionally filter, sort and paginate the returned buckets. Calling `listBuckets()` without any options returns all buckets. + subContent: + - name: limit + isOptional: true + type: int + description: The maximum number of buckets to return. + - name: offset + isOptional: true + type: int + description: The number of buckets to skip. + - name: search + isOptional: true + type: String + description: A search term used to filter buckets by name. + - name: sortColumn + isOptional: true + type: BucketSortColumn + description: The column to sort the buckets by. One of `BucketSortColumn.id`, `BucketSortColumn.name`, `BucketSortColumn.createdAt` or `BucketSortColumn.updatedAt`. + - name: sortOrder + isOptional: true + type: BucketSortOrder + description: The direction to sort the buckets in. Either `BucketSortOrder.ascending` or `BucketSortOrder.descending`. examples: - id: list-buckets name: List buckets @@ -4541,6 +4731,22 @@ functions: ), ] ``` + - id: list-buckets-with-options + name: With filter, sort and pagination + code: | + ```dart + final List buckets = await supabase + .storage + .listBuckets( + const ListBucketsOptions( + limit: 10, + offset: 0, + search: 'avatar', + sortColumn: BucketSortColumn.createdAt, + sortOrder: BucketSortOrder.descending, + ), + ); + ``` - id: get-bucket description: | @@ -4730,6 +4936,37 @@ functions: 'Successfully deleted' ``` + - id: purge-bucket-cache + description: | + Invalidates the CDN cache for every object in a bucket. + title: 'purgeBucketCache()' + notes: | + - Requires the `secret` key and the `purgeCache` feature enabled for your project on the storage server. + - When `transformations` is `true`, only the resized/formatted variants are purged, leaving the original cached objects intact. Otherwise the bucket's object cache is purged. + - Policy permissions required: + - `buckets` permissions: `select` + - `objects` permissions: none + - Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works + params: + - name: id + isOptional: false + type: String + description: The unique identifier of the bucket whose CDN cache should be purged. + - name: transformations + isOptional: true + type: bool + description: When true, only the transformed (resized/formatted) variants are purged, leaving the original cached objects intact. Defaults to false. + examples: + - id: purge-bucket-cache + name: Purge the CDN cache for a bucket + isSpotlight: true + code: | + ```dart + final String res = await supabase + .storage + .purgeBucketCache('avatars'); + ``` + - id: from-upload description: | Uploads a file to an existing bucket. @@ -4936,6 +5173,10 @@ functions: isOptional: true type: DownloadBehavior description: Triggers the file to be downloaded rather than opened in the browser. Use `DownloadBehavior.withOriginalName` to keep the original file name or `DownloadBehavior.named('custom.png')` to override it. + - name: cacheNonce + isOptional: true + type: String + description: Appends a `cacheNonce` query parameter to the signed URL to bypass CDN caching for a specific file version. - name: transform isOptional: true type: TransformOptions @@ -5005,6 +5246,19 @@ functions: download: DownloadBehavior.withOriginalName, ); ``` + - id: create-signed-url-with-cache-nonce + name: Bypass the CDN cache + code: | + ```dart + final String signedUrl = await supabase + .storage + .from('avatars') + .createSignedUrl( + 'avatar1.png', + 60, + cacheNonce: 'v2', + ); + ``` - id: from-create-signed-upload-url description: | @@ -5067,6 +5321,10 @@ functions: isOptional: true type: DownloadBehavior description: Triggers the file to be downloaded rather than opened in the browser. Use `DownloadBehavior.withOriginalName` to keep the original file name or `DownloadBehavior.named('custom.png')` to override it. + - name: cacheNonce + isOptional: true + type: String + description: Appends a `cacheNonce` query parameter to the URL to bypass CDN caching for a specific file version. - name: transform isOptional: true type: TransformOptions @@ -5139,6 +5397,22 @@ functions: download: DownloadBehavior.withOriginalName, ); ``` + - id: returns-the-url-for-an-asset-with-cache-nonce + name: Bypass the CDN cache + code: | + ```dart + final String publicUrl = supabase + .storage + .from('public-bucket') + .getPublicUrl( + 'avatar1.png', + cacheNonce: 'v2', + ); + ``` + response: | + ```json + 'https://example.supabase.co/storage/v1/object/public/public-bucket/avatar1.png?cacheNonce=v2' + ``` - id: from-download description: | @@ -5154,6 +5428,10 @@ functions: isOptional: false type: String description: The full path and file name of the file to be downloaded. For example folder/image.png. + - name: cacheNonce + isOptional: true + type: String + description: Adds a `cacheNonce` query parameter to bypass CDN caching for a specific file version. - name: transform isOptional: true type: TransformOptions @@ -5214,6 +5492,124 @@ functions: ```json ``` + - id: download-file-with-cache-nonce + name: Bypass the CDN cache + code: | + ```dart + final Uint8List file = await supabase + .storage + .from('avatars') + .download( + 'avatar1.png', + cacheNonce: 'v2', + ); + ``` + response: | + ```json + + ``` + + - id: from-download-stream + description: | + Downloads a file as a lazy `Stream`, streaming the bytes instead of buffering the whole file into memory like `download()`. + title: 'from.downloadStream()' + notes: | + - The request is sent when the stream is listened to. A non-success response surfaces as a `StorageException` on the stream before any bytes are emitted. + - Prefer this over [`download()`](/docs/reference/dart/storage-from-download) for large files to keep memory usage low. + - Policy permissions required: + - `buckets` permissions: none + - `objects` permissions: `select` + - Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works + params: + - name: path + isOptional: false + type: String + description: The full path and file name of the file to be downloaded. For example folder/image.png. + - name: cacheNonce + isOptional: true + type: String + description: Adds a `cacheNonce` query parameter to bypass CDN caching for a specific file version. + - name: transform + isOptional: true + type: TransformOptions + description: Transform the asset before serving it to the client. + subContent: + - name: width + isOptional: true + type: int + description: The width of the image in pixels. + - name: height + isOptional: true + type: int + description: The height of the image in pixels. + - name: resize + isOptional: true + type: ResizeMode + description: Specifies how image cropping should be handled when performing image transformations. Defaults to `ResizeMode.cover`. + - name: quality + isOptional: true + type: int + description: Set the quality of the returned image. A number from 20 to 100, with 100 being the highest quality. Defaults to 80 + - name: format + isOptional: true + type: RequestImageFormat + description: Specify the format of the image requested. When using 'origin' we force the format to be the same as the original image. When this option is not passed in, images are optimized to modern image formats like Webp. + examples: + - id: download-file-as-stream + name: Download a file as a stream + isSpotlight: true + code: | + ```dart + final Stream stream = supabase + .storage + .from('avatars') + .downloadStream('avatar1.png'); + + await for (final chunk in stream) { + // Handle each chunk of bytes as it arrives + } + ``` + + - id: from-purge-cache + description: | + Invalidates the CDN cache for a single object in a bucket. + title: 'from.purgeCache()' + notes: | + - Requires the `secret` key and the `purgeCache` feature enabled for your project on the storage server. + - When `transformations` is `true`, only the resized/formatted variants are purged, leaving the original cached object intact. Otherwise the object's cache is purged. + - Policy permissions required: + - `buckets` permissions: none + - `objects` permissions: `select` + - Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works + params: + - name: path + isOptional: false + type: String + description: The path and name of the object to purge from the CDN cache. For example folder/image.png. + - name: transformations + isOptional: true + type: bool + description: When true, only the transformed (resized/formatted) variants are purged, leaving the original cached object intact. Defaults to false. + examples: + - id: purge-object-cache + name: Purge the CDN cache for an object + isSpotlight: true + code: | + ```dart + final String res = await supabase + .storage + .from('avatars') + .purgeCache('avatar1.png'); + ``` + - id: purge-object-transformations-cache + name: Purge only the transformed variants + code: | + ```dart + final String res = await supabase + .storage + .from('avatars') + .purgeCache('avatar1.png', transformations: true); + ``` - id: from-remove description: | @@ -5334,17 +5730,621 @@ functions: ), ] ``` - - id: using-modifiers - title: Using Modifiers - category: Database - subcategory: Using modifiers + - id: storagefile-list-v2 description: | - Filters work on the row level. That is, they allow you to return rows that - only match certain conditions without changing the shape of the rows. - Modifiers are everything that don't fit that definition—allowing you to - change the format of the response (e.g., returning a CSV string). - - Modifiers must be specified after filters. Some modifiers only apply for + Lists files and folders within a bucket with cursor-based pagination and hierarchical (delimiter) listing. + title: 'from.listPaginated()' + notes: | + - Folder entries in `PaginatedListResult.folders` only contain a name (and optionally a key). Full metadata is only available on the file entries in `PaginatedListResult.objects`. + - To fetch the next page, pass the `PaginatedListResult.nextCursor` value from the previous request as `PaginatedSearchOptions.cursor`. Use `PaginatedListResult.hasNext` to check whether more results are available. + - Policy permissions required: + - `buckets` permissions: none + - `objects` permissions: `select` + - Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works + params: + - name: options + isOptional: true + type: PaginatedSearchOptions + description: Options for the paginated search operation. + subContent: + - name: limit + isOptional: true + type: int + description: The number of files to return. Defaults to 1000 on the server when omitted. + - name: prefix + isOptional: true + type: String + description: The prefix to filter files by. + - name: cursor + isOptional: true + type: String + description: The cursor used for pagination. Pass the `PaginatedListResult.nextCursor` value from the previous request to fetch the next page. + - name: withDelimiter + isOptional: true + type: bool + description: Whether to emulate a hierarchical listing of objects using delimiters. When `false` (default) all objects are listed as a flat list. When `true` the response groups objects by delimiter, separating them into `PaginatedListResult.folders` and `PaginatedListResult.objects`. + - name: sortBy + isOptional: true + type: FileSort + description: The column and direction to sort by. + subContent: + - name: column + isOptional: true + type: FileSortColumn + description: The column to sort by. One of `FileSortColumn.name`, `FileSortColumn.updatedAt` or `FileSortColumn.createdAt`. Defaults to `FileSortColumn.name`. + - name: order + isOptional: true + type: FileSortOrder + description: The sort direction. Either `FileSortOrder.ascending` or `FileSortOrder.descending`. Defaults to `FileSortOrder.ascending`. + examples: + - id: list-files-paginated + name: List files with pagination + isSpotlight: true + code: | + ```dart + final PaginatedListResult result = await supabase + .storage + .from('avatars') + .listPaginated( + options: const PaginatedSearchOptions( + prefix: 'folder/', + limit: 100, + withDelimiter: true, + sortBy: FileSort( + column: FileSortColumn.createdAt, + order: FileSortOrder.descending, + ), + ), + ); + + for (final folder in result.folders) { + // Handle each folder + } + for (final object in result.objects) { + // Handle each file + } + ``` + - id: list-files-paginated-next-page + name: Fetch the next page + code: | + ```dart + var result = await supabase + .storage + .from('avatars') + .listPaginated(); + + while (result.hasNext) { + result = await supabase + .storage + .from('avatars') + .listPaginated( + options: PaginatedSearchOptions(cursor: result.nextCursor), + ); + } + ``` + - id: analytics-buckets + title: 'Overview' + category: Storage + subcategory: Analytics Buckets + notes: | + This section contains methods for working with Analytics Buckets. + - id: storageanalytics-from + description: | + Returns an Iceberg REST Catalog client for an analytics bucket, used to manage the namespaces and tables (the warehouse) inside it. + title: 'storage.analyticsCatalog()' + notes: | + - Analytics buckets are backed by the Apache Iceberg table format. + - `analyticsCatalog()` returns an `IcebergRestCatalog` scoped to a single analytics bucket. Use it to create and manage namespaces and tables within that bucket. + - Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works + params: + - name: bucketId + isOptional: false + type: String + description: The identifier of the analytics bucket (the warehouse) whose namespaces and tables you want to manage. + - name: accessDelegation + isOptional: true + type: List + description: Requests server side access delegation for the catalog operations. + examples: + - id: get-analytics-catalog + name: Get an analytics catalog client + isSpotlight: true + code: | + ```dart + final catalog = supabase + .storage + .analyticsCatalog('my-analytics-bucket'); + + await catalog.createNamespace(['analytics']); + ``` + - id: storageanalytics-createbucket + description: | + Creates a new analytics bucket backed by the Apache Iceberg table format. + title: 'createAnalyticsBucket()' + notes: | + - Policy permissions required: + - `buckets` permissions: `insert` + - `objects` permissions: none + - Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works + params: + - name: id + isOptional: false + type: String + description: A unique identifier for the analytics bucket you are creating. + examples: + - id: create-analytics-bucket + name: Create analytics bucket + isSpotlight: true + code: | + ```dart + final AnalyticsBucket bucket = await supabase + .storage + .createAnalyticsBucket('warehouse'); + ``` + response: | + ```json + AnalyticsBucket( + id: 'warehouse', + name: 'warehouse', + created_at: '2024-01-01T00:00:00.000Z', + updated_at: '2024-01-02T00:00:00.000Z' + ) + ``` + - id: storageanalytics-listbuckets + description: | + Retrieves the details of all analytics buckets within an existing project. + title: 'listAnalyticsBuckets()' + notes: | + - Calling `listAnalyticsBuckets()` without any options returns all analytics buckets. + - Policy permissions required: + - `buckets` permissions: `select` + - `objects` permissions: none + - Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works + params: + - name: options + isOptional: true + type: ListBucketsOptions + description: Optionally filter, sort and paginate the returned buckets. + subContent: + - name: limit + isOptional: true + type: int + description: The maximum number of buckets to return. + - name: offset + isOptional: true + type: int + description: The number of buckets to skip. + - name: search + isOptional: true + type: String + description: A search term used to filter buckets by name. + - name: sortColumn + isOptional: true + type: BucketSortColumn + description: The column to sort the buckets by. One of `BucketSortColumn.id`, `BucketSortColumn.name`, `BucketSortColumn.createdAt` or `BucketSortColumn.updatedAt`. + - name: sortOrder + isOptional: true + type: BucketSortOrder + description: The direction to sort the buckets in. Either `BucketSortOrder.ascending` or `BucketSortOrder.descending`. + examples: + - id: list-analytics-buckets + name: List analytics buckets + isSpotlight: true + code: | + ```dart + final List buckets = await supabase + .storage + .listAnalyticsBuckets(); + ``` + response: | + ```json + [ + AnalyticsBucket( + id: 'warehouse', + name: 'warehouse', + created_at: '2024-01-01T00:00:00.000Z', + updated_at: '2024-01-02T00:00:00.000Z' + ), + ] + ``` + - id: list-analytics-buckets-with-options + name: With filter, sort and pagination + code: | + ```dart + final List buckets = await supabase + .storage + .listAnalyticsBuckets( + const ListBucketsOptions( + limit: 10, + offset: 0, + search: 'ware', + sortColumn: BucketSortColumn.createdAt, + sortOrder: BucketSortOrder.descending, + ), + ); + ``` + - id: storageanalytics-deletebucket + description: | + Deletes an existing analytics bucket. A bucket can't be deleted while it still contains namespaces or tables. + title: 'deleteAnalyticsBucket()' + notes: | + - Policy permissions required: + - `buckets` permissions: `select` and `delete` + - `objects` permissions: none + - Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works + params: + - name: id + isOptional: false + type: String + description: The unique identifier of the analytics bucket you would like to delete. + examples: + - id: delete-analytics-bucket + name: Delete analytics bucket + isSpotlight: true + code: | + ```dart + final String res = await supabase + .storage + .deleteAnalyticsBucket('warehouse'); + ``` + response: | + ```json + 'Successfully deleted' + ``` + + # --------------------------------------------------------------------------- + # Paginated file listing (list-v2) + # --------------------------------------------------------------------------- + - id: vector-buckets + title: 'Overview' + category: Storage + subcategory: Vector Buckets + notes: | + This section contains methods for working with Vector Buckets. + - id: storagevectors-from + title: 'from()' + notes: | + Scopes index operations to a single vector bucket. Returns a `StorageVectorBucketApi`. + examples: + - id: vectors-from + name: Scope operations to a bucket + isSpotlight: true + code: | + ```dart + final bucket = supabase.storage.vectors.from('embeddings'); + + await bucket.createIndex( + name: 'documents', + dimension: 3, + distanceMetric: DistanceMetric.cosine, + ); + ``` + - id: storagevectors-create-bucket + title: 'createBucket()' + notes: | + Creates a new vector bucket. Access the vectors client through `supabase.storage.vectors`. + examples: + - id: create-vector-bucket + name: Create a vector bucket + isSpotlight: true + code: | + ```dart + final vectors = supabase.storage.vectors; + + await vectors.createBucket('embeddings'); + ``` + - id: storagevectors-delete-bucket + title: 'deleteBucket()' + notes: | + Deletes a vector bucket. The bucket must have no indexes before it can be deleted. + examples: + - id: delete-vector-bucket + name: Delete a vector bucket + isSpotlight: true + code: | + ```dart + final vectors = supabase.storage.vectors; + + await vectors.deleteBucket('embeddings'); + ``` + - id: storagevectors-get-bucket + title: 'getBucket()' + notes: | + Retrieves the metadata of an existing vector bucket. + examples: + - id: get-vector-bucket + name: Get a vector bucket + isSpotlight: true + code: | + ```dart + final vectors = supabase.storage.vectors; + + final VectorBucket bucket = await vectors.getBucket('embeddings'); + ``` + - id: storagevectors-list-buckets + title: 'listBuckets()' + notes: | + Lists vector buckets. Use `prefix` to filter by name and `maxResults` / `nextToken` to paginate. + examples: + - id: list-vector-buckets + name: List vector buckets + isSpotlight: true + code: | + ```dart + final vectors = supabase.storage.vectors; + + final VectorBucketList result = await vectors.listBuckets(); + + for (final bucket in result.buckets) { + print(bucket.name); + } + ``` + - id: list-vector-buckets-paginated + name: Paginate and filter buckets + code: | + ```dart + final vectors = supabase.storage.vectors; + + final result = await vectors.listBuckets( + prefix: 'prod-', + maxResults: 50, + ); + + final nextToken = result.nextToken; + ``` + - id: vectorbucket-createindex + title: 'createIndex()' + notes: | + Creates a new vector index in the scoped bucket. `dimension` is the length of the vectors the index will store and `distanceMetric` is the metric used for similarity queries. Keys listed in `nonFilterableMetadataKeys` can be stored on vectors but not used in query filters. `dataType` defaults to `VectorDataType.float32`. + examples: + - id: create-index + name: Create an index + isSpotlight: true + code: | + ```dart + final bucket = supabase.storage.vectors.from('embeddings'); + + await bucket.createIndex( + name: 'documents', + dimension: 3, + distanceMetric: DistanceMetric.cosine, + ); + ``` + - id: create-index-non-filterable + name: Create an index with non-filterable metadata + code: | + ```dart + final bucket = supabase.storage.vectors.from('embeddings'); + + await bucket.createIndex( + name: 'documents', + dimension: 3, + distanceMetric: DistanceMetric.euclidean, + nonFilterableMetadataKeys: ['rawText'], + ); + ``` + - id: vectorbucket-deleteindex + title: 'deleteIndex()' + notes: | + Deletes an index and all of its vectors from the scoped bucket. + examples: + - id: delete-index + name: Delete an index + isSpotlight: true + code: | + ```dart + final bucket = supabase.storage.vectors.from('embeddings'); + + await bucket.deleteIndex('documents'); + ``` + - id: vectorbucket-getindex + title: 'getIndex()' + notes: | + Retrieves the metadata of an index in the scoped bucket. + examples: + - id: get-index + name: Get an index + isSpotlight: true + code: | + ```dart + final bucket = supabase.storage.vectors.from('embeddings'); + + final VectorIndex index = await bucket.getIndex('documents'); + + print(index.dimension); + print(index.distanceMetric); + ``` + - id: vectorbucket-listindexes + title: 'listIndexes()' + notes: | + Lists indexes in the scoped bucket. Use `prefix` to filter by name and `maxResults` / `nextToken` to paginate. + examples: + - id: list-indexes + name: List indexes + isSpotlight: true + code: | + ```dart + final bucket = supabase.storage.vectors.from('embeddings'); + + final VectorIndexList result = await bucket.listIndexes(); + + for (final index in result.indexes) { + print(index.name); + } + ``` + - id: vectorbucket-index + title: 'index()' + notes: | + Scopes vector data operations to a single index within a bucket. Returns a `StorageVectorIndexApi`. + examples: + - id: bucket-index + name: Scope operations to an index + isSpotlight: true + code: | + ```dart + final bucket = supabase.storage.vectors.from('embeddings'); + + final index = bucket.index('documents'); + + await index.putVectors([ + Vector(key: 'doc-1', data: [0.1, 0.2, 0.3]), + ]); + ``` + - id: vectorindex-deletevectors + title: 'deleteVectors()' + notes: | + Deletes vectors by their keys. The batch must contain between 1 and 500 keys. + examples: + - id: delete-vectors + name: Delete vectors + isSpotlight: true + code: | + ```dart + final index = supabase.storage.vectors + .from('embeddings') + .index('documents'); + + await index.deleteVectors(['doc-1', 'doc-2']); + ``` + - id: vectorindex-getvectors + title: 'getVectors()' + notes: | + Retrieves vectors by their keys. Set `returnData` and `returnMetadata` to include the embeddings and metadata in the result. Keys that do not exist are omitted from the returned list. + examples: + - id: get-vectors + name: Get vectors by key + isSpotlight: true + code: | + ```dart + final index = supabase.storage.vectors + .from('embeddings') + .index('documents'); + + final List vectors = await index.getVectors( + keys: ['doc-1', 'doc-2'], + returnData: true, + returnMetadata: true, + ); + + for (final vector in vectors) { + print('${vector.key}: ${vector.metadata}'); + } + ``` + - id: vectorindex-listvectors + title: 'listVectors()' + notes: | + Lists vectors in the scoped index with pagination. A full-index scan can be distributed across multiple workers by giving each worker a different `segmentIndex` (0 to `segmentCount - 1`) for the same `segmentCount` (1 to 16). + examples: + - id: list-vectors + name: List vectors + isSpotlight: true + code: | + ```dart + final index = supabase.storage.vectors + .from('embeddings') + .index('documents'); + + final VectorList result = await index.listVectors( + maxResults: 100, + returnMetadata: true, + ); + + for (final vector in result.vectors) { + print(vector.key); + } + ``` + - id: list-vectors-parallel-scan + name: Parallel scan with segments + code: | + ```dart + final index = supabase.storage.vectors + .from('embeddings') + .index('documents'); + + // Worker 2 of a 4-way parallel scan. + final result = await index.listVectors( + segmentCount: 4, + segmentIndex: 2, + ); + ``` + - id: vectorindex-putvectors + title: 'putVectors()' + notes: | + Inserts or updates a batch of vectors in the scoped index. The batch must contain between 1 and 500 vectors, and each vector's `data` length must match the index dimension. + examples: + - id: put-vectors + name: Put vectors + isSpotlight: true + code: | + ```dart + final index = supabase.storage.vectors + .from('embeddings') + .index('documents'); + + await index.putVectors([ + Vector( + key: 'doc-1', + data: [0.1, 0.2, 0.3], + metadata: {'title': 'Intro'}, + ), + Vector( + key: 'doc-2', + data: [0.4, 0.5, 0.6], + metadata: {'title': 'Guide'}, + ), + ]); + ``` + - id: vectorindex-queryvectors + title: 'queryVectors()' + notes: | + Searches the scoped index for the vectors most similar to `queryVector`. `topK` limits the number of matches returned. `filter` restricts the search to vectors whose metadata matches the given expression. Set `returnDistance` and `returnMetadata` to include the distance scores and metadata in the result. + examples: + - id: query-vectors + name: Query vectors + isSpotlight: true + code: | + ```dart + final index = supabase.storage.vectors + .from('embeddings') + .index('documents'); + + final VectorQueryResult result = await index.queryVectors( + queryVector: [0.1, 0.2, 0.3], + topK: 5, + returnDistance: true, + returnMetadata: true, + ); + + for (final match in result.matches) { + print('${match.key}: ${match.distance}'); + } + ``` + - id: query-vectors-filter + name: Query with a metadata filter + code: | + ```dart + final index = supabase.storage.vectors + .from('embeddings') + .index('documents'); + + final result = await index.queryVectors( + queryVector: [0.1, 0.2, 0.3], + topK: 10, + filter: {'category': 'docs'}, + returnMetadata: true, + ); + ``` + - id: using-modifiers + title: Using Modifiers + category: Database + subcategory: Using modifiers + description: | + Filters work on the row level. That is, they allow you to return rows that + only match certain conditions without changing the shape of the rows. + Modifiers are everything that don't fit that definition—allowing you to + change the format of the response (e.g., returning a CSV string). + + Modifiers must be specified after filters. Some modifiers only apply for queries that return rows (e.g., `select()` or `rpc()` on a function that returns a table response). @@ -5958,6 +6958,23 @@ functions: By default, the data is returned in TEXT format, but can also be returned as JSON by using the `format` parameter. hideCodeBlock: true isSpotlight: false + - id: strip-nulls + title: 'stripNulls()' + description: | + Omits `null`-valued properties from the response objects. + notes: | + - This uses the `nulls=stripped` variant of the `Accept` header and requires PostgREST 11.2 or higher. + examples: + - id: strip-null-values-from-the-response + name: Strip null values from the response + isSpotlight: true + code: | + ```dart + final data = await supabase + .from('users') + .select() + .stripNulls(); + ``` - id: using-filters title: Using Filters category: Database diff --git a/apps/studio/components/grid/components/header/filter/FilterRow.tsx b/apps/studio/components/grid/components/header/filter/FilterRow.tsx index 04dafe05db89e..6881a292dd062 100644 --- a/apps/studio/components/grid/components/header/filter/FilterRow.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterRow.tsx @@ -4,7 +4,9 @@ import { Button, Input } from 'ui' import { FilterOperatorOptions } from './Filter.constants' import { DropdownControl } from '@/components/grid/components/common/DropdownControl' +import { getColumnFormat } from '@/components/grid/components/grid/ColumnHeader.utils' import type { Filter, FilterOperator } from '@/components/grid/types' +import { getColumnType } from '@/components/grid/utils/gridColumns' import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table' export interface FilterRowProps { @@ -18,9 +20,15 @@ export interface FilterRowProps { const FilterRow = ({ filter, filterIdx, onChange, onDelete, onKeyDown }: FilterRowProps) => { const snap = useTableEditorTableStateSnapshot() const column = snap.table.columns.find((x) => x.name === filter.column) + // Prefer display labels that match column headers (format, with arrays as int4[]). + // Avoid raw dataType, which is "USER-DEFINED" for extension types like geography. const columnOptions = snap.table.columns?.map((x) => { - return { value: x.name, label: x.name, postLabel: x.dataType } + return { + value: x.name, + label: x.name, + postLabel: getColumnFormat(getColumnType(x), x.format), + } }) || [] const placeholder = diff --git a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx index 350898ed73019..30570a092e6bb 100644 --- a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx +++ b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx @@ -23,8 +23,10 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { DropdownControl } from '../../common/DropdownControl' import SortRow from './SortRow' +import { getColumnFormat } from '@/components/grid/components/grid/ColumnHeader.utils' import { useTableFilter } from '@/components/grid/hooks/useTableFilter' import type { Sort } from '@/components/grid/types' +import { getColumnType } from '@/components/grid/utils/gridColumns' import { InlineLink } from '@/components/ui/InlineLink' import { useTableRowsCountQuery } from '@/data/table-rows/table-rows-count-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' @@ -105,13 +107,14 @@ export const SortPopoverPrimitive = ({ }) }, [snap?.table?.columns, localSorts]) - // Format the columns for the dropdown + // Prefer display labels that match column headers (format, with arrays as int4[]). + // Avoid raw dataType, which is "USER-DEFINED" for extension types like geography. const dropdownOptions = useMemo(() => { return ( columns?.map((x) => ({ value: x.name, label: x.name, - postLabel: x.dataType, + postLabel: getColumnFormat(getColumnType(x), x.format), disabled: x.dataType === 'json' || x.dataType === 'jsonb', tooltip: x.dataType === 'json' || x.dataType === 'jsonb' diff --git a/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx index 55c42690c66df..ad21bc73acfb7 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx @@ -1,7 +1,19 @@ +import Image from 'next/image' +import { BASE_PATH } from 'ui-patterns/CommandMenu/prepackaged/shared/constants' + export const IntegrationsLayoutPreview = () => (
-

- Browse and install integrations from a new layout with improved filtering and search. +

+ Install Dashboard Integrations in a single click, with improved filtering and search to help + you find the one you need.

+ + integrations layout preview
) diff --git a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts index dd425cc52f50d..65be8e9df458b 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts +++ b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts @@ -92,12 +92,12 @@ export const useFeaturePreviews = (): FeaturePreview[] => { }, { key: LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE, - name: 'Integrations layout', + name: 'One-Click Integrations', discussionsUrl: undefined, enabled: isMarketplaceEnabled, isNew: true, - isPlatformOnly: true, - isDefaultOptIn: false, + isPlatformOnly: false, + isDefaultOptIn: true, getRoute: (ref?: string) => `/project/${ref}/integrations`, }, { diff --git a/apps/studio/components/interfaces/App/ShellFallback.tsx b/apps/studio/components/interfaces/App/ShellFallback.tsx new file mode 100644 index 0000000000000..ff6a1666af791 --- /dev/null +++ b/apps/studio/components/interfaces/App/ShellFallback.tsx @@ -0,0 +1,73 @@ +import { LogoLoader } from 'ui' + +import { IS_PLATFORM } from '@/lib/constants' + +// Baked into the prerendered SPA shell (_shell.html) — this is the static HTML +// shown on every cold load until the JS bundle hydrates, so nothing here can +// rely on JS: the loader animation, the 7s stuck-load help reveal, and the +// noscript message are all pure CSS/HTML. +export function ShellFallback() { + return ( + <> + +
+ {/* LogoLoader fills its parent, so give it a fixed-height box — otherwise + it stretches to the full column and pushes the help text offscreen. */} +
+ +
+ {/* data-nosnippet keeps Google from surfacing this boilerplate as the + search snippet for dashboard URLs (the shell serves every route). */} +

+ Taking longer than expected? Try clearing your browser cookies and reloading the page. + {IS_PLATFORM && ( + <> + {' '} + If the problem persists, contact{' '} + + support@supabase.com + + . + + )} +

+
+ {/* dangerouslySetInnerHTML keeps React hydration from diffing noscript + children, which the server renders as real markup but the client + treats as raw text. */} +