diff --git a/packages/teleterm/src/services/tshd/createClient.ts b/packages/teleterm/src/services/tshd/createClient.ts index 604ec3354..1b79e3db5 100644 --- a/packages/teleterm/src/services/tshd/createClient.ts +++ b/packages/teleterm/src/services/tshd/createClient.ts @@ -1,6 +1,7 @@ import { ChannelCredentials, ClientDuplexStream } from '@grpc/grpc-js'; import Logger from 'teleterm/logger'; +import * as uri from 'teleterm/ui/uri'; import * as api from './v1/service_pb'; import { TerminalServiceClient } from './v1/service_grpc_pb'; @@ -23,7 +24,7 @@ export default function createClient( const client = { createAbortController, - async logout(clusterUri: string) { + async logout(clusterUri: uri.RootClusterUri) { const req = new api.LogoutRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { tshd.logout(req, err => { @@ -36,7 +37,7 @@ export default function createClient( }); }, - async listApps(clusterUri: string) { + async listApps(clusterUri: uri.ClusterUri) { const req = new api.ListAppsRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { tshd.listApps(req, (err, response) => { @@ -49,14 +50,14 @@ export default function createClient( }); }, - async getAllKubes(clusterUri: string) { + async getAllKubes(clusterUri: uri.ClusterUri) { const req = new api.GetAllKubesRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { tshd.getAllKubes(req, (err, response) => { if (err) { reject(err); } else { - resolve(response.toObject().kubesList); + resolve(response.toObject().kubesList as types.Kube[]); } }); }); @@ -84,7 +85,7 @@ export default function createClient( if (err) { reject(err); } else { - resolve(response.toObject()); + resolve(response.toObject() as types.GetKubesResponse); } }); }); @@ -97,20 +98,20 @@ export default function createClient( if (err) { reject(err); } else { - resolve(response.toObject().gatewaysList); + resolve(response.toObject().gatewaysList as types.Gateway[]); } }); }); }, - async listLeafClusters(clusterUri: string) { + async listLeafClusters(clusterUri: uri.RootClusterUri) { const req = new api.ListLeafClustersRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { tshd.listLeafClusters(req, (err, response) => { if (err) { reject(err); } else { - resolve(response.toObject().clustersList); + resolve(response.toObject().clustersList as types.Cluster[]); } }); }); @@ -123,20 +124,20 @@ export default function createClient( if (err) { reject(err); } else { - resolve(response.toObject().clustersList); + resolve(response.toObject().clustersList as types.Cluster[]); } }); }); }, - async getAllDatabases(clusterUri: string) { + async getAllDatabases(clusterUri: uri.ClusterUri) { const req = new api.GetAllDatabasesRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { tshd.getAllDatabases(req, (err, response) => { if (err) { reject(err); } else { - resolve(response.toObject().databasesList); + resolve(response.toObject().databasesList as types.Database[]); } }); }); @@ -164,13 +165,13 @@ export default function createClient( if (err) { reject(err); } else { - resolve(response.toObject()); + resolve(response.toObject() as types.GetDatabasesResponse); } }); }); }, - async listDatabaseUsers(dbUri: string) { + async listDatabaseUsers(dbUri: uri.DatabaseUri) { const req = new api.ListDatabaseUsersRequest().setDbUri(dbUri); return new Promise((resolve, reject) => { tshd.listDatabaseUsers(req, (err, response) => { @@ -183,20 +184,20 @@ export default function createClient( }); }, - async getAllServers(clusterUri: string) { + async getAllServers(clusterUri: uri.ClusterUri) { const req = new api.GetAllServersRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { tshd.getAllServers(req, (err, response) => { if (err) { reject(err); } else { - resolve(response.toObject().serversList); + resolve(response.toObject().serversList as types.Server[]); } }); }); }, - async getAccessRequest(clusterUri: string, requestId: string) { + async getAccessRequest(clusterUri: uri.RootClusterUri, requestId: string) { const req = new api.GetAccessRequestRequest() .setClusterUri(clusterUri) .setAccessRequestId(requestId); @@ -211,7 +212,7 @@ export default function createClient( }); }, - async getAccessRequests(clusterUri: string) { + async getAccessRequests(clusterUri: uri.RootClusterUri) { const req = new api.GetAccessRequestsRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { tshd.getAccessRequests(req, (err, response) => { @@ -246,7 +247,7 @@ export default function createClient( if (err) { reject(err); } else { - resolve(response.toObject()); + resolve(response.toObject() as types.GetServersResponse); } }); }); @@ -278,7 +279,10 @@ export default function createClient( }); }, - async deleteAccessRequest(clusterUri: string, requestId: string) { + async deleteAccessRequest( + clusterUri: uri.RootClusterUri, + requestId: string + ) { const req = new api.DeleteAccessRequestRequest() .setRootClusterUri(clusterUri) .setAccessRequestId(requestId); @@ -294,7 +298,7 @@ export default function createClient( }, async assumeRole( - clusterUri: string, + clusterUri: uri.RootClusterUri, requestIds: string[], dropIds: string[] ) { @@ -314,7 +318,7 @@ export default function createClient( }, async reviewAccessRequest( - clusterUri: string, + clusterUri: uri.RootClusterUri, params: types.ReviewAccessRequestParams ) { const req = new api.ReviewAccessRequestRequest() @@ -366,20 +370,20 @@ export default function createClient( if (err) { reject(err); } else { - resolve(response.toObject()); + resolve(response.toObject() as types.Cluster); } }); }); }, - async getCluster(uri: string) { + async getCluster(uri: uri.RootClusterUri) { const req = new api.GetClusterRequest().setClusterUri(uri); return new Promise((resolve, reject) => { tshd.getCluster(req, (err, response) => { if (err) { reject(err); } else { - resolve(response.toObject()); + resolve(response.toObject() as types.Cluster); } }); }); @@ -533,7 +537,7 @@ export default function createClient( }); }, - async getAuthSettings(clusterUri = '') { + async getAuthSettings(clusterUri: uri.RootClusterUri) { const req = new api.GetAuthSettingsRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { tshd.getAuthSettings(req, (err, response) => { @@ -557,13 +561,13 @@ export default function createClient( if (err) { reject(err); } else { - resolve(response.toObject()); + resolve(response.toObject() as types.Gateway); } }); }); }, - async removeCluster(clusterUri = '') { + async removeCluster(clusterUri: uri.RootClusterUri) { const req = new api.RemoveClusterRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { tshd.removeCluster(req, err => { @@ -576,7 +580,7 @@ export default function createClient( }); }, - async removeGateway(gatewayUri = '') { + async removeGateway(gatewayUri: uri.GatewayUri) { const req = new api.RemoveGatewayRequest().setGatewayUri(gatewayUri); return new Promise((resolve, reject) => { tshd.removeGateway(req, err => { @@ -590,7 +594,7 @@ export default function createClient( }, async setGatewayTargetSubresourceName( - gatewayUri = '', + gatewayUri: uri.GatewayUri, targetSubresourceName = '' ) { const req = new api.SetGatewayTargetSubresourceNameRequest() @@ -601,13 +605,13 @@ export default function createClient( if (err) { reject(err); } else { - resolve(response.toObject()); + resolve(response.toObject() as types.Gateway); } }); }); }, - async setGatewayLocalPort(gatewayUri: string, localPort: string) { + async setGatewayLocalPort(gatewayUri: uri.GatewayUri, localPort: string) { const req = new api.SetGatewayLocalPortRequest() .setGatewayUri(gatewayUri) .setLocalPort(localPort); @@ -616,7 +620,7 @@ export default function createClient( if (err) { reject(err); } else { - resolve(response.toObject()); + resolve(response.toObject() as types.Gateway); } }); }); diff --git a/packages/teleterm/src/services/tshd/types.ts b/packages/teleterm/src/services/tshd/types.ts index aab921e19..40923e291 100644 --- a/packages/teleterm/src/services/tshd/types.ts +++ b/packages/teleterm/src/services/tshd/types.ts @@ -1,11 +1,10 @@ import { ResourceKind } from 'e-teleterm/ui/DocumentAccessRequests/NewRequest/useNewRequest'; - -import { SortType } from 'design/DataTable/types'; - import { RequestState } from 'e-teleport/services/workflow'; - +import { SortType } from 'design/DataTable/types'; import { FileTransferListeners } from 'shared/components/FileTransfer'; +import * as uri from 'teleterm/ui/uri'; + import apiCluster from './v1/cluster_pb'; import apiDb from './v1/database_pb'; import apigateway from './v1/gateway_pb'; @@ -17,15 +16,30 @@ import apiAuthSettings from './v1/auth_settings_pb'; import apiAccessRequest from './v1/access_request_pb'; export type Application = apiApp.App.AsObject; -export type Kube = apiKube.Kube.AsObject; -export type Server = apiServer.Server.AsObject; -export type Gateway = apigateway.Gateway.AsObject; +export interface Kube extends apiKube.Kube.AsObject { + uri: uri.KubeUri; +} +export interface Server extends apiServer.Server.AsObject { + uri: uri.ServerUri; +} +export interface Gateway extends apigateway.Gateway.AsObject { + uri: uri.GatewayUri; + targetUri: uri.DatabaseUri; +} export type AccessRequest = apiAccessRequest.AccessRequest.AsObject; export type ResourceId = apiAccessRequest.ResourceID.AsObject; export type AccessRequestReview = apiAccessRequest.AccessRequestReview.AsObject; -export type GetServersResponse = apiService.GetServersResponse.AsObject; -export type GetDatabasesResponse = apiService.GetDatabasesResponse.AsObject; -export type GetKubesResponse = apiService.GetKubesResponse.AsObject; +export interface GetServersResponse + extends apiService.GetServersResponse.AsObject { + agentsList: Server[]; +} +export interface GetDatabasesResponse + extends apiService.GetDatabasesResponse.AsObject { + agentsList: Database[]; +} +export interface GetKubesResponse extends apiService.GetKubesResponse.AsObject { + agentsList: Kube[]; +} export type GetRequestableRolesResponse = apiService.GetRequestableRolesResponse.AsObject; // Available types are listed here: @@ -39,10 +53,13 @@ export type GatewayProtocol = | 'cockroachdb' | 'redis' | 'sqlserver'; -export type Database = apiDb.Database.AsObject; -export type Cluster = Omit & { +export interface Database extends apiDb.Database.AsObject { + uri: uri.DatabaseUri; +} +export interface Cluster extends apiCluster.Cluster.AsObject { + uri: uri.ClusterUri; loggedInUser?: LoggedInUser; -}; +} export type LoggedInUser = apiCluster.LoggedInUser.AsObject & { assumedRequests?: Record; }; @@ -73,16 +90,16 @@ export type LoginPasswordlessRequest = export type TshClient = { listRootClusters: () => Promise; - listLeafClusters: (clusterUri: string) => Promise; - listApps: (clusterUri: string) => Promise; - getAllKubes: (clusterUri: string) => Promise; + listLeafClusters: (clusterUri: uri.RootClusterUri) => Promise; + listApps: (clusterUri: uri.ClusterUri) => Promise; + getAllKubes: (clusterUri: uri.ClusterUri) => Promise; getKubes: (params: ServerSideParams) => Promise; - getAllDatabases: (clusterUri: string) => Promise; + getAllDatabases: (clusterUri: uri.ClusterUri) => Promise; getDatabases: (params: ServerSideParams) => Promise; - listDatabaseUsers: (dbUri: string) => Promise; - getAllServers: (clusterUri: string) => Promise; + listDatabaseUsers: (dbUri: uri.DatabaseUri) => Promise; + getAllServers: (clusterUri: uri.ClusterUri) => Promise; assumeRole: ( - clusterUri: string, + clusterUri: uri.RootClusterUri, requestIds: string[], dropIds: string[] ) => Promise; @@ -90,37 +107,42 @@ export type TshClient = { params: GetRequestableRolesParams ) => Promise; getServers: (params: ServerSideParams) => Promise; - getAccessRequests: (clusterUri: string) => Promise; + getAccessRequests: ( + clusterUri: uri.RootClusterUri + ) => Promise; getAccessRequest: ( - clusterUri: string, + clusterUri: uri.RootClusterUri, requestId: string ) => Promise; reviewAccessRequest: ( - clusterUri: string, + clusterUri: uri.RootClusterUri, params: ReviewAccessRequestParams ) => Promise; createAccessRequest: ( params: CreateAccessRequestParams ) => Promise; - deleteAccessRequest: (clusterUri: string, requestId: string) => Promise; + deleteAccessRequest: ( + clusterUri: uri.RootClusterUri, + requestId: string + ) => Promise; createAbortController: () => TshAbortController; addRootCluster: (addr: string) => Promise; listGateways: () => Promise; createGateway: (params: CreateGatewayParams) => Promise; - removeGateway: (gatewayUri: string) => Promise; + removeGateway: (gatewayUri: uri.GatewayUri) => Promise; setGatewayTargetSubresourceName: ( - gatewayUri: string, + gatewayUri: uri.GatewayUri, targetSubresourceName: string ) => Promise; setGatewayLocalPort: ( - gatewayUri: string, + gatewayUri: uri.GatewayUri, localPort: string ) => Promise; - getCluster: (clusterUri: string) => Promise; - getAuthSettings: (clusterUri: string) => Promise; - removeCluster: (clusterUri: string) => Promise; + getCluster: (clusterUri: uri.RootClusterUri) => Promise; + getAuthSettings: (clusterUri: uri.RootClusterUri) => Promise; + removeCluster: (clusterUri: uri.RootClusterUri) => Promise; loginLocal: ( params: LoginLocalParams, abortSignal?: TshAbortSignal @@ -133,7 +155,7 @@ export type TshClient = { params: LoginPasswordlessParams, abortSignal?: TshAbortSignal ) => Promise; - logout: (clusterUri: string) => Promise; + logout: (clusterUri: uri.RootClusterUri) => Promise; transferFile: ( options: FileTransferRequest, abortSignal?: TshAbortSignal @@ -151,7 +173,7 @@ export type TshAbortSignal = { }; interface LoginParamsBase { - clusterUri: string; + clusterUri: uri.RootClusterUri; } export interface LoginLocalParams extends LoginParamsBase { @@ -170,14 +192,14 @@ export interface LoginPasswordlessParams extends LoginParamsBase { } export type CreateGatewayParams = { - targetUri: string; + targetUri: uri.DatabaseUri; port?: string; user: string; subresource_name?: string; }; export type ServerSideParams = { - clusterUri: string; + clusterUri: uri.ClusterUri; search?: string; searchAsRoles?: string; sort?: SortType; @@ -194,7 +216,7 @@ export type ReviewAccessRequestParams = { }; export type CreateAccessRequestParams = { - rootClusterUri: string; + rootClusterUri: uri.RootClusterUri; reason: string; roles: string[]; suggestedReviewers: string[]; @@ -202,7 +224,7 @@ export type CreateAccessRequestParams = { }; export type GetRequestableRolesParams = { - rootClusterUri: string; + rootClusterUri: uri.RootClusterUri; resourceIds?: { kind: ResourceKind; clusterName: string; id: string }[]; }; diff --git a/packages/teleterm/src/services/tshdEvents/index.ts b/packages/teleterm/src/services/tshdEvents/index.ts index 1a4dbc741..9446d8e41 100644 --- a/packages/teleterm/src/services/tshdEvents/index.ts +++ b/packages/teleterm/src/services/tshdEvents/index.ts @@ -3,11 +3,28 @@ import * as grpc from '@grpc/grpc-js'; import * as api from 'teleterm/services/tshd/v1/tshd_events_service_pb'; import * as apiService from 'teleterm/services/tshd/v1/tshd_events_service_grpc_pb'; +import * as uri from 'teleterm/ui/uri'; import Logger from 'teleterm/logger'; import { SubscribeToTshdEvent } from 'teleterm/types'; -export type ReloginRequest = api.ReloginRequest.AsObject; -export type SendNotificationRequest = api.SendNotificationRequest.AsObject; +export interface ReloginRequest extends api.ReloginRequest.AsObject { + rootClusterUri: uri.RootClusterUri; + gatewayCertExpired?: GatewayCertExpired; +} +export interface GatewayCertExpired extends api.GatewayCertExpired.AsObject { + gatewayUri: uri.GatewayUri; + targetUri: uri.DatabaseUri; +} + +export interface SendNotificationRequest + extends api.SendNotificationRequest.AsObject { + cannotProxyGatewayConnection?: CannotProxyGatewayConnection; +} +export interface CannotProxyGatewayConnection + extends api.CannotProxyGatewayConnection.AsObject { + gatewayUri: uri.GatewayUri; + targetUri: uri.DatabaseUri; +} /** * Starts tshd events server. diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterConnect.tsx b/packages/teleterm/src/ui/ClusterConnect/ClusterConnect.tsx index b041aff1b..e431c7ecf 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterConnect.tsx +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterConnect.tsx @@ -4,18 +4,19 @@ import Dialog from 'design/Dialog'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { ClusterConnectReason } from 'teleterm/ui/services/modals'; +import { RootClusterUri } from 'teleterm/ui/uri'; import { ClusterAdd } from './ClusterAdd'; import { ClusterLogin } from './ClusterLogin'; export function ClusterConnect(props: ClusterConnectProps) { const [createdClusterUri, setCreatedClusterUri] = useState< - string | undefined + RootClusterUri | undefined >(); const { clustersService } = useAppContext(); const clusterUri = props.clusterUri || createdClusterUri; - function handleClusterAdd(clusterUri: string): void { + function handleClusterAdd(clusterUri: RootClusterUri): void { const cluster = clustersService.findCluster(clusterUri); if (cluster?.connected) { props.onSuccess(clusterUri); @@ -50,8 +51,8 @@ export function ClusterConnect(props: ClusterConnectProps) { } interface ClusterConnectProps { - clusterUri: string | undefined; + clusterUri?: RootClusterUri; reason: ClusterConnectReason | undefined; onCancel(): void; - onSuccess(clusterUri: string): void; + onSuccess(clusterUri: RootClusterUri): void; } diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts index 35238ca9c..b4d5a1858 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts @@ -21,6 +21,7 @@ import { useAsync } from 'shared/hooks/useAsync'; import * as types from 'teleterm/ui/services/clusters/types'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { assertUnreachable } from 'teleterm/ui/utils'; +import { RootClusterUri } from 'teleterm/ui/uri'; export default function useClusterLogin(props: Props) { const { onSuccess, clusterUri } = props; @@ -183,7 +184,7 @@ export default function useClusterLogin(props: Props) { export type State = ReturnType; export type Props = { - clusterUri: string; + clusterUri: RootClusterUri; onCancel(): void; onSuccess?(): void; }; diff --git a/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts b/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts index 1f1410856..548c4341d 100644 --- a/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts +++ b/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts @@ -1,6 +1,8 @@ import { useAsync } from 'shared/hooks/useAsync'; import { useEffect } from 'react'; +import { RootClusterUri } from 'teleterm/ui/uri'; + import { useAppContext } from '../appContextProvider'; export function useClusterLogout({ clusterUri, onClose, clusterTitle }: Props) { @@ -40,7 +42,7 @@ export function useClusterLogout({ clusterUri, onClose, clusterTitle }: Props) { export type Props = { onClose?(): void; clusterTitle?: string; - clusterUri: string; + clusterUri: RootClusterUri; }; export type State = ReturnType; diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/Applications.tsx b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/Applications.tsx deleted file mode 100644 index b15875d51..000000000 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/Applications.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/* -Copyright 2019 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import styled from 'styled-components'; -import { Flex, Text, ButtonBorder } from 'design'; -import { - pink, - teal, - cyan, - blue, - green, - orange, - brown, - red, - deepOrange, - blueGrey, -} from 'design/theme/palette'; - -import { Cell } from 'design/DataTable'; - -import { Danger } from 'design/Alert'; - -import { Table } from 'teleterm/ui/components/Table'; -import * as types from 'teleterm/ui/services/clusters/types'; - -import { renderLabelCell } from '../renderLabelCell'; - -import { useApps, State } from './useApps'; - -export default function Container() { - const state = useApps(); - return ; -} - -export function AppList(props: State) { - const { apps = [], syncStatus } = props; - - return ( - <> - {syncStatus.status === 'failed' && ( - {syncStatus.statusText} - )} - - - ); -} - -const renderConnectButton = () => { - return ( - - LAUNCH - - ); -}; - -const renderAppIcon = (app: types.Application) => { - return ( - - - - {app.name[0]} - - - - ); -}; - -const renderAddress = (app: types.Application) => { - return https://{app.publicAddr}; -}; - -function getIconColor(appName: string) { - let stringValue = 0; - for (let i = 0; i < appName.length; i++) { - stringValue += appName.charCodeAt(i); - } - - const colors = [ - pink[700], - teal[700], - cyan[700], - blue[700], - green[700], - orange[700], - brown[700], - red[700], - deepOrange[700], - blueGrey[700], - ]; - - return colors[stringValue % 10]; -} - -const StyledTable = styled(Table)` - & > tbody > tr > td { - vertical-align: baseline; - } -`; diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/index.ts b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/index.ts deleted file mode 100644 index fa1e62b01..000000000 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2019 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import Applications from './Applications'; -export default Applications; diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/useApps.ts b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/useApps.ts deleted file mode 100644 index 42d442b97..000000000 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/useApps.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2019 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { useClusterContext } from 'teleterm/ui/DocumentCluster/clusterContext'; - -export function useApps() { - const ctx = useClusterContext(); - const apps = ctx.getApps(); - const syncStatus = ctx.getSyncStatus().apps; - - return { - apps, - syncStatus, - }; -} - -export type State = ReturnType; diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/ClusterResources.tsx b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/ClusterResources.tsx index 3c86f08fa..909acfb90 100644 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/ClusterResources.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/ClusterResources.tsx @@ -23,7 +23,6 @@ import { useClusterContext } from 'teleterm/ui/DocumentCluster/clusterContext'; import SideNav from './SideNav'; import Servers from './Servers'; import Databases from './Databases'; -import Applications from './Applications'; import Kubes from './Kubes'; export default function ClusterResources() { @@ -46,7 +45,6 @@ export default function ClusterResources() { {clusterCtx.isLocationActive('/resources/servers') && } {clusterCtx.isLocationActive('/resources/databases') && } - {clusterCtx.isLocationActive('/resources/apps') && } {clusterCtx.isLocationActive('/resources/kubes') && } diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx index 24ae28e20..ca255e36e 100644 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx @@ -23,6 +23,7 @@ import { useAppContext } from 'teleterm/ui/appContextProvider'; import { retryWithRelogin } from 'teleterm/ui/utils'; import { IAppContext } from 'teleterm/ui/types'; import { GatewayProtocol, makeDatabase } from 'teleterm/ui/services/clusters'; +import { DatabaseUri } from 'teleterm/ui/uri'; import { MenuLoginTheme } from '../MenuLoginTheme'; import { DarkenWhileDisabled } from '../DarkenWhileDisabled'; @@ -119,7 +120,7 @@ function ConnectButton({ protocol, onConnect, }: { - dbUri: string; + dbUri: DatabaseUri; protocol: GatewayProtocol; onConnect: (dbUser: string) => void; }) { @@ -165,7 +166,7 @@ function getMenuLoginOptions( }; } -async function getDatabaseUsers(appContext: IAppContext, dbUri: string) { +async function getDatabaseUsers(appContext: IAppContext, dbUri: DatabaseUri) { try { const dbUsers = await retryWithRelogin(appContext, dbUri, () => appContext.clustersService.getDbUsers(dbUri) diff --git a/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.story.tsx b/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.story.tsx index 726c18ef6..2d1d809a7 100644 --- a/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.story.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.story.tsx @@ -35,15 +35,15 @@ export default { const rootClusterDoc = { kind: 'doc.cluster' as const, - clusterUri: '/clusters/localhost', - uri: '/docs/123', + clusterUri: '/clusters/localhost' as const, + uri: '/docs/123' as const, title: 'sample', }; const leafClusterDoc = { kind: 'doc.cluster' as const, - clusterUri: '/clusters/localhost/leaves/foo', - uri: '/docs/456', + clusterUri: '/clusters/localhost/leaves/foo' as const, + uri: '/docs/456' as const, title: 'sample', }; diff --git a/packages/teleterm/src/ui/DocumentCluster/clusterContext.tsx b/packages/teleterm/src/ui/DocumentCluster/clusterContext.tsx index 02277c3db..87a4aab14 100644 --- a/packages/teleterm/src/ui/DocumentCluster/clusterContext.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/clusterContext.tsx @@ -20,7 +20,7 @@ import { useStore, Store } from 'shared/libs/stores'; import { tsh } from 'teleterm/ui/services/clusters/types'; import { IAppContext } from 'teleterm/ui/types'; -import { routing } from 'teleterm/ui/uri'; +import { ClusterUri, DocumentUri, KubeUri, routing } from 'teleterm/ui/uri'; import { retryWithRelogin } from 'teleterm/ui/utils'; type State = { @@ -37,11 +37,8 @@ class ClusterContext extends Store { private _cluster: tsh.Cluster; readonly appCtx: IAppContext; - - readonly clusterUri: string; - - readonly documentUri: string; - + readonly clusterUri: ClusterUri; + readonly documentUri: DocumentUri; readonly state: State = { navLocation: '/resources/servers', clusterName: '', @@ -52,7 +49,11 @@ class ClusterContext extends Store { statusText: '', }; - constructor(appCtx: IAppContext, clusterUri: string, documentUri: string) { + constructor( + appCtx: IAppContext, + clusterUri: ClusterUri, + documentUri: DocumentUri + ) { super(); this.clusterUri = clusterUri; this.documentUri = documentUri; @@ -72,7 +73,7 @@ class ClusterContext extends Store { }); }; - connectKube = (kubeUri: string) => { + connectKube = (kubeUri: KubeUri) => { this.appCtx.commandLauncher.executeCommand('kube-connect', { kubeUri }); }; @@ -144,12 +145,6 @@ class ClusterContext extends Store { this.setState({ searchValue }); }; - getApps() { - return this.appCtx.clustersService.searchApps(this.clusterUri, { - search: this.state.searchValue, - }); - } - getSyncStatus() { return this.appCtx.clustersService.getClusterSyncStatus(this.clusterUri); } diff --git a/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx b/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx index 4918ab67d..254798f18 100644 --- a/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx +++ b/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx @@ -6,16 +6,18 @@ import { makeErrorAttempt, } from 'shared/hooks/useAsync'; +import { Gateway } from 'teleterm/services/tshd/types'; + import { DocumentGateway } from './DocumentGateway'; export default { title: 'Teleterm/DocumentGateway', }; -const gateway = { - uri: '/bar', +const gateway: Gateway = { + uri: '/gateways/bar', targetName: 'sales-production', - targetUri: '/foo', + targetUri: '/clusters/bar/dbs/foo', targetUser: 'alice', localAddress: 'localhost', localPort: '1337', @@ -43,10 +45,10 @@ export function Online() { } export function OnlineWithLongValues() { - const gateway = { - uri: '/bar', + const gateway: Gateway = { + uri: '/gateways/bar', targetName: 'sales-production', - targetUri: '/foo', + targetUri: '/clusters/bar/dbs/foo', targetUser: 'quux-quuz-foo-bar-quux-quuz-foo-bar-quux-quuz-foo-bar-quux-quuz-foo-bar', localAddress: 'localhost', diff --git a/packages/teleterm/src/ui/DocumentTerminal/useTshFileTransferHandlers.ts b/packages/teleterm/src/ui/DocumentTerminal/useTshFileTransferHandlers.ts index 0b52ca54d..755613691 100644 --- a/packages/teleterm/src/ui/DocumentTerminal/useTshFileTransferHandlers.ts +++ b/packages/teleterm/src/ui/DocumentTerminal/useTshFileTransferHandlers.ts @@ -3,7 +3,7 @@ import { createFileTransferEventsEmitter, } from 'shared/components/FileTransfer'; -import { routing } from 'teleterm/ui/uri'; +import { routing, ServerUri } from 'teleterm/ui/uri'; import { FileTransferDirection } from 'teleterm/services/tshd/v1/service_pb'; import { retryWithRelogin } from 'teleterm/ui/utils'; import { useAppContext } from 'teleterm/ui/appContextProvider'; @@ -82,5 +82,5 @@ type FileTransferRequestObject = { source: string; destination: string; login: string; - serverUri: string; + serverUri: ServerUri; }; diff --git a/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx b/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx index b432ddb35..81258e38f 100644 --- a/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx +++ b/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx @@ -6,12 +6,16 @@ import { DocumentAccessRequests } from 'e-teleterm/ui/DocumentAccessRequests/Doc import { useAppContext } from 'teleterm/ui/appContextProvider'; import * as types from 'teleterm/ui/services/workspacesService'; -import { DocumentsService } from 'teleterm/ui/services/workspacesService'; +import { + DocumentsService, + Workspace, +} from 'teleterm/ui/services/workspacesService'; import DocumentCluster from 'teleterm/ui/DocumentCluster'; import DocumentGateway from 'teleterm/ui/DocumentGateway'; import DocumentTerminal from 'teleterm/ui/DocumentTerminal'; import Document from 'teleterm/ui/Document'; +import { RootClusterUri } from 'teleterm/ui/uri'; import { WorkspaceContextProvider } from './workspaceContext'; import { KeyboardShortcutsPanel } from './KeyboardShortcutsPanel'; @@ -29,7 +33,7 @@ export function DocumentsRenderer() { const workspaces = useMemo( () => Object.entries(workspacesService.getWorkspaces()).map( - ([clusterUri, workspace]) => ({ + ([clusterUri, workspace]: [RootClusterUri, Workspace]) => ({ rootClusterUri: clusterUri, localClusterUri: workspace.localClusterUri, documentsService: diff --git a/packages/teleterm/src/ui/Documents/workspaceContext.tsx b/packages/teleterm/src/ui/Documents/workspaceContext.tsx index 78be17414..8498c5094 100644 --- a/packages/teleterm/src/ui/Documents/workspaceContext.tsx +++ b/packages/teleterm/src/ui/Documents/workspaceContext.tsx @@ -19,18 +19,19 @@ import React from 'react'; import { DocumentsService } from 'teleterm/ui/services/workspacesService'; import { AccessRequestsService } from 'teleterm/ui/services/workspacesService/accessRequestsService'; import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { ClusterUri, RootClusterUri } from 'teleterm/ui/uri'; const WorkspaceContext = React.createContext<{ - rootClusterUri: string; - localClusterUri: string; + rootClusterUri: RootClusterUri; + localClusterUri: ClusterUri; documentsService: DocumentsService; accessRequestsService: AccessRequestsService; }>(null); export const WorkspaceContextProvider: React.FC<{ value: { - rootClusterUri: string; - localClusterUri: string; + rootClusterUri: RootClusterUri; + localClusterUri: ClusterUri; documentsService: DocumentsService; accessRequestsService: AccessRequestsService; }; diff --git a/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx b/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx index 1d178345d..b1d72f914 100644 --- a/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx +++ b/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx @@ -34,7 +34,7 @@ export const Story = () => { workspaces: { '/clusters/localhost': { documents: [], - location: '', + location: undefined, localClusterUri: '/clusters/localhost', accessRequests: { pending: getEmptyPendingAccessRequest(), diff --git a/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx b/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx index 4387041fb..5acd8d346 100644 --- a/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx +++ b/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx @@ -3,9 +3,9 @@ import { screen } from '@testing-library/react'; 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 { Cluster } from 'teleterm/services/tshd/types'; import { ShareFeedback } from './ShareFeedback'; @@ -28,7 +28,7 @@ test('email field is not prefilled with the username if is not an email', () => .mockImplementation(() => { return { loggedInUser: { name: 'alice' }, - } as Cluster.AsObject; + } as Cluster; }); jest @@ -53,7 +53,7 @@ test('email field is prefilled with the username if it looks like an email', () loggedInUser: { name: 'bob@prod.com', }, - } as Cluster.AsObject; + } as Cluster; }); jest diff --git a/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/RecentClusters.tsx b/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/RecentClusters.tsx index 3122e5677..0108d9c7c 100644 --- a/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/RecentClusters.tsx +++ b/packages/teleterm/src/ui/TabHost/ClusterConnectPanel/RecentClusters.tsx @@ -4,6 +4,7 @@ import { Box, ButtonBorder, Card, Text } from 'design'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { getUserWithClusterName } from 'teleterm/ui/utils'; +import { RootClusterUri } from 'teleterm/ui/uri'; export function RecentClusters() { const ctx = useAppContext(); @@ -21,7 +22,7 @@ export function RecentClusters() { uri: cluster.uri, })); - function connect(clusterUri: string): void { + function connect(clusterUri: RootClusterUri): void { ctx.workspacesService.setActiveWorkspace(clusterUri); } diff --git a/packages/teleterm/src/ui/TabHost/TabHost.test.tsx b/packages/teleterm/src/ui/TabHost/TabHost.test.tsx index 411c78198..d81259372 100644 --- a/packages/teleterm/src/ui/TabHost/TabHost.test.tsx +++ b/packages/teleterm/src/ui/TabHost/TabHost.test.tsx @@ -25,12 +25,12 @@ function getMockDocuments(): Document[] { return [ { kind: 'doc.blank', - uri: 'test_uri_1', + uri: '/docs/test_uri_1', title: 'Test 1', }, { kind: 'doc.blank', - uri: 'test_uri_2', + uri: '/docs/test_uri_2', title: 'Test 2', }, ]; @@ -88,17 +88,11 @@ function getTestSetup({ documents }: { documents: Document[] }) { }; const workspacesService: Partial = { - // @ts-expect-error - using mocks - getWorkspacesDocumentsServices() { - return [ - { clusterUri: 'test_uri', workspaceDocumentsService: docsService }, - ]; - }, isDocumentActive(documentUri: string) { return documentUri === documents[0].uri; }, getRootClusterUri() { - return 'test_uri'; + return '/clusters/test_uri'; }, getWorkspaces() { return {}; @@ -112,7 +106,7 @@ function getTestSetup({ documents }: { documents: Document[] }) { }, documents, location: undefined, - localClusterUri: 'test_uri', + localClusterUri: '/clusters/test_uri', }; }, // @ts-expect-error - using mocks @@ -122,7 +116,7 @@ function getTestSetup({ documents }: { documents: Document[] }) { useState: jest.fn(), state: { workspaces: {}, - rootClusterUri: 'test_uri', + rootClusterUri: '/clusters/test_uri', }, }; @@ -211,8 +205,8 @@ test('open new tab', () => { }); const { add, open } = docsService; const mockedClusterDocument: DocumentCluster = { - clusterUri: 'test', - uri: 'test', + clusterUri: '/clusters/test', + uri: '/docs/test', title: 'Test', kind: 'doc.cluster', }; diff --git a/packages/teleterm/src/ui/TabHost/useNewTabOpener.ts b/packages/teleterm/src/ui/TabHost/useNewTabOpener.ts index 5b09f5b47..6e4e77028 100644 --- a/packages/teleterm/src/ui/TabHost/useNewTabOpener.ts +++ b/packages/teleterm/src/ui/TabHost/useNewTabOpener.ts @@ -1,13 +1,14 @@ import { useCallback } from 'react'; import { DocumentsService } from 'teleterm/ui/services/workspacesService'; +import { ClusterUri } from 'teleterm/ui/uri'; export function useNewTabOpener({ documentsService, localClusterUri, }: { documentsService: DocumentsService; - localClusterUri: string; + localClusterUri: ClusterUri; }) { const openClusterTab = useCallback(() => { if (localClusterUri) { diff --git a/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx b/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx index f3a54cb3b..ca2da93be 100644 --- a/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx +++ b/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx @@ -22,60 +22,60 @@ function getMockDocuments(): Document[] { return [ { kind: 'doc.blank', - uri: 'test_uri_1', + uri: '/docs/test_uri_1', title: 'Test 1', }, { kind: 'doc.blank', - uri: 'test_uri_2', + uri: '/docs/test_uri_2', title: 'Test 2', }, { kind: 'doc.blank', - uri: 'test_uri_3', + uri: '/docs/test_uri_3', title: 'Test 3', }, { kind: 'doc.gateway', - uri: 'test_uri_4', + uri: '/docs/test_uri_4', title: 'Test 4', - gatewayUri: '', - targetUri: '', + gatewayUri: '/gateways/gateway4', + targetUri: '/clusters/bar/dbs/foobar', targetName: 'foobar', targetUser: 'foo', }, { kind: 'doc.gateway', - uri: 'test_uri_5', + uri: '/docs/test_uri_5', title: 'Test 5', - gatewayUri: '', - targetUri: '', + gatewayUri: '/gateways/gateway5', + targetUri: '/clusters/bar/dbs/foobar', targetName: 'foobar', targetUser: 'bar', }, { kind: 'doc.cluster', - uri: 'test_uri_6', + uri: '/docs/test_uri_6', title: 'Test 6', - clusterUri: 'none', + clusterUri: '/clusters/foo', }, { kind: 'doc.cluster', - uri: 'test_uri_7', + uri: '/docs/test_uri_7', title: 'Test 7', - clusterUri: 'test_uri', + clusterUri: '/clusters/test_uri', }, { kind: 'doc.cluster', - uri: 'test_uri_8', + uri: '/docs/test_uri_8', title: 'Test 8', - clusterUri: 'test_uri_8', + clusterUri: '/clusters/test_uri_8', }, { kind: 'doc.cluster', - uri: 'test_uri_9', + uri: '/docs/test_uri_9', title: 'Test 9', - clusterUri: 'test_uri_9', + clusterUri: '/clusters/test_uri_9', }, ]; } @@ -120,15 +120,15 @@ function getTestSetup({ documents }: { documents: Document[] }) { isBarCollapsed: false, pending: getEmptyPendingAccessRequest(), }, - localClusterUri: 'test_uri', + localClusterUri: '/clusters/test_uri', documents: [], - location: '', + location: '/docs/1', }; }, useState: jest.fn(), state: { workspaces: {}, - rootClusterUri: '', + rootClusterUri: '/clusters/test_uri', }, }; @@ -209,8 +209,8 @@ test('open new tab', () => { documents: [], }); const mockedClusterDocument: DocumentCluster = { - clusterUri: 'test', - uri: 'test', + clusterUri: '/clusters/test', + uri: '/docs/test', title: 'Test', kind: 'doc.cluster', }; diff --git a/packages/teleterm/src/ui/TabHost/useTabShortcuts.ts b/packages/teleterm/src/ui/TabHost/useTabShortcuts.ts index 0eba35271..36afb53ee 100644 --- a/packages/teleterm/src/ui/TabHost/useTabShortcuts.ts +++ b/packages/teleterm/src/ui/TabHost/useTabShortcuts.ts @@ -22,13 +22,14 @@ import { } from 'teleterm/ui/services/keyboardShortcuts'; import { DocumentsService } from 'teleterm/ui/services/workspacesService'; import { useNewTabOpener } from 'teleterm/ui/TabHost/useNewTabOpener'; +import { ClusterUri } from 'teleterm/ui/uri'; export function useTabShortcuts({ documentsService, localClusterUri, }: { documentsService: DocumentsService; - localClusterUri: string; + localClusterUri: ClusterUri; }) { const { openClusterTab } = useNewTabOpener({ documentsService, diff --git a/packages/teleterm/src/ui/TopBar/Clusters/Clusters.tsx b/packages/teleterm/src/ui/TopBar/Clusters/Clusters.tsx index b476ee1a5..f52281f58 100644 --- a/packages/teleterm/src/ui/TopBar/Clusters/Clusters.tsx +++ b/packages/teleterm/src/ui/TopBar/Clusters/Clusters.tsx @@ -4,8 +4,8 @@ import styled from 'styled-components'; import { Box } from 'design'; import { useKeyboardShortcuts } from 'teleterm/ui/services/keyboardShortcuts'; - import { KeyboardArrowsNavigation } from 'teleterm/ui/components/KeyboardArrowsNavigation'; +import { ClusterUri } from 'teleterm/ui/uri'; import { useClusters } from './useClusters'; import { ClusterSelector } from './ClusterSelector/ClusterSelector'; @@ -15,7 +15,9 @@ import ConfirmClusterChangeDialog from './ConfirmClusterChangeDialog'; export function Clusters() { const iconRef = useRef(); const [isPopoverOpened, setIsPopoverOpened] = useState(false); - const [confirmChangeTo, setConfirmChangeTo] = useState(null); + const [confirmChangeTo, setConfirmChangeTo] = useState( + null + ); const clusters = useClusters(); const togglePopover = useCallback(() => { @@ -31,12 +33,12 @@ export function Clusters() { ) ); - function selectItem(id: string): void { + function selectItem(clusterUri: ClusterUri): void { setIsPopoverOpened(false); if (clusters.hasPendingAccessRequest) { - setConfirmChangeTo(id); + setConfirmChangeTo(clusterUri); } else { - clusters.selectItem(id); + clusters.selectItem(clusterUri); } } diff --git a/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts b/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts index b87c2e106..4a9ee453d 100644 --- a/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts +++ b/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts @@ -1,4 +1,5 @@ import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { ClusterUri } from 'teleterm/ui/uri'; export function useClusters() { const { workspacesService, clustersService, commandLauncher } = @@ -51,7 +52,7 @@ export function useClusters() { clearPendingAccessRequest, selectedItem: localClusterUri && clustersService.findCluster(localClusterUri), - selectItem: (localClusterUri: string) => { + selectItem: (localClusterUri: ClusterUri) => { workspacesService.setWorkspaceLocalClusterUri( rootClusterUri, localClusterUri diff --git a/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx b/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx index fe9429c75..58c599cb5 100644 --- a/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx +++ b/packages/teleterm/src/ui/TopBar/Identity/Identity.story.tsx @@ -1,10 +1,10 @@ import React, { useRef, useEffect } from 'react'; - import Flex from 'design/Flex'; import * as tshd from 'teleterm/services/tshd/types'; import { Identity, IdentityHandler, IdentityProps } from './Identity'; +import { IdentityRootCluster } from './useIdentity'; export default { title: 'Teleterm/Identity', @@ -41,7 +41,7 @@ export function NoRootClusters() { } export function OneClusterWithNoActiveCluster() { - const identityRootCluster = { + const identityRootCluster: IdentityRootCluster = { active: false, clusterName: 'teleport-localhost', userName: '', @@ -63,7 +63,7 @@ export function OneClusterWithNoActiveCluster() { } export function OneClusterWithActiveCluster() { - const identityRootCluster = { + const identityRootCluster: IdentityRootCluster = { active: true, clusterName: 'Teleport-Localhost', userName: 'alice', @@ -99,7 +99,7 @@ export function OneClusterWithActiveCluster() { } export function ManyClustersWithNoActiveCluster() { - const identityRootCluster1 = { + const identityRootCluster1: IdentityRootCluster = { active: false, clusterName: 'orange', userName: 'bob', @@ -107,7 +107,7 @@ export function ManyClustersWithNoActiveCluster() { connected: true, isSyncing: false, }; - const identityRootCluster2 = { + const identityRootCluster2: IdentityRootCluster = { active: false, clusterName: 'violet', userName: 'sammy', @@ -115,7 +115,7 @@ export function ManyClustersWithNoActiveCluster() { connected: true, isSyncing: true, }; - const identityRootCluster3 = { + const identityRootCluster3: IdentityRootCluster = { active: false, clusterName: 'green', userName: '', @@ -141,7 +141,7 @@ export function ManyClustersWithNoActiveCluster() { } export function ManyClustersWithActiveCluster() { - const identityRootCluster1 = { + const identityRootCluster1: IdentityRootCluster = { active: false, clusterName: 'orange', userName: 'bob', @@ -149,7 +149,7 @@ export function ManyClustersWithActiveCluster() { connected: true, isSyncing: true, }; - const identityRootCluster2 = { + const identityRootCluster2: IdentityRootCluster = { active: true, clusterName: 'violet', userName: 'sammy', @@ -157,7 +157,7 @@ export function ManyClustersWithActiveCluster() { connected: true, isSyncing: false, }; - const identityRootCluster3 = { + const identityRootCluster3: IdentityRootCluster = { active: false, clusterName: 'green', userName: '', @@ -198,7 +198,7 @@ export function ManyClustersWithActiveCluster() { } export function LongNamesWithManyRoles() { - const identityRootCluster1 = { + const identityRootCluster1: IdentityRootCluster = { active: false, clusterName: 'orange', userName: 'bob', @@ -206,7 +206,7 @@ export function LongNamesWithManyRoles() { connected: true, isSyncing: true, }; - const identityRootCluster2 = { + const identityRootCluster2: IdentityRootCluster = { active: true, clusterName: 'psv-eindhoven-eredivisie-production-lorem-ipsum', userName: 'ruud-van-nistelrooy-van-der-sar', @@ -214,7 +214,7 @@ export function LongNamesWithManyRoles() { connected: true, isSyncing: false, }; - const identityRootCluster3 = { + const identityRootCluster3: IdentityRootCluster = { active: false, clusterName: 'green', userName: '', diff --git a/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts b/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts index a6c35d692..787bef1bd 100644 --- a/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts +++ b/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts @@ -1,5 +1,6 @@ import { useAppContext } from 'teleterm/ui/appContextProvider'; import { Cluster, LoggedInUser } from 'teleterm/services/tshd/types'; +import { RootClusterUri } from 'teleterm/ui/uri'; export function useIdentity() { const ctx = useAppContext(); @@ -7,7 +8,7 @@ export function useIdentity() { ctx.clustersService.useState(); ctx.workspacesService.useState(); - function changeRootCluster(clusterUri: string): Promise { + function changeRootCluster(clusterUri: RootClusterUri): Promise { return ctx.workspacesService.setActiveWorkspace(clusterUri); } @@ -15,7 +16,7 @@ export function useIdentity() { ctx.commandLauncher.executeCommand('cluster-connect', {}); } - function logout(clusterUri: string): void { + function logout(clusterUri: RootClusterUri): void { ctx.commandLauncher.executeCommand('cluster-logout', { clusterUri }); } @@ -65,7 +66,7 @@ export interface IdentityRootCluster { active: boolean; clusterName: string; userName: string; - uri: string; + uri: RootClusterUri; connected: boolean; isSyncing: boolean; } diff --git a/packages/teleterm/src/ui/TopBar/NavigationMenu/NavigationMenu.tsx b/packages/teleterm/src/ui/TopBar/NavigationMenu/NavigationMenu.tsx index 65f444c97..1c67d55b3 100644 --- a/packages/teleterm/src/ui/TopBar/NavigationMenu/NavigationMenu.tsx +++ b/packages/teleterm/src/ui/TopBar/NavigationMenu/NavigationMenu.tsx @@ -8,6 +8,7 @@ import { Box, Text, Flex } from 'design'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { DocumentsService } from 'teleterm/ui/services/workspacesService'; import { TopBarButton } from 'teleterm/ui/TopBar/TopBarButton'; +import { RootClusterUri } from 'teleterm/ui/uri'; import { useIdentity } from '../Identity/useIdentity'; @@ -15,7 +16,7 @@ import { NavigationItem } from './NavigationItem'; function getNavigationItems( documentsService: DocumentsService, - clusterUri: string + clusterUri: RootClusterUri ): { title: string; Icon: JSX.Element; diff --git a/packages/teleterm/src/ui/appContext.ts b/packages/teleterm/src/ui/appContext.ts index 8858bc190..96fe3c923 100644 --- a/packages/teleterm/src/ui/appContext.ts +++ b/packages/teleterm/src/ui/appContext.ts @@ -19,6 +19,10 @@ import { ElectronGlobals, SubscribeToTshdEvent, } from 'teleterm/types'; +import { + ReloginRequest, + SendNotificationRequest, +} from 'teleterm/services/tshdEvents'; import { ClustersService } from 'teleterm/ui/services/clusters'; import { ModalsService } from 'teleterm/ui/services/modals'; import { TerminalsService } from 'teleterm/ui/services/terminals'; @@ -131,11 +135,16 @@ export default class AppContext implements IAppContext { 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); + return this.reloginService.relogin( + request as ReloginRequest, + onCancelled + ); }); this.subscribeToTshdEvent('sendNotification', ({ request }) => { - this.tshdNotificationsService.sendNotification(request); + this.tshdNotificationsService.sendNotification( + request as SendNotificationRequest + ); }); } } diff --git a/packages/teleterm/src/ui/commandLauncher.ts b/packages/teleterm/src/ui/commandLauncher.ts index 9ec43d867..3fa4cc700 100644 --- a/packages/teleterm/src/ui/commandLauncher.ts +++ b/packages/teleterm/src/ui/commandLauncher.ts @@ -15,7 +15,13 @@ limitations under the License. */ import { IAppContext } from 'teleterm/ui/types'; -import { routing } from 'teleterm/ui/uri'; +import { + ClusterUri, + KubeUri, + RootClusterUri, + routing, + ServerUri, +} from 'teleterm/ui/uri'; import { tsh } from 'teleterm/ui/services/clusters/types'; import { TrackedKubeConnection } from 'teleterm/ui/services/connectionTracker'; @@ -26,7 +32,7 @@ const commands = { description: '', run( ctx: IAppContext, - args: { loginHost: string; localClusterUri: string } + args: { loginHost: string; localClusterUri: ClusterUri } ) { const { loginHost, localClusterUri } = args; const rootClusterUri = routing.ensureRootClusterUri(localClusterUri); @@ -62,7 +68,7 @@ const commands = { console.error('Ambiguous host'); } - let serverUri: string, serverHostname: string; + let serverUri: ServerUri, serverHostname: string; if (server) { serverUri = server.uri; @@ -89,7 +95,7 @@ const commands = { 'kube-connect': { displayName: '', description: '', - run(ctx: IAppContext, args: { kubeUri: string }) { + run(ctx: IAppContext, args: { kubeUri: KubeUri }) { const documentsService = ctx.workspacesService.getActiveWorkspaceDocumentService(); const kubeDoc = documentsService.createTshKubeDocument({ @@ -110,8 +116,11 @@ const commands = { 'cluster-connect': { displayName: '', description: '', - run(ctx: IAppContext, args: { clusterUri?: string; onSuccess?(): void }) { - const defaultHandler = (clusterUri: string) => { + run( + ctx: IAppContext, + args: { clusterUri?: RootClusterUri; onSuccess?(): void } + ) { + const defaultHandler = (clusterUri: RootClusterUri) => { ctx.commandLauncher.executeCommand('cluster-open', { clusterUri }); }; @@ -125,7 +134,7 @@ const commands = { 'cluster-logout': { displayName: '', description: '', - run(ctx: IAppContext, args: { clusterUri: string }) { + run(ctx: IAppContext, args: { clusterUri: RootClusterUri }) { const cluster = ctx.clustersService.findCluster(args.clusterUri); ctx.modalsService.openRegularDialog({ kind: 'cluster-logout', @@ -138,7 +147,7 @@ const commands = { 'cluster-open': { displayName: '', description: '', - async run(ctx: IAppContext, args: { clusterUri: string }) { + async run(ctx: IAppContext, args: { clusterUri: ClusterUri }) { const { clusterUri } = args; const rootCluster = ctx.clustersService.findRootClusterByResource(clusterUri); diff --git a/packages/teleterm/src/ui/services/clusters/clustersService.test.ts b/packages/teleterm/src/ui/services/clusters/clustersService.test.ts index 9a77b7fea..0a02a47cc 100644 --- a/packages/teleterm/src/ui/services/clusters/clustersService.test.ts +++ b/packages/teleterm/src/ui/services/clusters/clustersService.test.ts @@ -1,11 +1,11 @@ import { tsh, SyncStatus } from 'teleterm/ui/services/clusters/types'; - import { NotificationsService } from 'teleterm/ui/services/notifications'; import { MainProcessClient } from 'teleterm/mainProcess/types'; +import { RootClusterUri } from 'teleterm/ui/uri'; import { ClustersService } from './clustersService'; -const clusterUri = '/clusters/test'; +const clusterUri: RootClusterUri = '/clusters/test'; const clusterMock: tsh.Cluster = { uri: clusterUri, @@ -35,7 +35,7 @@ const dbMock: tsh.Database = { }; const gatewayMock: tsh.Gateway = { - uri: 'gatewayTestUri', + uri: '/gateways/gatewayTestUri', localAddress: 'localhost', localPort: '2000', protocol: 'https', @@ -71,23 +71,6 @@ const kubeMock: tsh.Kube = { ], }; -const appMock: tsh.Application = { - uri: `${clusterUri}/apps/appTestUri`, - name: 'TestApp', - labelsList: [ - { - name: 'Type', - value: 'OnDemand', - }, - ], - appUri: 'appTestUri', - awsConsole: false, - awsRolesList: [], - description: '', - fqdn: '', - publicAddr: 'app.test', -}; - const NotificationsServiceMock = NotificationsService as jest.MockedClass< typeof NotificationsService >; @@ -130,7 +113,6 @@ function testIfClusterResourcesHaveBeenCleared(service: ClustersService): void { syncing: false, dbs: { status: '' }, servers: { status: '' }, - apps: { status: '' }, kubes: { status: '' }, }); } @@ -231,7 +213,7 @@ test('create a gateway', async () => { const service = createService({ createGateway, }); - const targetUri = 'testId'; + const targetUri = '/clusters/foo/dbs/testId'; const port = '2000'; const user = 'alice'; @@ -248,7 +230,7 @@ test('remove a gateway', async () => { const service = createService({ removeGateway, }); - const gatewayUri = 'gatewayUri'; + const gatewayUri = '/gateways/gatewayUri'; await service.removeGateway(gatewayUri); @@ -337,17 +319,6 @@ test('find kubes by cluster uri', () => { expect(foundKubes).toStrictEqual([kubeMock]); }); -test('find apps by cluster uri', () => { - const service = createService({}); - service.setState(draftState => { - draftState.apps.set(appMock.uri, appMock); - }); - - const foundApps = service.findApps(clusterUri); - - expect(foundApps).toStrictEqual([appMock]); -}); - test('find cluster by resource uri', () => { const service = createService({}); service.setState(draftState => { @@ -378,24 +349,6 @@ test.each([ expect(foundDbs).toStrictEqual([dbMock]); }); -test.each([ - { prop: 'name', value: appMock.name }, - { prop: 'publicAddr', value: appMock.publicAddr }, - { prop: 'description', value: appMock.description }, - { prop: 'labelsList', value: appMock.labelsList[0].value }, -])('search apps by prop: $prop', ({ value }) => { - const service = createService({}); - service.setState(draftState => { - draftState.apps.set(appMock.uri, appMock); - }); - - const foundApps = service.searchApps(clusterUri, { - search: value.toLocaleLowerCase(), - }); - - expect(foundApps).toStrictEqual([appMock]); -}); - test.each([ { prop: 'name', value: kubeMock.name }, { prop: 'labelsList', value: kubeMock.labelsList[0].value }, diff --git a/packages/teleterm/src/ui/services/clusters/clustersService.ts b/packages/teleterm/src/ui/services/clusters/clustersService.ts index 4b8752284..247ff9086 100644 --- a/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -10,7 +10,7 @@ import { } from 'shared/services/databases'; import { pipe } from 'shared/utils/pipe'; -import { routing } from 'teleterm/ui/uri'; +import * as uri from 'teleterm/ui/uri'; import { NotificationsService } from 'teleterm/ui/services/notifications'; import { Cluster, @@ -37,9 +37,10 @@ import { Kube, } from './types'; +const { routing } = uri; + export function createClusterServiceState(): ClustersServiceState { return { - apps: new Map(), kubes: new Map(), clusters: new Map(), gateways: new Map(), @@ -48,7 +49,6 @@ export function createClusterServiceState(): ClustersServiceState { serversSyncStatus: new Map(), dbsSyncStatus: new Map(), kubesSyncStatus: new Map(), - appsSyncStatus: new Map(), }; } @@ -75,7 +75,7 @@ export class ClustersService extends ImmutableStore { return cluster; } - async logout(clusterUri: string) { + async logout(clusterUri: uri.RootClusterUri) { // TODO(gzdunek): logout and removeCluster should be combined into a single acton in tshd await this.client.logout(clusterUri); this.removeResources(clusterUri); @@ -101,7 +101,7 @@ export class ClustersService extends ImmutableStore { await this.syncRootClusterAndCatchErrors(params.clusterUri); } - async syncRootClusterAndCatchErrors(clusterUri: string) { + async syncRootClusterAndCatchErrors(clusterUri: uri.RootClusterUri) { try { await this.syncRootCluster(clusterUri); } catch (e) { @@ -116,7 +116,7 @@ export class ClustersService extends ImmutableStore { } } - async syncRootCluster(clusterUri: string) { + async syncRootCluster(clusterUri: uri.RootClusterUri) { try { await Promise.all([ // syncClusterInfo never fails with a retryable error, only syncLeafClusters does. @@ -132,7 +132,6 @@ export class ClustersService extends ImmutableStore { // // Arguably, it is a bit of a race condition, as we assume that syncClusterInfo will return // before syncLeafClusters, but for now this is a condition we can live with. - this.syncApps(clusterUri); this.syncDbs(clusterUri); this.syncServers(clusterUri); this.syncKubes(clusterUri); @@ -140,7 +139,7 @@ export class ClustersService extends ImmutableStore { } } - async syncLeafCluster(clusterUri: string) { + async syncLeafCluster(clusterUri: uri.LeafClusterUri) { try { // Sync leaf clusters list, so that in case of an error that can be resolved with login we can // propagate that error up. @@ -151,9 +150,10 @@ export class ClustersService extends ImmutableStore { } } - private async syncLeafClusterResourcesAndCatchErrors(clusterUri: string) { + private async syncLeafClusterResourcesAndCatchErrors( + clusterUri: uri.LeafClusterUri + ) { // Functions below handle their own errors, so we don't need to await them. - this.syncApps(clusterUri); this.syncDbs(clusterUri); this.syncServers(clusterUri); this.syncKubes(clusterUri); @@ -177,14 +177,14 @@ export class ClustersService extends ImmutableStore { } } - async syncCluster(clusterUri: string) { + async syncCluster(clusterUri: uri.ClusterUri) { const cluster = this.findCluster(clusterUri); if (!cluster) { throw Error(`missing cluster: ${clusterUri}`); } if (cluster.leaf) { - return await this.syncLeafCluster(clusterUri); + return await this.syncLeafCluster(clusterUri as uri.LeafClusterUri); } else { return await this.syncRootCluster(clusterUri); } @@ -204,7 +204,7 @@ export class ClustersService extends ImmutableStore { } } - async syncKubes(clusterUri: string) { + async syncKubes(clusterUri: uri.ClusterUri) { const cluster = this.state.clusters.get(clusterUri); if (!cluster.connected) { this.setState(draft => { @@ -237,40 +237,7 @@ export class ClustersService extends ImmutableStore { } } - async syncApps(clusterUri: string) { - const cluster = this.state.clusters.get(clusterUri); - if (!cluster.connected) { - this.setState(draft => { - draft.appsSyncStatus.delete(clusterUri); - helpers.updateMap(clusterUri, draft.apps, []); - }); - - return; - } - - this.setState(draft => { - draft.appsSyncStatus.set(clusterUri, { - status: 'processing', - }); - }); - - try { - const received = await this.client.listApps(clusterUri); - this.setState(draft => { - draft.appsSyncStatus.set(clusterUri, { status: 'ready' }); - helpers.updateMap(clusterUri, draft.apps, received); - }); - } catch (err) { - this.setState(draft => { - draft.appsSyncStatus.set(clusterUri, { - status: 'failed', - statusText: err.message, - }); - }); - } - } - - async syncDbs(clusterUri: string) { + async syncDbs(clusterUri: uri.ClusterUri) { const cluster = this.state.clusters.get(clusterUri); if (!cluster.connected) { this.setState(draft => { @@ -303,15 +270,17 @@ export class ClustersService extends ImmutableStore { } } - async syncLeafClusters(clusterUri: string) { + async syncLeafClusters(clusterUri: uri.RootClusterUri) { const leaves = await this.syncLeafClustersList(clusterUri); leaves .filter(c => c.connected) - .forEach(c => this.syncLeafClusterResourcesAndCatchErrors(c.uri)); + .forEach(c => + this.syncLeafClusterResourcesAndCatchErrors(c.uri as uri.LeafClusterUri) + ); } - private async syncLeafClustersList(clusterUri: string) { + private async syncLeafClustersList(clusterUri: uri.RootClusterUri) { const leaves = await this.client.listLeafClusters(clusterUri); this.setState(draft => { @@ -326,7 +295,7 @@ export class ClustersService extends ImmutableStore { return leaves; } - async syncServers(clusterUri: string) { + async syncServers(clusterUri: uri.ClusterUri) { const cluster = this.state.clusters.get(clusterUri); if (!cluster.connected) { this.setState(draft => { @@ -368,7 +337,7 @@ export class ClustersService extends ImmutableStore { return this.client.getRequestableRoles(params); } - getAssumedRequests(rootClusterUri: string) { + getAssumedRequests(rootClusterUri: uri.RootClusterUri) { const cluster = this.state.clusters.get(rootClusterUri); if (!cluster?.connected) { return {}; @@ -377,11 +346,11 @@ export class ClustersService extends ImmutableStore { return cluster.loggedInUser?.assumedRequests || {}; } - getAssumedRequest(rootClusterUri: string, requestId: string) { + getAssumedRequest(rootClusterUri: uri.RootClusterUri, requestId: string) { return this.getAssumedRequests(rootClusterUri)[requestId]; } - async getAccessRequests(rootClusterUri: string) { + async getAccessRequests(rootClusterUri: uri.RootClusterUri) { const cluster = this.state.clusters.get(rootClusterUri); if (!cluster.connected) { return; @@ -390,7 +359,10 @@ export class ClustersService extends ImmutableStore { return this.client.getAccessRequests(rootClusterUri); } - async deleteAccessRequest(rootClusterUri: string, requestId: string) { + async deleteAccessRequest( + rootClusterUri: uri.RootClusterUri, + requestId: string + ) { const cluster = this.state.clusters.get(rootClusterUri); if (!cluster.connected) { return; @@ -399,7 +371,7 @@ export class ClustersService extends ImmutableStore { } async assumeRole( - rootClusterUri: string, + rootClusterUri: uri.RootClusterUri, requestIds: string[], dropIds: string[] ) { @@ -411,7 +383,10 @@ export class ClustersService extends ImmutableStore { return this.syncCluster(rootClusterUri); } - async getAccessRequest(rootClusterUri: string, requestId: string) { + async getAccessRequest( + rootClusterUri: uri.RootClusterUri, + requestId: string + ) { const cluster = this.state.clusters.get(rootClusterUri); if (!cluster.connected) { return; @@ -421,7 +396,7 @@ export class ClustersService extends ImmutableStore { } async reviewAccessRequest( - rootClusterUri: string, + rootClusterUri: uri.RootClusterUri, params: ReviewAccessRequestParams ) { const cluster = this.state.clusters.get(rootClusterUri); @@ -444,7 +419,7 @@ export class ClustersService extends ImmutableStore { /** * Removes cluster and its leaf clusters (if any) */ - async removeCluster(clusterUri: string) { + async removeCluster(clusterUri: uri.RootClusterUri) { await this.client.removeCluster(clusterUri); const leafClustersUris = this.getClusters() .filter( @@ -465,7 +440,7 @@ export class ClustersService extends ImmutableStore { }); } - async getAuthSettings(clusterUri: string) { + async getAuthSettings(clusterUri: uri.RootClusterUri) { return (await this.client.getAuthSettings(clusterUri)) as AuthSettings; } @@ -477,7 +452,7 @@ export class ClustersService extends ImmutableStore { return gateway; } - async removeGateway(gatewayUri: string) { + async removeGateway(gatewayUri: uri.GatewayUri) { try { await this.client.removeGateway(gatewayUri); this.setState(draft => { @@ -499,7 +474,7 @@ export class ClustersService extends ImmutableStore { } async setGatewayTargetSubresourceName( - gatewayUri: string, + gatewayUri: uri.GatewayUri, targetSubresourceName: string ) { if (!this.findGateway(gatewayUri)) { @@ -518,7 +493,7 @@ export class ClustersService extends ImmutableStore { return gateway; } - async setGatewayLocalPort(gatewayUri: string, localPort: string) { + async setGatewayLocalPort(gatewayUri: uri.GatewayUri, localPort: string) { if (!this.findGateway(gatewayUri)) { throw new Error(`Could not find gateway ${gatewayUri}`); } @@ -535,43 +510,37 @@ export class ClustersService extends ImmutableStore { return gateway; } - findCluster(clusterUri: string) { + findCluster(clusterUri: uri.ClusterUri) { return this.state.clusters.get(clusterUri); } - findDbs(clusterUri: string) { + findDbs(clusterUri: uri.ClusterUri) { return [...this.state.dbs.values()].filter(db => routing.isClusterDb(clusterUri, db.uri) ); } - findGateway(gatewayUri: string) { + findGateway(gatewayUri: uri.GatewayUri) { return this.state.gateways.get(gatewayUri); } - findDb(dbUri: string) { + findDb(dbUri: uri.DatabaseUri) { return this.state.dbs.get(dbUri); } - findApps(clusterUri: string) { - return [...this.state.apps.values()].filter(s => - routing.isClusterApp(clusterUri, s.uri) - ); - } - - findKubes(clusterUri: string) { + findKubes(clusterUri: uri.ClusterUri) { return [...this.state.kubes.values()].filter(s => routing.isClusterKube(clusterUri, s.uri) ); } - findServers(clusterUri: string) { + findServers(clusterUri: uri.ClusterUri) { return [...this.state.servers.values()].filter(s => routing.isClusterServer(clusterUri, s.uri) ); } - findGateways(clusterUri: string) { + findGateways(clusterUri: uri.ClusterUri) { return [...this.state.gateways.values()].filter(s => routing.belongsToProfile(clusterUri, s.targetUri) ); @@ -599,7 +568,7 @@ export class ClustersService extends ImmutableStore { return this.findCluster(rootClusterUri); } - getServer(serverUri: string) { + getServer(serverUri: uri.ServerUri) { return this.state.servers.get(serverUri); } @@ -611,24 +580,21 @@ export class ClustersService extends ImmutableStore { return [...this.state.clusters.values()]; } - getClusterSyncStatus(clusterUri: string) { + getClusterSyncStatus(clusterUri: uri.ClusterUri) { const empty: SyncStatus = { status: '' }; const dbs = this.state.dbsSyncStatus.get(clusterUri) || empty; const servers = this.state.serversSyncStatus.get(clusterUri) || empty; - const apps = this.state.appsSyncStatus.get(clusterUri) || empty; const kubes = this.state.kubesSyncStatus.get(clusterUri) || empty; const syncing = dbs.status === 'processing' || servers.status === 'processing' || - apps.status === 'processing' || kubes.status === 'processing'; return { syncing, dbs, servers, - apps, kubes, }; } @@ -645,7 +611,7 @@ export class ClustersService extends ImmutableStore { return [...this.state.dbs.values()]; } - async getDbUsers(dbUri: string): Promise { + async getDbUsers(dbUri: uri.DatabaseUri): Promise { return await this.client.listDatabaseUsers(dbUri); } @@ -669,7 +635,7 @@ export class ClustersService extends ImmutableStore { return useStore(this).state; } - private async syncClusterInfo(clusterUri: string) { + private async syncClusterInfo(clusterUri: uri.RootClusterUri) { const cluster = await this.client.getCluster(clusterUri); const assumedRequests = cluster.loggedInUser ? await this.fetchClusterAssumedRequests( @@ -696,7 +662,7 @@ export class ClustersService extends ImmutableStore { private async fetchClusterAssumedRequests( activeRequestsList: string[], - clusterUri: string + clusterUri: uri.RootClusterUri ) { return ( await Promise.all( @@ -714,7 +680,7 @@ export class ClustersService extends ImmutableStore { }, {}); } - private removeResources(clusterUri: string) { + private removeResources(clusterUri: uri.ClusterUri) { this.setState(draft => { this.findDbs(clusterUri).forEach(db => { draft.dbs.delete(db.uri); @@ -724,10 +690,6 @@ export class ClustersService extends ImmutableStore { draft.servers.delete(server.uri); }); - this.findApps(clusterUri).forEach(app => { - draft.apps.delete(app.uri); - }); - this.findKubes(clusterUri).forEach(kube => { draft.kubes.delete(kube.uri); }); @@ -735,11 +697,10 @@ export class ClustersService extends ImmutableStore { draft.serversSyncStatus.delete(clusterUri); draft.dbsSyncStatus.delete(clusterUri); draft.kubesSyncStatus.delete(clusterUri); - draft.appsSyncStatus.delete(clusterUri); }); } - searchDbs(clusterUri: string, query: SearchQuery) { + searchDbs(clusterUri: uri.ClusterUri, query: SearchQuery) { const databases = this.findDbs(clusterUri); return databases.filter(obj => isMatch(obj, query.search, { @@ -753,20 +714,6 @@ export class ClustersService extends ImmutableStore { ); } - searchApps(clusterUri: string, query: SearchQuery) { - const apps = this.findApps(clusterUri); - return apps.filter(obj => - isMatch(obj, query.search, { - searchableProps: ['name', 'publicAddr', 'description', 'labelsList'], - cb: (targetValue, searchValue, propName) => { - if (propName === 'labelsList') { - return this._isIncludedInTagTargetValue(targetValue, searchValue); - } - }, - }) - ); - } - searchClusters(value: string) { const clusters = this.getClusters(); return clusters.filter(s => { @@ -774,7 +721,7 @@ export class ClustersService extends ImmutableStore { }); } - searchKubes(clusterUri: string, query: SearchQuery) { + searchKubes(clusterUri: uri.ClusterUri, query: SearchQuery) { const kubes = this.findKubes(clusterUri); return kubes.filter(obj => isMatch(obj, query.search, { @@ -788,7 +735,10 @@ export class ClustersService extends ImmutableStore { ); } - searchServers(clusterUri: string, query: SearchQueryWithProps) { + searchServers( + clusterUri: uri.ClusterUri, + query: SearchQueryWithProps + ) { const servers = this.findServers(clusterUri); const searchableProps = query.searchableProps || [ 'hostname', @@ -846,9 +796,9 @@ type SearchQueryWithProps = SearchQuery & { }; const helpers = { - updateMap( + updateMap( parentUri = '', - map: Map, + map: Map, received: T[] ) { // delete all entries under given uri diff --git a/packages/teleterm/src/ui/services/clusters/types.ts b/packages/teleterm/src/ui/services/clusters/types.ts index 130557b09..6bfc52696 100644 --- a/packages/teleterm/src/ui/services/clusters/types.ts +++ b/packages/teleterm/src/ui/services/clusters/types.ts @@ -17,6 +17,7 @@ limitations under the License. import * as shared from 'shared/services/types'; import * as tsh from 'teleterm/services/tshd/types'; +import * as uri from 'teleterm/ui/uri'; export type SyncStatus = { status: 'processing' | 'ready' | 'failed' | ''; @@ -79,14 +80,12 @@ export interface AuthSettings extends tsh.AuthSettings { export { tsh }; export type ClustersServiceState = { - clusters: Map; - gateways: Map; - apps: Map; - servers: Map; - kubes: Map; - dbs: Map; - kubesSyncStatus: Map; - appsSyncStatus: Map; - serversSyncStatus: Map; - dbsSyncStatus: Map; + clusters: Map; + gateways: Map; + servers: Map; + kubes: Map; + dbs: Map; + kubesSyncStatus: Map; + serversSyncStatus: Map; + dbsSyncStatus: Map; }; diff --git a/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts b/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts index afaaba51d..b99948bc0 100644 --- a/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts +++ b/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts @@ -120,7 +120,7 @@ it('updates the port of a gateway connection when the underlying doc gets update const document: DocumentGateway = { kind: 'doc.gateway', - uri: 'test-doc-uri', + uri: '/docs/test-doc-uri', title: 'Test title', gatewayUri: '/gateways/4f68927b-579c-47a8-b965-efa8159203c9', targetUri: '/clusters/localhost/dbs/test', @@ -137,8 +137,8 @@ it('updates the port of a gateway connection when the underlying doc gets update pending: getEmptyPendingAccessRequest(), isBarCollapsed: false, }, - localClusterUri: '', - location: '', + localClusterUri: '/clusters/localhost', + location: document.uri, documents: [document], }; }); diff --git a/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts b/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts index 2a0a1dffa..10840c6ec 100644 --- a/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts +++ b/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts @@ -22,8 +22,7 @@ import { WorkspacesService, } from 'teleterm/ui/services/workspacesService'; import { StatePersistenceService } from 'teleterm/ui/services/statePersistence'; - -import { routing } from 'teleterm/ui/uri'; +import { RootClusterUri, routing } from 'teleterm/ui/uri'; import { ImmutableStore } from '../immutableStore'; @@ -173,7 +172,9 @@ export class ConnectionTrackerService extends ImmutableStore { const docService = - this._workspacesService.getWorkspaceDocumentService(clusterUri); + this._workspacesService.getWorkspaceDocumentService( + clusterUri as RootClusterUri + ); return docService?.getDocuments(); }) .filter(Boolean) diff --git a/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionOperationsFactory.ts b/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionOperationsFactory.ts index 26581296a..b0f1b7e67 100644 --- a/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionOperationsFactory.ts +++ b/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionOperationsFactory.ts @@ -1,4 +1,4 @@ -import { routing } from 'teleterm/ui/uri'; +import { LeafClusterUri, RootClusterUri, routing } from 'teleterm/ui/uri'; import { ClustersService } from 'teleterm/ui/services/clusters'; import { WorkspacesService } from 'teleterm/ui/services/workspacesService'; @@ -176,13 +176,7 @@ export class TrackedConnectionOperationsFactory { }; } - private getClusterUris({ - rootClusterId, - leafClusterId, - }: { - rootClusterId: string; - leafClusterId: string; - }): { rootClusterUri: string; leafClusterUri: string } { + private getClusterUris({ rootClusterId, leafClusterId }) { const rootClusterUri = routing.getClusterUri({ rootClusterId, }); @@ -192,16 +186,18 @@ export class TrackedConnectionOperationsFactory { }); return { - rootClusterUri, + rootClusterUri: rootClusterUri as RootClusterUri, leafClusterUri: - rootClusterUri === leafClusterUri ? undefined : leafClusterUri, + rootClusterUri === leafClusterUri + ? undefined + : (leafClusterUri as LeafClusterUri), }; } } interface TrackedConnectionOperations { - rootClusterUri: string; - leafClusterUri: string; + rootClusterUri: RootClusterUri; + leafClusterUri: LeafClusterUri; activate(): void; diff --git a/packages/teleterm/src/ui/services/connectionTracker/types.ts b/packages/teleterm/src/ui/services/connectionTracker/types.ts index cdbd72722..98a135cc4 100644 --- a/packages/teleterm/src/ui/services/connectionTracker/types.ts +++ b/packages/teleterm/src/ui/services/connectionTracker/types.ts @@ -1,3 +1,5 @@ +import { DatabaseUri, GatewayUri, KubeUri, ServerUri } from 'teleterm/ui/uri'; + type TrackedConnectionBase = { connected: boolean; id: string; @@ -7,24 +9,24 @@ type TrackedConnectionBase = { export interface TrackedServerConnection extends TrackedConnectionBase { kind: 'connection.server'; title: string; - serverUri: string; + serverUri: ServerUri; login: string; } export interface TrackedGatewayConnection extends TrackedConnectionBase { kind: 'connection.gateway'; - targetUri: string; + targetUri: DatabaseUri; targetName: string; targetUser?: string; port?: string; - gatewayUri: string; + gatewayUri: GatewayUri; targetSubresourceName?: string; } export interface TrackedKubeConnection extends TrackedConnectionBase { kind: 'connection.kube'; kubeConfigRelativePath: string; - kubeUri: string; + kubeUri: KubeUri; } export type TrackedConnection = diff --git a/packages/teleterm/src/ui/services/modals/modalsService.ts b/packages/teleterm/src/ui/services/modals/modalsService.ts index c3ff5706e..432bf0df5 100644 --- a/packages/teleterm/src/ui/services/modals/modalsService.ts +++ b/packages/teleterm/src/ui/services/modals/modalsService.ts @@ -17,6 +17,7 @@ limitations under the License. import { useStore } from 'shared/libs/stores'; import * as types from 'teleterm/services/tshd/types'; +import { RootClusterUri } from 'teleterm/ui/uri'; import { ImmutableStore } from '../immutableStore'; @@ -93,8 +94,8 @@ export class ModalsService extends ImmutableStore { // TODO(ravicious): Remove this method in favor of calling openRegularDialog directly. openClusterConnectDialog(options: { - clusterUri?: string; - onSuccess?(clusterUri: string): void; + clusterUri?: RootClusterUri; + onSuccess?(clusterUri: RootClusterUri): void; onCancel?(): void; }) { return this.openRegularDialog({ @@ -141,9 +142,10 @@ export interface DialogBase { export interface DialogClusterConnect { kind: 'cluster-connect'; - clusterUri?: string; + clusterUri?: RootClusterUri; reason?: ClusterConnectReason; onSuccess?(clusterUri: string): void; + onSuccess?(clusterUri: RootClusterUri): void; onCancel?(): void; } @@ -159,7 +161,7 @@ export type ClusterConnectReason = ClusterConnectReasonGatewayCertExpired; export interface DialogClusterLogout { kind: 'cluster-logout'; - clusterUri: string; + clusterUri: RootClusterUri; clusterTitle: string; } diff --git a/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts b/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts index 14de70380..dc66d47c7 100644 --- a/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts +++ b/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts @@ -198,9 +198,9 @@ test('parse returns correct result for a database name suggestion', async () => isBarCollapsed: false, pending: getEmptyPendingAccessRequest(), }, - localClusterUri: 'test_uri', + localClusterUri: '/clusters/test_uri', documents: [], - location: '', + location: '/docs/1', })); jest .spyOn(ResourcesServiceMock.prototype, 'fetchDatabases') @@ -209,7 +209,7 @@ test('parse returns correct result for a database name suggestion', async () => agentsList: [ { hostname: 'foobar', - uri: '', + uri: '/clusters/test_uri/dbs/foobar', name: '', desc: '', protocol: '', @@ -380,7 +380,7 @@ test('parse returns correct result for an SSH host suggestion right after user@' hostname: 'bazbar', name: '', addr: '', - uri: '', + uri: '/clusters/foo/servers/bazbar', tunnel: false, labelsList: null, }, @@ -390,19 +390,17 @@ test('parse returns correct result for an SSH host suggestion right after user@' }); }); jest - .spyOn(ClustersServiceMock.prototype, 'searchServers') - .mockImplementation(() => { - return [ - { - hostname: 'bazbar', - name: '', - addr: '', - uri: '', - tunnel: false, - labelsList: null, - }, - ]; - }); + .spyOn(WorkspacesServiceMock.prototype, 'getActiveWorkspace') + .mockImplementation(() => ({ + accessRequests: { + assumed: {}, + isBarCollapsed: false, + pending: getEmptyPendingAccessRequest(), + }, + localClusterUri: '/clusters/test_uri', + documents: [], + location: '/docs/1', + })); const quickInputService = new QuickInputService( new CommandLauncherMock(undefined), new ClustersServiceMock(undefined, undefined, undefined), @@ -439,7 +437,7 @@ test('parse returns correct result for a partial match on an SSH host suggestion hostname: 'bazbar', name: '', addr: '', - uri: '', + uri: '/clusters/foo/servers/bazbar', tunnel: false, labelsList: null, }, @@ -456,9 +454,9 @@ test('parse returns correct result for a partial match on an SSH host suggestion isBarCollapsed: false, pending: getEmptyPendingAccessRequest(), }, - localClusterUri: 'test_uri', + localClusterUri: '/clusters/test_uri', documents: [], - location: '', + location: '/docs/1', })); const quickInputService = new QuickInputService( new CommandLauncherMock(undefined), diff --git a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts index 2495b02f3..8760cef63 100644 --- a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts +++ b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts @@ -5,9 +5,9 @@ import { Document, DocumentGateway, DocumentTshNode } from './types'; function getMockedDocuments(): Document[] { return [ - { uri: 'test1', kind: 'doc.terminal_shell', title: 'T1' }, - { uri: 'test2', kind: 'doc.terminal_shell', title: 'T2' }, - { uri: 'test3', kind: 'doc.terminal_shell', title: 'T3' }, + { uri: '/docs/test1', kind: 'doc.terminal_shell', title: 'T1' }, + { uri: '/docs/test2', kind: 'doc.terminal_shell', title: 'T2' }, + { uri: '/docs/test3', kind: 'doc.terminal_shell', title: 'T3' }, ]; } @@ -54,12 +54,12 @@ test('get document should return the document', () => { describe('document should be added', () => { const mockedDocuments = getMockedDocuments(); const newDocument: DocumentGateway = { - uri: 'new-doc', + uri: '/docs/new-doc', title: 'New doc', kind: 'doc.gateway', - gatewayUri: '', - targetUri: '', - targetName: '', + gatewayUri: '/gateways/123', + targetUri: '/clusters/bar/dbs/quux', + targetName: 'quux', targetUser: 'foo', }; @@ -106,12 +106,12 @@ test('only TSH node documents should be returned', () => { const service = createService(mockedDocks); const tshNodeDocument: DocumentTshNode = { - uri: 'test1', + uri: '/docs/test1', kind: 'doc.terminal_tsh_node', title: 'TSH', - serverId: '', + serverId: 'bar', login: '', - serverUri: '', + serverUri: '/clusters/foo/servers/bar', status: 'connecting', rootClusterId: '', }; @@ -126,12 +126,12 @@ test('only gateway documents should be returned', () => { const service = createService(mockedDocks); const gatewayDocument: DocumentGateway = { - uri: 'test1', + uri: '/docs/test1', kind: 'doc.gateway', title: 'gw', - gatewayUri: '', - targetUri: '', - targetName: '', + gatewayUri: '/gateways/123', + targetUri: '/clusters/bar/dbs/quux', + targetName: 'quux', targetUser: 'foo', }; diff --git a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts index 4ce1a6be0..73a895212 100644 --- a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts +++ b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts @@ -15,8 +15,7 @@ limitations under the License. */ import { unique } from 'teleterm/ui/utils/uid'; - -import { paths, routing } from 'teleterm/ui/uri'; +import { DocumentUri, paths, routing, ServerUri } from 'teleterm/ui/uri'; import { CreateAccessRequestDocumentOpts, @@ -40,7 +39,7 @@ export class DocumentsService { ) => void ) {} - open(docUri: string) { + open(docUri: DocumentUri) { if (!this.getDocument(docUri)) { this.add({ uri: docUri, @@ -90,17 +89,17 @@ export class DocumentsService { leafClusterId: params.leafClusterId, kubeId: params.kubeId, kubeUri: options.kubeUri, + // We prepend the name with `rootClusterId/` to create a kube config + // inside this directory. When the user logs out of the cluster, + // the entire directory is deleted. kubeConfigRelativePath: options.kubeConfigRelativePath || - // We prepend the name with `rootClusterId/` to create a kube config - // inside this directory. When the user logs out of the cluster, - // the entire directory is deleted. `${params.rootClusterId}/${params.kubeId}-${unique(5)}`, title: params.kubeId, }; } - createTshNodeDocument(serverUri: string): DocumentTshNode { + createTshNodeDocument(serverUri: ServerUri): DocumentTshNode { const { params } = routing.parseServerUri(serverUri); const uri = routing.getDocUri({ docId: unique() }); return { diff --git a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts index fb8e0102a..44103d8f8 100644 --- a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts +++ b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts @@ -1,9 +1,9 @@ -import { routing } from 'teleterm/ui/uri'; +import { ClusterOrResourceUri, routing } from 'teleterm/ui/uri'; import { assertUnreachable } from 'teleterm/ui/utils'; import { Document } from './types'; -export function getResourceUri(document: Document): string { +export function getResourceUri(document: Document): ClusterOrResourceUri { switch (document.kind) { case 'doc.cluster': return document.clusterUri; diff --git a/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts b/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts index 627524e71..28864b327 100644 --- a/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts +++ b/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import * as uri from 'teleterm/ui/uri'; + export type Kind = | 'doc.access_requests' | 'doc.cluster' @@ -24,7 +26,7 @@ export type Kind = | 'doc.terminal_tsh_kube'; interface DocumentBase { - uri: string; + uri: uri.DocumentUri; title: string; kind: Kind; } @@ -37,7 +39,7 @@ export interface DocumentTshNode extends DocumentBase { kind: 'doc.terminal_tsh_node'; status: 'connecting' | 'connected' | 'disconnected'; serverId: string; - serverUri: string; + serverUri: uri.ServerUri; rootClusterId: string; leafClusterId?: string; login?: string; @@ -47,7 +49,7 @@ export interface DocumentTshKube extends DocumentBase { kind: 'doc.terminal_tsh_kube'; status: 'connecting' | 'connected' | 'disconnected'; kubeId: string; - kubeUri: string; + kubeUri: uri.KubeUri; kubeConfigRelativePath: string; rootClusterId: string; leafClusterId?: string; @@ -55,8 +57,8 @@ export interface DocumentTshKube extends DocumentBase { export interface DocumentGateway extends DocumentBase { kind: 'doc.gateway'; - gatewayUri?: string; - targetUri: string; + gatewayUri?: uri.GatewayUri; + targetUri: uri.DatabaseUri; targetUser: string; targetName: string; targetSubresourceName?: string; @@ -65,12 +67,12 @@ export interface DocumentGateway extends DocumentBase { export interface DocumentCluster extends DocumentBase { kind: 'doc.cluster'; - clusterUri: string; + clusterUri: uri.ClusterUri; } export interface DocumentAccessRequests extends DocumentBase { kind: 'doc.access_requests'; - clusterUri: string; + clusterUri: uri.ClusterUri; state: AccessRequestDocumentState; requestId: string; } @@ -96,8 +98,8 @@ export type Document = | DocumentTerminal; export type CreateGatewayDocumentOpts = { - gatewayUri?: string; - targetUri: string; + gatewayUri?: uri.GatewayUri; + targetUri: uri.DatabaseUri; targetName: string; targetUser: string; targetSubresourceName?: string; @@ -106,16 +108,16 @@ export type CreateGatewayDocumentOpts = { }; export type CreateClusterDocumentOpts = { - clusterUri: string; + clusterUri: uri.ClusterUri; }; export type CreateTshKubeDocumentOptions = { - kubeUri: string; + kubeUri: uri.KubeUri; kubeConfigRelativePath?: string; }; export type CreateAccessRequestDocumentOpts = { - clusterUri: string; + clusterUri: uri.ClusterUri; state: AccessRequestDocumentState; title?: string; requestId?: string; diff --git a/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts b/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts index 695f48b06..b91d443b8 100644 --- a/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts +++ b/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts @@ -1,13 +1,14 @@ +import { RootClusterUri } from 'teleterm/ui/uri'; + import { ClustersService } from '../clusters'; import { StatePersistenceService } from '../statePersistence'; import { getEmptyPendingAccessRequest } from './accessRequestsService'; - import { Workspace, WorkspacesService } from './workspacesService'; describe('restoring workspace', () => { function getTestSetup(options: { - clusterUri: string; // assumes that only one cluster can be added + clusterUri: RootClusterUri; // assumes that only one cluster can be added persistedWorkspaces: Record; }) { const statePersistenceService: Partial = { @@ -44,7 +45,7 @@ describe('restoring workspace', () => { kind: 'doc.cluster', title: 'Cluster Test', clusterUri: options.clusterUri, - uri: 'docs/test-cluster-uri', + uri: '/docs/test-cluster-uri', }; const workspacesService = new WorkspacesService( @@ -76,11 +77,11 @@ describe('restoring workspace', () => { documents: [ { kind: 'doc.terminal_shell', - uri: 'docs/some_uri', + uri: '/docs/some_uri', title: '/Users/alice/Documents', }, ], - location: 'docs/some_uri', + location: '/docs/some_uri', }; const { workspacesService, clusterDocument } = getTestSetup({ diff --git a/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts b/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts index 8be552148..8a5296593 100644 --- a/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts +++ b/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts @@ -12,7 +12,13 @@ import { } from 'teleterm/ui/services/statePersistence'; import { ImmutableStore } from 'teleterm/ui/services/immutableStore'; import { NotificationsService } from 'teleterm/ui/services/notifications'; -import { routing } from 'teleterm/ui/uri'; +import { + ClusterOrResourceUri, + ClusterUri, + DocumentUri, + RootClusterUri, + routing, +} from 'teleterm/ui/uri'; import { AccessRequestsService, @@ -22,28 +28,28 @@ import { import { Document, DocumentsService } from './documentsService'; export interface WorkspacesState { - rootClusterUri?: string; - workspaces: Record; + rootClusterUri?: RootClusterUri; + workspaces: Record; } export interface Workspace { - localClusterUri: string; + localClusterUri: ClusterUri; documents: Document[]; - location: string; + location: DocumentUri; accessRequests: { isBarCollapsed: boolean; pending: PendingAccessRequest; }; previous?: { documents: Document[]; - location: string; + location: DocumentUri; }; } export class WorkspacesService extends ImmutableStore { - private documentsServicesCache = new Map(); + private documentsServicesCache = new Map(); private accessRequestsServicesCache = new Map< - string, + RootClusterUri, AccessRequestsService >(); state: WorkspacesState = { @@ -60,49 +66,39 @@ export class WorkspacesService extends ImmutableStore { super(); } - getActiveWorkspace(): Workspace | undefined { + getActiveWorkspace() { return this.state.workspaces[this.state.rootClusterUri]; } - getRootClusterUri(): string | undefined { + getRootClusterUri() { return this.state.rootClusterUri; } - getWorkspaces(): Record { + getWorkspaces() { return this.state.workspaces; } - getWorkspace(clusterUri: string): Workspace { + getWorkspace(clusterUri: RootClusterUri) { return this.state.workspaces[clusterUri]; } - getActiveWorkspaceDocumentService(): DocumentsService | undefined { + getActiveWorkspaceDocumentService() { if (!this.state.rootClusterUri) { return; } return this.getWorkspaceDocumentService(this.state.rootClusterUri); } - getActiveWorkspaceAccessRequestsService(): AccessRequestsService | undefined { + getActiveWorkspaceAccessRequestsService() { if (!this.state.rootClusterUri) { return; } return this.getWorkspaceAccessRequestsService(this.state.rootClusterUri); } - getWorkspacesDocumentsServices(): Array<{ - clusterUri: string; - workspaceDocumentsService: DocumentsService; - }> { - return Object.entries(this.state.workspaces).map(([clusterUri]) => ({ - clusterUri, - workspaceDocumentsService: this.getWorkspaceDocumentService(clusterUri), - })); - } - setWorkspaceLocalClusterUri( - clusterUri: string, - localClusterUri: string + clusterUri: RootClusterUri, + localClusterUri: ClusterUri ): void { this.setState(draftState => { draftState.workspaces[clusterUri].localClusterUri = localClusterUri; @@ -110,7 +106,7 @@ export class WorkspacesService extends ImmutableStore { } getWorkspaceDocumentService( - clusterUri: string + clusterUri: RootClusterUri ): DocumentsService | undefined { if (!this.documentsServicesCache.has(clusterUri)) { this.documentsServicesCache.set( @@ -131,7 +127,7 @@ export class WorkspacesService extends ImmutableStore { } getWorkspaceAccessRequestsService( - clusterUri: string + clusterUri: RootClusterUri ): AccessRequestsService | undefined { if (!this.accessRequestsServicesCache.has(clusterUri)) { this.accessRequestsServicesCache.set( @@ -155,7 +151,9 @@ export class WorkspacesService extends ImmutableStore { return documentService && documentService.isActive(documentUri); } - doesResourceBelongToActiveWorkspace(resourceUri: string): boolean { + doesResourceBelongToActiveWorkspace( + resourceUri: ClusterOrResourceUri + ): boolean { return ( this.state.rootClusterUri && routing.belongsToProfile(this.state.rootClusterUri, resourceUri) @@ -171,7 +169,7 @@ export class WorkspacesService extends ImmutableStore { this.persistState(); } - setActiveWorkspace(clusterUri: string): Promise { + setActiveWorkspace(clusterUri: RootClusterUri): Promise { const setWorkspace = () => { this.setState(draftState => { // adding a new workspace @@ -239,14 +237,14 @@ export class WorkspacesService extends ImmutableStore { .catch(() => undefined); // catch ClusterConnectDialog cancellation } - removeWorkspace(clusterUri: string): void { + removeWorkspace(clusterUri: RootClusterUri): void { this.setState(draftState => { delete draftState.workspaces[clusterUri]; }); } - getConnectedWorkspacesClustersUri(): string[] { - return Object.keys(this.state.workspaces).filter( + getConnectedWorkspacesClustersUri() { + return (Object.keys(this.state.workspaces) as RootClusterUri[]).filter( clusterUri => this.clustersService.findCluster(clusterUri)?.connected ); } @@ -286,7 +284,7 @@ export class WorkspacesService extends ImmutableStore { } } - private reopenPreviousDocuments(clusterUri: string): void { + private reopenPreviousDocuments(clusterUri: RootClusterUri): void { this.setState(draftState => { const workspace = draftState.workspaces[clusterUri]; workspace.documents = workspace.previous.documents.map(d => { @@ -310,7 +308,7 @@ export class WorkspacesService extends ImmutableStore { }); } - private discardPreviousDocuments(clusterUri: string): void { + private discardPreviousDocuments(clusterUri: RootClusterUri): void { this.setState(draftState => { const workspace = draftState.workspaces[clusterUri]; workspace.previous = undefined; @@ -336,7 +334,7 @@ export class WorkspacesService extends ImmutableStore { ); } - private getWorkspaceDefaultState(localClusterUri: string): Workspace { + private getWorkspaceDefaultState(localClusterUri: ClusterUri): Workspace { const rootClusterUri = routing.ensureRootClusterUri(localClusterUri); const defaultDocument = this.getWorkspaceDocumentService( rootClusterUri diff --git a/packages/teleterm/src/ui/uri.ts b/packages/teleterm/src/ui/uri.ts index 9bf344beb..d43ef07df 100644 --- a/packages/teleterm/src/ui/uri.ts +++ b/packages/teleterm/src/ui/uri.ts @@ -30,6 +30,48 @@ export const paths = { doc: '/docs/:docId', }; +type RootClusterId = string; +type LeafClusterId = string; +type ServerId = string; +type KubeId = string; +type DbId = string; +export type RootClusterUri = `/clusters/${RootClusterId}`; +export type RootClusterServerUri = + `/clusters/${RootClusterId}/servers/${ServerId}`; +export type RootClusterKubeUri = `/clusters/${RootClusterId}/kubes/${KubeId}`; +export type RootClusterDatabaseUri = `/clusters/${RootClusterId}/dbs/${DbId}`; +export type RootClusterResourceUri = + | RootClusterServerUri + | RootClusterKubeUri + | RootClusterDatabaseUri; +export type RootClusterOrResourceUri = RootClusterUri | RootClusterResourceUri; +export type LeafClusterUri = + `/clusters/${RootClusterId}/leaves/${LeafClusterId}`; +export type LeafClusterServerUri = + `/clusters/${RootClusterId}/leaves/${LeafClusterId}/servers/${ServerId}`; +export type LeafClusterKubeUri = + `/clusters/${RootClusterId}/leaves/${LeafClusterId}/kubes/${KubeId}`; +export type LeafClusterDatabaseUri = + `/clusters/${RootClusterId}/leaves/${LeafClusterId}/dbs/${DbId}`; +export type LeafClusterResourceUri = + | LeafClusterServerUri + | LeafClusterKubeUri + | LeafClusterDatabaseUri; +export type LeafClusterOrResourceUri = LeafClusterUri | LeafClusterResourceUri; + +export type ResourceUri = RootClusterResourceUri | LeafClusterResourceUri; +export type ClusterUri = RootClusterUri | LeafClusterUri; +export type ServerUri = RootClusterServerUri | LeafClusterServerUri; +export type KubeUri = RootClusterKubeUri | LeafClusterKubeUri; +export type DatabaseUri = RootClusterDatabaseUri | LeafClusterDatabaseUri; +export type ClusterOrResourceUri = ResourceUri | ClusterUri; + +type DocumentId = string; +export type DocumentUri = `/docs/${DocumentId}`; + +type GatewayId = string; +export type GatewayUri = `/gateways/${GatewayId}`; + export const routing = { parseClusterUri(uri: string) { const leafMatch = routing.parseUri(uri, paths.leafCluster); @@ -38,13 +80,13 @@ export const routing = { }, // Pass either a root or a leaf cluster URI to get back a root cluster URI. - ensureRootClusterUri(uri: string) { + ensureRootClusterUri(uri: ClusterOrResourceUri) { const { rootClusterId } = routing.parseClusterUri(uri).params; - return routing.getClusterUri({ rootClusterId }); + return routing.getClusterUri({ rootClusterId }) as RootClusterUri; }, // Pass any resource URI to get back a cluster URI. - ensureClusterUri(uri: string) { + ensureClusterUri(uri: ClusterOrResourceUri) { const params = routing.parseClusterUri(uri).params; return routing.getClusterUri(params); }, @@ -83,43 +125,46 @@ export const routing = { }, getDocUri(params: Params) { - return generatePath(paths.doc, params as any); + return generatePath(paths.doc, params as any) as DocumentUri; }, - getClusterUri(params: Params) { + getClusterUri(params: Params): ClusterUri { if (params.leafClusterId) { - return generatePath(paths.leafCluster, params as any); + return generatePath(paths.leafCluster, params as any) as LeafClusterUri; } - return generatePath(paths.rootCluster, params as any); + return generatePath(paths.rootCluster, params as any) as RootClusterUri; }, getServerUri(params: Params) { - return generatePath(paths.server, params as any); + return generatePath(paths.server, params as any) as ServerUri; }, - isClusterServer(clusterUri: string, serverUri: string) { + isClusterServer(clusterUri: ClusterUri, serverUri: ServerUri) { return serverUri.startsWith(`${clusterUri}/servers/`); }, - isClusterKube(clusterUri: string, kubeUri: string) { + isClusterKube(clusterUri: ClusterUri, kubeUri: KubeUri) { return kubeUri.startsWith(`${clusterUri}/kubes/`); }, - isClusterDb(clusterUri: string, dbUri: string) { + isClusterDb(clusterUri: ClusterUri, dbUri: DatabaseUri) { return dbUri.startsWith(`${clusterUri}/dbs/`); }, - isClusterApp(clusterUri: string, appUri: string) { + isClusterApp(clusterUri: ClusterUri, appUri: string) { return appUri.startsWith(`${clusterUri}/apps/`); }, - isLeafCluster(clusterUri: string) { + isLeafCluster(clusterUri: ClusterUri) { const match = routing.parseClusterUri(clusterUri); return match && Boolean(match.params.leafClusterId); }, - belongsToProfile(clusterUri: string, resourceUri: string) { + belongsToProfile( + clusterUri: ClusterOrResourceUri, + resourceUri: ClusterOrResourceUri + ) { const rootClusterUri = this.ensureRootClusterUri(clusterUri); const resourceRootClusterUri = this.ensureRootClusterUri(resourceUri); diff --git a/packages/teleterm/src/ui/utils/retryWithRelogin.test.ts b/packages/teleterm/src/ui/utils/retryWithRelogin.test.ts index 77760921e..a9c6d6fbc 100644 --- a/packages/teleterm/src/ui/utils/retryWithRelogin.test.ts +++ b/packages/teleterm/src/ui/utils/retryWithRelogin.test.ts @@ -15,7 +15,7 @@ it('returns the result of actionToRetry if no error is thrown', async () => { const actualReturnValue = await retryWithRelogin( undefined, - '', + '/clusters/foo', actionToRetry ); @@ -45,7 +45,7 @@ it('opens the login modal window and calls actionToRetry again on successful rel jest .spyOn(appContext.modalsService, 'openClusterConnectDialog') .mockImplementation(({ onSuccess }) => { - onSuccess(''); + onSuccess('/clusters/foo'); // Dialog cancel function. return { closeDialog: () => {} }; diff --git a/packages/teleterm/src/ui/utils/retryWithRelogin.ts b/packages/teleterm/src/ui/utils/retryWithRelogin.ts index a63207833..a19e81efd 100644 --- a/packages/teleterm/src/ui/utils/retryWithRelogin.ts +++ b/packages/teleterm/src/ui/utils/retryWithRelogin.ts @@ -1,4 +1,4 @@ -import { routing } from 'teleterm/ui/uri'; +import { ClusterOrResourceUri, RootClusterUri, routing } from 'teleterm/ui/uri'; import { IAppContext } from 'teleterm/ui/types'; import Logger from 'teleterm/logger'; @@ -26,7 +26,7 @@ const logger = new Logger('retryWithRelogin'); */ export async function retryWithRelogin( appContext: IAppContext, - resourceUri: string, + resourceUri: ClusterOrResourceUri, actionToRetry: () => Promise ): Promise { let retryableErrorFromActionToRetry: Error; @@ -71,7 +71,10 @@ 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: IAppContext, rootClusterUri: string): Promise { +function login( + appContext: IAppContext, + rootClusterUri: RootClusterUri +): Promise { return new Promise(resolve => { appContext.modalsService.openClusterConnectDialog({ clusterUri: rootClusterUri,