From e0510c4f9567a9994489c447d3d6b413c1867fea Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 25 Jun 2024 10:13:31 -0230 Subject: [PATCH 01/18] Chore/add change email instructions (#27093) * Add instructions for changing email * Check users account type * Lists --- .../Preferences/AccountInformation.tsx | 100 +++++++++++++++--- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/apps/studio/components/interfaces/Account/Preferences/AccountInformation.tsx b/apps/studio/components/interfaces/Account/Preferences/AccountInformation.tsx index c6c940aa975..8bc199d8904 100644 --- a/apps/studio/components/interfaces/Account/Preferences/AccountInformation.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/AccountInformation.tsx @@ -1,9 +1,19 @@ -import { Button, Input } from 'ui' +import { + Button, + Input, + Input_Shadcn_, + Label_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + Collapsible_Shadcn_, + IconChevronDown, +} from 'ui' import Link from 'next/link' import Panel from 'components/ui/Panel' import { useSession } from 'lib/auth' import type { Profile } from 'data/profile/types' +import { ChevronDown } from 'lucide-react' const AccountInformation = ({ profile }: { profile?: Profile }) => { const session = useSession() @@ -17,21 +27,79 @@ const AccountInformation = ({ profile }: { profile?: Profile }) => { } > -
- - +
+
+ Username + +
+
+ Email + +
+ +
+ + +
+

+ How can I change my email? +

+ +
+
+ +
+ {session?.user.app_metadata.provider === 'email' ? ( +
+
    +
  1. Create a new account with the new email
  2. +
  3. Invite this email to be an owner of the Org
  4. +
  5. Join the Org
  6. +
  7. Remove the old email from the Org
  8. +
  9. Email Supabase Support to delete the old email (Optional)
  10. +
+
+ ) : ( +
+
    +
  1. Log out of Supabase
  2. +
  3. + Change your Primary Email in{' '} + + GitHub + {' '} + (your public, not primary email) +
  4. +
  5. Log out of GitHub
  6. +
  7. + Log back into GitHub (with the new, desired email set as primary) +
  8. +
  9. Log back into Supabase
  10. +
+
+ )} +
+
+
+
+
+
{session?.user.app_metadata.provider === 'email' && (
From 02b3d50207b768178d9b837b36ea0c887cf67041 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Tue, 25 Jun 2024 14:49:52 +0200 Subject: [PATCH 02/18] feat: Support private channels in the realtime inspector (#27362) * Add the new realtime js library under an alias. * Add a isChannelPrivate property to realtime config. * Use the aliased realtime-js library to connect to the realtime server. * Add a new switch to enable users to connect to a private channel. * Hide the public/private switch behind a feature flag. * Fix an import. * Add description to the switch. * Small updates, add documentation button in choose channel popover --------- Co-authored-by: Joshen Lim --- .../Inspector/ChooseChannelPopover/index.tsx | 135 +++++++++++++----- .../interfaces/Realtime/Inspector/index.tsx | 1 + .../Realtime/Inspector/useRealtimeMessages.ts | 45 +++--- apps/studio/package.json | 1 + package-lock.json | 13 ++ 5 files changed, 141 insertions(+), 54 deletions(-) diff --git a/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx index 9719b931570..75507a329ee 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx @@ -1,24 +1,28 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useTelemetryProps } from 'common' -import Link from 'next/link' import { useRouter } from 'next/router' import { Dispatch, SetStateAction, useState } from 'react' import { useForm } from 'react-hook-form' import { Button, FormControl_Shadcn_, + FormDescription_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, + FormLabel_Shadcn_, Form_Shadcn_, IconChevronDown, Input_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, + Switch, } from 'ui' import * as z from 'zod' +import { useFlag } from 'hooks' import Telemetry from 'lib/telemetry' +import { ExternalLink } from 'lucide-react' import { RealtimeConfig } from '../useRealtimeMessages' interface ChooseChannelPopoverProps { @@ -26,17 +30,19 @@ interface ChooseChannelPopoverProps { onChangeConfig: Dispatch> } +const FormSchema = z.object({ channel: z.string(), isPrivate: z.boolean() }) + export const ChooseChannelPopover = ({ config, onChangeConfig }: ChooseChannelPopoverProps) => { const [open, setOpen] = useState(false) const telemetryProps = useTelemetryProps() const router = useRouter() + const authzEnabled = useFlag('authzRealtime') - const FormSchema = z.object({ channel: z.string() }) const form = useForm>({ mode: 'onBlur', reValidateMode: 'onBlur', resolver: zodResolver(FormSchema), - defaultValues: { channel: '' }, + defaultValues: { channel: '', isPrivate: false }, }) const onOpen = (v: boolean) => { @@ -58,7 +64,12 @@ export const ChooseChannelPopover = ({ config, onChangeConfig }: ChooseChannelPo telemetryProps, router ) - onChangeConfig({ ...config, channelName: form.getValues('channel'), enabled: true }) + onChangeConfig({ + ...config, + channelName: form.getValues('channel'), + isChannelPrivate: form.getValues('isPrivate'), + enabled: true, + }) } return ( @@ -83,52 +94,104 @@ export const ChooseChannelPopover = ({ config, onChangeConfig }: ChooseChannelPo {config.channelName.length === 0 ? ( <> -
onSubmit())}> + onSubmit())} + className="flex flex-col gap-y-4" + > ( - - -
- - - - + +
+ +
+ + + + + +
+ + The channel you initialize with the Supabase Realtime client. Learn more + in{' '} + + our docs + +
)} /> + + {authzEnabled ? ( + ( + +
+ + + + + Is channel private? + +
+ + If the channel is marked as private, it will use RLS policies to filter + messages. + +
+ )} + /> + ) : null} + + -

- The channel you initialize with the Supabase Realtime client. Learn more in{' '} - - our docs - -

) : (
-

Currently joined channel:

+

+ Currently joined{' '} + + {config.isChannelPrivate ? 'private' : 'public'} + {' '} + channel: +

{config.channelName}

diff --git a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx index 718ae8ec315..6a0bedafd1b 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx @@ -25,6 +25,7 @@ export const RealtimeInspector = () => { token: '', // will be filled out by RealtimeTokensPopover schema: 'public', table: '*', + isChannelPrivate: false, filter: undefined, bearer: null, enablePresence: true, diff --git a/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts b/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts index 46ceb7e3ad1..d70fb65d52a 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts +++ b/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts @@ -1,5 +1,9 @@ -import { RealtimeChannel, SupabaseClient } from '@supabase/supabase-js' -import { sortBy, take } from 'lodash' +import { RealtimeChannel, RealtimeClient } from '@supabase/realtime-js-next' +import { + DEFAULT_GLOBAL_OPTIONS, + DEFAULT_REALTIME_OPTIONS, +} from '@supabase/supabase-js/dist/main/lib/constants' +import { merge, sortBy, take } from 'lodash' import { Dispatch, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react' import toast from 'react-hot-toast' @@ -43,6 +47,7 @@ export interface RealtimeConfig { token: string schema: string table: string + isChannelPrivate: boolean filter: string | undefined bearer: string | null enableBroadcast: boolean @@ -62,6 +67,7 @@ export const useRealtimeMessages = ( token, schema, table, + isChannelPrivate, filter, bearer, enablePresence, @@ -76,40 +82,43 @@ export const useRealtimeMessages = ( ? `${data.autoApiService.protocol}://${data.autoApiService.endpoint}` : `https://${projectRef}.supabase.co` + const realtimeUrl = `${host}/realtime/v1`.replace(/^http/i, 'ws') + const [logData, dispatch] = useReducer(reducer, [] as LogData[]) const pushMessage = (messageType: string, metadata: any) => { dispatch({ type: 'add', payload: { messageType, metadata } }) } // Instantiate our client with the Realtime server and params to connect with - let [client, setClient] = useState | undefined>() + let [client, setClient] = useState() let [channel, setChannel] = useState() useEffect(() => { if (!enabled) { return } - const opts = { - global: { - headers: { - 'User-Agent': `supabase-api/${process.env.VERCEL_GIT_COMMIT_SHA || 'unknown-sha'}`, - }, - }, - realtime: { - params: { - log_level: logLevel, - }, + + const globalOptions = merge(DEFAULT_GLOBAL_OPTIONS, { + headers: { + 'User-Agent': `supabase-api/${process.env.VERCEL_GIT_COMMIT_SHA || 'unknown-sha'}`, }, + }) + const realtimeOptions = merge(DEFAULT_REALTIME_OPTIONS, { params: { log_level: logLevel } }) + + const options = { + headers: globalOptions.headers, + ...realtimeOptions, + params: { apikey: token, ...realtimeOptions.params }, } - const newClient = new SupabaseClient(host, token, opts) + const realtimeClient = new RealtimeClient(realtimeUrl, options) if (bearer) { - newClient.realtime.setAuth(bearer) + realtimeClient.setAuth(bearer) } - setClient(newClient) + setClient(realtimeClient) return () => { - client?.realtime.disconnect() + realtimeClient.disconnect() setClient(undefined) } }, [enabled, bearer, host, logLevel, token]) @@ -120,7 +129,7 @@ export const useRealtimeMessages = ( } dispatch({ type: 'clear' }) const newChannel = client?.channel(channelName, { - config: { broadcast: { self: true } }, + config: { broadcast: { self: true }, private: isChannelPrivate }, }) // Hack to confirm Postgres is subscribed // Need to add 'extension' key in the 'payload' diff --git a/apps/studio/package.json b/apps/studio/package.json index 7577acb527e..100ed2e156d 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -38,6 +38,7 @@ "@stripe/stripe-js": "^3.0.5", "@supabase/auth-helpers-react": "^0.4.2", "@supabase/pg-meta": "*", + "@supabase/realtime-js-next": "npm:@supabase/realtime-js@2.10.0-next.8", "@supabase/shared-types": "0.1.65", "@supabase/supabase-js": "^2.41.1", "@tanstack/react-query": "4.35.7", diff --git a/package-lock.json b/package-lock.json index e17df090917..676b2fb6eda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1237,6 +1237,7 @@ "@stripe/stripe-js": "^3.0.5", "@supabase/auth-helpers-react": "^0.4.2", "@supabase/pg-meta": "*", + "@supabase/realtime-js-next": "npm:@supabase/realtime-js@2.10.0-next.8", "@supabase/shared-types": "0.1.65", "@supabase/supabase-js": "^2.41.1", "@tanstack/react-query": "4.35.7", @@ -13675,6 +13676,18 @@ "ws": "^8.14.2" } }, + "node_modules/@supabase/realtime-js-next": { + "name": "@supabase/realtime-js", + "version": "2.10.0-next.8", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.0-next.8.tgz", + "integrity": "sha512-mwyvgG6pBqpThgGwZxDQNG7HIwEQgto6ImaLPkGclxyCucTkapCvZiYXQOWrVQKNqe5JiPksNbX+3Ssv7k4xUg==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.14.2" + } + }, "node_modules/@supabase/shared-types": { "version": "0.1.65", "resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.65.tgz", From 46514d895598f3bc3cc9c3cbe18a234e4d06b4fc Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 25 Jun 2024 10:35:37 -0230 Subject: [PATCH 03/18] Chore/csp round two (#27524) Avatars, round two --- apps/studio/next.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/studio/next.config.js b/apps/studio/next.config.js index 3c71c8fbc5f..e748fbeba14 100644 --- a/apps/studio/next.config.js +++ b/apps/studio/next.config.js @@ -52,10 +52,10 @@ const VERCEL_LIVE_URL = 'https://vercel.live' const PUSHER_URL = 'https://*.pusher.com' const PUSHER_URL_WS = 'wss://*.pusher.com' -const DEFAULT_SRC_URLS = `${API_URL} ${SUPABASE_URL} ${GOTRUE_URL} ${SUPABASE_LOCAL_PROJECTS_URL_WS} ${SUPABASE_PROJECTS_URL} ${SUPABASE_PROJECTS_URL_WS} ${HCAPTCHA_SUBDOMAINS_URL} ${CONFIGCAT_URL} ${STRIPE_SUBDOMAINS_URL} ${STRIPE_NETWORK_URL} ${CLOUDFLARE_URL} ${ONE_ONE_ONE_ONE_URL} ${VERCEL_INSIGHTS_URL} ${GITHUB_API_URL} ${GITHUB_USER_CONTENT_URL} ${GITHUB_USER_AVATAR_URL}` +const DEFAULT_SRC_URLS = `${API_URL} ${SUPABASE_URL} ${GOTRUE_URL} ${SUPABASE_LOCAL_PROJECTS_URL_WS} ${SUPABASE_PROJECTS_URL} ${SUPABASE_PROJECTS_URL_WS} ${HCAPTCHA_SUBDOMAINS_URL} ${CONFIGCAT_URL} ${STRIPE_SUBDOMAINS_URL} ${STRIPE_NETWORK_URL} ${CLOUDFLARE_URL} ${ONE_ONE_ONE_ONE_URL} ${VERCEL_INSIGHTS_URL} ${GITHUB_API_URL} ${GITHUB_USER_CONTENT_URL}` const SCRIPT_SRC_URLS = `${CLOUDFLARE_CDN_URL} ${HCAPTCHA_JS_URL} ${STRIPE_JS_URL}` const FRAME_SRC_URLS = `${HCAPTCHA_ASSET_URL} ${STRIPE_JS_URL}` -const IMG_SRC_URLS = `${SUPABASE_URL} ${SUPABASE_COM_URL} ${SUPABASE_PROJECTS_URL}` +const IMG_SRC_URLS = `${SUPABASE_URL} ${SUPABASE_COM_URL} ${SUPABASE_PROJECTS_URL} ${GITHUB_USER_AVATAR_URL}` const STYLE_SRC_URLS = `${CLOUDFLARE_CDN_URL}` const FONT_SRC_URLS = `${CLOUDFLARE_CDN_URL}` @@ -76,7 +76,7 @@ const csp = [ `default-src 'self' ${DEFAULT_SRC_URLS};`, `script-src 'self' 'unsafe-eval' 'unsafe-inline' ${SCRIPT_SRC_URLS};`, `frame-src 'self' ${FRAME_SRC_URLS};`, - `img-src 'self' blob: data: ${IMG_SRC_URLS};`, + `img-src 'self' blob: data: ${IMG_SRC_URLS} ;`, `style-src 'self' 'unsafe-inline' ${STYLE_SRC_URLS};`, `font-src 'self' ${FONT_SRC_URLS};`, `worker-src 'self' blob: data:;`, From e225efec664398106869335701848941da32b8da Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 25 Jun 2024 14:01:30 -0230 Subject: [PATCH 04/18] Chore/fix limit bug (#27540) Match uppercase limit --- .../components/interfaces/SQLEditor/SQLEditor.utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts index c9c8b4a2a89..cb778fcdb57 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts @@ -134,10 +134,10 @@ export const checkIfAppendLimitRequired = (sql: string, limit: number = 0) => { !hasComments && !hasMultipleQueries && cleanedSql.toLowerCase().startsWith('select') && - !cleanedSql.toLowerCase().match(/fetch\s+first/) && - !cleanedSql.endsWith('limit') && - !cleanedSql.endsWith('limit;') && - !cleanedSql.match('limit [0-9]*[;]?$') + !cleanedSql.toLowerCase().match(/fetch\s+first/i) && + !cleanedSql.match(/limit$/i) && + !cleanedSql.match(/limit;$/i) && + !cleanedSql.match(/limit [0-9]*[;]?$/i) return { cleanedSql, appendAutoLimit } } From 5e8efeb415c78f565c1b0c535b658ce13e92c78e Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 25 Jun 2024 14:24:32 -0300 Subject: [PATCH 05/18] docs: add quickstart for iOS and SwiftUI (#27535) --- .../NavigationMenu.constants.ts | 1 + apps/docs/content/guides/getting-started.mdx | 7 + .../quickstarts/ios-swiftui.mdx | 158 ++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index b11aab55b23..fc1d9593c16 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -238,6 +238,7 @@ export const gettingstarted: NavMenuConstant = { { name: 'NuxtJS', url: '/guides/getting-started/quickstarts/nuxtjs' }, { name: 'Vue', url: '/guides/getting-started/quickstarts/vue' }, { name: 'Flutter', url: '/guides/getting-started/quickstarts/flutter' }, + { name: 'iOS SwiftUI', url: '/guides/getting-started/quickstarts/ios-swiftui' }, { name: 'Android Kotlin', url: '/guides/getting-started/quickstarts/kotlin', diff --git a/apps/docs/content/guides/getting-started.mdx b/apps/docs/content/guides/getting-started.mdx index 5d59dbce971..bc458a709d7 100644 --- a/apps/docs/content/guides/getting-started.mdx +++ b/apps/docs/content/guides/getting-started.mdx @@ -131,6 +131,13 @@ hideToc: true 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app.', icon: '/docs/img/icons/flutter-icon', }, + { + title: 'iOS SwiftUI', + href: '/guides/getting-started/quickstarts/ios-swiftui', + description: + 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an iOS app.', + icon: '/docs/img/icons/swift-icon', + }, { title: 'Android Kotlin', href: '/guides/getting-started/quickstarts/kotlin', diff --git a/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx new file mode 100644 index 00000000000..7a7199d3f78 --- /dev/null +++ b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx @@ -0,0 +1,158 @@ +--- +title: 'Use Supabase with iOS and SwiftUI' +subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an iOS app.' +breadcrumb: 'Framework Quickstarts' +hideToc: true +--- + + + + + + + [Create a new project](https://supabase.com/dashboard) in the Supabase Dashboard. + + After your project is ready, create a table in your Supabase database using the [SQL Editor](https://supabase.com/dashboard/project/_/sql) in the Dashboard. Use the following SQL statement to create a `countries` table with some sample data. + + + + + + ```sql SQL_EDITOR + -- Create the table + CREATE TABLE countries ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL + ); + -- Insert some sample data into the table + INSERT INTO countries (name) VALUES ('United States'); + INSERT INTO countries (name) VALUES ('Canada'); + INSERT INTO countries (name) VALUES ('Mexico'); + ```` + + + + + + + + + + Open Xcode > New Project > iOS > App. You can skip this step if you already have a working app. + + + + + + + + + + Install Supabase package dependency using Xcode by following Apple's [tutorial](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app). + + Make sure to add `Supabase` product package as dependency to the application. + + + + + + + + + + Create a new `Supabase.swift` file add a new supabase instance using your project URL and public API (anon) key: + + + + + + + + + ```swift Supabase.swift + import Supabase + + let supabase = SupabaseClient( + supabaseURL: URL(string: "YOUR_SUPABASE_URL")!, + supabaseKey: "YOUR_SUPABASE_ANON_KEY" + ) + ``` + + + + + + + + + + Create a decodable struct to deserialize the data from the database. + + Add the following code to a new file named `Country.swift`. + + + + + + ```swift Supabase.swift + struct Country: Decodable, Identifiable { + let id: Int + let name: String + } + ``` + + + + + + + + + + Use a `task` to fetch the data from the database and display it using a `List`. + + Replace the default `ContentView` with the following code. + + + + + + ```swift ContentView.swift + struct ContentView: View { + + @State var countries: [Country] = [] + + var body: some View { + List(countries) { country in + Text(country.name) + } + .overlay { + if countries.isEmpty { + ProgressView() + } + } + .task { + do { + countries = try await supabase.from("countries").select().execute().value + } catch { + dump(error) + } + } + } + } + ``` + + + + + + + + + Run the app on a simulator or a physical device by hitting `Cmd + R` on Xcode. + + + + + + From 056af94ed8ec79fa5ed11050292b8220c405fb84 Mon Sep 17 00:00:00 2001 From: Arnold Rozon Date: Tue, 25 Jun 2024 13:41:55 -0700 Subject: [PATCH 06/18] docs: Remove extra `await` from Swift sample (#27511) Update with-swift.mdx Removed extra await in `AppView` example --- .../content/guides/getting-started/tutorials/with-swift.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx index e26045321ce..7a71bc87124 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx @@ -272,7 +272,7 @@ struct AppView: View { } } .task { - for await state in await supabase.auth.authStateChanges { + for await state in supabase.auth.authStateChanges { if [.initialSession, .signedIn, .signedOut].contains(state.event) { isAuthenticated = state.session != nil } From 906bfb4d9f4c40b77e754167436ac354e89dfea9 Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Wed, 26 Jun 2024 09:34:31 +0200 Subject: [PATCH 07/18] docs: update humans.txt (#27551) docs: add new human Updates humans.txt with new hire --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 3813ca9595e..f251af7c9c7 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -23,6 +23,7 @@ Deji I Div Arora Divit D Egor Romanov +Etienne Stalmans Fabrizio Fenoglio Francesco Sansalvadore Greg Richardson From 9aa4baf48f4eff0c7604c6f2908f87f546064039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 26 Jun 2024 16:05:44 +0800 Subject: [PATCH 08/18] chore: project addon text adjustments for Orb (#27532) --- .../Settings/Addons/CustomDomainSidePanel.tsx | 63 ++++++++++++------- .../Settings/Addons/IPv4SidePanel.tsx | 50 +++++++++++++-- .../Settings/Addons/PITRSidePanel.tsx | 53 +++++++++++----- .../[slug]/billing/subscription.ts | 1 + packages/api-types/types/api.d.ts | 1 + 5 files changed, 123 insertions(+), 45 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx index c76c0e1d33c..6982dc025b4 100644 --- a/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx @@ -256,36 +256,55 @@ const CustomDomainSidePanel = () => { <> {selectedOption === 'cd_none' || (selectedCustomDomain?.price ?? 0) < (subscriptionCDOption?.variant.price ?? 0) - ? subscription?.billing_via_partner === false && ( + ? subscription?.billing_via_partner === false && + // Old addon billing with upfront payment + subscription.usage_based_billing_project_addons === false && (

- Upon clicking confirm, the add-on is removed immediately and any unused time - in the current billing cycle is added as prorated credits to your organization - and used in subsequent billing cycles. + + Upon clicking confirm, the add-on is removed immediately and any unused time + in the current billing cycle is added as prorated credits to your + organization and used in subsequent billing cycles. +

) : !subscription?.billing_via_partner && (

- Upon clicking confirm, the amount of{' '} - - {formatCurrency(selectedCustomDomain?.price)} - {' '} - will be added to your monthly invoice. The addon is prepaid per month and in - case of a downgrade, you get credits for the remaining time. For the current - billing cycle you're immediately charged a prorated amount for the remaining - days. + {subscription?.usage_based_billing_project_addons === false ? ( + + Upon clicking confirm, the amount of{' '} + + {formatCurrency(selectedCustomDomain?.price)} + {' '} + will be added to your monthly invoice. The addon is prepaid per month and + in case of a downgrade, you get credits for the remaining time. For the + current billing cycle you're immediately charged a prorated amount for the + remaining days. + + ) : ( + + There are no immediate charges. The addon is billed at the end of your + billing cycle based on your usage and prorated to the hour. + + )}

)} - {subscription?.billing_via_partner && - subscription.scheduled_plan_change?.target_plan !== undefined && ( - - - - You have a scheduled subscription change that will be canceled if you change - your custom domain add on. - - - )} + { + // Billed via partner + subscription?.billing_via_partner && + // Project addons are still billed the old way (upfront payment) + subscription?.usage_based_billing_project_addons === false && + // Scheduled billing plan change + subscription.scheduled_plan_change?.target_plan !== undefined && ( + + + + You have a scheduled subscription change that will be canceled if you change + your custom domain add on. + + + ) + } )} diff --git a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx index a23bf816212..0e2c1b2c943 100644 --- a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx @@ -14,7 +14,17 @@ import { useCheckPermissions, useSelectedOrganization } from 'hooks' import { formatCurrency } from 'lib/helpers' import Telemetry from 'lib/telemetry' import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' -import { Alert, Button, cn, IconExternalLink, Radio, SidePanel } from 'ui' +import { + Alert, + Alert_Shadcn_, + AlertDescription_Shadcn_, + Button, + cn, + IconAlertTriangle, + IconExternalLink, + Radio, + SidePanel, +} from 'ui' const IPv4SidePanel = () => { const router = useRouter() @@ -224,7 +234,9 @@ const IPv4SidePanel = () => { <> {selectedOption === 'ipv4_none' || (selectedIPv4?.price ?? 0) < (subscriptionIpV4Option?.variant.price ?? 0) ? ( - subscription?.billing_via_partner === false && ( + subscription?.billing_via_partner === false && + // Old addon billing with upfront payment + subscription.usage_based_billing_project_addons === false && (

Upon clicking confirm, the add-on is removed immediately and any unused time in the current billing cycle is added as prorated credits to your organization and @@ -253,12 +265,38 @@ const IPv4SidePanel = () => {

{!subscription?.billing_via_partner && (

- Upon clicking confirm, the respective amount will be added to your monthly - invoice. The addon is prepaid per month and in case of a downgrade, you get - credits for the remaining time. For the current billing cycle you're - immediately charged a prorated amount for the remaining days. + {subscription?.usage_based_billing_project_addons === false ? ( + + Upon clicking confirm, the respective amount will be added to your monthly + invoice. The addon is prepaid per month and in case of a downgrade, you + get credits for the remaining time. For the current billing cycle you're + immediately charged a prorated amount for the remaining days. + + ) : ( + + There are no immediate charges. The addon is billed at the end of your + billing cycle based on your usage and prorated to the hour. + + )}

)} + + { + // Billed via partner + subscription?.billing_via_partner && + // Project addons are still billed the old way (upfront payment) + subscription?.usage_based_billing_project_addons === false && + // Scheduled billing plan change + subscription.scheduled_plan_change?.target_plan !== undefined && ( + + + + You have a scheduled subscription change that will be canceled if you + change your PITR add on. + + + ) + } )} diff --git a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx index 6e9d6037d86..86883dbef20 100644 --- a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx @@ -383,7 +383,9 @@ const PITRSidePanel = () => { <> {selectedOption === 'pitr_0' || (selectedPitr?.price ?? 0) < (subscriptionPitr?.variant.price ?? 0) - ? subscription?.billing_via_partner === false && ( + ? subscription?.billing_via_partner === false && + // Old addon billing with upfront payment + subscription.usage_based_billing_project_addons === false && (

Upon clicking confirm, the add-on is removed immediately and any unused time in the current billing cycle is added as prorated credits to your organization @@ -392,25 +394,42 @@ const PITRSidePanel = () => { ) : !subscription?.billing_via_partner && (

- Upon clicking confirm, the amount of{' '} - {formatCurrency(selectedPitr?.price)}{' '} - will be added to your monthly invoice. The addon is prepaid per month and in - case of a downgrade, you get credits for the remaining time. For the current - billing cycle you're immediately charged a prorated amount for the remaining - days. + {subscription?.usage_based_billing_project_addons === false ? ( + + Upon clicking confirm, the amount of{' '} + + {formatCurrency(selectedPitr?.price)} + {' '} + will be added to your monthly invoice. The addon is prepaid per month and + in case of a downgrade, you get credits for the remaining time. For the + current billing cycle you're immediately charged a prorated amount for the + remaining days. + + ) : ( + + There are no immediate charges. The addon is billed at the end of your + billing cycle based on your usage and prorated to the hour. + + )}

)} - {subscription?.billing_via_partner && - subscription.scheduled_plan_change?.target_plan !== undefined && ( - - - - You have a scheduled subscription change that will be canceled if you change - your PITR add on. - - - )} + { + // Billed via partner + subscription?.billing_via_partner && + // Project addons are still billed the old way (upfront payment) + subscription?.usage_based_billing_project_addons === false && + // Scheduled billing plan change + subscription.scheduled_plan_change?.target_plan !== undefined && ( + + + + You have a scheduled subscription change that will be canceled if you change + your PITR add on. + + + ) + } )}
diff --git a/apps/studio/pages/api/organizations/[slug]/billing/subscription.ts b/apps/studio/pages/api/organizations/[slug]/billing/subscription.ts index 5baa6aa7966..ed1da8d5d54 100644 --- a/apps/studio/pages/api/organizations/[slug]/billing/subscription.ts +++ b/apps/studio/pages/api/organizations/[slug]/billing/subscription.ts @@ -38,6 +38,7 @@ const handleGet = async (req: NextApiRequest, res: NextApiResponse scheduled_plan_change: null, customer_balance: 0, nano_enabled: false, + usage_based_billing_project_addons: false, } return res.status(200).json(response) diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index b47000ecc80..29b53f47670 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -3063,6 +3063,7 @@ export interface components { plan: components['schemas']['BillingSubscriptionPlan'] project_addons: components['schemas']['BillingProjectAddonResponse'][] scheduled_plan_change: components['schemas']['ScheduledPlanChange'] | null + usage_based_billing_project_addons: boolean usage_billing_enabled: boolean } GetUserContentByIdResponse: { From 15efe26730860d34b883e9f7889725317d6a51d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 26 Jun 2024 16:20:42 +0800 Subject: [PATCH 09/18] fix: EU tax ids for Orb (#27539) --- .../interfaces/Organization/BillingSettings/TaxID/TaxID.tsx | 1 + .../data/organizations/organization-tax-id-update-mutation.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx index a50b80737e2..629c92353dd 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx @@ -95,6 +95,7 @@ const TaxID = () => { ? { type: type, value: sanitizeTaxIdValue({ value, name: form.getValues().name }), + country: selectedTaxId?.countryIso2, } : null, }) diff --git a/apps/studio/data/organizations/organization-tax-id-update-mutation.ts b/apps/studio/data/organizations/organization-tax-id-update-mutation.ts index e0763e33220..cef8a6b336c 100644 --- a/apps/studio/data/organizations/organization-tax-id-update-mutation.ts +++ b/apps/studio/data/organizations/organization-tax-id-update-mutation.ts @@ -7,7 +7,7 @@ import { put, del, handleError } from 'data/fetchers' export type OrganizationTaxIdUpdateVariables = { slug: string - taxId: { type: string; value: string } | null + taxId: { type: string; value: string; country?: string } | null } export async function updateOrganizationTaxId({ slug, taxId }: OrganizationTaxIdUpdateVariables) { @@ -22,7 +22,7 @@ export async function updateOrganizationTaxId({ slug, taxId }: OrganizationTaxId slug, }, }, - body: { type: taxId.type, value: taxId.value }, + body: { type: taxId.type, value: taxId.value, country: taxId.country }, }) if (error) handleError(error) From ae8dafd0a439cb84c836ce55221a95da24eece38 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 26 Jun 2024 17:15:20 +0800 Subject: [PATCH 10/18] Chore/optimizations for table editor query (#27553) * Optimize table editor select query when cutting off column values * Fix auto limit logic to consider offset --- .../SQLEditor/SQLEditor.utils.test.ts | 18 +++++++ .../interfaces/SQLEditor/SQLEditor.utils.ts | 1 + .../data/table-rows/table-rows-query.ts | 48 +++++++++---------- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts index a8a3a4bafda..0f966c253de 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts @@ -19,6 +19,24 @@ describe('SQLEditor.utils.ts:checkIfAppendLimitRequired', () => { const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit) expect(appendAutoLimit).toBe(false) }) + test('Should return false if query already has a limit (check for case-insensitiveness)', () => { + const sql = 'SELECT * FROM countries LIMIT 10;' + const limit = 100 + const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit) + expect(appendAutoLimit).toBe(false) + }) + test('Should return false if query already has a limit and offset', () => { + const sql = 'select * from countries limit 10 offset 0;' + const limit = 100 + const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit) + expect(appendAutoLimit).toBe(false) + }) + test('Should return false if query already has a limit and offset (flip order of limit and offset)', () => { + const sql = 'select * from countries offset 0 limit 1;' + const limit = 100 + const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit) + expect(appendAutoLimit).toBe(false) + }) test('Should return false if query already has a limit, even if no value provided for limit', () => { const sql = 'select * from countries limit' const limit = 100 diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts index cb778fcdb57..8dc6bf5a84c 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts @@ -137,6 +137,7 @@ export const checkIfAppendLimitRequired = (sql: string, limit: number = 0) => { !cleanedSql.toLowerCase().match(/fetch\s+first/i) && !cleanedSql.match(/limit$/i) && !cleanedSql.match(/limit;$/i) && + !cleanedSql.match(/limit [0-9]* offset [0-9]*[;]?$/i) && !cleanedSql.match(/limit [0-9]*[;]?$/i) return { cleanedSql, appendAutoLimit } } diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index 5d0e3e56c54..5ce5bf5d7cc 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -125,28 +125,15 @@ export const getTableRowsSqlQuery = ({ if (!table) return `` - // [Joshen] Only truncate text/json based columns as their length could go really big - // Note: Risk of payload being too large if the user has many many text/json based columns - // although possibly negligible risk. - const truncatedColumns = table.columns - .filter((column) => { - return ( - ((column?.enum ?? []).length > 0 && column.dataType.toLowerCase() === 'array') || - TEXT_TYPES.includes(column.format) || - JSON_TYPES.includes(column.format) - ) - }) - .map((column) => { - if ((column?.enum ?? []).length > 0 && column.dataType.toLowerCase() === 'array') { - return `"${column.name}"::text[]` - } else { - return `case when length("${column.name}"::text) > ${MAX_CHARACTERS} then concat(left("${column.name}"::text, ${MAX_CHARACTERS}), '...') else "${column.name}"::text end "${column.name}"` - } - }) + const arrayBasedColumns = table.columns + .filter( + (column) => (column?.enum ?? []).length > 0 && column.dataType.toLowerCase() === 'array' + ) + .map((column) => `"${column.name}"::text[]`) let queryChains = query .from(table.name, table.schema ?? undefined) - .select(truncatedColumns.length > 0 ? `*,${truncatedColumns.join(',')}` : '*') + .select(arrayBasedColumns.length > 0 ? `*,${arrayBasedColumns.join(',')}` : '*') filters .filter((x) => x.value && x.value != '') @@ -157,10 +144,10 @@ export const getTableRowsSqlQuery = ({ // If sorts is empty, use the primary key as the default sort if (sorts.length === 0) { - const primaryKey = getDefaultOrderByColumn(table) + const defaultOrderByColumn = getDefaultOrderByColumn(table) - if (primaryKey) { - queryChains = queryChains.order(table.name, primaryKey, true, true) + if (defaultOrderByColumn) { + queryChains = queryChains.order(table.name, defaultOrderByColumn, true, true) } } else { sorts.forEach((x) => { @@ -170,9 +157,22 @@ export const getTableRowsSqlQuery = ({ // getPagination is expecting to start from 0 const { from, to } = getPagination((page ?? 1) - 1, limit) - const sql = queryChains.range(from, to).toSql() + const baseSql = queryChains.range(from, to).toSql() - return sql + // [Joshen] Only truncate text/json based columns as their length could go really big + // Note: Risk of payload being too large if the user has many many text/json based columns + // although possibly negligible risk. + const truncatedColumns = table.columns + .filter((column) => TEXT_TYPES.includes(column.format) || JSON_TYPES.includes(column.format)) + .map((column) => { + return `case when length("${column.name}"::text) > ${MAX_CHARACTERS} then concat(left("${column.name}"::text, ${MAX_CHARACTERS}), '...') else "${column.name}"::text end "${column.name}"` + }) + const outputSql = + truncatedColumns.length > 0 + ? `with _temp as (${baseSql.slice(0, -1)}) select *, ${truncatedColumns.join(',')} from _temp` + : baseSql + + return outputSql } export type TableRows = { From 140fbebaeea2ba05ce916e0173020a79e5dfebe8 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 26 Jun 2024 17:35:01 +0800 Subject: [PATCH 11/18] Connection strings to be prefixed with postgresql:// instead of postgres:// (#27533) --- .../Database/DatabaseSettings/DatabaseSettings.utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils.ts b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils.ts index 31da04db4ad..cb257a61176 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils.ts +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils.ts @@ -90,7 +90,7 @@ export const constructConnStringSyntax = ( if (selectedTab === 'uri' || selectedTab === 'nodejs') { if (isMd5) { return [ - { value: 'postgres://', tooltip: undefined }, + { value: 'postgresql://', tooltip: undefined }, { value: '[user]', tooltip: DB_USER_DESC }, { value: ':', tooltip: undefined }, { value: '[password]', tooltip: DB_PASS_DESC }, @@ -109,7 +109,7 @@ export const constructConnStringSyntax = ( ] } else { return [ - { value: 'postgres://', tooltip: undefined }, + { value: 'postgresql://', tooltip: undefined }, { value: '[user]', tooltip: DB_USER_DESC }, ...(usePoolerConnection ? [ From 45e9f39ae50457f3527a5b07a0fb3d29f3576e3c Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 26 Jun 2024 17:58:41 +0800 Subject: [PATCH 12/18] Small alignment fix in UtilityPanel (#27529) --- .../interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx index 7c044980ec6..b8f444dc39a 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx @@ -110,7 +110,7 @@ const UtilityPanel = ({ return ( - +
Results From 3530c022ec40c446e7dee8b429fe8338f86f4acd Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 26 Jun 2024 18:21:49 +0800 Subject: [PATCH 13/18] Support filtering by entity types in table editor (#27555) * Support filtering by entity types in table editor * Address feedback * Small language change --- .../TableEditorLayout/TableEditorMenu.tsx | 62 ++++++++++++++++++- .../entity-types-infinite-query.ts | 9 ++- apps/studio/data/entity-types/keys.ts | 1 + 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 061d1306273..24ebd09141b 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -1,7 +1,7 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { partition } from 'lodash' -import { Plus } from 'lucide-react' +import { Filter, Plus } from 'lucide-react' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' @@ -15,7 +15,17 @@ import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-que import { useCheckPermissions, useLocalStorage } from 'hooks' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import { useTableEditorStateSnapshot } from 'state/table-editor' -import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + Checkbox_Shadcn_, + Label_Shadcn_, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, + Popover_Shadcn_, +} from 'ui' import { InnerSideBarEmptyPanel, InnerSideBarFilterSearchInput, @@ -26,6 +36,7 @@ import { } from 'ui-patterns/InnerSideMenu' import { useProjectContext } from '../ProjectLayout/ProjectContext' import EntityListItem from './EntityListItem' +import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' const TableEditorMenu = () => { const router = useRouter() @@ -34,6 +45,7 @@ const TableEditorMenu = () => { const [showModal, setShowModal] = useState(false) const [searchText, setSearchText] = useState('') + const [visibleTypes, setVisibleTypes] = useState(Object.values(ENTITY_TYPE)) const [sort, setSort] = useLocalStorage<'alphabetical' | 'grouped-alphabetical'>( 'table-editor-sort', 'alphabetical' @@ -57,6 +69,7 @@ const TableEditorMenu = () => { schema: snap.selectedSchemaName, search: searchText || undefined, sort, + filterTypes: visibleTypes, }, { keepPreviousData: Boolean(searchText), @@ -188,6 +201,51 @@ const TableEditorMenu = () => { + + + +
+ ))} +
+
+ + {isLoading && } diff --git a/apps/studio/data/entity-types/entity-types-infinite-query.ts b/apps/studio/data/entity-types/entity-types-infinite-query.ts index f24f0bf96d1..470bd56933b 100644 --- a/apps/studio/data/entity-types/entity-types-infinite-query.ts +++ b/apps/studio/data/entity-types/entity-types-infinite-query.ts @@ -10,6 +10,7 @@ export type EntityTypesVariables = { limit?: number page?: number sort?: 'alphabetical' | 'grouped-alphabetical' + filterTypes?: string[] } & Pick export type EntityTypesResponse = { @@ -28,6 +29,7 @@ export async function getEntityTypes( limit = 100, page = 0, sort = 'alphabetical', + filterTypes, }: EntityTypesVariables, signal?: AbortSignal ) { @@ -55,7 +57,7 @@ export async function getEntityTypes( pg_namespace nc join pg_class c on nc.oid = c.relnamespace where - c.relkind in ('r', 'v', 'm', 'f', 'p') + c.relkind in (${filterTypes === undefined ? `'r', 'v', 'm', 'f', 'p'` : filterTypes.map((x) => `'${x}'`).join(', ')}) and not pg_is_other_temp_schema(nc.oid) and ( pg_has_role(c.relowner, 'USAGE') @@ -113,6 +115,7 @@ export const useEntityTypesQuery = ( search, limit = 100, sort, + filterTypes, }: Omit, { enabled = true, @@ -120,10 +123,10 @@ export const useEntityTypesQuery = ( }: UseInfiniteQueryOptions = {} ) => useInfiniteQuery( - entityTypeKeys.list(projectRef, { schema, search, sort, limit }), + entityTypeKeys.list(projectRef, { schema, search, sort, limit, filterTypes }), ({ signal, pageParam }) => getEntityTypes( - { projectRef, connectionString, schema, search, limit, page: pageParam, sort }, + { projectRef, connectionString, schema, search, limit, page: pageParam, sort, filterTypes }, signal ), { diff --git a/apps/studio/data/entity-types/keys.ts b/apps/studio/data/entity-types/keys.ts index d56b82cb4ba..c04fc7dbd12 100644 --- a/apps/studio/data/entity-types/keys.ts +++ b/apps/studio/data/entity-types/keys.ts @@ -6,6 +6,7 @@ export const entityTypeKeys = { search?: string sort?: 'alphabetical' | 'grouped-alphabetical' limit?: number + filterTypes?: string[] } ) => ['projects', projectRef, 'entity-types', ...(params ? [params] : [])] as const, } From 553f2aa7fcf76229d764428fff742c1c41929b1f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 26 Jun 2024 19:03:35 +0800 Subject: [PATCH 14/18] Trim column name in table editor when updating column (#27522) * Trim column name in table editor when updating column * Address comment * Address comment --- .../ColumnEditor/ColumnEditor.utils.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts index f9ce1f04b0a..43b2511f260 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts @@ -123,18 +123,22 @@ export const generateUpdateColumnPayload = ( const isOriginallyPrimaryKey = primaryKeyColumns.includes(originalColumn.name) // Only append the properties which are getting updated + const name = field.name.trim() const type = field.isArray ? `${field.format}[]` : field.format - const comment = (field.comment?.length ?? '') === 0 ? null : field.comment + const comment = ((field.comment?.length ?? '') === 0 ? null : field.comment)?.trim() + const check = field.check?.trim() const payload: Partial = {} - if (!isEqual(originalColumn.name, field.name)) { - payload.name = field.name + // [Joshen] Trimming on the original name as well so we don't rename columns that already + // contain whitespaces (and accidentally bringing user apps down) + if (!isEqual(originalColumn.name.trim(), name)) { + payload.name = name } - if (!isEqual(originalColumn.comment, comment)) { + if (!isEqual(originalColumn.comment?.trim(), comment)) { payload.comment = comment as string | undefined } - if (!isEqual(originalColumn.check?.trim(), field.check?.trim())) { - payload.check = field.check?.trim() + if (!isEqual(originalColumn.check?.trim(), check)) { + payload.check = check } if (!isEqual(originalColumn.format, type)) { From ee8f901bfd3ccfa104add57b43f006847368d882 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Wed, 26 Jun 2024 13:27:29 +0200 Subject: [PATCH 15/18] feat: RLS policies for realtime channels (#27490) * Add queries and mutations for realtime channels. * Change the layout, add a link for policies and modal for creating a channel. * Add a page for policies. * Add templates for realtime tables. * Fix the spacing in the PolicyDetails component. Add better types. * Pass templateData to the template generating functions. * Various CSS fixes. * Add components for rendering the realtime policies page. * Fix the editing policy flow. * Fix refetching when adding or deleting policies. * Minor various fixes. * Remove an old console.log. * More fixes for templates. * Make the realtime policies open by default when clicking on realtime. * Update the realtime policy templates. * Use a dropdown button for the three types of policies. * Fix a copy-paste mistake. * Fix icons. * Fix the toast. * Clean up the previous components. * Clean up more unneeded code. * Remove templateData prop. * Remove unneeded queries and mutations. * More cleanup. * Reuse the components from the Auth Policies. * Minor fixes for the new components. * Add a feature flag for AuthZ. * Revert some changes. * Fix a build error. * Fix all definitions. * Fix an issue with copying large lines which from templates which are word-wrapped. * Addressed various comments by Joshen. * Revert the UI changes in the Policy Editor. * Update the template names. * Small style fix --------- Co-authored-by: Joshen Lim --- .../AIPolicyEditorPanel/PolicyDetailsV2.tsx | 11 +- .../AIPolicyEditorPanel/PolicyTemplates.tsx | 13 +- .../AIPolicyEditorPanel/RLSCodeEditor.tsx | 10 +- .../Policies/AIPolicyEditorPanel/index.tsx | 4 +- .../interfaces/Auth/Policies/Policies.tsx | 2 +- .../PolicyEditorModal.constants.ts | 138 ++++++++++++++++++ .../PolicyTableRow/PolicyTableRowHeader.tsx | 44 +++--- .../interfaces/Realtime/Policies.tsx | 72 +++++++++ .../layouts/RealtimeLayout/RealtimeLayout.tsx | 14 +- .../RealtimeLayout/RealtimeMenu.utils.ts | 22 ++- apps/studio/data/database-policies/keys.ts | 2 +- .../pages/project/[ref]/auth/policies.tsx | 5 +- .../project/[ref]/realtime/inspector.tsx | 4 +- .../pages/project/[ref]/realtime/policies.tsx | 12 ++ 14 files changed, 312 insertions(+), 41 deletions(-) create mode 100644 apps/studio/components/interfaces/Realtime/Policies.tsx create mode 100644 apps/studio/pages/project/[ref]/realtime/policies.tsx diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyDetailsV2.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyDetailsV2.tsx index fa81a9fc59a..14a131c1aa5 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyDetailsV2.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyDetailsV2.tsx @@ -1,9 +1,10 @@ +import { Check, ChevronsUpDown } from 'lucide-react' import { useEffect, useState } from 'react' +import { UseFormReturn } from 'react-hook-form' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useTablesQuery } from 'data/tables/tables-query' -import { Check, ChevronsUpDown } from 'lucide-react' import { Button, CommandEmpty_Shadcn_, @@ -37,7 +38,13 @@ interface PolicyDetailsV2Props { searchString?: string selectedTable?: string isEditing: boolean - form: any + form: UseFormReturn<{ + name: string + table: string + behavior: string + command: string + roles: string + }> onUpdateCommand: (command: string) => void } diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx index 73fefa60219..31830fc8537 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx @@ -8,7 +8,10 @@ import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock' import CardButton from 'components/ui/CardButton' import CopyButton from 'components/ui/CopyButton' import NoSearchResults from 'components/ui/NoSearchResults' -import { getGeneralPolicyTemplates } from '../PolicyEditorModal/PolicyEditorModal.constants' +import { + getGeneralPolicyTemplates, + getRealtimePolicyTemplates, +} from '../PolicyEditorModal/PolicyEditorModal.constants' interface PolicyTemplatesProps { schema: string @@ -26,7 +29,11 @@ export const PolicyTemplates = ({ onSelectTemplate, }: PolicyTemplatesProps) => { const [search, setSearch] = useState('') - const templates = getGeneralPolicyTemplates(schema, table.length > 0 ? table : 'table_name') + + const templates = + schema === 'realtime' + ? getRealtimePolicyTemplates() + : getGeneralPolicyTemplates(schema, table.length > 0 ? table : 'table_name') const baseTemplates = selectedPolicy !== undefined @@ -68,7 +75,7 @@ export const PolicyTemplates = ({ title={template.name} titleClass="text-sm" className={cn( - 'transition', + 'transition w-full', template.id === selectedTemplate ? '!border-stronger bg-surface-200 hover:!border-stronger' : '' diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx index e51b2284519..6b33be84e2c 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx @@ -4,9 +4,9 @@ import { MutableRefObject, useEffect, useRef } from 'react' import { cn } from 'ui' import { Markdown } from 'components/interfaces/Markdown' -import { noop } from 'lodash' -import { formatQuery } from 'data/sql/format-sql-query' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { formatQuery } from 'data/sql/format-sql-query' +import { noop } from 'lodash' // [Joshen] Is there a way we can just have one single MonacoEditor component that's shared across the dashboard? // Feels like we're creating multiple copies of Editor. I'm keen to make this one the defacto as well so lets make sure @@ -119,6 +119,12 @@ const RLSCodeEditor = ({ onChange() } + // when the value has changed, trigger the onChange callback so that the height of the container can be adjusted. + // Happens when the value wordwraps and is updated via a template. + useEffect(() => { + onChange() + }, [value]) + async function formatPgsql(value: any) { try { const formatted = await formatQuery({ diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx index 6c905ed4de4..39edae2948f 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx @@ -195,9 +195,9 @@ export const AIPolicyEditorPanel = memo(function ({ }, [chatMessages, debugThread]) const { mutate: executeMutation, isLoading: isExecuting } = useExecuteSqlMutation({ - onSuccess: () => { + onSuccess: async () => { // refresh all policies - queryClient.invalidateQueries(databasePoliciesKeys.list(ref)) + await queryClient.invalidateQueries(databasePoliciesKeys.list(ref)) toast.success('Successfully created new policy') onSelectCancel() }, diff --git a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx index a299ea11ac1..24606d04e9a 100644 --- a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx @@ -145,7 +145,7 @@ const Policies = ({
onSelectCreatePolicy(table.name)} onSelectEditPolicy={onSelectEditPolicy} diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts index b71239c9bdb..ddcecc68903 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts @@ -173,3 +173,141 @@ for select using ( roles: [], }, ] + +export const getRealtimePolicyTemplates = (): PolicyTemplate[] => { + const results = [ + { + id: 'policy-broadcast-1', + preview: false, + templateName: 'Allow listening for broadcasts for authenticated users only', + description: 'This policy allows listening for broadcasts for authenticated users only.', + statement: ` +create policy "Allow listening for broadcasts for authenticated users only" +on realtime.messages for select +to authenticated +using ( realtime.messages.extension = 'broadcast' );`.trim(), + name: 'Allow listening for broadcasts for authenticated users only', + definition: "realtime.messages.extension = 'broadcast'", + check: '', + command: 'SELECT', + roles: ['authenticated'], + }, + { + id: 'policy-broadcast-2', + preview: false, + templateName: 'Allow pushing broadcasts for authenticated users only', + description: 'This policy allows pushing broadcasts for authenticated users only.', + statement: ` +create policy "Allow pushing broadcasts for authenticated users only" +ON realtime.messages for update +TO authenticated +using ( realtime.messages.extension = 'broadcast' ) +with check ( realtime.messages.extension = 'broadcast' );`.trim(), + name: 'Allow pushing broadcasts for authenticated users only', + definition: "realtime.messages.extension = 'broadcast'", + check: "realtime.messages.extension = 'broadcast'", + command: 'UPDATE', + roles: ['authenticated'], + }, + { + id: 'policy-broadcast-3', + preview: false, + templateName: 'Allow listening for broadcasts from a specific channel', + description: 'This policy allows listening for broadcasts from a specific channel.', + statement: ` +create policy "Allow listening for broadcasts from a specific channel" +on realtime.messages for select +using ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' );`.trim(), + name: 'Allow listening for broadcasts from a specific channel', + definition: `realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, + check: '', + command: 'SELECT', + roles: [], + }, + { + id: 'policy-broadcast-4', + preview: false, + templateName: 'Allow pushing broadcasts to specific channel', + description: 'This policy allow pushing broadcasts to specific channel.', + statement: ` +create policy "Allow pushing broadcasts to specific channel" +ON realtime.messages for update +using ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' ) +with check ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' );`.trim(), + name: 'Allow pushing broadcasts to specific channel', + definition: `realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, + check: `realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, + command: 'UPDATE', + roles: [], + }, + { + id: 'policy-presences-1', + preview: false, + templateName: 'Allow listening for presences on all channels for authenticated users only', + description: + 'This policy enables listening for presences on all channels for all authenticated users only.', + statement: ` +create policy "Allow listening for presences on all channels for authenticated users only" +on realtime.messages for select +to authenticated +using ( realtime.messages.extension = 'presence' );`.trim(), + name: 'Allow listening for presences on all channels for authenticated users only', + definition: "realtime.messages.extension = 'presence'", + check: '', + command: 'SELECT', + roles: ['authenticated'], + }, + { + id: 'policy-presences-2', + preview: false, + templateName: 'Allow broadcasting presences on all channels for authenticated users only', + description: + 'This policy enables broadcasting presences on all channels for all authenticated users only.', + statement: ` +create policy "Allow broadcasting presences on all channels for authenticated users only" +ON realtime.messages for update +TO authenticated +using ( realtime.messages.extension = 'presence' ) +with check ( realtime.messages.extension = 'presence' ); + ;`.trim(), + name: 'Allow broadcasting presences on all channels for authenticated users only', + definition: "realtime.messages.extension = 'presence'", + check: "realtime.messages.extension = 'presence'", + command: 'UPDATE', + roles: ['authenticated'], + }, + { + id: 'policy-presences-3', + preview: false, + templateName: 'Allow listening for presences from a specific channel', + description: 'This policy enables listening for presences from a specific channel.', + statement: ` +create policy "Allow listening for presences from a specific channel" +on realtime.messages for select +using ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' );`.trim(), + name: 'Allow listening for presences from a specific channel', + definition: `realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, + check: '', + command: 'SELECT', + roles: [], + }, + { + id: 'policy-presences-4', + preview: false, + templateName: 'Publish presence to a specific channel', + description: 'This policy allows publishing presence to a specific channel.', + statement: ` +create policy "Publish presence to a specific channel" +ON realtime.messages for update +using ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' ) +with check ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' ); + ;`.trim(), + name: 'Publish presence to a specific channel', + definition: `realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, + check: `realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, + command: 'UPDATE', + roles: [], + }, + ] as PolicyTemplate[] + return results +} diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx index cfee481bc00..530a2aa9f22 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx @@ -8,7 +8,7 @@ import { useRouter } from 'next/router' import { useIsRLSAIAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useCheckPermissions } from 'hooks' -import { Badge, Button } from 'ui' +import { Badge, Button, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui' interface PolicyTableRowHeaderProps { table: PostgresTable @@ -29,6 +29,10 @@ const PolicyTableRowHeader = ({ const isAiAssistantEnabled = useIsRLSAIAssistantEnabled() const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const isRealtimeSchema = table.schema === 'realtime' + const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages' + const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked + return (
@@ -36,7 +40,7 @@ const PolicyTableRowHeader = ({

{table.name}

- {isLocked && ( + {isTableLocked && ( Locked @@ -48,22 +52,22 @@ const PolicyTableRowHeader = ({
- {!isLocked && ( + {!isTableLocked && (
- - - - - {!canToggleRLS && ( - - + {!isRealtimeMessagesTable ? ( + + + + + {!canToggleRLS && ( +
-
-
- )} -
+ + )} + + ) : null} {!isAiAssistantEnabled && ( From 7e998139a954208423ea6f2bd64f22352d892d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thor=20=E9=9B=B7=E7=A5=9E=20Schaeff?= <5748289+thorwebdev@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:57:49 +0800 Subject: [PATCH 17/18] [DO NOT MERGE] blog: postgis vector tiles (#27436) * chore: update postgis docs dedicated schema. --------- Co-authored-by: Francesco Sansalvadore --- .../guides/database/extensions/postgis.mdx | 6 +- ...24-06-26-postgis-generate-vector-tiles.mdx | 296 ++++++++++++++++++ .../overture_postgis_mvt.png | Bin 0 -> 128290 bytes 3 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 apps/www/_blog/2024-06-26-postgis-generate-vector-tiles.mdx create mode 100644 apps/www/public/images/blog/postgis_vector_tiles/overture_postgis_mvt.png diff --git a/apps/docs/content/guides/database/extensions/postgis.mdx b/apps/docs/content/guides/database/extensions/postgis.mdx index 83e4057a62f..059060efa40 100644 --- a/apps/docs/content/guides/database/extensions/postgis.mdx +++ b/apps/docs/content/guides/database/extensions/postgis.mdx @@ -29,13 +29,17 @@ You can get started with PostGIS by enabling the PostGIS extension in your Supab 1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard. 2. Click on **Extensions** in the sidebar. 3. Search for "postgis" and enable the extension. +4. In the confirmation prompt select "Create a new schema" and name it `gis` for example. ```sql +-- Create a dedicated separate schema +create schema if not exists "gis"; + -- Example: enable the "postgis" extension -create extension postgis with schema extensions; +create extension postgis with schema "gis"; -- Example: disable the "postgis" extension drop extension if exists postgis; diff --git a/apps/www/_blog/2024-06-26-postgis-generate-vector-tiles.mdx b/apps/www/_blog/2024-06-26-postgis-generate-vector-tiles.mdx new file mode 100644 index 00000000000..ae29e4ef30b --- /dev/null +++ b/apps/www/_blog/2024-06-26-postgis-generate-vector-tiles.mdx @@ -0,0 +1,296 @@ +--- +title: 'Generate Vector Tiles with PostGIS' +description: 'Use PostGIS to programmatically generate Mapbox Vector Tiles and render them with MapLibre GL.' +author: bdon,thor_schaeff +image: postgis_vector_tiles/overture_postgis_mvt.png +thumb: postgis_vector_tiles/overture_postgis_mvt.png +categories: + - developers +tags: + - postgres + - postgis + - maps +date: '2024-06-26' +toc_depth: 3 +--- + + + +[Do you prefer audio-visual learning? Watch the video guide!](https://supabase.link/supa-gis-yt-docs) + +[Or jump straight into the code](https://github.com/bdon/supabase-vector-tile) + + + +[Overture Maps Foundation](https://overturemaps.org/) is a [Joint Development Foundation Project](https://jointdevelopment.org/) initiated by Amazon, Meta, Microsoft, and tomtom, aiming to create reliable, easy-to-use, and interoperable open map data. + +Overture Maps allows us to download open map data, like places of interest, as [GeoJSON](https://geojson.org/) which we can transform into SQL and ingest into our Postgres database on Supabase. + +Using PostGIS we can then programmatically generate vector tiles and serve them to our MapLibre GL client using supabase-js. + + + +Vector tiles are packets of geographic data, packaged into pre-defined roughly-square shaped "tiles" for transfer over the web. Map data is requested by a client as a set of "tiles" corresponding to square areas of land of a pre-defined size and location. + +Especially for large datasets, this has the benefit that the data transfer is greatly reduced because because only data within the current viewport, and at the current zoom level needs to be transferred. + + + +In this tutorial, you will learn to + +- Use Overture Maps to download open map places data in GeoJSON format. +- Use GDAL ogr2ogr to transform GeoJSON into SQL statements. +- Import location data and JSON metadata into your Supabase Postgres database using psql. +- Use PostGIS' `ST_AsMVT` to aggregate a set of rows corresponding to a tile layer into a binary vector tile representation. +- Use MapLibre's `addProtocol` to visualize large PostGIS tables by making remote procedure calls with supabase-js. +- Use supabase-js to fetch additional JSON metadata on demand + +## Download open map data with Overture Maps + +Overture Maps provides a [python command-line tool](https://docs.overturemaps.org/getting-data/overturemaps-py/) to download data within a region of interest and converts it to several common geospatial file formats. + +We can download places in Singapore into a GeoJSON file with this command: + +```bash +overturemaps download --bbox=103.570233,1.125077,104.115855,1.490957 -f geojson --type=place -o places.geojson +``` + +Depending on the size of the bounding box this can take quite some time! + +## Transform GeoJSON into SQL + +In the next step, we can use [GDAL ogr2ogr](https://gdal.org/programs/ogr2ogr.html) to transform the GeoJSON file into a PostGIS compatible SQL file. + +You can install `GDAL` via `homebrew brew install gdal` or follow the [download instructions](https://gdal.org/download.html). + +```bash +PG_USE_COPY=true ogr2ogr -f pgdump places.sql places.geojson +``` + +## Import location data into Supabase + +Enable the PostGIS extension on your Supabase Database on a dedicated separate `gis` schema. To do so you can navigate to the [SQL Editor](/dashboard/project/_/sql/new) and run the following SQL, or you can enable the extension from the [Database Extensions Settings](/dashboard/project/_/database/extensions). + +As PostGIS can be quite compute heavy, we recommend enabling it on a decidacted separate schema, for example names `gis`! + +```sql +CREATE SCHEMA IF NOT EXISTS "gis"; +CREATE EXTENSION IF NOT EXISTS "postgis" WITH SCHEMA "gis"; +``` + +Import the open map data into a `places` table in Supabase: + +```bash +psql -h aws-0-us-west-1.pooler.supabase.com -p 5432 -d postgres -U postgres.project-ref < places.sql +``` + +You can find the credentials in the [Database settings](/dashboard/project/_/settings/database) of your Supabase Dashboard. + +### Enable RLS and create a public read policy + +We want the places data to be available publicly, so we can create a row level security policy that enables public read access. + +In your Supabase Dashboard, navigate to the [SQL Editor](/dashboard/project/_/sql/new) and run the following: + +```sql +ALTER TABLE "public"."places" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Enable read access for all users" ON "public"."places" FOR SELECT USING (true); +``` + +## Generate vector tiles with PostGIS + +To programmatically generate vector tiles on client-side request, we need to create a Postgres function that we can invoke via a [remote procedure call](/docs/reference/javascript/rpc). In your SQL Editor, run: + +```sql +CREATE OR REPLACE FUNCTION mvt(z integer, x integer, y integer) +RETURNS text +LANGUAGE plpgsql +AS $$ +DECLARE + mvt_output text; +BEGIN + WITH + -- Define the bounds of the tile using the provided Z, X, Y coordinates + bounds AS ( + SELECT ST_TileEnvelope(z, x, y) AS geom + ), + -- Transform the geometries from EPSG:4326 to EPSG:3857 and clip them to the tile bounds + mvtgeom AS ( + SELECT + -- include the name and id only at zoom 13 to make low-zoom tiles smaller + CASE + WHEN z > 13 THEN id + ELSE NULL + END AS id, + CASE + WHEN z > 13 THEN names::json->>'primary' + ELSE NULL + END AS primary_name, + categories::json->>'main' as main_category, + ST_AsMVTGeom( + ST_Transform(wkb_geometry, 3857), -- Transform the geometry to Web Mercator + bounds.geom, + 4096, -- The extent of the tile in pixels (commonly 256 or 4096) + 0, -- Buffer around the tile in pixels + true -- Clip geometries to the tile extent + ) AS geom + FROM + places, bounds + WHERE + ST_Intersects(ST_Transform(wkb_geometry, 3857), bounds.geom) + ) + -- Generate the MVT from the clipped geometries + SELECT INTO mvt_output encode(ST_AsMVT(mvtgeom, 'places', 4096, 'geom'),'base64') + FROM mvtgeom; + + RETURN mvt_output; +END; +$$; +``` + +To limit the amount of data send over the wire, we limit the amount of metadata to include in the vector tile. For example we add a condition for the zoom level, and only return the place name when the user has zoomed in beyond level 13. + +## Use supabase-js to fetch vector tiles from MapLibre GL client + +You can find the full `index.html` code on [GitHub](https://github.com/bdon/supabase-vector-tile/blob/main/index.html). Here we'll highlight how to add a new protocol to MapLibreGL to fetch the bas64 encoded binary vector tile data via supabase-js so that MapLibre GL can fetch and render the data as your users interact with the map: + +```js index.html +const client = supabase.createClient('you-supabase-api-url', 'your-supabase-anon-key') + +function base64ToArrayBuffer(base64) { + var binaryString = atob(base64) + var bytes = new Uint8Array(binaryString.length) + for (var i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + return bytes +} + +maplibregl.addProtocol('supabase', async (params, abortController) => { + const re = new RegExp(/supabase:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/) + const result = params.url.match(re) + const { data, error } = await client.rpc('mvt', { + z: result[2], + x: result[3], + y: result[4], + }) + const encoded = base64ToArrayBuffer(data) + if (!error) { + return { data: encoded } + } else { + throw new Error(`Tile fetch error:`) + } +}) +``` + +With the supabase protocol registered, we can now add it to our MapLibre GL sources on top of a basemap like [Protomaps](https://protomaps.com/) for example: + +```js index.html +// ... +const map = new maplibregl.Map({ + hash: true, + container: 'map', + style: { + version: 8, + glyphs: 'https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf', + sources: { + supabase: { + type: 'vector', + tiles: ['supabase://boston/{z}/{x}/{y}'], + attribution: '© Overture Maps Foundation', + }, + protomaps: { + type: 'vector', + url: 'https://api.protomaps.com/tiles/v3.json?key=your-protomaps-api-key', + attribution: 'Basemap © OpenStreetMap', + }, + }, + }, +}) +// ... +``` + +## On demand fetch additional JSON metadata + +To limit the amount of data send over the wire, we don't encode all the metadata in the vector tile itself, but rather set up an onclick handler to fetch the additional metadata on demand within the MapLibre GL popup: + +```js index.html +// .. +const popup = new maplibregl.Popup({ + closeButton: true, + closeOnClick: false, + maxWidth: 'none', +}) + +function loadDetails(element, id) { + element.innerHTML = 'loading...' + client + .from('places') + .select( + ` + websites, + socials, + phones, + addresses, + source: sources->0->dataset + ` + ) + .eq('id', id) + .single() + .then(({ data, error }) => { + if (error) return console.error(error) + element.parentElement.innerHTML = `
${JSON.stringify(data, null, 2)}
` + }) +} + +map.on('click', 'overture-pois-text', async (e) => { + if (e.features.length > 0) { + const feature = e.features[0] + console.log(feature) + popup.setHTML( + ` + + + + + + + + + + + + + + + + + +
id:${feature.properties.id}
name:${feature.properties.primary_name}
main_category:${feature.properties.main_category}
details: + + load details + +
+ ` + ) + popup.setLngLat(e.lngLat) + popup.addTo(map) + } +}) +// ... +``` + +## Conclusion + +PostGIS is incredibly powerful, allowing you to programmatically generate vector tiles from table rows stored in Postgres. Paired with Supabase's auto generated REST API and supabase-js client library you're able to build interactive geospatial applications with ease! + +Want to learn more about Maps and PostGIS? Make sure to follow our [Twitter](https://x.com/supabase) and [YouTube](https://www.youtube.com/@Supabase) channels to not miss out! See you then! + +## More Supabase + +- [Watch the video guide](https://supabase.link/supa-gis-yt-docs) +- [Find the code](https://github.com/bdon/supabase-vector-tile) +- [Self-host Maps on Supabase Storage with Protomaps](https://supabase.link/protomaps-storage-yt) +- [Getting started with PostGIS](https://supabase.link/postgis-quickstart-yt) +- [PostGIS docs guide](/docs/guides/database/extensions/postgis) diff --git a/apps/www/public/images/blog/postgis_vector_tiles/overture_postgis_mvt.png b/apps/www/public/images/blog/postgis_vector_tiles/overture_postgis_mvt.png new file mode 100644 index 0000000000000000000000000000000000000000..3072365f50318f4be80f22edd4a48194f5c1fd09 GIT binary patch literal 128290 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 literal 0 HcmV?d00001 From f6aa85ff225980bb0ca839368979ee644606c355 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Wed, 26 Jun 2024 10:35:46 -0500 Subject: [PATCH 18/18] Update pause/restore window start date (#27561) update pause/restore window start date --- .../guides/platform/migrating-and-upgrading-projects.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/migrating-and-upgrading-projects.mdx b/apps/docs/content/guides/platform/migrating-and-upgrading-projects.mdx index 429a1f43dad..1e050755784 100644 --- a/apps/docs/content/guides/platform/migrating-and-upgrading-projects.mdx +++ b/apps/docs/content/guides/platform/migrating-and-upgrading-projects.mdx @@ -65,7 +65,7 @@ If you are upgrading from a significantly older version, you will need to consid #### Time Limits -Starting from 2024-06-13, when a project is paused, users then have a 90-day window to restore the project on the platform from within Supabase Studio. +Starting from 2024-06-24, when a project is paused, users then have a 90-day window to restore the project on the platform from within Supabase Studio. The 90-day window allows Supabase to introduce platform changes that may not be backwards compatible with older backups. Unlike active projects, static backups can't be updated to accommodate such changes.