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
133 changes: 103 additions & 30 deletions packages/teleport/src/DesktopSession/DesktopSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@ limitations under the License.
*/

import React, { PropsWithChildren } from 'react';
import styled from 'styled-components';
import { Indicator, Box, Alert, Text, Flex } from 'design';
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,114 @@ 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';
}
// 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;
});
};

const computeErrorDialog = () => {
const clipboardError = clipboardState.enabled && clipboardState.errorText;

if (alertText) {
// 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.';
}
const open = errorText !== '';
const fatal = tdpConnection.status !== '';

return { open, text: errorText, fatal };
};

const errorDialog = computeErrorDialog();

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>Unsupported Action</DialogTitle>
)}
</DialogHeader>
<DialogContent>
{errorDialog.fatal && (
<>
<Danger children={<>{errorDialog.text}</>} />
Refresh the page to try again.
</>
)}

{!errorDialog.fatal && (
<Warning my={2} children={errorDialog.text} />
)}
</DialogContent>
<DialogFooter>
{!errorDialog.fatal && (
<ButtonSecondary size="large" width="30%" onClick={onDialogClose}>
Dismiss
</ButtonSecondary>
)}
{errorDialog.fatal && (
<ButtonSecondary
size="large"
width="30%"
onClick={() => {
window.location.reload();
}}
>
Refresh
</ButtonSecondary>
)}
</DialogFooter>
</Dialog>
</Session>
);
}
Expand Down Expand Up @@ -145,7 +223,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 +306,3 @@ function Session(props: PropsWithChildren<State>) {
</Flex>
);
}

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