Skip to content
This repository was archived by the owner on Feb 8, 2024. It is now read-only.
Merged
26 changes: 16 additions & 10 deletions packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ClipboardError,
UnintendedDisconnect,
WebAuthnPrompt,
DismissibleError,
} from './DesktopSession.story';

test('connected settings false', () => {
Expand All @@ -29,26 +30,31 @@ test('disconnected', () => {
});

test('fetch error', () => {
const { container } = render(<FetchError />);
expect(container).toMatchSnapshot();
const { getByTestId } = render(<FetchError />);
expect(getByTestId('Modal')).toMatchSnapshot();
});

test('connection error', () => {
const { container } = render(<ConnectionError />);
expect(container).toMatchSnapshot();
const { getByTestId } = render(<ConnectionError />);
expect(getByTestId('Modal')).toMatchSnapshot();
});

test('clipboard error', () => {
const { container } = render(<ClipboardError />);
expect(container).toMatchSnapshot();
const { getByTestId } = render(<ClipboardError />);
expect(getByTestId('Modal')).toMatchSnapshot();
});

test('unintended disconnect', () => {
const { container } = render(<UnintendedDisconnect />);
expect(container).toMatchSnapshot();
const { getByTestId } = render(<UnintendedDisconnect />);
expect(getByTestId('Modal')).toMatchSnapshot();
});

test('dismissible error', () => {
const { getByTestId } = render(<DismissibleError />);
expect(getByTestId('Modal')).toMatchSnapshot();
});

test('webauthn prompt', () => {
const { container } = render(<WebAuthnPrompt />);
expect(container).toMatchSnapshot();
const { getByTestId } = render(<WebAuthnPrompt />);
expect(getByTestId('Modal')).toMatchSnapshot();
});
11 changes: 11 additions & 0 deletions packages/teleport/src/DesktopSession/DesktopSession.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const props: State = {
onContextMenu: () => false,
onMouseEnter: () => {},
onClipboardData: () => {},
setTdpConnection: () => {},
windowOnFocus: () => {},
webauthn: {
errorText: '',
Expand Down Expand Up @@ -196,6 +197,16 @@ export const ClipboardError = () => (
/>
);

export const DismissibleError = () => (
<DesktopSession
{...props}
fetchAttempt={{ status: 'success' }}
tdpConnection={{ status: '', statusText: 'dismissible error' }}
wsConnection={'open'}
disconnected={false}
/>
);

export const UnintendedDisconnect = () => (
<DesktopSession
{...props}
Expand Down
138 changes: 107 additions & 31 deletions packages/teleport/src/DesktopSession/DesktopSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { PropsWithChildren } from 'react';
import styled from 'styled-components';
import { Indicator, Box, Alert, Text, Flex } from 'design';
import React, { PropsWithChildren, useEffect, useState } from 'react';
import { Indicator, Box, Text, Flex, ButtonSecondary } from 'design';
import { Danger, Warning } from 'design/Alert';
import Dialog, {
DialogHeader,
DialogTitle,
DialogContent,
DialogFooter,
} from 'design/Dialog';

import TdpClientCanvas from 'teleport/components/TdpClientCanvas';
import AuthnDialog from 'teleport/components/AuthnDialog';
Expand All @@ -40,42 +46,117 @@ export function DesktopSession(props: State) {
clipboardState,
fetchAttempt,
tdpConnection,
wsConnection,
disconnected,
wsConnection,
setTdpConnection,
} = props;

const clipboardError = clipboardState.enabled && clipboardState.errorText;

const clipboardProcessing =
clipboardState.enabled && clipboardState.permission.state === 'prompt';

// Websocket is closed but we haven't
// closed it on purpose or registered a tdp error.
const unknownConnectionError =
wsConnection === 'closed' &&
!disconnected &&
tdpConnection.status === 'success';

const processing =
fetchAttempt.status === 'processing' ||
tdpConnection.status === 'processing' ||
clipboardProcessing;

let alertText: string;
if (fetchAttempt.status === 'failed') {
alertText = fetchAttempt.statusText || 'fetch attempt failed';
} else if (tdpConnection.status === 'failed') {
alertText = tdpConnection.statusText || 'tdp connection failed';
} else if (clipboardError) {
alertText = clipboardState.errorText || 'clipboard sharing failed';
} else if (unknownConnectionError) {
alertText = 'Session disconnected for an unknown reason';
}
// Manages the state of the error dialog.
const [errorDialog, setErrorDialog] = useState({
Comment thread
ibeckermayer marked this conversation as resolved.
Outdated
open: false,
text: '',
fatal: false,
});

// onDialogClose is called when a user
// dismisses a non-fatal error dialog.
const onDialogClose = () => {
// This setTdpConnection call will cause the useEffect below
// to calculate the errorDialog state.
setTdpConnection(prevState => {
// onDialogClose should only be called when
// the user dismisses a non-fatal error dialog,
// and prevState.status === '' means non-fatal
// error dialog, so the below if statement should
// always be true.
if (prevState.status === '') {
// If prevState.status was a non-fatal error,
// we assume that the TDP connection remains open.
return { status: 'success' };
}
return prevState;
});
};

if (alertText) {
// Calculate the state of the error dialog based on the various
// sub-states which determine it.
useEffect(() => {
const clipboardError = clipboardState.enabled && clipboardState.errorText;

// Websocket is closed but we haven't
// closed it on purpose or registered a fatal tdp error.
const unknownConnectionError =
wsConnection === 'closed' &&
!disconnected &&
(tdpConnection.status === 'success' || tdpConnection.status === '');

let errorText = '';
if (fetchAttempt.status === 'failed') {
errorText = fetchAttempt.statusText || 'fetch attempt failed';
} else if (tdpConnection.status === 'failed') {
errorText = tdpConnection.statusText || 'tdp connection failed';
} else if (tdpConnection.status === '') {
errorText = tdpConnection.statusText || 'encountered a non-fatal error';
} else if (clipboardError) {
errorText = clipboardState.errorText || 'clipboard sharing failed';
} else if (unknownConnectionError) {
errorText = 'Session disconnected for an unknown reason';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
errorText = 'Session disconnected for an unknown reason';
errorText = 'Session disconnected for an unknown reason. Try reloading (or refreshing) the browser.';

and then the button could say Reload or Refresh?

also i'm confused whether our error messages should be all lower cap or properly capitalized with punctuation. I'm just seeing a mixture of both. It's not a big deal to me but just wondering in general

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also i'm confused whether our error messages should be all lower cap or properly capitalized with punctuation.

In my opinion if we're displaying it as a message to the user, we should strive to give it proper capitalization and punctuation.

}

const open = errorText !== '';
const fatal = tdpConnection.status !== '';

setErrorDialog({ open, text: errorText, fatal });
}, [clipboardState, fetchAttempt, tdpConnection, wsConnection, disconnected]);

if (errorDialog.open) {
return (
<Session {...props}>
<DesktopSessionAlert my={2} mx={10} children={alertText} />
<Dialog
dialogCss={() => ({ width: '484px' })}
onClose={onDialogClose}
open={errorDialog.open}
>
<DialogHeader style={{ flexDirection: 'column' }}>
{errorDialog.fatal && <DialogTitle>Fatal Error</DialogTitle>}
{!errorDialog.fatal && (
<DialogTitle>Dismiss to Continue</DialogTitle>
Comment thread
ibeckermayer marked this conversation as resolved.
Outdated
)}
</DialogHeader>
<DialogContent>
{errorDialog.fatal && <Danger my={2} children={errorDialog.text} />}
{!errorDialog.fatal && (
<Warning my={2} children={errorDialog.text} />
)}
</DialogContent>

Comment thread
ibeckermayer marked this conversation as resolved.
Outdated
<DialogFooter>
{!errorDialog.fatal && (
<ButtonSecondary size="large" width="30%" onClick={onDialogClose}>
Dismiss
</ButtonSecondary>
)}
{errorDialog.fatal && (
<ButtonSecondary
size="large"
width="30%"
onClick={() => {
window.location.reload();
}}
>
Retry
Comment thread
ibeckermayer marked this conversation as resolved.
Outdated
</ButtonSecondary>
)}
</DialogFooter>
</Dialog>
</Session>
);
}
Expand Down Expand Up @@ -145,7 +226,7 @@ function Session(props: PropsWithChildren<State>) {

const showCanvas =
fetchAttempt.status === 'success' &&
tdpConnection.status === 'success' &&
(tdpConnection.status === 'success' || tdpConnection.status === '') &&
wsConnection === 'open' &&
!disconnected &&
clipboardSuccess;
Expand Down Expand Up @@ -228,8 +309,3 @@ function Session(props: PropsWithChildren<State>) {
</Flex>
);
}

const DesktopSessionAlert = styled(Alert)`
align-self: center;
min-width: 450px;
`;
Loading