import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { Check, X } from 'lucide-react' import { useMemo, useState } from 'react' import { Button, Card, cn, Table, TableBody, TableCell, TableHead, TableHeader, TableHeadSort, TableRow, } from 'ui' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionAside, PageSectionContent, PageSectionDescription, PageSectionMeta, PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { AuthorizedAppRow } from './AuthorizedAppRow' import { DeleteAppModal } from './DeleteAppModal' import { OAuthAppRow } from './OAuthAppRow' import { PublishAppSidePanel } from './PublishAppSidePanel' import { RevokeAppModal } from './RevokeAppModal' import AlertError from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import CopyButton from '@/components/ui/CopyButton' import NoPermission from '@/components/ui/NoPermission' import { AuthorizedApp, useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query' import { OAuthAppCreateResponse } from '@/data/oauth/oauth-app-create-mutation' import { OAuthApp, useOAuthAppsQuery } from '@/data/oauth/oauth-apps-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' // [Joshen] Note on nav UX // Kang Ming mentioned that it might be better to split Published Apps and Authorized Apps into 2 separate tabs // to prevent any confusion (case study: GitHub). Authorized apps could be in the "integrations" tab, but let's // check in again after we wrap up Vercel integration type SortOrder = 'asc' | 'desc' type PublishedAppsSort = 'created:asc' | 'created:desc' type PublishedAppsSortColumn = 'created' type AuthorizedAppsSort = 'authorized:asc' | 'authorized:desc' type AuthorizedAppsSortColumn = 'authorized' const parseSort = (sort: string): [C, SortOrder] => { return sort.split(':') as [C, SortOrder] } const toggleSort = ( currentSort: S, column: string, setSort: (sort: S) => void ) => { const [currentColumn, currentOrder] = parseSort(currentSort) if (currentColumn === column) { setSort(`${column}:${currentOrder === 'asc' ? 'desc' : 'asc'}` as S) } else { setSort(`${column}:asc` as S) } } export const OAuthApps = () => { const { slug } = useParams() const [showPublishModal, setShowPublishModal] = useState(false) const [createdApp, setCreatedApp] = useState() const [selectedAppToUpdate, setSelectedAppToUpdate] = useState() const [selectedAppToDelete, setSelectedAppToDelete] = useState() const [selectedAppToRevoke, setSelectedAppToRevoke] = useState() const [publishedAppsSort, setPublishedAppsSort] = useState('created:asc') const [authorizedAppsSort, setAuthorizedAppsSort] = useState('authorized:asc') const { can: canReadOAuthApps, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( PermissionAction.READ, 'approved_oauth_apps' ) const { can: canCreateOAuthApps } = useAsyncCheckPermissions( PermissionAction.CREATE, 'approved_oauth_apps' ) const { data: publishedApps, error: publishedAppsError, isPending: isLoadingPublishedApps, isSuccess: isSuccessPublishedApps, isError: isErrorPublishedApps, } = useOAuthAppsQuery({ slug }, { enabled: canReadOAuthApps }) const sortedPublishedApps = useMemo(() => { const [sortColumn, sortOrder] = parseSort(publishedAppsSort) const orderMultiplier = sortOrder === 'asc' ? 1 : -1 return [...(publishedApps ?? [])].sort((a, b) => { if (sortColumn === 'created') { return ( (new Date(a.created_at ?? '').getTime() - new Date(b.created_at ?? '').getTime()) * orderMultiplier ) } return 0 }) }, [publishedApps, publishedAppsSort]) const { data: authorizedApps, isPending: isLoadingAuthorizedApps, isSuccess: isSuccessAuthorizedApps, isError: isErrorAuthorizedApps, } = useAuthorizedAppsQuery({ slug }) const sortedAuthorizedApps = useMemo(() => { const [sortColumn, sortOrder] = parseSort(authorizedAppsSort) const orderMultiplier = sortOrder === 'asc' ? 1 : -1 return [...(authorizedApps ?? [])].sort((a, b) => { if (sortColumn === 'authorized') { return ( (new Date(a.authorized_at).getTime() - new Date(b.authorized_at).getTime()) * orderMultiplier ) } return 0 }) }, [authorizedApps, authorizedAppsSort]) const hasPublishedApps = (publishedApps?.length ?? 0) > 0 const hasAuthorizedApps = (authorizedApps?.length ?? 0) > 0 const avatarHeadClass = 'w-[62px] min-w-[62px] max-w-[62px]' const avatarHeadCollapsedClass = 'w-0 min-w-0 max-w-0 p-0' const handlePublishedSortChange = (column: PublishedAppsSortColumn) => { toggleSort(publishedAppsSort, column, setPublishedAppsSort) } const handleAuthorizedSortChange = (column: AuthorizedAppsSortColumn) => { toggleSort(authorizedAppsSort, column, setAuthorizedAppsSort) } return ( <> Published apps Build integrations that extend Briven's functionality setShowPublishModal(true)} tooltip={{ content: { side: 'bottom', text: !canCreateOAuthApps ? 'You need additional permissions to create apps' : undefined, }, }} > Publish OAuth app {isLoadingPublishedApps || isLoadingPermissions ? (
) : !canReadOAuthApps ? ( ) : null} {isErrorPublishedApps && ( )} {createdApp !== undefined && (

You've created your new OAuth application.

Ensure that you store the client secret securely - you will not be able to see it again.

Client ID

{createdApp.client_id}

Client Secret

{createdApp.client_secret}

)} {isSuccessPublishedApps && ( Avatar Name Client ID {hasPublishedApps ? ( CREATED ) : ( 'CREATED' )} Actions {hasPublishedApps ? ( sortedPublishedApps?.map((app) => ( { setShowPublishModal(true) setSelectedAppToUpdate(app) }} onSelectDelete={() => setSelectedAppToDelete(app)} /> )) ) : (

No results found

You do not have any published applications yet

)}
)}
Authorized apps Applications that have access to your organization's settings and projects {isLoadingAuthorizedApps || isLoadingPermissions ? (
) : !canReadOAuthApps ? ( ) : null} {isErrorAuthorizedApps && } {isSuccessAuthorizedApps && ( Avatar Name Author App ID {hasAuthorizedApps ? ( AUTHORIZED ) : ( 'AUTHORIZED' )} Actions {hasAuthorizedApps ? ( sortedAuthorizedApps?.map((app) => ( setSelectedAppToRevoke(app)} /> )) ) : (

No results found

You do not have any authorized applications yet

)}
)}
{ setSelectedAppToUpdate(undefined) setShowPublishModal(false) }} onCreateSuccess={setCreatedApp} /> setSelectedAppToDelete(undefined)} /> setSelectedAppToRevoke(undefined)} /> ) }