diff --git a/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx b/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx index 57f27b33f..d2dfe060e 100644 --- a/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx +++ b/packages/teleport/src/DesktopSession/DesktopSession.story.test.tsx @@ -11,6 +11,7 @@ import { ClipboardError, UnintendedDisconnect, WebAuthnPrompt, + DismissibleError, } from './DesktopSession.story'; test('connected settings false', () => { @@ -29,26 +30,31 @@ test('disconnected', () => { }); test('fetch error', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); }); test('connection error', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); }); test('clipboard error', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); }); test('unintended disconnect', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); + const { getByTestId } = render(); + expect(getByTestId('Modal')).toMatchSnapshot(); +}); + +test('dismissible error', () => { + 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; + }); + }; + + 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 ( - + ({ width: '484px' })} + onClose={onDialogClose} + open={errorDialog.open} + > + + {errorDialog.fatal && Fatal Error} + {!errorDialog.fatal && ( + Unsupported Action + )} + + + {errorDialog.fatal && ( + <> + {errorDialog.text}} /> + Refresh the page to try again. + + )} + + {!errorDialog.fatal && ( + + )} + + + {!errorDialog.fatal && ( + + Dismiss + + )} + {errorDialog.fatal && ( + { + window.location.reload(); + }} + > + Refresh + + )} + + ); } @@ -145,7 +223,7 @@ function Session(props: PropsWithChildren) { const showCanvas = fetchAttempt.status === 'success' && - tdpConnection.status === 'success' && + (tdpConnection.status === 'success' || tdpConnection.status === '') && wsConnection === 'open' && !disconnected && clipboardSuccess; @@ -228,8 +306,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..b3da4a124 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,197 @@ // 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; + 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; } -
+
`; @@ -514,194 +517,197 @@ 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; + 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; -} - -
+
`; @@ -887,302 +893,401 @@ 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; + 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 +1300,418 @@ 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 f8cf89c1e..b67a791a2 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, @@ -43,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', } @@ -118,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); @@ -145,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); } } @@ -188,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); @@ -204,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); } } @@ -218,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; } @@ -234,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); } } @@ -261,7 +267,7 @@ export default class Client extends EventEmitterWebAuthnSender { }, }); } else { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } } } @@ -281,7 +287,7 @@ export default class Client extends EventEmitterWebAuthnSender { readData, }); } catch (e) { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } } @@ -300,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 + // Always send back Failed for now, see https://github.com/gravitational/webapps/issues/1064 + this.sendSharedDirectoryMoveResponse({ + completionId: req.completionId, + errCode: SharedDirectoryErrCode.Failed, + }); + this.handleError( + new Error( + 'Moving files and directories within a shared \ + directory is not supported.' + ), + TdpClientEvent.CLIENT_ERROR, + false + ); } async handleSharedDirectoryListRequest(buffer: ArrayBuffer) { @@ -328,7 +343,7 @@ export default class Client extends EventEmitterWebAuthnSender { fsoList, }); } catch (e) { - this.handleError(e); + this.handleError(e, TdpClientEvent.CLIENT_ERROR); } } @@ -348,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) { @@ -394,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); } } @@ -402,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) { @@ -424,6 +442,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)); } @@ -436,12 +458,15 @@ 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 - // it needn't emit a generic unknown error event. - private handleError(err: Error) { + // Emits an errType event, closing the socket if the error was fatal. + 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. 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;