import { useCallback, useEffect, useState } from "react"; import { Effect, Exit } from "effect"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@executor-js/react/components/alert-dialog"; import { Alert, AlertDescription, AlertTitle } from "@executor-js/react/components/alert"; import { Button } from "@executor-js/react/components/button"; import { FormErrorAlert } from "@executor-js/react/lib/integration-add"; import { listCustomToolSourcesEffect, removeCustomToolSourceEffect, syncCustomToolSourceEffect, type AppSourceRecord, } from "./custom-tools-client"; import { sourceFailureLines, sourcePanelModel, syncNoticeFromResult, toolsCountLabel, type SyncNoticeModel, } from "./source-panel-model"; const integrationsHref = (): string => { const match = window.location.pathname.match(/^(.*)\/integrations(?:\/.*)?$/); return `${match?.[1] ?? ""}/integrations`; }; type LoadState = | { readonly status: "loading" } | { readonly status: "error"; readonly message: string } | { readonly status: "ready"; readonly sources: readonly AppSourceRecord[] }; export default function CustomToolsAccountsPanel(props: { readonly integrationId: string }) { const [loadState, setLoadState] = useState({ status: "loading" }); const [syncingSlug, setSyncingSlug] = useState(null); const [removingSlug, setRemovingSlug] = useState(null); const [notice, setNotice] = useState>({}); const [removeError, setRemoveError] = useState(null); const sourcesForApp = useCallback( (sources: readonly AppSourceRecord[]): readonly AppSourceRecord[] => sources.filter((source) => source.app === props.integrationId), [props.integrationId], ); const loadSources = async () => { setLoadState({ status: "loading" }); const exit = await Effect.runPromiseExit(listCustomToolSourcesEffect()); if (Exit.isFailure(exit)) { setLoadState({ status: "error", message: "Failed to load custom tools sources." }); return; } setLoadState({ status: "ready", sources: sourcesForApp(exit.value.sources) }); }; useEffect(() => { let active = true; void (async () => { const exit = await Effect.runPromiseExit(listCustomToolSourcesEffect()); if (!active) return; if (Exit.isFailure(exit)) { setLoadState({ status: "error", message: "Failed to load custom tools sources." }); return; } setLoadState({ status: "ready", sources: sourcesForApp(exit.value.sources) }); })(); return () => { active = false; }; }, [props.integrationId, sourcesForApp]); const syncSource = async (source: AppSourceRecord) => { setSyncingSlug(source.slug); setNotice((current) => ({ ...current, [source.slug]: undefined })); const beforeTools = source.status.type === "published" || source.status.type === "up-to-date" ? source.status.tools : []; const exit = await Effect.runPromiseExit(syncCustomToolSourceEffect(source.slug)); if (Exit.isFailure(exit)) { setNotice((current) => ({ ...current, [source.slug]: { status: "failed", message: "Sync failed.", added: [], removed: [], errors: ["Failed to sync custom tools source."], }, })); setSyncingSlug(null); return; } setNotice((current) => ({ ...current, [source.slug]: syncNoticeFromResult(exit.value, beforeTools), })); await loadSources(); setSyncingSlug(null); }; const removeSource = async (source: AppSourceRecord) => { setRemovingSlug(source.slug); setRemoveError(null); const exit = await Effect.runPromiseExit(removeCustomToolSourceEffect(source.slug)); if (Exit.isFailure(exit)) { setRemoveError("Failed to remove custom tools source."); setRemovingSlug(null); return; } await loadSources(); setRemovingSlug(null); window.location.assign(integrationsHref()); }; return (
{loadState.status === "loading" && (
Loading sources...
)} {loadState.status === "error" && ( Failed to load sources

{loadState.message}

)} {loadState.status === "ready" && loadState.sources.length === 0 && (

No custom tool sources

Add a Git repository to publish tools into the Apps catalog.

)} {loadState.status === "ready" && loadState.sources.map((source) => ( void syncSource(source)} onRemove={() => void removeSource(source)} /> ))}
); } function SourceDetail(props: { readonly source: AppSourceRecord; readonly notice: SyncNoticeModel | null; readonly removeError: string | null; readonly syncing: boolean; readonly removing: boolean; readonly onSync: () => void; readonly onRemove: () => void; }) { const { source, notice } = props; const model = sourcePanelModel(source); const failureLines = sourceFailureLines(source); const noticeHasDetails = notice !== null && (notice.added.length > 0 || notice.removed.length > 0 || notice.errors.length > 0 || notice.sourceRef !== undefined); return (

{model.title}

{model.source}

Remove {source.app}? This removes {toolsCountLabel(model.tools.length)} from the catalog. The source is untouched and can be added again later. Cancel {props.removing ? "Removing..." : "Remove source"}
{failureLines.length > 0 && ( Last sync failed
{failureLines.map((line) => (

{line}

))}
)} {notice && ( {notice.message} {noticeHasDetails && (
{notice.added.length > 0 &&

Added: {notice.added.join(", ")}

} {notice.removed.length > 0 &&

Removed: {notice.removed.join(", ")}

} {notice.errors.map((error) => (

{error}

))} {notice.sourceRef && (

Commit {notice.sourceRef}

)}
)}
)} {props.removeError && }
); } function Meta(props: { readonly label: string; readonly value: string; readonly mono?: boolean }) { return (

{props.label}

{props.value}

); }