diff --git a/packages/shared/components/Notification/Notification.tsx b/packages/shared/components/Notification/Notification.tsx index 02da659e5..dbd248ada 100644 --- a/packages/shared/components/Notification/Notification.tsx +++ b/packages/shared/components/Notification/Notification.tsx @@ -161,7 +161,6 @@ function getRenderedContent( { return Promise.resolve(undefined); } + + forceFocusWindow() {} } diff --git a/packages/teleterm/src/mainProcess/mainProcess.ts b/packages/teleterm/src/mainProcess/mainProcess.ts index 715370127..b8cf21bdf 100644 --- a/packages/teleterm/src/mainProcess/mainProcess.ts +++ b/packages/teleterm/src/mainProcess/mainProcess.ts @@ -26,12 +26,14 @@ import { import { subscribeToTerminalContextMenuEvent } from './contextMenus/terminalContextMenu'; import { subscribeToTabContextMenuEvent } from './contextMenus/tabContextMenu'; import { resolveNetworkAddress } from './resolveNetworkAddress'; +import { WindowsManager } from './windowsManager'; type Options = { settings: RuntimeSettings; logger: Logger; configService: ConfigService; fileStorage: FileStorage; + windowsManager: WindowsManager; }; export default class MainProcess { @@ -42,12 +44,14 @@ export default class MainProcess { private sharedProcess: ChildProcess; private fileStorage: FileStorage; private resolvedChildProcessAddresses: Promise; + private windowsManager: WindowsManager; private constructor(opts: Options) { this.settings = opts.settings; this.logger = opts.logger; this.configService = opts.configService; this.fileStorage = opts.fileStorage; + this.windowsManager = opts.windowsManager; } static create(opts: Options) { @@ -195,6 +199,10 @@ export default class MainProcess { }) ); + ipcMain.handle('main-process-force-focus-window', () => { + this.windowsManager.forceFocusWindow(); + }); + subscribeToTerminalContextMenuEvent(); subscribeToTabContextMenuEvent(); subscribeToConfigServiceEvents(this.configService); diff --git a/packages/teleterm/src/mainProcess/mainProcessClient.ts b/packages/teleterm/src/mainProcess/mainProcessClient.ts index 17e029a07..200a557bc 100644 --- a/packages/teleterm/src/mainProcess/mainProcessClient.ts +++ b/packages/teleterm/src/mainProcess/mainProcessClient.ts @@ -28,5 +28,8 @@ export default function createMainProcessClient(): MainProcessClient { removeKubeConfig(options) { return ipcRenderer.invoke('main-process-remove-kube-config', options); }, + forceFocusWindow() { + return ipcRenderer.invoke('main-process-force-focus-window'); + }, }; } diff --git a/packages/teleterm/src/mainProcess/types.ts b/packages/teleterm/src/mainProcess/types.ts index 2041a4d2a..bad89dc6e 100644 --- a/packages/teleterm/src/mainProcess/types.ts +++ b/packages/teleterm/src/mainProcess/types.ts @@ -41,6 +41,7 @@ export type MainProcessClient = { relativePath: string; isDirectory?: boolean; }): Promise; + forceFocusWindow(): void; }; export type ChildProcessAddresses = { diff --git a/packages/teleterm/src/mainProcess/windowsManager.ts b/packages/teleterm/src/mainProcess/windowsManager.ts index bdd18e6d9..d40b63256 100644 --- a/packages/teleterm/src/mainProcess/windowsManager.ts +++ b/packages/teleterm/src/mainProcess/windowsManager.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { BrowserWindow, Menu, Rectangle, screen } from 'electron'; +import { app, BrowserWindow, Menu, Rectangle, screen } from 'electron'; import { FileStorage } from 'teleterm/services/fileStorage'; import { RuntimeSettings } from 'teleterm/mainProcess/types'; @@ -78,6 +78,11 @@ export class WindowsManager { this.window = window; } + /** + * focusWindow is for situations where the app has privileges to do so, for example in a scenario + * where the user attempts to launch a second instance of the app – the same process that the user + * interacted with asks for its window to receive focus. + */ focusWindow(): void { if (!this.window) { return; @@ -90,6 +95,64 @@ export class WindowsManager { this.window.focus(); } + /** + * forceFocusWindow if for situations where Connect wants to essentially steal focus. + * + * One example would be 3rd party apps interacting with resources exposed by Connect, e.g. + * gateways. If the user attempts to make a connection through a gateway but the certs have + * expired, Connect should receive focus and show an appropriate message to the user. + */ + forceFocusWindow(): void { + if (!this.window) { + return; + } + + if (this.window.isFocused()) { + return; + } + + // What follows is a special focus handler for windows. + // + // On Windows, app.focus() doesn't work as expected so instead we call win.focus(). + // If the window is minimized, win.focus() will bring it to the front and give it focus. + // If the window is not minimized but simply covered by other another window, win.focus() will + // flash the icon of Connect in the task bar. + // + // Ideally, we'd like the not minimized window to receive focus too. We considered two + // workarounds to bring focus to a window that's not minimized: + // + // * win.minimized() followed by win.focus() – this reportedly doesn't work anymore (see the + // comment linked below) though it did work at the time of implementing forceFocusWindow. + // Admittedly, this seems like a hack and does cause the window to first minimize and then show + // up which feels weird. + // * win.setAlwaysOnTop(true) followed by win.show() – this does bring the window to the top + // but doesn't give it focus. Super awkward because Connect shows up over another app that you + // were using, you start typing to fill out whatever form Connect has shown you. But your + // keystrokes go to the app that the Connect window just covered. + // + // Since we cannot reliably steal focus, let's just not attempt to do it and instead defer to + // flashing the icon in the task bar. + // + // https://github.com/electron/electron/issues/2867#issuecomment-1080573240 + // + // I don't understand why calling win.focus() on a minimized window gives it focus in the + // first place. In theory it shouldn't work, see the links below: + // + // https://stackoverflow.com/a/72620653/742872 + // https://devblogs.microsoft.com/oldnewthing/20090220-00/?p=19083 + // https://github.com/electron/electron/issues/2867#issuecomment-142480964 + // https://github.com/electron/electron/issues/2867#issuecomment-142511956 + if (this.settings.platform === 'win32') { + this.window.focus(); + return; + } + + app.dock?.bounce('informational'); + // app.focus() alone doesn't un-minimize the window if the window is minimized. + this.window.show(); + app.focus({ steal: true }); + } + private saveWindowState(window: BrowserWindow): void { const windowState: WindowState = { ...window.getNormalBounds(), diff --git a/packages/teleterm/src/services/relogin.ts b/packages/teleterm/src/services/relogin.ts new file mode 100644 index 000000000..2047736ab --- /dev/null +++ b/packages/teleterm/src/services/relogin.ts @@ -0,0 +1,49 @@ +import { MainProcessClient } from 'teleterm/types'; +import { ReloginRequest } from 'teleterm/services/tshdEvents'; +import { + ModalsService, + ClusterConnectReason, +} from 'teleterm/ui/services/modals'; +import { ClustersService } from 'teleterm/ui/services/clusters'; + +export class ReloginService { + constructor( + private mainProcessClient: MainProcessClient, + private modalsService: ModalsService, + private clustersService: ClustersService + ) {} + + relogin( + request: ReloginRequest, + onRequestCancelled: (callback: () => void) => void + ): Promise { + this.mainProcessClient.forceFocusWindow(); + let reason: ClusterConnectReason; + + if (request.gatewayCertExpired) { + const gateway = this.clustersService.findGateway( + request.gatewayCertExpired.gatewayUri + ); + reason = { + kind: 'reason.gateway-cert-expired', + targetUri: request.gatewayCertExpired.targetUri, + gateway: gateway, + }; + } + + return new Promise((resolve, reject) => { + // GatewayCertReissuer in tshd makes sure that we only ever have one concurrent request to the + // relogin event. So at the moment, ReloginService won't ever call openImportantDialog twice. + const { closeDialog } = this.modalsService.openImportantDialog({ + kind: 'cluster-connect', + clusterUri: request.rootClusterUri, + reason, + onSuccess: () => resolve(), + onCancel: () => + reject(new Error('Login process was canceled by the user')), + }); + + onRequestCancelled(closeDialog); + }); + } +} diff --git a/packages/teleterm/src/services/tshd/createClient.ts b/packages/teleterm/src/services/tshd/createClient.ts index 14fbd02fa..bf74cc8bb 100644 --- a/packages/teleterm/src/services/tshd/createClient.ts +++ b/packages/teleterm/src/services/tshd/createClient.ts @@ -579,19 +579,6 @@ export default function createClient( }); }, - async restartGateway(gatewayUri = '') { - const req = new api.RestartGatewayRequest().setGatewayUri(gatewayUri); - return new Promise((resolve, reject) => { - tshd.restartGateway(req, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - }, - async setGatewayTargetSubresourceName( gatewayUri = '', targetSubresourceName = '' diff --git a/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/packages/teleterm/src/services/tshd/fixtures/mocks.ts index fa32b6d2d..6dab6ea46 100644 --- a/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -59,7 +59,6 @@ export class MockTshClient implements TshClient { listGateways: () => Promise; createGateway: (params: CreateGatewayParams) => Promise; removeGateway: (gatewayUri: string) => Promise; - restartGateway: (gatewayUri: string) => Promise; setGatewayTargetSubresourceName: ( gatewayUri: string, targetSubresourceName: string @@ -87,3 +86,15 @@ export class MockTshClient implements TshClient { logout: (clusterUri: string) => Promise; transferFile: () => undefined; } + +export const gateway: Gateway = { + uri: '/gateways/gateway1', + targetName: 'postgres', + targetUri: '/clusters/teleport-local/dbs/postgres', + targetUser: 'alice', + targetSubresourceName: '', + localAddress: 'localhost', + localPort: '59116', + protocol: 'postgres', + cliCommand: 'psql postgres://alice@localhost:59116', +}; diff --git a/packages/teleterm/src/services/tshd/types.ts b/packages/teleterm/src/services/tshd/types.ts index c25f5f5d3..41c1050f4 100644 --- a/packages/teleterm/src/services/tshd/types.ts +++ b/packages/teleterm/src/services/tshd/types.ts @@ -105,7 +105,6 @@ export type TshClient = { listGateways: () => Promise; createGateway: (params: CreateGatewayParams) => Promise; removeGateway: (gatewayUri: string) => Promise; - restartGateway: (gatewayUri: string) => Promise; setGatewayTargetSubresourceName: ( gatewayUri: string, targetSubresourceName: string diff --git a/packages/teleterm/src/services/tshd/v1/tshd_events_service_grpc_pb.d.ts b/packages/teleterm/src/services/tshd/v1/tshd_events_service_grpc_pb.d.ts index 6e10cb03d..6f2f5780f 100644 --- a/packages/teleterm/src/services/tshd/v1/tshd_events_service_grpc_pb.d.ts +++ b/packages/teleterm/src/services/tshd/v1/tshd_events_service_grpc_pb.d.ts @@ -8,34 +8,51 @@ import * as grpc from "grpc"; import * as v1_tshd_events_service_pb from "../v1/tshd_events_service_pb"; interface ITshdEventsServiceService extends grpc.ServiceDefinition { - test: ITshdEventsServiceService_ITest; + relogin: ITshdEventsServiceService_IRelogin; + sendNotification: ITshdEventsServiceService_ISendNotification; } -interface ITshdEventsServiceService_ITest extends grpc.MethodDefinition { - path: "/teleport.terminal.v1.TshdEventsService/Test"; +interface ITshdEventsServiceService_IRelogin extends grpc.MethodDefinition { + path: "/teleport.terminal.v1.TshdEventsService/Relogin"; requestStream: false; responseStream: false; - requestSerialize: grpc.serialize; - requestDeserialize: grpc.deserialize; - responseSerialize: grpc.serialize; - responseDeserialize: grpc.deserialize; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} +interface ITshdEventsServiceService_ISendNotification extends grpc.MethodDefinition { + path: "/teleport.terminal.v1.TshdEventsService/SendNotification"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; } export const TshdEventsServiceService: ITshdEventsServiceService; export interface ITshdEventsServiceServer { - test: grpc.handleUnaryCall; + relogin: grpc.handleUnaryCall; + sendNotification: grpc.handleUnaryCall; } export interface ITshdEventsServiceClient { - test(request: v1_tshd_events_service_pb.TestRequest, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.TestResponse) => void): grpc.ClientUnaryCall; - test(request: v1_tshd_events_service_pb.TestRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.TestResponse) => void): grpc.ClientUnaryCall; - test(request: v1_tshd_events_service_pb.TestRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.TestResponse) => void): grpc.ClientUnaryCall; + relogin(request: v1_tshd_events_service_pb.ReloginRequest, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.ReloginResponse) => void): grpc.ClientUnaryCall; + relogin(request: v1_tshd_events_service_pb.ReloginRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.ReloginResponse) => void): grpc.ClientUnaryCall; + relogin(request: v1_tshd_events_service_pb.ReloginRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.ReloginResponse) => void): grpc.ClientUnaryCall; + sendNotification(request: v1_tshd_events_service_pb.SendNotificationRequest, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.SendNotificationResponse) => void): grpc.ClientUnaryCall; + sendNotification(request: v1_tshd_events_service_pb.SendNotificationRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.SendNotificationResponse) => void): grpc.ClientUnaryCall; + sendNotification(request: v1_tshd_events_service_pb.SendNotificationRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.SendNotificationResponse) => void): grpc.ClientUnaryCall; } export class TshdEventsServiceClient extends grpc.Client implements ITshdEventsServiceClient { constructor(address: string, credentials: grpc.ChannelCredentials, options?: object); - public test(request: v1_tshd_events_service_pb.TestRequest, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.TestResponse) => void): grpc.ClientUnaryCall; - public test(request: v1_tshd_events_service_pb.TestRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.TestResponse) => void): grpc.ClientUnaryCall; - public test(request: v1_tshd_events_service_pb.TestRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.TestResponse) => void): grpc.ClientUnaryCall; + public relogin(request: v1_tshd_events_service_pb.ReloginRequest, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.ReloginResponse) => void): grpc.ClientUnaryCall; + public relogin(request: v1_tshd_events_service_pb.ReloginRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.ReloginResponse) => void): grpc.ClientUnaryCall; + public relogin(request: v1_tshd_events_service_pb.ReloginRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.ReloginResponse) => void): grpc.ClientUnaryCall; + public sendNotification(request: v1_tshd_events_service_pb.SendNotificationRequest, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.SendNotificationResponse) => void): grpc.ClientUnaryCall; + public sendNotification(request: v1_tshd_events_service_pb.SendNotificationRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.SendNotificationResponse) => void): grpc.ClientUnaryCall; + public sendNotification(request: v1_tshd_events_service_pb.SendNotificationRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_tshd_events_service_pb.SendNotificationResponse) => void): grpc.ClientUnaryCall; } diff --git a/packages/teleterm/src/services/tshd/v1/tshd_events_service_grpc_pb.js b/packages/teleterm/src/services/tshd/v1/tshd_events_service_grpc_pb.js index 6859c723e..9ef264c20 100644 --- a/packages/teleterm/src/services/tshd/v1/tshd_events_service_grpc_pb.js +++ b/packages/teleterm/src/services/tshd/v1/tshd_events_service_grpc_pb.js @@ -19,47 +19,80 @@ var grpc = require('@grpc/grpc-js'); var v1_tshd_events_service_pb = require('../v1/tshd_events_service_pb.js'); -function serialize_teleport_terminal_v1_TestRequest(arg) { - if (!(arg instanceof v1_tshd_events_service_pb.TestRequest)) { - throw new Error('Expected argument of type teleport.terminal.v1.TestRequest'); +function serialize_teleport_terminal_v1_ReloginRequest(arg) { + if (!(arg instanceof v1_tshd_events_service_pb.ReloginRequest)) { + throw new Error('Expected argument of type teleport.terminal.v1.ReloginRequest'); } return Buffer.from(arg.serializeBinary()); } -function deserialize_teleport_terminal_v1_TestRequest(buffer_arg) { - return v1_tshd_events_service_pb.TestRequest.deserializeBinary(new Uint8Array(buffer_arg)); +function deserialize_teleport_terminal_v1_ReloginRequest(buffer_arg) { + return v1_tshd_events_service_pb.ReloginRequest.deserializeBinary(new Uint8Array(buffer_arg)); } -function serialize_teleport_terminal_v1_TestResponse(arg) { - if (!(arg instanceof v1_tshd_events_service_pb.TestResponse)) { - throw new Error('Expected argument of type teleport.terminal.v1.TestResponse'); +function serialize_teleport_terminal_v1_ReloginResponse(arg) { + if (!(arg instanceof v1_tshd_events_service_pb.ReloginResponse)) { + throw new Error('Expected argument of type teleport.terminal.v1.ReloginResponse'); } return Buffer.from(arg.serializeBinary()); } -function deserialize_teleport_terminal_v1_TestResponse(buffer_arg) { - return v1_tshd_events_service_pb.TestResponse.deserializeBinary(new Uint8Array(buffer_arg)); +function deserialize_teleport_terminal_v1_ReloginResponse(buffer_arg) { + return v1_tshd_events_service_pb.ReloginResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_teleport_terminal_v1_SendNotificationRequest(arg) { + if (!(arg instanceof v1_tshd_events_service_pb.SendNotificationRequest)) { + throw new Error('Expected argument of type teleport.terminal.v1.SendNotificationRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_teleport_terminal_v1_SendNotificationRequest(buffer_arg) { + return v1_tshd_events_service_pb.SendNotificationRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_teleport_terminal_v1_SendNotificationResponse(arg) { + if (!(arg instanceof v1_tshd_events_service_pb.SendNotificationResponse)) { + throw new Error('Expected argument of type teleport.terminal.v1.SendNotificationResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_teleport_terminal_v1_SendNotificationResponse(buffer_arg) { + return v1_tshd_events_service_pb.SendNotificationResponse.deserializeBinary(new Uint8Array(buffer_arg)); } // TshdEventsService is served by the Electron app. The tsh daemon calls this service to notify the -// app about actions that happen outside of the app itself. For example, when the user tries to -// connect to a gateway served by the daemon but the cert has since expired and needs to be -// reissued. +// app about actions that happen outside of the app itself. var TshdEventsServiceService = exports.TshdEventsServiceService = { - // Test is an RPC that's used to demonstrate how the implementation of a tshd event may look like -// from the beginning till the end. -// TODO(ravicious): Remove this once we add an actual RPC to tshd events service. -test: { - path: '/teleport.terminal.v1.TshdEventsService/Test', + // Relogin makes the Electron app display a login modal for the specific root cluster. The request +// returns a response after the relogin procedure has been successfully finished. +relogin: { + path: '/teleport.terminal.v1.TshdEventsService/Relogin', + requestStream: false, + responseStream: false, + requestType: v1_tshd_events_service_pb.ReloginRequest, + responseType: v1_tshd_events_service_pb.ReloginResponse, + requestSerialize: serialize_teleport_terminal_v1_ReloginRequest, + requestDeserialize: deserialize_teleport_terminal_v1_ReloginRequest, + responseSerialize: serialize_teleport_terminal_v1_ReloginResponse, + responseDeserialize: deserialize_teleport_terminal_v1_ReloginResponse, + }, + // SendNotification causes the Electron app to display a notification in the UI. The request +// accepts a specific message rather than a generic string so that the Electron is in control as +// to what message is displayed and how exactly it looks. +sendNotification: { + path: '/teleport.terminal.v1.TshdEventsService/SendNotification', requestStream: false, responseStream: false, - requestType: v1_tshd_events_service_pb.TestRequest, - responseType: v1_tshd_events_service_pb.TestResponse, - requestSerialize: serialize_teleport_terminal_v1_TestRequest, - requestDeserialize: deserialize_teleport_terminal_v1_TestRequest, - responseSerialize: serialize_teleport_terminal_v1_TestResponse, - responseDeserialize: deserialize_teleport_terminal_v1_TestResponse, + requestType: v1_tshd_events_service_pb.SendNotificationRequest, + responseType: v1_tshd_events_service_pb.SendNotificationResponse, + requestSerialize: serialize_teleport_terminal_v1_SendNotificationRequest, + requestDeserialize: deserialize_teleport_terminal_v1_SendNotificationRequest, + responseSerialize: serialize_teleport_terminal_v1_SendNotificationResponse, + responseDeserialize: deserialize_teleport_terminal_v1_SendNotificationResponse, }, }; diff --git a/packages/teleterm/src/services/tshd/v1/tshd_events_service_pb.d.ts b/packages/teleterm/src/services/tshd/v1/tshd_events_service_pb.d.ts index 18c43d1d1..1c1f59330 100644 --- a/packages/teleterm/src/services/tshd/v1/tshd_events_service_pb.d.ts +++ b/packages/teleterm/src/services/tshd/v1/tshd_events_service_pb.d.ts @@ -6,40 +6,162 @@ import * as jspb from "google-protobuf"; -export class TestRequest extends jspb.Message { - getFoo(): string; - setFoo(value: string): TestRequest; +export class ReloginRequest extends jspb.Message { + getRootClusterUri(): string; + setRootClusterUri(value: string): ReloginRequest; + + + hasGatewayCertExpired(): boolean; + clearGatewayCertExpired(): void; + getGatewayCertExpired(): GatewayCertExpired | undefined; + setGatewayCertExpired(value?: GatewayCertExpired): ReloginRequest; + + + getReasonCase(): ReloginRequest.ReasonCase; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ReloginRequest.AsObject; + static toObject(includeInstance: boolean, msg: ReloginRequest): ReloginRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ReloginRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ReloginRequest; + static deserializeBinaryFromReader(message: ReloginRequest, reader: jspb.BinaryReader): ReloginRequest; +} + +export namespace ReloginRequest { + export type AsObject = { + rootClusterUri: string, + gatewayCertExpired?: GatewayCertExpired.AsObject, + } + + export enum ReasonCase { + REASON_NOT_SET = 0, + + GATEWAY_CERT_EXPIRED = 2, + + } + +} + +export class GatewayCertExpired extends jspb.Message { + getGatewayUri(): string; + setGatewayUri(value: string): GatewayCertExpired; + + getTargetUri(): string; + setTargetUri(value: string): GatewayCertExpired; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): GatewayCertExpired.AsObject; + static toObject(includeInstance: boolean, msg: GatewayCertExpired): GatewayCertExpired.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: GatewayCertExpired, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): GatewayCertExpired; + static deserializeBinaryFromReader(message: GatewayCertExpired, reader: jspb.BinaryReader): GatewayCertExpired; +} + +export namespace GatewayCertExpired { + export type AsObject = { + gatewayUri: string, + targetUri: string, + } +} + +export class ReloginResponse extends jspb.Message { + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ReloginResponse.AsObject; + static toObject(includeInstance: boolean, msg: ReloginResponse): ReloginResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ReloginResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ReloginResponse; + static deserializeBinaryFromReader(message: ReloginResponse, reader: jspb.BinaryReader): ReloginResponse; +} + +export namespace ReloginResponse { + export type AsObject = { + } +} + +export class SendNotificationRequest extends jspb.Message { + + hasCannotProxyGatewayConnection(): boolean; + clearCannotProxyGatewayConnection(): void; + getCannotProxyGatewayConnection(): CannotProxyGatewayConnection | undefined; + setCannotProxyGatewayConnection(value?: CannotProxyGatewayConnection): SendNotificationRequest; + + + getSubjectCase(): SendNotificationRequest.SubjectCase; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): SendNotificationRequest.AsObject; + static toObject(includeInstance: boolean, msg: SendNotificationRequest): SendNotificationRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: SendNotificationRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): SendNotificationRequest; + static deserializeBinaryFromReader(message: SendNotificationRequest, reader: jspb.BinaryReader): SendNotificationRequest; +} + +export namespace SendNotificationRequest { + export type AsObject = { + cannotProxyGatewayConnection?: CannotProxyGatewayConnection.AsObject, + } + + export enum SubjectCase { + SUBJECT_NOT_SET = 0, + + CANNOT_PROXY_GATEWAY_CONNECTION = 1, + + } + +} + +export class CannotProxyGatewayConnection extends jspb.Message { + getGatewayUri(): string; + setGatewayUri(value: string): CannotProxyGatewayConnection; + + getTargetUri(): string; + setTargetUri(value: string): CannotProxyGatewayConnection; + + getError(): string; + setError(value: string): CannotProxyGatewayConnection; serializeBinary(): Uint8Array; - toObject(includeInstance?: boolean): TestRequest.AsObject; - static toObject(includeInstance: boolean, msg: TestRequest): TestRequest.AsObject; + toObject(includeInstance?: boolean): CannotProxyGatewayConnection.AsObject; + static toObject(includeInstance: boolean, msg: CannotProxyGatewayConnection): CannotProxyGatewayConnection.AsObject; static extensions: {[key: number]: jspb.ExtensionFieldInfo}; static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; - static serializeBinaryToWriter(message: TestRequest, writer: jspb.BinaryWriter): void; - static deserializeBinary(bytes: Uint8Array): TestRequest; - static deserializeBinaryFromReader(message: TestRequest, reader: jspb.BinaryReader): TestRequest; + static serializeBinaryToWriter(message: CannotProxyGatewayConnection, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): CannotProxyGatewayConnection; + static deserializeBinaryFromReader(message: CannotProxyGatewayConnection, reader: jspb.BinaryReader): CannotProxyGatewayConnection; } -export namespace TestRequest { +export namespace CannotProxyGatewayConnection { export type AsObject = { - foo: string, + gatewayUri: string, + targetUri: string, + error: string, } } -export class TestResponse extends jspb.Message { +export class SendNotificationResponse extends jspb.Message { serializeBinary(): Uint8Array; - toObject(includeInstance?: boolean): TestResponse.AsObject; - static toObject(includeInstance: boolean, msg: TestResponse): TestResponse.AsObject; + toObject(includeInstance?: boolean): SendNotificationResponse.AsObject; + static toObject(includeInstance: boolean, msg: SendNotificationResponse): SendNotificationResponse.AsObject; static extensions: {[key: number]: jspb.ExtensionFieldInfo}; static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; - static serializeBinaryToWriter(message: TestResponse, writer: jspb.BinaryWriter): void; - static deserializeBinary(bytes: Uint8Array): TestResponse; - static deserializeBinaryFromReader(message: TestResponse, reader: jspb.BinaryReader): TestResponse; + static serializeBinaryToWriter(message: SendNotificationResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): SendNotificationResponse; + static deserializeBinaryFromReader(message: SendNotificationResponse, reader: jspb.BinaryReader): SendNotificationResponse; } -export namespace TestResponse { +export namespace SendNotificationResponse { export type AsObject = { } } diff --git a/packages/teleterm/src/services/tshd/v1/tshd_events_service_pb.js b/packages/teleterm/src/services/tshd/v1/tshd_events_service_pb.js index b7416effd..972ae81ea 100644 --- a/packages/teleterm/src/services/tshd/v1/tshd_events_service_pb.js +++ b/packages/teleterm/src/services/tshd/v1/tshd_events_service_pb.js @@ -15,8 +15,14 @@ var jspb = require('google-protobuf'); var goog = jspb; var global = (function() { return this || window || global || self || Function('return this')(); }).call(null); -goog.exportSymbol('proto.teleport.terminal.v1.TestRequest', null, global); -goog.exportSymbol('proto.teleport.terminal.v1.TestResponse', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.CannotProxyGatewayConnection', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.GatewayCertExpired', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.ReloginRequest', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.ReloginRequest.ReasonCase', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.ReloginResponse', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.SendNotificationRequest', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.SendNotificationRequest.SubjectCase', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.SendNotificationResponse', null, global); /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -27,16 +33,100 @@ goog.exportSymbol('proto.teleport.terminal.v1.TestResponse', null, global); * @extends {jspb.Message} * @constructor */ -proto.teleport.terminal.v1.TestRequest = function(opt_data) { +proto.teleport.terminal.v1.ReloginRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.teleport.terminal.v1.ReloginRequest.oneofGroups_); +}; +goog.inherits(proto.teleport.terminal.v1.ReloginRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.ReloginRequest.displayName = 'proto.teleport.terminal.v1.ReloginRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.GatewayCertExpired = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.teleport.terminal.v1.GatewayCertExpired, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.GatewayCertExpired.displayName = 'proto.teleport.terminal.v1.GatewayCertExpired'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.ReloginResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.teleport.terminal.v1.ReloginResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.ReloginResponse.displayName = 'proto.teleport.terminal.v1.ReloginResponse'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.SendNotificationRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.teleport.terminal.v1.SendNotificationRequest.oneofGroups_); +}; +goog.inherits(proto.teleport.terminal.v1.SendNotificationRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.SendNotificationRequest.displayName = 'proto.teleport.terminal.v1.SendNotificationRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection = function(opt_data) { jspb.Message.initialize(this, opt_data, 0, -1, null, null); }; -goog.inherits(proto.teleport.terminal.v1.TestRequest, jspb.Message); +goog.inherits(proto.teleport.terminal.v1.CannotProxyGatewayConnection, jspb.Message); if (goog.DEBUG && !COMPILED) { /** * @public * @override */ - proto.teleport.terminal.v1.TestRequest.displayName = 'proto.teleport.terminal.v1.TestRequest'; + proto.teleport.terminal.v1.CannotProxyGatewayConnection.displayName = 'proto.teleport.terminal.v1.CannotProxyGatewayConnection'; } /** * Generated by JsPbCodeGenerator. @@ -48,19 +138,225 @@ if (goog.DEBUG && !COMPILED) { * @extends {jspb.Message} * @constructor */ -proto.teleport.terminal.v1.TestResponse = function(opt_data) { +proto.teleport.terminal.v1.SendNotificationResponse = function(opt_data) { jspb.Message.initialize(this, opt_data, 0, -1, null, null); }; -goog.inherits(proto.teleport.terminal.v1.TestResponse, jspb.Message); +goog.inherits(proto.teleport.terminal.v1.SendNotificationResponse, jspb.Message); if (goog.DEBUG && !COMPILED) { /** * @public * @override */ - proto.teleport.terminal.v1.TestResponse.displayName = 'proto.teleport.terminal.v1.TestResponse'; + proto.teleport.terminal.v1.SendNotificationResponse.displayName = 'proto.teleport.terminal.v1.SendNotificationResponse'; +} + +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.teleport.terminal.v1.ReloginRequest.oneofGroups_ = [[2]]; + +/** + * @enum {number} + */ +proto.teleport.terminal.v1.ReloginRequest.ReasonCase = { + REASON_NOT_SET: 0, + GATEWAY_CERT_EXPIRED: 2 +}; + +/** + * @return {proto.teleport.terminal.v1.ReloginRequest.ReasonCase} + */ +proto.teleport.terminal.v1.ReloginRequest.prototype.getReasonCase = function() { + return /** @type {proto.teleport.terminal.v1.ReloginRequest.ReasonCase} */(jspb.Message.computeOneofCase(this, proto.teleport.terminal.v1.ReloginRequest.oneofGroups_[0])); +}; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.ReloginRequest.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.ReloginRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.ReloginRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ReloginRequest.toObject = function(includeInstance, msg) { + var f, obj = { + rootClusterUri: jspb.Message.getFieldWithDefault(msg, 1, ""), + gatewayCertExpired: (f = msg.getGatewayCertExpired()) && proto.teleport.terminal.v1.GatewayCertExpired.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; } +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.ReloginRequest} + */ +proto.teleport.terminal.v1.ReloginRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.ReloginRequest; + return proto.teleport.terminal.v1.ReloginRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.ReloginRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.ReloginRequest} + */ +proto.teleport.terminal.v1.ReloginRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setRootClusterUri(value); + break; + case 2: + var value = new proto.teleport.terminal.v1.GatewayCertExpired; + reader.readMessage(value,proto.teleport.terminal.v1.GatewayCertExpired.deserializeBinaryFromReader); + msg.setGatewayCertExpired(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.ReloginRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.ReloginRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.ReloginRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ReloginRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getRootClusterUri(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getGatewayCertExpired(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.teleport.terminal.v1.GatewayCertExpired.serializeBinaryToWriter + ); + } +}; + + +/** + * optional string root_cluster_uri = 1; + * @return {string} + */ +proto.teleport.terminal.v1.ReloginRequest.prototype.getRootClusterUri = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.terminal.v1.ReloginRequest} returns this + */ +proto.teleport.terminal.v1.ReloginRequest.prototype.setRootClusterUri = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional GatewayCertExpired gateway_cert_expired = 2; + * @return {?proto.teleport.terminal.v1.GatewayCertExpired} + */ +proto.teleport.terminal.v1.ReloginRequest.prototype.getGatewayCertExpired = function() { + return /** @type{?proto.teleport.terminal.v1.GatewayCertExpired} */ ( + jspb.Message.getWrapperField(this, proto.teleport.terminal.v1.GatewayCertExpired, 2)); +}; + + +/** + * @param {?proto.teleport.terminal.v1.GatewayCertExpired|undefined} value + * @return {!proto.teleport.terminal.v1.ReloginRequest} returns this +*/ +proto.teleport.terminal.v1.ReloginRequest.prototype.setGatewayCertExpired = function(value) { + return jspb.Message.setOneofWrapperField(this, 2, proto.teleport.terminal.v1.ReloginRequest.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.teleport.terminal.v1.ReloginRequest} returns this + */ +proto.teleport.terminal.v1.ReloginRequest.prototype.clearGatewayCertExpired = function() { + return this.setGatewayCertExpired(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.teleport.terminal.v1.ReloginRequest.prototype.hasGatewayCertExpired = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** @@ -75,8 +371,8 @@ if (jspb.Message.GENERATE_TO_OBJECT) { * http://goto/soy-param-migration * @return {!Object} */ -proto.teleport.terminal.v1.TestRequest.prototype.toObject = function(opt_includeInstance) { - return proto.teleport.terminal.v1.TestRequest.toObject(opt_includeInstance, this); +proto.teleport.terminal.v1.GatewayCertExpired.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.GatewayCertExpired.toObject(opt_includeInstance, this); }; @@ -85,13 +381,14 @@ proto.teleport.terminal.v1.TestRequest.prototype.toObject = function(opt_include * @param {boolean|undefined} includeInstance Deprecated. Whether to include * the JSPB instance for transitional soy proto support: * http://goto/soy-param-migration - * @param {!proto.teleport.terminal.v1.TestRequest} msg The msg instance to transform. + * @param {!proto.teleport.terminal.v1.GatewayCertExpired} msg The msg instance to transform. * @return {!Object} * @suppress {unusedLocalVariables} f is only used for nested messages */ -proto.teleport.terminal.v1.TestRequest.toObject = function(includeInstance, msg) { +proto.teleport.terminal.v1.GatewayCertExpired.toObject = function(includeInstance, msg) { var f, obj = { - foo: jspb.Message.getFieldWithDefault(msg, 1, "") + gatewayUri: jspb.Message.getFieldWithDefault(msg, 1, ""), + targetUri: jspb.Message.getFieldWithDefault(msg, 2, "") }; if (includeInstance) { @@ -105,23 +402,23 @@ proto.teleport.terminal.v1.TestRequest.toObject = function(includeInstance, msg) /** * Deserializes binary data (in protobuf wire format). * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.teleport.terminal.v1.TestRequest} + * @return {!proto.teleport.terminal.v1.GatewayCertExpired} */ -proto.teleport.terminal.v1.TestRequest.deserializeBinary = function(bytes) { +proto.teleport.terminal.v1.GatewayCertExpired.deserializeBinary = function(bytes) { var reader = new jspb.BinaryReader(bytes); - var msg = new proto.teleport.terminal.v1.TestRequest; - return proto.teleport.terminal.v1.TestRequest.deserializeBinaryFromReader(msg, reader); + var msg = new proto.teleport.terminal.v1.GatewayCertExpired; + return proto.teleport.terminal.v1.GatewayCertExpired.deserializeBinaryFromReader(msg, reader); }; /** * Deserializes binary data (in protobuf wire format) from the * given reader into the given message object. - * @param {!proto.teleport.terminal.v1.TestRequest} msg The message object to deserialize into. + * @param {!proto.teleport.terminal.v1.GatewayCertExpired} msg The message object to deserialize into. * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.teleport.terminal.v1.TestRequest} + * @return {!proto.teleport.terminal.v1.GatewayCertExpired} */ -proto.teleport.terminal.v1.TestRequest.deserializeBinaryFromReader = function(msg, reader) { +proto.teleport.terminal.v1.GatewayCertExpired.deserializeBinaryFromReader = function(msg, reader) { while (reader.nextField()) { if (reader.isEndGroup()) { break; @@ -130,7 +427,11 @@ proto.teleport.terminal.v1.TestRequest.deserializeBinaryFromReader = function(ms switch (field) { case 1: var value = /** @type {string} */ (reader.readString()); - msg.setFoo(value); + msg.setGatewayUri(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setTargetUri(value); break; default: reader.skipField(); @@ -145,9 +446,9 @@ proto.teleport.terminal.v1.TestRequest.deserializeBinaryFromReader = function(ms * Serializes the message to binary data (in protobuf wire format). * @return {!Uint8Array} */ -proto.teleport.terminal.v1.TestRequest.prototype.serializeBinary = function() { +proto.teleport.terminal.v1.GatewayCertExpired.prototype.serializeBinary = function() { var writer = new jspb.BinaryWriter(); - proto.teleport.terminal.v1.TestRequest.serializeBinaryToWriter(this, writer); + proto.teleport.terminal.v1.GatewayCertExpired.serializeBinaryToWriter(this, writer); return writer.getResultBuffer(); }; @@ -155,40 +456,532 @@ proto.teleport.terminal.v1.TestRequest.prototype.serializeBinary = function() { /** * Serializes the given message to binary data (in protobuf wire * format), writing to the given BinaryWriter. - * @param {!proto.teleport.terminal.v1.TestRequest} message + * @param {!proto.teleport.terminal.v1.GatewayCertExpired} message * @param {!jspb.BinaryWriter} writer * @suppress {unusedLocalVariables} f is only used for nested messages */ -proto.teleport.terminal.v1.TestRequest.serializeBinaryToWriter = function(message, writer) { +proto.teleport.terminal.v1.GatewayCertExpired.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getFoo(); + f = message.getGatewayUri(); if (f.length > 0) { writer.writeString( 1, f ); } + f = message.getTargetUri(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } }; /** - * optional string foo = 1; + * optional string gateway_uri = 1; * @return {string} */ -proto.teleport.terminal.v1.TestRequest.prototype.getFoo = function() { +proto.teleport.terminal.v1.GatewayCertExpired.prototype.getGatewayUri = function() { return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); }; /** * @param {string} value - * @return {!proto.teleport.terminal.v1.TestRequest} returns this + * @return {!proto.teleport.terminal.v1.GatewayCertExpired} returns this */ -proto.teleport.terminal.v1.TestRequest.prototype.setFoo = function(value) { +proto.teleport.terminal.v1.GatewayCertExpired.prototype.setGatewayUri = function(value) { return jspb.Message.setProto3StringField(this, 1, value); }; +/** + * optional string target_uri = 2; + * @return {string} + */ +proto.teleport.terminal.v1.GatewayCertExpired.prototype.getTargetUri = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.terminal.v1.GatewayCertExpired} returns this + */ +proto.teleport.terminal.v1.GatewayCertExpired.prototype.setTargetUri = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.ReloginResponse.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.ReloginResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.ReloginResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ReloginResponse.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.ReloginResponse} + */ +proto.teleport.terminal.v1.ReloginResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.ReloginResponse; + return proto.teleport.terminal.v1.ReloginResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.ReloginResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.ReloginResponse} + */ +proto.teleport.terminal.v1.ReloginResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.ReloginResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.ReloginResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.ReloginResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ReloginResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.teleport.terminal.v1.SendNotificationRequest.oneofGroups_ = [[1]]; + +/** + * @enum {number} + */ +proto.teleport.terminal.v1.SendNotificationRequest.SubjectCase = { + SUBJECT_NOT_SET: 0, + CANNOT_PROXY_GATEWAY_CONNECTION: 1 +}; + +/** + * @return {proto.teleport.terminal.v1.SendNotificationRequest.SubjectCase} + */ +proto.teleport.terminal.v1.SendNotificationRequest.prototype.getSubjectCase = function() { + return /** @type {proto.teleport.terminal.v1.SendNotificationRequest.SubjectCase} */(jspb.Message.computeOneofCase(this, proto.teleport.terminal.v1.SendNotificationRequest.oneofGroups_[0])); +}; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.SendNotificationRequest.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.SendNotificationRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.SendNotificationRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.SendNotificationRequest.toObject = function(includeInstance, msg) { + var f, obj = { + cannotProxyGatewayConnection: (f = msg.getCannotProxyGatewayConnection()) && proto.teleport.terminal.v1.CannotProxyGatewayConnection.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.SendNotificationRequest} + */ +proto.teleport.terminal.v1.SendNotificationRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.SendNotificationRequest; + return proto.teleport.terminal.v1.SendNotificationRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.SendNotificationRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.SendNotificationRequest} + */ +proto.teleport.terminal.v1.SendNotificationRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.teleport.terminal.v1.CannotProxyGatewayConnection; + reader.readMessage(value,proto.teleport.terminal.v1.CannotProxyGatewayConnection.deserializeBinaryFromReader); + msg.setCannotProxyGatewayConnection(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.SendNotificationRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.SendNotificationRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.SendNotificationRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.SendNotificationRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getCannotProxyGatewayConnection(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.teleport.terminal.v1.CannotProxyGatewayConnection.serializeBinaryToWriter + ); + } +}; + + +/** + * optional CannotProxyGatewayConnection cannot_proxy_gateway_connection = 1; + * @return {?proto.teleport.terminal.v1.CannotProxyGatewayConnection} + */ +proto.teleport.terminal.v1.SendNotificationRequest.prototype.getCannotProxyGatewayConnection = function() { + return /** @type{?proto.teleport.terminal.v1.CannotProxyGatewayConnection} */ ( + jspb.Message.getWrapperField(this, proto.teleport.terminal.v1.CannotProxyGatewayConnection, 1)); +}; + + +/** + * @param {?proto.teleport.terminal.v1.CannotProxyGatewayConnection|undefined} value + * @return {!proto.teleport.terminal.v1.SendNotificationRequest} returns this +*/ +proto.teleport.terminal.v1.SendNotificationRequest.prototype.setCannotProxyGatewayConnection = function(value) { + return jspb.Message.setOneofWrapperField(this, 1, proto.teleport.terminal.v1.SendNotificationRequest.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.teleport.terminal.v1.SendNotificationRequest} returns this + */ +proto.teleport.terminal.v1.SendNotificationRequest.prototype.clearCannotProxyGatewayConnection = function() { + return this.setCannotProxyGatewayConnection(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.teleport.terminal.v1.SendNotificationRequest.prototype.hasCannotProxyGatewayConnection = function() { + return jspb.Message.getField(this, 1) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.CannotProxyGatewayConnection.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.CannotProxyGatewayConnection} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.toObject = function(includeInstance, msg) { + var f, obj = { + gatewayUri: jspb.Message.getFieldWithDefault(msg, 1, ""), + targetUri: jspb.Message.getFieldWithDefault(msg, 2, ""), + error: jspb.Message.getFieldWithDefault(msg, 3, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.CannotProxyGatewayConnection} + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.CannotProxyGatewayConnection; + return proto.teleport.terminal.v1.CannotProxyGatewayConnection.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.CannotProxyGatewayConnection} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.CannotProxyGatewayConnection} + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setGatewayUri(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setTargetUri(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setError(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.CannotProxyGatewayConnection.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.CannotProxyGatewayConnection} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getGatewayUri(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getTargetUri(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } + f = message.getError(); + if (f.length > 0) { + writer.writeString( + 3, + f + ); + } +}; + + +/** + * optional string gateway_uri = 1; + * @return {string} + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.prototype.getGatewayUri = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.terminal.v1.CannotProxyGatewayConnection} returns this + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.prototype.setGatewayUri = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string target_uri = 2; + * @return {string} + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.prototype.getTargetUri = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.terminal.v1.CannotProxyGatewayConnection} returns this + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.prototype.setTargetUri = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + +/** + * optional string error = 3; + * @return {string} + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.prototype.getError = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.terminal.v1.CannotProxyGatewayConnection} returns this + */ +proto.teleport.terminal.v1.CannotProxyGatewayConnection.prototype.setError = function(value) { + return jspb.Message.setProto3StringField(this, 3, value); +}; + + @@ -205,8 +998,8 @@ if (jspb.Message.GENERATE_TO_OBJECT) { * http://goto/soy-param-migration * @return {!Object} */ -proto.teleport.terminal.v1.TestResponse.prototype.toObject = function(opt_includeInstance) { - return proto.teleport.terminal.v1.TestResponse.toObject(opt_includeInstance, this); +proto.teleport.terminal.v1.SendNotificationResponse.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.SendNotificationResponse.toObject(opt_includeInstance, this); }; @@ -215,11 +1008,11 @@ proto.teleport.terminal.v1.TestResponse.prototype.toObject = function(opt_includ * @param {boolean|undefined} includeInstance Deprecated. Whether to include * the JSPB instance for transitional soy proto support: * http://goto/soy-param-migration - * @param {!proto.teleport.terminal.v1.TestResponse} msg The msg instance to transform. + * @param {!proto.teleport.terminal.v1.SendNotificationResponse} msg The msg instance to transform. * @return {!Object} * @suppress {unusedLocalVariables} f is only used for nested messages */ -proto.teleport.terminal.v1.TestResponse.toObject = function(includeInstance, msg) { +proto.teleport.terminal.v1.SendNotificationResponse.toObject = function(includeInstance, msg) { var f, obj = { }; @@ -235,23 +1028,23 @@ proto.teleport.terminal.v1.TestResponse.toObject = function(includeInstance, msg /** * Deserializes binary data (in protobuf wire format). * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.teleport.terminal.v1.TestResponse} + * @return {!proto.teleport.terminal.v1.SendNotificationResponse} */ -proto.teleport.terminal.v1.TestResponse.deserializeBinary = function(bytes) { +proto.teleport.terminal.v1.SendNotificationResponse.deserializeBinary = function(bytes) { var reader = new jspb.BinaryReader(bytes); - var msg = new proto.teleport.terminal.v1.TestResponse; - return proto.teleport.terminal.v1.TestResponse.deserializeBinaryFromReader(msg, reader); + var msg = new proto.teleport.terminal.v1.SendNotificationResponse; + return proto.teleport.terminal.v1.SendNotificationResponse.deserializeBinaryFromReader(msg, reader); }; /** * Deserializes binary data (in protobuf wire format) from the * given reader into the given message object. - * @param {!proto.teleport.terminal.v1.TestResponse} msg The message object to deserialize into. + * @param {!proto.teleport.terminal.v1.SendNotificationResponse} msg The message object to deserialize into. * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.teleport.terminal.v1.TestResponse} + * @return {!proto.teleport.terminal.v1.SendNotificationResponse} */ -proto.teleport.terminal.v1.TestResponse.deserializeBinaryFromReader = function(msg, reader) { +proto.teleport.terminal.v1.SendNotificationResponse.deserializeBinaryFromReader = function(msg, reader) { while (reader.nextField()) { if (reader.isEndGroup()) { break; @@ -271,9 +1064,9 @@ proto.teleport.terminal.v1.TestResponse.deserializeBinaryFromReader = function(m * Serializes the message to binary data (in protobuf wire format). * @return {!Uint8Array} */ -proto.teleport.terminal.v1.TestResponse.prototype.serializeBinary = function() { +proto.teleport.terminal.v1.SendNotificationResponse.prototype.serializeBinary = function() { var writer = new jspb.BinaryWriter(); - proto.teleport.terminal.v1.TestResponse.serializeBinaryToWriter(this, writer); + proto.teleport.terminal.v1.SendNotificationResponse.serializeBinaryToWriter(this, writer); return writer.getResultBuffer(); }; @@ -281,11 +1074,11 @@ proto.teleport.terminal.v1.TestResponse.prototype.serializeBinary = function() { /** * Serializes the given message to binary data (in protobuf wire * format), writing to the given BinaryWriter. - * @param {!proto.teleport.terminal.v1.TestResponse} message + * @param {!proto.teleport.terminal.v1.SendNotificationResponse} message * @param {!jspb.BinaryWriter} writer * @suppress {unusedLocalVariables} f is only used for nested messages */ -proto.teleport.terminal.v1.TestResponse.serializeBinaryToWriter = function(message, writer) { +proto.teleport.terminal.v1.SendNotificationResponse.serializeBinaryToWriter = function(message, writer) { var f = undefined; }; diff --git a/packages/teleterm/src/services/tshdEvents/index.ts b/packages/teleterm/src/services/tshdEvents/index.ts index 97097e14c..1a4dbc741 100644 --- a/packages/teleterm/src/services/tshdEvents/index.ts +++ b/packages/teleterm/src/services/tshdEvents/index.ts @@ -1,5 +1,4 @@ -import { EventEmitter } from 'node:events'; - +import Emittery from 'emittery'; import * as grpc from '@grpc/grpc-js'; import * as api from 'teleterm/services/tshd/v1/tshd_events_service_pb'; @@ -7,6 +6,9 @@ import * as apiService from 'teleterm/services/tshd/v1/tshd_events_service_grpc_ import Logger from 'teleterm/logger'; import { SubscribeToTshdEvent } from 'teleterm/types'; +export type ReloginRequest = api.ReloginRequest.AsObject; +export type SendNotificationRequest = api.SendNotificationRequest.AsObject; + /** * Starts tshd events server. * @return {Promise} Object containing the address the server is listening on and subscribeToEvent @@ -19,11 +21,13 @@ export async function createTshdEventsServer( resolvedAddress: string; subscribeToTshdEvent: SubscribeToTshdEvent; }> { + const logger = new Logger('tshd events'); const { server, resolvedAddress } = await createServer( requestedAddress, - credentials + credentials, + logger ); - const { service, subscribeToTshdEvent } = createService(); + const { service, subscribeToTshdEvent } = createService(logger); server.addService( apiService.TshdEventsServiceService, @@ -42,9 +46,9 @@ export async function createTshdEventsServer( async function createServer( requestedAddress: string, - credentials: grpc.ServerCredentials + credentials: grpc.ServerCredentials, + logger: Logger ): Promise<{ server: grpc.Server; resolvedAddress: string }> { - const logger = new Logger('tshd events'); const server = new grpc.Server(); // grpc-js requires us to pass localhost:port for TCP connections, @@ -86,21 +90,62 @@ async function createServer( // Instead, we create an event emitter and expose subscribeToEvent through the contextBridge. // subscribeToEvent lets UI code register a callback for a specific event. That callback receives // a simple JS object which can freely pass the contextBridge. -function createService(): { +// +// # Async behavior +// +// The callback can return a promise. The service will not return a response until all callbacks +// resolve. This lets us model behavior where tshd calls the Electron app and then blocks until it +// receives a response, in case the Electron app needs to do some work before we want to unblock +// tshd. +// +// If any of the callbacks return an error, the service will return that error immediately, without +// waiting for other listeners. +function createService(logger: Logger): { service: apiService.ITshdEventsServiceServer; subscribeToTshdEvent: SubscribeToTshdEvent; } { - const emitter = new EventEmitter(); + const emitter = new Emittery(); const subscribeToTshdEvent: SubscribeToTshdEvent = (eventName, listener) => { emitter.on(eventName, listener); }; const service: apiService.ITshdEventsServiceServer = { - // TODO(ravicious): Remove this once we add an actual RPC to tshd events service. - test: (call, callback) => { - emitter.emit('test', call.request.toObject()); - callback(null, new api.TestResponse()); + relogin: (call, callback) => { + const request = call.request.toObject(); + + logger.info('Emitting relogin', request); + + const onCancelled = (callback: () => void) => { + call.on('cancelled', callback); + }; + + emitter.emit('relogin', { request, onCancelled }).then( + () => { + callback(null, new api.ReloginResponse()); + }, + error => { + callback(error); + } + ); + }, + sendNotification: (call, callback) => { + const request = call.request.toObject(); + + logger.info('Emitting sendNotification', request); + + const onCancelled = (callback: () => void) => { + call.on('cancelled', callback); + }; + + emitter.emit('sendNotification', { request, onCancelled }).then( + () => { + callback(null, new api.SendNotificationResponse()); + }, + error => { + callback(error); + } + ); }, }; diff --git a/packages/teleterm/src/services/tshdNotifications.ts b/packages/teleterm/src/services/tshdNotifications.ts new file mode 100644 index 000000000..0a96606d3 --- /dev/null +++ b/packages/teleterm/src/services/tshdNotifications.ts @@ -0,0 +1,44 @@ +import { SendNotificationRequest } from 'teleterm/services/tshdEvents'; +import { ClustersService } from 'teleterm/ui/services/clusters'; +import { NotificationsService } from 'teleterm/ui/services/notifications'; +import { routing } from 'teleterm/ui/uri'; + +export class TshdNotificationsService { + constructor( + private notificationsService: NotificationsService, + private clustersService: ClustersService + ) {} + + sendNotification(request: SendNotificationRequest) { + if (request.cannotProxyGatewayConnection) { + const { gatewayUri, targetUri, error } = + request.cannotProxyGatewayConnection; + const gateway = this.clustersService.findGateway(gatewayUri); + const clusterName = routing.parseClusterName(targetUri); + let shortTargetDesc: string; + let longTargetDesc: string; + + if (gateway) { + shortTargetDesc = `${gateway.targetName} as ${gateway.targetUser}`; + longTargetDesc = shortTargetDesc; + } else { + const targetName = routing.parseDbUri(targetUri)?.params['dbId']; + + if (targetName) { + shortTargetDesc = targetName; + longTargetDesc = shortTargetDesc; + } else { + shortTargetDesc = 'a database server'; + longTargetDesc = `a database server under ${targetUri}`; + } + } + + const notificationContent = { + title: `Cannot connect to ${shortTargetDesc} (${clusterName})`, + description: `You tried to connect to ${longTargetDesc} but we encountered an unexpected error: ${error}`, + }; + + this.notificationsService.notifyError(notificationContent); + } + } +} diff --git a/packages/teleterm/src/types.ts b/packages/teleterm/src/types.ts index ebf4c415f..a4e33ebd0 100644 --- a/packages/teleterm/src/types.ts +++ b/packages/teleterm/src/types.ts @@ -15,24 +15,27 @@ export { AppearanceConfig, }; -// SubscribeToTshdEvent is a type of the subscribeToTshdEvent function which gets exposed to the -// renderer through the context bridge. -// -// A typical implementation of a gRPC service looks something like this: -// -// { -// nameOfTheRpc: (call, callback) => { -// const request = call.request.toObject() -// // Do something with the request fields… -// } -// } -// -// subscribeToTshdEvent lets you add a listener that's going to be called every time a client makes -// a particular RPC to the tshd events service. The listener receives the request converted to a -// simple JS object since classes cannot be passed through the context bridge. -// -// The SubscribeToTshdEvent type expresses all of this so that our subscribeToTshdEvent can stay -// type safe. +/** + * SubscribeToTshdEvent is a type of the subscribeToTshdEvent function which gets exposed to the + * renderer through the context bridge. + * + * A typical implementation of a gRPC service looks something like this: + * + * { + * nameOfTheRpc: (call, callback) => { + * call.onCancelled(() => { … }) + * const request = call.request.toObject() + * // Do something with the request fields… + * } + * } + * + * subscribeToTshdEvent lets you add a listener that's going to be called every time a client makes + * a particular RPC to the tshd events service. The listener receives the request converted to a + * simple JS object since classes cannot be passed through the context bridge. + * + * The SubscribeToTshdEvent type expresses all of this so that our subscribeToTshdEvent can stay + * type safe. + */ export type SubscribeToTshdEvent = < RpcName extends keyof ITshdEventsServiceServer, RpcHandler extends ITshdEventsServiceServer[RpcName], @@ -42,7 +45,10 @@ export type SubscribeToTshdEvent = < > >( eventName: RpcName, - listener: (request: RpcHandlerRequestObject) => void + listener: (eventData: { + request: RpcHandlerRequestObject; + onCancelled: (callback: () => void) => void; + }) => void | Promise ) => void; export type ElectronGlobals = { diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterConnect.tsx b/packages/teleterm/src/ui/ClusterConnect/ClusterConnect.tsx index 200c04228..b041aff1b 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterConnect.tsx +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterConnect.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import Dialog from 'design/Dialog'; import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { ClusterConnectReason } from 'teleterm/ui/services/modals'; import { ClusterAdd } from './ClusterAdd'; import { ClusterLogin } from './ClusterLogin'; @@ -38,6 +39,7 @@ export function ClusterConnect(props: ClusterConnectProps) { ) : ( props.onSuccess(clusterUri)} @@ -48,9 +50,8 @@ export function ClusterConnect(props: ClusterConnectProps) { } interface ClusterConnectProps { - clusterUri?: string; - + clusterUri: string | undefined; + reason: ClusterConnectReason | undefined; onCancel(): void; - onSuccess(clusterUri: string): void; } diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx index c0771e34f..915d881d6 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx @@ -21,14 +21,18 @@ import { Attempt } from 'shared/hooks/useAsync'; import * as types from 'teleterm/ui/services/clusters/types'; -import { ClusterLoginPresentation } from './ClusterLogin'; -import { State } from './useClusterLogin'; +import { gateway } from 'teleterm/services/tshd/fixtures/mocks'; + +import { + ClusterLoginPresentation, + ClusterLoginPresentationProps, +} from './ClusterLogin'; export default { title: 'Teleterm/ClusterLogin', }; -function makeProps(): State { +function makeProps(): ClusterLoginPresentationProps { return { shouldPromptSsoStatus: false, title: 'localhost', @@ -60,6 +64,7 @@ function makeProps(): State { onLoginWithSso: () => null, clearLoginAttempt: () => null, webauthnLogin: null, + reason: undefined, }; } @@ -111,6 +116,40 @@ export const LocalOnly = () => { ); }; +export const LocalOnlyWithReasonGatewayCertExpiredWithGateway = () => { + const props = makeProps(); + props.initAttempt.data.secondFactor = 'off'; + props.initAttempt.data.allowPasswordless = false; + props.reason = { + kind: 'reason.gateway-cert-expired', + targetUri: gateway.targetUri, + gateway: gateway, + }; + + return ( + + + + ); +}; + +export const LocalOnlyWithReasonGatewayCertExpiredWithoutGateway = () => { + const props = makeProps(); + props.initAttempt.data.secondFactor = 'off'; + props.initAttempt.data.allowPasswordless = false; + props.reason = { + kind: 'reason.gateway-cert-expired', + targetUri: gateway.targetUri, + gateway: undefined, + }; + + return ( + + + + ); +}; + export const SsoOnly = () => { const props = makeProps(); props.initAttempt.data.localAuthEnabled = false; diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx index 7e428fe89..b94b54ea4 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx @@ -18,21 +18,26 @@ import React from 'react'; import * as Alerts from 'design/Alert'; import { ButtonIcon, Text, Indicator, Box } from 'design'; import * as Icons from 'design/Icon'; - import { DialogHeader, DialogContent } from 'design/Dialog'; - import { PrimaryAuthType } from 'shared/services'; import { AuthSettings } from 'teleterm/ui/services/clusters/types'; +import { ClusterConnectReason } from 'teleterm/ui/services/modals'; +import { routing } from 'teleterm/ui/uri'; import LoginForm from './FormLogin'; import useClusterLogin, { State, Props } from './useClusterLogin'; -export function ClusterLogin(props: Props) { - const state = useClusterLogin(props); - return ; +export function ClusterLogin(props: Props & { reason: ClusterConnectReason }) { + const { reason, ...otherProps } = props; + const state = useClusterLogin(otherProps); + return ; } +export type ClusterLoginPresentationProps = State & { + reason: ClusterConnectReason; +}; + export function ClusterLoginPresentation({ title, initAttempt, @@ -46,7 +51,8 @@ export function ClusterLoginPresentation({ loggedInUserName, shouldPromptSsoStatus, webauthnLogin, -}: State) { + reason, +}: ClusterLoginPresentationProps) { return ( <> @@ -58,6 +64,8 @@ export function ClusterLoginPresentation({ + {reason && } + {initAttempt.status === 'error' && ( Unable to retrieve cluster auth preferences,{' '} @@ -101,3 +109,42 @@ function getPrimaryAuthType(auth: AuthSettings): PrimaryAuthType { return 'local'; } + +function Reason({ reason }: { reason: ClusterConnectReason }) { + switch (reason.kind) { + case 'reason.gateway-cert-expired': { + const { gateway, targetUri } = reason; + let $targetDesc: React.ReactFragment; + if (gateway) { + $targetDesc = ( + <> + {gateway.targetName} as{' '} + {gateway.targetUser} + + ); + } else { + const targetName = routing.parseDbUri(targetUri)?.params['dbId']; + + if (targetName) { + $targetDesc = {targetName}; + } else { + $targetDesc = ( + <> + a database server under {targetUri} + + ); + } + } + + return ( + + You tried to connect to {$targetDesc} but your session has expired. + Please log in to refresh the session. + + ); + } + default: { + return; + } + } +} diff --git a/packages/teleterm/src/ui/ModalsHost/ModalsHost.story.tsx b/packages/teleterm/src/ui/ModalsHost/ModalsHost.story.tsx new file mode 100644 index 000000000..30ad1e460 --- /dev/null +++ b/packages/teleterm/src/ui/ModalsHost/ModalsHost.story.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import { + DialogClusterLogout, + DialogDocumentsReopen, + ModalsService, +} from 'teleterm/ui/services/modals'; + +import ModalsHost from './ModalsHost'; + +export default { + title: 'Teleterm/ModalsHost', +}; + +const clusterLogoutDialog: DialogClusterLogout = { + kind: 'cluster-logout', + clusterUri: '/clusters/foo', + clusterTitle: 'Foo', +}; + +const documentsReopenDialog: DialogDocumentsReopen = { + kind: 'documents-reopen', + onConfirm: () => {}, + onCancel: () => {}, +}; + +const importantDialog = clusterLogoutDialog; +const regularDialog = documentsReopenDialog; + +export const RegularModal = () => { + const modalsService = new ModalsService(); + modalsService.openRegularDialog(regularDialog); + + const appContext = new MockAppContext(); + appContext.modalsService = modalsService; + + return ( + + + + ); +}; + +export const ImportantModal = () => { + const modalsService = new ModalsService(); + modalsService.openImportantDialog(importantDialog); + + const appContext = new MockAppContext(); + appContext.modalsService = modalsService; + + return ( + + + + ); +}; + +export const ImportantAndRegularModal = () => { + const modalsService = new ModalsService(); + modalsService.openRegularDialog(regularDialog); + modalsService.openImportantDialog(importantDialog); + + const appContext = new MockAppContext(); + appContext.modalsService = modalsService; + + return ( + + + + ); +}; diff --git a/packages/teleterm/src/ui/ModalsHost/ModalsHost.test.tsx b/packages/teleterm/src/ui/ModalsHost/ModalsHost.test.tsx new file mode 100644 index 000000000..ead5b820e --- /dev/null +++ b/packages/teleterm/src/ui/ModalsHost/ModalsHost.test.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { render, screen } from 'design/utils/testing'; + +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import { + DialogClusterLogout, + DialogDocumentsReopen, + ModalsService, +} from 'teleterm/ui/services/modals'; + +import ModalsHost from './ModalsHost'; + +const clusterLogoutDialog: DialogClusterLogout = { + kind: 'cluster-logout', + clusterUri: '/clusters/foo', + clusterTitle: 'Foo', +}; + +const documentsReopenDialog: DialogDocumentsReopen = { + kind: 'documents-reopen', + onConfirm: () => {}, + onCancel: () => {}, +}; + +jest.mock('teleterm/ui/ClusterLogout/ClusterLogout', () => { + const MockClusterLogout = () => ( +
+ ); + return MockClusterLogout; +}); + +jest.mock('teleterm/ui/DocumentsReopen', () => ({ + DocumentsReopen: () => ( +
+ ), +})); + +test('the important dialog is rendered above the regular dialog', () => { + const importantDialog = clusterLogoutDialog; + const regularDialog = documentsReopenDialog; + + const modalsService = new ModalsService(); + modalsService.openRegularDialog(regularDialog); + modalsService.openImportantDialog(importantDialog); + + const appContext = new MockAppContext(); + appContext.modalsService = modalsService; + + render( + + + + ); + + // The DOM testing library doesn't really allow us to test actual visibility in terms of the order + // of rendering, so we have to fall back to manually checking items in the array. + // https://github.com/testing-library/react-testing-library/issues/313 + const dialogs = screen.queryAllByTestId('mocked-dialog'); + + // The important dialog should be after the regular dialog in the DOM so that it's shown over the + // regular dialog. + expect(dialogs[0]).toHaveAttribute('data-dialog-kind', regularDialog.kind); + expect(dialogs[1]).toHaveAttribute('data-dialog-kind', importantDialog.kind); +}); diff --git a/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx b/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx index 17d53e8c3..090ff0e92 100644 --- a/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx +++ b/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx @@ -20,55 +20,69 @@ import { useAppContext } from 'teleterm/ui/appContextProvider'; import { ClusterConnect } from 'teleterm/ui/ClusterConnect'; import { DocumentsReopen } from 'teleterm/ui/DocumentsReopen'; +import { Dialog } from 'teleterm/ui/services/modals'; import ClusterLogout from '../ClusterLogout/ClusterLogout'; export default function ModalsHost() { const { modalsService } = useAppContext(); - const dialog = modalsService.useState(); + const { regular: regularDialog, important: importantDialog } = + modalsService.useState(); - const handleClose = () => modalsService.closeDialog(); + const closeRegularDialog = () => modalsService.closeRegularDialog(); + const closeImportantDialog = () => modalsService.closeImportantDialog(); - if (dialog.kind === 'cluster-connect') { - return ( - { - handleClose(); - dialog.onCancel?.(); - }} - onSuccess={clusterUri => { - handleClose(); - dialog.onSuccess(clusterUri); - }} - /> - ); - } - - if (dialog.kind === 'cluster-logout') { - return ( - - ); - } + return ( + <> + {renderDialog(regularDialog, closeRegularDialog)} + {renderDialog(importantDialog, closeImportantDialog)} + + ); +} - if (dialog.kind === 'documents-reopen') { - return ( - { - handleClose(); - dialog.onCancel(); - }} - onConfirm={() => { - handleClose(); - dialog.onConfirm(); - }} - /> - ); +function renderDialog(dialog: Dialog, handleClose: () => void) { + switch (dialog.kind) { + case 'cluster-connect': { + return ( + { + handleClose(); + dialog.onCancel?.(); + }} + onSuccess={clusterUri => { + handleClose(); + dialog.onSuccess(clusterUri); + }} + /> + ); + } + case 'cluster-logout': { + return ( + + ); + } + case 'documents-reopen': { + return ( + { + handleClose(); + dialog.onCancel(); + }} + onConfirm={() => { + handleClose(); + dialog.onConfirm(); + }} + /> + ); + } + default: { + return null; + } } - - return null; } diff --git a/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx b/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx index 56ffc8968..4387041fb 100644 --- a/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx +++ b/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx @@ -5,7 +5,6 @@ import { fireEvent, render } from 'design/utils/testing'; import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; import { Cluster } from 'teleterm/services/tshd/v1/cluster_pb'; import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; - import { IAppContext } from 'teleterm/ui/types'; import { ShareFeedback } from './ShareFeedback'; diff --git a/packages/teleterm/src/ui/TabHost/TabHost.tsx b/packages/teleterm/src/ui/TabHost/TabHost.tsx index 55a0db890..5cf123f3f 100644 --- a/packages/teleterm/src/ui/TabHost/TabHost.tsx +++ b/packages/teleterm/src/ui/TabHost/TabHost.tsx @@ -21,11 +21,8 @@ import { Flex } from 'design'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import * as types from 'teleterm/ui/services/workspacesService/documentsService/types'; import { Tabs } from 'teleterm/ui/Tabs'; - import { DocumentsRenderer } from 'teleterm/ui/Documents'; - -import AppContext from 'teleterm/ui/appContext'; - +import { IAppContext } from 'teleterm/ui/types'; import { useKeyboardShortcutFormatters } from 'teleterm/ui/services/keyboardShortcuts'; import { useTabShortcuts } from './useTabShortcuts'; @@ -43,7 +40,7 @@ export function TabHostContainer() { return ; } -export function TabHost({ ctx }: { ctx: AppContext }) { +export function TabHost({ ctx }: { ctx: IAppContext }) { const documentsService = ctx.workspacesService.getActiveWorkspaceDocumentService(); const activeDocument = documentsService?.getActive(); diff --git a/packages/teleterm/src/ui/appContext.ts b/packages/teleterm/src/ui/appContext.ts index 3405bac76..32f64dd12 100644 --- a/packages/teleterm/src/ui/appContext.ts +++ b/packages/teleterm/src/ui/appContext.ts @@ -14,7 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MainProcessClient, ElectronGlobals } from 'teleterm/types'; +import { + MainProcessClient, + ElectronGlobals, + SubscribeToTshdEvent, +} from 'teleterm/types'; import { ClustersService } from 'teleterm/ui/services/clusters'; import { ModalsService } from 'teleterm/ui/services/modals'; import { TerminalsService } from 'teleterm/ui/services/terminals'; @@ -25,6 +29,8 @@ import { KeyboardShortcutsService } from 'teleterm/ui/services/keyboardShortcuts import { WorkspacesService } from 'teleterm/ui/services/workspacesService/workspacesService'; import { NotificationsService } from 'teleterm/ui/services/notifications'; import { FileTransferService } from 'teleterm/ui/services/fileTransferClient'; +import { ReloginService } from 'teleterm/services/relogin'; +import { TshdNotificationsService } from 'teleterm/services/tshdNotifications'; import { CommandLauncher } from './commandLauncher'; import { IAppContext } from './types'; @@ -44,9 +50,26 @@ export default class AppContext implements IAppContext { connectionTracker: ConnectionTrackerService; fileTransferService: FileTransferService; resourcesService: ResourcesService; + /** + * subscribeToTshdEvent lets you add a listener that's going to be called every time a client + * makes a particular RPC to the tshd events service. The listener receives the request converted + * to a simple JS object since classes cannot be passed through the context bridge. + * + * @param {string} eventName - Name of the event. + * @param {function} listener - A function that gets called when a client calls the specific + * event. It accepts an object with two properties: + * + * - request is the request payload converted to a simple JS object. + * - onCancelled is a function which lets you register a callback which will be called when the + * request gets canceled by the client. + */ + subscribeToTshdEvent: SubscribeToTshdEvent; + reloginService: ReloginService; + tshdNotificationsService: TshdNotificationsService; constructor(config: ElectronGlobals) { const { tshClient, ptyServiceClient, mainProcessClient } = config; + this.subscribeToTshdEvent = config.subscribeToTshdEvent; this.mainProcessClient = mainProcessClient; this.fileTransferService = new FileTransferService(tshClient); this.resourcesService = new ResourcesService(tshClient); @@ -86,10 +109,32 @@ export default class AppContext implements IAppContext { this.workspacesService, this.clustersService ); + + this.reloginService = new ReloginService( + mainProcessClient, + this.modalsService, + this.clustersService + ); + this.tshdNotificationsService = new TshdNotificationsService( + this.notificationsService, + this.clustersService + ); } async init(): Promise { + this.setUpTshdEventSubscriptions(); await this.clustersService.syncRootClusters(); this.workspacesService.restorePersistedState(); } + + private setUpTshdEventSubscriptions() { + this.subscribeToTshdEvent('relogin', ({ request, onCancelled }) => { + // The handler for the relogin event should return only after the relogin procedure finishes. + return this.reloginService.relogin(request, onCancelled); + }); + + this.subscribeToTshdEvent('sendNotification', ({ request }) => { + this.tshdNotificationsService.sendNotification(request); + }); + } } diff --git a/packages/teleterm/src/ui/appContextProvider.tsx b/packages/teleterm/src/ui/appContextProvider.tsx index 045e6f538..0dfa7fc39 100644 --- a/packages/teleterm/src/ui/appContextProvider.tsx +++ b/packages/teleterm/src/ui/appContextProvider.tsx @@ -16,9 +16,9 @@ limitations under the License. import React from 'react'; -import AppContext from './appContext'; +import { IAppContext } from 'teleterm/ui/types'; -export const AppReactContext = React.createContext(null); +export const AppReactContext = React.createContext(null); const AppContextProvider: React.FC = props => { return ; @@ -33,5 +33,5 @@ export function useAppContext() { } type Props = { - value: AppContext; + value: IAppContext; }; diff --git a/packages/teleterm/src/ui/commandLauncher.ts b/packages/teleterm/src/ui/commandLauncher.ts index a26c0eb3b..9ec43d867 100644 --- a/packages/teleterm/src/ui/commandLauncher.ts +++ b/packages/teleterm/src/ui/commandLauncher.ts @@ -127,7 +127,7 @@ const commands = { description: '', run(ctx: IAppContext, args: { clusterUri: string }) { const cluster = ctx.clustersService.findCluster(args.clusterUri); - ctx.modalsService.openDialog({ + ctx.modalsService.openRegularDialog({ kind: 'cluster-logout', clusterUri: cluster.uri, clusterTitle: cluster.name, diff --git a/packages/teleterm/src/ui/fixtures/MockAppContextProvider.tsx b/packages/teleterm/src/ui/fixtures/MockAppContextProvider.tsx index a856977e3..5e9e399c5 100644 --- a/packages/teleterm/src/ui/fixtures/MockAppContextProvider.tsx +++ b/packages/teleterm/src/ui/fixtures/MockAppContextProvider.tsx @@ -5,12 +5,12 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { DndProvider } from 'react-dnd'; import AppContextProvider from 'teleterm/ui/appContextProvider'; -import AppContext from 'teleterm/ui/appContext'; +import { IAppContext } from 'teleterm/ui/types'; import { MockAppContext } from './mocks'; export const MockAppContextProvider: React.FC<{ - appContext?: AppContext; + appContext?: IAppContext; }> = props => { const appContext = new MockAppContext(); return ( diff --git a/packages/teleterm/src/ui/services/clusters/clustersService.test.ts b/packages/teleterm/src/ui/services/clusters/clustersService.test.ts index 1e69e5b05..9a77b7fea 100644 --- a/packages/teleterm/src/ui/services/clusters/clustersService.test.ts +++ b/packages/teleterm/src/ui/services/clusters/clustersService.test.ts @@ -120,7 +120,6 @@ function getClientMocks(): Partial { getAllServers: jest.fn().mockResolvedValueOnce([serverMock]), createGateway: jest.fn().mockResolvedValueOnce(gatewayMock), removeGateway: jest.fn().mockResolvedValueOnce(undefined), - restartGateway: jest.fn().mockResolvedValueOnce(undefined), }; } @@ -206,7 +205,6 @@ test('login into cluster and sync resources', async () => { expect(client.listGateways).toHaveBeenCalledWith(); expect(client.getAllDatabases).toHaveBeenCalledWith(clusterUri); expect(client.getAllServers).toHaveBeenCalledWith(clusterUri); - expect(client.restartGateway).toHaveBeenCalledWith(gatewayMock.uri); expect(service.findCluster(clusterUri).connected).toBe(true); }); diff --git a/packages/teleterm/src/ui/services/clusters/clustersService.ts b/packages/teleterm/src/ui/services/clusters/clustersService.ts index 8dc561eb0..fdb8e6f0d 100644 --- a/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -84,16 +84,12 @@ export class ClustersService extends ImmutableStore { async loginLocal(params: LoginLocalParams, abortSignal: tsh.TshAbortSignal) { await this.client.loginLocal(params, abortSignal); - await this.syncRootClusterAndRestartClusterGatewaysAndCatchErrors( - params.clusterUri - ); + await this.syncRootClusterAndCatchErrors(params.clusterUri); } async loginSso(params: LoginSsoParams, abortSignal: tsh.TshAbortSignal) { await this.client.loginSso(params, abortSignal); - await this.syncRootClusterAndRestartClusterGatewaysAndCatchErrors( - params.clusterUri - ); + await this.syncRootClusterAndCatchErrors(params.clusterUri); } async loginPasswordless( @@ -101,44 +97,7 @@ export class ClustersService extends ImmutableStore { abortSignal: tsh.TshAbortSignal ) { await this.client.loginPasswordless(params, abortSignal); - await this.syncRootClusterAndRestartClusterGatewaysAndCatchErrors( - params.clusterUri - ); - } - - private async syncRootClusterAndRestartClusterGatewaysAndCatchErrors( - clusterUri: string - ) { - await Promise.allSettled([ - this.syncRootClusterAndCatchErrors(clusterUri), - // A temporary workaround until the gateways are able to refresh their own certs on incoming - // connections. - // - // After logging in and obtaining fresh certs for the cluster, we need to make the gateways - // obtain fresh certs as well. Currently, the only way to achieve that is to restart them. - this.restartClusterGatewaysAndCatchErrors(clusterUri).then(() => - // Sync gateways to update their status, in case one of them failed to start back up. - // In that case, that gateway won't be included in the gateway list in the tsh daemon. - this.syncGateways() - ), - ]); - } - - async restartClusterGatewaysAndCatchErrors(rootClusterUri: string) { - await Promise.allSettled( - this.findGateways(rootClusterUri).map(async gateway => { - try { - await this.restartGateway(gateway.uri); - } catch (error) { - const title = `Could not restart the database connection for ${gateway.targetUser}@${gateway.targetName}`; - - this.notificationsService.notifyError({ - title, - description: error.message, - }); - } - }) - ); + await this.syncRootClusterAndCatchErrors(params.clusterUri); } async syncRootClusterAndCatchErrors(clusterUri: string) { @@ -538,10 +497,6 @@ export class ClustersService extends ImmutableStore { } } - async restartGateway(gatewayUri: string) { - await this.client.restartGateway(gatewayUri); - } - async setGatewayTargetSubresourceName( gatewayUri: string, targetSubresourceName: string diff --git a/packages/teleterm/src/ui/services/modals/modalsService.ts b/packages/teleterm/src/ui/services/modals/modalsService.ts index 327a39d17..c3ff5706e 100644 --- a/packages/teleterm/src/ui/services/modals/modalsService.ts +++ b/packages/teleterm/src/ui/services/modals/modalsService.ts @@ -16,42 +16,118 @@ limitations under the License. import { useStore } from 'shared/libs/stores'; +import * as types from 'teleterm/services/tshd/types'; + import { ImmutableStore } from '../immutableStore'; -export class ModalsService extends ImmutableStore { - state: Dialog = { - kind: 'none', +type State = { + // At most two modals can be displayed at the same time. + // The important dialog is displayed above the regular one. This is to avoid losing the state of + // the regular modal if we happen to need to interrupt whatever the user is doing and display an + // important modal. + important: Dialog; + regular: Dialog; +}; + +export class ModalsService extends ImmutableStore { + state: State = { + important: { + kind: 'none', + }, + regular: { + kind: 'none', + }, }; - openDialog(dialog: Dialog) { - this.setState(() => dialog); + /** + * openRegularDialog opens the given dialog as a regular dialog. A regular dialog can get covered + * by an important dialog. The regular dialog won't get unmounted if an important dialog is shown + * over the regular one. + * + * Calling openRegularDialog while another regular dialog is displayed will simply overwrite the + * old dialog with the new one. + * + * The returned closeDialog function can be used to close the dialog and automatically call the + * dialog's onCancel callback (if present). + */ + openRegularDialog(dialog: Dialog): { closeDialog: () => void } { + this.setState(draftState => { + draftState.regular = dialog; + }); + + return { + closeDialog: () => { + this.closeRegularDialog(); + dialog['onCancel']?.(); + }, + }; } + /** + * openImportantDialog opens the given dialog as an important dialog. An important dialog will be + * displayed above a regular dialog but it will not affect the regular dialog in any other way. + * + * openImportantDialog should be reserved for situations where the interaction with the app + * happens outside of its UI and requires us to interrupt the user and show them a modal. + * One example of such scenario is showing the modal to relogin after the user attempts to make a + * database connection through a gateway with expired user and db certs. + * + * Calling openImportantDialog while another important dialog is displayed will simply overwrite + * the old dialog with the new one. + * + * The returned closeDialog function can be used to close the dialog and automatically call the + * dialog's onCancel callback (if present). + */ + openImportantDialog(dialog: Dialog): { closeDialog: () => void } { + this.setState(draftState => { + draftState.important = dialog; + }); + + return { + closeDialog: () => { + this.closeImportantDialog(); + dialog['onCancel']?.(); + }, + }; + } + + // TODO(ravicious): Remove this method in favor of calling openRegularDialog directly. openClusterConnectDialog(options: { clusterUri?: string; onSuccess?(clusterUri: string): void; onCancel?(): void; }) { - this.setState(() => ({ + return this.openRegularDialog({ kind: 'cluster-connect', ...options, - })); + }); } + // TODO(ravicious): Remove this method in favor of calling openRegularDialog directly. openDocumentsReopenDialog(options: { onConfirm?(): void; onCancel?(): void; }) { - this.setState(() => ({ + return this.openRegularDialog({ kind: 'documents-reopen', ...options, - })); + }); } - closeDialog() { - this.setState(() => ({ - kind: 'none', - })); + closeRegularDialog() { + this.setState(draftState => { + draftState.regular = { + kind: 'none', + }; + }); + } + + closeImportantDialog() { + this.setState(draftState => { + draftState.important = { + kind: 'none', + }; + }); } useState() { @@ -66,12 +142,21 @@ export interface DialogBase { export interface DialogClusterConnect { kind: 'cluster-connect'; clusterUri?: string; - + reason?: ClusterConnectReason; onSuccess?(clusterUri: string): void; - onCancel?(): void; } +export interface ClusterConnectReasonGatewayCertExpired { + kind: 'reason.gateway-cert-expired'; + targetUri: string; + // The original RPC message passes gatewayUri but we might not always be able to resolve it to a + // gateway, hence the use of undefined. + gateway: types.Gateway | undefined; +} + +export type ClusterConnectReason = ClusterConnectReasonGatewayCertExpired; + export interface DialogClusterLogout { kind: 'cluster-logout'; clusterUri: string; diff --git a/packages/teleterm/src/ui/types.ts b/packages/teleterm/src/ui/types.ts index 0ed88f8b0..5bb7d7ca1 100644 --- a/packages/teleterm/src/ui/types.ts +++ b/packages/teleterm/src/ui/types.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MainProcessClient } from 'teleterm/types'; +import { MainProcessClient, SubscribeToTshdEvent } from 'teleterm/types'; import { ClustersService } from 'teleterm/ui/services/clusters'; import { ModalsService } from 'teleterm/ui/services/modals'; import { TerminalsService } from 'teleterm/ui/services/terminals'; @@ -27,6 +27,8 @@ import { NotificationsService } from 'teleterm/ui/services/notifications'; import { ConnectionTrackerService } from 'teleterm/ui/services/connectionTracker'; import { FileTransferService } from 'teleterm/ui/services/fileTransferClient'; import { ResourcesService } from 'teleterm/ui/services/resources'; +import { ReloginService } from 'teleterm/services/relogin'; +import { TshdNotificationsService } from 'teleterm/services/tshdNotifications'; export interface IAppContext { clustersService: ClustersService; @@ -42,6 +44,9 @@ export interface IAppContext { connectionTracker: ConnectionTrackerService; resourcesService: ResourcesService; fileTransferService: FileTransferService; + subscribeToTshdEvent: SubscribeToTshdEvent; + reloginService: ReloginService; + tshdNotificationsService: TshdNotificationsService; init(): Promise; } diff --git a/packages/teleterm/src/ui/utils/retryWithRelogin.test.ts b/packages/teleterm/src/ui/utils/retryWithRelogin.test.ts index d83764033..77760921e 100644 --- a/packages/teleterm/src/ui/utils/retryWithRelogin.test.ts +++ b/packages/teleterm/src/ui/utils/retryWithRelogin.test.ts @@ -44,7 +44,12 @@ it('opens the login modal window and calls actionToRetry again on successful rel // Immediately resolve the login promise. jest .spyOn(appContext.modalsService, 'openClusterConnectDialog') - .mockImplementation(({ onSuccess }) => onSuccess('')); + .mockImplementation(({ onSuccess }) => { + onSuccess(''); + + // Dialog cancel function. + return { closeDialog: () => {} }; + }); jest .spyOn(appContext.workspacesService, 'doesResourceBelongToActiveWorkspace') @@ -109,7 +114,12 @@ it('calls actionToRetry again if relogin attempt was canceled', async () => { jest .spyOn(appContext.modalsService, 'openClusterConnectDialog') - .mockImplementation(({ onCancel }) => onCancel()); + .mockImplementation(({ onCancel }) => { + onCancel(); + + // Dialog cancel function. + return { closeDialog: () => {} }; + }); jest .spyOn(appContext.workspacesService, 'doesResourceBelongToActiveWorkspace') diff --git a/packages/teleterm/src/ui/utils/retryWithRelogin.ts b/packages/teleterm/src/ui/utils/retryWithRelogin.ts index b430cba32..a63207833 100644 --- a/packages/teleterm/src/ui/utils/retryWithRelogin.ts +++ b/packages/teleterm/src/ui/utils/retryWithRelogin.ts @@ -1,5 +1,5 @@ import { routing } from 'teleterm/ui/uri'; -import AppContext from 'teleterm/ui/appContext'; +import { IAppContext } from 'teleterm/ui/types'; import Logger from 'teleterm/logger'; const logger = new Logger('retryWithRelogin'); @@ -25,7 +25,7 @@ const logger = new Logger('retryWithRelogin'); * cluster the login modal should use and whether the workspace of that resource is still active. */ export async function retryWithRelogin( - appContext: AppContext, + appContext: IAppContext, resourceUri: string, actionToRetry: () => Promise ): Promise { @@ -71,7 +71,7 @@ export async function retryWithRelogin( // Notice that we don't differentiate between onSuccess and onCancel. In both cases, we're going to // retry the action anyway in case the cert was refreshed externally before the modal was closed, // for example through tsh login. -function login(appContext: AppContext, rootClusterUri: string): Promise { +function login(appContext: IAppContext, rootClusterUri: string): Promise { return new Promise(resolve => { appContext.modalsService.openClusterConnectDialog({ clusterUri: rootClusterUri, diff --git a/yarn.lock b/yarn.lock index 57e0fae8e..5d8f130a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6780,6 +6780,11 @@ emittery@^0.8.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== +emittery@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-1.0.1.tgz#e0cf36e2d7eef94dbd025969f642d57ae50a56cd" + integrity sha512-2ID6FdrMD9KDLldGesP6317G78K7km/kMcwItRtVFva7I/cSEOIaLpewaUb+YLXVwdAp3Ctfxh/V5zIl1sj7dQ== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"