mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
feat(connectionlog): show session timeline in detail dialog
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user