AvailableIntegrations.tsx137 lines · main
| 1 | import { Search } from 'lucide-react' |
| 2 | import { parseAsString, useQueryState } from 'nuqs' |
| 3 | import { buttonVariants, cn, Tabs_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' |
| 4 | import { Admonition } from 'ui-patterns/admonition' |
| 5 | import { Input } from 'ui-patterns/DataInputs/Input' |
| 6 | |
| 7 | import { IntegrationCard, IntegrationLoadingCard } from './IntegrationCard' |
| 8 | import { useAvailableIntegrations } from './useAvailableIntegrations' |
| 9 | import { useInstalledIntegrations } from './useInstalledIntegrations' |
| 10 | import AlertError from '@/components/ui/AlertError' |
| 11 | import { NoSearchResults } from '@/components/ui/NoSearchResults' |
| 12 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 13 | |
| 14 | type IntegrationCategory = 'all' | 'wrapper' | 'postgres_extensions' | 'custom' |
| 15 | const CATEGORIES = [ |
| 16 | { key: 'all', label: 'All Integrations' }, |
| 17 | { key: 'wrapper', label: 'Wrappers' }, |
| 18 | { key: 'postgres_extension', label: 'Postgres Modules' }, |
| 19 | ] as const |
| 20 | |
| 21 | export 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 | } |