ProjectIntegrationsLayout.tsx180 lines · main
1// @ts-nocheck
2import { useQuery } from '@tanstack/react-query'
3import { IS_PLATFORM, useFeatureFlags, useParams } from 'common'
4import { useRouter } from 'next/router'
5import { PropsWithChildren } from 'react'
6import { Menu, Separator } from 'ui'
7import { GenericSkeletonLoader } from 'ui-patterns'
8
9import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
10import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations'
11import { ProjectLayout } from '@/components/layouts/ProjectLayout'
12import AlertError from '@/components/ui/AlertError'
13import { ProductMenu } from '@/components/ui/ProductMenu'
14import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query'
15import { useMarketplaceIntegrationsQuery } from '@/data/marketplace/integrations-query'
16import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
17import { withAuth } from '@/hooks/misc/withAuth'
18
19/**
20 * Layout component for the Integrations section
21 * Provides sidebar navigation for integrations
22 */
23export const ProjectIntegrationsLayout = withAuth(({ children }: PropsWithChildren) => {
24 const router = useRouter()
25 const segments = router.asPath.split('/')
26 // construct the page url to be used to determine the active state for the sidebar
27 const page = `${segments[3]}${segments[4] ? `/${segments[4]}` : ''}`
28
29 return (
30 <ProjectLayout
31 product="Integrations"
32 browserTitle={{ section: 'Integrations' }}
33 isBlocking={false}
34 productMenu={
35 <>
36 <IntegrationCategoriesMenu page={page} />
37 <Separator />
38 <InstalledIntegrationsMenu page={page} />
39 </>
40 }
41 >
42 {children}
43 </ProjectLayout>
44 )
45})
46
47const IntegrationCategoriesMenu = ({ page }: { page: string }) => {
48 const router = useRouter()
49 const { ref } = useParams()
50 const { hasLoaded: flagsLoaded } = useFeatureFlags()
51 const isMarketplaceEnabled = useIsMarketplaceEnabled()
52
53 const urlParams = new URLSearchParams(router.asPath.split('?')[1] || '')
54 const categoryParam = urlParams.get('category')
55 const pageKey = categoryParam || page
56
57 const { integrationsWrappers: showWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
58 const { data: categories = [], isPending: isPendingCategories } = useQuery(
59 useMarketplaceCategoriesQuery({ enabled: isMarketplaceEnabled })
60 )
61 const { data: listings = [], isPending: isPendingListings } = useQuery(
62 useMarketplaceIntegrationsQuery({ enabled: isMarketplaceEnabled })
63 )
64
65 const populatedCategoryIds = new Set(
66 listings.flatMap((listing) =>
67 Array.isArray(listing.categories)
68 ? (listing.categories as Array<{ id: string }>).map((c) => c.id)
69 : []
70 )
71 )
72 const nonEmptyCategories = categories.filter(
73 (category) => category.id && populatedCategoryIds.has(category.id)
74 )
75
76 const isLoading = IS_PLATFORM
77 ? !flagsLoaded || (isMarketplaceEnabled && (isPendingCategories || isPendingListings))
78 : false
79
80 const allCategories = [
81 {
82 name: 'All',
83 key: 'integrations',
84 url: `/project/${ref}/integrations`,
85 pages: ['integrations'],
86 items: [],
87 },
88 ...(showWrappers
89 ? [
90 {
91 name: 'Wrappers',
92 key: 'wrapper',
93 url: `/project/${ref}/integrations?category=wrapper`,
94 items: [],
95 },
96 ]
97 : []),
98 {
99 name: 'Postgres Modules',
100 key: 'postgres_extension',
101 url: `/project/${ref}/integrations?category=postgres_extension`,
102 items: [],
103 },
104 ...nonEmptyCategories.map((category) => ({
105 name: category.name ?? '',
106 key: category.slug ?? '',
107 url: `/project/${ref}/integrations?category=${category.slug}`,
108 items: [],
109 })),
110 ]
111
112 return (
113 <>
114 {isLoading ? (
115 <div className="px-4 py-6 md:px-6">
116 <Menu type="pills">
117 <Menu.Group title={<span className="uppercase font-mono">Explore</span>} />
118 </Menu>
119 <GenericSkeletonLoader />
120 </div>
121 ) : (
122 <ProductMenu
123 page={pageKey}
124 menu={[{ key: 'explore', title: 'Explore', items: allCategories }]}
125 />
126 )}
127 </>
128 )
129}
130
131const InstalledIntegrationsMenu = ({ page }: { page: string }) => {
132 const { ref } = useParams()
133
134 const {
135 installedIntegrations: integrations,
136 error,
137 isLoading,
138 isSuccess,
139 isError,
140 } = useInstalledIntegrations()
141
142 const installedIntegrationItems = integrations.map((integration) => ({
143 name: integration.name,
144 label: integration.status,
145 key: `integrations/${integration.id}`,
146 url: `/project/${ref}/integrations/${integration.id}/overview`,
147 icon: (
148 <div className="relative w-6 h-6 bg-white border rounded-sm flex items-center justify-center">
149 {integration.icon({ className: 'p-1' })}
150 </div>
151 ),
152 items: [],
153 }))
154
155 return (
156 <>
157 {(isLoading || isError) && (
158 <div className="px-4 py-6 md:px-6">
159 <Menu type="pills">
160 <Menu.Group title={<span className="uppercase font-mono">Installed</span>} />
161 </Menu>
162 {isLoading && <GenericSkeletonLoader />}
163 {isError && (
164 <AlertError
165 showIcon={false}
166 error={error}
167 subject="Failed to retrieve installed integrations"
168 />
169 )}
170 </div>
171 )}
172 {isSuccess && (
173 <ProductMenu
174 page={page}
175 menu={[{ key: 'installed', title: 'Installed', items: installedIntegrationItems }]}
176 />
177 )}
178 </>
179 )
180}