| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- import dayjs from 'dayjs'
- import Link from 'next/link'
- import { useMemo, type ReactNode } from 'react'
- import type { UseFormReturn } from 'react-hook-form'
- import {
- Alert,
- AlertDescription,
- AlertTitle,
- Button,
- Card,
- CardContent,
- CardFooter,
- CardHeader,
- Form,
- FormControl,
- FormField,
- FormItem,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- WarningIcon,
- } from 'ui'
- import { ShimmeringLoader } from 'ui-patterns'
- import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
- import type { ApprovalState, IApprovalFormSchema } from './ApiAuthorization.Schema'
- import { AuthorizeRequesterDetails } from '@/components/interfaces/Organization/OAuthApps/AuthorizeRequesterDetails'
- import type { ApiAuthorizationResponse } from '@/data/api-authorization/api-authorization-query'
- import { BASE_PATH } from '@/lib/constants'
- import type { Organization, ResponseError } from '@/types'
- type OrganizationsState_Loading = {
- _tag: 'loading'
- }
- type OrganizationsState_Error = {
- _tag: 'error'
- error: ResponseError | null
- }
- type OrganizationsState_Empty = {
- _tag: 'empty'
- }
- type OrganizationsState_NotMember = {
- _tag: 'not_member'
- }
- type OrganizationsState_Success = {
- _tag: 'success'
- organizations: Array<Organization>
- }
- type OrganizationsState =
- | OrganizationsState_Loading
- | OrganizationsState_Error
- | OrganizationsState_Empty
- | OrganizationsState_NotMember
- | OrganizationsState_Success
- export interface ApiAuthorizationMainViewProps {
- approvalState: ApprovalState
- form: UseFormReturn<IApprovalFormSchema>
- requester: ApiAuthorizationResponse
- organizations: OrganizationsState
- requestedOrganizationSlug: string | undefined
- onApprove: () => void
- onDecline: () => void
- }
- export function ApiAuthorizationMainView({
- approvalState,
- form,
- requester,
- organizations,
- requestedOrganizationSlug,
- onApprove,
- onDecline,
- }: ApiAuthorizationMainViewProps): ReactNode {
- const isMcpClient = requester.registration_type === 'dynamic'
- const isExpired = dayjs().isAfter(dayjs(requester.expires_at))
- return (
- <FormShell title={`Authorize API access for ${requester.name}`}>
- {isMcpClient && <McpNotice />}
- <AuthorizeRequesterDetails
- icon={requester.icon}
- name={requester.name}
- domain={requester.domain}
- scopes={requester.scopes}
- />
- {isExpired && <ExpiredNotice />}
- {organizations._tag === 'loading' && <OrganizationsLoader />}
- {organizations._tag === 'error' && <OrganizationsErrorNotice error={organizations.error} />}
- {organizations._tag === 'empty' && <OrganizationsEmptyState />}
- {organizations._tag === 'not_member' && <NotMemberOfOrganizationNotice />}
- {organizations._tag === 'success' && (
- <OrganizationSelector
- form={form}
- disabled={isExpired || !!requestedOrganizationSlug}
- requester={requester}
- organizations={organizations.organizations}
- requestedOrganizationSlug={requestedOrganizationSlug}
- />
- )}
- <FormFooter
- disabled={isExpired || organizations._tag !== 'success'}
- approvalState={approvalState}
- requester={requester}
- organizations={organizations}
- onApprove={onApprove}
- onDecline={onDecline}
- />
- </FormShell>
- )
- }
- interface FormShellProps {
- title: string
- children: ReactNode
- }
- function FormShell({ title, children }: FormShellProps): ReactNode {
- return (
- <Card>
- <CardHeader>{title}</CardHeader>
- <CardContent className="space-y-8">{children}</CardContent>
- </Card>
- )
- }
- function McpNotice(): ReactNode {
- return (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>MCP Client Connection</AlertTitle>
- <AlertDescription>
- This is an MCP (Model Context Protocol) client designed to connect with AI applications.
- Please ensure you trust this application before granting access to your organization's data.
- </AlertDescription>
- </Alert>
- )
- }
- function ExpiredNotice(): ReactNode {
- return (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>This authorization request is expired</AlertTitle>
- <AlertDescription>
- Please retry your authorization request from the requesting app
- </AlertDescription>
- </Alert>
- )
- }
- function OrganizationsLoader(): ReactNode {
- return (
- <div className="py-4 space-y-2">
- <ShimmeringLoader />
- <ShimmeringLoader className="w-3/4" />
- </div>
- )
- }
- interface OrganizationsErrorNoticeProps {
- error: ResponseError | null
- }
- function OrganizationsErrorNotice({ error }: OrganizationsErrorNoticeProps): ReactNode {
- return (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>There was an error loading your organizations</AlertTitle>
- <AlertDescription>
- Please try again. If the problem persists, contact support.
- {error && <p className="mt-2">Error: {error.message}</p>}
- </AlertDescription>
- </Alert>
- )
- }
- function OrganizationsEmptyState(): ReactNode {
- return (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>Organization is needed for installing an integration</AlertTitle>
- <AlertDescription>
- Your account isn't associated with any organizations. To use this integration, it must be
- installed within an organization. You'll be redirected to create an organization first.
- </AlertDescription>
- </Alert>
- )
- }
- function NotMemberOfOrganizationNotice(): ReactNode {
- return (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>Organization is needed for installing an integration</AlertTitle>
- <AlertDescription>
- Your account is not a member of the pre-selected organization. To use this integration, it
- must be installed within an organization your account is associated with.
- </AlertDescription>
- </Alert>
- )
- }
- interface OrganizationSelectorProps {
- form: UseFormReturn<IApprovalFormSchema>
- requester: ApiAuthorizationResponse
- requestedOrganizationSlug: string | undefined
- organizations: Array<Organization>
- disabled?: boolean
- }
- function OrganizationSelector({
- form,
- requester,
- requestedOrganizationSlug,
- organizations,
- disabled = false,
- }: OrganizationSelectorProps): ReactNode {
- return (
- <Form {...form}>
- <FormField
- control={form.control}
- name="selectedOrgSlug"
- render={({ field }) => (
- <FormItem>
- <FormLayout
- label="Organization to grant API access to"
- description={
- requestedOrganizationSlug
- ? `This organization has been pre-selected by ${requester.name}.`
- : undefined
- }
- isReactForm
- >
- <FormControl>
- <Select
- value={field.value || undefined}
- disabled={disabled}
- onValueChange={field.onChange}
- >
- <SelectTrigger size="small">
- <SelectValue placeholder="Select an organization" />
- </SelectTrigger>
- <SelectContent>
- {organizations.map((organization) => (
- <SelectItem
- key={organization.slug}
- value={organization.slug}
- className="text-xs"
- >
- {organization.name}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </FormControl>
- </FormLayout>
- </FormItem>
- )}
- />
- </Form>
- )
- }
- interface FormFooterProps {
- disabled?: boolean
- approvalState: ApprovalState
- requester: ApiAuthorizationResponse
- organizations: OrganizationsState
- onDecline: () => void
- onApprove: () => void
- }
- function FormFooter({
- disabled = false,
- approvalState,
- requester,
- organizations,
- onDecline,
- onApprove,
- }: FormFooterProps): ReactNode {
- const showApprovalButton = organizations._tag === 'success' || organizations._tag === 'not_member'
- return (
- <CardFooter className="justify-end space-x-2">
- <Button
- type="default"
- loading={approvalState === 'declining'}
- disabled={disabled || approvalState !== 'indeterminate'}
- onClick={onDecline}
- >
- Decline
- </Button>
- {organizations._tag === 'loading' && (
- <LoadingApprovalButton>Authorize {requester.name}</LoadingApprovalButton>
- )}
- {organizations._tag === 'empty' && <CreateOrganizationLink />}
- {showApprovalButton && (
- <ApprovalButton
- disabled={disabled || approvalState !== 'indeterminate'}
- approvalState={approvalState}
- requester={requester}
- onApprove={onApprove}
- />
- )}
- </CardFooter>
- )
- }
- interface LoadingApprovalButtonProps {
- children: ReactNode
- }
- function LoadingApprovalButton({ children }: LoadingApprovalButtonProps): ReactNode {
- return <Button loading={true}>{children}</Button>
- }
- function createReturnToSearchParam(): string | null {
- if (typeof window === 'undefined') {
- return null
- }
- const basePath = BASE_PATH
- let pathname = basePath ? location.pathname.replace(basePath, '') : location.pathname
- if (location.search) {
- pathname += location.search
- }
- return pathname
- }
- function CreateOrganizationLink(): ReactNode {
- const searchParamString = useMemo(function createSearchParams() {
- const searchParams = new URLSearchParams()
- const returnTo = createReturnToSearchParam()
- if (returnTo) {
- searchParams.set('returnTo', returnTo)
- }
- return searchParams.toString()
- }, [])
- return (
- <Button asChild>
- <Link href={`/new?${searchParamString}`}>Create an organization</Link>
- </Button>
- )
- }
- interface ApprovalButtonProps {
- disabled?: boolean
- approvalState: ApprovalState
- requester: ApiAuthorizationResponse
- onApprove: () => void
- }
- function ApprovalButton({
- disabled,
- approvalState,
- requester,
- onApprove,
- }: ApprovalButtonProps): ReactNode {
- return (
- <Button loading={approvalState === 'approving'} disabled={disabled} onClick={onApprove}>
- Authorize {requester.name}
- </Button>
- )
- }
|