diff --git a/e b/e index b3710b8225e84..e1aaa6cb60bb7 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit b3710b8225e844c9807b3c3b071e13ffbb7123a2 +Subproject commit e1aaa6cb60bb77f2206888ed0bf67fc876e02322 diff --git a/web/packages/teleterm/src/services/tshd/createClient.ts b/web/packages/teleterm/src/services/tshd/createClient.ts index 7541485a23276..cf2b36eb15e73 100644 --- a/web/packages/teleterm/src/services/tshd/createClient.ts +++ b/web/packages/teleterm/src/services/tshd/createClient.ts @@ -61,20 +61,24 @@ export default function createClient( async getKubes({ clusterUri, search, - sort = { fieldName: 'name', dir: 'ASC' }, + sort, query, searchAsRoles, startKey, limit, - }: types.ServerSideParams) { + }: types.GetResourcesParams) { const req = new api.GetKubesRequest() .setClusterUri(clusterUri) .setSearchAsRoles(searchAsRoles) .setStartKey(startKey) - .setSortBy(`${sort.fieldName}:${sort.dir.toLowerCase()}`) .setSearch(search) .setQuery(query) .setLimit(limit); + + if (sort) { + req.setSortBy(`${sort.fieldName}:${sort.dir.toLowerCase()}`); + } + return new Promise((resolve, reject) => { tshd.getKubes(req, (err, response) => { if (err) { @@ -128,20 +132,24 @@ export default function createClient( async getDatabases({ clusterUri, search, - sort = { fieldName: 'name', dir: 'ASC' }, + sort, query, searchAsRoles, startKey, limit, - }: types.ServerSideParams) { + }: types.GetResourcesParams) { const req = new api.GetDatabasesRequest() .setClusterUri(clusterUri) .setSearchAsRoles(searchAsRoles) .setStartKey(startKey) - .setSortBy(`${sort.fieldName}:${sort.dir.toLowerCase()}`) .setSearch(search) .setQuery(query) .setLimit(limit); + + if (sort) { + req.setSortBy(`${sort.fieldName}:${sort.dir.toLowerCase()}`); + } + return new Promise((resolve, reject) => { tshd.getDatabases(req, (err, response) => { if (err) { @@ -198,19 +206,23 @@ export default function createClient( clusterUri, search, query, - sort = { fieldName: 'hostname', dir: 'ASC' }, + sort, searchAsRoles, startKey, limit, - }: types.ServerSideParams) { + }: types.GetResourcesParams) { const req = new api.GetServersRequest() .setClusterUri(clusterUri) .setSearchAsRoles(searchAsRoles) .setStartKey(startKey) - .setSortBy(`${sort.fieldName}:${sort.dir.toLowerCase()}`) .setSearch(search) .setQuery(query) .setLimit(limit); + + if (sort) { + req.setSortBy(`${sort.fieldName}:${sort.dir.toLowerCase()}`); + } + return new Promise((resolve, reject) => { tshd.getServers(req, (err, response) => { if (err) { diff --git a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts index 5809b0f5edd2e..343af90cb640c 100644 --- a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -29,7 +29,7 @@ import { LoginPasswordlessParams, LoginSsoParams, ReviewAccessRequestParams, - ServerSideParams, + GetResourcesParams, TshAbortController, TshAbortSignal, TshClient, @@ -39,13 +39,13 @@ import { export class MockTshClient implements TshClient { listRootClusters: () => Promise; listLeafClusters: (clusterUri: string) => Promise; - getKubes: (params: ServerSideParams) => Promise; - getDatabases: (params: ServerSideParams) => Promise; + getKubes: (params: GetResourcesParams) => Promise; + getDatabases: (params: GetResourcesParams) => Promise; listDatabaseUsers: (dbUri: string) => Promise; getRequestableRoles: ( params: GetRequestableRolesParams ) => Promise; - getServers: (params: ServerSideParams) => Promise; + getServers: (params: GetResourcesParams) => Promise; assumeRole: ( clusterUri: string, requestIds: string[], diff --git a/web/packages/teleterm/src/services/tshd/types.ts b/web/packages/teleterm/src/services/tshd/types.ts index 043b31f40b622..eeead405a7940 100644 --- a/web/packages/teleterm/src/services/tshd/types.ts +++ b/web/packages/teleterm/src/services/tshd/types.ts @@ -144,8 +144,8 @@ export type LoginPasswordlessRequest = export type TshClient = { listRootClusters: () => Promise; listLeafClusters: (clusterUri: uri.RootClusterUri) => Promise; - getKubes: (params: ServerSideParams) => Promise; - getDatabases: (params: ServerSideParams) => Promise; + getKubes: (params: GetResourcesParams) => Promise; + getDatabases: (params: GetResourcesParams) => Promise; listDatabaseUsers: (dbUri: uri.DatabaseUri) => Promise; assumeRole: ( clusterUri: uri.RootClusterUri, @@ -155,7 +155,7 @@ export type TshClient = { getRequestableRoles: ( params: GetRequestableRolesParams ) => Promise; - getServers: (params: ServerSideParams) => Promise; + getServers: (params: GetResourcesParams) => Promise; getAccessRequests: ( clusterUri: uri.RootClusterUri ) => Promise; @@ -248,18 +248,25 @@ export type CreateGatewayParams = { subresource_name?: string; }; -export type ServerSideParams = { +export type GetResourcesParams = { clusterUri: uri.ClusterUri; + // sort is a required field because it has direct implications on performance of ListResources. + sort: SortType | null; + // limit cannot be omitted and must be greater than zero, otherwise ListResources is going to + // return an error. + limit: number; // search is used for regular search. search?: string; searchAsRoles?: string; - sort?: SortType; startKey?: string; - limit?: number; // query is used for advanced search. query?: string; }; +// Compatibility type to make sure teleport.e doesn't break. +// TODO(ravicious): Remove after teleterm.e is updated to use GetResourcesParams. +export type ServerSideParams = GetResourcesParams; + export type ReviewAccessRequestParams = { state: RequestState; reason: string; diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx index 44603cc5073e1..31f6823b1ab5f 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx @@ -170,7 +170,7 @@ function getMenuLoginOptions( async function getDatabaseUsers(appContext: IAppContext, dbUri: DatabaseUri) { try { const dbUsers = await retryWithRelogin(appContext, dbUri, () => - appContext.clustersService.getDbUsers(dbUri) + appContext.resourcesService.getDbUsers(dbUri) ); return dbUsers.map(user => ({ login: user, url: '' })); } catch (e) { diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/useDatabases.ts b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/useDatabases.ts index abeea60105821..533d7848972eb 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/useDatabases.ts +++ b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/useDatabases.ts @@ -18,7 +18,7 @@ import { useAppContext } from 'teleterm/ui/appContextProvider'; import { Database, GatewayProtocol, - ServerSideParams, + GetResourcesParams, } from 'teleterm/services/tshd/types'; import { routing } from 'teleterm/ui/uri'; import { makeDatabase } from 'teleterm/ui/services/clusters'; @@ -31,7 +31,7 @@ export function useDatabases() { const { fetchAttempt, ...serverSideResources } = useServerSideResources( { fieldName: 'name', dir: 'ASC' }, // default sort - (params: ServerSideParams) => + (params: GetResourcesParams) => appContext.resourcesService.fetchDatabases(params) ); diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Kubes/useKubes.ts b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Kubes/useKubes.ts index 58f5f7b5e5347..05f8d8d8756b3 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Kubes/useKubes.ts +++ b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Kubes/useKubes.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Kube, ServerSideParams } from 'teleterm/services/tshd/types'; +import { Kube, GetResourcesParams } from 'teleterm/services/tshd/types'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { useClusterContext } from 'teleterm/ui/DocumentCluster/clusterContext'; @@ -25,7 +25,8 @@ export function useKubes() { const ctx = useClusterContext(); const { fetchAttempt, ...serversideResources } = useServerSideResources( { fieldName: 'name', dir: 'ASC' }, // default sort - (params: ServerSideParams) => appContext.resourcesService.fetchKubes(params) + (params: GetResourcesParams) => + appContext.resourcesService.fetchKubes(params) ); return { diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Servers/useServers.ts b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Servers/useServers.ts index 9dad56feb3f26..f88e6d056b46e 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Servers/useServers.ts +++ b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Servers/useServers.ts @@ -13,7 +13,7 @@ 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 { Server, ServerSideParams } from 'teleterm/services/tshd/types'; +import { Server, GetResourcesParams } from 'teleterm/services/tshd/types'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { makeServer } from 'teleterm/ui/services/clusters'; @@ -27,7 +27,7 @@ export function useServers() { const { fetchAttempt, ...serversideResources } = useServerSideResources( { fieldName: 'hostname', dir: 'ASC' }, // default sort - (params: ServerSideParams) => + (params: GetResourcesParams) => appContext.resourcesService.fetchServers(params) ); diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/useServerSideResources.ts b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/useServerSideResources.ts index 72427b65ccfd2..6a48d349e6abf 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/useServerSideResources.ts +++ b/web/packages/teleterm/src/ui/DocumentCluster/ClusterResources/useServerSideResources.ts @@ -17,14 +17,19 @@ import { useState, useEffect, useMemo } from 'react'; import { SortType } from 'design/DataTable/types'; import { useAsync } from 'shared/hooks/useAsync'; -import { AgentFilter, AgentLabel } from 'teleport/services/agents'; +import { + AgentFilter as WeakAgentFilter, + AgentLabel, +} from 'teleport/services/agents'; -import { ServerSideParams } from 'teleterm/services/tshd/types'; +import { GetResourcesParams } from 'teleterm/services/tshd/types'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { retryWithRelogin } from 'teleterm/ui/utils'; import { useClusterContext } from '../clusterContext'; +type AgentFilter = WeakAgentFilter & { sort: SortType }; + function addAgentLabelToQuery(filter: AgentFilter, label: AgentLabel) { const queryParts = []; @@ -49,7 +54,7 @@ const limit = 15; export function useServerSideResources( defaultSort: SortType, - fetchFunction: (params: ServerSideParams) => Promise> + fetchFunction: (params: GetResourcesParams) => Promise> ) { const ctx = useAppContext(); const { clusterUri } = useClusterContext(); diff --git a/web/packages/teleterm/src/ui/Search/useSearch.ts b/web/packages/teleterm/src/ui/Search/useSearch.ts new file mode 100644 index 0000000000000..62d14c7c8a4bf --- /dev/null +++ b/web/packages/teleterm/src/ui/Search/useSearch.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2023 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 { useCallback } from 'react'; + +import { useAppContext } from 'teleterm/ui/appContextProvider'; + +/** + * useSearch returns a function which searches for the given list of space-separated keywords across + * all root and leaf clusters that the user is currently logged in to. + * + * It does so by issuing a separate request for each resource type to each cluster. It fails if any + * of those requests fail. + */ +export function useSearch() { + const { clustersService, resourcesService } = useAppContext(); + clustersService.useState(); + + return useCallback( + async (search: string) => { + const connectedClusters = clustersService + .getClusters() + .filter(c => c.connected); + const searchPromises = connectedClusters.map(cluster => + resourcesService.searchResources(cluster.uri, search) + ); + + return (await Promise.all(searchPromises)).flat(); + }, + [clustersService, resourcesService] + ); +} diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts index e7760a6db96fd..8734d401eab84 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -29,7 +29,7 @@ import { CreateAccessRequestParams, GetRequestableRolesParams, ReviewAccessRequestParams, - ServerSideParams, + GetResourcesParams, } from 'teleterm/services/tshd/types'; import { MainProcessClient } from 'teleterm/mainProcess/types'; import { UsageService } from 'teleterm/ui/services/usage'; @@ -196,6 +196,8 @@ export class ClustersService extends ImmutableStore async getRequestableRoles(params: GetRequestableRolesParams) { const cluster = this.state.clusters.get(params.rootClusterUri); + // TODO(ravicious): Remove check for cluster.connected. This check should be done earlier in the + // UI rather than be repeated in each ClustersService method. if (!cluster.connected) { return; } @@ -205,6 +207,7 @@ export class ClustersService extends ImmutableStore getAssumedRequests(rootClusterUri: uri.RootClusterUri) { const cluster = this.state.clusters.get(rootClusterUri); + // TODO(ravicious): Remove check for cluster.connected. See the comment in getRequestableRoles. if (!cluster?.connected) { return {}; } @@ -218,6 +221,7 @@ export class ClustersService extends ImmutableStore async getAccessRequests(rootClusterUri: uri.RootClusterUri) { const cluster = this.state.clusters.get(rootClusterUri); + // TODO(ravicious): Remove check for cluster.connected. See the comment in getRequestableRoles. if (!cluster.connected) { return; } @@ -230,6 +234,7 @@ export class ClustersService extends ImmutableStore requestId: string ) { const cluster = this.state.clusters.get(rootClusterUri); + // TODO(ravicious): Remove check for cluster.connected. See the comment in getRequestableRoles. if (!cluster.connected) { return; } @@ -242,6 +247,7 @@ export class ClustersService extends ImmutableStore dropIds: string[] ) { const cluster = this.state.clusters.get(rootClusterUri); + // TODO(ravicious): Remove check for cluster.connected. See the comment in getRequestableRoles. if (!cluster.connected) { return; } @@ -255,6 +261,7 @@ export class ClustersService extends ImmutableStore requestId: string ) { const cluster = this.state.clusters.get(rootClusterUri); + // TODO(ravicious): Remove check for cluster.connected. See the comment in getRequestableRoles. if (!cluster.connected) { return; } @@ -267,6 +274,7 @@ export class ClustersService extends ImmutableStore params: ReviewAccessRequestParams ) { const cluster = this.state.clusters.get(rootClusterUri); + // TODO(ravicious): Remove check for cluster.connected. See the comment in getRequestableRoles. if (!cluster.connected) { return; } @@ -281,6 +289,7 @@ export class ClustersService extends ImmutableStore async createAccessRequest(params: CreateAccessRequestParams) { const cluster = this.state.clusters.get(params.rootClusterUri); + // TODO(ravicious): Remove check for cluster.connected. See the comment in getRequestableRoles. if (!cluster.connected) { return; } @@ -426,16 +435,6 @@ export class ClustersService extends ImmutableStore return this.getClusters().filter(c => !c.leaf); } - // TODO(ravicious): Use ResourcesService instead. - async fetchKubes(params: ServerSideParams) { - return await this.client.getKubes(params); - } - - // TODO(ravicious): Move to ResourceService. - async getDbUsers(dbUri: uri.DatabaseUri): Promise { - return await this.client.listDatabaseUsers(dbUri); - } - async removeClusterKubeConfigs(clusterUri: string): Promise { const { params: { rootClusterId }, diff --git a/web/packages/teleterm/src/ui/services/quickInput/suggesters.ts b/web/packages/teleterm/src/ui/services/quickInput/suggesters.ts index 4c22e93c01922..43ab423bc89fb 100644 --- a/web/packages/teleterm/src/ui/services/quickInput/suggesters.ts +++ b/web/packages/teleterm/src/ui/services/quickInput/suggesters.ts @@ -83,6 +83,7 @@ export class QuickServerSuggester clusterUri: localClusterUri, search: input, limit, + sort: { fieldName: 'hostname', dir: 'ASC' }, }); return servers.agentsList.map(server => ({ @@ -111,6 +112,7 @@ export class QuickDatabaseSuggester clusterUri: localClusterUri, search: input, limit, + sort: { fieldName: 'name', dir: 'ASC' }, }); return databases.agentsList.map(database => ({ diff --git a/web/packages/teleterm/src/ui/services/resources/resourcesService.test.ts b/web/packages/teleterm/src/ui/services/resources/resourcesService.test.ts index ab27d190fcb14..64bda031c21d4 100644 --- a/web/packages/teleterm/src/ui/services/resources/resourcesService.test.ts +++ b/web/packages/teleterm/src/ui/services/resources/resourcesService.test.ts @@ -86,6 +86,7 @@ test.each(getServerByHostnameTests)( clusterUri: '/clusters/bar', query: 'name == "foo"', limit: 2, + sort: null, }); } ); diff --git a/web/packages/teleterm/src/ui/services/resources/resourcesService.ts b/web/packages/teleterm/src/ui/services/resources/resourcesService.ts index c2ac329b8a611..8c6e053ef5db6 100644 --- a/web/packages/teleterm/src/ui/services/resources/resourcesService.ts +++ b/web/packages/teleterm/src/ui/services/resources/resourcesService.ts @@ -20,10 +20,12 @@ import type * as uri from 'teleterm/ui/uri'; export class ResourcesService { constructor(private tshClient: types.TshClient) {} - fetchServers(params: types.ServerSideParams) { + fetchServers(params: types.GetResourcesParams) { return this.tshClient.getServers(params); } + // TODO(ravicious): Refactor it to use logic similar to that in the Web UI. + // https://github.com/gravitational/teleport/blob/2a2b08dbfdaf71706a5af3812d3a7ec843d099b4/lib/web/apiserver.go#L2471 async getServerByHostname( clusterUri: uri.ClusterUri, hostname: string @@ -33,6 +35,7 @@ export class ResourcesService { clusterUri, query, limit: 2, + sort: null, }); if (servers.length > 1) { @@ -42,13 +45,46 @@ export class ResourcesService { return servers[0]; } - fetchDatabases(params: types.ServerSideParams) { + fetchDatabases(params: types.GetResourcesParams) { return this.tshClient.getDatabases(params); } - fetchKubes(params: types.ServerSideParams) { + fetchKubes(params: types.GetResourcesParams) { return this.tshClient.getKubes(params); } + + async getDbUsers(dbUri: uri.DatabaseUri): Promise { + return await this.tshClient.listDatabaseUsers(dbUri); + } + + /** + * searchResources searches for the given list of space-separated keywords across all resource + * types on the given cluster. + * + * It does so by issuing a separate request for each resource type. It fails if any of those + * requests fail. + * + * The results need to be wrapped in SearchResult because if we returned raw types (Server, + * Database, Kube) then there would be no easy way to differentiate between them on type level. + */ + async searchResources( + clusterUri: uri.ClusterUri, + search: string + ): Promise { + const params = { search, clusterUri, sort: null, limit: 100 }; + + const servers = this.fetchServers(params).then(res => + res.agentsList.map(resource => ({ kind: 'server' as const, resource })) + ); + const databases = this.fetchDatabases(params).then(res => + res.agentsList.map(resource => ({ kind: 'database' as const, resource })) + ); + const kubes = this.fetchKubes(params).then(res => + res.agentsList.map(resource => ({ kind: 'kube' as const, resource })) + ); + + return (await Promise.all([servers, databases, kubes])).flat(); + } } export class AmbiguousHostnameError extends Error { @@ -57,3 +93,17 @@ export class AmbiguousHostnameError extends Error { this.name = 'AmbiguousHostname'; } } + +type SearchResultBase = { + kind: Kind; + resource: Resource; +}; + +export type SearchResultServer = SearchResultBase<'server', types.Server>; +export type SearchResultDatabase = SearchResultBase<'database', types.Database>; +export type SearchResultKube = SearchResultBase<'kube', types.Kube>; + +export type SearchResult = + | SearchResultServer + | SearchResultDatabase + | SearchResultKube;