Files
kandev/apps/web/components/jira/jira-issue-watch-table.tsx
T
2026-06-15 18:54:16 +01:00

201 lines
6.2 KiB
TypeScript

"use client";
import {
IconTrash,
IconRefresh,
IconPlayerPlay,
IconPlayerPause,
IconRestore,
} from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import { Badge } from "@kandev/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kandev/ui/table";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useAppStore } from "@/components/state-provider";
import type { JiraIssueWatch } from "@/lib/types/jira";
type JiraIssueWatchTableProps = {
watches: JiraIssueWatch[];
// showWorkspace renders a Workspace column when the table aggregates rows
// from every workspace (install-wide settings page). Off for the legacy
// single-workspace surfaces.
showWorkspace?: boolean;
onEdit: (watch: JiraIssueWatch) => void;
onDelete: (id: string) => void;
onTrigger: (id: string) => void;
onReset: (id: string) => void;
onToggleEnabled: (watch: JiraIssueWatch) => void;
};
function formatLastPolled(dateStr?: string | null): string {
if (!dateStr) return "Never";
const diff = Date.now() - new Date(dateStr).getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return "Just now";
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return `${Math.floor(hours / 24)}d ago`;
}
function WatchActions({
watch,
onToggleEnabled,
onTrigger,
onReset,
onDelete,
}: {
watch: JiraIssueWatch;
onToggleEnabled: (watch: JiraIssueWatch) => void;
onTrigger: (id: string) => void;
onReset: (id: string) => void;
onDelete: (id: string) => void;
}) {
return (
<div className="flex items-center justify-end gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
onToggleEnabled(watch);
}}
>
{watch.enabled ? (
<IconPlayerPause className="h-3.5 w-3.5" />
) : (
<IconPlayerPlay className="h-3.5 w-3.5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>{watch.enabled ? "Pause" : "Enable"}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
// Toast is fired by the parent's wrappedTrigger on completion —
// the inline "Checking…" toast it used to fire here was a duplicate.
onTrigger(watch.id);
}}
>
<IconRefresh className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Check now</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 cursor-pointer"
data-testid="watch-reset-button"
aria-label="Reset watch"
onClick={(e) => {
e.stopPropagation();
onReset(watch.id);
}}
>
<IconRestore className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Reset</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-red-500 hover:text-red-600 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
onDelete(watch.id);
}}
>
<IconTrash className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</div>
);
}
export function JiraIssueWatchTable({
watches,
showWorkspace,
onEdit,
onDelete,
onTrigger,
onReset,
onToggleEnabled,
}: JiraIssueWatchTableProps) {
const workspaces = useAppStore((s) => s.workspaces.items);
const workspaceName = (id: string) => workspaces.find((w) => w.id === id)?.name ?? id;
if (watches.length === 0) {
return (
<p className="text-sm text-muted-foreground py-4 text-center">
No JIRA watchers configured. Create one to auto-create tasks from JQL queries.
</p>
);
}
return (
<Table>
<TableHeader>
<TableRow>
{showWorkspace && <TableHead>Workspace</TableHead>}
<TableHead>JQL</TableHead>
<TableHead>Interval</TableHead>
<TableHead>Last Polled</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{watches.map((watch) => (
<TableRow key={watch.id} className="cursor-pointer" onClick={() => onEdit(watch)}>
{showWorkspace && (
<TableCell className="text-xs text-muted-foreground">
{workspaceName(watch.workspaceId)}
</TableCell>
)}
<TableCell className="font-mono text-xs max-w-md truncate" title={watch.jql}>
{watch.jql}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{Math.round(watch.pollIntervalSeconds / 60)}m
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{formatLastPolled(watch.lastPolledAt)}
</TableCell>
<TableCell>
<Badge variant={watch.enabled ? "default" : "secondary"} className="text-xs">
{watch.enabled ? "Active" : "Paused"}
</Badge>
</TableCell>
<TableCell className="text-right">
<WatchActions
watch={watch}
onToggleEnabled={onToggleEnabled}
onTrigger={onTrigger}
onReset={onReset}
onDelete={onDelete}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}