diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/KubeNamespaceSelector.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/KubeNamespaceSelector.tsx
index 116ac3765b898..dc4221942bc63 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/KubeNamespaceSelector.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/KubeNamespaceSelector.tsx
@@ -129,12 +129,16 @@ export function KubeNamespaceSelector({
};
async function handleLoadOptions(input: string) {
- const options = await fetchKubeNamespaces({
+ const namespaces = await fetchKubeNamespaces({
kubeCluster: kubeClusterItem.id,
search: input,
});
- return options;
+ return namespaces.map(namespace => ({
+ kind: 'namespace',
+ value: namespace,
+ label: namespace,
+ }));
}
return (
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/index.ts b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/index.ts
index 95712e2755a15..9e727450cd3e1 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/index.ts
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/index.ts
@@ -17,7 +17,11 @@
*/
export { RequestCheckoutWithSlider, RequestCheckout } from './RequestCheckout';
-export type { RequestCheckoutProps, PendingListItem } from './RequestCheckout';
+export type {
+ RequestCheckoutProps,
+ PendingListItem,
+ PendingKubeResourceItem,
+} from './RequestCheckout';
export * from './utils';
export type { ReviewerOption } from './types';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/index.ts b/web/packages/shared/components/AccessRequests/NewRequest/index.ts
index 44057733335fa..9074113221b16 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/index.ts
+++ b/web/packages/shared/components/AccessRequests/NewRequest/index.ts
@@ -20,3 +20,5 @@ export * from './RequestCheckout';
export * from './ResourceList';
export type { ResourceMap, RequestableResourceKind } from './resource';
export { getEmptyResourceState } from './resource';
+export type { KubeNamespaceRequest } from './kube';
+export { isKubeClusterWithNamespaces } from './kube';
diff --git a/web/packages/teleterm/src/ui/AccessRequestCheckout/AccessRequestCheckout.tsx b/web/packages/teleterm/src/ui/AccessRequestCheckout/AccessRequestCheckout.tsx
index 8e0904efbeee2..dbcc521911839 100644
--- a/web/packages/teleterm/src/ui/AccessRequestCheckout/AccessRequestCheckout.tsx
+++ b/web/packages/teleterm/src/ui/AccessRequestCheckout/AccessRequestCheckout.tsx
@@ -32,6 +32,7 @@ import * as Icon from 'design/Icon';
import { pluralize } from 'shared/utils/text';
import { RequestCheckoutWithSlider } from 'shared/components/AccessRequests/NewRequest';
+import { isKubeClusterWithNamespaces } from 'shared/components/AccessRequests/NewRequest/kube';
import useAccessRequestCheckout from './useAccessRequestCheckout';
import { AssumedRolesBar } from './AssumedRolesBar';
@@ -102,6 +103,8 @@ export function AccessRequestCheckout() {
pendingRequestTtlOptions,
startTime,
onStartTimeChange,
+ fetchKubeNamespaces,
+ bulkToggleKubeResources,
} = useAccessRequestCheckout();
const isRoleRequest = pendingAccessRequests[0]?.kind === 'role';
@@ -111,116 +114,126 @@ export function AccessRequestCheckout() {
setShowCheckout(false);
}
+ const pendingAccessRequestsWithoutParentResource =
+ pendingAccessRequests.filter(
+ d => !isKubeClusterWithNamespaces(d, pendingAccessRequests)
+ );
+
+ const numAddedResources = pendingAccessRequestsWithoutParentResource.length;
+
// We should rather detect how much space we have,
// but for simplicity we only count items.
const moreToShow = Math.max(
- pendingAccessRequests.length - MAX_RESOURCES_IN_BAR_TO_SHOW,
+ pendingAccessRequestsWithoutParentResource.length -
+ MAX_RESOURCES_IN_BAR_TO_SHOW,
0
);
- const numPendingAccessRequests = pendingAccessRequests.length;
-
return (
<>
- {pendingAccessRequests.length > 0 && !isCollapsed() && (
- props.theme.colors.spotBackground[1]};
- `}
- >
- 0 &&
+ !isCollapsed() && (
+ props.theme.space[1]}px;
+ border-top: 1px solid
+ ${props => props.theme.colors.spotBackground[1]};
`}
>
-
-
- {numPendingAccessRequests}{' '}
- {pluralize(
- numPendingAccessRequests,
- isRoleRequest ? 'role' : 'resource'
- )}{' '}
- added to access request:
-
-
- {pendingAccessRequests
- .slice(0, MAX_RESOURCES_IN_BAR_TO_SHOW)
- .map(c => {
- let resource = {
- name: c.name,
- key: `${c.clusterName}-${c.kind}-${c.id}`,
- Icon: undefined,
- };
- switch (c.kind) {
- case 'app':
- case 'saml_idp_service_provider':
- resource.Icon = Icon.Application;
- break;
- case 'node':
- resource.Icon = Icon.Server;
- break;
- case 'db':
- resource.Icon = Icon.Database;
- break;
- case 'kube_cluster':
- resource.Icon = Icon.Kubernetes;
- break;
- case 'role':
- break;
- default:
- c satisfies never;
- }
- return resource;
- })
- .map(c => (
-
- ))}
- {!!moreToShow && (
- + {moreToShow} more
- )}
+ {c.Icon && }
+
+ {c.name}
+
+
+ ))}
+ {!!moreToShow && (
+ + {moreToShow} more
+ )}
+
+
+
+ setShowCheckout(!showCheckout)}
+ textTransform="none"
+ css={`
+ white-space: nowrap;
+ `}
+ >
+ Proceed to request
+
+
+
+
-
- setShowCheckout(!showCheckout)}
- textTransform="none"
- css={`
- white-space: nowrap;
- `}
- >
- Proceed to request
-
-
-
-
-
-
-
- )}
+
+ )}
{assumedRequests.map(request => (
))}
@@ -270,11 +283,8 @@ export function AccessRequestCheckout() {
setPendingRequestTtl={setPendingRequestTtl}
startTime={startTime}
onStartTimeChange={onStartTimeChange}
- // TODO: these are placeholders to satisy linters.
- // There is a split PR that handles teleterm support
- // that will be merged right after this one (once both are approved)
- bulkToggleKubeResources={() => null}
- fetchKubeNamespaces={() => null}
+ fetchKubeNamespaces={fetchKubeNamespaces}
+ bulkToggleKubeResources={bulkToggleKubeResources}
/>
)}
diff --git a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.test.tsx b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.test.tsx
index 11e5c955f2fc8..a4dc881042005 100644
--- a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.test.tsx
+++ b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.test.tsx
@@ -21,11 +21,14 @@ import { renderHook, waitFor } from '@testing-library/react';
import {
makeRootCluster,
makeServer,
+ makeKube,
rootClusterUri,
} from 'teleterm/services/tshd/testHelpers';
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
+import { mapRequestToKubeNamespaceUri } from '../services/workspacesService/accessRequestsService';
+
import useAccessRequestCheckout from './useAccessRequestCheckout';
test('fetching requestable roles for servers uses UUID, not hostname', async () => {
@@ -64,3 +67,120 @@ test('fetching requestable roles for servers uses UUID, not hostname', async ()
})
);
});
+
+test('fetching requestable roles for a kube_cluster resource without specifying a namespace', async () => {
+ const kube = makeKube();
+ const cluster = makeRootCluster();
+ const appContext = new MockAppContext();
+ appContext.clustersService.setState(draftState => {
+ draftState.clusters.set(rootClusterUri, cluster);
+ });
+ await appContext.workspacesService.setActiveWorkspace(rootClusterUri);
+ await appContext.workspacesService
+ .getWorkspaceAccessRequestsService(rootClusterUri)
+ .addOrRemoveResource({
+ kind: 'kube',
+ resource: kube,
+ });
+
+ jest.spyOn(appContext.tshd, 'getRequestableRoles');
+
+ const wrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ renderHook(useAccessRequestCheckout, { wrapper });
+
+ await waitFor(() =>
+ expect(appContext.tshd.getRequestableRoles).toHaveBeenCalledWith({
+ clusterUri: rootClusterUri,
+ resourceIds: [
+ {
+ clusterName: 'teleport-local',
+ kind: 'kube_cluster',
+ name: kube.name,
+ subResourceName: '',
+ },
+ ],
+ })
+ );
+});
+
+test(`fetching requestable roles for a kube cluster's namespaces only creates resource IDs for its namespaces`, async () => {
+ const kube1 = makeKube();
+ const kube2 = makeKube({
+ name: 'kube2',
+ uri: `${rootClusterUri}/kubes/kube2`,
+ });
+ const cluster = makeRootCluster();
+ const appContext = new MockAppContext();
+ appContext.clustersService.setState(draftState => {
+ draftState.clusters.set(rootClusterUri, cluster);
+ });
+ await appContext.workspacesService.setActiveWorkspace(rootClusterUri);
+ await appContext.workspacesService
+ .getWorkspaceAccessRequestsService(rootClusterUri)
+ .addOrRemoveResource({
+ kind: 'kube',
+ resource: kube1,
+ });
+ await appContext.workspacesService
+ .getWorkspaceAccessRequestsService(rootClusterUri)
+ .addOrRemoveResource({
+ kind: 'kube',
+ resource: kube2,
+ });
+
+ await appContext.workspacesService
+ .getWorkspaceAccessRequestsService(rootClusterUri)
+ .addOrRemoveKubeNamespaces([
+ mapRequestToKubeNamespaceUri({
+ clusterUri: rootClusterUri,
+ id: kube1.name,
+ name: 'namespace1',
+ }),
+ mapRequestToKubeNamespaceUri({
+ clusterUri: rootClusterUri,
+ id: kube1.name,
+ name: 'namespace2',
+ }),
+ ]);
+
+ jest.spyOn(appContext.tshd, 'getRequestableRoles');
+
+ const wrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ renderHook(useAccessRequestCheckout, { wrapper });
+
+ await waitFor(() =>
+ expect(appContext.tshd.getRequestableRoles).toHaveBeenCalledWith({
+ clusterUri: rootClusterUri,
+ resourceIds: [
+ {
+ clusterName: 'teleport-local',
+ kind: 'namespace',
+ name: kube1.name,
+ subResourceName: 'namespace1',
+ },
+ {
+ clusterName: 'teleport-local',
+ kind: 'namespace',
+ name: kube1.name,
+ subResourceName: 'namespace2',
+ },
+ {
+ clusterName: 'teleport-local',
+ kind: 'kube_cluster',
+ name: kube2.name,
+ subResourceName: '',
+ },
+ ],
+ })
+ );
+});
diff --git a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts
index 4ad261cf61b3c..7f8095a019828 100644
--- a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts
+++ b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts
@@ -24,6 +24,9 @@ import useAttempt from 'shared/hooks/useAttemptNext';
import {
getDryRunMaxDuration,
PendingListItem,
+ PendingKubeResourceItem,
+ isKubeClusterWithNamespaces,
+ KubeNamespaceRequest,
} from 'shared/components/AccessRequests/NewRequest';
import { useSpecifiableFields } from 'shared/components/AccessRequests/NewRequest/useSpecifiableFields';
@@ -34,6 +37,8 @@ import {
PendingAccessRequest,
extractResourceRequestProperties,
ResourceRequest,
+ mapRequestToKubeNamespaceUri,
+ mapKubeNamespaceUriToRequest,
} from 'teleterm/ui/services/workspacesService/accessRequestsService';
import { retryWithRelogin } from 'teleterm/ui/utils';
import {
@@ -87,9 +92,18 @@ export default function useAccessRequestCheckout() {
const workspaceAccessRequest =
ctx.workspacesService.getActiveWorkspaceAccessRequestsService();
const docService = ctx.workspacesService.getActiveWorkspaceDocumentService();
- const pendingAccessRequest =
+ const pendingAccessRequestRequest =
workspaceAccessRequest?.getPendingAccessRequest();
+ const pendingAccessRequests = getPendingAccessRequestsPerResource(
+ pendingAccessRequestRequest
+ );
+
+ const pendingAccessRequestsWithoutParentResource =
+ pendingAccessRequests.filter(
+ p => !isKubeClusterWithNamespaces(p, pendingAccessRequests)
+ );
+
useEffect(() => {
// Do a new dry run per changes to pending access requests
// to get the latest time options and latest calculated
@@ -99,20 +113,18 @@ export default function useAccessRequestCheckout() {
if (showCheckout && requestedCount == 0) {
performDryRun();
}
- }, [showCheckout, pendingAccessRequest]);
+ }, [showCheckout, pendingAccessRequestRequest]);
useEffect(() => {
- if (!pendingAccessRequest || requestedCount > 0) {
+ if (!pendingAccessRequestRequest || requestedCount > 0) {
return;
}
- const pendingAccessRequests =
- getPendingAccessRequestsPerResource(pendingAccessRequest);
runFetchResourceRoles(() =>
retryWithRelogin(ctx, clusterUri, async () => {
const { response } = await ctx.tshd.getRequestableRoles({
clusterUri: rootClusterUri,
- resourceIds: pendingAccessRequests
+ resourceIds: pendingAccessRequestsWithoutParentResource
.filter(d => d.kind !== 'role')
.map(d => ({
// We have to use id, not name.
@@ -121,14 +133,14 @@ export default function useAccessRequestCheckout() {
name: d.id,
kind: d.kind,
clusterName: d.clusterName,
- subResourceName: '',
+ subResourceName: d.subResourceName || '',
})),
});
setResourceRequestRoles(response.applicableRoles);
setSelectedResourceRequestRoles(response.applicableRoles);
})
);
- }, [pendingAccessRequest]);
+ }, [pendingAccessRequestRequest]);
useEffect(() => {
clearCreateAttempt();
@@ -146,6 +158,9 @@ export default function useAccessRequestCheckout() {
}
}, [showCheckout, hasExited, createRequestAttempt.status]);
+ /**
+ * @param pendingRequest holds a list or map of resources to process
+ */
function getPendingAccessRequestsPerResource(
pendingRequest: PendingAccessRequest
): PendingListItemWithOriginalItem[] {
@@ -170,9 +185,34 @@ export default function useAccessRequestCheckout() {
}
case 'resource': {
pendingRequest.resources.forEach(resourceRequest => {
+ // If this request is a kube cluster and has namespaces
+ // extract each as own request.
+ if (
+ resourceRequest.kind === 'kube' &&
+ resourceRequest.resource.namespaces?.size > 0
+ ) {
+ // Process each namespace.
+ resourceRequest.resource.namespaces.forEach(namespaceRequestUri => {
+ const { kind, id, name } =
+ mapKubeNamespaceUriToRequest(namespaceRequestUri);
+
+ const item = {
+ kind,
+ id,
+ name,
+ subResourceName: name,
+ originalItem: resourceRequest,
+ clusterName:
+ ctx.clustersService.findClusterByResource(namespaceRequestUri)
+ ?.name,
+ };
+ pendingAccessRequests.push(item);
+ });
+ }
+
const { kind, id, name } =
extractResourceRequestProperties(resourceRequest);
- pendingAccessRequests.push({
+ const item: PendingListItemWithOriginalItem = {
kind,
id,
name,
@@ -180,7 +220,8 @@ export default function useAccessRequestCheckout() {
clusterName: ctx.clustersService.findClusterByResource(
resourceRequest.resource.uri
)?.name,
- });
+ };
+ pendingAccessRequests.push(item);
});
}
}
@@ -207,6 +248,21 @@ export default function useAccessRequestCheckout() {
);
}
+ async function bulkToggleKubeResources(
+ items: PendingKubeResourceItem[],
+ kubeCluster: PendingListKubeClusterWithOriginalItem
+ ) {
+ await workspaceAccessRequest.addOrRemoveKubeNamespaces(
+ items.map(item =>
+ mapRequestToKubeNamespaceUri({
+ id: item.id,
+ name: item.subResourceName,
+ clusterUri: kubeCluster.originalItem.resource.uri,
+ })
+ )
+ );
+ }
+
function getAssumedRequests() {
if (!clusterUri) {
return [];
@@ -222,22 +278,22 @@ export default function useAccessRequestCheckout() {
* Shared logic used both during dry runs and regular access request creation.
*/
function prepareAndCreateRequest(req: CreateRequest) {
- const pendingAccessRequests =
- getPendingAccessRequestsPerResource(pendingAccessRequest);
const params: CreateAccessRequestRequest = {
rootClusterUri,
reason: req.reason,
suggestedReviewers: req.suggestedReviewers || [],
dryRun: req.dryRun,
- resourceIds: pendingAccessRequests
+ resourceIds: pendingAccessRequestsWithoutParentResource
.filter(d => d.kind !== 'role')
- .map(d => ({
- name: d.id,
- clusterName: d.clusterName,
- kind: d.kind,
- subResourceName: '',
- })),
- roles: pendingAccessRequests
+ .map(d => {
+ return {
+ name: d.id,
+ clusterName: d.clusterName,
+ kind: d.kind,
+ subResourceName: d.subResourceName || '',
+ };
+ }),
+ roles: pendingAccessRequestsWithoutParentResource
.filter(d => d.kind === 'role')
.map(d => d.name),
assumeStartTime: req.start && Timestamp.fromDate(req.start),
@@ -245,6 +301,11 @@ export default function useAccessRequestCheckout() {
requestTtl: req.requestTTL && Timestamp.fromDate(req.requestTTL),
};
+ // Don't attempt creating anything if there are no resources selected.
+ if (!params.resourceIds.length && !params.roles.length) {
+ return;
+ }
+
// if we have a resource access request, we pass along the selected roles from the checkout
if (params.resourceIds.length > 0) {
params.roles = selectedResourceRequestRoles;
@@ -256,7 +317,8 @@ export default function useAccessRequestCheckout() {
ctx.clustersService.createAccessRequest(params).then(({ response }) => {
return {
accessRequest: response.request,
- requestedCount: pendingAccessRequests.length,
+ requestedCount:
+ pendingAccessRequestsWithoutParentResource.filter.length,
};
})
).catch(e => {
@@ -275,9 +337,9 @@ export default function useAccessRequestCheckout() {
});
teletermAccessRequest = accessRequest;
} catch {
+ setCreateRequestAttempt({ status: '' });
return;
}
-
setCreateRequestAttempt({ status: '' });
const accessRequest = makeUiAccessRequest(teletermAccessRequest);
@@ -333,9 +395,27 @@ export default function useAccessRequestCheckout() {
}
}
+ async function fetchKubeNamespaces({
+ kubeCluster,
+ search,
+ }: KubeNamespaceRequest): Promise {
+ const { response } = await ctx.tshd.listKubernetesResources({
+ searchKeywords: search,
+ limit: 50,
+ useSearchAsRoles: true,
+ nextKey: '',
+ resourceType: 'namespace',
+ clusterUri,
+ predicateExpression: '',
+ kubernetesCluster: kubeCluster,
+ kubernetesNamespace: '',
+ });
+ return response.resources.map(i => i.name);
+ }
+
const shouldShowClusterNameColumn =
- pendingAccessRequest?.kind === 'resource' &&
- Array.from(pendingAccessRequest.resources.values()).some(a =>
+ pendingAccessRequestRequest?.kind === 'resource' &&
+ Array.from(pendingAccessRequestRequest.resources.values()).some(a =>
routing.isLeafCluster(a.resource.uri)
);
@@ -344,8 +424,7 @@ export default function useAccessRequestCheckout() {
isCollapsed,
assumedRequests: getAssumedRequests(),
toggleResource,
- pendingAccessRequests:
- getPendingAccessRequestsPerResource(pendingAccessRequest),
+ pendingAccessRequests,
shouldShowClusterNameColumn,
createRequest,
reset,
@@ -373,6 +452,8 @@ export default function useAccessRequestCheckout() {
pendingRequestTtlOptions,
startTime,
onStartTimeChange,
+ fetchKubeNamespaces,
+ bulkToggleKubeResources,
};
}
@@ -386,3 +467,8 @@ type PendingListItemWithOriginalItem = Omit &
kind: 'role';
}
);
+
+type PendingListKubeClusterWithOriginalItem = Omit & {
+ kind: Extract;
+ originalItem: Extract;
+};
diff --git a/web/packages/teleterm/src/ui/DocumentAccessRequests/NewRequest/useNewRequest.ts b/web/packages/teleterm/src/ui/DocumentAccessRequests/NewRequest/useNewRequest.ts
index fefd237f739f0..bcc9d7b04f3e0 100644
--- a/web/packages/teleterm/src/ui/DocumentAccessRequests/NewRequest/useNewRequest.ts
+++ b/web/packages/teleterm/src/ui/DocumentAccessRequests/NewRequest/useNewRequest.ts
@@ -22,6 +22,7 @@ import { FetchStatus, SortType } from 'design/DataTable/types';
import useAttempt from 'shared/hooks/useAttemptNext';
import { makeAdvancedSearchQueryForLabel } from 'shared/utils/advancedSearchLabelQuery';
+import { RequestableResourceKind } from 'shared/components/AccessRequests/NewRequest/resource';
import {
ShowResources,
@@ -49,7 +50,6 @@ import type {
ResourceLabel,
ResourceFilter as WeakAgentFilter,
ResourcesResponse,
- ResourceIdKind,
UnifiedResource,
} from 'teleport/services/agents';
import type * as teleportApps from 'teleport/services/apps';
@@ -211,6 +211,17 @@ export default function useNewRequest(rootCluster: Cluster) {
return;
}
+ /**
+ * This should never happen but just a safeguard.
+ * This function is used in the "unified resources" view,
+ * where a user can click on a "request access" button.
+ * Selecting kube_cluster's namespace is not available in this view
+ * (instead it is rendered in the "request checkout" view).
+ */
+ if (kind === 'namespace') {
+ return;
+ }
+
accessRequestsService.addOrRemoveResource(
toResourceRequest({
kind,
@@ -348,8 +359,13 @@ function getDefaultSort(kind: ResourceKind): SortType {
export type ResourceKind =
| Extract<
- ResourceIdKind,
- 'node' | 'app' | 'db' | 'kube_cluster' | 'saml_idp_service_provider'
+ RequestableResourceKind,
+ | 'node'
+ | 'app'
+ | 'db'
+ | 'kube_cluster'
+ | 'saml_idp_service_provider'
+ | 'namespace'
>
| 'role';
diff --git a/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx b/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx
index 03da9da97f998..1d2f6e7613701 100644
--- a/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx
+++ b/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx
@@ -223,7 +223,7 @@ export function UnifiedResources(props: {
const bulkAddResources = useCallback(
(resources: UnifiedResourceResponse[]) => {
- accessRequestsService.addOrRemoveResources(resources);
+ accessRequestsService.addAllOrRemoveAllResources(resources);
},
[accessRequestsService]
);
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 751cab9849052..c0b2f46f99217 100644
--- a/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.test.ts
+++ b/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.test.ts
@@ -124,7 +124,7 @@ test('getAddedItemsCount() returns added resource count for pending request', ()
expect(service.getAddedItemsCount()).toBe(0);
});
-test('addOrRemoveResources() adds all resources to pending request', async () => {
+test('addAllOrRemoveAllResources() adds all resources to pending request', async () => {
const { accessRequestsService: service } = getTestSetup(
getMockPendingResourceAccessRequest()
);
@@ -138,7 +138,9 @@ test('addOrRemoveResources() adds all resources to pending request', async () =>
});
// add a single resource that isn't added should add to the request
- await service.addOrRemoveResources([{ kind: 'server', resource: server }]);
+ await service.addAllOrRemoveAllResources([
+ { kind: 'server', resource: server },
+ ]);
let pendingAccessRequest = service.getPendingAccessRequest();
expect(
pendingAccessRequest.kind === 'resource' &&
@@ -149,7 +151,7 @@ test('addOrRemoveResources() adds all resources to pending request', async () =>
});
// padding an array that contains some resources already added and some that aren't should add them all
- await service.addOrRemoveResources([
+ await service.addAllOrRemoveAllResources([
{ kind: 'server', resource: server },
{ kind: 'server', resource: server2 },
]);
@@ -170,7 +172,7 @@ test('addOrRemoveResources() adds all resources to pending request', async () =>
});
// passing an array of resources that are all already added should remove all the passed resources
- await service.addOrRemoveResources([
+ await service.addAllOrRemoveAllResources([
{ kind: 'server', resource: server },
{ kind: 'server', resource: server2 },
]);
diff --git a/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.ts b/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.ts
index f8ff36b9c2491..e408e8244de68 100644
--- a/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.ts
+++ b/web/packages/teleterm/src/ui/services/workspacesService/accessRequestsService.ts
@@ -24,6 +24,7 @@ import {
DatabaseUri,
KubeUri,
AppUri,
+ KubeResourceNamespaceUri,
} from 'teleterm/ui/uri';
import { ModalsService } from 'teleterm/ui/services/modals';
@@ -98,7 +99,44 @@ export class AccessRequestsService {
});
}
- async addOrRemoveResources(requestedResources: ResourceRequest[]) {
+ async addOrRemoveKubeNamespaces(namespaceUris: KubeResourceNamespaceUri[]) {
+ this.setState(draftState => {
+ if (draftState.pending.kind !== 'resource') {
+ throw new Error('Cannot add a kube namespace to a role access request');
+ }
+
+ const { resources } = draftState.pending;
+
+ namespaceUris.forEach(resourceUri => {
+ const requestedResource = resources.get(
+ routing.getKubeUri(
+ routing.parseKubeResourceNamespaceUri(resourceUri).params
+ )
+ );
+ if (!requestedResource || requestedResource.kind !== 'kube') {
+ throw new Error('Cannot add a kube namespace to a non-kube resource');
+ }
+ const kubeResource = requestedResource.resource;
+
+ if (!kubeResource.namespaces) {
+ kubeResource.namespaces = new Set();
+ }
+ if (kubeResource.namespaces.has(resourceUri)) {
+ kubeResource.namespaces.delete(resourceUri);
+ } else {
+ kubeResource.namespaces.add(resourceUri);
+ }
+ });
+ });
+ }
+
+ /**
+ * Removes all requested resources, if all the requested resources were already added
+ * or adds all requested resources, if not all requested resources were added.
+ *
+ * Typically used when user "selects all or deselects all"
+ */
+ async addAllOrRemoveAllResources(requestedResources: ResourceRequest[]) {
if (!(await this.canUpdateRequest('resource'))) {
return;
}
@@ -258,6 +296,7 @@ export type ResourceRequest =
kind: 'kube';
resource: {
uri: KubeUri;
+ namespaces?: Set;
};
}
| {
@@ -287,8 +326,7 @@ export function extractResourceRequestProperties({
kind: SharedResourceAccessRequestKind;
id: string;
/**
- * Pretty name of the resource (can be the same as `id`).
- * For example, for nodes, we want to show hostname instead of its id.
+ * Can refer to a pretty name of the resource (can be the same as `id`)
*/
name: string;
} {
@@ -317,6 +355,42 @@ export function extractResourceRequestProperties({
}
}
+export function mapRequestToKubeNamespaceUri({
+ clusterUri,
+ id,
+ name,
+}: {
+ clusterUri: ClusterUri;
+ /** kubeId */
+ id: string;
+ /** kubeNamespaceId */
+ name: string;
+}) {
+ const {
+ params: { rootClusterId, leafClusterId },
+ } = routing.parseClusterUri(clusterUri);
+ return routing.getKubeResourceNamespaceUri({
+ rootClusterId,
+ leafClusterId,
+ kubeId: id,
+ kubeNamespaceId: name,
+ });
+}
+
+export function mapKubeNamespaceUriToRequest(
+ kubeNamespaceUri: KubeResourceNamespaceUri
+): {
+ kind: 'namespace';
+ /** kubeId */
+ id: string;
+ /** kubeNamespaceId */
+ name: string;
+} {
+ const { kubeNamespaceId, kubeId } =
+ routing.parseKubeResourceNamespaceUri(kubeNamespaceUri).params;
+ return { kind: 'namespace', id: kubeId, name: kubeNamespaceId };
+}
+
/**
* Maps the type used by the shared access requests to the type
* required by the access requests service.