From ea857b0a4b0117d5a77c8b56b7417ba1245e3a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Tue, 6 Dec 2022 12:31:15 +0100 Subject: [PATCH] Connect: Implement tshd event handlers for db cert renewal (#1383) * tshd events: Wait for listeners before responding When tshd is sending the relogin event, it needs to be able to know when the Electron app has finished relogging the user. I wanted to implement this by simply waiting for the response from the RPC. I'm so glad we did not use gRPC streams for tshd events as this would be much harder to implement with streams. * Return a function from ModalsService.openDialog which closes dialog With the introduction of important and regular modals, this will help us close the specific dialog if need arises. * Make tshd events listeners aware of request cancellation This will be useful in the upcoming commits. Basically, tshd is going to ask the Electron app to relogin the user, with a 1 minute timeout. The Electron app will show a login modal but if the user doesn't submit it within 1 minute, tshd is going to cancel the request. In that situation, we need to be able to close the modal. * Add support for passing reason in DialogClusterConnect * Add support for important modals This will let us show the relogin modal on expired cert, even if the user was using some other modal at that moment. * Remove title attr from notification text The user can read more by expanding the notification. The title attribute persisted even after expanding the notification, making reading it harder. * Add WindowsManager.forceFocusWindow * Use IAppContext instead of AppContext The next commit is going to add a private method to AppContext. IAppContext is an interface which enables us to pass a mocked version of AppContext in tests. That mock is not going have that private method, so any place accepting AppContext wouldn't be able to accept the mocked AppContext. Instead, classes & functions should accept IAppContext rather than AppContext. * Implement handlers for new tshd events tshd needs to be able to do two things: - Ask the user to log in again. - Forward errors from goroutines running gateways to the Electron app in form of a notification. Otherwise those error would be visible only in the logs. * Don't restart gateways after logging in Restarting the gateways on login was a workaround from times where gateways didn't manage their own certs. In the new flow, a gateway takes care of refreshing the certs itself through the middleware passed to alpnproxy.LocalProxy. syncRootClusterAndRestartClusterGatewaysAndCatchErrors used to call two functions: - syncRootClusterAndCatchErrors - restartClusterGatewaysAndCatchErrors The second function is no longer necessary, so we can make any place that was calling syncRootClusterAndRestartClusterGatewaysAndCatchErrors call just syncRootClusterAndCatchErrors instead. --- packages/teleterm/package.json | 1 + packages/teleterm/src/main.ts | 1 + .../src/mainProcess/fixtures/mocks.ts | 2 + .../teleterm/src/mainProcess/mainProcess.ts | 8 + .../src/mainProcess/mainProcessClient.ts | 3 + packages/teleterm/src/mainProcess/types.ts | 1 + .../src/mainProcess/windowsManager.ts | 65 +- packages/teleterm/src/services/relogin.ts | 49 + .../src/services/tshd/createClient.ts | 13 - .../src/services/tshd/fixtures/mocks.ts | 13 +- packages/teleterm/src/services/tshd/types.ts | 1 - .../tshd/v1/tshd_events_service_grpc_pb.d.ts | 45 +- .../tshd/v1/tshd_events_service_grpc_pb.js | 81 +- .../tshd/v1/tshd_events_service_pb.d.ts | 156 +++- .../tshd/v1/tshd_events_service_pb.js | 883 +++++++++++++++++- .../teleterm/src/services/tshdEvents/index.ts | 69 +- .../src/services/tshdNotifications.ts | 44 + packages/teleterm/src/types.ts | 44 +- .../src/ui/ClusterConnect/ClusterConnect.tsx | 7 +- .../ClusterLogin/ClusterLogin.story.tsx | 45 +- .../ClusterLogin/ClusterLogin.tsx | 59 +- .../src/ui/ModalsHost/ModalsHost.story.tsx | 73 ++ .../src/ui/ModalsHost/ModalsHost.test.tsx | 71 ++ .../teleterm/src/ui/ModalsHost/ModalsHost.tsx | 98 +- .../ShareFeedback/ShareFeedback.test.tsx | 1 - packages/teleterm/src/ui/TabHost/TabHost.tsx | 7 +- packages/teleterm/src/ui/appContext.ts | 47 +- .../teleterm/src/ui/appContextProvider.tsx | 6 +- packages/teleterm/src/ui/commandLauncher.ts | 2 +- .../components/Notifcations/Notification.tsx | 1 - .../ui/fixtures/MockAppContextProvider.tsx | 4 +- .../services/clusters/clustersService.test.ts | 2 - .../ui/services/clusters/clustersService.ts | 51 +- .../src/ui/services/modals/modalsService.ts | 115 ++- packages/teleterm/src/ui/types.ts | 7 +- .../src/ui/utils/retryWithRelogin.test.ts | 14 +- .../teleterm/src/ui/utils/retryWithRelogin.ts | 6 +- yarn.lock | 5 + 38 files changed, 1814 insertions(+), 286 deletions(-) create mode 100644 packages/teleterm/src/services/relogin.ts create mode 100644 packages/teleterm/src/services/tshdNotifications.ts create mode 100644 packages/teleterm/src/ui/ModalsHost/ModalsHost.story.tsx create mode 100644 packages/teleterm/src/ui/ModalsHost/ModalsHost.test.tsx diff --git a/packages/teleterm/package.json b/packages/teleterm/package.json index 40cc2fee7..f08751f13 100644 --- a/packages/teleterm/package.json +++ b/packages/teleterm/package.json @@ -29,6 +29,7 @@ "private": true, "homepage": "https://goteleport.com", "dependencies": { + "emittery": "^1.0.1", "node-pty": "0.10.0" }, "devDependencies": { diff --git a/packages/teleterm/src/main.ts b/packages/teleterm/src/main.ts index 9e30f7719..571125b6e 100644 --- a/packages/teleterm/src/main.ts +++ b/packages/teleterm/src/main.ts @@ -43,6 +43,7 @@ function initializeApp(): void { logger, configService, fileStorage, + windowsManager, }); app.on( diff --git a/packages/teleterm/src/mainProcess/fixtures/mocks.ts b/packages/teleterm/src/mainProcess/fixtures/mocks.ts index 4da822179..41545f17f 100644 --- a/packages/teleterm/src/mainProcess/fixtures/mocks.ts +++ b/packages/teleterm/src/mainProcess/fixtures/mocks.ts @@ -56,4 +56,6 @@ export class MockMainProcessClient implements MainProcessClient { removeKubeConfig(): Promise { 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 80a8c3e6e..eda7d6b57 100644 --- a/packages/teleterm/src/services/tshd/createClient.ts +++ b/packages/teleterm/src/services/tshd/createClient.ts @@ -589,19 +589,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 2d9d98833..0c19ceb70 100644 --- a/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -63,7 +63,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 @@ -91,3 +90,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 7055f4e8e..aab921e19 100644 --- a/packages/teleterm/src/services/tshd/types.ts +++ b/packages/teleterm/src/services/tshd/types.ts @@ -109,7 +109,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/components/Notifcations/Notification.tsx b/packages/teleterm/src/ui/components/Notifcations/Notification.tsx index ed1c516df..b94830aa8 100644 --- a/packages/teleterm/src/ui/components/Notifcations/Notification.tsx +++ b/packages/teleterm/src/ui/components/Notifcations/Notification.tsx @@ -155,7 +155,6 @@ function getRenderedContent( = 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 9f1da5c07..4b8752284 100644 --- a/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -85,16 +85,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( @@ -102,44 +98,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) { @@ -539,10 +498,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 729a2fba5..5747b5c87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6743,6 +6743,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"