diff --git a/web/packages/teleterm/src/services/tshd/testHelpers.ts b/web/packages/teleterm/src/services/tshd/testHelpers.ts index ef507a41e5e84..9ff030edc670e 100644 --- a/web/packages/teleterm/src/services/tshd/testHelpers.ts +++ b/web/packages/teleterm/src/services/tshd/testHelpers.ts @@ -48,7 +48,7 @@ export const makeServer = (props: Partial = {}): tsh.Server => ({ export const databaseUri = `${rootClusterUri}/dbs/foo`; export const kubeUri = `${rootClusterUri}/kubes/foo`; export const appUri = `${rootClusterUri}/apps/foo`; -export const windowsDesktopUri = `${rootClusterUri}/windowsDesktops/foo`; +export const windowsDesktopUri = `${rootClusterUri}/windows_desktops/foo`; export const makeDatabase = ( props: Partial = {} diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx index 053b549c0ad3c..6e8de628a80a5 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx @@ -288,7 +288,7 @@ function WindowsDesktop() { return ( ); diff --git a/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx b/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx index 7bc5bd4b9865a..df505037c7b8a 100644 --- a/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx +++ b/web/packages/teleterm/src/ui/DocumentDesktopSession/DocumentDesktopSession.tsx @@ -16,8 +16,9 @@ * along with this program. If not, see . */ -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import { Text } from 'design'; import { ACL } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb'; import { DesktopSession } from 'shared/components/DesktopSession'; import { @@ -25,17 +26,18 @@ import { makeProcessingAttempt, makeSuccessAttempt, } from 'shared/hooks/useAsync'; -import { BrowserFileSystem, TdpClient } from 'shared/libs/tdp'; +import { BrowserFileSystem, TdpClient, useListener } from 'shared/libs/tdp'; import { TdpTransport } from 'shared/libs/tdp/client'; import Logger from 'teleterm/logger'; import { cloneAbortSignal, TshdClient } from 'teleterm/services/tshd'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import Document from 'teleterm/ui/Document'; +import { useWorkspaceContext } from 'teleterm/ui/Documents'; import { useWorkspaceLoggedInUser } from 'teleterm/ui/hooks/useLoggedInUser'; import { useLogger } from 'teleterm/ui/hooks/useLogger'; import * as types from 'teleterm/ui/services/workspacesService'; -import { routing, WindowsDesktopUri } from 'teleterm/ui/uri'; +import { DesktopUri, isWindowsDesktopUri, routing } from 'teleterm/ui/uri'; // The check for another active session is disabled in Connect: // 1. This protection was added to the Web UI to prevent a situation where a user could be tricked @@ -50,11 +52,12 @@ export function DocumentDesktopSession(props: { doc: types.DocumentDesktopSession; }) { const logger = useLogger('DocumentDesktopSession'); - const { desktopUri, login, origin } = props.doc; + const { desktopUri, login, origin, uri } = props.doc; const appCtx = useAppContext(); + const { documentsService } = useWorkspaceContext(); const loggedInUser = useWorkspaceLoggedInUser(); const acl = useMemo>(() => { - if (!loggedInUser) { + if (!loggedInUser?.acl) { return makeProcessingAttempt(); } return makeSuccessAttempt(loggedInUser.acl); @@ -83,8 +86,38 @@ export function DocumentDesktopSession(props: { }, new BrowserFileSystem()) ); - return ( - + useListener( + client.onTransportOpen, + useCallback( + () => documentsService.update(uri, { status: 'connected' }), + [documentsService, uri] + ) + ); + useListener( + client.onTransportClose, + useCallback( + error => documentsService.update(uri, { status: error ? 'error' : '' }), + [documentsService, uri] + ) + ); + useListener( + client.onError, + useCallback( + () => documentsService.update(uri, { status: 'error' }), + [documentsService, uri] + ) + ); + + let content = ( + + Cannot open a connection to "{desktopUri}". +
+ Only Windows desktops are supported. +
+ ); + + if (isWindowsDesktopUri(desktopUri)) { + content = ( -
- ); + ); + } + + return {content}; } async function adaptGRPCStreamToTdpTransport( stream: ReturnType, targetDesktop: { - desktopUri: WindowsDesktopUri; + desktopUri: DesktopUri; login: string; }, logger: Logger diff --git a/web/packages/teleterm/src/ui/Search/pickers/results.story.tsx b/web/packages/teleterm/src/ui/Search/pickers/results.story.tsx index 71bacd65097fb..0071d2a4468ed 100644 --- a/web/packages/teleterm/src/ui/Search/pickers/results.story.tsx +++ b/web/packages/teleterm/src/ui/Search/pickers/results.story.tsx @@ -486,7 +486,7 @@ const SearchResultItems = () => { kind: 'windows_desktop', requiresRequest: false, resource: makeWindowsDesktopWithoutDefaultPort({ - uri: `${clusterUri}/windowsDesktops/long-name`, + uri: `${clusterUri}/windows_desktops/long-name`, name: 'super-long-windows-desktop-name-with-uuid-7a96e498-88ec-442f-a25b-569fa9150123c', labels: makeLabelsList({ 'aws/Environment': 'demo-13-biz', @@ -499,7 +499,7 @@ const SearchResultItems = () => { makeResourceResult({ kind: 'windows_desktop', resource: makeWindowsDesktopWithoutDefaultPort({ - uri: `${clusterUri}/windowsDesktops/long-label-list`, + uri: `${clusterUri}/windows_desktops/long-label-list`, name: 'long-label-list', labels: makeLabelsList({ 'aws/Environment': 'demo-13-biz', @@ -514,7 +514,7 @@ const SearchResultItems = () => { kind: 'windows_desktop', requiresRequest: true, resource: makeWindowsDesktopWithoutDefaultPort({ - uri: `${clusterUri}/windowsDesktops/short-label-list`, + uri: `${clusterUri}/windows_desktops/short-label-list`, name: 'short-label-list', labels: makeLabelsList({ 'im-just-a-smol': 'win', diff --git a/web/packages/teleterm/src/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionItem.tsx b/web/packages/teleterm/src/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionItem.tsx index 551180a129d8f..89ff3d7e86f7c 100644 --- a/web/packages/teleterm/src/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionItem.tsx +++ b/web/packages/teleterm/src/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionItem.tsx @@ -166,13 +166,9 @@ function getKindName(connection: ExtendedTrackedConnection): string { return 'SSH'; case 'connection.kube': return 'KUBE'; + case 'connection.desktop': + return 'DESKTOP'; default: - // The default branch is triggered when the state read from the disk - // contains a connection not supported by the given Connect version. - // - // For example, the user can open an app connection in Connect v15 - // and then downgrade to a version that doesn't support apps. - // That connection should be shown as 'UNKNOWN' in the connection list. return 'UNKNOWN'; } } diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts index af1d53feca6fd..a9ff030fe1fbc 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts @@ -33,10 +33,12 @@ import { assertUnreachable } from 'teleterm/ui/utils'; import { ImmutableStore } from '../immutableStore'; import { TrackedConnectionOperationsFactory } from './trackedConnectionOperationsFactory'; import { + createDesktopConnection, createGatewayConnection, createGatewayKubeConnection, createKubeConnection, createServerConnection, + getDesktopConnectionByDocument, getGatewayConnectionByDocument, getGatewayKubeConnectionByDocument, getKubeConnectionByDocument, @@ -76,15 +78,27 @@ export class ConnectionTrackerService extends ImmutableStore { - const { rootClusterUri, leafClusterUri } = - this._trackedConnectionOperationsFactory.create(connection); - const clusterUri = leafClusterUri || rootClusterUri; - const clusterName = - this._clusterService.findCluster(clusterUri)?.name || - routing.parseClusterName(clusterUri); - return { ...connection, clusterName }; - }); + return this.state.connections + .map(connection => { + const trackedConnection = + this._trackedConnectionOperationsFactory.create(connection); + // A connection is undefined when the state read from the disk + // contains a connection not supported by the given Connect version. + // + // For example, the user can open a desktop connection in Connect v18 + // and then downgrade to a version that doesn't support desktops. + // That connection should be shown as 'UNKNOWN' in the connection list. + if (!trackedConnection) { + return; + } + const { rootClusterUri, leafClusterUri } = trackedConnection; + const clusterUri = leafClusterUri || rootClusterUri; + const clusterName = + this._clusterService.findCluster(clusterUri)?.name || + routing.parseClusterName(clusterUri); + return { ...connection, clusterName }; + }) + .filter(Boolean); } async activateItem( @@ -123,6 +137,10 @@ export class ConnectionTrackerService extends ImmutableStore { + let doc = documentsService + .getDocuments() + .find(getDesktopDocumentByConnection(connection)); + + if (!doc) { + doc = createDesktopSessionDocument({ + desktopUri: connection.desktopUri, + login: connection.login, + origin: params.origin, + }); + documentsService.add(doc); + } + documentsService.open(doc.uri); + }, + disconnect: async () => { + documentsService + .getDocuments() + .filter(getDesktopDocumentByConnection(connection)) + .forEach(document => { + documentsService.close(document.uri); + }); + }, + remove: async () => {}, + }; + } + private getClusterUris({ rootClusterId, leafClusterId }) { const rootClusterUri = routing.getClusterUri({ rootClusterId, diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts b/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts index 2fe91129e37cc..fc79220f7e421 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts @@ -18,6 +18,7 @@ import { Document, + DocumentDesktopSession, DocumentGateway, DocumentGatewayKube, DocumentTshKube, @@ -30,6 +31,7 @@ import { unique } from 'teleterm/ui/utils/uid'; import { TrackedConnection, + TrackedDesktopConnection, TrackedGatewayConnection, TrackedKubeConnection, TrackedServerConnection, @@ -99,6 +101,24 @@ export function getGatewayKubeConnectionByDocument( i.kind === 'connection.kube' && i.kubeUri === document.targetUri; } +export function getDesktopDocumentByConnection( + connection: TrackedDesktopConnection +): (d: Document) => boolean { + return d => + d.kind === 'doc.desktop_session' && + d.desktopUri === connection.desktopUri && + d.login === connection.login; +} + +export function getDesktopConnectionByDocument( + document: DocumentDesktopSession +) { + return (i: TrackedDesktopConnection) => + i.kind === 'connection.desktop' && + i.desktopUri === document.desktopUri && + i.login === document.login; +} + /* * Getting a document by a connection. */ @@ -220,3 +240,16 @@ export function createGatewayKubeConnection( kubeUri: document.targetUri, }; } + +export function createDesktopConnection( + document: DocumentDesktopSession +): TrackedDesktopConnection { + return { + kind: 'connection.desktop', + connected: true, + id: unique(), + title: document.title, + desktopUri: document.desktopUri, + login: document.login, + }; +} diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/types.ts b/web/packages/teleterm/src/ui/services/connectionTracker/types.ts index a43af4c9b36c5..f9251d5fc3512 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/types.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/types.ts @@ -16,7 +16,13 @@ * along with this program. If not, see . */ -import { AppUri, DatabaseUri, KubeUri, ServerUri } from 'teleterm/ui/uri'; +import { + AppUri, + DatabaseUri, + DesktopUri, + KubeUri, + ServerUri, +} from 'teleterm/ui/uri'; type TrackedConnectionBase = { connected: boolean; @@ -49,10 +55,17 @@ export interface TrackedKubeConnection extends TrackedConnectionBase { kubeUri: KubeUri; } +export interface TrackedDesktopConnection extends TrackedConnectionBase { + kind: 'connection.desktop'; + desktopUri: DesktopUri; + login: string; +} + export type TrackedConnection = | TrackedServerConnection | TrackedGatewayConnection - | TrackedKubeConnection; + | TrackedKubeConnection + | TrackedDesktopConnection; export type ExtendedTrackedConnection = TrackedConnection & { clusterName: string; diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts index 5b0289b5b454d..9b8aafed82bd1 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts @@ -23,6 +23,7 @@ import type { RuntimeSettings } from 'teleterm/mainProcess/types'; import * as uri from 'teleterm/ui/uri'; import { DocumentUri, + isWindowsDesktopUri, KubeUri, paths, RootClusterUri, @@ -591,17 +592,20 @@ export function createClusterDocument(opts: { } export function createDesktopSessionDocument(opts: { - desktopUri: uri.WindowsDesktopUri; + desktopUri: uri.DesktopUri; login: string; origin: DocumentOrigin; }): DocumentDesktopSession { return { kind: 'doc.desktop_session' as const, uri: routing.getDocUri({ docId: unique() }), - title: `${opts.login} on ${routing.parseWindowsDesktopUri(opts.desktopUri).params?.windowsDesktopId}`, + title: isWindowsDesktopUri(opts.desktopUri) + ? `${opts.login} on ${routing.parseWindowsDesktopUri(opts.desktopUri).params.windowsDesktopId}` + : 'Unknown', desktopUri: opts.desktopUri, login: opts.login, origin: opts.origin, + status: '', }; } diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/testHelpers.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/testHelpers.ts index 82834f0d0731c..526f188eee715 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/testHelpers.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/testHelpers.ts @@ -235,6 +235,7 @@ export function makeDocumentDesktopSession( desktopUri: windowsDesktopUri, login: 'admin', origin: 'resource_table', + status: '', ...props, }; } diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts index b884b4c925c16..1b1c86670864f 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts @@ -308,9 +308,11 @@ export interface DocumentAuthorizeWebSession extends DocumentBase { export interface DocumentDesktopSession extends DocumentBase { kind: 'doc.desktop_session'; - desktopUri: uri.WindowsDesktopUri; + desktopUri: uri.DesktopUri; login: string; origin: DocumentOrigin; + // status is used merely to indicate that a connection is established in the connection tracker. + status: '' | 'connected' | 'error'; } export interface WebSessionRequest { diff --git a/web/packages/teleterm/src/ui/uri.ts b/web/packages/teleterm/src/ui/uri.ts index bd3d2ccf24f96..2001ea31313cf 100644 --- a/web/packages/teleterm/src/ui/uri.ts +++ b/web/packages/teleterm/src/ui/uri.ts @@ -77,6 +77,9 @@ export type WindowsDesktopUri = export type ClusterOrResourceUri = ResourceUri | ClusterUri; export type GatewayTargetUri = DatabaseUri | KubeUri | AppUri; +/** General type for desktop URI. */ +export type DesktopUri = WindowsDesktopUri; + /* * Document URIs * These are for documents (tabs) within the app. @@ -356,6 +359,10 @@ export function isKubeUri(uri: string): uri is KubeUri { return !!routing.parseKubeUri(uri); } +export function isWindowsDesktopUri(uri: string): uri is WindowsDesktopUri { + return !!routing.parseWindowsDesktopUri(uri); +} + export type Params = { rootClusterId?: string; leafClusterId?: string;