Skip to content

Commit 8b2f075

Browse files
authored
make "new step" more robust (#160)
* better actions * robust new step
1 parent d32b3cc commit 8b2f075

File tree

5 files changed

+500
-126
lines changed

5 files changed

+500
-126
lines changed

components/settings/integration-form-dialog.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
getIntegrationLabels,
4444
getSortedIntegrationTypes,
4545
} from "@/plugins";
46+
import { getIntegrationDescriptions } from "@/plugins/registry";
4647

4748
type IntegrationFormDialogProps = {
4849
open: boolean;
@@ -65,6 +66,9 @@ const SYSTEM_INTEGRATION_TYPES: IntegrationType[] = ["database"];
6566
const SYSTEM_INTEGRATION_LABELS: Record<string, string> = {
6667
database: "Database",
6768
};
69+
const SYSTEM_INTEGRATION_DESCRIPTIONS: Record<string, string> = {
70+
database: "Connect to PostgreSQL databases",
71+
};
6872

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

83+
// Get description for any integration type
84+
const getDescription = (type: IntegrationType): string =>
85+
getIntegrationDescriptions()[type] ||
86+
SYSTEM_INTEGRATION_DESCRIPTIONS[type] ||
87+
"";
88+
7989
function SecretField({
8090
fieldId,
8191
label,
@@ -419,20 +429,31 @@ function TypeSelector({
419429
No services found
420430
</p>
421431
) : (
422-
filteredTypes.map((type) => (
423-
<button
424-
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-muted/50"
425-
key={type}
426-
onClick={() => onSelectType(type)}
427-
type="button"
428-
>
429-
<IntegrationIcon
430-
className="size-5"
431-
integration={type === "ai-gateway" ? "vercel" : type}
432-
/>
433-
<span className="font-medium">{getLabel(type)}</span>
434-
</button>
435-
))
432+
filteredTypes.map((type) => {
433+
const description = getDescription(type);
434+
return (
435+
<button
436+
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-muted/50"
437+
key={type}
438+
onClick={() => onSelectType(type)}
439+
type="button"
440+
>
441+
<IntegrationIcon
442+
className="size-5 shrink-0"
443+
integration={type === "ai-gateway" ? "vercel" : type}
444+
/>
445+
<span className="min-w-0 flex-1 truncate">
446+
<span className="font-medium">{getLabel(type)}</span>
447+
{description && (
448+
<span className="text-muted-foreground text-xs">
449+
{" "}
450+
- {description}
451+
</span>
452+
)}
453+
</span>
454+
</button>
455+
);
456+
})
436457
)}
437458
</div>
438459
</div>

components/settings/integrations-dialog.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useSetAtom } from "jotai";
4-
import { Plus } from "lucide-react";
4+
import { Plus, Search } from "lucide-react";
55
import { useCallback, useEffect, useRef, useState } from "react";
66
import { Button } from "@/components/ui/button";
77
import {
@@ -12,6 +12,7 @@ import {
1212
DialogHeader,
1313
DialogTitle,
1414
} from "@/components/ui/dialog";
15+
import { Input } from "@/components/ui/input";
1516
import { Spinner } from "@/components/ui/spinner";
1617
import { integrationsVersionAtom } from "@/lib/integrations-store";
1718
import { IntegrationsManager } from "./integrations-manager";
@@ -27,6 +28,7 @@ export function IntegrationsDialog({
2728
}: IntegrationsDialogProps) {
2829
const [loading, setLoading] = useState(true);
2930
const [showCreateDialog, setShowCreateDialog] = useState(false);
31+
const [filter, setFilter] = useState("");
3032
const setIntegrationsVersion = useSetAtom(integrationsVersionAtom);
3133
// Track if any changes were made during this dialog session
3234
const hasChangesRef = useRef(false);
@@ -48,6 +50,8 @@ export function IntegrationsDialog({
4850
hasChangesRef.current = false;
4951
// Reset create dialog state when opening
5052
setShowCreateDialog(false);
53+
// Reset filter when opening
54+
setFilter("");
5155
}
5256
}, [open, loadAll]);
5357

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

6973
return (
7074
<Dialog onOpenChange={handleClose} open={open}>
71-
<DialogContent
72-
className="max-h-[90vh] max-w-4xl overflow-y-auto"
73-
showCloseButton={false}
74-
>
75+
<DialogContent className="max-w-4xl" showCloseButton={false}>
7576
<DialogHeader>
7677
<DialogTitle>Connections</DialogTitle>
7778
<DialogDescription>
@@ -84,12 +85,24 @@ export function IntegrationsDialog({
8485
<Spinner />
8586
</div>
8687
) : (
87-
<div className="mt-4">
88-
<IntegrationsManager
89-
onCreateDialogClose={() => setShowCreateDialog(false)}
90-
onIntegrationChange={handleIntegrationChange}
91-
showCreateDialog={showCreateDialog}
92-
/>
88+
<div className="mt-4 space-y-4">
89+
<div className="relative">
90+
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
91+
<Input
92+
className="pl-9"
93+
onChange={(e) => setFilter(e.target.value)}
94+
placeholder="Filter connections..."
95+
value={filter}
96+
/>
97+
</div>
98+
<div className="max-h-[300px] overflow-y-auto">
99+
<IntegrationsManager
100+
filter={filter}
101+
onCreateDialogClose={() => setShowCreateDialog(false)}
102+
onIntegrationChange={handleIntegrationChange}
103+
showCreateDialog={showCreateDialog}
104+
/>
105+
</div>
93106
</div>
94107
)}
95108

components/settings/integrations-manager.tsx

Lines changed: 88 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ type IntegrationsManagerProps = {
2929
showCreateDialog: boolean;
3030
onCreateDialogClose?: () => void;
3131
onIntegrationChange?: () => void;
32+
filter?: string;
3233
};
3334

3435
export function IntegrationsManager({
3536
showCreateDialog: externalShowCreateDialog,
3637
onCreateDialogClose,
3738
onIntegrationChange,
39+
filter = "",
3840
}: IntegrationsManagerProps) {
3941
const [integrations, setIntegrations] = useState<Integration[]>([]);
4042
const [loading, setLoading] = useState(true);
@@ -69,6 +71,7 @@ export function IntegrationsManager({
6971
// Get integrations with their labels, sorted by label then name
7072
const integrationsWithLabels = useMemo(() => {
7173
const labels = getIntegrationLabels() as Record<string, string>;
74+
const filterLower = filter.toLowerCase();
7275

7376
return integrations
7477
.map((integration) => ({
@@ -78,14 +81,22 @@ export function IntegrationsManager({
7881
SYSTEM_INTEGRATION_LABELS[integration.type] ||
7982
integration.type,
8083
}))
84+
.filter((integration) => {
85+
if (!filter) return true;
86+
return (
87+
integration.label.toLowerCase().includes(filterLower) ||
88+
integration.name.toLowerCase().includes(filterLower) ||
89+
integration.type.toLowerCase().includes(filterLower)
90+
);
91+
})
8192
.sort((a, b) => {
8293
const labelCompare = a.label.localeCompare(b.label);
8394
if (labelCompare !== 0) {
8495
return labelCompare;
8596
}
8697
return a.name.localeCompare(b.name);
8798
});
88-
}, [integrations]);
99+
}, [integrations, filter]);
89100

90101
const handleDelete = async (id: string) => {
91102
try {
@@ -139,70 +150,88 @@ export function IntegrationsManager({
139150
);
140151
}
141152

142-
return (
143-
<div className="space-y-1">
144-
{integrations.length === 0 ? (
153+
const renderIntegrationsList = () => {
154+
if (integrations.length === 0) {
155+
return (
145156
<div className="py-8 text-center">
146157
<p className="text-muted-foreground text-sm">
147158
No connections configured yet
148159
</p>
149160
</div>
150-
) : (
151-
<div className="space-y-1">
152-
{integrationsWithLabels.map((integration) => (
153-
<div
154-
className="flex items-center justify-between rounded-md px-2 py-1.5"
155-
key={integration.id}
156-
>
157-
<div className="flex items-center gap-2">
158-
<IntegrationIcon
159-
className="size-4"
160-
integration={
161-
integration.type === "ai-gateway"
162-
? "vercel"
163-
: integration.type
164-
}
165-
/>
166-
<span className="font-medium text-sm">{integration.label}</span>
167-
<span className="text-muted-foreground text-sm">
168-
{integration.name}
169-
</span>
170-
</div>
171-
<div className="flex items-center gap-1">
172-
<Button
173-
className="h-7 px-2"
174-
disabled={testingId === integration.id}
175-
onClick={() => handleTest(integration.id)}
176-
size="sm"
177-
variant="outline"
178-
>
179-
{testingId === integration.id ? (
180-
<Spinner className="size-3" />
181-
) : (
182-
<span className="text-xs">Test</span>
183-
)}
184-
</Button>
185-
<Button
186-
className="size-7"
187-
onClick={() => setEditingIntegration(integration)}
188-
size="icon"
189-
variant="outline"
190-
>
191-
<Pencil className="size-3" />
192-
</Button>
193-
<Button
194-
className="size-7"
195-
onClick={() => setDeletingId(integration.id)}
196-
size="icon"
197-
variant="outline"
198-
>
199-
<Trash2 className="size-3" />
200-
</Button>
201-
</div>
202-
</div>
203-
))}
161+
);
162+
}
163+
164+
if (integrationsWithLabels.length === 0) {
165+
return (
166+
<div className="py-8 text-center">
167+
<p className="text-muted-foreground text-sm">
168+
No connections match your filter
169+
</p>
204170
</div>
205-
)}
171+
);
172+
}
173+
174+
return (
175+
<div className="space-y-1">
176+
{integrationsWithLabels.map((integration) => (
177+
<div
178+
className="flex items-center justify-between rounded-md px-2 py-1.5"
179+
key={integration.id}
180+
>
181+
<div className="flex items-center gap-2">
182+
<IntegrationIcon
183+
className="size-4"
184+
integration={
185+
integration.type === "ai-gateway"
186+
? "vercel"
187+
: integration.type
188+
}
189+
/>
190+
<span className="font-medium text-sm">{integration.label}</span>
191+
<span className="text-muted-foreground text-sm">
192+
{integration.name}
193+
</span>
194+
</div>
195+
<div className="flex items-center gap-1">
196+
<Button
197+
className="h-7 px-2"
198+
disabled={testingId === integration.id}
199+
onClick={() => handleTest(integration.id)}
200+
size="sm"
201+
variant="outline"
202+
>
203+
{testingId === integration.id ? (
204+
<Spinner className="size-3" />
205+
) : (
206+
<span className="text-xs">Test</span>
207+
)}
208+
</Button>
209+
<Button
210+
className="size-7"
211+
onClick={() => setEditingIntegration(integration)}
212+
size="icon"
213+
variant="outline"
214+
>
215+
<Pencil className="size-3" />
216+
</Button>
217+
<Button
218+
className="size-7"
219+
onClick={() => setDeletingId(integration.id)}
220+
size="icon"
221+
variant="outline"
222+
>
223+
<Trash2 className="size-3" />
224+
</Button>
225+
</div>
226+
</div>
227+
))}
228+
</div>
229+
);
230+
};
231+
232+
return (
233+
<div className="space-y-1">
234+
{renderIntegrationsList()}
206235

207236
{(showCreateDialog || editingIntegration) && (
208237
<IntegrationFormDialog

0 commit comments

Comments
 (0)