| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { RadioGroupCard, RadioGroupCardItem } from '@ui/components/radio-group-card'
- import { cn } from '@ui/lib/utils'
- import { ChevronRight } from 'lucide-react'
- import Link from 'next/link'
- import { useRouter } from 'next/router'
- import { useMemo, useState } from 'react'
- import { SubmitHandler, useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Button,
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
- Form,
- FormField,
- Skeleton,
- } from 'ui'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { z } from 'zod'
- import { OrganizationCard } from '../OrganizationCard'
- import AwsMarketplaceAutoRenewalWarning from './AwsMarketplaceAutoRenewalWarning'
- import AwsMarketplaceOnboardingPlaceholder from './AwsMarketplaceOnboardingPlaceholder'
- import AwsMarketplaceOnboardingSuccessModal from './AwsMarketplaceOnboardingSuccessModal'
- import { useCloudMarketplaceOnboardingInfoQuery } from './cloud-marketplace-query'
- import NewAwsMarketplaceOrgModal from './NewAwsMarketplaceOrgModal'
- import {
- ScaffoldSection,
- ScaffoldSectionContent,
- ScaffoldSectionDetail,
- } from '@/components/layouts/Scaffold'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { useOrganizationLinkAwsMarketplaceMutation } from '@/data/organizations/organization-link-aws-marketplace-mutation'
- import { DOCS_URL } from '@/lib/constants'
- import type { Organization } from '@/types'
- interface AwsMarketplaceLinkExistingOrgProps {
- organizations?: Organization[] | undefined
- }
- const FormSchema = z.object({
- orgSlug: z.string(),
- })
- type LinkExistingOrgForm = z.infer<typeof FormSchema>
- export const AwsMarketplaceLinkExistingOrg = ({
- organizations,
- }: AwsMarketplaceLinkExistingOrgProps) => {
- const router = useRouter()
- const {
- query: { buyer_id: buyerId },
- } = router
- const { data: onboardingInfo, isPending: isLoadingOnboardingInfo } =
- useCloudMarketplaceOnboardingInfoQuery({
- buyerId: buyerId as string,
- })
- const form = useForm<LinkExistingOrgForm>({
- resolver: zodResolver(FormSchema as any),
- defaultValues: {
- orgSlug: undefined,
- },
- mode: 'onBlur',
- reValidateMode: 'onChange',
- })
- const isDirty = !!Object.keys(form.formState.dirtyFields).length
- // Sort organizations by name ascending
- const sortedOrganizations = useMemo(() => {
- return organizations?.slice().sort((a, b) => a.name.localeCompare(b.name))
- }, [organizations])
- const { orgsLinkable, orgsNotLinkable } = useMemo(() => {
- const orgQualifiesForLinking = (org: Organization) => {
- const validationResult = onboardingInfo?.organization_linking_eligibility.find(
- (result) => result.slug === org.slug
- )
- return validationResult?.is_eligible ?? false
- }
- const linkable: Organization[] = []
- const notLinkable: Organization[] = []
- sortedOrganizations?.forEach((org) => {
- if (orgQualifiesForLinking(org)) {
- linkable.push(org)
- } else {
- notLinkable.push(org)
- }
- })
- return { orgsLinkable: linkable, orgsNotLinkable: notLinkable }
- }, [sortedOrganizations, onboardingInfo?.organization_linking_eligibility])
- const [isNotLinkableOrgListOpen, setIsNotLinkableOrgListOpen] = useState(false)
- const [orgLinkedSuccessfully, setOrgLinkedSuccessfully] = useState(false)
- const [showOrgCreationDialog, setShowOrgCreationDialog] = useState(false)
- const [orgToRedirectTo, setOrgToRedirectTo] = useState('')
- const { mutate: linkOrganization, isPending: isLinkingOrganization } =
- useOrganizationLinkAwsMarketplaceMutation({
- onSuccess: (_) => {
- //TODO(thomas): send tracking event?
- setOrgLinkedSuccessfully(true)
- setOrgToRedirectTo(form.getValues('orgSlug'))
- },
- onError: (res) => {
- toast.error(res.message, {
- duration: 7_000,
- })
- },
- })
- const onSubmit: SubmitHandler<LinkExistingOrgForm> = async (values) => {
- linkOrganization({ slug: values.orgSlug, buyerId: buyerId as string })
- }
- return (
- <>
- {onboardingInfo &&
- !onboardingInfo.aws_contract_auto_renewal &&
- !onboardingInfo.aws_contract_is_private_offer && (
- <AwsMarketplaceAutoRenewalWarning
- awsContractEndDate={onboardingInfo.aws_contract_end_date}
- awsContractSettingsUrl={onboardingInfo.aws_contract_settings_url}
- />
- )}
- {isLoadingOnboardingInfo ? (
- <AwsMarketplaceOnboardingPlaceholder />
- ) : (
- <ScaffoldSection>
- <ScaffoldSectionDetail className="text-base">
- <>
- <p>
- You’ve subscribed to the Briven{' '}
- {onboardingInfo?.plan_name_selected_on_marketplace} Plan via the AWS Marketplace. As
- a final step, you need to link a Briven organization to that subscription. Select
- the organization you want to be managed and billed through AWS.
- </p>
- <p>
- You can read more on billing through AWS in our {''}
- <Link
- href={`${DOCS_URL}/guides/platform/aws-marketplace`}
- target="_blank"
- className="underline"
- >
- Billing Docs.
- </Link>
- </p>
- <p className="mt-10">
- <span className="font-bold text-foreground-light">Want to start fresh?</span> Create
- a new organization and it will be linked automatically.
- </p>
- <Button
- size="tiny"
- htmlType="submit"
- type="primary"
- onClick={async (e) => {
- e.preventDefault()
- setShowOrgCreationDialog(true)
- }}
- >
- Create organization
- </Button>
- </>
- </ScaffoldSectionDetail>
- <ScaffoldSectionContent className="lg:ml-10">
- <Form {...form}>
- <form className="flex flex-col">
- <FormField
- name="orgSlug"
- control={form.control}
- render={({ field }) => (
- <RadioGroupCard
- {...field}
- defaultValue={field.value}
- onValueChange={(value: string) => {
- form.setValue('orgSlug', value, {
- shouldDirty: true,
- shouldValidate: false,
- })
- }}
- >
- <FormItemLayout id={field.name}>
- <div className={'grid gap-4 grid-cols-1'}>
- {isLoadingOnboardingInfo ? (
- Array(3)
- .fill(0)
- .map((_, i) => (
- <Skeleton key={i} className="w-full h-[110px] rounded-md" />
- ))
- ) : (
- <>
- <p className="font-bold text-foreground-light">
- Organizations that can be linked
- </p>
- {orgsLinkable.length === 0 ? (
- <p className="text-sm text-foreground-light">
- None of your organizations can be linked to your AWS Marketplace
- subscription at the moment.
- </p>
- ) : (
- orgsLinkable.map((org) => (
- <RadioGroupCardItem
- id={org.slug}
- key={org.slug}
- showIndicator={false}
- value={org.slug}
- className={cn(
- 'relative text-sm text-left flex flex-col gap-0 p-0 [&_label]:w-full group w-full'
- )}
- label={
- <OrganizationCard
- key={org.id}
- isLink={false}
- organization={org}
- />
- }
- />
- ))
- )}
- </>
- )}
- </div>
- </FormItemLayout>
- </RadioGroupCard>
- )}
- />
- </form>
- </Form>
- {orgsNotLinkable.length > 0 && !isLoadingOnboardingInfo && (
- <Collapsible
- className="-space-y-px"
- open={isNotLinkableOrgListOpen || orgsLinkable.length === 0}
- onOpenChange={() => setIsNotLinkableOrgListOpen((prev) => !prev)}
- >
- <CollapsibleTrigger className="py-2 w-full flex items-center group justify-between">
- <p className="text-xs font-bold text-foreground-light">
- Organizations that can't be linked
- </p>
- <ChevronRight
- size={16}
- className="text-foreground-lighter transition-all group-data-open:rotate-90"
- strokeWidth={1}
- />
- </CollapsibleTrigger>
- <CollapsibleContent
- className={cn(
- 'flex flex-col gap-4 transition-all',
- 'data-closed:animate-collapsible-up data-open:animate-collapsible-down'
- )}
- >
- <p className="text-foreground-light text-xs">
- The following organizations can’t be linked to your AWS Marketplace subscription
- at the moment. This may be due to missing permissions, outstanding invoices, or
- an existing marketplace link. If you'd like to link one of these organizations,
- please review the organization settings. You need to be Owner or Administrator
- of the organization to link it.
- </p>
- <div className="text-sm text-left flex flex-col gap-4 p-0 [&_label]:w-full group] w-full opacity-60">
- {orgsNotLinkable.map((org) => (
- <OrganizationCard
- isLink={false}
- key={org.id}
- organization={org}
- className="cursor-not-allowed"
- />
- ))}
- </div>
- </CollapsibleContent>
- </Collapsible>
- )}
- <div className={cn('flex gap-3 justify-end')}>
- <ButtonTooltip
- size="medium"
- htmlType="submit"
- type="primary"
- onClick={async () => {
- await onSubmit(form.getValues())
- }}
- loading={isLinkingOrganization}
- disabled={!isDirty || isLinkingOrganization || isLoadingOnboardingInfo}
- tooltip={{
- content: {
- side: 'top',
- text: !isDirty ? 'No organization selected' : undefined,
- },
- }}
- >
- Link organization
- </ButtonTooltip>
- </div>
- </ScaffoldSectionContent>
- </ScaffoldSection>
- )}
- <AwsMarketplaceOnboardingSuccessModal
- visible={orgLinkedSuccessfully}
- onClose={() => {
- setOrgLinkedSuccessfully(false)
- router.push(`/org/${orgToRedirectTo}`)
- }}
- />
- <NewAwsMarketplaceOrgModal
- visible={showOrgCreationDialog}
- onClose={() => setShowOrgCreationDialog(false)}
- buyerId={buyerId as string}
- onSuccess={(newlyCreatedOrgSlug) => {
- setShowOrgCreationDialog(false)
- setOrgToRedirectTo(newlyCreatedOrgSlug)
- setOrgLinkedSuccessfully(true)
- }}
- />
- </>
- )
- }
|