From b6cf5e8491a29de9f55c9b21f99aa64f94282f90 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 25 Jan 2026 22:03:25 +0000 Subject: [PATCH] usb: implement AlreadyInUse alert Add an alert that notifies the user when the USB device is already in use by another application. Closes: https://github.com/pybricks/support/issues/2373 --- src/usb/alerts/AlreadyInUse.test.tsx | 19 +++++++++++++++++++ src/usb/alerts/AlreadyInUse.tsx | 24 ++++++++++++++++++++++++ src/usb/alerts/index.ts | 4 +++- src/usb/alerts/translations/en.json | 3 +++ src/usb/sagas.ts | 12 +++++++++++- 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 src/usb/alerts/AlreadyInUse.test.tsx create mode 100644 src/usb/alerts/AlreadyInUse.tsx diff --git a/src/usb/alerts/AlreadyInUse.test.tsx b/src/usb/alerts/AlreadyInUse.test.tsx new file mode 100644 index 00000000..91a78807 --- /dev/null +++ b/src/usb/alerts/AlreadyInUse.test.tsx @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025-2026 The Pybricks Authors + +import { Toast } from '@blueprintjs/core'; +import { act } from '@testing-library/react'; +import React from 'react'; +import { testRender } from '../../../test'; +import { alreadyInUse } from './AlreadyInUse'; + +it('should dismiss when close is clicked', async () => { + const callback = jest.fn(); + const toast = alreadyInUse(callback, undefined as never); + + const [user, message] = testRender(); + + await act(() => user.click(message.getByRole('button', { name: /close/i }))); + + expect(callback).toHaveBeenCalledWith('dismiss'); +}); diff --git a/src/usb/alerts/AlreadyInUse.tsx b/src/usb/alerts/AlreadyInUse.tsx new file mode 100644 index 00000000..834b48bb --- /dev/null +++ b/src/usb/alerts/AlreadyInUse.tsx @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025-2026 The Pybricks Authors + +import { Intent } from '@blueprintjs/core'; +import { Error } from '@blueprintjs/icons'; +import React from 'react'; +import type { CreateToast } from '../../toasterTypes'; +import { useI18n } from './i18n'; + +const AlreadyInUse: React.FunctionComponent = () => { + const i18n = useI18n(); + return ( + <> +

{i18n.translate('alreadyInUse.message')}

+ + ); +}; + +export const alreadyInUse: CreateToast = (onAction) => ({ + message: , + icon: , + intent: Intent.DANGER, + onDismiss: () => onAction('dismiss'), +}); diff --git a/src/usb/alerts/index.ts b/src/usb/alerts/index.ts index c8f30b51..f6d5c0fa 100644 --- a/src/usb/alerts/index.ts +++ b/src/usb/alerts/index.ts @@ -1,12 +1,14 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2025 The Pybricks Authors +// Copyright (c) 2025-2026 The Pybricks Authors +import { alreadyInUse } from './AlreadyInUse'; import { newPybricksProfile } from './NewPybricksProfile'; import { noWebUsb } from './NoWebUsb'; import { oldFirmware } from './OldFirmware'; // gathers all of the alert creation functions for passing up to the top level export default { + alreadyInUse, newPybricksProfile, noWebUsb, oldFirmware, diff --git a/src/usb/alerts/translations/en.json b/src/usb/alerts/translations/en.json index 93732567..2f0019a2 100644 --- a/src/usb/alerts/translations/en.json +++ b/src/usb/alerts/translations/en.json @@ -4,6 +4,9 @@ "suggestion": "Use a supported browser such as Google Chrome or Microsoft Edge.", "action": "More Info" }, + "alreadyInUse": { + "message": "This hub is already in use by another application." + }, "oldFirmware": { "message": "A new firmware version is available for this hub. Please install the latest version to use all new features.", "flashFirmware": { diff --git a/src/usb/sagas.ts b/src/usb/sagas.ts index eb9a1c96..85f38a59 100644 --- a/src/usb/sagas.ts +++ b/src/usb/sagas.ts @@ -205,7 +205,17 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { maybe(usbDevice.claimInterface(iface.interfaceNumber)), ); if (claimErr) { - // TODO: show error message to user here + // Only show error to the user if they initiated the connection. + if (hotPlugDevice === undefined) { + if (claimErr.name === 'NetworkError') { + yield* put(alertsShowAlert('usb', 'alreadyInUse')); + } else { + yield* put( + alertsShowAlert('alerts', 'unexpectedError', { error: claimErr }), + ); + } + } + console.error('Failed to claim USB interface:', claimErr); yield* put(usbDidFailToConnectPybricks()); yield* cleanup();