3c222d4f0f
ToolStarter (used by FAB and workspace detail): - Add Session Name text input, defaulting to workspace.name - Pass user-provided name to createInstance display_name parameter - If left empty or only whitespace, falls back to auto-generated name Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
140 lines
3.4 KiB
TypeScript
140 lines
3.4 KiB
TypeScript
import type { Session } from "../../../api/sessions";
|
|
import { SessionCard } from "./session-card";
|
|
import type { InstanceHealth } from "../../../api/sessions";
|
|
|
|
export interface SessionListProps {
|
|
sessions: Session[];
|
|
onOpen?: (session: Session) => void;
|
|
onStart?: (session: Session) => void;
|
|
onStop?: (session: Session) => void;
|
|
onDelete?: (session: Session) => void;
|
|
onRecreateTunnel?: (session: Session) => void;
|
|
onRename?: (session: Session, newName: string) => void;
|
|
actionBusyId?: string | null;
|
|
tunnelHealth?: Record<string, InstanceHealth>;
|
|
showGrouping?: boolean;
|
|
activeTitle?: string;
|
|
recentTitle?: string;
|
|
maxRecent?: number;
|
|
emptyMessage?: string;
|
|
}
|
|
|
|
const activeStatuses = [
|
|
"running",
|
|
"building",
|
|
"starting",
|
|
"probing",
|
|
"pending",
|
|
"unhealthy",
|
|
];
|
|
const recentStatuses = ["stopped", "error"];
|
|
|
|
export function SessionList({
|
|
sessions,
|
|
onOpen,
|
|
onStart,
|
|
onStop,
|
|
onDelete,
|
|
onRecreateTunnel,
|
|
onRename,
|
|
actionBusyId = null,
|
|
tunnelHealth = {},
|
|
showGrouping = true,
|
|
activeTitle = "Active Sessions",
|
|
recentTitle = "Recent Sessions",
|
|
maxRecent = 5,
|
|
emptyMessage = "No sessions",
|
|
}: SessionListProps) {
|
|
const activeSessions = sessions.filter((s) =>
|
|
activeStatuses.includes(s.status),
|
|
);
|
|
const recentSessions = sessions
|
|
.filter((s) => recentStatuses.includes(s.status))
|
|
.slice(0, maxRecent);
|
|
|
|
if (!showGrouping) {
|
|
return (
|
|
<div className="sessions-grid">
|
|
{sessions.length === 0 ? (
|
|
<p className="muted">{emptyMessage}</p>
|
|
) : (
|
|
sessions.map((session) => (
|
|
<SessionCard
|
|
key={session.id}
|
|
session={session}
|
|
onOpen={onOpen}
|
|
onStart={onStart}
|
|
onStop={onStop}
|
|
onDelete={onDelete}
|
|
onRecreateTunnel={onRecreateTunnel}
|
|
onRename={onRename}
|
|
isBusy={actionBusyId === session.id}
|
|
tunnelHealth={tunnelHealth[session.id] || null}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="session-list">
|
|
{/* Active Sessions */}
|
|
<div className="session-group">
|
|
<div className="session-group-header">
|
|
<h3>{activeTitle}</h3>
|
|
{activeSessions.length > 0 && (
|
|
<span className="badge">{activeSessions.length}</span>
|
|
)}
|
|
</div>
|
|
{activeSessions.length === 0 ? (
|
|
<p className="muted">No active sessions</p>
|
|
) : (
|
|
<div className="sessions-grid">
|
|
{activeSessions.map((session) => (
|
|
<SessionCard
|
|
key={session.id}
|
|
session={session}
|
|
onOpen={onOpen}
|
|
onStart={onStart}
|
|
onStop={onStop}
|
|
onDelete={onDelete}
|
|
onRecreateTunnel={onRecreateTunnel}
|
|
onRename={onRename}
|
|
isBusy={actionBusyId === session.id}
|
|
tunnelHealth={tunnelHealth[session.id] || null}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Recent Sessions */}
|
|
{recentSessions.length > 0 && (
|
|
<div className="session-group">
|
|
<div className="session-group-header">
|
|
<h3>{recentTitle}</h3>
|
|
<span className="badge">{recentSessions.length}</span>
|
|
</div>
|
|
<div className="sessions-grid">
|
|
{recentSessions.map((session) => (
|
|
<SessionCard
|
|
key={session.id}
|
|
session={session}
|
|
onOpen={onOpen}
|
|
onStart={onStart}
|
|
onStop={onStop}
|
|
onDelete={onDelete}
|
|
onRecreateTunnel={onRecreateTunnel}
|
|
onRename={onRename}
|
|
isBusy={actionBusyId === session.id}
|
|
tunnelHealth={tunnelHealth[session.id] || null}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|