Files
kandev/apps/web/components/settings/executor-profiles-card.tsx
T
2026-09-10 18:14:05 +01:00

165 lines
5.6 KiB
TypeScript

"use client";
import { useState, useCallback } from "react";
import { useRouter } from "@/lib/routing/client-router";
import { IconTrash, IconPlus, IconChevronRight } from "@tabler/icons-react";
import { Badge } from "@kandev/ui/badge";
import { Button } from "@kandev/ui/button";
import { Card, CardContent } from "@kandev/ui/card";
import { deleteExecutorProfile, listExecutorProfiles } from "@/lib/api/domains/settings-api";
import { ExecutorProfileDialog } from "@/components/settings/executor-profile-dialog";
import { useAppStore } from "@/components/state-provider";
import type { ExecutorProfile } from "@/lib/types/http";
import { useTranslation } from "react-i18next";
import { SettingsCardHeader } from "@/components/settings/settings-card-header";
import { SETTINGS_TYPOGRAPHY } from "@/components/settings/settings-typography";
import { settingsActionClassName } from "@/components/settings/settings-control";
import { executorProfileSettingsPath } from "@/lib/settings/executor-settings-routes";
type ExecutorProfilesCardProps = {
executorId: string;
profiles: ExecutorProfile[];
};
const executorProfileDeleteActionClassName = settingsActionClassName(
"p-0 text-destructive hover:text-destructive cursor-pointer",
);
function ExecutorProfilesHeader({
title,
description,
actionLabel,
onCreate,
}: {
title: string;
description: string;
actionLabel: string;
onCreate: () => void;
}) {
return (
<SettingsCardHeader
title={title}
description={description}
actions={
<Button
variant="outline"
size="sm"
onClick={onCreate}
className={settingsActionClassName("cursor-pointer")}
>
<IconPlus className="h-4 w-4 mr-1" />
{actionLabel}
</Button>
}
/>
);
}
export function ExecutorProfilesCard({ executorId, profiles }: ExecutorProfilesCardProps) {
const { t } = useTranslation();
const router = useRouter();
const [dialogOpen, setDialogOpen] = useState(false);
const executors = useAppStore((state) => state.executors.items);
const setExecutors = useAppStore((state) => state.setExecutors);
const executorType = executors.find((executor) => executor.id === executorId)?.type;
const refreshProfiles = useCallback(async () => {
try {
const resp = await listExecutorProfiles(executorId, { cache: "no-store" });
setExecutors(
executors.map((e) => (e.id === executorId ? { ...e, profiles: resp.profiles } : e)),
);
} catch {
// ignore refresh failure
}
}, [executorId, executors, setExecutors]);
const handleCreate = useCallback(() => {
setDialogOpen(true);
}, []);
const handleProfileCreated = useCallback(
(profile: ExecutorProfile) => {
refreshProfiles();
router.push(executorProfileSettingsPath({ id: executorId, type: executorType }, profile.id));
},
[executorId, executorType, refreshProfiles, router],
);
const handleDelete = useCallback(
async (e: React.MouseEvent, profileId: string) => {
e.stopPropagation();
try {
await deleteExecutorProfile(executorId, profileId);
await refreshProfiles();
} catch {
// ignore delete failure
}
},
[executorId, refreshProfiles],
);
return (
<>
<Card>
<ExecutorProfilesHeader
title={t("executors:profiles")}
description={t("executors:differentConfigurationsForThisExecutorEach")}
actionLabel={t("executors:add")}
onCreate={handleCreate}
/>
<CardContent>
{profiles.length === 0 ? (
<p className="text-sm text-muted-foreground">{t("executors:noProfilesConfigured")}</p>
) : (
<div className="space-y-2">
{profiles.map((profile) => (
<div
key={profile.id}
className="flex items-center justify-between rounded-md border px-3 py-2 hover:bg-muted/50 cursor-pointer transition-colors"
onClick={() =>
router.push(
executorProfileSettingsPath(
{ id: executorId, type: executorType },
profile.id,
),
)
}
>
<div className="flex items-center gap-2 min-w-0">
<span className="text-sm font-medium truncate">{profile.name}</span>
{profile.prepare_script && (
<Badge variant="outline" className={`${SETTINGS_TYPOGRAPHY.meta} shrink-0`}>
{t("executors:prepareScriptBadge")}
</Badge>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<Button
variant="ghost"
size="icon"
onClick={(e) => handleDelete(e, profile.id)}
aria-label={t("executors:deleteProfile")}
data-testid="executor-profile-delete-button"
className={executorProfileDeleteActionClassName}
>
<IconTrash className="h-3.5 w-3.5" />
</Button>
<IconChevronRight className="h-4 w-4 text-muted-foreground" />
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
<ExecutorProfileDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
executorId={executorId}
onSaved={handleProfileCreated}
/>
</>
);
}