AvailableIntegrations.tsx137 lines · main
1import { Search } from 'lucide-react'
2import { parseAsString, useQueryState } from 'nuqs'
3import { buttonVariants, cn, Tabs_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
4import { Admonition } from 'ui-patterns/admonition'
5import { Input } from 'ui-patterns/DataInputs/Input'
6
7import { IntegrationCard, IntegrationLoadingCard } from './IntegrationCard'
8import { useAvailableIntegrations } from './useAvailableIntegrations'
9import { useInstalledIntegrations } from './useInstalledIntegrations'
10import AlertError from '@/components/ui/AlertError'
11import { NoSearchResults } from '@/components/ui/NoSearchResults'
12import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
13
14type IntegrationCategory = 'all' | 'wrapper' | 'postgres_extensions' | 'custom'
15const CATEGORIES = [
16 { key: 'all', label: 'All Integrations' },
17 { key: 'wrapper', label: 'Wrappers' },
18 { key: 'postgres_extension', label: 'Postgres Modules' },
19] as const
20
21export const AvailableIntegrations = () => {
22 const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
23
24 const [selectedCategory, setSelectedCategory] = useQueryState(
25 'category',
26 parseAsString.withDefault('all').withOptions({ clearOnDefault: true })
27 )
28 const [search, setSearch] = useQueryState(
29 'search',
30 parseAsString.withDefault('').withOptions({ clearOnDefault: true })
31 )
32
33 const { data: allIntegrations = [] } = useAvailableIntegrations()
34 const { installedIntegrations, error, isError, isLoading, isSuccess } = useInstalledIntegrations()
35
36 const installedIds = installedIntegrations.map((i) => i.id)
37
38 // available integrations for install
39 const availableIntegrations = integrationsWrappers
40 ? allIntegrations
41 : allIntegrations.filter((x) => !x.id.endsWith('_wrapper'))
42 const integrationsByCategory =
43 selectedCategory === 'all'
44 ? availableIntegrations
45 : availableIntegrations.filter((i) => i.type === selectedCategory)
46
47 const filteredIntegrations = (
48 search.length > 0
49 ? integrationsByCategory.filter((i) => i.name.toLowerCase().includes(search.toLowerCase()))
50 : integrationsByCategory
51 ).sort((a, b) => a.name.localeCompare(b.name))
52
53 return (
54 <>
55 <Tabs_Shadcn_
56 className="mt-4"
57 value={selectedCategory}
58 onValueChange={(value) => setSelectedCategory(value as IntegrationCategory)}
59 >
60 <TabsList_Shadcn_ className="px-4 md:px-10 gap-2 border-b-0 border-t pt-5">
61 {CATEGORIES.map((category) => (
62 <TabsTrigger_Shadcn_
63 key={category.key}
64 value={category.key}
65 onClick={() => setSelectedCategory(category.key as IntegrationCategory)}
66 className={cn(
67 buttonVariants({
68 size: 'tiny',
69 type: selectedCategory === category.key ? 'default' : 'outline',
70 }),
71 selectedCategory === category.key ? 'text-foreground' : 'text-foreground-lighter',
72 'rounded-full! px-3'
73 )}
74 >
75 {category.label}
76 </TabsTrigger_Shadcn_>
77 ))}
78 <Input
79 value={search}
80 onChange={(e) => {
81 setSearch(e.target.value)
82 setSelectedCategory('all')
83 }}
84 containerClassName="group w-40 ml-5"
85 icon={
86 <Search
87 size={14}
88 className="transition text-foreground-lighter group-hover:text-foreground"
89 />
90 }
91 iconContainerClassName="p-0"
92 className="pl-7 rounded-none border-0! border-transparent bg-transparent shadow-none! ring-0! ring-offset-0!"
93 placeholder="Search..."
94 />
95 </TabsList_Shadcn_>
96 </Tabs_Shadcn_>
97 <div className="p-4 md:p-10 md:py-8 flex flex-col gap-y-5">
98 <div className="grid xl:grid-cols-3 2xl:grid-cols-4 gap-x-4 gap-y-3">
99 {isLoading &&
100 Array.from({ length: 3 }).map((_, idx) => (
101 <IntegrationLoadingCard key={`integration-loading-${idx}`} />
102 ))}
103 {isError && (
104 <AlertError
105 className="xl:col-span-3 2xl:col-span-4"
106 subject="Failed to retrieve available integrations"
107 error={error}
108 />
109 )}
110 {isSuccess &&
111 filteredIntegrations.map((i) => (
112 <IntegrationCard key={i.id} {...i} isInstalled={installedIds.includes(i.id)} />
113 ))}
114 {isSuccess && search.length > 0 && filteredIntegrations.length === 0 && (
115 <NoSearchResults
116 className="xl:col-span-3 2xl:col-span-4"
117 searchString={search}
118 onResetFilter={() => setSearch('')}
119 />
120 )}
121 {isSuccess &&
122 selectedCategory !== 'all' &&
123 search.length === 0 &&
124 filteredIntegrations.length === 0 && (
125 <Admonition
126 showIcon={false}
127 className="xl:col-span-3 2xl:col-span-4"
128 type="default"
129 title="All integrations in this category are currently in use"
130 description="Manage your installed integrations in the section above"
131 />
132 )}
133 </div>
134 </div>
135 </>
136 )
137}