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
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user