Files
triggerdotdev--trigger.dev/apps/webapp/app/components/integrations/AddApiKeyButton.tsx
T

161 lines
5.3 KiB
TypeScript

import { InformationCircleIcon } from "@heroicons/react/24/outline";
import type {
APIKeyAuthentication,
IntegrationMetadata,
} from "@trigger.dev/integration-sdk";
import { marked } from "marked";
import { Fragment, useEffect, useState } from "react";
import { useTypedFetcher } from "remix-typedjson";
import type { Response as CreateResponse } from "~/routes/resources/connection";
import { PrimaryButton, SecondaryButton } from "../primitives/Buttons";
import { StyledDialog } from "../primitives/Dialog";
import { FormError } from "../primitives/FormError";
import { Input } from "../primitives/Input";
import { InputGroup } from "../primitives/InputGroup";
import { Label } from "../primitives/Label";
import { Body } from "../primitives/text/Body";
type Status = "loading" | "idle";
export function AddApiKeyButton({
integration,
authentication,
organizationId,
sourceId,
serviceId,
className,
children,
}: {
integration: IntegrationMetadata;
authentication: APIKeyAuthentication;
organizationId: string;
sourceId?: string;
serviceId?: string;
className?: string;
children: (status: Status) => React.ReactNode;
}) {
const fetcher = useTypedFetcher<CreateResponse>();
const [isOpen, setIsOpen] = useState(false);
const errors =
fetcher.type === "done" && !fetcher.data.success
? fetcher.data.errors
: undefined;
useEffect(() => {
if (
fetcher.type === "done" &&
fetcher.state === "idle" &&
fetcher.data?.success === true
) {
setIsOpen(false);
}
}, [fetcher.type, fetcher.state, fetcher.data]);
return (
<>
<button onClick={(e) => setIsOpen(true)} className={className}>
{children(fetcher.state === "idle" ? "idle" : "loading")}
</button>
<StyledDialog.Dialog
onClose={(e) => setIsOpen(false)}
appear
show={isOpen}
as={Fragment}
>
<StyledDialog.Panel>
<StyledDialog.Title>
Add your {integration.name} API keys
</StyledDialog.Title>
<div className="overflow-hidden overflow-y-auto max-h-40 mb-3 mt-4 bg-slate-700/50 rounded px-4 pb-3 pt-4">
<div className="flex gap-0.5 items-center -m-0.5 mb-1 text-slate-400">
<InformationCircleIcon className="h-5 w-5" />
<Body size="small" className="uppercase tracking-wide">
Instructions
</Body>
</div>
<p
className="prose prose-sm prose-invert"
dangerouslySetInnerHTML={{
__html: marked(authentication.documentation),
}}
/>
</div>
<fetcher.Form method="post" action="/resources/connection?index">
<input type="hidden" name="type" value="api_key" />
<input type="hidden" name="organizationId" value={organizationId} />
<input type="hidden" name="service" value={integration.slug} />
{sourceId && (
<input type="hidden" name="sourceId" value={sourceId} />
)}
{serviceId && (
<input type="hidden" name="serviceId" value={serviceId} />
)}
<InputGroup>
<Label htmlFor="title">Name</Label>
<div className="flex">
<img
src={integration.icon}
alt={integration.icon}
className="flex pointer-events-none z-10 -mr-7 ml-2.5 mt-2.5 w-5 h-5"
/>
<Input
id="title"
name="title"
placeholder="Name this integration"
spellCheck={false}
defaultValue={integration.name}
className="pl-9"
/>
</div>
{errors && <FormError errors={errors} path={["title"]} />}
</InputGroup>
<InputGroup>
<Label htmlFor="api_key">API Key</Label>
<Input
id="api_key"
name="api_key"
placeholder="<api_key>"
spellCheck={false}
type="password"
/>
{errors && <FormError errors={errors} path={["api_key"]} />}
</InputGroup>
{authentication.additionalFields?.map((field) => {
const fieldName = `additionalFields[${field.key}]`;
return (
<InputGroup key={field.key}>
<Label htmlFor={fieldName}>{field.name}</Label>
<Input
id={fieldName}
name={fieldName}
placeholder={field.placeholder}
spellCheck={false}
/>
{errors && (
<FormError
errors={errors}
path={["additionalFields", field.key]}
/>
)}
</InputGroup>
);
})}
<div className="mt-4 flex justify-between">
<SecondaryButton type="button" onClick={(e) => setIsOpen(false)}>
Cancel
</SecondaryButton>
<PrimaryButton type="submit">Save</PrimaryButton>
</div>
</fetcher.Form>
</StyledDialog.Panel>
</StyledDialog.Dialog>
</>
);
}