From 250e9b7f02b47b2e18da5483afc43d257729dc59 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 28 Jul 2022 20:03:01 -0700 Subject: [PATCH 01/10] Adds SharedDirectoryMoveResponse plumbing --- packages/teleport/src/lib/tdp/client.ts | 5 +++++ packages/teleport/src/lib/tdp/codec.ts | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/teleport/src/lib/tdp/client.ts b/packages/teleport/src/lib/tdp/client.ts index f8cf89c1e..220aeadec 100644 --- a/packages/teleport/src/lib/tdp/client.ts +++ b/packages/teleport/src/lib/tdp/client.ts @@ -29,6 +29,7 @@ import Codec, { SharedDirectoryErrCode, SharedDirectoryInfoResponse, SharedDirectoryListResponse, + SharedDirectoryMoveResponse, SharedDirectoryReadResponse, SharedDirectoryWriteResponse, FileSystemObject, @@ -424,6 +425,10 @@ export default class Client extends EventEmitterWebAuthnSender { this.send(this.codec.encodeSharedDirectoryListResponse(res)); } + sendSharedDirectoryMoveResponse(res: SharedDirectoryMoveResponse) { + this.send(this.codec.encodeSharedDirectoryMoveResponse(res)); + } + sendSharedDirectoryReadResponse(response: SharedDirectoryReadResponse) { this.send(this.codec.encodeSharedDirectoryReadResponse(response)); } diff --git a/packages/teleport/src/lib/tdp/codec.ts b/packages/teleport/src/lib/tdp/codec.ts index 293608dd5..842884ba0 100644 --- a/packages/teleport/src/lib/tdp/codec.ts +++ b/packages/teleport/src/lib/tdp/codec.ts @@ -46,6 +46,7 @@ export enum MessageType { SHARED_DIRECTORY_WRITE_REQUEST = 21, SHARED_DIRECTORY_WRITE_RESPONSE = 22, SHARED_DIRECTORY_MOVE_REQUEST = 23, + SHARED_DIRECTORY_MOVE_RESPONSE = 24, SHARED_DIRECTORY_LIST_REQUEST = 25, SHARED_DIRECTORY_LIST_RESPONSE = 26, __LAST, // utility value @@ -168,6 +169,12 @@ export type SharedDirectoryMoveRequest = { newPath: string; }; +// | message type (24) | completion_id uint32 | err_code uint32 | +export type SharedDirectoryMoveResponse = { + completionId: number; + errCode: SharedDirectoryErrCode; +}; + // | message type (25) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | export type SharedDirectoryListRequest = { completionId: number; @@ -589,6 +596,7 @@ export default class Codec { return buffer; } + // | message type (22) | completion_id uint32 | err_code uint32 | bytes_written uint32 | encodeSharedDirectoryWriteResponse( res: SharedDirectoryWriteResponse ): Message { @@ -609,6 +617,23 @@ export default class Codec { return buffer; } + // | message type (24) | completion_id uint32 | err_code uint32 | + encodeSharedDirectoryMoveResponse(res: SharedDirectoryMoveResponse): Message { + const bufLen = byteLength + 2 * uint32Length; + const buffer = new ArrayBuffer(bufLen); + const view = new DataView(buffer); + let offset = 0; + + view.setUint8(offset, MessageType.SHARED_DIRECTORY_MOVE_RESPONSE); + offset += byteLength; + view.setUint32(offset, res.completionId); + offset += uint32Length; + view.setUint32(offset, res.errCode); + offset += uint32Length; + + return buffer; + } + // | message type (26) | completion_id uint32 | err_code uint32 | fso_list_length uint32 | fso_list fso[] | encodeSharedDirectoryListResponse(res: SharedDirectoryListResponse): Message { const bufLenSansFsoList = byteLength + 3 * uint32Length; From 53510cc105b11b276848996a5882fb119f7dbaac Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Mon, 1 Aug 2022 10:15:23 -0700 Subject: [PATCH 02/10] Adds a move method which won't actually work until that feature is added to browsers https://github.com/whatwg/fs/pull/10 --- .../src/lib/tdp/sharedDirectoryManager.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts b/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts index 45620b1bb..be1e21349 100644 --- a/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts +++ b/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts @@ -66,7 +66,7 @@ export class SharedDirectoryManager { /** * Gets the FileOrDirInfo for all the children of the directory at path. * @throws Will throw an error if a directory has not already been initialized via add(). - * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory + * @throws {PathDoesNotExistError} if the path isn't a valid path in the shared directory */ async listContents(path: string): Promise { this.checkReady(); @@ -119,7 +119,7 @@ export class SharedDirectoryManager { /** * Writes the bytes in writeData to the file at path starting at offset. * @throws Will throw an error if a directory has not already been initialized via add(). - * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory + * @throws {PathDoesNotExistError} if the path isn't a valid path in the shared directory */ async writeFile( path: string, @@ -143,6 +143,29 @@ export class SharedDirectoryManager { return writeData.length; } + /** + * Moves the file or directory at originalPath to newPath. It's designed to work similar to + * the Linux mv utility with no options: https://linux.die.net/man/1/mv. + * @throws Will throw an error if a directory has not already been initialized via add(). + * @throws {PathDoesNotExistError} if the path isn't a valid path in the shared directory + */ + async move(originalPath: string, newPath: string): Promise { + // See https://web.dev/file-system-access/#renaming-and-moving-files-and-folders + this.checkReady(); + + const originalFileOrDir = await this.walkPath(originalPath); + + let split = newPath.split('/'); + const newName = split.pop(); + const newDirPath = split.join('/'); + const newDir = await this.walkPath(newDirPath); + if (newDir.kind !== 'directory') { + throw new Error('cannot move a file into another file'); + } + + await originalFileOrDir.move(newDir, newName); + } + /** * walkPath walks a pathstr (assumed to be in the qualified Unix format specified * in the TDP spec), returning the FileSystemDirectoryHandle | FileSystemFileHandle From 5b9d7dd1400795c9929de19d2d8e7699b27f4ff6 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Mon, 1 Aug 2022 10:16:47 -0700 Subject: [PATCH 03/10] Revert "Adds a move method which won't actually work until that feature is added to browsers https://github.com/whatwg/fs/pull/10" This reverts commit 53510cc105b11b276848996a5882fb119f7dbaac. --- .../src/lib/tdp/sharedDirectoryManager.ts | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts b/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts index be1e21349..45620b1bb 100644 --- a/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts +++ b/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts @@ -66,7 +66,7 @@ export class SharedDirectoryManager { /** * Gets the FileOrDirInfo for all the children of the directory at path. * @throws Will throw an error if a directory has not already been initialized via add(). - * @throws {PathDoesNotExistError} if the path isn't a valid path in the shared directory + * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory */ async listContents(path: string): Promise { this.checkReady(); @@ -119,7 +119,7 @@ export class SharedDirectoryManager { /** * Writes the bytes in writeData to the file at path starting at offset. * @throws Will throw an error if a directory has not already been initialized via add(). - * @throws {PathDoesNotExistError} if the path isn't a valid path in the shared directory + * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory */ async writeFile( path: string, @@ -143,29 +143,6 @@ export class SharedDirectoryManager { return writeData.length; } - /** - * Moves the file or directory at originalPath to newPath. It's designed to work similar to - * the Linux mv utility with no options: https://linux.die.net/man/1/mv. - * @throws Will throw an error if a directory has not already been initialized via add(). - * @throws {PathDoesNotExistError} if the path isn't a valid path in the shared directory - */ - async move(originalPath: string, newPath: string): Promise { - // See https://web.dev/file-system-access/#renaming-and-moving-files-and-folders - this.checkReady(); - - const originalFileOrDir = await this.walkPath(originalPath); - - let split = newPath.split('/'); - const newName = split.pop(); - const newDirPath = split.join('/'); - const newDir = await this.walkPath(newDirPath); - if (newDir.kind !== 'directory') { - throw new Error('cannot move a file into another file'); - } - - await originalFileOrDir.move(newDir, newName); - } - /** * walkPath walks a pathstr (assumed to be in the qualified Unix format specified * in the TDP spec), returning the FileSystemDirectoryHandle | FileSystemFileHandle From a3a4d546779c0e8a461830183656916ae24234d9 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 2 Aug 2022 13:28:53 -0700 Subject: [PATCH 04/10] Makes all errors in a DesktopSession pop up as a dialog, allowing some errors to be dismissible --- .../DesktopSession.story.test.tsx | 40 +- .../DesktopSession/DesktopSession.story.tsx | 11 + .../src/DesktopSession/DesktopSession.tsx | 138 +- .../DesktopSession.story.test.tsx.snap | 1425 ++++++++++------- .../src/DesktopSession/useDesktopSession.tsx | 5 +- .../src/DesktopSession/useTdpClientCanvas.tsx | 10 +- .../teleport/src/Player/DesktopPlayer.tsx | 3 +- .../TdpClientCanvas/TdpClientCanvas.tsx | 4 +- packages/teleport/src/lib/tdp/client.ts | 89 +- 9 files changed, 1064 insertions(+), 661 deletions(-) diff --git a/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx b/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx index 57f27b33f..7ef0cf1e6 100644 --- a/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx +++ b/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx @@ -11,44 +11,50 @@ import { ClipboardError, UnintendedDisconnect, WebAuthnPrompt, + DismissibleError, } from './DesktopSession.story'; -test('connected settings false', () => { +test('connected settings false', async () => { const { container } = render(); expect(container).toMatchSnapshot(); }); -test('connected settings true', () => { +test('connected settings true', async () => { const { container } = render(); expect(container).toMatchSnapshot(); }); -test('disconnected', () => { +test('disconnected', async () => { const { container } = render(); expect(container).toMatchSnapshot(); }); -test('fetch error', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); +test('fetch error', async () => { + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); }); -test('connection error', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); +test('connection error', async () => { + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); }); -test('clipboard error', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); +test('clipboard error', async () => { + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); }); -test('unintended disconnect', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); +test('unintended disconnect', async () => { + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); +}); + +test('dismissible error', async () => { + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); }); test('webauthn prompt', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); }); diff --git a/packages/teleport/src/DesktopSession/DesktopSession.story.tsx b/packages/teleport/src/DesktopSession/DesktopSession.story.tsx index 59dc91eac..d35b2f44c 100644 --- a/packages/teleport/src/DesktopSession/DesktopSession.story.tsx +++ b/packages/teleport/src/DesktopSession/DesktopSession.story.tsx @@ -71,6 +71,7 @@ const props: State = { onContextMenu: () => false, onMouseEnter: () => {}, onClipboardData: () => {}, + setTdpConnection: () => {}, windowOnFocus: () => {}, webauthn: { errorText: '', @@ -196,6 +197,16 @@ export const ClipboardError = () => ( /> ); +export const DismissibleError = () => ( + +); + export const UnintendedDisconnect = () => ( { + // 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'; + } + + const open = errorText !== ''; + const fatal = tdpConnection.status !== ''; + + setErrorDialog({ open, text: errorText, fatal }); + }, [clipboardState, fetchAttempt, tdpConnection, wsConnection, disconnected]); + + if (errorDialog.open) { return ( - + ({ width: '484px' })} + onClose={onDialogClose} + open={errorDialog.open} + > + + {errorDialog.fatal && Fatal Error} + {!errorDialog.fatal && ( + Dismiss to Continue + )} + + + {errorDialog.fatal && } + {!errorDialog.fatal && ( + + )} + + + + {!errorDialog.fatal && ( + + Dismiss + + )} + {errorDialog.fatal && ( + { + window.location.reload(); + }} + > + Retry + + )} + + ); } @@ -145,7 +226,7 @@ function Session(props: PropsWithChildren) { const showCanvas = fetchAttempt.status === 'success' && - tdpConnection.status === 'success' && + (tdpConnection.status === 'success' || tdpConnection.status === '') && wsConnection === 'open' && !disconnected && clipboardSuccess; @@ -228,8 +309,3 @@ function Session(props: PropsWithChildren) { ); } - -const DesktopSessionAlert = styled(Alert)` - align-self: center; - min-width: 450px; -`; diff --git a/packages/teleport/src/DesktopSession/__snapshots__/DesktopSession.story.test.tsx.snap b/packages/teleport/src/DesktopSession/__snapshots__/DesktopSession.story.test.tsx.snap index c6191c76c..6e31b5fca 100644 --- a/packages/teleport/src/DesktopSession/__snapshots__/DesktopSession.story.test.tsx.snap +++ b/packages/teleport/src/DesktopSession/__snapshots__/DesktopSession.story.test.tsx.snap @@ -1,194 +1,198 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`clipboard error 1`] = ` -.c6 { - display: inline-block; - transition: color 0.3s; - padding-right: 16px; +.c7 { + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + box-sizing: border-box; + box-shadow: 0 1px 4px rgba(0,0,0,0.24); + margin: 0 0 24px 0; + min-height: 40px; + padding: 8px 16px; + overflow: auto; + word-break: break-word; + line-height: 1.5; + margin-top: 8px; + margin-bottom: 8px; + background: #f50057; color: #FFFFFF; } -.c10 { - display: inline-block; - transition: color 0.3s; +.c7 a { color: #FFFFFF; } .c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; align-items: center; + box-sizing: border-box; border: none; + border-radius: 4px; cursor: pointer; - display: flex; + font-family: inherit; + font-weight: 600; outline: none; - border-radius: 50%; - overflow: visible; - justify-content: center; + position: relative; text-align: center; - flex: 0 0 auto; - background: transparent; - color: inherit; + text-decoration: none; + text-transform: uppercase; transition: all 0.3s; -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 40px; font-size: 12px; - height: 24px; - width: 24px; - margin-left: 24px; - color: rgba(255,255,255,0.56); + padding: 0px 40px; + width: 30%; } -.c9 .c5 { - color: inherit; +.c9:active { + opacity: 0.56; } -.c9:disabled { - color: rgba(255,255,255,0.3); +.c9:hover, +.c9:focus { + background: #2C3A73; } .c9:disabled { + background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.3); } -.c9:hover, -.c9:focus { - background: rgba(255,255,255,0.1); -} - -.c2 { +.c5 { overflow: hidden; text-overflow: ellipsis; + font-weight: 300; + font-size: 22px; + line-height: 32px; + text-transform: uppercase; margin: 0px; - padding-left: 16px; - padding-right: 16px; + color: rgba(255,255,255,0.87); +} + +.c1 { + z-index: -1; + position: fixed; + right: 0; + bottom: 0; + top: 0; + left: 0; + background-color: rgba(0,0,0,0.5); + opacity: 1; + touch-action: none; } .c0 { - box-sizing: border-box; - display: flex; - flex-direction: column; + position: fixed; + z-index: 1200; + right: 0; + bottom: 0; + top: 0; + left: 0; } -.c1 { - box-sizing: border-box; - height: 40px; - background-color: #000; - flex: 0 0 auto; +.c2 { + height: 100%; + outline: none; + color: black; display: flex; align-items: center; - flex-direction: row; + justify-content: center; + opacity: 1; + will-change: opacity; + transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms; } .c3 { - box-sizing: border-box; - padding-left: 16px; - padding-right: 16px; + padding: 32px; + padding-top: 24px; + background: #1C254D; + color: rgba(255,255,255,0.87); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.24); display: flex; + flex-direction: column; + position: relative; + overflow-y: auto; + max-height: calc(100% - 96px); + width: 484px; } .c4 { box-sizing: border-box; + margin-bottom: 16px; + min-height: 32px; display: flex; align-items: center; } -.c8 { - font-weight: 600; - font-size: 18px; - align-self: 'center'; -} - -.c7 { - font-weight: 600; - font-size: 22px; - align-self: 'center'; -} - -.c11 { - display: flex; - align-items: center; - justify-content: center; - border-radius: 2px; +.c6 { box-sizing: border-box; - box-shadow: 0 1px 4px rgba(0,0,0,0.24); - margin: 0 0 24px 0; - min-height: 40px; - padding: 8px 16px; - overflow: auto; - word-break: break-word; - line-height: 1.5; - margin-left: 72px; - margin-right: 72px; - margin-top: 8px; - margin-bottom: 8px; - background: #f50057; - color: #FFFFFF; - align-self: center; - min-width: 450px; + margin-bottom: 32px; + flex: 1; + display: flex; + flex-direction: column; } -.c11 a { - color: #FFFFFF; +.c8 { + box-sizing: border-box; } -
+
`; @@ -347,7 +351,8 @@ exports[`connected settings false 1`] = `
@@ -507,201 +512,206 @@ exports[`connected settings true 1`] = ` `; exports[`connection error 1`] = ` -.c6 { - display: inline-block; - transition: color 0.3s; - padding-right: 16px; +.c7 { + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + box-sizing: border-box; + box-shadow: 0 1px 4px rgba(0,0,0,0.24); + margin: 0 0 24px 0; + min-height: 40px; + padding: 8px 16px; + overflow: auto; + word-break: break-word; + line-height: 1.5; + margin-top: 8px; + margin-bottom: 8px; + background: #f50057; color: #FFFFFF; } -.c10 { - display: inline-block; - transition: color 0.3s; +.c7 a { color: #FFFFFF; } .c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; align-items: center; + box-sizing: border-box; border: none; + border-radius: 4px; cursor: pointer; - display: flex; + font-family: inherit; + font-weight: 600; outline: none; - border-radius: 50%; - overflow: visible; - justify-content: center; + position: relative; text-align: center; - flex: 0 0 auto; - background: transparent; - color: inherit; + text-decoration: none; + text-transform: uppercase; transition: all 0.3s; -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 40px; font-size: 12px; - height: 24px; - width: 24px; - margin-left: 24px; - color: rgba(255,255,255,0.56); + padding: 0px 40px; + width: 30%; } -.c9 .c5 { - color: inherit; +.c9:active { + opacity: 0.56; } -.c9:disabled { - color: rgba(255,255,255,0.3); +.c9:hover, +.c9:focus { + background: #2C3A73; } .c9:disabled { + background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.3); } -.c9:hover, -.c9:focus { - background: rgba(255,255,255,0.1); -} - -.c2 { +.c5 { overflow: hidden; text-overflow: ellipsis; + font-weight: 300; + font-size: 22px; + line-height: 32px; + text-transform: uppercase; margin: 0px; - padding-left: 16px; - padding-right: 16px; + color: rgba(255,255,255,0.87); +} + +.c1 { + z-index: -1; + position: fixed; + right: 0; + bottom: 0; + top: 0; + left: 0; + background-color: rgba(0,0,0,0.5); + opacity: 1; + touch-action: none; } .c0 { - box-sizing: border-box; - display: flex; - flex-direction: column; + position: fixed; + z-index: 1200; + right: 0; + bottom: 0; + top: 0; + left: 0; } -.c1 { - box-sizing: border-box; - height: 40px; - background-color: #000; - flex: 0 0 auto; +.c2 { + height: 100%; + outline: none; + color: black; display: flex; align-items: center; - flex-direction: row; + justify-content: center; + opacity: 1; + will-change: opacity; + transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms; } .c3 { - box-sizing: border-box; - padding-left: 16px; - padding-right: 16px; + padding: 32px; + padding-top: 24px; + background: #1C254D; + color: rgba(255,255,255,0.87); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.24); display: flex; + flex-direction: column; + position: relative; + overflow-y: auto; + max-height: calc(100% - 96px); + width: 484px; } .c4 { box-sizing: border-box; + margin-bottom: 16px; + min-height: 32px; display: flex; align-items: center; } -.c8 { - font-weight: 600; - font-size: 18px; - align-self: 'center'; +.c6 { + box-sizing: border-box; + margin-bottom: 32px; + flex: 1; + display: flex; + flex-direction: column; } -.c7 { - font-weight: 600; - font-size: 22px; - align-self: 'center'; +.c8 { + box-sizing: border-box; } -.c11 { - display: flex; - align-items: center; - justify-content: center; - border-radius: 2px; - box-sizing: border-box; - box-shadow: 0 1px 4px rgba(0,0,0,0.24); - margin: 0 0 24px 0; - min-height: 40px; - padding: 8px 16px; - overflow: auto; - word-break: break-word; - line-height: 1.5; - margin-left: 72px; - margin-right: 72px; - margin-top: 8px; - margin-bottom: 8px; - background: #f50057; - color: #FFFFFF; - align-self: center; - min-width: 450px; -} - -.c11 a { - color: #FFFFFF; -} - -
+
`; @@ -881,308 +891,409 @@ exports[`disconnected 1`] = `
`; -exports[`fetch error 1`] = ` -.c6 { - display: inline-block; - transition: color 0.3s; - padding-right: 16px; +exports[`dismissible error 1`] = ` +.c7 { + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + box-sizing: border-box; + box-shadow: 0 1px 4px rgba(0,0,0,0.24); + margin: 0 0 24px 0; + min-height: 40px; + padding: 8px 16px; + overflow: auto; + word-break: break-word; + line-height: 1.5; + margin-top: 8px; + margin-bottom: 8px; + background: #ff9100; color: #FFFFFF; } -.c10 { - display: inline-block; - transition: color 0.3s; +.c7 a { color: #FFFFFF; } .c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; align-items: center; + box-sizing: border-box; border: none; + border-radius: 4px; cursor: pointer; - display: flex; + font-family: inherit; + font-weight: 600; outline: none; - border-radius: 50%; - overflow: visible; - justify-content: center; + position: relative; text-align: center; - flex: 0 0 auto; - background: transparent; - color: inherit; + text-decoration: none; + text-transform: uppercase; transition: all 0.3s; -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 40px; font-size: 12px; - height: 24px; - width: 24px; - margin-left: 24px; - color: rgba(255,255,255,0.56); + padding: 0px 40px; + width: 30%; } -.c9 .c5 { - color: inherit; +.c9:active { + opacity: 0.56; } -.c9:disabled { - color: rgba(255,255,255,0.3); +.c9:hover, +.c9:focus { + background: #2C3A73; } .c9:disabled { + background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.3); } -.c9:hover, -.c9:focus { - background: rgba(255,255,255,0.1); -} - -.c2 { +.c5 { overflow: hidden; text-overflow: ellipsis; + font-weight: 300; + font-size: 22px; + line-height: 32px; + text-transform: uppercase; margin: 0px; - padding-left: 16px; - padding-right: 16px; + color: rgba(255,255,255,0.87); +} + +.c1 { + z-index: -1; + position: fixed; + right: 0; + bottom: 0; + top: 0; + left: 0; + background-color: rgba(0,0,0,0.5); + opacity: 1; + touch-action: none; } .c0 { - box-sizing: border-box; - display: flex; - flex-direction: column; + position: fixed; + z-index: 1200; + right: 0; + bottom: 0; + top: 0; + left: 0; } -.c1 { - box-sizing: border-box; - height: 40px; - background-color: #000; - flex: 0 0 auto; +.c2 { + height: 100%; + outline: none; + color: black; display: flex; align-items: center; - flex-direction: row; + justify-content: center; + opacity: 1; + will-change: opacity; + transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms; } .c3 { - box-sizing: border-box; - padding-left: 16px; - padding-right: 16px; + padding: 32px; + padding-top: 24px; + background: #1C254D; + color: rgba(255,255,255,0.87); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.24); display: flex; + flex-direction: column; + position: relative; + overflow-y: auto; + max-height: calc(100% - 96px); + width: 484px; } .c4 { box-sizing: border-box; + margin-bottom: 16px; + min-height: 32px; display: flex; align-items: center; } -.c8 { - font-weight: 600; - font-size: 18px; - align-self: 'center'; -} - -.c7 { - font-weight: 600; - font-size: 22px; - align-self: 'center'; -} - -.c11 { - display: flex; - align-items: center; - justify-content: center; - border-radius: 2px; +.c6 { box-sizing: border-box; - box-shadow: 0 1px 4px rgba(0,0,0,0.24); - margin: 0 0 24px 0; - min-height: 40px; - padding: 8px 16px; - overflow: auto; - word-break: break-word; - line-height: 1.5; - margin-left: 72px; - margin-right: 72px; - margin-top: 8px; - margin-bottom: 8px; - background: #f50057; - color: #FFFFFF; - align-self: center; - min-width: 450px; + margin-bottom: 32px; + flex: 1; + display: flex; + flex-direction: column; } -.c11 a { - color: #FFFFFF; +.c8 { + box-sizing: border-box; } -
+
`; -exports[`unintended disconnect 1`] = ` -.c6 { - display: inline-block; - transition: color 0.3s; - padding-right: 16px; +exports[`fetch error 1`] = ` +.c7 { + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + box-sizing: border-box; + box-shadow: 0 1px 4px rgba(0,0,0,0.24); + margin: 0 0 24px 0; + min-height: 40px; + padding: 8px 16px; + overflow: auto; + word-break: break-word; + line-height: 1.5; + margin-top: 8px; + margin-bottom: 8px; + background: #f50057; color: #FFFFFF; } -.c10 { - display: inline-block; - transition: color 0.3s; +.c7 a { color: #FFFFFF; } .c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; align-items: center; + box-sizing: border-box; border: none; + border-radius: 4px; cursor: pointer; - display: flex; + font-family: inherit; + font-weight: 600; outline: none; - border-radius: 50%; - overflow: visible; - justify-content: center; + position: relative; text-align: center; - flex: 0 0 auto; - background: transparent; - color: inherit; + text-decoration: none; + text-transform: uppercase; transition: all 0.3s; -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 40px; font-size: 12px; - height: 24px; - width: 24px; - margin-left: 24px; - color: rgba(255,255,255,0.56); + padding: 0px 40px; + width: 30%; } -.c9 .c5 { - color: inherit; +.c9:active { + opacity: 0.56; } -.c9:disabled { - color: rgba(255,255,255,0.3); +.c9:hover, +.c9:focus { + background: #2C3A73; } .c9:disabled { + background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.3); } -.c9:hover, -.c9:focus { - background: rgba(255,255,255,0.1); -} - -.c2 { +.c5 { overflow: hidden; text-overflow: ellipsis; + font-weight: 300; + font-size: 22px; + line-height: 32px; + text-transform: uppercase; margin: 0px; - padding-left: 16px; - padding-right: 16px; + color: rgba(255,255,255,0.87); +} + +.c1 { + z-index: -1; + position: fixed; + right: 0; + bottom: 0; + top: 0; + left: 0; + background-color: rgba(0,0,0,0.5); + opacity: 1; + touch-action: none; } .c0 { - box-sizing: border-box; - display: flex; - flex-direction: column; + position: fixed; + z-index: 1200; + right: 0; + bottom: 0; + top: 0; + left: 0; } -.c1 { - box-sizing: border-box; - height: 40px; - background-color: #000; - flex: 0 0 auto; +.c2 { + height: 100%; + outline: none; + color: black; display: flex; align-items: center; - flex-direction: row; + justify-content: center; + opacity: 1; + will-change: opacity; + transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms; } .c3 { - box-sizing: border-box; - padding-left: 16px; - padding-right: 16px; + padding: 32px; + padding-top: 24px; + background: #1C254D; + color: rgba(255,255,255,0.87); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.24); display: flex; + flex-direction: column; + position: relative; + overflow-y: auto; + max-height: calc(100% - 96px); + width: 484px; } .c4 { box-sizing: border-box; + margin-bottom: 16px; + min-height: 32px; display: flex; align-items: center; } -.c8 { - font-weight: 600; - font-size: 18px; - align-self: 'center'; +.c6 { + box-sizing: border-box; + margin-bottom: 32px; + flex: 1; + display: flex; + flex-direction: column; } -.c7 { - font-weight: 600; - font-size: 22px; - align-self: 'center'; +.c8 { + box-sizing: border-box; } -.c11 { +
+ +`; + +exports[`unintended disconnect 1`] = ` +.c7 { display: flex; align-items: center; justify-content: center; @@ -1195,249 +1306,419 @@ exports[`unintended disconnect 1`] = ` overflow: auto; word-break: break-word; line-height: 1.5; - margin-left: 72px; - margin-right: 72px; margin-top: 8px; margin-bottom: 8px; background: #f50057; color: #FFFFFF; - align-self: center; - min-width: 450px; } -.c11 a { +.c7 a { color: #FFFFFF; } -
+.c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 40px; + font-size: 12px; + padding: 0px 40px; + width: 30%; +} + +.c9:active { + opacity: 0.56; +} + +.c9:hover, +.c9:focus { + background: #2C3A73; +} + +.c9:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); +} + +.c5 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 300; + font-size: 22px; + line-height: 32px; + text-transform: uppercase; + margin: 0px; + color: rgba(255,255,255,0.87); +} + +.c1 { + z-index: -1; + position: fixed; + right: 0; + bottom: 0; + top: 0; + left: 0; + background-color: rgba(0,0,0,0.5); + opacity: 1; + touch-action: none; +} + +.c0 { + position: fixed; + z-index: 1200; + right: 0; + bottom: 0; + top: 0; + left: 0; +} + +.c2 { + height: 100%; + outline: none; + color: black; + display: flex; + align-items: center; + justify-content: center; + opacity: 1; + will-change: opacity; + transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms; +} + +.c3 { + padding: 32px; + padding-top: 24px; + background: #1C254D; + color: rgba(255,255,255,0.87); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.24); + display: flex; + flex-direction: column; + position: relative; + overflow-y: auto; + max-height: calc(100% - 96px); + width: 484px; +} + +.c4 { + box-sizing: border-box; + margin-bottom: 16px; + min-height: 32px; + display: flex; + align-items: center; +} + +.c6 { + box-sizing: border-box; + margin-bottom: 32px; + flex: 1; + display: flex; + flex-direction: column; +} + +.c8 { + box-sizing: border-box; +} + +
`; exports[`webauthn prompt 1`] = ` -.c11 { +.c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; box-sizing: border-box; - margin: 72px; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + background: #512FC9; + color: rgba(255,255,255,0.87); + min-height: 32px; + font-size: 12px; + padding: 0px 24px; + margin-right: 16px; + width: 130px; } -.c6 { - display: inline-block; - transition: color 0.3s; - padding-right: 16px; - color: #FFFFFF; +.c9:active { + opacity: 0.56; } -.c10 { - display: inline-block; - transition: color 0.3s; - color: #FFFFFF; +.c9:hover, +.c9:focus { + background: #651FFF; } -.c9 { +.c9:active { + background: #354AA4; +} + +.c9:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); +} + +.c10 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; align-items: center; + box-sizing: border-box; border: none; + border-radius: 4px; cursor: pointer; - display: flex; + font-family: inherit; + font-weight: 600; outline: none; - border-radius: 50%; - overflow: visible; - justify-content: center; + position: relative; text-align: center; - flex: 0 0 auto; - background: transparent; - color: inherit; + text-decoration: none; + text-transform: uppercase; transition: all 0.3s; -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 32px; font-size: 12px; - height: 24px; - width: 24px; - margin-left: 24px; - color: rgba(255,255,255,0.56); + padding: 0px 24px; } -.c9 .c5 { - color: inherit; +.c10:active { + opacity: 0.56; } -.c9:disabled { - color: rgba(255,255,255,0.3); +.c10:hover, +.c10:focus { + background: #2C3A73; } -.c9:disabled { +.c10:disabled { + background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.3); } -.c9:hover, -.c9:focus { - background: rgba(255,255,255,0.1); +.c5 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 300; + font-size: 22px; + line-height: 32px; + text-transform: uppercase; + margin: 0px; + color: rgba(255,255,255,0.87); + text-align: center; } -.c2 { +.c7 { overflow: hidden; text-overflow: ellipsis; margin: 0px; - padding-left: 16px; - padding-right: 16px; + text-align: center; +} + +.c1 { + z-index: -1; + position: fixed; + right: 0; + bottom: 0; + top: 0; + left: 0; + background-color: rgba(0,0,0,0.5); + opacity: 1; + touch-action: none; } .c0 { - box-sizing: border-box; - display: flex; - flex-direction: column; + position: fixed; + z-index: 1200; + right: 0; + bottom: 0; + top: 0; + left: 0; } -.c1 { - box-sizing: border-box; - height: 40px; - background-color: #000; - flex: 0 0 auto; +.c2 { + height: 100%; + outline: none; + color: black; display: flex; align-items: center; - flex-direction: row; + justify-content: center; + opacity: 1; + will-change: opacity; + transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms; } .c3 { - box-sizing: border-box; - padding-left: 16px; - padding-right: 16px; + padding: 32px; + padding-top: 24px; + background: #1C254D; + color: rgba(255,255,255,0.87); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.24); display: flex; + flex-direction: column; + position: relative; + overflow-y: auto; + max-height: calc(100% - 96px); + width: 400px; } .c4 { box-sizing: border-box; + margin-bottom: 16px; + min-height: 32px; display: flex; align-items: center; } -.c8 { - font-weight: 600; - font-size: 18px; - align-self: 'center'; +.c6 { + box-sizing: border-box; + margin-bottom: 40px; + flex: 1; + display: flex; + flex-direction: column; } -.c7 { - font-weight: 600; - font-size: 22px; - align-self: 'center'; +.c8 { + box-sizing: border-box; + text-align: center; } -
+
`; diff --git a/packages/teleport/src/DesktopSession/useDesktopSession.tsx b/packages/teleport/src/DesktopSession/useDesktopSession.tsx index c7eca99cc..3119decce 100644 --- a/packages/teleport/src/DesktopSession/useDesktopSession.tsx +++ b/packages/teleport/src/DesktopSession/useDesktopSession.tsx @@ -30,10 +30,10 @@ export default function useDesktopSession() { const { attempt: fetchAttempt, run } = useAttempt('processing'); // tdpConnection tracks the state of the tdpClient's TDP connection - // tdpConnection.status === // - 'processing' at first // - 'success' once the first TdpClientEvent.IMAGE_FRAGMENT is seen - // - 'failed' if a TdpClientEvent.TDP_ERROR is encountered + // - 'failed' if a fatal error is encountered + // - '' if a non-fatal error is encountered const { attempt: tdpConnection, setAttempt: setTdpConnection } = useAttempt('processing'); @@ -162,6 +162,7 @@ export default function useDesktopSession() { disconnected, setDisconnected, webauthn, + setTdpConnection, ...clientCanvasProps, }; } diff --git a/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx b/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx index 92fa4a431..d6f8194ec 100644 --- a/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx +++ b/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx @@ -79,14 +79,18 @@ export default function useTdpClientCanvas(props: Props) { } }; - // Default TdpClientEvent.TDP_ERROR handler - const onTdpError = (err: Error) => { + // Default TdpClientEvent.TDP_ERROR and TdpClientEvent.CLIENT_ERROR handler + const onTdpError = (error: { err: Error; isFatal: boolean }) => { + const { err, isFatal } = error; setIsSharingDirectory(false); setClipboardState(prevState => ({ ...prevState, enabled: false, })); - setTdpConnection({ status: 'failed', statusText: err.message }); + setTdpConnection({ + status: isFatal ? 'failed' : '', + statusText: err.message, + }); }; const onWsClose = () => { diff --git a/packages/teleport/src/Player/DesktopPlayer.tsx b/packages/teleport/src/Player/DesktopPlayer.tsx index 34c8a8a5c..cc39320d2 100644 --- a/packages/teleport/src/Player/DesktopPlayer.tsx +++ b/packages/teleport/src/Player/DesktopPlayer.tsx @@ -171,7 +171,8 @@ const useDesktopPlayer = ({ }); }; - const tdpCliOnTdpError = (err: Error) => { + const tdpCliOnTdpError = (error: { err: Error; isFatal: boolean }) => { + const { err } = error; setAttempt({ status: 'failed', statusText: err.message, diff --git a/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx b/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx index 460cd9ad1..ffae41ec4 100644 --- a/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx +++ b/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx @@ -129,9 +129,11 @@ export default function TdpClientCanvas(props: Props) { useEffect(() => { if (tdpCli && tdpCliOnTdpError) { tdpCli.on(TdpClientEvent.TDP_ERROR, tdpCliOnTdpError); + tdpCli.on(TdpClientEvent.CLIENT_ERROR, tdpCliOnTdpError); return () => { tdpCli.removeListener(TdpClientEvent.TDP_ERROR, tdpCliOnTdpError); + tdpCli.removeListener(TdpClientEvent.CLIENT_ERROR, tdpCliOnTdpError); }; } }, [tdpCli, tdpCliOnTdpError]); @@ -292,7 +294,7 @@ export type Props = { pngFrame: PngFrame ) => void; tdpCliOnClipboardData?: (clipboardData: ClipboardData) => void; - tdpCliOnTdpError?: (err: Error) => void; + tdpCliOnTdpError?: (error: { err: Error; isFatal: boolean }) => void; tdpCliOnWsClose?: () => void; tdpCliOnWsOpen?: () => void; tdpCliOnClientScreenSpec?: ( diff --git a/packages/teleport/src/lib/tdp/client.ts b/packages/teleport/src/lib/tdp/client.ts index 220aeadec..89994af38 100644 --- a/packages/teleport/src/lib/tdp/client.ts +++ b/packages/teleport/src/lib/tdp/client.ts @@ -44,7 +44,10 @@ export enum TdpClientEvent { TDP_CLIENT_SCREEN_SPEC = 'tdp client screen spec', TDP_PNG_FRAME = 'tdp png frame', TDP_CLIPBOARD_DATA = 'tdp clipboard data', + // TDP_ERROR corresponds with https://github.com/gravitational/teleport/blob/86e824fc7879538e4de400eb1518e4f88930c109/rfd/0037-desktop-access-protocol.md?plain=1#L200-L206 TDP_ERROR = 'tdp error', + // CLIENT_ERROR represents an error event in the client that isn't a TDP_ERROR + CLIENT_ERROR = 'client error', WS_OPEN = 'ws open', WS_CLOSE = 'ws close', } @@ -119,7 +122,10 @@ export default class Client extends EventEmitterWebAuthnSender { this.handleClipboardData(buffer); break; case MessageType.ERROR: - this.handleError(new Error(this.codec.decodeErrorMessage(buffer))); + this.handleError( + new Error(this.codec.decodeErrorMessage(buffer)), + TdpClientEvent.TDP_ERROR + ); break; case MessageType.MFA_JSON: this.handleMfaChallenge(buffer); @@ -146,7 +152,7 @@ export default class Client extends EventEmitterWebAuthnSender { this.logger.warn(`received unsupported message type ${messageType}`); } } catch (err) { - this.handleError(err); + this.handleError(err, TdpClientEvent.CLIENT_ERROR); } } @@ -189,9 +195,6 @@ export default class Client extends EventEmitterWebAuthnSender { ); } - // TODO(isaiah): neither of the TdpClientEvent.TDP_ERROR are accurate, they should - // instead be associated with a new event TdpClientEvent.CLIENT_ERROR. - // https://github.com/gravitational/webapps/issues/615 handleMfaChallenge(buffer: ArrayBuffer) { try { const mfaJson = this.codec.decodeMfaJson(buffer); @@ -205,11 +208,12 @@ export default class Client extends EventEmitterWebAuthnSender { however the U2F API for hardware keys is not supported for desktop sessions. \ Please notify your system administrator to update cluster settings \ to use WebAuthn as the second factor protocol.' - ) + ), + TdpClientEvent.CLIENT_ERROR ); } } catch (err) { - this.handleError(err); + this.handleError(err, TdpClientEvent.CLIENT_ERROR); } } @@ -219,7 +223,8 @@ export default class Client extends EventEmitterWebAuthnSender { } this.handleError( - new Error(`Encountered shared directory error: ${errCode}`) + new Error(`Encountered shared directory error: ${errCode}`), + TdpClientEvent.CLIENT_ERROR ); return false; } @@ -235,7 +240,7 @@ export default class Client extends EventEmitterWebAuthnSender { 'Started sharing directory: ' + this.sdManager.getName() ); } catch (e) { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } } @@ -262,7 +267,7 @@ export default class Client extends EventEmitterWebAuthnSender { }, }); } else { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } } } @@ -282,7 +287,7 @@ export default class Client extends EventEmitterWebAuthnSender { readData, }); } catch (e) { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } } @@ -301,16 +306,25 @@ export default class Client extends EventEmitterWebAuthnSender { bytesWritten, }); } catch (e) { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } } handleSharedDirectoryMoveRequest(buffer: ArrayBuffer) { const req = this.codec.decodeSharedDirectoryMoveRequest(buffer); - // TODO(isaiah): delete debug logs - this.logger.debug('Received SharedDirectoryMoveRequest:'); - this.logger.debug(req); - // TODO(isaiah): here's where we'll respond with a SharedDirectoryMoveResponse + this.sendSharedDirectoryMoveResponse({ + completionId: req.completionId, + errCode: SharedDirectoryErrCode.Failed, + }); + this.handleError( + new Error( + 'Moving the files and directories within a shared \ + directory on the Windows side is not supported. \ + Try moving the files on your local machine instead.' + ), + TdpClientEvent.CLIENT_ERROR, + false + ); } async handleSharedDirectoryListRequest(buffer: ArrayBuffer) { @@ -329,7 +343,7 @@ export default class Client extends EventEmitterWebAuthnSender { fsoList, }); } catch (e) { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } } @@ -349,12 +363,15 @@ export default class Client extends EventEmitterWebAuthnSender { try { this.socket.send(data); } catch (e) { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } return; } - this.handleError(new Error('websocket unavailable')); + this.handleError( + new Error('websocket unavailable'), + TdpClientEvent.CLIENT_ERROR + ); } sendUsername(username: string) { @@ -395,7 +412,7 @@ export default class Client extends EventEmitterWebAuthnSender { try { this.sdManager.add(sharedDirectory); } catch (err) { - this.handleError(err); + this.handleError(err, TdpClientEvent.CLIENT_ERROR); } } @@ -403,18 +420,18 @@ export default class Client extends EventEmitterWebAuthnSender { let name: string; try { name = this.sdManager.getName(); + this.send( + this.codec.encodeSharedDirectoryAnnounce({ + completionId: 0, // This is always the first request. + // Hardcode directoryId for now since we only support sharing 1 directory. + // We're using 2 because the smartcard device is hardcoded to 1 in the backend. + directoryId: 2, + name, + }) + ); } catch (e) { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } - this.send( - this.codec.encodeSharedDirectoryAnnounce({ - completionId: 0, // This is always the first request. - // Hardcode directoryId for now since we only support sharing 1 directory. - // We're using 2 because the smartcard device is hardcoded to 1 in the backend. - directoryId: 2, - name, - }) - ); } sendSharedDirectoryInfoResponse(res: SharedDirectoryInfoResponse) { @@ -441,12 +458,16 @@ export default class Client extends EventEmitterWebAuthnSender { this.send(this.codec.encodeClientScreenSpec(spec)); } - // Emits an TdpClientEvent.ERROR event. Sets this.errored to true to alert the socket.onclose handler that + // Emits an TdpClientEvent.TDP_ERROR event. Sets this.errored to true to alert the socket.onclose handler that // it needn't emit a generic unknown error event. - private handleError(err: Error) { + private handleError( + err: Error, + errType: TdpClientEvent.TDP_ERROR | TdpClientEvent.CLIENT_ERROR, + isFatal = true + ) { this.logger.error(err); - this.emit(TdpClientEvent.TDP_ERROR, err); - this.socket?.close(); + this.emit(errType, { err, isFatal }); + if (isFatal) this.socket?.close(); } // Ensures full cleanup of this object. From 6718389c8fe9497bdae8c4b6151e268e09499100 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 2 Aug 2022 13:54:46 -0700 Subject: [PATCH 05/10] removes unnecessary async --- .../DesktopSession/DesktopSession.story.test.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx b/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx index 7ef0cf1e6..d2dfe060e 100644 --- a/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx +++ b/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx @@ -14,42 +14,42 @@ import { DismissibleError, } from './DesktopSession.story'; -test('connected settings false', async () => { +test('connected settings false', () => { const { container } = render(); expect(container).toMatchSnapshot(); }); -test('connected settings true', async () => { +test('connected settings true', () => { const { container } = render(); expect(container).toMatchSnapshot(); }); -test('disconnected', async () => { +test('disconnected', () => { const { container } = render(); expect(container).toMatchSnapshot(); }); -test('fetch error', async () => { +test('fetch error', () => { const { getByTestId } = render(); expect(getByTestId('Modal')).toMatchSnapshot(); }); -test('connection error', async () => { +test('connection error', () => { const { getByTestId } = render(); expect(getByTestId('Modal')).toMatchSnapshot(); }); -test('clipboard error', async () => { +test('clipboard error', () => { const { getByTestId } = render(); expect(getByTestId('Modal')).toMatchSnapshot(); }); -test('unintended disconnect', async () => { +test('unintended disconnect', () => { const { getByTestId } = render(); expect(getByTestId('Modal')).toMatchSnapshot(); }); -test('dismissible error', async () => { +test('dismissible error', () => { const { getByTestId } = render(); expect(getByTestId('Modal')).toMatchSnapshot(); }); From 71502da8e89aede6edd2402a7b821d15224677a9 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 2 Aug 2022 13:57:51 -0700 Subject: [PATCH 06/10] updates deprecated handleError description --- packages/teleport/src/lib/tdp/client.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/teleport/src/lib/tdp/client.ts b/packages/teleport/src/lib/tdp/client.ts index 89994af38..d46245a2d 100644 --- a/packages/teleport/src/lib/tdp/client.ts +++ b/packages/teleport/src/lib/tdp/client.ts @@ -458,8 +458,7 @@ export default class Client extends EventEmitterWebAuthnSender { this.send(this.codec.encodeClientScreenSpec(spec)); } - // Emits an TdpClientEvent.TDP_ERROR event. Sets this.errored to true to alert the socket.onclose handler that - // it needn't emit a generic unknown error event. + // Emits an errType event, closing the socket if the error was fatal. private handleError( err: Error, errType: TdpClientEvent.TDP_ERROR | TdpClientEvent.CLIENT_ERROR, From cc3e2a1ee5124781d8e175a8ef46110e00f02879 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 2 Aug 2022 13:59:54 -0700 Subject: [PATCH 07/10] Adds clarifying comment in handleSharedDirectoryMoveRequest --- packages/teleport/src/lib/tdp/client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/teleport/src/lib/tdp/client.ts b/packages/teleport/src/lib/tdp/client.ts index d46245a2d..5f6b9d4e0 100644 --- a/packages/teleport/src/lib/tdp/client.ts +++ b/packages/teleport/src/lib/tdp/client.ts @@ -312,6 +312,7 @@ export default class Client extends EventEmitterWebAuthnSender { handleSharedDirectoryMoveRequest(buffer: ArrayBuffer) { const req = this.codec.decodeSharedDirectoryMoveRequest(buffer); + // Always send back Failed for now, see https://github.com/gravitational/webapps/issues/1064 this.sendSharedDirectoryMoveResponse({ completionId: req.completionId, errCode: SharedDirectoryErrCode.Failed, From 5849b9bf06b76a69b90026874be1fe0345f12f10 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 4 Aug 2022 12:12:21 -0700 Subject: [PATCH 08/10] Changing copy --- .../src/DesktopSession/DesktopSession.tsx | 21 ++++++++++++++----- packages/teleport/src/lib/tdp/client.ts | 5 ++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/teleport/src/DesktopSession/DesktopSession.tsx b/packages/teleport/src/DesktopSession/DesktopSession.tsx index 4c78b5aba..7013f7bb4 100644 --- a/packages/teleport/src/DesktopSession/DesktopSession.tsx +++ b/packages/teleport/src/DesktopSession/DesktopSession.tsx @@ -108,7 +108,7 @@ export function DesktopSession(props: State) { } else if (clipboardError) { errorText = clipboardState.errorText || 'clipboard sharing failed'; } else if (unknownConnectionError) { - errorText = 'Session disconnected for an unknown reason'; + errorText = 'Session disconnected for an unknown reason.'; } const open = errorText !== ''; @@ -128,16 +128,27 @@ export function DesktopSession(props: State) { {errorDialog.fatal && Fatal Error} {!errorDialog.fatal && ( - Dismiss to Continue + Unsupported Action )} - {errorDialog.fatal && } + {errorDialog.fatal && ( + + {errorDialog.text} +
+
+ Refresh the page to try again. +
+ } + /> + )} {!errorDialog.fatal && ( )} - {!errorDialog.fatal && ( @@ -152,7 +163,7 @@ export function DesktopSession(props: State) { window.location.reload(); }} > - Retry + Refresh )} diff --git a/packages/teleport/src/lib/tdp/client.ts b/packages/teleport/src/lib/tdp/client.ts index 5f6b9d4e0..b67a791a2 100644 --- a/packages/teleport/src/lib/tdp/client.ts +++ b/packages/teleport/src/lib/tdp/client.ts @@ -319,9 +319,8 @@ export default class Client extends EventEmitterWebAuthnSender { }); this.handleError( new Error( - 'Moving the files and directories within a shared \ - directory on the Windows side is not supported. \ - Try moving the files on your local machine instead.' + 'Moving files and directories within a shared \ + directory is not supported.' ), TdpClientEvent.CLIENT_ERROR, false From b56814854b057082cc3dd3440f4f861f90059b9a Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 4 Aug 2022 12:26:12 -0700 Subject: [PATCH 09/10] slight update to copy and removes unnecessary react.useState --- .../src/DesktopSession/DesktopSession.tsx | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/packages/teleport/src/DesktopSession/DesktopSession.tsx b/packages/teleport/src/DesktopSession/DesktopSession.tsx index 7013f7bb4..38a331c8c 100644 --- a/packages/teleport/src/DesktopSession/DesktopSession.tsx +++ b/packages/teleport/src/DesktopSession/DesktopSession.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { PropsWithChildren, useEffect, useState } from 'react'; +import React, { PropsWithChildren } from 'react'; import { Indicator, Box, Text, Flex, ButtonSecondary } from 'design'; import { Danger, Warning } from 'design/Alert'; import Dialog, { @@ -59,13 +59,6 @@ export function DesktopSession(props: State) { tdpConnection.status === 'processing' || clipboardProcessing; - // Manages the state of the error dialog. - const [errorDialog, setErrorDialog] = useState({ - open: false, - text: '', - fatal: false, - }); - // onDialogClose is called when a user // dismisses a non-fatal error dialog. const onDialogClose = () => { @@ -86,9 +79,7 @@ export function DesktopSession(props: State) { }); }; - // Calculate the state of the error dialog based on the various - // sub-states which determine it. - useEffect(() => { + const computeErrorDialog = () => { const clipboardError = clipboardState.enabled && clipboardState.errorText; // Websocket is closed but we haven't @@ -110,12 +101,13 @@ export function DesktopSession(props: State) { } else if (unknownConnectionError) { errorText = 'Session disconnected for an unknown reason.'; } - const open = errorText !== ''; const fatal = tdpConnection.status !== ''; - setErrorDialog({ open, text: errorText, fatal }); - }, [clipboardState, fetchAttempt, tdpConnection, wsConnection, disconnected]); + return { open, text: errorText, fatal }; + }; + + const errorDialog = computeErrorDialog(); if (errorDialog.open) { return ( @@ -133,18 +125,12 @@ export function DesktopSession(props: State) { {errorDialog.fatal && ( - - {errorDialog.text} -
-
- Refresh the page to try again. -
- } - /> + <> + {errorDialog.text}} /> + Refresh the page to try again. + )} + {!errorDialog.fatal && ( )} From ad9acbfa2af38d7cfba03b6a717a5e727d4cb8c4 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 4 Aug 2022 12:30:45 -0700 Subject: [PATCH 10/10] Updates snapshots --- .../DesktopSession.story.test.tsx.snap | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/teleport/src/DesktopSession/__snapshots__/DesktopSession.story.test.tsx.snap b/packages/teleport/src/DesktopSession/__snapshots__/DesktopSession.story.test.tsx.snap index 6e31b5fca..b3da4a124 100644 --- a/packages/teleport/src/DesktopSession/__snapshots__/DesktopSession.story.test.tsx.snap +++ b/packages/teleport/src/DesktopSession/__snapshots__/DesktopSession.story.test.tsx.snap @@ -14,8 +14,6 @@ exports[`clipboard error 1`] = ` overflow: auto; word-break: break-word; line-height: 1.5; - margin-top: 8px; - margin-bottom: 8px; background: #f50057; color: #FFFFFF; } @@ -180,6 +178,7 @@ exports[`clipboard error 1`] = ` > clipboard error
+ Refresh the page to try again.
- Retry + Refresh
@@ -351,8 +350,7 @@ exports[`connected settings false 1`] = `
@@ -512,8 +510,7 @@ exports[`connected settings true 1`] = ` @@ -533,8 +530,6 @@ exports[`connection error 1`] = ` overflow: auto; word-break: break-word; line-height: 1.5; - margin-top: 8px; - margin-bottom: 8px; background: #f50057; color: #FFFFFF; } @@ -699,6 +694,7 @@ exports[`connection error 1`] = ` > some connection error + Refresh the page to try again.
- Retry + Refresh
@@ -891,8 +887,7 @@ exports[`disconnected 1`] = ` @@ -1066,7 +1061,7 @@ exports[`dismissible error 1`] = ` class="c5" color="text.primary" > - Dismiss to Continue + Unsupported Action
some fetch error
+ Refresh the page to try again.
- Retry + Refresh
@@ -1306,8 +1300,6 @@ exports[`unintended disconnect 1`] = ` overflow: auto; word-break: break-word; line-height: 1.5; - margin-top: 8px; - margin-bottom: 8px; background: #f50057; color: #FFFFFF; } @@ -1470,8 +1462,9 @@ exports[`unintended disconnect 1`] = ` class="c7" kind="danger" > - Session disconnected for an unknown reason + Session disconnected for an unknown reason. + Refresh the page to try again.
- Retry + Refresh