import type { ReactNode } from "react";
import {
CapabilityCatalogIndicator,
CapabilityCatalogRow,
type CapabilityCatalogStatus,
} from "./capability-catalog-row";
import { useOpenGeniText } from "./messages/runtime";
/** Status comes from the host/controller, never inferred from a display label. */
export type ConnectionCatalogOption = {
id: string;
name: string;
description?: string | undefined;
status: string;
connected: boolean;
/** Explicit health/availability. Omit to retain visible legacy status text. */
state?: CapabilityCatalogStatus;
onOpen: () => void;
action?: ReactNode;
};
export type ConnectionCatalogService = {
id: string;
name: string;
logo?: ReactNode;
options: ConnectionCatalogOption[];
};
export type ConnectionCatalogProps = {
services: ConnectionCatalogService[];
query?: string;
columns?: 1 | 2;
resultLimit?: number;
onShowMore?: () => void;
/** Group capabilities by service, or show every capability independently. */
grouped?: boolean;
className?: string;
emptyMessage?: string;
};
export function ConnectionOptionRow({ option }: { option: ConnectionCatalogOption }) {
return (
{option.action ? {option.action} : null}
);
}
export function ConnectionServiceRow({ service }: { service: ConnectionCatalogService }) {
const t = useOpenGeniText();
if (!service.options.length) return null;
const connected = service.options.filter((option) => option.connected).length;
const attention = service.options.find(
(option) => option.state === "attention" || option.state === "unavailable",
);
const groupState =
attention?.state ??
(service.options.some((option) => option.state === "loading")
? "loading"
: connected === service.options.length
? "added"
: "available");
const identity = (
<>
{service.logo}
{service.name}
{service.options.length === 1
? service.options[0]?.description
: service.options.map((option) => option.name).join(" · ")}
>
);
return service.options.length === 1 ? (
{service.options[0]?.action}
) : (
{identity}
⌄
{service.options.map((option) => (
))}
);
}
export function ConnectionCatalog({
services,
query = "",
columns = 1,
grouped = true,
resultLimit,
onShowMore,
className = "",
emptyMessage: emptyMessageProp,
}: ConnectionCatalogProps) {
const t = useOpenGeniText();
const emptyMessage =
emptyMessageProp === undefined
? t("connectionCatalog.noConnectionsMatchYourSearch")
: emptyMessageProp;
const search = query.trim().toLowerCase();
const entries = grouped
? services
: services.flatMap((service) =>
service.options.map((option) => ({
...service,
id: `${service.id}/${option.id}`,
name:
service.options.length > 1 && option.name !== service.name
? `${service.name} · ${option.name}`
: service.name,
options: [option],
})),
);
const visible = entries.filter((service) =>
[service.name, ...service.options.flatMap((option) => [option.name, option.description ?? ""])]
.join(" ")
.toLowerCase()
.includes(search),
);
return (
<>
{visible.length ? (
visible
.slice(0, resultLimit)
.map((service) =>
)
) : (
{emptyMessage}
)}
{resultLimit && visible.length > resultLimit ? (
) : null}
>
);
}