Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions apps/studio/components/interfaces/App/RouteValidationWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { toast } from 'sonner'

import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsMFAEnabled, useParams } from 'common'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import useLatest from 'hooks/misc/useLatest'
Expand All @@ -18,6 +18,7 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {

const isLoggedIn = useIsLoggedIn()
const snap = useAppStateSnapshot()
const isUserMFAEnabled = useIsMFAEnabled()

const organization = useSelectedOrganization()

Expand Down Expand Up @@ -122,7 +123,17 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
}, [isSuccessStorage, ref])

useEffect(() => {
if (organization) setLastVisitedOrganization(organization.slug)
if (organization) {
setLastVisitedOrganization(organization.slug)

if (
organization.organization_requires_mfa &&
!isUserMFAEnabled &&
router.pathname !== '/org/[slug]'
) {
router.push(`/org/${organization.slug}`)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [organization])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const AddPaymentMethodForm = ({
<PaymentElement
className="[.p-LinkAutofillPrompt]:pt-0"
options={{
defaultValues: { billingDetails: { email: selectedOrganization?.billing_email } },
defaultValues: { billingDetails: { email: selectedOrganization?.billing_email ?? '' } },
}}
/>
{showSetDefaultCheckbox && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const BillingEmail = () => {
useEffect(() => {
if (billingCustomer) {
form.reset({
billingEmail: billing_email,
billingEmail: billing_email ?? '',
additionalBillingEmails: billingCustomer.additional_emails ?? [],
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
import { CriticalIcon, WarningIcon } from 'ui'
import { PricingMetric } from 'data/analytics/org-daily-stats-query'
import { usePathname } from 'next/navigation'

export const Restriction = () => {
const org = useSelectedOrganization()
const { data: usage, isSuccess: isSuccessOrgUsage } = useOrgUsageQuery({ orgSlug: org?.slug })

const pathname = usePathname()
const isUsagePage = pathname?.endsWith('/usage')

const hasExceededAnyLimits = Boolean(
usage?.usages.find(
(metric) =>
Expand Down Expand Up @@ -69,6 +73,11 @@ export const Restriction = () => {
{org.plan.id === 'free' ? 'Upgrade plan' : 'Change spend cap'}
</Link>
</Button>
{!isUsagePage && (
<Button key="view-usage-button" asChild type="default">
<Link href={`/org/${org?.slug}/usage`}>View usage</Link>
</Button>
)}
<Button asChild type="default" icon={<ExternalLink />}>
<a href="https://supabase.com/docs/guides/platform/cost-control#spend-cap">
About spend cap
Expand Down Expand Up @@ -106,6 +115,13 @@ export const Restriction = () => {
{org.plan.id === 'free' ? 'Upgrade plan' : 'Disable spend cap'}
</Link>
</Button>

{!isUsagePage && (
<Button key="view-usage-button" asChild type="default">
<Link href={`/org/${org?.slug}/usage`}>View usage</Link>
</Button>
)}

<Button asChild type="default" icon={<ExternalLink />}>
<a href="https://supabase.com/docs/guides/platform/billing-faq#fair-use-policy">
About Fair Use Policy
Expand Down Expand Up @@ -141,6 +157,11 @@ export const Restriction = () => {
{org.plan.id === 'free' ? 'Upgrade plan' : 'Disable spend cap'}
</Link>
</Button>
{!isUsagePage && (
<Button key="view-usage-button" asChild type="default">
<Link href={`/org/${org?.slug}/usage`}>View usage</Link>
</Button>
)}
<Button asChild type="default" icon={<ExternalLink />}>
<a href="https://supabase.com/docs/guides/platform/billing-faq#fair-use-policy">
About Fair Use Policy
Expand Down Expand Up @@ -177,6 +198,11 @@ export const Restriction = () => {
{org.plan.id === 'free' ? 'Upgrade plan' : 'Disable spend cap'}
</Link>
</Button>
{!isUsagePage && (
<Button key="view-usage-button" asChild type="default">
<Link href={`/org/${org?.slug}/usage`}>View usage</Link>
</Button>
)}
<Button asChild type="default" icon={<ExternalLink />}>
<a href="https://supabase.com/docs/guides/platform/billing-faq#fair-use-policy">
About Fair Use Policy
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Boxes } from 'lucide-react'
import { Boxes, Lock } from 'lucide-react'
import Link from 'next/link'

import { useIsMFAEnabled } from 'common'
import { ActionCard } from 'components/ui/ActionCard'
import { useProjectsQuery } from 'data/projects/projects-query'
import { Organization } from 'types'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'

export const OrganizationCard = ({
organization,
Expand All @@ -12,17 +14,44 @@ export const OrganizationCard = ({
organization: Organization
href?: string
}) => {
const isUserMFAEnabled = useIsMFAEnabled()
const { data: allProjects = [] } = useProjectsQuery()

const numProjects = allProjects.filter((x) => x.organization_slug === organization.slug).length
const isMfaRequired = organization.organization_requires_mfa

return (
<Link href={href ?? `/org/${organization.slug}`}>
<ActionCard
bgColor="bg border"
className="[&>div]:items-center"
className={cn('flex items-center min-h-[70px] [&>div]:w-full [&>div]:items-center')}
icon={<Boxes size={18} strokeWidth={1} className="text-foreground" />}
title={organization.name}
description={`${organization.plan.name} Plan${numProjects > 0 ? `${' '}•${' '}${numProjects} project${numProjects > 1 ? 's' : ''}` : ''}`}
description={
<div className="flex items-center justify-between text-xs text-foreground-light font-sans">
<div className="flex items-center gap-x-1.5">
<span>{organization.plan.name} Plan</span>
{numProjects > 0 && (
<>
<span>•</span>
<span>
{numProjects} project{numProjects > 1 ? 's' : ''}
</span>
</>
)}
</div>
{isMfaRequired && (
<Tooltip>
<TooltipTrigger className="cursor-default">
<Lock size={12} />
</TooltipTrigger>
<TooltipContent side="bottom" className={!isUserMFAEnabled ? 'w-80' : ''}>
MFA enforced
</TooltipContent>
</Tooltip>
)}
</div>
}
/>
</Link>
)
Expand Down
13 changes: 11 additions & 2 deletions apps/studio/components/interfaces/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Link from 'next/link'
import { useRouter } from 'next/router'
import { ComponentProps, ComponentPropsWithoutRef, FC, ReactNode, useEffect } from 'react'

import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useIsMFAEnabled, useParams } from 'common'
import {
generateOtherRoutes,
generateProductRoutes,
Expand All @@ -18,6 +18,7 @@ import { useHideSidebar } from 'hooks/misc/useHideSidebar'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLints } from 'hooks/misc/useLints'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useFlag } from 'hooks/ui/useFlag'
import { Home } from 'icons'
import { useAppStateSnapshot } from 'state/app-state'
Expand Down Expand Up @@ -158,10 +159,12 @@ export function SideBarNavLink({
route,
active,
onClick,
disabled,
...props
}: {
route: any
active?: boolean
disabled?: boolean
onClick?: () => void
} & ComponentPropsWithoutRef<typeof SidebarMenuButton>) {
const [sidebarBehaviour] = useLocalStorageQuery(
Expand All @@ -170,6 +173,7 @@ export function SideBarNavLink({
)

const buttonProps = {
disabled,
tooltip: sidebarBehaviour === 'closed' ? route.label : '',
isActive: active,
className: cn('text-sm', sidebarBehaviour === 'open' ? '!px-2' : ''),
Expand All @@ -188,7 +192,7 @@ export function SideBarNavLink({

return (
<SidebarMenuItem>
{route.link ? (
{route.link && !disabled ? (
<SidebarMenuButton {...buttonProps} asChild>
<Link href={route.link}>{content}</Link>
</SidebarMenuButton>
Expand Down Expand Up @@ -352,6 +356,10 @@ const OrganizationLinks = () => {
const router = useRouter()
const { slug } = useParams()

const org = useSelectedOrganization()
const isUserMFAEnabled = useIsMFAEnabled()
const disableAccessMfa = org?.organization_requires_mfa && !isUserMFAEnabled

const activeRoute = router.pathname.split('/')[3]

const navMenuItems = [
Expand Down Expand Up @@ -399,6 +407,7 @@ const OrganizationLinks = () => {
{navMenuItems.map((item, i) => (
<SideBarNavLink
key={item.key}
disabled={disableAccessMfa}
active={
i === 0
? activeRoute === undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const OrganizationDropdown = () => {
const href = !!routeSlug
? router.pathname.replace('[slug]', org.slug)
: `/org/${org.slug}`

return (
<CommandItem_Shadcn_
key={org.slug}
Expand Down
11 changes: 8 additions & 3 deletions apps/studio/components/ui/ActionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { JSX, ReactNode } from 'react'
import { Badge, Card, cn } from 'ui'

export const ActionCard = (card: {
icon: JSX.Element
title: string
bgColor?: string
description?: string
description?: string | ReactNode
isBeta?: boolean
className?: string
onClick?: () => void
Expand All @@ -28,9 +29,13 @@ export const ActionCard = (card: {
>
{card.icon}
</div>
<div className="flex flex-col gap-0">
<div className="flex flex-col gap-0 w-full">
<h3 className="text-sm text-foreground mb-0">{card.title}</h3>
<pre className="text-xs text-foreground-light font-sans">{card.description}</pre>
{typeof card.description === 'string' ? (
<pre className="text-xs text-foreground-light font-sans">{card.description}</pre>
) : (
card.description
)}
</div>
</div>
</Card>
Expand Down
6 changes: 3 additions & 3 deletions apps/studio/data/organizations/organizations-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { useProfile } from 'lib/profile'
import type { Organization, ResponseError } from 'types'
import { organizationKeys } from './keys'

export function castOrganizationResponseToOrganization(
org: components['schemas']['OrganizationResponse']
): Organization {
export type OrganizationBase = components['schemas']['OrganizationResponse']

export function castOrganizationResponseToOrganization(org: OrganizationBase): Organization {
return {
...org,
billing_email: org.billing_email ?? 'Unknown',
Expand Down
42 changes: 30 additions & 12 deletions apps/studio/pages/org/[slug]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { useState } from 'react'

import { useIsMFAEnabled } from 'common'
import { ProjectList } from 'components/interfaces/Home/ProjectList'
import HomePageActions from 'components/interfaces/HomePageActions'
import DefaultLayout from 'components/layouts/DefaultLayout'
import OrganizationLayout from 'components/layouts/OrganizationLayout'
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
import { InlineLink } from 'components/ui/InlineLink'
import { useAutoProjectsPrefetch } from 'data/projects/projects-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { PROJECT_STATUS } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
import { Admonition } from 'ui-patterns'

const ProjectsPage: NextPageWithLayout = () => {
const org = useSelectedOrganization()
const isUserMFAEnabled = useIsMFAEnabled()
const disableAccessMfa = org?.organization_requires_mfa && !isUserMFAEnabled

const [search, setSearch] = useState('')
const [filterStatus, setFilterStatus] = useState<string[]>([
PROJECT_STATUS.ACTIVE_HEALTHY,
Expand All @@ -20,22 +28,32 @@ const ProjectsPage: NextPageWithLayout = () => {

return (
<ScaffoldContainerLegacy>
<div>
<HomePageActions
search={search}
setSearch={setSearch}
filterStatus={filterStatus}
setFilterStatus={setFilterStatus}
/>

<div className="my-6 space-y-8">
<ProjectList
{disableAccessMfa ? (
<Admonition type="note" title={`The organization "${org?.name}" has MFA enforced`}>
<p className="!m-0">
Set up MFA on your account through your{' '}
<InlineLink href="/account/security">account preferences</InlineLink> to access this
organization
</p>
</Admonition>
) : (
<div>
<HomePageActions
search={search}
setSearch={setSearch}
filterStatus={filterStatus}
resetFilterStatus={() => setFilterStatus(['ACTIVE_HEALTHY', 'INACTIVE'])}
setFilterStatus={setFilterStatus}
/>

<div className="my-6 space-y-8">
<ProjectList
search={search}
filterStatus={filterStatus}
resetFilterStatus={() => setFilterStatus(['ACTIVE_HEALTHY', 'INACTIVE'])}
/>
</div>
</div>
</div>
)}
</ScaffoldContainerLegacy>
)
}
Expand Down
18 changes: 3 additions & 15 deletions apps/studio/types/base.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { OrganizationBase } from 'data/organizations/organizations-query'
import { PlanId } from 'data/subscriptions/types'
import jsonLogic from 'json-logic-js'

export interface Organization {
id: number
slug: string
name: string
billing_email: string
is_owner?: boolean
opt_in_tags: string[]
subscription_id?: string | null
restriction_status: 'grace_period' | 'grace_period_over' | 'restricted' | null
restriction_data: Record<string, string> | null
export interface Organization extends OrganizationBase {
managed_by: 'supabase' | 'vercel-marketplace' | 'aws-marketplace'
partner_id?: string
plan: {
id: PlanId
name: string
}
usage_billing_enabled: boolean
plan: { id: PlanId; name: string }
}

/**
Expand Down
Loading
Loading