diff --git a/web/packages/shared/components/DesktopSession/ActionMenu.tsx b/web/packages/shared/components/DesktopSession/ActionMenu.tsx
index 9e8e1a7ae469d..0f4596095f0d8 100644
--- a/web/packages/shared/components/DesktopSession/ActionMenu.tsx
+++ b/web/packages/shared/components/DesktopSession/ActionMenu.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import { Flex } from 'design';
import * as Icons from 'design/Icon';
import { MenuIcon, MenuItem, MenuItemIcon } from 'shared/components/MenuAction';
@@ -25,32 +24,30 @@ export default function ActionMenu(props: Props) {
props;
return (
-
-
- {showShareDirectory && (
-
- )}
-
+ )}
+
+
+ Send Ctrl+Alt+Del
+
+
+
+ Disconnect
+
+
);
}
diff --git a/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx b/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx
index 356ea21a2dd12..df8088ebf9944 100644
--- a/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx
+++ b/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx
@@ -71,6 +71,7 @@ const props: DesktopSessionProps = {
client: fakeClient(),
username: 'user',
desktop: 'windows-11',
+ browserSupportsSharing: true,
hasAnotherSession: () => Promise.resolve(false),
};
diff --git a/web/packages/shared/components/DesktopSession/DesktopSession.test.tsx b/web/packages/shared/components/DesktopSession/DesktopSession.test.tsx
index cf079f8c4fbd3..44ade6977cb7b 100644
--- a/web/packages/shared/components/DesktopSession/DesktopSession.test.tsx
+++ b/web/packages/shared/components/DesktopSession/DesktopSession.test.tsx
@@ -19,11 +19,11 @@
import { EventEmitter } from 'events';
import { screen } from '@testing-library/react';
+import { act } from 'react';
import { render } from 'design/utils/testing';
import { makeSuccessAttempt } from 'shared/hooks/useAsync';
-import { BrowserFileSystem, TdpClient } from 'shared/libs/tdp';
-import { wait } from 'shared/utils/wait';
+import { BrowserFileSystem, MessageType, TdpClient } from 'shared/libs/tdp';
import { DesktopSession } from './DesktopSession';
@@ -36,6 +36,19 @@ import { TdpTransport } from 'shared/libs/tdp/client';
// Disable WASM in tests.
jest.mock('shared/libs/ironrdp/pkg/ironrdp');
+// Matches codec.decodePngFrame.
+function encodePngFrame(): ArrayBuffer {
+ const buffer = new ArrayBuffer(21);
+ const view = new DataView(buffer);
+ view.setUint8(0, MessageType.PNG_FRAME);
+ view.setUint32(1, 0);
+ view.setUint32(5, 0);
+ view.setUint32(9, 0);
+ view.setUint32(13, 0);
+ view.setUint32(17, 0);
+ return buffer;
+}
+
const hasNoOtherSession = jest.fn().mockResolvedValue(false);
const aclAttempt = makeSuccessAttempt({
clipboardSharingEnabled: true,
@@ -46,9 +59,9 @@ const getMockTransport = () => {
return {
emitTransportError: () =>
emitter.emit('error', new Error('Could not send bytes')),
+ emitPngFrameMessage: () => emitter.emit('message', encodePngFrame()),
getTransport: async (abortSignal: AbortSignal): Promise => {
- abortSignal.onabort = async () => {
- await wait(50);
+ abortSignal.onabort = () => {
emitter.emit('complete');
};
return {
@@ -70,6 +83,21 @@ const getMockTransport = () => {
};
};
+let originalQuery: typeof navigator.permissions.query;
+
+beforeEach(() => {
+ originalQuery = navigator.permissions.query;
+
+ navigator.permissions.query = jest.fn().mockResolvedValue({
+ state: 'granted',
+ onchange: null,
+ });
+});
+
+afterEach(() => {
+ navigator.permissions.query = originalQuery;
+});
+
test('reconnect button reinitializes the connection', async () => {
const transport = getMockTransport();
const tpdClient = new TdpClient(
@@ -85,6 +113,7 @@ test('reconnect button reinitializes the connection', async () => {
desktop="win-lab"
aclAttempt={aclAttempt}
hasAnotherSession={hasNoOtherSession}
+ browserSupportsSharing
/>
);
@@ -114,3 +143,40 @@ test('reconnect button reinitializes the connection', async () => {
// Called 2 times: the first one during reconnecting, the second one after unmounting.
expect(tpdClient.shutdown).toHaveBeenCalledTimes(2);
});
+
+test('ensure sharing remains enabled if the initial desktop connection attempt fails', async () => {
+ const transport = getMockTransport();
+ const tpdClient = new TdpClient(
+ transport.getTransport,
+ new BrowserFileSystem()
+ );
+ render(
+
+ );
+
+ // The session is initializing.
+ expect(await screen.findByTestId('indicator')).toBeInTheDocument();
+
+ // An error occurred, the connection has been closed.
+ transport.emitTransportError();
+
+ expect(
+ await screen.findByText('The desktop session is offline.')
+ ).toBeInTheDocument();
+ const reconnect = await screen.findByRole('button', { name: 'Reconnect' });
+
+ await userEvent.click(reconnect);
+ // This time the connection succeeded.
+ await act(() => transport.emitPngFrameMessage());
+
+ expect(await screen.findByTitle('More actions')).toBeVisible();
+ await userEvent.click(screen.getByTitle('More actions'));
+ expect(await screen.findByText('Share Directory')).toBeVisible();
+});
diff --git a/web/packages/shared/components/DesktopSession/DesktopSession.tsx b/web/packages/shared/components/DesktopSession/DesktopSession.tsx
index 0481cb7cc1797..6eaddd101da12 100644
--- a/web/packages/shared/components/DesktopSession/DesktopSession.tsx
+++ b/web/packages/shared/components/DesktopSession/DesktopSession.tsx
@@ -53,8 +53,6 @@ import { KeyboardHandler } from './KeyboardHandler';
import TopBar from './TopBar';
import useDesktopSession, {
clipboardSharingMessage,
- defaultClipboardSharingState,
- defaultDirectorySharingState,
directorySharingPossible,
isSharingClipboard,
isSharingDirectory,
@@ -70,6 +68,8 @@ export interface DesktopSessionProps {
clipboardSharingEnabled: boolean;
directorySharingEnabled: boolean;
}>;
+ /** Determines if the browser client support directory and clipboard sharing. */
+ browserSupportsSharing: boolean;
/**
* Injects a custom component that overrides other connection states.
* Useful for per-session MFA, which differs between Web UI and Connect.
@@ -92,19 +92,19 @@ export function DesktopSession({
hasAnotherSession,
customConnectionState,
keyboardLayout = 0,
+ browserSupportsSharing,
}: DesktopSessionProps) {
const {
directorySharingState,
- setDirectorySharingState,
onClipboardData,
sendLocalClipboardToRemote,
clipboardSharingState,
- setClipboardSharingState,
+ clearSharing,
onShareDirectory,
alerts,
onRemoveAlert,
addAlert,
- } = useDesktopSession(client, aclAttempt);
+ } = useDesktopSession(client, aclAttempt, browserSupportsSharing);
const [tdpConnectionStatus, setTdpConnectionStatus] =
useState({ status: '' });
@@ -145,8 +145,7 @@ export function DesktopSession({
const handleFatalError = useCallback(
(error: Error) => {
- setDirectorySharingState(defaultDirectorySharingState);
- setClipboardSharingState(defaultClipboardSharingState);
+ clearSharing();
setTdpConnectionStatus({
status: 'disconnected',
fromTdpError: error instanceof TdpError,
@@ -154,7 +153,7 @@ export function DesktopSession({
});
initialTdpConnectionSucceeded.current = false;
},
- [setClipboardSharingState, setDirectorySharingState]
+ [clearSharing]
);
useListener(client.onError, handleFatalError);
@@ -362,14 +361,7 @@ export function DesktopSession({
{
- setClipboardSharingState(prevState => ({
- ...prevState,
- isSharing: false,
- }));
- setDirectorySharingState(prevState => ({
- ...prevState,
- isSharing: false,
- }));
+ clearSharing();
client.shutdown();
}}
userHost={`${username} on ${desktop}`}
diff --git a/web/packages/shared/components/DesktopSession/TopBar.tsx b/web/packages/shared/components/DesktopSession/TopBar.tsx
index b40d841f8091a..4ad1a5718fd31 100644
--- a/web/packages/shared/components/DesktopSession/TopBar.tsx
+++ b/web/packages/shared/components/DesktopSession/TopBar.tsx
@@ -54,35 +54,28 @@ export default function TopBar(props: Props) {
-
- {userHost}
-
+ {userHost}
{isConnected && (
-
-
- {latency && }
-
-
-
-
-
-
-
-
+
+ {latency && }
+
+
+
+
+
+
+
+ }>,
+ browserSupportsSharing: boolean
) {
const encoder = useRef(new TextEncoder());
const latestClipboardDigest = useRef('');
- const [directorySharingState, setDirectorySharingState] =
- useState(defaultDirectorySharingState);
-
- const [clipboardSharingState, setClipboardSharingState] =
- useState(defaultClipboardSharingState);
+ const [directorySharingState, setDirectorySharingState] = useState<{
+ directorySelected: boolean;
+ }>({ directorySelected: false });
+
+ const [clipboardSharingState, setClipboardSharingState] = useState<{
+ readState?: PermissionState;
+ writeState?: PermissionState;
+ }>({});
+
+ const clipboardSharing: ClipboardSharingState = {
+ ...clipboardSharingState,
+ browserSupported: browserSupportsSharing,
+ allowedByAcl:
+ aclAttempt.status === 'success' &&
+ aclAttempt.data.clipboardSharingEnabled,
+ };
+ const directorySharing: DirectorySharingState = {
+ ...directorySharingState,
+ browserSupported: browserSupportsSharing,
+ allowedByAcl:
+ aclAttempt.status === 'success' &&
+ aclAttempt.data.directorySharingEnabled,
+ };
useEffect(() => {
const clearReadListenerPromise = initClipboardPermissionTracking(
@@ -68,21 +87,6 @@ export default function useDesktopSession(
};
}, []);
- //TODO(gzdunek): This is workaround for synchronizing *sharingState with aclAttempt.
- //Refactor clipboard and directory sharing so that we won't need allowedByAcl fields in state.
- useEffect(() => {
- if (aclAttempt.status === 'success') {
- setClipboardSharingState(prevState => ({
- ...prevState,
- allowedByAcl: aclAttempt.data.clipboardSharingEnabled,
- }));
- setDirectorySharingState(prevState => ({
- ...prevState,
- allowedByAcl: aclAttempt.data.directorySharingEnabled,
- }));
- }
- }, [aclAttempt]);
-
const [alerts, setAlerts] = useState([]);
const onRemoveAlert = (id: string) => {
setAlerts(prevState => prevState.filter(alert => alert.id !== id));
@@ -95,7 +99,7 @@ export default function useDesktopSession(
}, []);
async function sendLocalClipboardToRemote() {
- if (!(await sysClipboardGuard(clipboardSharingState, 'read'))) {
+ if (!(await sysClipboardGuard(clipboardSharing, 'read'))) {
return;
}
const text = await navigator.clipboard.readText();
@@ -111,7 +115,7 @@ export default function useDesktopSession(
async function onClipboardData(clipboardData: ClipboardData) {
if (
clipboardData.data &&
- (await sysClipboardGuard(clipboardSharingState, 'write'))
+ (await sysClipboardGuard(clipboardSharing, 'write'))
) {
await navigator.clipboard.writeText(clipboardData.data);
latestClipboardDigest.current = await sha256Digest(
@@ -124,15 +128,13 @@ export default function useDesktopSession(
const onShareDirectory = async () => {
try {
await tdpClient.shareDirectory();
- setDirectorySharingState(prevState => ({
- ...prevState,
+ setDirectorySharingState({
directorySelected: true,
- }));
+ });
} catch (e) {
- setDirectorySharingState(prevState => ({
- ...prevState,
+ setDirectorySharingState({
directorySelected: false,
- }));
+ });
addAlert({
severity: 'warn',
content: {
@@ -143,11 +145,17 @@ export default function useDesktopSession(
}
};
+ /** Clears sharing state. */
+ const clearSharing = useCallback(() => {
+ setDirectorySharingState({
+ directorySelected: false,
+ });
+ }, []);
+
return {
- clipboardSharingState,
- setClipboardSharingState,
- directorySharingState,
- setDirectorySharingState,
+ clipboardSharingState: clipboardSharing,
+ directorySharingState: directorySharing,
+ clearSharing,
onShareDirectory,
alerts,
onRemoveAlert,
@@ -311,15 +319,6 @@ export function isSharingDirectory(
);
}
-export const defaultDirectorySharingState: DirectorySharingState = {
- browserSupported: navigator.userAgent.includes('Chrome'),
- directorySelected: false,
-};
-
-export const defaultClipboardSharingState: ClipboardSharingState = {
- browserSupported: navigator.userAgent.includes('Chrome'),
-};
-
/**
* To be called before any system clipboard read/write operation.
*/
diff --git a/web/packages/teleport/src/DesktopSession/DesktopSession.tsx b/web/packages/teleport/src/DesktopSession/DesktopSession.tsx
index e24008757e084..e497ac444ab4b 100644
--- a/web/packages/teleport/src/DesktopSession/DesktopSession.tsx
+++ b/web/packages/teleport/src/DesktopSession/DesktopSession.tsx
@@ -131,6 +131,7 @@ export function DesktopSession() {
}
}}
aclAttempt={aclAttempt}
+ browserSupportsSharing={navigator.userAgent.includes('Chrome')}
hasAnotherSession={hasAnotherSession}
keyboardLayout={preferences.keyboardLayout}
/>
diff --git a/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx b/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx
index 9cd24ee1e92f0..67225c8829752 100644
--- a/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx
+++ b/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx
@@ -130,6 +130,7 @@ export function DocumentDesktopSession(props: {
client={client}
username={login}
aclAttempt={acl}
+ browserSupportsSharing
/>
);
}