Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v16] Support resource availability switcher in Connect #42756

Merged
merged 5 commits into from
Jun 11, 2024
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
16 changes: 1 addition & 15 deletions lib/teleterm/services/userpreferences/userpreferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,36 +161,22 @@ func Update(ctx context.Context, rootClient Client, leafClient Client, newPrefer
// and LabelsViewMode fields in UnifiedResourcePreferences.
// The fields are updated one by one (instead of passing the entire struct as new preferences)
// to prevent potential new fields from being overwritten.
// Supports oldPreferences being nil.
func updateUnifiedResourcePreferences(oldPreferences *userpreferencesv1.UnifiedResourcePreferences, newPreferences *userpreferencesv1.UnifiedResourcePreferences) *userpreferencesv1.UnifiedResourcePreferences {
updated := oldPreferences
// TODO(gzdunek): DELETE IN 16.0.0.
// We won't have to support old preferences being nil.
if oldPreferences == nil {
updated = &userpreferencesv1.UnifiedResourcePreferences{}
}

updated.DefaultTab = newPreferences.DefaultTab
updated.ViewMode = newPreferences.ViewMode
updated.LabelsViewMode = newPreferences.LabelsViewMode
updated.AvailableResourceMode = newPreferences.AvailableResourceMode

return updated
}

// updateClusterPreferences updates pinned resources in ClusterUserPreferences.
// The fields are updated one by one (instead of passing the entire struct as new preferences)
// to prevent potential new fields from being overwritten.
// Supports oldPreferences being nil.
func updateClusterPreferences(oldPreferences *userpreferencesv1.ClusterUserPreferences, newPreferences *userpreferencesv1.ClusterUserPreferences) *userpreferencesv1.ClusterUserPreferences {
updated := oldPreferences
// TODO(gzdunek): DELETE IN 16.0.0.
// We won't have to support old preferences being nil.
if oldPreferences == nil {
updated = &userpreferencesv1.ClusterUserPreferences{}
}
if updated.PinnedResources == nil {
updated.PinnedResources = &userpreferencesv1.PinnedResourcesUserPreferences{}
}

updated.PinnedResources.ResourceIds = newPreferences.PinnedResources.ResourceIds

Expand Down
14 changes: 8 additions & 6 deletions lib/teleterm/services/userpreferences/userpreferences_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ var rootPreferencesMock = &userpreferencesv1.UserPreferences{
},
},
UnifiedResourcePreferences: &userpreferencesv1.UnifiedResourcePreferences{
DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_ALL,
ViewMode: userpreferencesv1.ViewMode_VIEW_MODE_CARD,
LabelsViewMode: userpreferencesv1.LabelsViewMode_LABELS_VIEW_MODE_COLLAPSED,
DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_ALL,
ViewMode: userpreferencesv1.ViewMode_VIEW_MODE_CARD,
LabelsViewMode: userpreferencesv1.LabelsViewMode_LABELS_VIEW_MODE_COLLAPSED,
AvailableResourceMode: userpreferencesv1.AvailableResourceMode_AVAILABLE_RESOURCE_MODE_NONE,
},
}

Expand Down Expand Up @@ -106,9 +107,10 @@ func TestUserPreferencesUpdateForRootAndLeaf(t *testing.T) {
},
},
UnifiedResourcePreferences: &userpreferencesv1.UnifiedResourcePreferences{
DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_PINNED,
ViewMode: userpreferencesv1.ViewMode_VIEW_MODE_LIST,
LabelsViewMode: userpreferencesv1.LabelsViewMode_LABELS_VIEW_MODE_EXPANDED,
DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_PINNED,
ViewMode: userpreferencesv1.ViewMode_VIEW_MODE_LIST,
LabelsViewMode: userpreferencesv1.LabelsViewMode_LABELS_VIEW_MODE_EXPANDED,
AvailableResourceMode: userpreferencesv1.AvailableResourceMode_AVAILABLE_RESOURCE_MODE_REQUESTABLE,
},
}

Expand Down
272 changes: 272 additions & 0 deletions web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/**
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { render, screen } from 'design/utils/testing';
import { mockIntersectionObserver } from 'jsdom-testing-mocks';
import { act } from '@testing-library/react';

import {
AvailableResourceMode,
DefaultTab,
ViewMode,
LabelsViewMode,
} from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb';
import { ShowResources } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';

import { UnifiedResources } from 'teleterm/ui/DocumentCluster/UnifiedResources';
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
import { ResourcesContextProvider } from 'teleterm/ui/DocumentCluster/resourcesContext';
import { ConnectMyComputerContextProvider } from 'teleterm/ui/ConnectMyComputer';
import { MockWorkspaceContextProvider } from 'teleterm/ui/fixtures/MockWorkspaceContextProvider';
import { makeDocumentCluster } from 'teleterm/ui/services/workspacesService/documentsService/testHelpers';
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
import {
makeRootCluster,
rootClusterUri,
} from 'teleterm/services/tshd/testHelpers';
import { getEmptyPendingAccessRequest } from 'teleterm/ui/services/workspacesService/accessRequestsService';

import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient';

const mio = mockIntersectionObserver();

const tests = [
{
name: 'fetches only available resources if cluster does not support access requests',
conditions: {
isClusterSupportingAccessRequests: false,
showResources: ShowResources.REQUESTABLE,
availableResourceModePreference: AvailableResourceMode.ALL,
},
expect: {
searchAsRoles: false,
includeRequestable: false,
},
},
{
name: 'fetches all resources if cluster allows listing all and user preferences says all',
conditions: {
isClusterSupportingAccessRequests: true,
showResources: ShowResources.REQUESTABLE,
availableResourceModePreference: AvailableResourceMode.ALL,
},
expect: {
searchAsRoles: false,
includeRequestable: true,
},
},
{
name: 'fetches all resources if cluster allows listing all and user preferences says none',
conditions: {
isClusterSupportingAccessRequests: true,
showResources: ShowResources.REQUESTABLE,
availableResourceModePreference: AvailableResourceMode.ALL,
},
expect: {
searchAsRoles: false,
includeRequestable: true,
},
},
{
name: 'fetches accessible resources if cluster allows listing all and user preferences says accessible',
conditions: {
isClusterSupportingAccessRequests: true,
showResources: ShowResources.REQUESTABLE,
availableResourceModePreference: AvailableResourceMode.ACCESSIBLE,
},
expect: {
searchAsRoles: false,
includeRequestable: false,
},
},
{
name: 'fetches requestable resources if cluster allows listing all and user preferences says requestable',
conditions: {
isClusterSupportingAccessRequests: true,
showResources: ShowResources.REQUESTABLE,
availableResourceModePreference: AvailableResourceMode.REQUESTABLE,
},
expect: {
searchAsRoles: true,
includeRequestable: false,
},
},
{
name: 'fetches only accessible resources if cluster does not allow listing all',
conditions: {
isClusterSupportingAccessRequests: true,
showResources: ShowResources.ACCESSIBLE_ONLY,
availableResourceModePreference: AvailableResourceMode.UNSPECIFIED,
},
expect: {
searchAsRoles: false,
includeRequestable: false,
},
},
{
name: 'fetches only accessible resources if cluster does not allow listing all and user preferences says accessible',
conditions: {
isClusterSupportingAccessRequests: true,
showResources: ShowResources.ACCESSIBLE_ONLY,
availableResourceModePreference: AvailableResourceMode.ALL,
},
expect: {
searchAsRoles: false,
includeRequestable: false,
},
},
{
name: 'fetches only requestable resources if cluster does not allow listing all and user preferences says requestable',
conditions: {
isClusterSupportingAccessRequests: true,
showResources: ShowResources.ACCESSIBLE_ONLY,
availableResourceModePreference: AvailableResourceMode.REQUESTABLE,
},
expect: {
searchAsRoles: true,
includeRequestable: false,
},
},
{
name: 'fetches only accessible resources if cluster does not allow listing all but user preferences says all',
conditions: {
isClusterSupportingAccessRequests: true,
showResources: ShowResources.ACCESSIBLE_ONLY,
availableResourceModePreference: AvailableResourceMode.ALL,
},
expect: {
searchAsRoles: false,
includeRequestable: false,
},
},
{
name: 'fetches only accessible resources if cluster does not allow listing all but user preferences says none',
conditions: {
isClusterSupportingAccessRequests: true,
showResources: ShowResources.ACCESSIBLE_ONLY,
availableResourceModePreference: AvailableResourceMode.NONE,
},
expect: {
searchAsRoles: false,
includeRequestable: false,
},
},
];

test.each(tests)('$name', async testCase => {
const doc = makeDocumentCluster();

const appContext = new MockAppContext({ platform: 'darwin' });
appContext.clustersService.setState(draft => {
draft.clusters.set(
doc.clusterUri,
makeRootCluster({
uri: doc.clusterUri,
features: {
advancedAccessWorkflows:
testCase.conditions.isClusterSupportingAccessRequests,
isUsageBasedBilling: false,
},
showResources: testCase.conditions.showResources,
})
);
});

appContext.workspacesService.setState(draftState => {
const rootClusterUri = doc.clusterUri;
draftState.rootClusterUri = rootClusterUri;
draftState.workspaces[rootClusterUri] = {
localClusterUri: doc.clusterUri,
documents: [doc],
location: doc.uri,
unifiedResourcePreferences: {
defaultTab: DefaultTab.ALL,
viewMode: ViewMode.CARD,
labelsViewMode: LabelsViewMode.COLLAPSED,
availableResourceMode:
testCase.conditions.availableResourceModePreference,
},
accessRequests: {
pending: getEmptyPendingAccessRequest(),
isBarCollapsed: true,
},
};
});

jest.spyOn(appContext.tshd, 'getUserPreferences').mockResolvedValue(
new MockedUnaryCall({
userPreferences: {
unifiedResourcePreferences: {
defaultTab: DefaultTab.ALL,
viewMode: ViewMode.CARD,
labelsViewMode: LabelsViewMode.COLLAPSED,
availableResourceMode:
testCase.conditions.availableResourceModePreference,
},
},
})
);

jest
.spyOn(appContext.resourcesService, 'listUnifiedResources')
.mockResolvedValue({
resources: [],
nextKey: '',
});

render(
<MockAppContextProvider appContext={appContext}>
<MockWorkspaceContextProvider>
<ResourcesContextProvider>
<ConnectMyComputerContextProvider rootClusterUri={doc.clusterUri}>
<UnifiedResources
clusterUri={doc.clusterUri}
docUri={doc.uri}
queryParams={doc.queryParams}
/>
</ConnectMyComputerContextProvider>
</ResourcesContextProvider>
</MockWorkspaceContextProvider>
</MockAppContextProvider>
);

act(mio.enterAll);

await expect(
screen.findByText('Add your first resource to Teleport')
).resolves.toBeInTheDocument();

expect(appContext.resourcesService.listUnifiedResources).toHaveBeenCalledWith(
{
clusterUri: rootClusterUri,
includeRequestable: testCase.expect.includeRequestable,
kinds: [],
limit: 48,
pinnedOnly: false,
query: '',
search: '',
searchAsRoles: testCase.expect.searchAsRoles,
sortBy: {
field: 'name',
isDesc: false,
},
startKey: '',
},
new AbortController().signal
);
});
Loading
Loading