AvailableIntegrations.tsx 5.2 KB

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