diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index d5c8479e33fcc..94fb0203d4e8a 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1223,6 +1223,10 @@ export const functions: NavMenuConstant = { name: 'Examples', url: undefined, items: [ + { + name: 'Auth Send Email Hook', + url: '/guides/functions/examples/auth-send-email-hook-react-email-resend', + }, { name: 'CORS support for invoking from the browser', url: '/guides/functions/cors', @@ -1295,6 +1299,10 @@ export const functions: NavMenuConstant = { url: '/guides/functions/examples/sentry-monitoring', }, { name: 'OpenAI API', url: '/guides/ai/examples/openai' }, + { + name: 'React Email', + url: '/guides/functions/examples/auth-send-email-hook-react-email-resend', + }, { name: 'Sending Emails with Resend', url: '/guides/functions/examples/send-emails', diff --git a/apps/docs/content/guides/api/api-keys.mdx b/apps/docs/content/guides/api/api-keys.mdx index d53bff81a4266..33c869cf01116 100644 --- a/apps/docs/content/guides/api/api-keys.mdx +++ b/apps/docs/content/guides/api/api-keys.mdx @@ -28,7 +28,7 @@ select using (true); ``` -And similarity for disallowing access: +And similarly for disallowing access: ```sql create policy "Disallow public access" on profiles to anon for diff --git a/apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx b/apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx index 369a742fa038c..d8945b23bb227 100644 --- a/apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx +++ b/apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx @@ -453,7 +453,9 @@ select defaultActiveId="http-send-email-with-resend" > -You can configure [Resend](https://resend.com/) as the custom email provider through the "Send Email" hook. This allows you to take advantage of Resend's developer-friendly APIs to send emails and leverage [React Email](https://react.email/) for managing your email templates. If you want to send emails through the Supabase Resend integration, which uses Resend's SMTP server, check out [this integration](/partners/integrations/resend) instead. +You can configure [Resend](https://resend.com/) as the custom email provider through the "Send Email" hook. This allows you to take advantage of Resend's developer-friendly APIs to send emails and leverage [React Email](https://react.email/) for managing your email templates. For a more advanced React Email tutorial, refer to [this guide](/docs/guides/functions/examples/auth-send-email-hook-react-email-resend). + +If you want to send emails through the Supabase Resend integration, which uses Resend's SMTP server, check out [this integration](/partners/integrations/resend) instead. Create a `.env` file with the following environment variables: @@ -462,16 +464,22 @@ RESEND_API_KEY=your_resend_api_key SEND_EMAIL_HOOK_SECRET= ``` + + +You can generate the secret in the [Auth Hooks](/dashboard/project/_/auth/hooks) section of the Supabase dashboard. Make sure to remove the `v1,whsec_` prefix! + + + Set the secrets in your Supabase project: ```bash -$ supabase secret set --env-file .env +supabase secrets set --env-file .env ``` Create a new edge function: ```bash -$ supabase functions new send-email +supabase functions new send-email ``` Add the following code to your edge function: @@ -484,6 +492,10 @@ const resend = new Resend(Deno.env.get("RESEND_API_KEY") as string); const hookSecret = Deno.env.get("SEND_EMAIL_HOOK_SECRET") as string; Deno.serve(async (req) => { + if (req.method !== "POST") { + return new Response("not allowed", { status: 400 }); + } + const payload = await req.text(); const headers = Object.fromEntries(req.headers); const wh = new Webhook(hookSecret); diff --git a/apps/docs/content/guides/auth/jwts.mdx b/apps/docs/content/guides/auth/jwts.mdx index 300347bb7c704..5392067c05ec5 100644 --- a/apps/docs/content/guides/auth/jwts.mdx +++ b/apps/docs/content/guides/auth/jwts.mdx @@ -66,7 +66,7 @@ The first segment `eyJhbGciOiJIUzI1NiJ9` is known as the "header", and when deco } ``` -The second segment `eyJzdWIiOiIwMDAxIiwibmFtZSI6IlNhbSBWaW1lcyIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE4MjM5MDIyfQ` contains our original payload: +The second segment eyJzdWIiOiIwMDAxIiwibmFtZSI6IlNhbSBWaW1lcyIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE4MjM5MDIyfQ contains our original payload: ```js { diff --git a/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx b/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx new file mode 100644 index 0000000000000..3eeaded1bed81 --- /dev/null +++ b/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx @@ -0,0 +1,302 @@ +--- +title: 'Custom Auth Emails with React Email and Resend' +description: 'Use the send email hook to send custom auth emails with React Email and Resend in Supabase Edge Functions.' +tocVideo: 'tlA7BomSCgU' +--- + +Use the [send email hook](/docs/guides/auth/auth-hooks/send-email-hook?queryGroups=language&language=http) to send custom auth emails with [React Email](https://react.email/) and [Resend](https://resend.com/) in Supabase Edge Functions. + + + +Prefer to jump straight to the code? [Check out the example on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/auth-hook-react-email-resend). + + + +### Prerequisites + +To get the most out of this guide, you’ll need to: + +- [Create a Resend API key](https://resend.com/api-keys) +- [Verify your domain](https://resend.com/domains) + +Make sure you have the latest version of the [Supabase CLI](https://supabase.com/docs/guides/cli#installation) installed. + +### 1. Create Supabase function + +Create a new function locally: + +```bash +supabase functions new send-email +``` + +### 2. Edit the handler function + +Paste the following code into the `index.ts` file: + +```tsx supabase/functions/send-email/index.ts +import React from 'npm:react@18.3.1' +import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0' +import { Resend } from 'npm:resend@4.0.0' +import { renderAsync } from 'npm:@react-email/components@0.0.22' +import { MagicLinkEmail } from './_templates/magic-link.tsx' + +const resend = new Resend(Deno.env.get('RESEND_API_KEY') as string) +const hookSecret = Deno.env.get('SEND_EMAIL_HOOK_SECRET') as string + +Deno.serve(async (req) => { + if (req.method !== 'POST') { + return new Response('not allowed', { status: 400 }) + } + + const payload = await req.text() + const headers = Object.fromEntries(req.headers) + const wh = new Webhook(hookSecret) + try { + const { + user, + email_data: { token, token_hash, redirect_to, email_action_type }, + } = wh.verify(payload, headers) as { + user: { + email: string + } + email_data: { + token: string + token_hash: string + redirect_to: string + email_action_type: string + site_url: string + token_new: string + token_hash_new: string + } + } + + const html = await renderAsync( + React.createElement(MagicLinkEmail, { + supabase_url: Deno.env.get('SUPABASE_URL') ?? '', + token, + token_hash, + redirect_to, + email_action_type, + }) + ) + + const { error } = await resend.emails.send({ + from: 'welcome ', + to: [user.email], + subject: 'Supa Custom MagicLink!', + html, + }) + if (error) { + throw error + } + } catch (error) { + console.log(error) + return new Response( + JSON.stringify({ + error: { + http_code: error.code, + message: error.message, + }, + }), + { + status: 401, + headers: { 'Content-Type': 'application/json' }, + } + ) + } + + const responseHeaders = new Headers() + responseHeaders.set('Content-Type', 'application/json') + return new Response(JSON.stringify({}), { + status: 200, + headers: responseHeaders, + }) +}) +``` + +### 3. Create React Email Templates + +Create a new folder `_templates` and create a new file `magic-link.tsx` with the following code: + +```tsx supabase/functions/send-email/_templates/magic-link.tsx +import { + Body, + Container, + Head, + Heading, + Html, + Link, + Preview, + Text, +} from 'npm:@react-email/components@0.0.22' +import * as React from 'npm:react@18.3.1' + +interface MagicLinkEmailProps { + supabase_url: string + email_action_type: string + redirect_to: string + token_hash: string + token: string +} + +export const MagicLinkEmail = ({ + token, + supabase_url, + email_action_type, + redirect_to, + token_hash, +}: MagicLinkEmailProps) => ( + + + Log in with this magic link + + + Login + + Click here to log in with this magic link + + + Or, copy and paste this temporary login code: + + {token} + + If you didn't try to login, you can safely ignore this email. + + + + ACME Corp + + , the famouse demo corp. + + + + +) + +export default MagicLinkEmail + +const main = { + backgroundColor: '#ffffff', +} + +const container = { + paddingLeft: '12px', + paddingRight: '12px', + margin: '0 auto', +} + +const h1 = { + color: '#333', + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + fontSize: '24px', + fontWeight: 'bold', + margin: '40px 0', + padding: '0', +} + +const link = { + color: '#2754C5', + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + fontSize: '14px', + textDecoration: 'underline', +} + +const text = { + color: '#333', + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + fontSize: '14px', + margin: '24px 0', +} + +const footer = { + color: '#898989', + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + fontSize: '12px', + lineHeight: '22px', + marginTop: '12px', + marginBottom: '24px', +} + +const code = { + display: 'inline-block', + padding: '16px 4.5%', + width: '90.5%', + backgroundColor: '#f4f4f4', + borderRadius: '5px', + border: '1px solid #eee', + color: '#333', +} +``` + + + +You can find a selection of React Email templates in the [React Email Eamples](https://react.email/examples). + + + +### 4. Deploy the Function + +Deploy function to Supabase: + +```bash +supabase functions deploy send-email --no-verify-jwt +``` + +Note down the function URL, you will need it in the next step! + +### 5. Configure the Send Email Hook + +- Go to the [Auth Hooks](/dashboard/project/_/auth/hooks) section of the Supabase dashboard and create a new "Send Email hook". +- Select HTTPS as the hook type. +- Paste the function URL in the "URL" field. +- Click "Generate Secret" to generate your webhook secret and note it down. +- Click "Create" to save the hook configuration. + +Store these secrets in your `.env` file. + +```bash supabase/functions/.env +RESEND_API_KEY=your_resend_api_key +SEND_EMAIL_HOOK_SECRET= +``` + + + +You can generate the secret in the [Auth Hooks](/dashboard/project/_/auth/hooks) section of the Supabase dashboard. Make sure to remove the `v1,whsec_` prefix! + + + +Set the secrets from the `.env` file: + +```bash +supabase secrets set --env-file supabase/functions/.env +``` + +That's it, now your Supabase Edge Function will be triggered anytime an Auth Email needs to be send to the user! + +## More Resources + +- [Send Email Hooks](/docs/guides/auth/auth-hooks/send-email-hook) +- [Auth Hooks](/docs/guides/auth/auth-hooks) diff --git a/apps/docs/content/guides/platform/going-into-prod.mdx b/apps/docs/content/guides/platform/going-into-prod.mdx index f55771245b1e4..41f55db623671 100644 --- a/apps/docs/content/guides/platform/going-into-prod.mdx +++ b/apps/docs/content/guides/platform/going-into-prod.mdx @@ -57,6 +57,9 @@ After developing your project and deciding it's Production Ready, you should run - You can set up your own backup systems using tools like [pg_dump](https://www.postgresqltutorial.com/postgresql-backup-database/) or [wal-g](https://github.com/wal-g/wal-g). - Nightly backups for Pro Plan projects are available on the Supabase dashboard for up to 7 days. - Point in Time Recovery (PITR) allows a project to be backed up at much shorter intervals. This provides users an option to restore to any chosen point of up to seconds in granularity. In terms of Recovery Point Objective (RPO), Daily Backups would be suitable for projects willing to lose up to 24 hours worth of data. If a lower RPO is required, enable PITR. +- Supabase Projects use disks that offer 99.8-99.9% durability by default. + - Use Read Replicas if you require availability resilience to a disk failure event + - Use PITR if you require durability resilience to a disk failure event - Upgrading to the Supabase Pro Plan will give you [access to our support team](https://supabase.com/dashboard/support/new). ## Rate limiting, resource allocation, & abuse prevention diff --git a/apps/docs/content/guides/platform/log-drains.mdx b/apps/docs/content/guides/platform/log-drains.mdx index c41ba44074432..f2f508f581e3a 100644 --- a/apps/docs/content/guides/platform/log-drains.mdx +++ b/apps/docs/content/guides/platform/log-drains.mdx @@ -6,7 +6,7 @@ description: 'Getting started with Supabase Log Drains' Log drains will send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Team and Enterprise Plans. Log drains is available in the dashboard under [Project Settings > Log Drains](https://supabase.com/dashboard/project/_/settings/log-drains). -You can read about the intiial announcement [here](https://supabase.com/blog/log-drains) and vote for your preferred drains in [this discussion](https://github.com/orgs/supabase/discussions/28324?sort=top). +You can read about the initial announcement [here](https://supabase.com/blog/log-drains) and vote for your preferred drains in [this discussion](https://github.com/orgs/supabase/discussions/28324?sort=top). # Supported Destinations diff --git a/apps/docs/content/guides/platform/read-replicas.mdx b/apps/docs/content/guides/platform/read-replicas.mdx index e3c53a54a89fa..10d4a47ec30b6 100644 --- a/apps/docs/content/guides/platform/read-replicas.mdx +++ b/apps/docs/content/guides/platform/read-replicas.mdx @@ -186,3 +186,16 @@ We combine it with streaming replication to reduce replication lag. Once WAL-G f Replication lag for a specific Read Replica can be monitored through the dashboard. On the [Database Reports page](/dashboard/project/_/reports/database) Read Replicas will have an additional chart under `Replica Information` displaying historical replication lag in seconds. Realtime replication lag in seconds can be observed on the [Infrastructure Settings page](/dashboard/project/_/settings/infrastructure). This is the value on top of the Read Replica. Do note that there is no single threshold to indicate when replication lag should be addressed. It would be fully dependent on the requirements of your project. If you are already ingesting your [project's metrics](/docs/guides/platform/metrics#accessing-the-metrics-endpoint) into your own environment, you can also keep track of replication lag and set alarms accordingly with the metric: `physical_replication_lag_physical_replica_lag_seconds`. + +Some common sources of high replication lag include: + +1. Exclusive locks on tables on the Primary. + Operations such as `drop table`, `reindex` (amongst others) take an Access Exclusive lock on the table. This can result in increasing replication lag for the duration of the lock. +1. Resource Constraints on the database + Heavy utilization on the primary or the replica, if run on an under-resourced project, can result in high replication lag. This includes the characteristics of the disk being utilized (IOPS, Throughput). +1. Long-running transactions on the Primary. + Transactions that run for a long-time on the primary can also result in high replication lag. You can use the `pg_stat_activity` view to identify and terminate such transactions if needed. `pg_stat_activity` is a live view, and does not offer historical data on transactions that might have been active for a long time in the past. + +High replication lag can result in stale data being returned for queries being executed against the affected read replicas. + +You can [consult](https://cloud.google.com/sql/docs/postgres/replication/replication-lag) [additional](https://repost.aws/knowledge-center/rds-postgresql-replication-lag) [resources](https://severalnines.com/blog/what-look-if-your-postgresql-replication-lagging/) on the subject as well. diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 7942deee4a1dc..16d011f689de2 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -21,6 +21,7 @@ Chris Caruso Chris Chandler Chris Copplestone Chris Gwilliams +Chris Stockton Craig Cannon Dave Wilson Deji I diff --git a/apps/studio/.env b/apps/studio/.env index 99cd64513ce8f..be3ac1c7d1734 100644 --- a/apps/studio/.env +++ b/apps/studio/.env @@ -6,7 +6,6 @@ DEFAULT_PROJECT_NAME=Default Project DASHBOARD_USERNAME=supabase DASHBOARD_PASSWORD=supabase1234 - SUPABASE_URL=http://localhost:8000 SUPABASE_PUBLIC_URL=http://localhost:8000 SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE @@ -24,4 +23,4 @@ NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzUwOTQ4MzUsImV4cCI6MTk5MDY3MDgzNX0.0PMlOxtKL4O9GGZuAP_Xl4f-Tut1qOnW4bNEmAtoB8w NEXT_PUBLIC_SUPABASE_URL=https://xguihxuzqibwxjnimxev.supabase.co -DOCKER_SOCKET_LOCATION=/var/run/docker.sock \ No newline at end of file +DOCKER_SOCKET_LOCATION=/var/run/docker.sock diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx index 29ee738b47398..b3f598d9b0a58 100644 --- a/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx +++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx @@ -1,8 +1,10 @@ import { groupBy } from 'lodash' +import { Plus } from 'lucide-react' import Link from 'next/link' import AlertError from 'components/ui/AlertError' import NoSearchResults from 'components/ui/NoSearchResults' +import PartnerIcon from 'components/ui/PartnerIcon' import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query' import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only' import { @@ -16,7 +18,6 @@ import { ResourceWarning, useResourceWarningsQuery } from 'data/usage/resource-w import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { makeRandomString } from 'lib/helpers' -import { Plus } from 'lucide-react' import type { Organization, ResponseError } from 'types' import { Button, cn } from 'ui' import ProjectCard from './ProjectCard' @@ -222,8 +223,10 @@ const OrganizationProjects = ({ return (
-

{organization.name}

- +
+

{organization.name}

{' '} + +
{!!overdueInvoices.length && (
- {!canReadBillingAddress ? ( + {selectedOrganization?.managed_by !== undefined && + selectedOrganization?.managed_by !== 'supabase' ? ( + + ) : !canReadBillingAddress ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx index c495058ddd53a..bbc4d550150fa 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx @@ -15,11 +15,13 @@ import AlertError from 'components/ui/AlertError' import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection' import NoPermission from 'components/ui/NoPermission' +import PartnerManagedResource from 'components/ui/PartnerManagedResource' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { organizationKeys } from 'data/organizations/keys' import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { BASE_PATH } from 'lib/constants' import { getURL } from 'lib/helpers' import { @@ -40,6 +42,7 @@ import DeletePaymentMethodModal from './DeletePaymentMethodModal' const PaymentMethods = () => { const { slug } = useParams() + const selectedOrganization = useSelectedOrganization() const queryClient = useQueryClient() const [selectedMethodForUse, setSelectedMethodForUse] = useState() const [selectedMethodToDelete, setSelectedMethodToDelete] = useState() @@ -76,7 +79,16 @@ const PaymentMethods = () => {
- {!canReadPaymentMethods ? ( + {selectedOrganization?.managed_by !== undefined && + selectedOrganization?.managed_by !== 'supabase' ? ( + + ) : !canReadPaymentMethods ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/EnterpriseCard.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/EnterpriseCard.tsx index d85e884eae3d2..30e804702ca2c 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/EnterpriseCard.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/EnterpriseCard.tsx @@ -6,7 +6,7 @@ import { pickFeatures } from 'shared-data/plans' export interface EnterpriseCardProps { plan: PricingInformation isCurrentPlan: boolean - billingPartner: 'fly' | 'aws' | undefined + billingPartner: 'fly' | 'aws' | 'vercel_marketplace' | undefined } const EnterpriseCard = ({ plan, isCurrentPlan, billingPartner }: EnterpriseCardProps) => { diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index 84bf3dd4bfad7..36df82b6e8919 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -25,13 +25,24 @@ import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' import { formatCurrency } from 'lib/helpers' import { pickFeatures, pickFooter, plans as subscriptionsPlans } from 'shared-data/plans' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' -import { Button, IconCheck, IconInfo, Modal, SidePanel, cn } from 'ui' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + IconCheck, + IconInfo, + Modal, + SidePanel, + cn, +} from 'ui' import DowngradeModal from './DowngradeModal' import EnterpriseCard from './EnterpriseCard' import ExitSurveyModal from './ExitSurveyModal' import MembersExceedLimitModal from './MembersExceedLimitModal' import PaymentMethodSelection from './PaymentMethodSelection' import UpgradeSurveyModal from './UpgradeModal' +import PartnerIcon from 'components/ui/PartnerIcon' const PlanUpdateSidePanel = () => { const router = useRouter() @@ -231,7 +242,12 @@ const PlanUpdateSidePanel = () => { setSelectedTier(plan.id as any)} tooltip={{ content: { @@ -241,7 +257,10 @@ const PlanUpdateSidePanel = () => { ? 'Reach out to us via support to update your plan from Enterprise' : !canUpdateSubscription ? 'You do not have permission to change the subscription plan' - : undefined, + : plan.id === 'tier_team' && + selectedOrganization?.managed_by === 'vercel-marketplace' + ? 'The Team plan is currently unavailable for Vercel Marketplace managed organizations' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx index dd9867b448fe6..ef0a0670fe6b0 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx @@ -16,10 +16,12 @@ import AlertError from 'components/ui/AlertError' import { FormActions } from 'components/ui/Forms/FormActions' import NoPermission from 'components/ui/NoPermission' import Panel from 'components/ui/Panel' +import PartnerManagedResource from 'components/ui/PartnerManagedResource' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrganizationTaxIdQuery } from 'data/organizations/organization-tax-id-query' import { useOrganizationTaxIdUpdateMutation } from 'data/organizations/organization-tax-id-update-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { Button, FormControl_Shadcn_, @@ -39,6 +41,8 @@ import { checkTaxIdEqual, sanitizeTaxIdValue } from './TaxID.utils' const TaxID = () => { const { slug } = useParams() + const selectedOrganization = useSelectedOrganization() + const { data: taxId, error, isLoading, isSuccess, isError } = useOrganizationTaxIdQuery({ slug }) const { mutate: updateTaxId, isLoading: isUpdating } = useOrganizationTaxIdUpdateMutation({ onSuccess: () => { @@ -137,7 +141,16 @@ const TaxID = () => {
- {!canReadTaxId ? ( + {selectedOrganization?.managed_by !== undefined && + selectedOrganization?.managed_by !== 'supabase' ? ( + + ) : !canReadTaxId ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx index 3f5e7d0b5d718..c9f057bce1101 100644 --- a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx +++ b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx @@ -18,6 +18,7 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { formatCurrency } from 'lib/helpers' import { Button, IconChevronLeft, IconChevronRight, IconDownload, IconFileText } from 'ui' +import PartnerManagedResource from 'components/ui/PartnerManagedResource' const PAGE_LIMIT = 10 @@ -30,14 +31,20 @@ const InvoicesSettings = () => { const canReadInvoices = useCheckPermissions(PermissionAction.READ, 'invoices') - const { data: count, isError: isErrorCount } = useInvoicesCountQuery({ - slug, - }) - const { data, error, isLoading, isError, isSuccess } = useInvoicesQuery({ - slug, - offset, - limit: PAGE_LIMIT, - }) + const { data: count, isError: isErrorCount } = useInvoicesCountQuery( + { + slug, + }, + { enabled: selectedOrganization?.managed_by === 'supabase' } + ) + const { data, error, isLoading, isError, isSuccess } = useInvoicesQuery( + { + slug, + offset, + limit: PAGE_LIMIT, + }, + { enabled: selectedOrganization?.managed_by === 'supabase' } + ) const invoices = data || [] useEffect(() => { @@ -61,6 +68,25 @@ const InvoicesSettings = () => { ) } + if ( + selectedOrganization?.managed_by !== undefined && + selectedOrganization?.managed_by !== 'supabase' + ) { + return ( + + + + ) + } + return ( {isLoading && } diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx index 6ac7b40a40ea0..45332ef9e6995 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx @@ -14,11 +14,16 @@ import { Badge, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { getUserDisplayName, isInviteExpired } from '../Organization.utils' import { MemberActions } from './MemberActions' +import PartnerIcon from 'components/ui/PartnerIcon' interface MemberRowProps { member: OrganizationMember } +const MEMBER_ORIGIN_TO_MANAGED_BY = { + vercel: 'vercel-marketplace', +} as const + export const MemberRow = ({ member }: MemberRowProps) => { const { slug } = useParams() const { profile } = useProfile() @@ -85,6 +90,18 @@ export const MemberRow = ({ member }: MemberRowProps) => { )} {member.primary_email === profile?.primary_email && You} + + {(member.metadata as any)?.origin && ( + + )} diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx index e50e7346d8429..245b265dfff7b 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx @@ -2,7 +2,9 @@ import { useRouter } from 'next/router' import { ControllerRenderProps, UseFormReturn } from 'react-hook-form' import { useDefaultRegionQuery } from 'data/misc/get-default-region-query' +import { useFlag } from 'hooks/ui/useFlag' import { PROVIDERS } from 'lib/constants' +import type { CloudProvider } from 'shared-data' import { SelectContent_Shadcn_, SelectGroup_Shadcn_, @@ -12,7 +14,6 @@ import { Select_Shadcn_, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import type { CloudProvider } from 'shared-data' import { getAvailableRegions } from './ProjectCreation.utils' interface RegionSelectorProps { @@ -21,13 +22,23 @@ interface RegionSelectorProps { form: UseFormReturn } +// [Joshen] Let's use a library to maintain the flag SVGs in the future +// I tried using https://flagpack.xyz/docs/development/react/ but couldn't get it to render +// ^ can try again next time + export const RegionSelector = ({ cloudProvider, field }: RegionSelectorProps) => { const router = useRouter() + const newRegions = ['EAST_US_2', 'WEST_EU_3', 'CENTRAL_EU_2', 'NORTH_EU'] + const enableNewRegions = useFlag('enableNewRegions') + const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' || process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' const availableRegions = getAvailableRegions(PROVIDERS[cloudProvider].id) + const regionsArray = enableNewRegions + ? Object.entries(availableRegions) + : Object.entries(availableRegions).filter(([key]) => !newRegions.includes(key)) const { isLoading: isLoadingDefaultRegion } = useDefaultRegionQuery({ cloudProvider, @@ -56,15 +67,15 @@ export const RegionSelector = ({ cloudProvider, field }: RegionSelectorProps) => - {Object.keys(availableRegions).map((option: string, i) => { - const label = Object.values(availableRegions)[i].displayName as string + {regionsArray.map(([key, value]) => { + const label = value.displayName as string return ( - +
region icon {label}
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts index ef07cde26c1f8..36c994eeb4443 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts @@ -40,6 +40,11 @@ export const AWS_REGIONS_COORDINATES: { [key: string]: [number, number] } = { SOUTH_ASIA: [72.88, 19.08], OCEANIA: [151.2, -33.86], SOUTH_AMERICA: [-46.38, -23.34], + + // CENTRAL_EU_2: To find, + // EAST_US_2: [-83, 39.96], + // NORTH_EU: To find, + // WEST_EU_3: [2.35, 48.86], } export const FLY_REGIONS_COORDINATES: { [key: string]: [number, number] } = { diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.utils.ts b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.utils.ts index be22cf22df4ef..acb575bbb542f 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.utils.ts +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.utils.ts @@ -6,10 +6,12 @@ import type { LoadBalancer } from 'data/read-replicas/load-balancers-query' import type { Database } from 'data/read-replicas/replicas-query' import { AVAILABLE_REPLICA_REGIONS, + AWS_REGIONS_COORDINATES, NODE_ROW_HEIGHT, NODE_SEP, NODE_WIDTH, } from './InstanceConfiguration.constants' +import { AWS_REGIONS, AWS_REGIONS_KEYS } from 'shared-data' // [Joshen] Just FYI the nodes generation assumes each project only has one load balancer // Will need to change if this eventually becomes otherwise @@ -48,9 +50,27 @@ export const generateNodes = ({ } : undefined - const primaryRegion = AVAILABLE_REPLICA_REGIONS.find((region) => - primary.region.includes(region.region) - ) + // [Joshen] We should be finding from AVAILABLE_REPLICA_REGIONS instead + // but because the new regions (zurich, stockholm, ohio, paris) dont have + // coordinates yet in AWS_REGIONS_COORDINATES - we'll need to add them in once + // they are ready to spin up coordinates for + const primaryRegion = Object.keys(AWS_REGIONS) + .map((key) => { + return { + key: key as AWS_REGIONS_KEYS, + name: AWS_REGIONS?.[key as AWS_REGIONS_KEYS].displayName, + region: AWS_REGIONS?.[key as AWS_REGIONS_KEYS].code, + coordinates: AWS_REGIONS_COORDINATES[key], + } + }) + .find((region) => primary.region.includes(region.region)) + + // [Joshen] Once we have the coordinates for Zurich and Stockholm, we can remove the above + // and uncomment below for better simplicity + // const primaryRegion = AVAILABLE_REPLICA_REGIONS.find((region) => + // primary.region.includes(region.region) + // ) + const primaryNode: Node = { position, id: primary.identifier, diff --git a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx index a645d5eccb738..0503b6e635ca3 100644 --- a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx +++ b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx @@ -2,6 +2,7 @@ import Head from 'next/head' import { useRouter } from 'next/router' import { PropsWithChildren, useEffect } from 'react' +import PartnerIcon from 'components/ui/PartnerIcon' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { withAuth } from 'hooks/misc/withAuth' @@ -46,6 +47,7 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren, })) .sort((a, b) => a.label.localeCompare(b.label)) @@ -79,14 +81,12 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren ) @@ -144,10 +146,12 @@ const SidebarItem = ({ links, subitems, subitemsParentKey }: SidebarItemProps) = label={y.label} onClick={y.onClick} isExternal={link.isExternal || false} + icon={link.icon} /> )) render = [render, ...subItemsRender] } + return render })} @@ -167,15 +171,16 @@ const SidebarLinkItem = ({ isSubitem, isExternal, onClick, + icon, }: SidebarLinkProps) => { if (isUndefined(href)) { let icon if (isExternal) { - icon = + icon = } if (label === 'Log out') { - icon = + icon = } return ( @@ -185,7 +190,7 @@ const SidebarLinkItem = ({ style={{ marginLeft: isSubitem ? '.5rem' : '0rem', }} - active={isActive ? true : false} + active={isActive} onClick={onClick || (() => {})} icon={icon} > @@ -194,20 +199,28 @@ const SidebarLinkItem = ({ ) } + console.log(label, isActive) + return ( {isExternal && ( - + )} - - {isSubitem ?

{label}

: label} -
+
+ + {isSubitem ?

{label}

: label} +
+ {icon} +
) diff --git a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx index b5ef61ce27057..e870b0801225d 100644 --- a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx +++ b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx @@ -23,6 +23,7 @@ import { Popover_Shadcn_, ScrollArea, } from 'ui' +import PartnerIcon from 'components/ui/PartnerIcon' interface OrganizationDropdownProps { isNewNav?: boolean @@ -83,7 +84,10 @@ const OrganizationDropdown = ({ isNewNav = false }: OrganizationDropdownProps) = onClick={() => setOpen(false)} > - {org.name} +
+ {org.name} + +
{org.slug === slug && } diff --git a/apps/studio/components/layouts/OrganizationLayout.tsx b/apps/studio/components/layouts/OrganizationLayout.tsx index f6d7e66a8a8bb..24c7b8f3f2ba4 100644 --- a/apps/studio/components/layouts/OrganizationLayout.tsx +++ b/apps/studio/components/layouts/OrganizationLayout.tsx @@ -1,31 +1,34 @@ -import { useRouter } from 'next/router' import type { PropsWithChildren } from 'react' import { useParams } from 'common' +import PartnerIcon from 'components/ui/PartnerIcon' +import { PARTNER_TO_NAME } from 'components/ui/PartnerManagedResource' +import { useVercelRedirectQuery } from 'data/integrations/vercel-redirect-query' import { useCurrentPath } from 'hooks/misc/useCurrentPath' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useFlag } from 'hooks/ui/useFlag' +import { ExternalLink } from 'lucide-react' import Link from 'next/link' -import { NavMenu, NavMenuItem } from 'ui' -import { useOrgSubscriptionQuery } from '../../data/subscriptions/org-subscription-query' +import { Alert_Shadcn_, AlertTitle_Shadcn_, Button, NavMenu, NavMenuItem } from 'ui' import AccountLayout from './AccountLayout/AccountLayout' import { ScaffoldContainer, ScaffoldDivider, ScaffoldHeader, ScaffoldTitle } from './Scaffold' import SettingsLayout from './SettingsLayout/SettingsLayout' const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => { const selectedOrganization = useSelectedOrganization() - const router = useRouter() const currentPath = useCurrentPath() const { slug } = useParams() const invoicesEnabledOnProfileLevel = useIsFeatureEnabled('billing:invoices') - const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: slug }) - const isNotOrgWithPartnerBilling = !(subscription?.billing_via_partner ?? true) - const invoicesEnabled = invoicesEnabledOnProfileLevel && isNotOrgWithPartnerBilling + const invoicesEnabled = invoicesEnabledOnProfileLevel const navLayoutV2 = useFlag('navigationLayoutV2') + const { data, isSuccess } = useVercelRedirectQuery({ + installationId: selectedOrganization?.partner_id, + }) + if (navLayoutV2) { return {children} } @@ -91,7 +94,25 @@ const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => { + + + {selectedOrganization && selectedOrganization?.managed_by !== 'supabase' && ( + + + + + This organization is managed by {PARTNER_TO_NAME[selectedOrganization.managed_by]}. + + + + + )} + {children} ) diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 369b961d0a5ef..9748e51596ac8 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -352,7 +352,7 @@ const NavigationBar = () => { - + 账户设置 @@ -361,7 +361,11 @@ const NavigationBar = () => { onClick={() => snap.setShowFeaturePreviewModal(true)} onSelect={() => snap.setShowFeaturePreviewModal(true)} > - + 功能预览 diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconButton.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconButton.tsx index 99ff2ccd90b4c..eac06187e0714 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconButton.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconButton.tsx @@ -23,7 +23,7 @@ export const NavigationIconButton = forwardRef< props.className )} > -
{icon}
+
{icon}
+ showTooltip?: boolean + tooltipText?: string + size?: 'small' | 'medium' | 'large' +} + +function PartnerIcon({ + organization, + showTooltip = true, + tooltipText = 'This organization is managed by Vercel Marketplace.', + size = 'small', +}: PartnerIconProps) { + if (organization.managed_by === 'vercel-marketplace') { + const icon = ( + + + + ) + + if (!showTooltip) { + return ( +
+ {icon} +
+ ) + } + + return ( + + +
+ {icon} +
+
+ {tooltipText} +
+ ) + } + + return null +} + +export default PartnerIcon diff --git a/apps/studio/components/ui/PartnerManagedResource.tsx b/apps/studio/components/ui/PartnerManagedResource.tsx new file mode 100644 index 0000000000000..48bc4fbcc75e9 --- /dev/null +++ b/apps/studio/components/ui/PartnerManagedResource.tsx @@ -0,0 +1,54 @@ +import { ExternalLink } from 'lucide-react' + +import { useVercelRedirectQuery } from 'data/integrations/vercel-redirect-query' +import { Alert_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui' +import PartnerIcon from './PartnerIcon' + +interface PartnerManagedResourceProps { + partner: 'vercel-marketplace' | 'aws-marketplace' + resource: string + cta?: { + installationId?: string + path?: string + } +} + +export const PARTNER_TO_NAME = { + 'vercel-marketplace': 'Vercel Marketplace', + 'aws-marketplace': 'AWS Marketplace', +} as const + +function PartnerManagedResource({ partner, resource, cta }: PartnerManagedResourceProps) { + const ctaEnabled = cta !== undefined + + const { data, isLoading, isError } = useVercelRedirectQuery( + { + installationId: cta?.installationId, + }, + { + enabled: ctaEnabled, + } + ) + + const ctaUrl = (data?.url ?? '') + (cta?.path ?? '') + + return ( + + + + + {resource} are managed by {PARTNER_TO_NAME[partner]}. + + + {ctaEnabled && ( + + )} + + ) +} + +export default PartnerManagedResource diff --git a/apps/studio/data/integrations/keys.ts b/apps/studio/data/integrations/keys.ts index 71ac8bea3ad66..1a86cfdfa407e 100644 --- a/apps/studio/data/integrations/keys.ts +++ b/apps/studio/data/integrations/keys.ts @@ -24,4 +24,5 @@ export const integrationKeys = { githubBranchesList: (connectionId: number | undefined) => ['github-branches', connectionId], githubConnectionsList: (organizationId: number | undefined) => ['organizations', organizationId, 'github-connections'] as const, + vercelRedirect: (installationId?: string) => ['vercel-redirect', installationId] as const, } diff --git a/apps/studio/data/integrations/vercel-redirect-query.ts b/apps/studio/data/integrations/vercel-redirect-query.ts new file mode 100644 index 0000000000000..3b25224fb0d45 --- /dev/null +++ b/apps/studio/data/integrations/vercel-redirect-query.ts @@ -0,0 +1,42 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { get, handleError } from 'data/fetchers' +import type { ResponseError } from 'types' +import { integrationKeys } from './keys' + +export type VercelRedirectVariables = { + installationId?: string +} + +export async function getVercelRedirect( + { installationId }: VercelRedirectVariables, + signal?: AbortSignal +) { + if (!installationId) throw new Error('installationId is required') + + const { data, error } = await get(`/platform/vercel/redirect/{installation_id}`, { + params: { path: { installation_id: installationId } }, + signal, + }) + if (error) handleError(error) + return data +} + +export type VercelRedirectData = Awaited> +export type VercelRedirectError = ResponseError + +export const useVercelRedirectQuery = ( + { installationId }: VercelRedirectVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + integrationKeys.vercelRedirect(installationId), + ({ signal }) => getVercelRedirect({ installationId }, signal), + { + enabled: enabled && typeof installationId !== 'undefined', + ...options, + } + ) diff --git a/apps/studio/data/organizations/organizations-query.ts b/apps/studio/data/organizations/organizations-query.ts index afed1dde457ec..0c1fc04fe3543 100644 --- a/apps/studio/data/organizations/organizations-query.ts +++ b/apps/studio/data/organizations/organizations-query.ts @@ -1,16 +1,30 @@ import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { components } from 'api-types' import { get, handleError } from 'data/fetchers' import type { Organization, ResponseError } from 'types' import { organizationKeys } from './keys' +function castOrganizationResponseToOrganization( + org: components['schemas']['OrganizationResponse'] +): Organization { + return { + ...org, + billing_email: org.billing_email ?? 'Unknown', + managed_by: org.slug.startsWith('vercel_icfg_') ? 'vercel-marketplace' : 'supabase', + partner_id: org.slug.startsWith('vercel_') ? org.slug.replace('vercel_', '') : undefined, + } +} + export async function getOrganizations(signal?: AbortSignal): Promise { const { data, error } = await get('/platform/organizations', { signal }) if (error) handleError(error) if (!Array.isArray(data)) return [] - const sorted = (data as Organization[]).sort((a, b) => a.name.localeCompare(b.name)) - return sorted + return data + .map(castOrganizationResponseToOrganization) + .sort((a, b) => a.name.localeCompare(b.name)) } export type OrganizationsData = Awaited> diff --git a/apps/studio/public/img/regions/CENTRAL_EU_2.svg b/apps/studio/public/img/regions/CENTRAL_EU_2.svg new file mode 100644 index 0000000000000..65be7b4fa7a97 --- /dev/null +++ b/apps/studio/public/img/regions/CENTRAL_EU_2.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/apps/studio/public/img/regions/EAST_US_2.svg b/apps/studio/public/img/regions/EAST_US_2.svg new file mode 100644 index 0000000000000..3189d8e2dc3a7 --- /dev/null +++ b/apps/studio/public/img/regions/EAST_US_2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/studio/public/img/regions/NORTH_EU.svg b/apps/studio/public/img/regions/NORTH_EU.svg new file mode 100644 index 0000000000000..5d82e9f5f828d --- /dev/null +++ b/apps/studio/public/img/regions/NORTH_EU.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/studio/public/img/regions/WEST_EU_3.svg b/apps/studio/public/img/regions/WEST_EU_3.svg new file mode 100644 index 0000000000000..deb14284ba46e --- /dev/null +++ b/apps/studio/public/img/regions/WEST_EU_3.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/studio/types/base.ts b/apps/studio/types/base.ts index 83db1c8fe0a5e..e593dcf2d2c3f 100644 --- a/apps/studio/types/base.ts +++ b/apps/studio/types/base.ts @@ -11,6 +11,8 @@ export interface Organization { subscription_id?: string | null restriction_status: 'grace_period' | 'grace_period_over' | 'restricted' | null restriction_data: Record + managed_by: 'supabase' | 'vercel-marketplace' | 'aws-marketplace' + partner_id?: string } /** diff --git a/apps/www/app/api-v2/ticket-og/route.tsx b/apps/www/app/api-v2/ticket-og/route.tsx index d4e890c1a6fbc..a041b7fcf6107 100644 --- a/apps/www/app/api-v2/ticket-og/route.tsx +++ b/apps/www/app/api-v2/ticket-og/route.tsx @@ -366,9 +366,15 @@ export async function GET(req: Request, res: Response) { "role"
: - - "{user.role}" - + {user.role ? ( + + "{user.role}" + + ) : ( + + null + + )} , @@ -379,9 +385,15 @@ export async function GET(req: Request, res: Response) { "company" : - - "{user.company}" - + {user.company ? ( + + "{user.company}" + + ) : ( + + null + + )} , @@ -392,9 +404,15 @@ export async function GET(req: Request, res: Response) { "location" : - - "{user.location}" - + {user.location ? ( + + "{user.location}" + + ) : ( + + null + + )} , diff --git a/apps/www/components/Globe.tsx b/apps/www/components/Globe.tsx index d9c94d2cb94a9..47ecce12c6d42 100644 --- a/apps/www/components/Globe.tsx +++ b/apps/www/components/Globe.tsx @@ -1,16 +1,22 @@ import createGlobe from 'cobe' -import { useEffect, useRef } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { useTheme } from 'next-themes' +import { debounce } from 'lodash' const Globe = () => { const { resolvedTheme } = useTheme() const canvasRef = useRef() + let rotation: number = 0 + let width: number = 0 + const onResize = useCallback( + () => canvasRef.current && (width = canvasRef.current.offsetWidth), + [resolvedTheme] + ) + useEffect(() => { - let rotation: number = 0 - let width: number = 0 - const onResize = () => canvasRef.current && (width = canvasRef.current.offsetWidth) - window.addEventListener('resize', onResize) + const debouncedResize = debounce(onResize, 10) + window.addEventListener('resize', debouncedResize) onResize() const cobe = createGlobe(canvasRef.current, { devicePixelRatio: 2, @@ -50,24 +56,17 @@ const Globe = () => { state.height = width * 2 }, }) - setTimeout(() => (canvasRef.current.style.opacity = '1')) + setTimeout(() => (canvasRef.current.style.opacity = '0.8'), 10) return () => { - cobe.destroy() window.removeEventListener('resize', onResize) + cobe.destroy() } }, [resolvedTheme]) return ( ) } diff --git a/apps/www/components/LaunchWeek/12/Ticket/Ticket.tsx b/apps/www/components/LaunchWeek/12/Ticket/Ticket.tsx index 25e0ee4678436..07f022ad6cf45 100644 --- a/apps/www/components/LaunchWeek/12/Ticket/Ticket.tsx +++ b/apps/www/components/LaunchWeek/12/Ticket/Ticket.tsx @@ -52,9 +52,9 @@ export default function Ticket() { .single() ` - const HAS_ROLE = user.role - const HAS_COMPANY = user.company - const HAS_LOCATION = user.location + const HAS_ROLE = true // user.role + const HAS_COMPANY = true // user.company + const HAS_LOCATION = true // user.location // Keep following indentation for proper json layout with conditionals const responseJson = codeBlock` @@ -63,7 +63,10 @@ export default function Ticket() { "name": "${user.name}", "username": "${username}", "ticket_number": "${ticketNumber}", - ${HAS_ROLE && ` "role": "${user.role}",\n`}${HAS_COMPANY && ` "company": "${user.company}",\n`}${HAS_LOCATION && ` "location": "${user.location}",\n`}}, + "role": ${user.role ? `"${user.role}"` : 'null'}, + "company": ${user.company ? `"${user.company}"` : 'null'}, + "location": ${user.location ? `"${user.location}"` : 'null'}, + }, "error": null } ` diff --git a/apps/www/components/Nav/HamburgerMenu.tsx b/apps/www/components/Nav/HamburgerMenu.tsx index 7eb40b392a5b3..144810262fc70 100644 --- a/apps/www/components/Nav/HamburgerMenu.tsx +++ b/apps/www/components/Nav/HamburgerMenu.tsx @@ -13,7 +13,7 @@ const HamburgerButton = (props: HamburgerButtonProps) => ( >