From a64cdff74227a62d4d1fe832148e78fa77022140 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 15 Feb 2023 18:03:37 +0100 Subject: [PATCH] feat: introduced cloud state in sketchbook view Closes #1879 Signed-off-by: Akos Kitta --- arduino-ide-extension/package.json | 1 + .../browser/arduino-ide-frontend-module.ts | 6 + .../src/browser/contributions/account.ts | 18 +- .../src/browser/contributions/contribution.ts | 5 +- .../browser/icons/cloud-filled-offline.svg | 4 + .../src/browser/icons/cloud-filled.svg | 3 + .../src/browser/icons/cloud-offline.svg | 4 + .../src/browser/icons/cloud.svg | 3 + .../src/browser/style/account-icon.svg | 1 - .../src/browser/style/cloud-sketchbook.css | 43 ++- .../src/browser/style/sketch-folder-icon.svg | 4 - .../src/browser/style/sketchbook.css | 7 - .../theia/core/connection-status-service.ts | 346 ++++++++++++++---- .../theia/core/sidebar-bottom-menu-widget.tsx | 6 +- .../theia/messages/notifications-manager.ts | 8 - .../cloud-sketchbook-composite-widget.tsx | 7 +- .../cloud-sketchbook-tree-widget.tsx | 26 ++ .../cloud-sketchbook/cloud-sketchbook-tree.ts | 30 +- ...cloud-user-status.tsx => cloud-status.tsx} | 58 +-- .../sketchbook/sketchbook-tree-widget.tsx | 5 +- .../browser/connection-status-service.test.ts | 46 +++ i18n/en.json | 4 + yarn.lock | 83 +++++ 23 files changed, 584 insertions(+), 134 deletions(-) create mode 100644 arduino-ide-extension/src/browser/icons/cloud-filled-offline.svg create mode 100644 arduino-ide-extension/src/browser/icons/cloud-filled.svg create mode 100644 arduino-ide-extension/src/browser/icons/cloud-offline.svg create mode 100644 arduino-ide-extension/src/browser/icons/cloud.svg delete mode 100644 arduino-ide-extension/src/browser/style/account-icon.svg delete mode 100644 arduino-ide-extension/src/browser/style/sketch-folder-icon.svg rename arduino-ide-extension/src/browser/widgets/cloud-sketchbook/{cloud-user-status.tsx => cloud-status.tsx} (61%) create mode 100644 arduino-ide-extension/src/test/browser/connection-status-service.test.ts diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index be99e0366..a1c458363 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -77,6 +77,7 @@ "glob": "^7.1.6", "google-protobuf": "^3.20.1", "hash.js": "^1.1.7", + "is-online": "^9.0.1", "js-yaml": "^3.13.1", "just-diff": "^5.1.1", "jwt-decode": "^3.1.2", diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 7dd6fc1b9..4a4f615f0 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -90,6 +90,8 @@ import { EditorCommandContribution as TheiaEditorCommandContribution } from '@th import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution, + DaemonPort, + IsOnline, } from './theia/core/connection-status-service'; import { FrontendConnectionStatusService as TheiaFrontendConnectionStatusService, @@ -1021,4 +1023,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(SidebarBottomMenuWidget).toSelf(); rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget); + bind(DaemonPort).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(DaemonPort); + bind(IsOnline).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(IsOnline); }); diff --git a/arduino-ide-extension/src/browser/contributions/account.ts b/arduino-ide-extension/src/browser/contributions/account.ts index cb82229ad..a8f728de2 100644 --- a/arduino-ide-extension/src/browser/contributions/account.ts +++ b/arduino-ide-extension/src/browser/contributions/account.ts @@ -8,6 +8,7 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { CloudUserCommands, LEARN_MORE_URL } from '../auth/cloud-user-commands'; import { CreateFeatures } from '../create/create-features'; import { ArduinoMenus } from '../menu/arduino-menus'; +import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service'; import { Command, CommandRegistry, @@ -29,6 +30,8 @@ export class Account extends Contribution { private readonly windowService: WindowService; @inject(CreateFeatures) private readonly createFeatures: CreateFeatures; + @inject(ApplicationConnectionStatusContribution) + private readonly connectionStatus: ApplicationConnectionStatusContribution; private readonly toDispose = new DisposableCollection(); private app: FrontendApplication; @@ -50,21 +53,28 @@ export class Account extends Contribution { override registerCommands(registry: CommandRegistry): void { const openExternal = (url: string) => this.windowService.openNewWindow(url, { external: true }); + const loggedIn = () => Boolean(this.createFeatures.session); + const loggedInWithInternetConnection = () => + loggedIn() && this.connectionStatus.offlineStatus !== 'internet'; registry.registerCommand(Account.Commands.LEARN_MORE, { execute: () => openExternal(LEARN_MORE_URL), - isEnabled: () => !Boolean(this.createFeatures.session), + isEnabled: () => !loggedIn(), + isVisible: () => !loggedIn(), }); registry.registerCommand(Account.Commands.GO_TO_PROFILE, { execute: () => openExternal('https://id.arduino.cc/'), - isEnabled: () => Boolean(this.createFeatures.session), + isEnabled: () => loggedInWithInternetConnection(), + isVisible: () => loggedIn(), }); registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, { execute: () => openExternal('https://create.arduino.cc/editor'), - isEnabled: () => Boolean(this.createFeatures.session), + isEnabled: () => loggedInWithInternetConnection(), + isVisible: () => loggedIn(), }); registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, { execute: () => openExternal('https://create.arduino.cc/iot/'), - isEnabled: () => Boolean(this.createFeatures.session), + isEnabled: () => loggedInWithInternetConnection(), + isVisible: () => loggedIn(), }); } diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts index 3260ab7b9..c53bac1ff 100644 --- a/arduino-ide-extension/src/browser/contributions/contribution.ts +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -14,7 +14,6 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { MessageService } from '@theia/core/lib/common/message-service'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; - import { MenuModelRegistry, MenuContribution, @@ -58,7 +57,7 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { ExecuteWithProgress } from '../../common/protocol/progressible'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsDataStore } from '../boards/boards-data-store'; -import { NotificationManager } from '../theia/messages/notifications-manager'; +import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager'; import { MessageType } from '@theia/core/lib/common/message-service-protocol'; import { WorkspaceService } from '../theia/workspace/workspace-service'; import { MainMenuManager } from '../../common/main-menu-manager'; @@ -295,7 +294,7 @@ export abstract class CoreServiceContribution extends SketchContribution { } private notificationId(message: string, ...actions: string[]): string { - return this.notificationManager.getMessageId({ + return this.notificationManager['getMessageId']({ text: message, actions, type: MessageType.Error, diff --git a/arduino-ide-extension/src/browser/icons/cloud-filled-offline.svg b/arduino-ide-extension/src/browser/icons/cloud-filled-offline.svg new file mode 100644 index 000000000..b86bb5209 --- /dev/null +++ b/arduino-ide-extension/src/browser/icons/cloud-filled-offline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/arduino-ide-extension/src/browser/icons/cloud-filled.svg b/arduino-ide-extension/src/browser/icons/cloud-filled.svg new file mode 100644 index 000000000..e51c12063 --- /dev/null +++ b/arduino-ide-extension/src/browser/icons/cloud-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/arduino-ide-extension/src/browser/icons/cloud-offline.svg b/arduino-ide-extension/src/browser/icons/cloud-offline.svg new file mode 100644 index 000000000..26a284adc --- /dev/null +++ b/arduino-ide-extension/src/browser/icons/cloud-offline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/arduino-ide-extension/src/browser/icons/cloud.svg b/arduino-ide-extension/src/browser/icons/cloud.svg new file mode 100644 index 000000000..4d23d15ae --- /dev/null +++ b/arduino-ide-extension/src/browser/icons/cloud.svg @@ -0,0 +1,3 @@ + + + diff --git a/arduino-ide-extension/src/browser/style/account-icon.svg b/arduino-ide-extension/src/browser/style/account-icon.svg deleted file mode 100644 index 2759d882f..000000000 --- a/arduino-ide-extension/src/browser/style/account-icon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/arduino-ide-extension/src/browser/style/cloud-sketchbook.css b/arduino-ide-extension/src/browser/style/cloud-sketchbook.css index f19159260..68b066c28 100644 --- a/arduino-ide-extension/src/browser/style/cloud-sketchbook.css +++ b/arduino-ide-extension/src/browser/style/cloud-sketchbook.css @@ -15,10 +15,10 @@ .p-TabBar-tabIcon.cloud-sketchbook-tree-icon { background-color: var(--theia-foreground); - -webkit-mask: url(./cloud-sketchbook-tree-icon.svg); + -webkit-mask: url(../icons/cloud.svg); -webkit-mask-position: center; -webkit-mask-repeat: no-repeat; - width: var(--theia-icon-size); + width: 19px !important; height: var(--theia-icon-size); -webkit-mask-size: 100%; } @@ -26,7 +26,7 @@ .p-mod-current .cloud-sketchbook-tree-icon { background-color: var(--theia-foreground); - -webkit-mask: url(./cloud-sketchbook-tree-icon-filled.svg); + -webkit-mask: url(../icons/cloud-filled.svg); -webkit-mask-position: center; -webkit-mask-repeat: no-repeat; -webkit-mask-size: 100%; @@ -118,7 +118,6 @@ } .account-icon { - background: url("./account-icon.svg") center center no-repeat; width: var(--theia-private-sidebar-icon-size); height: var(--theia-private-sidebar-icon-size); border-radius: 50%; @@ -199,3 +198,39 @@ .arduino-share-sketch-dialog .sketch-link-embed textarea { width: 100%; } + +.theia-file-icons-js.file-icon > .sketch-folder-icon { + -webkit-mask-position: center; + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: 100%; + width: var(--theia-icon-size); + height: var(--theia-icon-size); + background-color: var(--theia-foreground); +} + +.theia-file-icons-js.file-icon > .sketch-folder-icon.cloud { + -webkit-mask: url('../icons/cloud.svg'); +} + +.theia-file-icons-js.file-icon > .sketch-folder-icon.cloud.offline { + -webkit-mask: url('../icons/cloud-offline.svg'); + background-color: var(--theia-activityBar-inactiveForeground); +} + +.theia-file-icons-js.file-icon > .sketch-folder-icon.cloud.synced { + -webkit-mask: url('../icons/cloud-filled.svg'); +} + +.theia-TreeNodeContent > .theia-file-icons-js.file-icon > .sketch-folder-icon.cloud.synced.offline { + -webkit-mask: url('../icons/cloud-filled-offline.svg'); +} + +.sketch-folder-icon.cloud.offline.action { + -webkit-mask: url('../icons/cloud-offline.svg'); + -webkit-mask-position: center; + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: 100%; + width: var(--theia-icon-size); + height: var(--theia-icon-size); + background-color: var(--theia-foreground); +} diff --git a/arduino-ide-extension/src/browser/style/sketch-folder-icon.svg b/arduino-ide-extension/src/browser/style/sketch-folder-icon.svg deleted file mode 100644 index 363c5df10..000000000 --- a/arduino-ide-extension/src/browser/style/sketch-folder-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/arduino-ide-extension/src/browser/style/sketchbook.css b/arduino-ide-extension/src/browser/style/sketchbook.css index 87143e60c..8a9270c36 100644 --- a/arduino-ide-extension/src/browser/style/sketchbook.css +++ b/arduino-ide-extension/src/browser/style/sketchbook.css @@ -3,13 +3,6 @@ mask: url('./sketchbook.svg'); } -.sketch-folder-icon { - background: url('./sketch-folder-icon.svg') center center no-repeat; - background-position-x: 1px; - width: var(--theia-icon-size); - height: var(--theia-icon-size); -} - .p-TabBar-tabIcon.sketchbook-tree-icon { background-color: var(--theia-foreground); -webkit-mask: url(./sketchbook-tree-icon.svg); diff --git a/arduino-ide-extension/src/browser/theia/core/connection-status-service.ts b/arduino-ide-extension/src/browser/theia/core/connection-status-service.ts index 0738772c6..95dd07b8f 100644 --- a/arduino-ide-extension/src/browser/theia/core/connection-status-service.ts +++ b/arduino-ide-extension/src/browser/theia/core/connection-status-service.ts @@ -1,106 +1,328 @@ +import { + ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution, + ConnectionStatus, + FrontendConnectionStatusService as TheiaFrontendConnectionStatusService, +} from '@theia/core/lib/browser/connection-status-service'; +import type { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar'; +import { Disposable } from '@theia/core/lib/common/disposable'; +import { Emitter, Event } from '@theia/core/lib/common/event'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { MessageType } from '@theia/core/lib/common/message-service-protocol'; +import { nls } from '@theia/core/lib/common/nls'; import { inject, injectable, postConstruct, } from '@theia/core/shared/inversify'; -import { Disposable } from '@theia/core/lib/common/disposable'; -import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar'; -import { - FrontendConnectionStatusService as TheiaFrontendConnectionStatusService, - ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution, - ConnectionStatus, -} from '@theia/core/lib/browser/connection-status-service'; +import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager'; import { ArduinoDaemon } from '../../../common/protocol'; import { NotificationCenter } from '../../notification-center'; -import { nls } from '@theia/core/lib/common'; import debounce = require('lodash.debounce'); +import isOnline = require('is-online'); +import { CreateFeatures } from '../../create/create-features'; @injectable() -export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService { - @inject(ArduinoDaemon) - protected readonly daemon: ArduinoDaemon; +export class IsOnline implements FrontendApplicationContribution { + private readonly onDidChangeOnlineEmitter = new Emitter(); + private _online = false; + private stopped = false; + + onStart(): void { + const checkOnline = async () => { + if (!this.stopped) { + try { + const online = await isOnline(); + this.setOnline(online); + } finally { + window.setTimeout(() => checkOnline(), 6_000); // 6 seconds poll interval + } + } + }; + checkOnline(); + } + onStop(): void { + this.stopped = true; + this.onDidChangeOnlineEmitter.dispose(); + } + + get online(): boolean { + return this._online; + } + + get onDidChangeOnline(): Event { + return this.onDidChangeOnlineEmitter.event; + } + + private setOnline(online: boolean) { + const oldOnline = this._online; + this._online = online; + if (!this.stopped && this._online !== oldOnline) { + this.onDidChangeOnlineEmitter.fire(this._online); + } + } +} + +@injectable() +export class DaemonPort implements FrontendApplicationContribution { + @inject(ArduinoDaemon) + private readonly daemon: ArduinoDaemon; @inject(NotificationCenter) - protected readonly notificationCenter: NotificationCenter; + private readonly notificationCenter: NotificationCenter; + + private readonly onPortDidChangeEmitter = new Emitter(); + private _port: string | undefined; + + onStart(): void { + this.daemon.tryGetPort().then( + (port) => this.setPort(port), + (reason) => + console.warn('Could not retrieve the CLI daemon port.', reason) + ); + this.notificationCenter.onDaemonDidStart((port) => this.setPort(port)); + this.notificationCenter.onDaemonDidStop(() => this.setPort(undefined)); + } - protected connectedPort: string | undefined; + onStop(): void { + this.onPortDidChangeEmitter.dispose(); + } + + get port(): string | undefined { + return this._port; + } + + get onDidChangePort(): Event { + return this.onPortDidChangeEmitter.event; + } + + private setPort(port: string | undefined): void { + const oldPort = this._port; + this._port = port; + if (this._port !== oldPort) { + this.onPortDidChangeEmitter.fire(this._port); + } + } +} + +@injectable() +export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService { + @inject(DaemonPort) + private readonly daemonPort: DaemonPort; + @inject(IsOnline) + private readonly isOnline: IsOnline; @postConstruct() protected override async init(): Promise { this.schedulePing(); - try { - this.connectedPort = await this.daemon.tryGetPort(); - } catch {} - this.notificationCenter.onDaemonDidStart( - (port) => (this.connectedPort = port) - ); - this.notificationCenter.onDaemonDidStop( - () => (this.connectedPort = undefined) - ); const refresh = debounce(() => { - this.updateStatus(!!this.connectedPort); + this.updateStatus(Boolean(this.daemonPort.port) && this.isOnline.online); this.schedulePing(); }, this.options.offlineTimeout - 10); this.wsConnectionProvider.onIncomingMessageActivity(() => refresh()); } + + protected override async performPingRequest(): Promise { + try { + await this.pingService.ping(); + this.updateStatus(this.isOnline.online); + } catch (e) { + this.updateStatus(false); + this.logger.error(e); + } + } } +const connectionStatusStatusBar = 'connection-status'; +const theiaOffline = 'theia-mod-offline'; + +export type OfflineConnectionStatus = + /** + * There is no websocket connection between the frontend and the backend. + */ + | 'backend' + /** + * The CLI daemon port is not available. Could not establish the gRPC connection between the backend and the CLI. + */ + | 'daemon' + /** + * Cloud not connect to the Internet from the browser. + */ + | 'internet'; + @injectable() export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution { - @inject(ArduinoDaemon) - protected readonly daemon: ArduinoDaemon; + @inject(DaemonPort) + private readonly daemonPort: DaemonPort; + @inject(IsOnline) + private readonly isOnline: IsOnline; + @inject(MessageService) + private readonly messageService: MessageService; + @inject(NotificationManager) + private readonly notificationManager: NotificationManager; + @inject(CreateFeatures) + private readonly createFeatures: CreateFeatures; - @inject(NotificationCenter) - protected readonly notificationCenter: NotificationCenter; + private readonly offlineStatusDidChangeEmitter = new Emitter< + OfflineConnectionStatus | undefined + >(); + private noInternetConnectionNotificationId: string | undefined; + private _offlineStatus: OfflineConnectionStatus | undefined; - protected connectedPort: string | undefined; + get offlineStatus(): OfflineConnectionStatus | undefined { + return this._offlineStatus; + } - @postConstruct() - protected async init(): Promise { - try { - this.connectedPort = await this.daemon.tryGetPort(); - } catch {} - this.notificationCenter.onDaemonDidStart( - (port) => (this.connectedPort = port) - ); - this.notificationCenter.onDaemonDidStop( - () => (this.connectedPort = undefined) - ); + get onOfflineStatusDidChange(): Event { + return this.offlineStatusDidChangeEmitter.event; } protected override onStateChange(state: ConnectionStatus): void { - if (!this.connectedPort && state === ConnectionStatus.ONLINE) { + if ( + (!Boolean(this.daemonPort.port) || !this.isOnline.online) && + state === ConnectionStatus.ONLINE + ) { return; } super.onStateChange(state); } protected override handleOffline(): void { - this.statusBar.setElement('connection-status', { + const params = { + port: this.daemonPort.port, + online: this.isOnline.online, + }; + this._offlineStatus = offlineConnectionStatusType(params); + const { text, tooltip } = offlineMessage(params); + this.statusBar.setElement(connectionStatusStatusBar, { alignment: StatusBarAlignment.LEFT, - text: this.connectedPort - ? nls.localize('theia/core/offline', 'Offline') - : '$(bolt) ' + - nls.localize('theia/core/daemonOffline', 'CLI Daemon Offline'), - tooltip: this.connectedPort - ? nls.localize( - 'theia/core/cannotConnectBackend', - 'Cannot connect to the backend.' - ) - : nls.localize( - 'theia/core/cannotConnectDaemon', - 'Cannot connect to the CLI daemon.' - ), + text, + tooltip, priority: 5000, }); - this.toDisposeOnOnline.push( - Disposable.create(() => this.statusBar.removeElement('connection-status')) - ); - document.body.classList.add('theia-mod-offline'); - this.toDisposeOnOnline.push( + document.body.classList.add(theiaOffline); + this.toDisposeOnOnline.pushAll([ Disposable.create(() => - document.body.classList.remove('theia-mod-offline') - ) - ); + this.statusBar.removeElement(connectionStatusStatusBar) + ), + Disposable.create(() => document.body.classList.remove(theiaOffline)), + Disposable.create(() => { + this._offlineStatus = undefined; + this.fireStatusDidChange(); + }), + ]); + if (!this.isOnline.online) { + const text = nls.localize( + 'arduino/connectionStatus/connectionLost', + "Connection lost. Cloud Sketches actions and Updates won't be available" + ); + this.noInternetConnectionNotificationId = this.notificationManager[ + 'getMessageId' + ]({ text, type: MessageType.Warning }); + if (this.createFeatures.enabled) { + this.messageService.warn(text); + } + this.toDisposeOnOnline.push( + Disposable.create(() => this.clearNoInternetConnectionNotification()) + ); + } + this.fireStatusDidChange(); + } + + private clearNoInternetConnectionNotification(): void { + if (this.noInternetConnectionNotificationId) { + this.notificationManager.clear(this.noInternetConnectionNotificationId); + this.noInternetConnectionNotificationId = undefined; + } + } + + private fireStatusDidChange(): void { + if (this.createFeatures.enabled) { + return this.offlineStatusDidChangeEmitter.fire(this._offlineStatus); + } + } +} + +interface OfflineMessageParams { + readonly port: string | undefined; + readonly online: boolean; +} +interface OfflineMessage { + readonly text: string; + readonly tooltip: string; +} + +/** + * (non-API) exported for testing + * + * The precedence of the offline states are the following: + * - No connection to the Theia backend, + * - CLI daemon is offline, and + * - There is no Internet connection. + */ +export function offlineMessage(params: OfflineMessageParams): OfflineMessage { + const statusType = offlineConnectionStatusType(params); + const text = getOfflineText(statusType); + const tooltip = getOfflineTooltip(statusType); + return { text, tooltip }; +} + +function offlineConnectionStatusType( + params: OfflineMessageParams +): OfflineConnectionStatus { + const { port, online } = params; + if (port && online) { + return 'backend'; + } + if (!port) { + return 'daemon'; + } + return 'internet'; +} + +export const backendOfflineText = nls.localize('theia/core/offline', 'Offline'); +export const daemonOfflineText = nls.localize( + 'theia/core/daemonOffline', + 'CLI Daemon Offline' +); +export const offlineText = nls.localize('theia/core/offlineText', 'Offline'); +export const backendOfflineTooltip = nls.localize( + 'theia/core/cannotConnectBackend', + 'Cannot connect to the backend.' +); +export const daemonOfflineTooltip = nls.localize( + 'theia/core/cannotConnectDaemon', + 'Cannot connect to the CLI daemon.' +); +export const offlineTooltip = offlineText; + +function getOfflineText(statusType: OfflineConnectionStatus): string { + switch (statusType) { + case 'backend': + return backendOfflineText; + case 'daemon': + return '$(bolt) ' + daemonOfflineText; + case 'internet': + return '$(alert) ' + offlineText; + default: + assertUnreachable(statusType); } } + +function getOfflineTooltip(statusType: OfflineConnectionStatus): string { + switch (statusType) { + case 'backend': + return backendOfflineTooltip; + case 'daemon': + return daemonOfflineTooltip; + case 'internet': + return offlineTooltip; + default: + assertUnreachable(statusType); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function assertUnreachable(_: never): never { + throw new Error(); +} diff --git a/arduino-ide-extension/src/browser/theia/core/sidebar-bottom-menu-widget.tsx b/arduino-ide-extension/src/browser/theia/core/sidebar-bottom-menu-widget.tsx index 308d77260..037ba2b60 100644 --- a/arduino-ide-extension/src/browser/theia/core/sidebar-bottom-menu-widget.tsx +++ b/arduino-ide-extension/src/browser/theia/core/sidebar-bottom-menu-widget.tsx @@ -28,7 +28,7 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget { menuPath: MenuPath ): void { const button = e.currentTarget.getBoundingClientRect(); - this.contextMenuRenderer.render({ + const options = { menuPath, includeAnchorArg: false, anchor: { @@ -37,7 +37,9 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget { // https://github.com/eclipse-theia/theia/discussions/12170 y: button.top, }, - }); + showDisabled: true, + }; + this.contextMenuRenderer.render(options); } protected override render(): React.ReactNode { diff --git a/arduino-ide-extension/src/browser/theia/messages/notifications-manager.ts b/arduino-ide-extension/src/browser/theia/messages/notifications-manager.ts index bdc1ed35f..51339dbf3 100644 --- a/arduino-ide-extension/src/browser/theia/messages/notifications-manager.ts +++ b/arduino-ide-extension/src/browser/theia/messages/notifications-manager.ts @@ -1,6 +1,5 @@ import { CancellationToken } from '@theia/core/lib/common/cancellation'; import type { - Message, ProgressMessage, ProgressUpdate, } from '@theia/core/lib/common/message-service-protocol'; @@ -46,11 +45,4 @@ export class NotificationManager extends TheiaNotificationManager { } return Math.min((update.work.done / update.work.total) * 100, 100); } - - /** - * For `public` visibility. - */ - override getMessageId(message: Message): string { - return super.getMessageId(message); - } } diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx index 0b4d26a94..46e273b25 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx @@ -5,7 +5,7 @@ import { injectable, postConstruct, } from '@theia/core/shared/inversify'; -import { CloudStatus } from './cloud-user-status'; +import { CloudStatus } from './cloud-status'; import { nls } from '@theia/core/lib/common/nls'; import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget'; import { AuthenticationClientService } from '../../auth/authentication-client-service'; @@ -13,6 +13,7 @@ import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model'; import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget'; import { CreateNew } from '../sketchbook/create-new'; import { AuthenticationSession } from '../../../node/auth/types'; +import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service'; @injectable() export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidget { @@ -20,6 +21,9 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge private readonly authenticationService: AuthenticationClientService; @inject(CloudSketchbookTreeWidget) private readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget; + @inject(ApplicationConnectionStatusContribution) + private readonly connectionStatus: ApplicationConnectionStatusContribution; + private _session: AuthenticationSession | undefined; constructor() { @@ -66,6 +70,7 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel } authenticationService={this.authenticationService} + connectionStatus={this.connectionStatus} /> ); diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-widget.tsx b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-widget.tsx index 3f1ae430f..925d42e43 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-widget.tsx @@ -15,6 +15,7 @@ import { CompositeTreeNode } from '@theia/core/lib/browser'; import { shell } from '@theia/core/electron-shared/@electron/remote'; import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget'; import { nls } from '@theia/core/lib/common'; +import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service'; @injectable() export class CloudSketchbookTreeWidget extends SketchbookTreeWidget { @@ -27,6 +28,9 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget { @inject(CloudSketchbookTree) protected readonly cloudSketchbookTree: CloudSketchbookTree; + @inject(ApplicationConnectionStatusContribution) + private readonly connectionStatus: ApplicationConnectionStatusContribution; + protected override renderTree(model: TreeModel): React.ReactNode { if (this.shouldShowWelcomeView()) return this.renderViewWelcome(); if (this.shouldShowEmptyView()) return this.renderEmptyView(); @@ -91,6 +95,28 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget { return classNames; } + protected override renderIcon( + node: TreeNode, + props: NodeProps + ): React.ReactNode { + if (CloudSketchbookTree.CloudSketchDirNode.is(node)) { + const iconClassName = ['sketch-folder-icon', 'cloud']; + if (CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) { + iconClassName.push('synced'); + } + if (this.connectionStatus.offlineStatus === 'internet') { + iconClassName.push('offline'); + } + const icon = iconClassName.join(' '); + return ( +
+
+
+ ); + } + return super.renderIcon(node, props); + } + protected override renderInlineCommands(node: any): React.ReactNode { if (CloudSketchbookTree.CloudSketchDirNode.is(node) && node.commands) { return Array.from(new Set(node.commands)).map((command) => diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts index e3dd60e70..8a652c10e 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts @@ -31,6 +31,7 @@ import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree'; import { posix, splitSketchPath } from '../../create/create-paths'; import { Create } from '../../create/typings'; import { nls } from '@theia/core/lib/common'; +import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service'; const MESSAGE_TIMEOUT = 5 * 1000; const deepmerge = require('deepmerge').default; @@ -54,6 +55,16 @@ export class CloudSketchbookTree extends SketchbookTree { @inject(CreateApi) private readonly createApi: CreateApi; + @inject(ApplicationConnectionStatusContribution) + private readonly connectionStatus: ApplicationConnectionStatusContribution; + + protected override init(): void { + this.toDispose.push( + this.connectionStatus.onOfflineStatusDidChange(() => this.refresh()) + ); + super.init(); + } + async pushPublicWarn( node: CloudSketchbookTree.CloudSketchDirNode ): Promise { @@ -501,7 +512,7 @@ export class CloudSketchbookTree extends SketchbookTree { }; } - protected readonly notInSyncDecoration: WidgetDecoration.Data = { + protected readonly notInSyncOfflineDecoration: WidgetDecoration.Data = { fontData: { color: 'var(--theia-activityBar-inactiveForeground)', }, @@ -522,11 +533,15 @@ export class CloudSketchbookTree extends SketchbookTree { node.fileStat.resource.path.toString() ); - const commands = [CloudSketchbookCommands.PULL_SKETCH]; + const commands: Command[] = []; + if (this.connectionStatus.offlineStatus !== 'internet') { + commands.push(CloudSketchbookCommands.PULL_SKETCH); + } if ( CloudSketchbookTree.CloudSketchTreeNode.is(node) && - CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) + CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) && + this.connectionStatus.offlineStatus !== 'internet' ) { commands.push(CloudSketchbookCommands.PUSH_SKETCH); } @@ -557,14 +572,15 @@ export class CloudSketchbookTree extends SketchbookTree { } } - // add style decoration for not-in-sync files + // add style decoration for not-in-sync files when offline if ( CloudSketchbookTree.CloudSketchTreeNode.is(node) && - !CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) + !CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) && + this.connectionStatus.offlineStatus === 'internet' ) { - this.mergeDecoration(node, this.notInSyncDecoration); + this.mergeDecoration(node, this.notInSyncOfflineDecoration); } else { - this.removeDecoration(node, this.notInSyncDecoration); + this.removeDecoration(node, this.notInSyncOfflineDecoration); } return node; diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-status.tsx similarity index 61% rename from arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx rename to arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-status.tsx index 68a7da5eb..77c21ca2f 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-status.tsx @@ -1,19 +1,17 @@ import * as React from '@theia/core/shared/react'; -import { - Disposable, - DisposableCollection, -} from '@theia/core/lib/common/disposable'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model'; import { AuthenticationClientService } from '../../auth/authentication-client-service'; import { nls } from '@theia/core/lib/common'; +import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service'; export class CloudStatus extends React.Component< - UserStatus.Props, - UserStatus.State + CloudStatus.Props, + CloudStatus.State > { protected readonly toDispose = new DisposableCollection(); - constructor(props: UserStatus.Props) { + constructor(props: CloudStatus.Props) { super(props); this.state = { status: this.status, @@ -22,17 +20,11 @@ export class CloudStatus extends React.Component< } override componentDidMount(): void { - const statusListener = () => this.setState({ status: this.status }); - window.addEventListener('online', statusListener); - window.addEventListener('offline', statusListener); - this.toDispose.pushAll([ - Disposable.create(() => - window.removeEventListener('online', statusListener) - ), - Disposable.create(() => - window.removeEventListener('offline', statusListener) - ), - ]); + this.toDispose.push( + this.props.connectionStatus.onOfflineStatusDidChange(() => + this.setState({ status: this.status }) + ) + ); } override componentWillUnmount(): void { @@ -58,14 +50,21 @@ export class CloudStatus extends React.Component< : nls.localize('arduino/cloud/offline', 'Offline')}
-
+ {this.props.connectionStatus.offlineStatus === 'internet' ? ( +
+ ) : ( +
+ )}
); @@ -83,14 +82,17 @@ export class CloudStatus extends React.Component< }; private get status(): 'connected' | 'offline' { - return window.navigator.onLine ? 'connected' : 'offline'; + return this.props.connectionStatus.offlineStatus === 'internet' + ? 'offline' + : 'connected'; } } -export namespace UserStatus { +export namespace CloudStatus { export interface Props { readonly model: CloudSketchbookTreeModel; readonly authenticationService: AuthenticationClientService; + readonly connectionStatus: ApplicationConnectionStatusContribution; } export interface State { status: 'connected' | 'offline'; diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx index 1c3323227..20cab5d68 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx @@ -23,7 +23,6 @@ import { SketchesServiceClientImpl, } from '../../sketches-service-client-impl'; import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection'; -import { Sketch } from '../../contributions/contribution'; import { nls } from '@theia/core/lib/common'; const customTreeProps: TreeProps = { @@ -91,8 +90,8 @@ export class SketchbookTreeWidget extends FileTreeWidget { node: TreeNode, props: NodeProps ): React.ReactNode { - if (SketchbookTree.SketchDirNode.is(node) || Sketch.isSketchFile(node.id)) { - return
; + if (SketchbookTree.SketchDirNode.is(node)) { + return undefined; } const icon = this.toNodeIcon(node); if (icon) { diff --git a/arduino-ide-extension/src/test/browser/connection-status-service.test.ts b/arduino-ide-extension/src/test/browser/connection-status-service.test.ts new file mode 100644 index 000000000..1f1228ce6 --- /dev/null +++ b/arduino-ide-extension/src/test/browser/connection-status-service.test.ts @@ -0,0 +1,46 @@ +import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; +const disableJSDOM = enableJSDOM(); + +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +FrontendApplicationConfigProvider.set({}); + +import { expect } from 'chai'; +import { + backendOfflineText, + backendOfflineTooltip, + daemonOfflineText, + daemonOfflineTooltip, + offlineText, + offlineTooltip, + offlineMessage, +} from '../../browser/theia/core/connection-status-service'; + +disableJSDOM(); + +describe('connection-status-service', () => { + describe('offlineMessage', () => { + it('should warn about the offline backend if connected to both CLI daemon and Internet but offline', () => { + const actual = offlineMessage({ port: '50051', online: true }); + expect(actual.text).to.be.equal(backendOfflineText); + expect(actual.tooltip).to.be.equal(backendOfflineTooltip); + }); + + it('should warn about the offline CLI daemon if the CLI daemon port is missing but has Internet connection', () => { + const actual = offlineMessage({ port: undefined, online: true }); + expect(actual.text.endsWith(daemonOfflineText)).to.be.true; + expect(actual.tooltip).to.be.equal(daemonOfflineTooltip); + }); + + it('should warn about the offline CLI daemon if the CLI daemon port is missing and has no Internet connection', () => { + const actual = offlineMessage({ port: undefined, online: false }); + expect(actual.text.endsWith(daemonOfflineText)).to.be.true; + expect(actual.tooltip).to.be.equal(daemonOfflineTooltip); + }); + + it('should warn about no Internet connection if CLI daemon port is available but the Internet connection is offline', () => { + const actual = offlineMessage({ port: '50051', online: false }); + expect(actual.text.endsWith(offlineText)).to.be.true; + expect(actual.tooltip).to.be.equal(offlineTooltip); + }); + }); +}); diff --git a/i18n/en.json b/i18n/en.json index e02f7eeb4..6278791be 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -173,6 +173,9 @@ "inaccessibleDirectory": "Could not access the sketchbook location at '{0}': {1}" } }, + "connectionStatus": { + "connectionLost": "Connection lost. Cloud Sketches actions and Updates won't be available" + }, "contributions": { "addFile": "Add File", "fileAdded": "One file added to the sketch.", @@ -488,6 +491,7 @@ "couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.", "daemonOffline": "CLI Daemon Offline", "offline": "Offline", + "offlineText": "Offline", "quitTitle": "Are you sure you want to quit?" }, "editor": { diff --git a/yarn.lock b/yarn.lock index e77dd480d..1be8a5928 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1191,6 +1191,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" + integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + "@lerna/add@6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-6.1.0.tgz#0f09495c5e1af4c4f316344af34b6d1a91b15b19" @@ -6422,6 +6427,20 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +dns-packet@^5.2.4: + version "5.4.0" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b" + integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +dns-socket@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/dns-socket/-/dns-socket-4.2.2.tgz#58b0186ec053ea0731feb06783c7eeac4b95b616" + integrity sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg== + dependencies: + dns-packet "^5.2.4" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -8082,6 +8101,23 @@ got@^11.7.0, got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" +got@^11.8.0: + version "11.8.6" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" + integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + got@^8.3.1: version "8.3.2" resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" @@ -8751,6 +8787,11 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== +ip-regex@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" + integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== + ip@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" @@ -8940,6 +8981,13 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-ip@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8" + integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q== + dependencies: + ip-regex "^4.0.0" + is-lambda@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" @@ -9008,6 +9056,16 @@ is-odd@^0.1.2: dependencies: is-number "^3.0.0" +is-online@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/is-online/-/is-online-9.0.1.tgz#71a34202fa826bae6f3ff8bea420c56573448a5f" + integrity sha512-+08dRW0dcFOtleR2N3rHRVxDyZtQitUp9cC+KpKTds0mXibbQyW5js7xX0UGyQXkaLUJObe0w6uQ4ex34lX9LA== + dependencies: + got "^11.8.0" + p-any "^3.0.0" + p-timeout "^3.2.0" + public-ip "^4.0.4" + is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -11444,6 +11502,14 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== +p-any@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-any/-/p-any-3.0.0.tgz#79847aeed70b5d3a10ea625296c0c3d2e90a87b9" + integrity sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w== + dependencies: + p-cancelable "^2.0.0" + p-some "^5.0.0" + p-cancelable@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" @@ -11575,6 +11641,14 @@ p-reduce@^2.0.0, p-reduce@^2.1.0: resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a" integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== +p-some@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-some/-/p-some-5.0.0.tgz#8b730c74b4fe5169d7264a240ad010b6ebc686a4" + integrity sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig== + dependencies: + aggregate-error "^3.0.0" + p-cancelable "^2.0.0" + p-timeout@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" @@ -12153,6 +12227,15 @@ psl@^1.1.28, psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +public-ip@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/public-ip/-/public-ip-4.0.4.tgz#b3784a5a1ff1b81d015b9a18450be65ffd929eb3" + integrity sha512-EJ0VMV2vF6Cu7BIPo3IMW1Maq6ME+fbR0NcPmqDfpfNGIRPue1X8QrGjrg/rfjDkOsIkKHIf2S5FlEa48hFMTA== + dependencies: + dns-socket "^4.2.2" + got "^9.6.0" + is-ip "^3.1.0" + pump@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"