feat: aws marketplace billing in dashboard (#37670)
This commit is contained in:
committed by
GitHub
parent
bb886317c7
commit
11e2512df0
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
38
apps/studio/data/integrations/aws-redirect-query.ts
Normal file
38
apps/studio/data/integrations/aws-redirect-query.ts
Normal 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,
|
||||
}
|
||||
)
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user