ae420708f2
- 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
195 lines
4.6 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
};
|