Files
headquarter/apps/web/src/components/features/session/SessionList.tsx
T
Developer ae420708f2 refactor: split tool-workshop page into tab components (Task 4.1)
- Extract ToolTypesTab, ToolConfigsTab, ConfigFoldersTab from inline page
- Each tab is self-contained with own state, API calls, and forms
- Slim page to 77 lines (tab switcher + composition only)
- Add barrel export for tool-workshop feature components
- Add tsconfig path alias for @/* imports

Quality gates: tsc (pass), eslint (pass)
Refs: repo-restructure Task 4.1
2026-06-02 21:49:20 +00:00

195 lines
4.6 KiB
TypeScript

import React, { useCallback, useEffect, useState } from "react";
import {
stopInstance,
deleteInstance,
checkInstanceHealth,
recreateInstanceTunnel,
} from "@/api/sessions";
import type { Session } from "@/types/session";
import { SessionCard } from "./SessionCard";
interface SessionListProps {
sessions: Session[];
variant: "active" | "recent";
onSessionChange?: () => void;
onOpen?: (session: Session) => void;
}
export const SessionList: React.FC<SessionListProps> = ({
sessions,
variant,
onSessionChange,
onOpen,
}) => {
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const [stopConfirmId, setStopConfirmId] = useState<string | null>(null);
const [tunnelHealth, setTunnelHealth] = useState<
Record<
string,
{ healthy: boolean; status_code: number | null; error?: string }
>
>({});
const [recreatingId, setRecreatingId] = useState<string | null>(null);
// Poll tunnel health every 30 seconds for running instances (active only)
useEffect(() => {
if (variant !== "active") return;
const checkHealth = async () => {
const runningSessions = sessions.filter(
(s) => s.status === "running" && s.url,
);
for (const session of runningSessions) {
try {
const health = await checkInstanceHealth(
session.project_id,
session.repository_id,
session.id,
);
setTunnelHealth((prev) => ({ ...prev, [session.id]: health }));
} catch {
setTunnelHealth((prev) => ({
...prev,
[session.id]: {
healthy: false,
status_code: null,
error: "check failed",
},
}));
}
}
};
void checkHealth();
const interval = setInterval(() => void checkHealth(), 30000);
return () => clearInterval(interval);
}, [sessions, variant]);
const handleStop = useCallback(
async (session: Session) => {
try {
await stopInstance(
session.project_id,
session.repository_id,
session.id,
);
setStopConfirmId(null);
onSessionChange?.();
} catch {
setStopConfirmId(null);
}
},
[onSessionChange],
);
const handleDelete = useCallback(
async (session: Session) => {
try {
await deleteInstance(
session.project_id,
session.repository_id,
session.id,
);
setDeleteConfirmId(null);
onSessionChange?.();
} catch {
setDeleteConfirmId(null);
}
},
[onSessionChange],
);
const handleRecreateTunnel = useCallback(
async (session: Session) => {
setRecreatingId(session.id);
try {
await recreateInstanceTunnel(
session.project_id,
session.repository_id,
session.id,
);
onSessionChange?.();
} catch {
// ignore
} finally {
setRecreatingId(null);
}
},
[onSessionChange],
);
const handleOpen = useCallback(
(session: Session) => {
if (session.url) {
window.open(session.url, "_blank", "noopener,noreferrer");
} else {
onOpen?.(session);
}
},
[onOpen],
);
if (sessions.length === 0) {
return (
<p className="muted">
{variant === "active" ? "No active sessions" : "No recent sessions"}
</p>
);
}
return (
<>
{variant === "active" ? (
<div className="sessions-grid">
{sessions.map((session) => (
<SessionCard
key={session.id}
session={session}
variant="active"
tunnelHealth={tunnelHealth[session.id] ?? null}
isRecreating={recreatingId === session.id}
isStopConfirming={stopConfirmId === session.id}
isDeleteConfirming={deleteConfirmId === session.id}
onOpen={() => handleOpen(session)}
onStop={() =>
stopConfirmId === session.id
? handleStop(session)
: setStopConfirmId(session.id)
}
onDelete={() =>
deleteConfirmId === session.id
? handleDelete(session)
: setDeleteConfirmId(session.id)
}
onRecreateTunnel={() => handleRecreateTunnel(session)}
onCancelStop={() => setStopConfirmId(null)}
onCancelDelete={() => setDeleteConfirmId(null)}
/>
))}
</div>
) : (
<div className="recent-sessions-list">
{sessions.map((session) => (
<SessionCard
key={session.id}
session={session}
variant="recent"
isDeleteConfirming={deleteConfirmId === session.id}
onOpen={() => handleOpen(session)}
onStop={() => {}}
onDelete={() =>
deleteConfirmId === session.id
? handleDelete(session)
: setDeleteConfirmId(session.id)
}
onRecreateTunnel={() => {}}
onCancelStop={() => {}}
onCancelDelete={() => setDeleteConfirmId(null)}
/>
))}
</div>
)}
</>
);
};