Skip to content
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
49 changes: 35 additions & 14 deletions components/settings/integration-form-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
getIntegrationLabels,
getSortedIntegrationTypes,
} from "@/plugins";
import { getIntegrationDescriptions } from "@/plugins/registry";

type IntegrationFormDialogProps = {
open: boolean;
Expand All @@ -65,6 +66,9 @@ const SYSTEM_INTEGRATION_TYPES: IntegrationType[] = ["database"];
const SYSTEM_INTEGRATION_LABELS: Record<string, string> = {
database: "Database",
};
const SYSTEM_INTEGRATION_DESCRIPTIONS: Record<string, string> = {
database: "Connect to PostgreSQL databases",
};

// Get all integration types (plugins + system)
const getIntegrationTypes = (): IntegrationType[] => [
Expand All @@ -76,6 +80,12 @@ const getIntegrationTypes = (): IntegrationType[] => [
const getLabel = (type: IntegrationType): string =>
getIntegrationLabels()[type] || SYSTEM_INTEGRATION_LABELS[type] || type;

// Get description for any integration type
const getDescription = (type: IntegrationType): string =>
getIntegrationDescriptions()[type] ||
SYSTEM_INTEGRATION_DESCRIPTIONS[type] ||
"";

function SecretField({
fieldId,
label,
Expand Down Expand Up @@ -419,20 +429,31 @@ function TypeSelector({
No services found
</p>
) : (
filteredTypes.map((type) => (
<button
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-muted/50"
key={type}
onClick={() => onSelectType(type)}
type="button"
>
<IntegrationIcon
className="size-5"
integration={type === "ai-gateway" ? "vercel" : type}
/>
<span className="font-medium">{getLabel(type)}</span>
</button>
))
filteredTypes.map((type) => {
const description = getDescription(type);
return (
<button
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-muted/50"
key={type}
onClick={() => onSelectType(type)}
type="button"
>
<IntegrationIcon
className="size-5 shrink-0"
integration={type === "ai-gateway" ? "vercel" : type}
/>
<span className="min-w-0 flex-1 truncate">
<span className="font-medium">{getLabel(type)}</span>
{description && (
<span className="text-muted-foreground text-xs">
{" "}
- {description}
</span>
)}
</span>
</button>
);
})
)}
</div>
</div>
Expand Down
35 changes: 24 additions & 11 deletions components/settings/integrations-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useSetAtom } from "jotai";
import { Plus } from "lucide-react";
import { Plus, Search } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Expand All @@ -12,6 +12,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
import { integrationsVersionAtom } from "@/lib/integrations-store";
import { IntegrationsManager } from "./integrations-manager";
Expand All @@ -27,6 +28,7 @@ export function IntegrationsDialog({
}: IntegrationsDialogProps) {
const [loading, setLoading] = useState(true);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [filter, setFilter] = useState("");
const setIntegrationsVersion = useSetAtom(integrationsVersionAtom);
// Track if any changes were made during this dialog session
const hasChangesRef = useRef(false);
Expand All @@ -48,6 +50,8 @@ export function IntegrationsDialog({
hasChangesRef.current = false;
// Reset create dialog state when opening
setShowCreateDialog(false);
// Reset filter when opening
setFilter("");
}
}, [open, loadAll]);

Expand All @@ -68,10 +72,7 @@ export function IntegrationsDialog({

return (
<Dialog onOpenChange={handleClose} open={open}>
<DialogContent
className="max-h-[90vh] max-w-4xl overflow-y-auto"
showCloseButton={false}
>
<DialogContent className="max-w-4xl" showCloseButton={false}>
<DialogHeader>
<DialogTitle>Connections</DialogTitle>
<DialogDescription>
Expand All @@ -84,12 +85,24 @@ export function IntegrationsDialog({
<Spinner />
</div>
) : (
<div className="mt-4">
<IntegrationsManager
onCreateDialogClose={() => setShowCreateDialog(false)}
onIntegrationChange={handleIntegrationChange}
showCreateDialog={showCreateDialog}
/>
<div className="mt-4 space-y-4">
<div className="relative">
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
<Input
className="pl-9"
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter connections..."
value={filter}
/>
</div>
<div className="max-h-[300px] overflow-y-auto">
<IntegrationsManager
filter={filter}
onCreateDialogClose={() => setShowCreateDialog(false)}
onIntegrationChange={handleIntegrationChange}
showCreateDialog={showCreateDialog}
/>
</div>
</div>
)}

Expand Down
147 changes: 88 additions & 59 deletions components/settings/integrations-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ type IntegrationsManagerProps = {
showCreateDialog: boolean;
onCreateDialogClose?: () => void;
onIntegrationChange?: () => void;
filter?: string;
};

export function IntegrationsManager({
showCreateDialog: externalShowCreateDialog,
onCreateDialogClose,
onIntegrationChange,
filter = "",
}: IntegrationsManagerProps) {
const [integrations, setIntegrations] = useState<Integration[]>([]);
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -69,6 +71,7 @@ export function IntegrationsManager({
// Get integrations with their labels, sorted by label then name
const integrationsWithLabels = useMemo(() => {
const labels = getIntegrationLabels() as Record<string, string>;
const filterLower = filter.toLowerCase();

return integrations
.map((integration) => ({
Expand All @@ -78,14 +81,22 @@ export function IntegrationsManager({
SYSTEM_INTEGRATION_LABELS[integration.type] ||
integration.type,
}))
.filter((integration) => {
if (!filter) return true;
return (
integration.label.toLowerCase().includes(filterLower) ||
integration.name.toLowerCase().includes(filterLower) ||
integration.type.toLowerCase().includes(filterLower)
);
})
.sort((a, b) => {
const labelCompare = a.label.localeCompare(b.label);
if (labelCompare !== 0) {
return labelCompare;
}
return a.name.localeCompare(b.name);
});
}, [integrations]);
}, [integrations, filter]);

const handleDelete = async (id: string) => {
try {
Expand Down Expand Up @@ -139,70 +150,88 @@ export function IntegrationsManager({
);
}

return (
<div className="space-y-1">
{integrations.length === 0 ? (
const renderIntegrationsList = () => {
if (integrations.length === 0) {
return (
<div className="py-8 text-center">
<p className="text-muted-foreground text-sm">
No connections configured yet
</p>
</div>
) : (
<div className="space-y-1">
{integrationsWithLabels.map((integration) => (
<div
className="flex items-center justify-between rounded-md px-2 py-1.5"
key={integration.id}
>
<div className="flex items-center gap-2">
<IntegrationIcon
className="size-4"
integration={
integration.type === "ai-gateway"
? "vercel"
: integration.type
}
/>
<span className="font-medium text-sm">{integration.label}</span>
<span className="text-muted-foreground text-sm">
{integration.name}
</span>
</div>
<div className="flex items-center gap-1">
<Button
className="h-7 px-2"
disabled={testingId === integration.id}
onClick={() => handleTest(integration.id)}
size="sm"
variant="outline"
>
{testingId === integration.id ? (
<Spinner className="size-3" />
) : (
<span className="text-xs">Test</span>
)}
</Button>
<Button
className="size-7"
onClick={() => setEditingIntegration(integration)}
size="icon"
variant="outline"
>
<Pencil className="size-3" />
</Button>
<Button
className="size-7"
onClick={() => setDeletingId(integration.id)}
size="icon"
variant="outline"
>
<Trash2 className="size-3" />
</Button>
</div>
</div>
))}
);
}

if (integrationsWithLabels.length === 0) {
return (
<div className="py-8 text-center">
<p className="text-muted-foreground text-sm">
No connections match your filter
</p>
</div>
)}
);
}

return (
<div className="space-y-1">
{integrationsWithLabels.map((integration) => (
<div
className="flex items-center justify-between rounded-md px-2 py-1.5"
key={integration.id}
>
<div className="flex items-center gap-2">
<IntegrationIcon
className="size-4"
integration={
integration.type === "ai-gateway"
? "vercel"
: integration.type
}
/>
<span className="font-medium text-sm">{integration.label}</span>
<span className="text-muted-foreground text-sm">
{integration.name}
</span>
</div>
<div className="flex items-center gap-1">
<Button
className="h-7 px-2"
disabled={testingId === integration.id}
onClick={() => handleTest(integration.id)}
size="sm"
variant="outline"
>
{testingId === integration.id ? (
<Spinner className="size-3" />
) : (
<span className="text-xs">Test</span>
)}
</Button>
<Button
className="size-7"
onClick={() => setEditingIntegration(integration)}
size="icon"
variant="outline"
>
<Pencil className="size-3" />
</Button>
<Button
className="size-7"
onClick={() => setDeletingId(integration.id)}
size="icon"
variant="outline"
>
<Trash2 className="size-3" />
</Button>
</div>
</div>
))}
</div>
);
};

return (
<div className="space-y-1">
{renderIntegrationsList()}

{(showCreateDialog || editingIntegration) && (
<IntegrationFormDialog
Expand Down
Loading