diff --git a/web/packages/teleterm/src/ui/Search/pickers/ActionPicker.test.ts b/web/packages/teleterm/src/ui/Search/pickers/ActionPicker.test.ts index 06ef67102b7d4..506f5d458aa57 100644 --- a/web/packages/teleterm/src/ui/Search/pickers/ActionPicker.test.ts +++ b/web/packages/teleterm/src/ui/Search/pickers/ActionPicker.test.ts @@ -22,142 +22,206 @@ import { ResourceSearchError } from 'teleterm/ui/services/resources'; import { getActionPickerStatus } from './ActionPicker'; describe('getActionPickerStatus', () => { - it('partitions resource search errors into clusters with expired certs and non-retryable errors', () => { - const retryableError = new ResourceSearchError( - '/clusters/foo', - 'server', - new Error('ssh: cert has expired') - ); - - const nonRetryableError = new ResourceSearchError( - '/clusters/bar', - 'database', - new Error('whoops') - ); - - const status = getActionPickerStatus({ - inputValue: 'foo', - filters: [], - filterActionsAttempt: makeSuccessAttempt([]), - allClusters: [], - actionAttempts: [makeSuccessAttempt([])], - resourceSearchAttempt: makeSuccessAttempt({ - errors: [retryableError, nonRetryableError], - results: [], - search: 'foo', - }), - }); - - expect(status.inputState).toBe('some-input'); - - const { clustersWithExpiredCerts, nonRetryableResourceSearchErrors } = - status.inputState === 'some-input' && status; - - expect([...clustersWithExpiredCerts]).toEqual([retryableError.clusterUri]); - expect(nonRetryableResourceSearchErrors).toEqual([nonRetryableError]); - }); - - it('merges non-connected clusters with clusters that returned retryable errors', () => { - const offlineCluster = makeRootCluster({ connected: false }); - const retryableError = new ResourceSearchError( - '/clusters/foo', - 'server', - new Error('ssh: cert has expired') - ); - - const status = getActionPickerStatus({ - inputValue: 'foo', - filters: [], - filterActionsAttempt: makeSuccessAttempt([]), - allClusters: [offlineCluster], - actionAttempts: [makeSuccessAttempt([])], - resourceSearchAttempt: makeSuccessAttempt({ - errors: [retryableError], - results: [], - search: 'foo', - }), - }); - - expect(status.inputState).toBe('some-input'); - const { clustersWithExpiredCerts } = - status.inputState === 'some-input' && status; - - expect(clustersWithExpiredCerts.size).toBe(2); - expect(clustersWithExpiredCerts).toContain(offlineCluster.uri); - expect(clustersWithExpiredCerts).toContain(retryableError.clusterUri); - }); - - it('includes a cluster with expired cert only once even if multiple requests fail with retryable errors', () => { - const retryableErrors = [ - new ResourceSearchError( + describe('some-input search mode', () => { + it('partitions resource search errors into clusters with expired certs and non-retryable errors', () => { + const retryableError = new ResourceSearchError( '/clusters/foo', 'server', new Error('ssh: cert has expired') - ), - new ResourceSearchError( - '/clusters/foo', + ); + + const nonRetryableError = new ResourceSearchError( + '/clusters/bar', 'database', - new Error('ssh: cert has expired') - ), - new ResourceSearchError( - '/clusters/foo', - 'kube', - new Error('ssh: cert has expired') - ), - ]; - const status = getActionPickerStatus({ - inputValue: 'foo', - filters: [], - filterActionsAttempt: makeSuccessAttempt([]), - allClusters: [], - actionAttempts: [makeSuccessAttempt([])], - resourceSearchAttempt: makeSuccessAttempt({ - errors: retryableErrors, - results: [], - search: 'foo', - }), + new Error('whoops') + ); + + const status = getActionPickerStatus({ + inputValue: 'foo', + filters: [], + filterActionsAttempt: makeSuccessAttempt([]), + allClusters: [], + actionAttempts: [makeSuccessAttempt([])], + resourceSearchAttempt: makeSuccessAttempt({ + errors: [retryableError, nonRetryableError], + results: [], + search: 'foo', + }), + }); + + expect(status.inputState).toBe('some-input'); + + const { clustersWithExpiredCerts, nonRetryableResourceSearchErrors } = + status.inputState === 'some-input' && status; + + expect([...clustersWithExpiredCerts]).toEqual([ + retryableError.clusterUri, + ]); + expect(nonRetryableResourceSearchErrors).toEqual([nonRetryableError]); }); - expect(status.inputState).toBe('some-input'); - const { clustersWithExpiredCerts } = - status.inputState === 'some-input' && status; - expect([...clustersWithExpiredCerts]).toEqual(['/clusters/foo']); - }); - - it('returns non-retryable errors when fetching a preview after selecting a filter fails', () => { - const nonRetryableError = new ResourceSearchError( - '/clusters/bar', - 'server', - new Error('non-retryable error') - ); - const resourceSearchErrors = [ - new ResourceSearchError( + it('merges non-connected clusters with clusters that returned retryable errors', () => { + const offlineCluster = makeRootCluster({ connected: false }); + const retryableError = new ResourceSearchError( '/clusters/foo', 'server', new Error('ssh: cert has expired') - ), - nonRetryableError, - ]; - const status = getActionPickerStatus({ - inputValue: '', - filters: [{ filter: 'resource-type', resourceType: 'servers' }], - filterActionsAttempt: makeSuccessAttempt([]), - allClusters: [], - actionAttempts: [makeSuccessAttempt([])], - resourceSearchAttempt: makeSuccessAttempt({ - errors: resourceSearchErrors, - results: [], - search: 'foo', - }), + ); + + const status = getActionPickerStatus({ + inputValue: 'foo', + filters: [], + filterActionsAttempt: makeSuccessAttempt([]), + allClusters: [offlineCluster], + actionAttempts: [makeSuccessAttempt([])], + resourceSearchAttempt: makeSuccessAttempt({ + errors: [retryableError], + results: [], + search: 'foo', + }), + }); + + expect(status.inputState).toBe('some-input'); + const { clustersWithExpiredCerts } = + status.inputState === 'some-input' && status; + + expect(clustersWithExpiredCerts.size).toBe(2); + expect(clustersWithExpiredCerts).toContain(offlineCluster.uri); + expect(clustersWithExpiredCerts).toContain(retryableError.clusterUri); }); - expect(status.inputState).toBe('no-input'); + it('includes a cluster with expired cert only once even if multiple requests fail with retryable errors', () => { + const retryableErrors = [ + new ResourceSearchError( + '/clusters/foo', + 'server', + new Error('ssh: cert has expired') + ), + new ResourceSearchError( + '/clusters/foo', + 'database', + new Error('ssh: cert has expired') + ), + new ResourceSearchError( + '/clusters/foo', + 'kube', + new Error('ssh: cert has expired') + ), + ]; + const status = getActionPickerStatus({ + inputValue: 'foo', + filters: [], + filterActionsAttempt: makeSuccessAttempt([]), + allClusters: [], + actionAttempts: [makeSuccessAttempt([])], + resourceSearchAttempt: makeSuccessAttempt({ + errors: retryableErrors, + results: [], + search: 'foo', + }), + }); + + expect(status.inputState).toBe('some-input'); + const { clustersWithExpiredCerts } = + status.inputState === 'some-input' && status; + expect([...clustersWithExpiredCerts]).toEqual(['/clusters/foo']); + }); - const { searchMode } = status.inputState === 'no-input' && status; - expect(searchMode.kind).toBe('preview'); + describe('when there are no results', () => { + it('lists only the filtered offline cluster if a cluster filter is selected and the filtered cluster is offline', () => { + const filteredCluster = makeRootCluster({ + connected: false, + uri: '/clusters/filtered-cluster', + }); + const otherOfflineCluster = makeRootCluster({ + connected: false, + uri: '/clusters/other-offline-cluster', + }); + const status = getActionPickerStatus({ + inputValue: 'foo', + filters: [{ filter: 'cluster', clusterUri: filteredCluster.uri }], + filterActionsAttempt: makeSuccessAttempt([]), + allClusters: [filteredCluster, otherOfflineCluster], + actionAttempts: [makeSuccessAttempt([])], + resourceSearchAttempt: makeSuccessAttempt({ + errors: [], + results: [], + search: 'foo', + }), + }); + + expect(status.inputState).toBe('some-input'); + const { clustersWithExpiredCerts } = + status.inputState === 'some-input' && status; + expect([...clustersWithExpiredCerts]).toEqual([filteredCluster.uri]); + }); + + it('does not list offline clusters if a cluster filter is selected and that cluster is online and there are no results', () => { + const filteredCluster = makeRootCluster({ + connected: true, + uri: '/clusters/filtered-cluster', + }); + const otherOfflineCluster = makeRootCluster({ + connected: false, + uri: '/clusters/other-offline-cluster', + }); + const status = getActionPickerStatus({ + inputValue: 'foo', + filters: [{ filter: 'cluster', clusterUri: filteredCluster.uri }], + filterActionsAttempt: makeSuccessAttempt([]), + allClusters: [filteredCluster, otherOfflineCluster], + actionAttempts: [makeSuccessAttempt([])], + resourceSearchAttempt: makeSuccessAttempt({ + errors: [], + results: [], + search: 'foo', + }), + }); + + expect(status.inputState).toBe('some-input'); + const { clustersWithExpiredCerts } = + status.inputState === 'some-input' && status; + expect([...clustersWithExpiredCerts]).toHaveLength(0); + }); + }); + }); - const { nonRetryableResourceSearchErrors } = - searchMode.kind === 'preview' && searchMode; - expect(nonRetryableResourceSearchErrors).toEqual([nonRetryableError]); + describe('no-input search mode', () => { + it('returns non-retryable errors when fetching a preview after selecting a filter fails', () => { + const nonRetryableError = new ResourceSearchError( + '/clusters/bar', + 'server', + new Error('non-retryable error') + ); + const resourceSearchErrors = [ + new ResourceSearchError( + '/clusters/foo', + 'server', + new Error('ssh: cert has expired') + ), + nonRetryableError, + ]; + const status = getActionPickerStatus({ + inputValue: '', + filters: [{ filter: 'resource-type', resourceType: 'servers' }], + filterActionsAttempt: makeSuccessAttempt([]), + allClusters: [], + actionAttempts: [makeSuccessAttempt([])], + resourceSearchAttempt: makeSuccessAttempt({ + errors: resourceSearchErrors, + results: [], + search: '', + }), + }); + + expect(status.inputState).toBe('no-input'); + + const { searchMode } = status.inputState === 'no-input' && status; + expect(searchMode.kind).toBe('preview'); + + const { nonRetryableResourceSearchErrors } = + searchMode.kind === 'preview' && searchMode; + expect(nonRetryableResourceSearchErrors).toEqual([nonRetryableError]); + }); }); }); diff --git a/web/packages/teleterm/src/ui/Search/pickers/ActionPicker.tsx b/web/packages/teleterm/src/ui/Search/pickers/ActionPicker.tsx index 5a70a4f00b79a..9af3f521ccc28 100644 --- a/web/packages/teleterm/src/ui/Search/pickers/ActionPicker.tsx +++ b/web/packages/teleterm/src/ui/Search/pickers/ActionPicker.tsx @@ -30,6 +30,7 @@ import { Attempt, hasFinished } from 'shared/hooks/useAsync'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { + ClusterSearchFilter, ResourceMatch, SearchResult, ResourceSearchResult, @@ -401,7 +402,7 @@ export function getActionPickerStatus({ } const nonRetryableResourceSearchErrors = []; - const clustersWithExpiredCerts = new Set( + let clustersWithExpiredCerts = new Set( allClusters.filter(c => !c.connected).map(c => c.uri) ); const haveActionAttemptsFinished = actionAttempts.every(attempt => @@ -433,6 +434,21 @@ export function getActionPickerStatus({ }); } + // Make sure we don't list extra clusters with expired certs if a cluster filter is selected. + const clusterFilter = filters.find( + filter => filter.filter === 'cluster' + ) as ClusterSearchFilter; + if (clusterFilter) { + const hasClusterCertExpired = clustersWithExpiredCerts.has( + clusterFilter.clusterUri + ); + clustersWithExpiredCerts = new Set(); + + if (hasClusterCertExpired) { + clustersWithExpiredCerts.add(clusterFilter.clusterUri); + } + } + return { inputState: 'some-input', hasNoResults,