Skip to content

Commit 417b053

Browse files
feat: implement terminal reconnection UI components (Phase 2)
- Create TerminalRetryConnection component with countdown display and retry button - Add comprehensive Storybook stories covering all retry states - Integrate component with TerminalAlerts for proper positioning - Use consistent TerminalAlert styling for seamless integration - Ensure proper resize handling through existing MutationObserver Implements Phase 2 of terminal reconnection feature as outlined in: coder/internal#659 Co-authored-by: BrunoQuaresma <3165839+BrunoQuaresma@users.noreply.github.com>
1 parent dd7adda commit 417b053

File tree

3 files changed

+231
-1
lines changed

3 files changed

+231
-1
lines changed

site/src/pages/TerminalPage/TerminalAlerts.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,29 @@ import { Button } from "components/Button/Button";
55
import { type FC, useEffect, useRef, useState } from "react";
66
import { docs } from "utils/docs";
77
import type { ConnectionStatus } from "./types";
8+
import { TerminalRetryConnection } from "./TerminalRetryConnection";
89

910
type TerminalAlertsProps = {
1011
agent: WorkspaceAgent | undefined;
1112
status: ConnectionStatus;
1213
onAlertChange: () => void;
14+
// Retry connection props
15+
isRetrying?: boolean;
16+
timeUntilNextRetry?: number | null;
17+
attemptCount?: number;
18+
maxAttempts?: number;
19+
onRetryNow?: () => void;
1320
};
1421

1522
export const TerminalAlerts = ({
1623
agent,
1724
status,
1825
onAlertChange,
26+
isRetrying = false,
27+
timeUntilNextRetry = null,
28+
attemptCount = 0,
29+
maxAttempts = 10,
30+
onRetryNow,
1931
}: TerminalAlertsProps) => {
2032
const lifecycleState = agent?.lifecycle_state;
2133
const prevLifecycleState = useRef(lifecycleState);
@@ -49,7 +61,13 @@ export const TerminalAlerts = ({
4961
return (
5062
<div ref={wrapperRef}>
5163
{status === "disconnected" ? (
52-
<DisconnectedAlert />
64+
<TerminalRetryConnection
65+
isRetrying={isRetrying}
66+
timeUntilNextRetry={timeUntilNextRetry}
67+
attemptCount={attemptCount}
68+
maxAttempts={maxAttempts}
69+
onRetryNow={onRetryNow || (() => {})}
70+
/>
5371
) : lifecycleState === "start_error" ? (
5472
<ErrorScriptAlert />
5573
) : lifecycleState === "starting" ? (
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { TerminalRetryConnection } from "./TerminalRetryConnection";
3+
4+
const meta: Meta<typeof TerminalRetryConnection> = {
5+
title: "pages/TerminalPage/TerminalRetryConnection",
6+
component: TerminalRetryConnection,
7+
parameters: {
8+
layout: "padded",
9+
},
10+
args: {
11+
onRetryNow: () => {
12+
console.log("Retry now clicked");
13+
},
14+
maxAttempts: 10,
15+
},
16+
};
17+
18+
export default meta;
19+
type Story = StoryObj<typeof TerminalRetryConnection>;
20+
21+
// Hidden state - component returns null
22+
export const Hidden: Story = {
23+
args: {
24+
isRetrying: false,
25+
timeUntilNextRetry: null,
26+
attemptCount: 0,
27+
},
28+
};
29+
30+
// Currently retrying state
31+
export const Retrying: Story = {
32+
args: {
33+
isRetrying: true,
34+
timeUntilNextRetry: null,
35+
attemptCount: 1,
36+
},
37+
};
38+
39+
// Countdown to next retry - first attempt (1 second)
40+
export const CountdownFirstAttempt: Story = {
41+
args: {
42+
isRetrying: false,
43+
timeUntilNextRetry: 1000, // 1 second
44+
attemptCount: 1,
45+
},
46+
};
47+
48+
// Countdown to next retry - second attempt (2 seconds)
49+
export const CountdownSecondAttempt: Story = {
50+
args: {
51+
isRetrying: false,
52+
timeUntilNextRetry: 2000, // 2 seconds
53+
attemptCount: 2,
54+
},
55+
};
56+
57+
// Countdown to next retry - longer delay (15 seconds)
58+
export const CountdownLongerDelay: Story = {
59+
args: {
60+
isRetrying: false,
61+
timeUntilNextRetry: 15000, // 15 seconds
62+
attemptCount: 5,
63+
},
64+
};
65+
66+
// Countdown with 1 second remaining (singular)
67+
export const CountdownOneSecond: Story = {
68+
args: {
69+
isRetrying: false,
70+
timeUntilNextRetry: 1000, // 1 second
71+
attemptCount: 3,
72+
},
73+
};
74+
75+
// Countdown with less than 1 second remaining
76+
export const CountdownLessThanOneSecond: Story = {
77+
args: {
78+
isRetrying: false,
79+
timeUntilNextRetry: 500, // 0.5 seconds (should show "1 second")
80+
attemptCount: 3,
81+
},
82+
};
83+
84+
// Max attempts reached - no more automatic retries
85+
export const MaxAttemptsReached: Story = {
86+
args: {
87+
isRetrying: false,
88+
timeUntilNextRetry: null,
89+
attemptCount: 10,
90+
maxAttempts: 10,
91+
},
92+
};
93+
94+
// Connection lost but no retry scheduled yet
95+
export const ConnectionLostNoRetry: Story = {
96+
args: {
97+
isRetrying: false,
98+
timeUntilNextRetry: null,
99+
attemptCount: 1,
100+
maxAttempts: 10,
101+
},
102+
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Alert, type AlertProps } from "components/Alert/Alert";
2+
import { Button } from "components/Button/Button";
3+
import { Spinner } from "components/Spinner/Spinner";
4+
import type { FC } from "react";
5+
6+
interface TerminalRetryConnectionProps {
7+
/**
8+
* Whether a retry is currently in progress
9+
*/
10+
isRetrying: boolean;
11+
/**
12+
* Time in milliseconds until the next automatic retry (null if not scheduled)
13+
*/
14+
timeUntilNextRetry: number | null;
15+
/**
16+
* Number of retry attempts made
17+
*/
18+
attemptCount: number;
19+
/**
20+
* Maximum number of retry attempts
21+
*/
22+
maxAttempts: number;
23+
/**
24+
* Callback to manually trigger a retry
25+
*/
26+
onRetryNow: () => void;
27+
}
28+
29+
/**
30+
* Formats milliseconds into a human-readable countdown
31+
*/
32+
function formatCountdown(ms: number): string {
33+
const seconds = Math.ceil(ms / 1000);
34+
return `${seconds} second${seconds !== 1 ? "s" : ""}`;
35+
}
36+
37+
/**
38+
* Terminal-specific alert component with consistent styling
39+
*/
40+
const TerminalAlert: FC<AlertProps> = (props) => {
41+
return (
42+
<Alert
43+
{...props}
44+
css={(theme) => ({
45+
borderRadius: 0,
46+
borderWidth: 0,
47+
borderBottomWidth: 1,
48+
borderBottomColor: theme.palette.divider,
49+
backgroundColor: theme.palette.background.paper,
50+
borderLeft: `3px solid ${theme.palette[props.severity!].light}`,
51+
marginBottom: 1,
52+
})}
53+
/>
54+
);
55+
};
56+
57+
export const TerminalRetryConnection: FC<TerminalRetryConnectionProps> = ({
58+
isRetrying,
59+
timeUntilNextRetry,
60+
attemptCount,
61+
maxAttempts,
62+
onRetryNow,
63+
}) => {
64+
// Don't show anything if we're not in a retry state
65+
if (!isRetrying && timeUntilNextRetry === null) {
66+
return null;
67+
}
68+
69+
// Show different messages based on state
70+
let message: string;
71+
let showRetryButton = true;
72+
73+
if (isRetrying) {
74+
message = "Reconnecting to terminal...";
75+
showRetryButton = false; // Don't show button while actively retrying
76+
} else if (timeUntilNextRetry !== null) {
77+
const countdown = formatCountdown(timeUntilNextRetry);
78+
message = `Connection lost. Retrying in ${countdown}...`;
79+
} else if (attemptCount >= maxAttempts) {
80+
message = "Connection failed after multiple attempts.";
81+
} else {
82+
message = "Connection lost.";
83+
}
84+
85+
return (
86+
<TerminalAlert
87+
severity="warning"
88+
actions={
89+
showRetryButton ? (
90+
<Button
91+
variant="outline"
92+
size="sm"
93+
onClick={onRetryNow}
94+
disabled={isRetrying}
95+
css={{
96+
display: "flex",
97+
alignItems: "center",
98+
gap: "0.5rem",
99+
}}
100+
>
101+
{isRetrying && <Spinner size="sm" />}
102+
Retry now
103+
</Button>
104+
) : null
105+
}
106+
>
107+
{message}
108+
</TerminalAlert>
109+
);
110+
};

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy