Skip to content
This repository has been archived by the owner on Oct 31, 2024. It is now read-only.

Add category filter to add connection tab #70

Merged
merged 4 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 68 additions & 51 deletions packages/engine-frontend/components/IntegrationSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use client'

import {Loader, Search} from 'lucide-react'
import React from 'react'
import {Input} from '@openint/ui'
import {useState} from 'react'
import {Input, parseCategory} from '@openint/ui'
import {CheckboxFilter} from '@openint/ui/components/CheckboxFilter'
import {ConnectionCard} from '@openint/ui/domain-components/ConnectionCard'
import type {ConnectorConfig} from '../hocs/WithConnectConfig'
import type {ConnectEventType} from '../hocs/WithConnectorConnect'
Expand All @@ -25,7 +26,8 @@ export function IntegrationSearch({
type: ConnectEventType
}) => void
}) {
const [searchText, setSearchText] = React.useState('')
const [searchText, setSearchText] = useState('')
const [categoryFilter, setCategoryFilter] = useState<string[]>([])

const listIntegrationsRes = _trpcReact.listConfiguredIntegrations.useQuery({
connector_config_ids: connectorConfigs.map((ccfg) => ccfg.id),
Expand All @@ -36,83 +38,98 @@ export function IntegrationSearch({
ccfg: connectorConfigs.find((ccfg) => ccfg.id === int.connector_config_id)!,
}))

const categories = Array.from(
new Set(connectorConfigs.flatMap((ccfg) => ccfg.verticals)),
)

const intsByCategory = ints?.reduce(
(acc, int) => {
int.ccfg.verticals.forEach((vertical) => {
acc[vertical] = (acc[vertical] || []).concat(int)
if (categoryFilter.length === 0 || categoryFilter.includes(vertical)) {
acc[vertical] = (acc[vertical] || []).concat(int)
}
})
return acc
},
{} as Record<string, typeof ints>,
)

const onApplyFilter = (selected: string[]) => {
setCategoryFilter(selected)
}

return (
<div className={className}>
{/* Search integrations */}
<div className="mb-2 bg-background/95 pt-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<form>
<div className="relative">
<div className="flex flex-row gap-2">
<div className="relative w-[450px]">
{/* top-2.5 is not working for some reason due to tailwind setup */}
<Search className="absolute left-2 top-2 h-4 w-4 text-muted-foreground" />
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search"
className="pl-8"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</div>
</form>
{categories.length > 1 && (
<CheckboxFilter options={categories} onApply={onApplyFilter} />
)}
</div>
</div>
{/* Search results */}
{listIntegrationsRes.isLoading ? (
<div className="flex h-full items-center justify-center">
<div className="flex h-full min-h-[500px] items-center justify-center">
<Loader className="size-5 animate-spin text-[#8A5DF6]" />
</div>
) : (
<div className="space-y-6 overflow-y-auto py-4">
{Object.entries(intsByCategory ?? {}).map(
([category, categoryInts]) => (
<div key={category}>
<h3 className="mb-2 text-lg font-semibold">
{category.length < 5
? category.toUpperCase()
: category
.split('-')
.map(
(word) =>
word.charAt(0).toUpperCase() + word.slice(1),
)
.join(' ')}
</h3>
<div className="flex flex-row gap-4">
{categoryInts.map((int) => (
<WithConnectorConnect
key={int.id}
connectorConfig={{
id: int.connector_config_id,
connector: int.ccfg.connector,
}}
onEvent={(e) => {
onEvent?.({
type: e.type,
integration: {
connectorConfigId: int.connector_config_id,
id: int.id,
},
})
}}>
{({openConnect}) => (
<ConnectionCard
onClick={openConnect}
logo={int.ccfg.connector.logoUrl ?? ''}
name={int.name}
/>
)}
</WithConnectorConnect>
))}
{(ints && ints.length > 0) ||
Object.keys(intsByCategory ?? {}).length > 0 ? (
Object.entries(intsByCategory ?? {}).map(
([category, categoryInts]) => (
<div key={category}>
<h3 className="mb-2 text-lg font-semibold">
{parseCategory(category)}
</h3>
<div className="flex flex-row gap-4">
{categoryInts.map((int) => (
<WithConnectorConnect
key={int.id}
connectorConfig={{
id: int.connector_config_id,
connector: int.ccfg.connector,
}}
onEvent={(e) => {
onEvent?.({
type: e.type,
integration: {
connectorConfigId: int.connector_config_id,
id: int.id,
},
})
}}>
{({openConnect}) => (
<ConnectionCard
onClick={openConnect}
logo={int.ccfg.connector.logoUrl ?? ''}
name={int.name}
/>
)}
</WithConnectorConnect>
))}
</div>
</div>
</div>
),
),
)
) : (
<div>
<p className="text-lg font-semibold">
No available connectors, please check that you have configured
connectors available or review your filter values.
</p>
</div>
)}
</div>
)}
Expand Down
36 changes: 30 additions & 6 deletions packages/ui/components/CheckboxFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import {useState} from 'react'
import {Button} from '../shadcn/Button'
import {Checkbox} from '../shadcn/Checkbox'
import {Popover, PopoverContent, PopoverTrigger} from '../shadcn/Popover'
import {parseCategory} from '../utils'

export function CheckboxFilter({
options,
onApply,
}: {
options: string[]
onApply: () => void
onApply: (selected: string[]) => void
}) {
const [checkedState, setCheckedState] = useState<Record<string, boolean>>(
options.reduce(
Expand Down Expand Up @@ -37,7 +38,7 @@ export function CheckboxFilter({
<span className="ml-2">Category</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-40 bg-white">
<PopoverContent className="w-48 bg-white">
<div className="flex flex-col gap-2">
{options.map((option) => (
<div
Expand All @@ -58,7 +59,7 @@ export function CheckboxFilter({
<span
className={`block h-4 w-4 rounded-sm ${
checkedState[option]
? 'bg-[#8A7DFF] text-white'
? 'bg-button text-button-foreground'
: 'bg-transparent'
}`}>
{checkedState[option] && (
Expand All @@ -80,14 +81,37 @@ export function CheckboxFilter({
htmlFor={option}
className="cursor-pointer text-sm font-semibold">
{' '}
{option}
{parseCategory(option)}
</label>
</div>
))}
{/* Added a visible divider here */}
<div className="my-2 w-full border-t border-[#E6E6E6]" />
<div className="col-span-3 flex justify-end">
<Button onClick={onApply} size="sm">
<div className="col-span-3 flex justify-end gap-2">
<Button
onClick={() => {
setCheckedState(
options.reduce(
(acc, option) => {
acc[option] = false
return acc
},
{} as Record<string, boolean>,
),
)
onApply([])
}}
size="sm"
variant="secondary">
Clear
</Button>
<Button
onClick={() =>
onApply(
Object.keys(checkedState).filter((key) => checkedState[key]),
)
}
size="sm">
Apply
</Button>
</div>
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@ export function getValidChildren(children: React.ReactNode) {
React.isValidElement(child),
) as React.ReactElement[]
}

export function parseCategory(category: string) {
return category.length < 5
? category.toUpperCase()
: category
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
Loading