Files
supabase/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx
T
Joshen LimandKaterina Skroumpelou 9949b75011 Joshen/fe 2670 dashboard to pull partner integrations into project (#43545)
## Context

Initial ground work for Marketplace Integrations (feature flagged under
the flag `marketplaceIntegrations`), in which some integrations will be
fetched over a network request (than hard-coded within the repo).
Feature flag just controls whether to we need to fetch them.

Similarly, categories under "Explore" will also be fetched, and the flag
just decides whether that should be enabled.

We're also planning to update the overview tab for integrations as well,
in which integrations fetched over the network request will use the new
UI (altho not functional just yet)

## To test
Note that the LOC changes are seemingly large, but that's just because
I've added in a types file for the Supabase project that we're using to
store the integrations data
- [x] Verify that behaviour is status quo with the flag off (Priority)
- [x] Verify that behaviour is status quo on CLI / self-host env
- [x] Verify that with the flag on you'll see one of the new
integrations fetched over network (Grafana), including an additional
category "Observability" (and clicking into it will show the new UI)
- [x] Verify that all other existing integrations are status quo

---------

Co-authored-by: Katerina Skroumpelou <sk.katherine@gmail.com>
2026-03-11 15:17:41 +07:00

138 lines
5.2 KiB
TypeScript

import AlertError from 'components/ui/AlertError'
import { NoSearchResults } from 'components/ui/NoSearchResults'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { Search } from 'lucide-react'
import { parseAsString, useQueryState } from 'nuqs'
import { buttonVariants, cn, Tabs_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Input } from 'ui-patterns/DataInputs/Input'
import { IntegrationCard, IntegrationLoadingCard } from './IntegrationCard'
import { useAvailableIntegrations } from './useAvailableIntegrations'
import { useInstalledIntegrations } from './useInstalledIntegrations'
type IntegrationCategory = 'all' | 'wrapper' | 'postgres_extensions' | 'custom'
const CATEGORIES = [
{ key: 'all', label: 'All Integrations' },
{ key: 'wrapper', label: 'Wrappers' },
{ key: 'postgres_extension', label: 'Postgres Modules' },
] as const
export const AvailableIntegrations = () => {
const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
const [selectedCategory, setSelectedCategory] = useQueryState(
'category',
parseAsString.withDefault('all').withOptions({ clearOnDefault: true })
)
const [search, setSearch] = useQueryState(
'search',
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
)
const { data: allIntegrations = [] } = useAvailableIntegrations()
const { installedIntegrations, error, isError, isLoading, isSuccess } = useInstalledIntegrations()
const installedIds = installedIntegrations.map((i) => i.id)
// available integrations for install
const availableIntegrations = integrationsWrappers
? allIntegrations
: allIntegrations.filter((x) => !x.id.endsWith('_wrapper'))
const integrationsByCategory =
selectedCategory === 'all'
? availableIntegrations
: availableIntegrations.filter((i) => i.type === selectedCategory)
const filteredIntegrations = (
search.length > 0
? integrationsByCategory.filter((i) => i.name.toLowerCase().includes(search.toLowerCase()))
: integrationsByCategory
).sort((a, b) => a.name.localeCompare(b.name))
return (
<>
<Tabs_Shadcn_
className="mt-4"
value={selectedCategory}
onValueChange={(value) => setSelectedCategory(value as IntegrationCategory)}
>
<TabsList_Shadcn_ className="px-4 md:px-10 gap-2 border-b-0 border-t pt-5">
{CATEGORIES.map((category) => (
<TabsTrigger_Shadcn_
key={category.key}
value={category.key}
onClick={() => setSelectedCategory(category.key as IntegrationCategory)}
className={cn(
buttonVariants({
size: 'tiny',
type: selectedCategory === category.key ? 'default' : 'outline',
}),
selectedCategory === category.key ? 'text-foreground' : 'text-foreground-lighter',
'!rounded-full px-3'
)}
>
{category.label}
</TabsTrigger_Shadcn_>
))}
<Input
value={search}
onChange={(e) => {
setSearch(e.target.value)
setSelectedCategory('all')
}}
containerClassName="group w-40 ml-5"
icon={
<Search
size={14}
className="transition text-foreground-lighter group-hover:text-foreground"
/>
}
iconContainerClassName="p-0"
className="pl-7 rounded-none !border-0 border-transparent bg-transparent !shadow-none !ring-0 !ring-offset-0"
placeholder="Search..."
/>
</TabsList_Shadcn_>
</Tabs_Shadcn_>
<div className="p-4 md:p-10 md:py-8 flex flex-col gap-y-5">
<div className="grid xl:grid-cols-3 2xl:grid-cols-4 gap-x-4 gap-y-3">
{isLoading &&
new Array(3)
.fill(0)
.map((_, idx) => <IntegrationLoadingCard key={`integration-loading-${idx}`} />)}
{isError && (
<AlertError
className="xl:col-span-3 2xl:col-span-4"
subject="Failed to retrieve available integrations"
error={error}
/>
)}
{isSuccess &&
filteredIntegrations.map((i) => (
<IntegrationCard key={i.id} {...i} isInstalled={installedIds.includes(i.id)} />
))}
{isSuccess && search.length > 0 && filteredIntegrations.length === 0 && (
<NoSearchResults
className="xl:col-span-3 2xl:col-span-4"
searchString={search}
onResetFilter={() => setSearch('')}
/>
)}
{isSuccess &&
selectedCategory !== 'all' &&
search.length === 0 &&
filteredIntegrations.length === 0 && (
<Admonition
showIcon={false}
className="xl:col-span-3 2xl:col-span-4"
type="default"
title="All integrations in this category are currently in use"
description="Manage your installed integrations in the section above"
/>
)}
</div>
</div>
</>
)
}