import {
deleteIntegration,
redeliverIntegration,
runGithubJob,
testIntegration,
updateIntegration,
} from "~/fns/integrations";
import { useAction } from "~/hooks/use-action";
import { Button } from "~/ui/Button";
import { CheckboxField, SelectField, TextAreaField, TextField } from "~/ui/Field";
import { ErrorBanner } from "~/ui/Callout";
import { Badge } from "~/ui/Badge";
import { Panel } from "~/ui/Panel";
import { RelativeTime } from "~/ui/RelativeTime";
import {
DEFAULT_LINEAR_MAPPING,
INTEGRATION_EVENT_LABELS,
INTEGRATION_EVENTS,
INTEGRATION_KIND_LABELS,
} from "~/lib/integrations";
import type { GithubMirrorDirection, IntegrationEvent, SlackChannelRoute } from "~/lib/integrations";
import type { IntegrationsData, ScopeParams } from "./IntegrationsPage";
type Integration = IntegrationsData["integrations"][number];
const DELIVERY_OPTIONS = [
{ value: "instant", label: "Instant" },
{ value: "digest", label: "Digest (batched)" },
];
const MIRROR_OPTIONS = [
{ value: "none", label: "Off" },
{ value: "push", label: "gitfor → GitHub" },
{ value: "pull", label: "GitHub → gitfor" },
];
const STATUS_TONE = { success: "success", failed: "danger", dead: "danger", pending: "warning" } as const;
function isEvent(value: string): value is IntegrationEvent {
return INTEGRATION_EVENTS.some((event) => event === value);
}
/** One channel per line: `C0123ABCD: pull_request, release`. */
function parseChannels(text: string): SlackChannelRoute[] {
return text
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [channel, events = ""] = line.split(":", 2);
return {
channel: channel.trim(),
events: events
.split(",")
.map((event) => event.trim())
.filter(isEvent),
};
});
}
function formatChannels(routes: SlackChannelRoute[] | undefined): string {
return (routes ?? []).map((route) => `${route.channel}: ${route.events.join(", ")}`).join("\n");
}
function Deliveries({ integration, scope }: { integration: Integration; scope: ScopeParams }) {
const redeliver = useAction(redeliverIntegration);
return (
Recent deliveries
{integration.deliveries.length === 0 && (
- No deliveries yet
)}
{integration.deliveries.map((delivery) => (
-
{delivery.status}
{delivery.status_code ? ` ${delivery.status_code}` : ""}
{delivery.event}
{delivery.direction === "inbound" && inbound}
{delivery.error}
{delivery.duration_ms ?? 0}ms · try {delivery.attempt} ·
{delivery.direction === "outbound" && (
)}
))}
);
}
function GithubJobs({ integration, scope }: { integration: Integration; scope: ScopeParams }) {
const job = useAction(runGithubJob);
return (
{integration.jobs.map((entry) => (
{entry.status}
{" "}
import · {entry.progress} {entry.error} ·
))}
);
}
const LINEAR_MAPPINGS = [
{ key: "opened", name: "mapOpened", label: "On PR open move to", hint: undefined },
{ key: "merged", name: "mapMerged", label: "On merge move to", hint: undefined },
{ key: "closed", name: "mapClosed", label: "On close move to", hint: "Empty leaves the issue alone" },
] as const;
export function IntegrationCard({ integration, scope }: { integration: Integration; scope: ScopeParams }) {
const save = useAction(updateIntegration);
const remove = useAction(deleteIntegration);
const test = useAction(testIntegration);
const config = integration.config;
const chat = integration.kind !== "linear" && integration.kind !== "github";
const mapping = { ...DEFAULT_LINEAR_MAPPING, ...config.linearMapping };
return (
{INTEGRATION_KIND_LABELS[integration.kind]}
{integration.name}
{!integration.active && paused}
{integration.deliveryMode === "digest" && digest}
}
actions={
}
>
{integration.kind === "github" && }
{integration.audit.length > 0 && (
Audit
{integration.audit.map((entry) => (
-
{entry.action} by {entry.actor ?? "unknown"} · {entry.detail}{" "}
·
))}
)}
);
}