feat(connectionlog): show session timeline in detail dialog

This commit is contained in:
M Atif Ali
2026-02-13 18:38:24 +05:00
parent 20bdaf3248
commit 19b0ce5586
2 changed files with 112 additions and 1 deletions
@@ -1,8 +1,11 @@
import Collapse from "@mui/material/Collapse";
import type {
DiagnosticTimelineEvent,
GlobalWorkspaceSession,
UserDiagnosticResponse,
WorkspaceConnection,
} from "api/typesGenerated";
import { API } from "api/api";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import { TableCell } from "components/Table/Table";
import { TimelineEntry } from "components/Timeline/TimelineEntry";
@@ -14,6 +17,7 @@ import {
} from "modules/resources/ConnectionStatus";
import { ConnectionDetailDialog } from "pages/WorkspaceSessionsPage/ConnectionDetailDialog";
import { type FC, useState } from "react";
import { useQuery } from "react-query";
import { Link } from "react-router";
interface GlobalSessionRowProps {
@@ -25,7 +29,15 @@ export const GlobalSessionRow: FC<GlobalSessionRowProps> = ({ session }) => {
const [selectedConnection, setSelectedConnection] =
useState<WorkspaceConnection | null>(null);
const diagnosticQuery = useQuery<UserDiagnosticResponse>({
queryKey: ["userDiagnostic", session.workspace_owner_username],
queryFn: () => API.getUserDiagnostic(session.workspace_owner_username),
enabled: selectedConnection !== null,
staleTime: 60 * 1000,
});
const hasConnections = session.connections.length > 0;
const selectedTimeline = findSessionTimeline(diagnosticQuery.data, session);
// Client location: hostname or IP (where the connection came from).
const clientLocation = session.client_hostname || session.ip || "Unknown";
@@ -158,11 +170,53 @@ export const GlobalSessionRow: FC<GlobalSessionRowProps> = ({ session }) => {
connection={selectedConnection}
open={selectedConnection !== null}
onClose={() => setSelectedConnection(null)}
timeline={selectedTimeline}
timelineLoading={diagnosticQuery.isLoading}
timelineError={diagnosticQuery.error}
showTimeline={selectedConnection !== null}
/>
</>
);
};
function findSessionTimeline(
diagnostic: UserDiagnosticResponse | undefined,
session: GlobalWorkspaceSession,
): readonly DiagnosticTimelineEvent[] {
if (!diagnostic) {
return [];
}
const workspace =
diagnostic.workspaces.find((w) => w.id === session.workspace_id) ??
diagnostic.workspaces.find((w) => w.name === session.workspace_name);
if (!workspace) {
return [];
}
if (session.id) {
const byID = workspace.sessions.find((s) => s.id === session.id);
if (byID) {
return byID.timeline;
}
}
const byComposite = workspace.sessions.find(
(s) =>
s.started_at === session.started_at &&
s.short_description === (session.short_description ?? "") &&
s.client_hostname === (session.client_hostname ?? ""),
);
if (byComposite) {
return byComposite.timeline;
}
const byStartTime = workspace.sessions.find(
(s) => s.started_at === session.started_at,
);
return byStartTime?.timeline ?? [];
}
function formatTimeRange(startedAt: string, endedAt?: string): string {
const start = new Date(startedAt);
const startStr = start.toLocaleTimeString();
@@ -1,4 +1,4 @@
import type { WorkspaceConnection } from "api/typesGenerated";
import type { DiagnosticTimelineEvent, WorkspaceConnection } from "api/typesGenerated";
import {
Dialog,
DialogContent,
@@ -18,6 +18,10 @@ interface ConnectionDetailDialogProps {
connection: WorkspaceConnection | null;
open: boolean;
onClose: () => void;
timeline?: readonly DiagnosticTimelineEvent[];
timelineLoading?: boolean;
timelineError?: unknown;
showTimeline?: boolean;
}
function formatDuration(startISO: string, endISO: string): string {
@@ -57,6 +61,10 @@ export const ConnectionDetailDialog: FC<ConnectionDetailDialogProps> = ({
connection,
open,
onClose,
timeline = [],
timelineLoading = false,
timelineError,
showTimeline = false,
}) => {
if (!connection) {
return null;
@@ -147,6 +155,55 @@ export const ConnectionDetailDialog: FC<ConnectionDetailDialogProps> = ({
<DetailRow label="Home DERP" value={connection.home_derp.name} />
)}
</div>
{showTimeline && (
<div className="mt-4">
<h3 className="text-sm font-semibold mb-2">Timeline</h3>
{timelineLoading ? (
<p className="text-xs text-content-secondary">
Loading timeline…
</p>
) : timelineError ? (
<p className="text-xs text-content-destructive">
Unable to load timeline for this session.
</p>
) : timeline.length === 0 ? (
<p className="text-xs text-content-secondary italic">
No timeline data for this session.
</p>
) : (
<ol className="max-h-56 overflow-y-auto border border-border rounded p-3 space-y-2">
{timeline.map((event, idx) => (
<li
key={`${event.timestamp}-${idx}`}
className="flex items-start gap-2"
>
<span
className={`mt-1.5 inline-block size-2 rounded-full shrink-0 ${
event.severity === "error"
? "bg-content-destructive"
: event.severity === "warning"
? "bg-content-warning"
: "bg-highlight-sky"
}`}
/>
<span className="text-xs text-content-secondary font-mono shrink-0 w-16">
{new Date(event.timestamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
})}
</span>
<span className="text-xs text-content-primary">
{event.description}
</span>
</li>
))}
</ol>
)}
</div>
)}
</DialogContent>
</Dialog>
);