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
4 changes: 2 additions & 2 deletions web/packages/teleterm/src/services/tshd/fixtures/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class MockTshClient implements TshClient {

getCluster: (clusterUri: string) => Promise<Cluster>;
getAuthSettings: (clusterUri: string) => Promise<AuthSettings>;
removeCluster: (clusterUri: string) => Promise<undefined>;
removeCluster = () => Promise.resolve();
loginLocal: (
params: LoginLocalParams,
abortSignal?: TshAbortSignal
Expand All @@ -94,7 +94,7 @@ export class MockTshClient implements TshClient {
params: LoginPasswordlessParams,
abortSignal?: TshAbortSignal
) => Promise<undefined>;
logout: (clusterUri: string) => Promise<undefined>;
logout = () => Promise.resolve();
transferFile: () => undefined;
reportUsageEvent: () => undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import React from 'react';

import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';

import { ClusterLogout } from './ClusterLogout';

export default {
Expand All @@ -24,13 +26,12 @@ export default {

export const Story = () => {
return (
<ClusterLogout
status=""
statusText=""
removeCluster={() => Promise.resolve([undefined, null])}
onClose={() => {}}
clusterUri="/clusters/foo"
clusterTitle="foo"
/>
<MockAppContextProvider>
<ClusterLogout
onClose={() => {}}
clusterUri="/clusters/foo"
clusterTitle="foo"
/>
</MockAppContextProvider>
);
};
30 changes: 21 additions & 9 deletions web/packages/teleterm/src/ui/ClusterLogout/ClusterLogout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,32 @@ import { ButtonIcon, ButtonPrimary, Text } from 'design';

import { Close } from 'design/Icon';

import { Props, State, useClusterLogout } from './useClusterLogout';
import { RootClusterUri } from 'teleterm/ui/uri';

export default function Container(props: Props) {
const state = useClusterLogout(props);
return <ClusterLogout {...state} />;
import { useClusterLogout } from './useClusterLogout';

interface ClusterLogoutProps {
clusterTitle: string;
clusterUri: RootClusterUri;
onClose(): void;
}

export function ClusterLogout({
status,
clusterUri,
onClose,
statusText,
clusterTitle,
removeCluster,
}: State) {
}: ClusterLogoutProps) {
const { removeCluster, status, statusText } = useClusterLogout({
clusterUri,
});

async function removeClusterAndClose(): Promise<void> {
const [, err] = await removeCluster();
if (!err) {
onClose();
}
}

return (
<DialogConfirmation
open={true}
Expand All @@ -51,7 +63,7 @@ export function ClusterLogout({
<form
onSubmit={e => {
e.preventDefault();
removeCluster();
removeClusterAndClose();
}}
>
<DialogHeader justifyContent="space-between" mb={0}>
Expand Down
31 changes: 11 additions & 20 deletions web/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
*/

import { useAsync } from 'shared/hooks/useAsync';
import { useEffect } from 'react';

import { RootClusterUri } from 'teleterm/ui/uri';

import { useAppContext } from '../appContextProvider';

export function useClusterLogout({ clusterUri, onClose, clusterTitle }: Props) {
export function useClusterLogout({
clusterUri,
}: {
clusterUri: RootClusterUri;
}) {
const ctx = useAppContext();
const [{ status, statusText }, removeCluster] = useAsync(async () => {
await ctx.clustersService.logout(clusterUri);
Expand All @@ -35,30 +38,18 @@ export function useClusterLogout({ clusterUri, onClose, clusterTitle }: Props) {
await ctx.workspacesService.setActiveWorkspace(null);
}
}
ctx.workspacesService.removeWorkspace(clusterUri);

// remove connections first, they depend both on the cluster and the workspace
ctx.connectionTracker.removeItemsBelongingToRootCluster(clusterUri);
// remove the workspace next, because it depends on the cluster
ctx.workspacesService.removeWorkspace(clusterUri);
// remove the cluster, it does not depend on anything
await ctx.clustersService.removeClusterAndResources(clusterUri);
});

useEffect(() => {
if (status === 'success') {
onClose();
}
}, [status]);

return {
status,
statusText,
removeCluster,
onClose,
clusterUri,
clusterTitle,
};
}

export type Props = {
onClose?(): void;
clusterTitle?: string;
clusterUri: RootClusterUri;
};

export type State = ReturnType<typeof useClusterLogout>;
9 changes: 4 additions & 5 deletions web/packages/teleterm/src/ui/ModalsHost/ModalsHost.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@ const documentsReopenDialog: DialogDocumentsReopen = {
onCancel: () => {},
};

jest.mock('teleterm/ui/ClusterLogout/ClusterLogout', () => {
const MockClusterLogout = () => (
jest.mock('teleterm/ui/ClusterLogout', () => ({
ClusterLogout: () => (
<div
data-testid="mocked-dialog"
data-dialog-kind={clusterLogoutDialog.kind}
></div>
);
return MockClusterLogout;
});
),
}));

jest.mock('teleterm/ui/DocumentsReopen', () => ({
DocumentsReopen: () => (
Expand Down
2 changes: 1 addition & 1 deletion web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ClusterConnect } from 'teleterm/ui/ClusterConnect';
import { DocumentsReopen } from 'teleterm/ui/DocumentsReopen';
import { Dialog } from 'teleterm/ui/services/modals';

import ClusterLogout from '../ClusterLogout/ClusterLogout';
import { ClusterLogout } from '../ClusterLogout';
import { ResourceSearchErrors } from '../Search/ResourceSearchErrors';
import { assertUnreachable } from '../utils';

Expand Down
9 changes: 3 additions & 6 deletions web/packages/teleterm/src/ui/hooks/useLoggedInUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ import { LoggedInUser } from 'teleterm/services/tshd/types';
* It should be used within components that reside outside of WorkspaceContext, typically anything
* that's outside of Document-type components.
*
* It might return undefined if there's no active workspace or during the logout procedure because
* ClustersService state is cleared up before WorkspacesService state.
* It might return undefined if there's no active workspace.
*/
export function useLoggedInUser(): LoggedInUser | undefined {
const { clustersService, workspacesService } = useAppContext();
Expand All @@ -51,10 +50,8 @@ export function useLoggedInUser(): LoggedInUser | undefined {
* rendered inside of them.
*
* In general, the callsite should always assume that this function might return undefined.
* One case where it will for sure return undefined is during the logout process as
* ClustersService state is cleared up before WorkspacesService state. On top of that, each
* workspace is always rendered, even when the cluster is not connected, with at least the default
* document. In that scenario useWorkspaceLoggedInUser could return undefined when used within the
* Each workspace is always rendered, even when the cluster is not connected, with at least the default
* document. In that scenario, useWorkspaceLoggedInUser could return undefined when used within the
* default document.
*/
export function useWorkspaceLoggedInUser(): LoggedInUser | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,7 @@ test('add cluster', async () => {
});

test('remove cluster', async () => {
const { removeCluster } = getClientMocks();
const service = createService({
removeCluster,
});
const service = createService({});

service.setState(draftState => {
draftState.clusters = new Map([
Expand All @@ -135,9 +132,8 @@ test('remove cluster', async () => {
]);
});

await service.removeCluster(clusterUri);
await service.removeClusterAndResources(clusterUri);

expect(removeCluster).toHaveBeenCalledWith(clusterUri);
expect(service.findCluster(clusterUri)).toBeUndefined();
expect(service.findCluster(leafClusterMock.uri)).toBeUndefined();
});
Expand Down Expand Up @@ -192,8 +188,9 @@ test('logout from cluster', async () => {
await service.logout(clusterUri);

expect(logout).toHaveBeenCalledWith(clusterUri);
expect(service.findCluster(clusterUri)).toBeUndefined();
expect(service.findCluster(leafClusterMock.uri)).toBeUndefined();
expect(removeCluster).toHaveBeenCalledWith(clusterUri);
expect(service.findCluster(clusterMock.uri).connected).toBe(false);
expect(service.findCluster(leafClusterMock.uri).connected).toBe(false);
});

test('create a gateway', async () => {
Expand Down
41 changes: 25 additions & 16 deletions web/packages/teleterm/src/ui/services/clusters/clustersService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,27 @@ export class ClustersService extends ImmutableStore<types.ClustersServiceState>
return cluster;
}

/**
* Logs out of the cluster and removes the profile.
* Does not remove the cluster from the state, but sets the cluster and its leafs as disconnected.
* It needs to be done, because some code can operate on the cluster the intermediate period between logout
* and actually removing it from the state.
* A code that operates on that intermediate state is in `useClusterLogout.tsx`.
* After invoking `logout()`, it looks for the next workspace to switch to. If we hadn't marked the cluster as disconnected,
* the method might have returned us the same cluster we wanted to log out of.
*/
async logout(clusterUri: uri.RootClusterUri) {
// TODO(gzdunek): logout and removeCluster should be combined into a single acton in tshd
await this.client.logout(clusterUri);
await this.removeCluster(clusterUri);
await this.removeClusterKubeConfigs(clusterUri);
await this.client.removeCluster(clusterUri);

this.setState(draft => {
draft.clusters.forEach(cluster => {
if (routing.belongsToProfile(clusterUri, cluster.uri)) {
cluster.connected = false;
}
});
});
}

async loginLocal(
Expand Down Expand Up @@ -301,23 +317,16 @@ export class ClustersService extends ImmutableStore<types.ClustersServiceState>
return response;
}

/**
* Removes cluster and its leaf clusters (if any)
*/
async removeCluster(clusterUri: uri.RootClusterUri) {
await this.client.removeCluster(clusterUri);
const leafClustersUris = this.getClusters()
.filter(
item =>
item.leaf && routing.ensureRootClusterUri(item.uri) === clusterUri
)
.map(cluster => cluster.uri);
/** Removes cluster, its leafs and other resources. */
async removeClusterAndResources(clusterUri: uri.RootClusterUri) {
this.setState(draft => {
draft.clusters.delete(clusterUri);
leafClustersUris.forEach(leafClusterUri => {
draft.clusters.delete(leafClusterUri);
draft.clusters.forEach(cluster => {
if (routing.belongsToProfile(clusterUri, cluster.uri)) {
draft.clusters.delete(cluster.uri);
}
});
});
await this.removeClusterKubeConfigs(clusterUri);
}

async getAuthSettings(clusterUri: uri.RootClusterUri) {
Expand Down