549d13f469
- Add SessionCard component with status indicators, actions, and confirmation dialogs - Add SessionList component with grouping (active/recent) and filtering - Refactor dashboard.tsx to use unified components - Refactor sessions.tsx to use unified components - Remove duplicated session rendering logic from both pages Refs: session-list-overhaul tasks 1-4
126 lines
3.7 KiB
TypeScript
126 lines
3.7 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;
|
|
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,
|
|
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}
|
|
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}
|
|
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}
|
|
isBusy={actionBusyId === session.id}
|
|
tunnelHealth={tunnelHealth[session.id] || null}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|