Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 188 additions & 124 deletions web/packages/teleterm/src/ui/Search/pickers/ActionPicker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});
});
18 changes: 17 additions & 1 deletion web/packages/teleterm/src/ui/Search/pickers/ActionPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { Attempt, hasFinished } from 'shared/hooks/useAsync';

import { useAppContext } from 'teleterm/ui/appContextProvider';
import {
ClusterSearchFilter,
ResourceMatch,
SearchResult,
ResourceSearchResult,
Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -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,
Expand Down