feat: aws marketplace billing in dashboard (#37670)

This commit is contained in:
Ignacio Dobronich
2025-08-20 03:43:37 -03:00
committed by GitHub
parent bb886317c7
commit 11e2512df0
13 changed files with 269 additions and 71 deletions

View File

@@ -16,10 +16,14 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import UpcomingInvoice from './UpcomingInvoice'
import { MANAGED_BY } from 'lib/constants/infrastructure'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
const BillingBreakdown = () => {
const { slug: orgSlug } = useParams()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } =
useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions')
@@ -44,26 +48,43 @@ const BillingBreakdown = () => {
<div className="sticky space-y-2 top-12 pr-6">
<p className="text-foreground text-base m-0">Upcoming Invoice</p>
<div className="py-2">
<SparkBar
type="horizontal"
value={daysWithinCycle - daysToCycleEnd}
max={daysWithinCycle}
barClass="bg-foreground"
labelBottom={`${billingCycleStart.format('MMMM DD')} - ${billingCycleEnd.format('MMMM DD')}`}
bgClass="bg-surface-300"
labelBottomClass="!text-foreground-light p-1 m-0"
labelTop={
subscription
? `${daysToCycleEnd} ${daysToCycleEnd === 1 ? 'day' : 'days'} left`
: ''
}
labelTopClass="p-1 m-0"
/>
{selectedOrganization?.managed_by !== MANAGED_BY.AWS_MARKETPLACE && (
<SparkBar
type="horizontal"
value={daysWithinCycle - daysToCycleEnd}
max={daysWithinCycle}
barClass="bg-foreground"
labelBottom={`${billingCycleStart.format('MMMM DD')} - ${billingCycleEnd.format('MMMM DD')}`}
bgClass="bg-surface-300"
labelBottomClass="!text-foreground-light p-1 m-0"
labelTop={
subscription
? `${daysToCycleEnd} ${daysToCycleEnd === 1 ? 'day' : 'days'} left`
: ''
}
labelTopClass="p-1 m-0"
/>
)}
</div>
<p className="prose text-sm">
Your upcoming invoice (excluding credits) will continue to update until the end of your
billing cycle on {billingCycleEnd.format('MMMM DD')}. For a more detailed breakdown,
visit the <Link href={`/org/${orgSlug}/usage`}>usage page.</Link>
{selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE ? (
<>
You'll receive two invoices from AWS Marketplace: one on the 3rd of{' '}
{billingCycleEnd.format('MMMM')} for your usage in{' '}
{billingCycleStart.format('MMMM')} and one on {billingCycleEnd.format('MMMM DD')}{' '}
for the fixed subscription fee.
</>
) : (
<>
Your upcoming invoice (excluding credits) will continue to update until the end of
your billing cycle on {billingCycleEnd.format('MMMM DD')}.
</>
)}
<>
{' '}
For a more detailed breakdown, visit the{' '}
<Link href={`/org/${orgSlug}/usage`}>usage page.</Link>
</>
</p>
<br />
<p className="text-sm text-foreground-light mt-4">

View File

@@ -118,7 +118,7 @@ export const BillingCustomerData = () => {
{selectedOrganization?.managed_by !== undefined &&
selectedOrganization?.managed_by !== 'supabase' ? (
<PartnerManagedResource
partner={selectedOrganization?.managed_by}
managedBy={selectedOrganization?.managed_by}
resource="Billing Addresses"
cta={{
installationId: selectedOrganization?.partner_id,

View File

@@ -12,21 +12,27 @@ import {
} from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import { PARTNER_TO_NAME } from 'components/ui/PartnerManagedResource'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useFlag } from 'hooks/ui/useFlag'
import { BASE_PATH } from 'lib/constants'
import { MANAGED_BY } from 'lib/constants/infrastructure'
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
import { Alert, Button } from 'ui'
import { Alert, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip'
import SpendCapSidePanel from './SpendCapSidePanel'
import PartnerIcon from 'components/ui/PartnerIcon'
export interface CostControlProps {}
const CostControl = ({}: CostControlProps) => {
const { slug } = useParams()
const { resolvedTheme } = useTheme()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } =
useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions')
@@ -47,6 +53,8 @@ const CostControl = ({}: CostControlProps) => {
const canChangeTier =
!projectUpdateDisabled && !['team', 'enterprise'].includes(currentPlan?.id || '')
const costControlDisabled = selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE
return (
<>
<ScaffoldSection>
@@ -97,7 +105,22 @@ const CostControl = ({}: CostControlProps) => {
{isError && <AlertError subject="Failed to retrieve subscription" error={error} />}
{isSuccess && (
{isSuccess && costControlDisabled && (
<Alert_Shadcn_ className="flex flex-col items-center gap-y-2 border-0 rounded-none">
<PartnerIcon
organization={{ managed_by: selectedOrganization?.managed_by }}
showTooltip={false}
size="large"
/>
<AlertTitle_Shadcn_ className="text-sm">
The Spend Cap is not available for organizations managed by{' '}
{PARTNER_TO_NAME[selectedOrganization?.managed_by]}.
</AlertTitle_Shadcn_>
</Alert_Shadcn_>
)}
{isSuccess && !costControlDisabled && (
<div className="space-y-6">
{['team', 'enterprise'].includes(currentPlan?.id || '') ? (
<Alert

View File

@@ -29,6 +29,7 @@ import { Alert, Button } from 'ui'
import ChangePaymentMethodModal from './ChangePaymentMethodModal'
import CreditCard from './CreditCard'
import DeletePaymentMethodModal from './DeletePaymentMethodModal'
import { MANAGED_BY } from 'lib/constants/infrastructure'
const PaymentMethods = () => {
const { slug } = useParams()
@@ -66,9 +67,9 @@ const PaymentMethods = () => {
</ScaffoldSectionDetail>
<ScaffoldSectionContent>
{selectedOrganization?.managed_by !== undefined &&
selectedOrganization?.managed_by !== 'supabase' ? (
selectedOrganization?.managed_by !== MANAGED_BY.SUPABASE ? (
<PartnerManagedResource
partner={selectedOrganization?.managed_by}
managedBy={selectedOrganization?.managed_by}
resource="Payment Methods"
cta={{
installationId: selectedOrganization?.partner_id,

View File

@@ -30,7 +30,23 @@ import { ExitSurveyModal } from './ExitSurveyModal'
import MembersExceedLimitModal from './MembersExceedLimitModal'
import { SubscriptionPlanUpdateDialog } from './SubscriptionPlanUpdateDialog'
import UpgradeSurveyModal from './UpgradeModal'
import { MANAGED_BY } from 'lib/constants/infrastructure'
import { Organization } from 'types/base'
const getPartnerManagedResourceCta = (selectedOrganization: Organization) => {
if (selectedOrganization.managed_by === MANAGED_BY.VERCEL_MARKETPLACE) {
return {
installationId: selectedOrganization?.partner_id,
path: '/settings',
message: 'Change Plan on Vercel Marketplace',
}
}
if (selectedOrganization.managed_by === MANAGED_BY.AWS_MARKETPLACE) {
return {
organizationSlug: selectedOrganization?.slug,
}
}
}
const PlanUpdateSidePanel = () => {
const router = useRouter()
const { slug } = useParams()
@@ -144,16 +160,11 @@ const PlanUpdateSidePanel = () => {
</div>
}
>
{selectedOrganization?.managed_by === 'vercel-marketplace' && (
{selectedOrganization && selectedOrganization.managed_by !== MANAGED_BY.SUPABASE && (
<PartnerManagedResource
partner={selectedOrganization?.managed_by}
managedBy={selectedOrganization.managed_by}
resource="Organization plans"
cta={{
installationId: selectedOrganization?.partner_id,
path: '/settings',
message: 'Change Plan on Vercel Marketplace',
}}
// TODO: support AWS marketplace here: `https://us-east-1.console.aws.amazon.com/billing/home#/bills`
cta={getPartnerManagedResourceCta(selectedOrganization)}
/>
)}
<SidePanel.Content>
@@ -216,8 +227,10 @@ const PlanUpdateSidePanel = () => {
disabled={
subscription?.plan?.id === 'enterprise' ||
// Downgrades to free are still allowed through the dashboard given we have much better control about showing customers the impact + any possible issues with downgrading to free
(selectedOrganization?.managed_by !== 'supabase' &&
(selectedOrganization?.managed_by !== MANAGED_BY.SUPABASE &&
plan.id !== 'tier_free') ||
// Orgs managed by AWS marketplace are not allowed to change the plan
selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE ||
hasOrioleProjects ||
!canUpdateSubscription
}
@@ -243,7 +256,10 @@ const PlanUpdateSidePanel = () => {
? 'Your organization has projects that are using the OrioleDB extension which is only available on the Free plan. Remove all OrioleDB projects before changing your plan.'
: !canUpdateSubscription
? 'You do not have permission to change the subscription plan'
: undefined,
: selectedOrganization?.managed_by ===
MANAGED_BY.AWS_MARKETPLACE
? 'You cannot change the plan for an organization managed by AWS Marketplace'
: undefined,
},
}}
>

View File

@@ -3,6 +3,7 @@ import PartnerManagedResource from 'components/ui/PartnerManagedResource'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { Admonition } from 'ui-patterns'
import { DeleteOrganizationButton } from './DeleteOrganizationButton'
import { MANAGED_BY } from 'lib/constants/infrastructure'
const OrganizationDeletePanel = () => {
const { data: selectedOrganization } = useSelectedOrganizationQuery()
@@ -20,7 +21,7 @@ const OrganizationDeletePanel = () => {
</Admonition>
) : (
<PartnerManagedResource
partner="vercel-marketplace"
managedBy={MANAGED_BY.VERCEL_MARKETPLACE}
resource="Organizations"
cta={{
installationId: selectedOrganization?.partner_id,

View File

@@ -17,9 +17,25 @@ import { Button } from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import InvoicePayButton from './InvoicePayButton'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { Organization } from 'types/base'
import { MANAGED_BY } from 'lib/constants/infrastructure'
const PAGE_LIMIT = 5
const getPartnerManagedResourceCta = (selectedOrganization: Organization) => {
if (selectedOrganization.managed_by === MANAGED_BY.VERCEL_MARKETPLACE) {
return {
installationId: selectedOrganization?.partner_id,
path: '/invoices',
}
}
if (selectedOrganization.managed_by === MANAGED_BY.AWS_MARKETPLACE) {
return {
organizationSlug: selectedOrganization?.slug,
overrideUrl: 'https://console.aws.amazon.com/billing/home#/bills',
}
}
}
const InvoicesSettings = () => {
const [page, setPage] = useState(1)
@@ -62,13 +78,9 @@ const InvoicesSettings = () => {
) {
return (
<PartnerManagedResource
partner={selectedOrganization?.managed_by}
managedBy={selectedOrganization?.managed_by}
resource="Invoices"
cta={{
installationId: selectedOrganization?.partner_id,
path: '/invoices',
}}
// TODO: support AWS marketplace here: `https://us-east-1.console.aws.amazon.com/billing/home#/bills`
cta={getPartnerManagedResourceCta(selectedOrganization)}
/>
)
}

View File

@@ -7,12 +7,33 @@ import { useVercelRedirectQuery } from 'data/integrations/vercel-redirect-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { withAuth } from 'hooks/misc/withAuth'
import { Alert_Shadcn_, AlertTitle_Shadcn_, Button, cn } from 'ui'
import { useAwsRedirectQuery } from 'data/integrations/aws-redirect-query'
import { MANAGED_BY } from 'lib/constants/infrastructure'
const OrganizationLayoutContent = ({ children }: PropsWithChildren<{}>) => {
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { data, isSuccess } = useVercelRedirectQuery({
installationId: selectedOrganization?.partner_id,
})
const vercelQuery = useVercelRedirectQuery(
{
installationId: selectedOrganization?.partner_id,
},
{
enabled: selectedOrganization?.managed_by === MANAGED_BY.VERCEL_MARKETPLACE,
}
)
const awsQuery = useAwsRedirectQuery(
{
organizationSlug: selectedOrganization?.slug,
},
{
enabled: selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE,
}
)
// Select the appropriate query based on partner
const { data, isSuccess } =
selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE ? awsQuery : vercelQuery
return (
<div className={cn('h-full w-full flex flex-col overflow-hidden')}>

View File

@@ -1,5 +1,7 @@
import { Organization } from 'types'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { MANAGED_BY } from 'lib/constants/infrastructure'
import { PARTNER_TO_NAME } from './PartnerManagedResource'
interface PartnerIconProps {
organization: Pick<Organization, 'managed_by'>
@@ -8,27 +10,71 @@ interface PartnerIconProps {
size?: 'small' | 'medium' | 'large'
}
function getPartnerIcon(
organization: Pick<Organization, 'managed_by'>,
size: 'small' | 'medium' | 'large'
) {
switch (organization.managed_by) {
case MANAGED_BY.VERCEL_MARKETPLACE:
return (
<svg
className={cn(
size === 'small' && 'w-2.5 h-2.5',
size === 'medium' && 'w-3.5 h-3.5',
size === 'large' && 'w-5 h-5'
)}
viewBox="0 0 76 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="hsl(var(--foreground-default) / 1)" />
</svg>
)
case MANAGED_BY.AWS_MARKETPLACE:
return (
<svg
className={cn(
size === 'small' && 'w-2.5 h-2.5',
size === 'medium' && 'w-3.5 h-3.5',
size === 'large' && 'w-5 h-5'
)}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="hsl(var(--foreground-default) / 1)">
<path d="M4.51 7.687c0 .197.02.357.058.475.042.117.096.245.17.384a.233.233 0 01.037.123c0 .053-.032.107-.1.16l-.336.224a.255.255 0 01-.138.048c-.054 0-.107-.026-.16-.074a1.652 1.652 0 01-.192-.251 4.137 4.137 0 01-.164-.315c-.416.491-.937.737-1.565.737-.447 0-.804-.129-1.064-.385-.261-.256-.394-.598-.394-1.025 0-.454.16-.822.484-1.1.325-.278.756-.416 1.304-.416.18 0 .367.016.564.042.197.027.4.07.612.118v-.39c0-.406-.085-.689-.25-.854-.17-.166-.458-.246-.868-.246-.186 0-.377.022-.574.07a4.23 4.23 0 00-.575.181 1.525 1.525 0 01-.186.07.326.326 0 01-.085.016c-.075 0-.112-.054-.112-.166v-.262c0-.085.01-.15.037-.186a.399.399 0 01.15-.113c.185-.096.409-.176.67-.24.26-.07.537-.101.83-.101.633 0 1.096.144 1.394.432.293.288.442.726.442 1.314v1.73h.01zm-2.161.811c.175 0 .356-.032.548-.096.192-.064.362-.182.505-.342a.848.848 0 00.181-.341c.032-.129.054-.283.054-.465V7.03a4.43 4.43 0 00-.49-.09 3.996 3.996 0 00-.5-.033c-.357 0-.617.07-.793.214-.176.144-.26.347-.26.614 0 .25.063.437.196.566.128.133.314.197.559.197zm4.273.577c-.096 0-.16-.016-.202-.054-.043-.032-.08-.106-.112-.208l-1.25-4.127a.938.938 0 01-.048-.214c0-.085.042-.133.127-.133h.522c.1 0 .17.016.207.053.043.032.075.107.107.208l.894 3.535.83-3.535c.026-.106.058-.176.101-.208a.365.365 0 01.213-.053h.426c.1 0 .17.016.212.053.043.032.08.107.102.208l.84 3.578.92-3.578a.459.459 0 01.107-.208.347.347 0 01.208-.053h.495c.085 0 .133.043.133.133 0 .027-.006.054-.01.086a.768.768 0 01-.038.133l-1.283 4.127c-.031.107-.069.177-.111.209a.34.34 0 01-.203.053h-.457c-.101 0-.17-.016-.213-.053-.043-.038-.08-.107-.101-.214L8.213 5.37l-.82 3.439c-.026.107-.058.176-.1.213-.043.038-.118.054-.213.054h-.458zm6.838.144a3.51 3.51 0 01-.82-.096c-.266-.064-.473-.134-.612-.214-.085-.048-.143-.101-.165-.15a.38.38 0 01-.031-.149v-.272c0-.112.042-.166.122-.166a.3.3 0 01.096.016c.032.011.08.032.133.054.18.08.378.144.585.187.213.042.42.064.633.064.336 0 .596-.059.777-.176a.575.575 0 00.277-.508.52.52 0 00-.144-.373c-.095-.102-.276-.193-.537-.278l-.772-.24c-.388-.123-.676-.305-.851-.545a1.275 1.275 0 01-.266-.774c0-.224.048-.422.143-.593.096-.17.224-.32.384-.438.16-.122.34-.213.553-.277.213-.064.436-.091.67-.091.118 0 .24.005.357.021.122.016.234.038.346.06.106.026.208.052.303.085.096.032.17.064.224.096a.461.461 0 01.16.133.289.289 0 01.047.176v.251c0 .112-.042.171-.122.171a.552.552 0 01-.202-.064 2.428 2.428 0 00-1.022-.208c-.303 0-.543.048-.708.15-.165.1-.25.256-.25.475 0 .149.053.277.16.379.106.101.303.202.585.293l.756.24c.383.123.66.294.825.513.165.219.244.47.244.748 0 .23-.047.437-.138.619a1.435 1.435 0 01-.388.47c-.165.133-.362.23-.591.299-.24.075-.49.112-.761.112z" />
<path
fillRule="evenodd"
d="M14.465 11.813c-1.75 1.297-4.294 1.986-6.481 1.986-3.065 0-5.827-1.137-7.913-3.027-.165-.15-.016-.353.18-.235 2.257 1.313 5.04 2.109 7.92 2.109 1.941 0 4.075-.406 6.039-1.239.293-.133.543.192.255.406z"
clipRule="evenodd"
/>
<path
fillRule="evenodd"
d="M15.194 10.98c-.223-.287-1.479-.138-2.048-.069-.17.022-.197-.128-.043-.24 1-.705 2.645-.502 2.836-.267.192.24-.053 1.89-.99 2.68-.143.123-.281.06-.217-.1.212-.53.686-1.72.462-2.003z"
clipRule="evenodd"
/>
</g>
</svg>
)
default:
return null
}
}
function PartnerIcon({
organization,
showTooltip = true,
tooltipText = 'This organization is managed by Vercel Marketplace.',
tooltipText,
size = 'small',
}: PartnerIconProps) {
if (organization.managed_by === 'vercel-marketplace') {
const icon = (
<svg
className={cn(
size === 'small' && 'w-2.5 h-2.5',
size === 'medium' && 'w-3.5 h-3.5',
size === 'large' && 'w-5 h-5'
)}
viewBox="0 0 76 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="hsl(var(--foreground-default) / 1)" />
</svg>
)
if (
organization.managed_by === MANAGED_BY.VERCEL_MARKETPLACE ||
organization.managed_by === MANAGED_BY.AWS_MARKETPLACE
) {
const icon = getPartnerIcon(organization, size)
if (!showTooltip) {
return (
@@ -45,6 +91,8 @@ function PartnerIcon({
)
}
const defaultTooltipText = `This organization is managed by ${PARTNER_TO_NAME[organization.managed_by]}`
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -59,7 +107,7 @@ function PartnerIcon({
{icon}
</div>
</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
<TooltipContent>{tooltipText ?? defaultTooltipText}</TooltipContent>
</Tooltip>
)
}

View File

@@ -1,15 +1,18 @@
import { ExternalLink } from 'lucide-react'
import { useVercelRedirectQuery } from 'data/integrations/vercel-redirect-query'
import { useAwsRedirectQuery } from 'data/integrations/aws-redirect-query'
import { Alert_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
import PartnerIcon from './PartnerIcon'
import { MANAGED_BY, ManagedBy } from 'lib/constants/infrastructure'
interface PartnerManagedResourceProps {
partner: ManagedBy
managedBy: ManagedBy
resource: string
cta?: {
installationId?: string
organizationSlug?: string
overrideUrl?: string
path?: string
message?: string
}
@@ -21,35 +24,47 @@ export const PARTNER_TO_NAME = {
[MANAGED_BY.SUPABASE]: 'Supabase',
} as const
function PartnerManagedResource({ partner, resource, cta }: PartnerManagedResourceProps) {
const isManagedBySupabase = partner === MANAGED_BY.SUPABASE
function PartnerManagedResource({ managedBy, resource, cta }: PartnerManagedResourceProps) {
const ctaEnabled = cta !== undefined
const { data, isLoading, isError } = useVercelRedirectQuery(
// Use appropriate redirect query based on partner
const vercelQuery = useVercelRedirectQuery(
{
installationId: cta?.installationId,
},
{
enabled: ctaEnabled && !isManagedBySupabase,
enabled: ctaEnabled && managedBy === MANAGED_BY.VERCEL_MARKETPLACE,
}
)
if (isManagedBySupabase) return null
const awsQuery = useAwsRedirectQuery(
{
organizationSlug: cta?.organizationSlug,
},
{
enabled: ctaEnabled && managedBy === MANAGED_BY.AWS_MARKETPLACE,
}
)
if (managedBy === MANAGED_BY.SUPABASE) return null
const { data, isLoading, isError } =
managedBy === MANAGED_BY.VERCEL_MARKETPLACE ? vercelQuery : awsQuery
const ctaUrl = (data?.url ?? '') + (cta?.path ?? '')
return (
<Alert_Shadcn_ className="flex flex-col items-center gap-y-2 border-0 rounded-none">
<PartnerIcon organization={{ managed_by: partner }} showTooltip={false} size="large" />
<PartnerIcon organization={{ managed_by: managedBy }} showTooltip={false} size="large" />
<AlertTitle_Shadcn_ className="text-sm">
{resource} are managed by {PARTNER_TO_NAME[partner]}.
{resource} are managed by {PARTNER_TO_NAME[managedBy]}.
</AlertTitle_Shadcn_>
{ctaEnabled && (
<Button asChild type="default" iconRight={<ExternalLink />} disabled={isLoading || isError}>
<a href={ctaUrl} target="_blank" rel="noopener noreferrer">
{cta.message || `View ${resource} on ${PARTNER_TO_NAME[partner]}`}
<a href={cta.overrideUrl ?? ctaUrl} target="_blank" rel="noopener noreferrer">
{cta.message || `View ${resource} on ${PARTNER_TO_NAME[managedBy]}`}
</a>
</Button>
)}

View File

@@ -0,0 +1,38 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import type { ResponseError } from 'types'
import { integrationKeys } from './keys'
import { get, handleError } from 'data/fetchers'
export type AwsRedirectVariables = {
organizationSlug?: string
}
export async function getAwsRedirect(
{ organizationSlug }: AwsRedirectVariables,
signal?: AbortSignal
) {
if (!organizationSlug) throw new Error('organizationSlug is required')
const { data, error } = await get(`/platform/organizations/{slug}/cloud-marketplace/redirect`, {
params: { path: { slug: organizationSlug } },
signal,
})
if (error) handleError(error)
return data
}
export type AwsRedirectData = Awaited<ReturnType<typeof getAwsRedirect>>
export type AwsRedirectError = ResponseError
export const useAwsRedirectQuery = <TData = AwsRedirectData>(
{ organizationSlug }: AwsRedirectVariables,
{ enabled = true, ...options }: UseQueryOptions<AwsRedirectData, AwsRedirectError, TData> = {}
) =>
useQuery<AwsRedirectData, AwsRedirectError, TData>(
integrationKeys.awsRedirect(organizationSlug),
({ signal }) => getAwsRedirect({ organizationSlug }, signal),
{
enabled: enabled && typeof organizationSlug !== 'undefined',
...options,
}
)

View File

@@ -25,4 +25,5 @@ export const integrationKeys = {
githubConnectionsList: (organizationId: number | undefined) =>
['organizations', organizationId, 'github-connections'] as const,
vercelRedirect: (installationId?: string) => ['vercel-redirect', installationId] as const,
awsRedirect: (organizationSlug?: string) => ['aws-redirect', organizationSlug] as const,
}

View File

@@ -60,6 +60,7 @@ import {
DEFAULT_MINIMUM_PASSWORD_STRENGTH,
DEFAULT_PROVIDER,
FLY_REGIONS_DEFAULT,
MANAGED_BY,
PROJECT_STATUS,
PROVIDERS,
} from 'lib/constants'
@@ -961,7 +962,7 @@ const Wizard: NextPageWithLayout = () => {
) : isManagedByVercel ? (
<Panel.Content>
<PartnerManagedResource
partner="vercel-marketplace"
managedBy={MANAGED_BY.VERCEL_MARKETPLACE}
resource="Projects"
cta={{
installationId: currentOrg?.partner_id,