Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 23 additions & 26 deletions web/packages/shared/components/DesktopSession/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { Flex } from 'design';
import * as Icons from 'design/Icon';
import { MenuIcon, MenuItem, MenuItemIcon } from 'shared/components/MenuAction';

Expand All @@ -25,32 +24,30 @@ export default function ActionMenu(props: Props) {
props;

return (
<Flex alignItems="center">
<MenuIcon
buttonIconProps={{
ml: 4,
size: 0,
color: 'text.slightlyMuted',
style: { fontSize: '20px' },
}}
menuProps={menuProps}
>
{showShareDirectory && (
<MenuItem onClick={onShareDirectory}>
<MenuItemIcon as={Icons.FolderPlus} mr="2" />
Share Directory
</MenuItem>
)}
<MenuItem onClick={onCtrlAltDel}>
<MenuItemIcon as={Icons.Keyboard} mr="2" />
Send Ctrl+Alt+Del
<MenuIcon
buttonIconProps={{
size: 0,
color: 'text.slightlyMuted',
style: { fontSize: '20px' },
title: 'More actions',
}}
menuProps={menuProps}
>
{showShareDirectory && (
<MenuItem onClick={onShareDirectory}>
<MenuItemIcon as={Icons.FolderPlus} mr="2" />
Share Directory
</MenuItem>
<MenuItem onClick={onDisconnect}>
<MenuItemIcon as={Icons.PowerSwitch} mr="2" />
Disconnect
</MenuItem>
</MenuIcon>
</Flex>
)}
<MenuItem onClick={onCtrlAltDel}>
<MenuItemIcon as={Icons.Keyboard} mr="2" />
Send Ctrl+Alt+Del
</MenuItem>
<MenuItem onClick={onDisconnect}>
<MenuItemIcon as={Icons.PowerSwitch} mr="2" />
Disconnect
</MenuItem>
</MenuIcon>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const props: DesktopSessionProps = {
client: fakeClient(),
username: 'user',
desktop: 'windows-11',
browserSupportsSharing: true,
hasAnotherSession: () => Promise.resolve(false),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
Expand All @@ -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<TdpTransport> => {
abortSignal.onabort = async () => {
await wait(50);
abortSignal.onabort = () => {
emitter.emit('complete');
};
return {
Expand All @@ -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(
Expand All @@ -85,6 +113,7 @@ test('reconnect button reinitializes the connection', async () => {
desktop="win-lab"
aclAttempt={aclAttempt}
hasAnotherSession={hasNoOtherSession}
browserSupportsSharing
/>
);

Expand Down Expand Up @@ -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(
<DesktopSession
client={tpdClient}
username="admin"
desktop="win-lab"
aclAttempt={aclAttempt}
hasAnotherSession={hasNoOtherSession}
browserSupportsSharing
/>
);

// 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();
});
24 changes: 8 additions & 16 deletions web/packages/shared/components/DesktopSession/DesktopSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ import { KeyboardHandler } from './KeyboardHandler';
import TopBar from './TopBar';
import useDesktopSession, {
clipboardSharingMessage,
defaultClipboardSharingState,
defaultDirectorySharingState,
directorySharingPossible,
isSharingClipboard,
isSharingDirectory,
Expand All @@ -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.
Expand All @@ -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<TdpConnectionStatus>({ status: '' });
Expand Down Expand Up @@ -145,16 +145,15 @@ export function DesktopSession({

const handleFatalError = useCallback(
(error: Error) => {
setDirectorySharingState(defaultDirectorySharingState);
setClipboardSharingState(defaultClipboardSharingState);
clearSharing();
setTdpConnectionStatus({
status: 'disconnected',
fromTdpError: error instanceof TdpError,
message: error.message,
});
initialTdpConnectionSucceeded.current = false;
},
[setClipboardSharingState, setDirectorySharingState]
[clearSharing]
);
useListener(client.onError, handleFatalError);

Expand Down Expand Up @@ -362,14 +361,7 @@ export function DesktopSession({
<TopBar
isConnected={screenState.state === 'canvas-visible'}
onDisconnect={() => {
setClipboardSharingState(prevState => ({
...prevState,
isSharing: false,
}));
setDirectorySharingState(prevState => ({
...prevState,
isSharing: false,
}));
clearSharing();
client.shutdown();
}}
userHost={`${username} on ${desktop}`}
Expand Down
45 changes: 19 additions & 26 deletions web/packages/shared/components/DesktopSession/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,35 +54,28 @@ export default function TopBar(props: Props) {
<TopNav
height="40px"
bg="levels.deep"
style={{
justifyContent: 'space-between',
}}
justifyContent="space-between"
gap={3}
px={3}
>
<Text px={3} style={{ color: theme.colors.text.slightlyMuted }}>
{userHost}
</Text>
<Text style={{ color: theme.colors.text.slightlyMuted }}>{userHost}</Text>

{isConnected && (
<Flex px={3}>
<Flex alignItems="center" gap={3}>
{latency && <LatencyDiagnostic latency={latency} />}
<HoverTooltip
tipContent={directorySharingToolTip(
canShareDirectory,
isSharingDirectory
)}
placement="bottom"
>
<FolderShared style={primaryOnTrue(isSharingDirectory)} />
</HoverTooltip>
<HoverTooltip
tipContent={clipboardSharingMessage}
placement="bottom"
>
<Clipboard style={primaryOnTrue(isSharingClipboard)} />
</HoverTooltip>
<AlertDropdown alerts={alerts} onRemoveAlert={onRemoveAlert} />
</Flex>
<Flex gap={3} alignItems="center">
{latency && <LatencyDiagnostic latency={latency} />}
<HoverTooltip
tipContent={directorySharingToolTip(
canShareDirectory,
isSharingDirectory
)}
placement="bottom"
>
<FolderShared style={primaryOnTrue(isSharingDirectory)} />
</HoverTooltip>
<HoverTooltip tipContent={clipboardSharingMessage} placement="bottom">
<Clipboard style={primaryOnTrue(isSharingClipboard)} />
</HoverTooltip>
<AlertDropdown alerts={alerts} onRemoveAlert={onRemoveAlert} />
<ActionMenu
onDisconnect={onDisconnect}
showShareDirectory={canShareDirectory && !isSharingDirectory}
Expand Down
Loading
Loading