From 944d79f9c6923dcae732a540144bf6b8f9095d8f Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 4 Sep 2024 10:26:35 -0500 Subject: [PATCH] This will add `addOrRemoveResources` to support the "Add/Remove to request" bulk action in connect (#45964) --- .../RequestCheckout/RequestCheckout.tsx | 1 + .../UnifiedResources/UnifiedResources.tsx | 12 +-- .../ui/DocumentCluster/UnifiedResources.tsx | 88 ++++++++++++++++--- .../accessRequestsService.test.ts | 61 +++++++++++++ .../accessRequestsService.ts | 27 ++++++ 5 files changed, 173 insertions(+), 16 deletions(-) diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx index f6d629321fa34..6dc9509864375 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx @@ -194,6 +194,7 @@ export function RequestCheckout({ diff --git a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx index 2b9c698a16a8e..5426b0d908a74 100644 --- a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx +++ b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx @@ -113,12 +113,12 @@ export type BulkAction = { * over if this prop is supplied */ tooltip?: string; - action: ( - selectedResources: { - unifiedResourceId: string; - resource: SharedUnifiedResource['resource']; - }[] - ) => void; + action: (selectedResources: SelectedResource[]) => void; +}; + +export type SelectedResource = { + unifiedResourceId: string; + resource: SharedUnifiedResource['resource']; }; export type FilterKind = { diff --git a/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx b/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx index 1bb1bb8cfbeaf..5c343b7a21db5 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx @@ -189,6 +189,10 @@ export function UnifiedResources(props: { const requestStarted = accessRequestsService.getAddedItemsCount() > 0; + const getAddedItemsCount = useCallback(() => { + return accessRequestsService.getAddedItemsCount(); + }, [accessRequestsService]); + const getAccessRequestButton = useCallback( (resource: UnifiedResourceResponse) => { const isResourceAdded = addedResources?.has(resource.resource.uri); @@ -219,6 +223,13 @@ export function UnifiedResources(props: { ] ); + const bulkAddResources = useCallback( + (resources: UnifiedResourceResponse[]) => { + accessRequestsService.addOrRemoveResources(resources); + }, + [accessRequestsService] + ); + return ( JSX.Element; + getAccessRequestButton: (resource: UnifiedResourceResponse) => JSX.Element; + getAddedItemsCount: () => number; + bulkAddResources: (resources: UnifiedResourceResponse[]) => void; integratedAccessRequests: IntegratedAccessRequests; }) => { const appContext = useAppContext(); @@ -321,6 +336,44 @@ const Resources = memo( return cleanup; }, [onResourcesRefreshRequest, fetch]); + const { getAccessRequestButton } = props; + // The action callback in the requestAccess action has access to + // `SharedUnifiedResource['resource']`, but `props.bulkAddResources` accepts + // `UnifiedResourceResponse`. Because of that, we need to to have the + // getUnifiedResourceFromSharedResource function. + const { sharedResources, getUnifiedResourceFromSharedResource } = + useMemo(() => { + const sharedResources: SharedUnifiedResource[] = []; + const sharedResourceToUnifiedResource = new Map< + SharedUnifiedResource['resource'], + UnifiedResourceResponse + >(); + + resources.forEach(resource => { + let sharedResource = mapToSharedResource(resource); + const accessRequestButton = getAccessRequestButton(resource); + if (accessRequestButton) { + sharedResource.ui.ActionButton = accessRequestButton; + } + + sharedResources.push(sharedResource); + sharedResourceToUnifiedResource.set( + sharedResource.resource, + resource + ); + }); + + const getUnifiedResourceFromSharedResource = + sharedResourceToUnifiedResource.get.bind( + sharedResourceToUnifiedResource + ); + + return { + sharedResources, + getUnifiedResourceFromSharedResource, + }; + }, [resources, getAccessRequestButton]); + const resourceIds = props.userPreferences.clusterPreferences?.pinnedResources?.resourceIds; const { updateUserPreferences } = props; @@ -340,6 +393,29 @@ const Resources = memo( params={props.queryParams} setParams={props.onParamsChange} unifiedResourcePreferencesAttempt={props.userPreferencesAttempt} + bulkActions={ + props.integratedAccessRequests.supported === 'yes' + ? [ + { + key: 'requestAccess', + Icon: icons.AddCircle, + text: + props.getAddedItemsCount() > 0 + ? 'Add/Remove to Request' + : 'Request Access', + disabled: false, + action: selectedResources => + props.bulkAddResources( + selectedResources.map(sharedResource => + getUnifiedResourceFromSharedResource( + sharedResource.resource + ) + ) + ), + }, + ] + : [] + } unifiedResourcePreferences={ props.userPreferences.unifiedResourcePreferences } @@ -352,15 +428,7 @@ const Resources = memo( ? props.integratedAccessRequests.availabilityFilter : undefined } - resources={resources.map(r => { - const { resource, ui } = mapToSharedResource(r); - return { - resource, - ui: { - ActionButton: props.getAccessRequestButton(r) || ui.ActionButton, - }, - }; - })} + resources={sharedResources} resourcesFetchAttempt={attempt} fetchResources={fetch} availableKinds={[ diff --git a/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.test.ts index 2f33a7485d692..751cab9849052 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.test.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.test.ts @@ -124,6 +124,67 @@ test('getAddedItemsCount() returns added resource count for pending request', () expect(service.getAddedItemsCount()).toBe(0); }); +test('addOrRemoveResources() adds all resources to pending request', async () => { + const { accessRequestsService: service } = getTestSetup( + getMockPendingResourceAccessRequest() + ); + const server = makeServer({ + uri: `${rootClusterUri}/servers/ser`, + hostname: 'ser', + }); + const server2 = makeServer({ + uri: `${rootClusterUri}/servers/ser2`, + hostname: 'ser2', + }); + + // add a single resource that isn't added should add to the request + await service.addOrRemoveResources([{ kind: 'server', resource: server }]); + let pendingAccessRequest = service.getPendingAccessRequest(); + expect( + pendingAccessRequest.kind === 'resource' && + pendingAccessRequest.resources.get(server.uri) + ).toStrictEqual({ + kind: 'server', + resource: { hostname: server.hostname, uri: server.uri }, + }); + + // padding an array that contains some resources already added and some that aren't should add them all + await service.addOrRemoveResources([ + { kind: 'server', resource: server }, + { kind: 'server', resource: server2 }, + ]); + pendingAccessRequest = service.getPendingAccessRequest(); + expect( + pendingAccessRequest.kind === 'resource' && + pendingAccessRequest.resources.get(server.uri) + ).toStrictEqual({ + kind: 'server', + resource: { hostname: server.hostname, uri: server.uri }, + }); + expect( + pendingAccessRequest.kind === 'resource' && + pendingAccessRequest.resources.get(server2.uri) + ).toStrictEqual({ + kind: 'server', + resource: { hostname: server2.hostname, uri: server2.uri }, + }); + + // passing an array of resources that are all already added should remove all the passed resources + await service.addOrRemoveResources([ + { kind: 'server', resource: server }, + { kind: 'server', resource: server2 }, + ]); + pendingAccessRequest = service.getPendingAccessRequest(); + expect( + pendingAccessRequest.kind === 'resource' && + pendingAccessRequest.resources.get(server.uri) + ).toStrictEqual(undefined); + expect( + pendingAccessRequest.kind === 'resource' && + pendingAccessRequest.resources.get(server2.uri) + ).toStrictEqual(undefined); +}); + test('addOrRemoveResource() adds resource to pending request', async () => { const { accessRequestsService: service } = getTestSetup( getMockPendingResourceAccessRequest() diff --git a/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.ts b/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.ts index 06f0aae88216c..f8ff36b9c2491 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.ts @@ -98,6 +98,33 @@ export class AccessRequestsService { }); } + async addOrRemoveResources(requestedResources: ResourceRequest[]) { + if (!(await this.canUpdateRequest('resource'))) { + return; + } + this.setState(draftState => { + if (draftState.pending.kind !== 'resource') { + draftState.pending = { + kind: 'resource', + resources: new Map(), + }; + } + + const { resources } = draftState.pending; + const allAdded = requestedResources.every(r => + resources.has(r.resource.uri) + ); + + requestedResources.forEach(request => { + if (allAdded) { + resources.delete(request.resource.uri); + } else { + resources.set(request.resource.uri, getRequiredProperties(request)); + } + }); + }); + } + async addResource(request: ResourceRequest): Promise { if (!(await this.canUpdateRequest('resource'))) { return;