diff --git a/web/packages/teleport/src/Discover/ConnectMyComputer/LegacyTestConnection/LegacyTestConnection.story.tsx b/web/packages/teleport/src/Discover/ConnectMyComputer/LegacyTestConnection/LegacyTestConnection.story.tsx
new file mode 100644
index 0000000000000..e144d4e693f77
--- /dev/null
+++ b/web/packages/teleport/src/Discover/ConnectMyComputer/LegacyTestConnection/LegacyTestConnection.story.tsx
@@ -0,0 +1,201 @@
+/**
+ * Copyright 2023 Gravitational, Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { MemoryRouter } from 'react-router';
+import { initialize, mswLoader } from 'msw-storybook-addon';
+import { rest } from 'msw';
+
+import { nodes } from 'teleport/Nodes/fixtures';
+import { ContextProvider } from 'teleport';
+import cfg from 'teleport/config';
+import { UserContext } from 'teleport/User/UserContext';
+import { createTeleportContext } from 'teleport/mocks/contexts';
+import { makeDefaultUserPreferences } from 'teleport/services/userPreferences/userPreferences';
+
+import { LegacyTestConnection } from './LegacyTestConnection';
+
+export default {
+ title: 'Teleport/Discover/ConnectMyComputer/LegacyTestConnection',
+ loaders: [mswLoader],
+};
+
+initialize();
+
+const node = nodes[0];
+
+const agentStepProps = {
+ prevStep: () => {},
+ nextStep: () => {},
+ agentMeta: { resourceName: node.hostname, node, agentMatcherLabels: [] },
+};
+
+export const SingleLogin = () => {
+ return (
+
+
+
+ );
+};
+
+SingleLogin.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res(ctx.json({}))
+ ),
+ rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
+ res(ctx.json({ logins: ['foo'] }))
+ ),
+ ],
+ },
+};
+
+export const MultipleLogins = () => {
+ return (
+
+
+
+ );
+};
+
+MultipleLogins.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res(ctx.json({}))
+ ),
+ rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
+ res(ctx.json({ logins: ['foo', 'bar', 'baz'] }))
+ ),
+ ],
+ },
+};
+
+export const NoLogins = () => {
+ return (
+
+
+
+ );
+};
+
+NoLogins.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res(ctx.json({}))
+ ),
+ rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
+ res(ctx.json({ logins: [] }))
+ ),
+ ],
+ },
+};
+
+export const NoRole = () => {
+ return (
+
+
+
+ );
+};
+
+NoRole.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res(ctx.json({}))
+ ),
+ rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
+ // TODO Check how our error responses look like.
+ res(ctx.status(404), ctx.text('Whoops no role found'))
+ ),
+ ],
+ },
+};
+
+export const ReloadUserProcessing = () => {
+ return (
+
+
+
+ );
+};
+
+ReloadUserProcessing.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res(ctx.delay('infinite'))
+ ),
+ ],
+ },
+};
+
+export const ReloadUserError = () => {
+ return (
+
+
+
+ );
+};
+
+ReloadUserError.parameters = {
+ msw: {
+ handlers: [
+ // The first handler returns an error immediately. Subsequent requests return after a delay so
+ // that we can show a spinner after clicking on "Retry".
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res.once(
+ ctx.status(500),
+ ctx.json({ message: 'Could not renew session' })
+ )
+ ),
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res(
+ ctx.delay(1000),
+ ctx.status(500),
+ ctx.json({ message: 'Could not renew session' })
+ )
+ ),
+ ],
+ },
+};
+
+const Provider = ({ children }) => {
+ const ctx = createTeleportContext();
+
+ const preferences = makeDefaultUserPreferences();
+ const updatePreferences = () => Promise.resolve();
+ const getClusterPinnedResources = () => Promise.resolve([]);
+ const updateClusterPinnedResources = () => Promise.resolve();
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/web/packages/teleport/src/Discover/ConnectMyComputer/LegacyTestConnection/LegacyTestConnection.tsx b/web/packages/teleport/src/Discover/ConnectMyComputer/LegacyTestConnection/LegacyTestConnection.tsx
new file mode 100644
index 0000000000000..9d27ee5368183
--- /dev/null
+++ b/web/packages/teleport/src/Discover/ConnectMyComputer/LegacyTestConnection/LegacyTestConnection.tsx
@@ -0,0 +1,235 @@
+/**
+ * Copyright 2023 Gravitational, Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useEffect, useCallback, useRef } from 'react';
+
+import {
+ ButtonPrimary,
+ ButtonSecondary,
+ Flex,
+ Indicator,
+ Text,
+ Link,
+} from 'design';
+import * as Icons from 'design/Icon';
+import { useAsync } from 'shared/hooks/useAsync';
+import { MenuLogin } from 'shared/components/MenuLogin';
+import * as connectMyComputer from 'shared/connectMyComputer';
+
+import cfg from 'teleport/config';
+import useTeleport from 'teleport/useTeleport';
+import {
+ ActionButtons,
+ StyledBox,
+ Header,
+ TextIcon,
+} from 'teleport/Discover/Shared';
+import { openNewTab } from 'teleport/lib/util';
+import { Node, sortNodeLogins } from 'teleport/services/nodes';
+import { ApiError } from 'teleport/services/api/parseError';
+
+import { NodeMeta } from '../../useDiscover';
+
+import type { AgentStepProps } from '../../types';
+
+export const LegacyTestConnection = (props: AgentStepProps) => {
+ const { userService, storeUser } = useTeleport();
+ const meta = props.agentMeta as NodeMeta;
+
+ const abortController = useRef();
+ // When the user sets up Connect My Computer in Teleport Connect, a new role gets added to the
+ // user. Because of that, we need to reload the current session so that the user is able to
+ // connect to the new node, without having to log in to the cluster again.
+ //
+ // We also need to fetch the list of logins from that role. The user might have many logins
+ // available, but the Connect My Computer agent is always started by the system user that is
+ // running Connect. As such, the Connect My Computer role should include that valid login.
+ const [fetchLoginsAttempt, fetchLogins] = useAsync(
+ useCallback(
+ async (signal: AbortSignal) => {
+ await userService.reloadUser(signal);
+
+ return await userService.fetchConnectMyComputerLogins(signal);
+ },
+ [userService]
+ )
+ );
+
+ useEffect(() => {
+ abortController.current = new AbortController();
+
+ if (fetchLoginsAttempt.status === '') {
+ fetchLogins(abortController.current.signal);
+ }
+
+ return () => {
+ abortController.current.abort();
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+
+ Step 1: Connect to Your Computer
+
+ Optionally verify that you can connect to “
+ {meta.resourceName}
+ ” by starting a session.
+
+ {(fetchLoginsAttempt.status === '' ||
+ fetchLoginsAttempt.status === 'processing') && }
+
+ {fetchLoginsAttempt.status === 'error' &&
+ (fetchLoginsAttempt.error instanceof ApiError &&
+ fetchLoginsAttempt.error.response.status === 404 ? (
+ window.location.reload()}
+ >
+ <>
+ For Connect My Computer to work, the role{' '}
+ {connectMyComputer.getRoleNameForUser(storeUser.getUsername())}{' '}
+ must be assigned to you. Refresh this page to repeat the process
+ of enrolling a new resource and then{' '}
+
+ restart the Connect My Computer setup
+ {' '}
+ in Teleport Connect.
+ >
+
+ ) : (
+ fetchLogins(abortController.current.signal)}
+ >
+ <>Encountered Error: {fetchLoginsAttempt.statusText}>
+
+ ))}
+
+ {fetchLoginsAttempt.status === 'success' &&
+ (fetchLoginsAttempt.data.length > 0 ? (
+
+ ) : (
+ window.location.reload()}
+ >
+ <>
+ The role{' '}
+ {connectMyComputer.getRoleNameForUser(storeUser.getUsername())}{' '}
+ does not contain any logins. It has likely been manually edited.
+ Refresh this page to repeat the process of enrolling a new
+ resource and then{' '}
+
+ restart the Connect My Computer setup
+ {' '}
+ in Teleport Connect.
+ >
+
+ ))}
+
+
+
+
+ );
+};
+
+const ErrorWithinStep = (props: {
+ buttonText: string;
+ buttonOnClick: () => void;
+ children: React.ReactNode;
+}) => (
+ <>
+
+
+ {props.children}
+
+
+
+ {props.buttonText}
+
+ >
+);
+
+const ConnectButton = ({ logins, node }: { logins: string[]; node: Node }) => {
+ if (logins.length === 1) {
+ return (
+
+ Connect
+
+ );
+ }
+
+ return (
+ {
+ return sortNodeLogins(logins).map(login => ({
+ login,
+ url: cfg.getSshConnectRoute({
+ clusterId: node.clusterId,
+ serverId: node.id,
+ login,
+ }),
+ }));
+ }}
+ onSelect={(event, login) => {
+ event.preventDefault();
+ openNewTab(
+ cfg.getSshConnectRoute({
+ clusterId: node.clusterId,
+ serverId: node.id,
+ login,
+ })
+ );
+ }}
+ transformOrigin={{
+ vertical: 'top',
+ horizontal: 'right',
+ }}
+ anchorOrigin={{
+ vertical: 'center',
+ horizontal: 'right',
+ }}
+ />
+ );
+};
diff --git a/web/packages/teleport/src/Discover/ConnectMyComputer/LegacyTestConnection/index.ts b/web/packages/teleport/src/Discover/ConnectMyComputer/LegacyTestConnection/index.ts
new file mode 100644
index 0000000000000..351a32d4623de
--- /dev/null
+++ b/web/packages/teleport/src/Discover/ConnectMyComputer/LegacyTestConnection/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Copyright 2023 Gravitational, Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { LegacyTestConnection } from './LegacyTestConnection';
diff --git a/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.story.tsx b/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.story.tsx
index a434554a9bc9e..4f9eb6c910e50 100644
--- a/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.story.tsx
+++ b/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.story.tsx
@@ -1,11 +1,11 @@
/**
- * Copyright 2023 Gravitational, Inc
+ * Copyright 2022 Gravitational, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,48 +19,86 @@ import { MemoryRouter } from 'react-router';
import { initialize, mswLoader } from 'msw-storybook-addon';
import { rest } from 'msw';
-import { nodes } from 'teleport/Nodes/fixtures';
import { ContextProvider } from 'teleport';
import cfg from 'teleport/config';
-import { UserContext } from 'teleport/User/UserContext';
+import { nodes } from 'teleport/Nodes/fixtures';
import { createTeleportContext } from 'teleport/mocks/contexts';
-import { makeDefaultUserPreferences } from 'teleport/services/userPreferences/userPreferences';
+import {
+ DiscoverProvider,
+ DiscoverContextState,
+} from 'teleport/Discover/useDiscover';
import { TestConnection } from './TestConnection';
export default {
title: 'Teleport/Discover/ConnectMyComputer/TestConnection',
loaders: [mswLoader],
+ parameters: {
+ msw: {
+ // All handlers within the story must be specified as keys in order to use Storybook's
+ // parameter inheritance to share handlers between stories.
+ //
+ // https://github.com/mswjs/msw-storybook-addon/tree/v1.10.0#composing-request-handlers
+ // https://storybook.js.org/docs/6.5/writing-stories/parameters#rules-of-parameter-inheritance
+ handlers: {
+ renewToken: rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res(ctx.json({}))
+ ),
+ mfaRequired: [
+ rest.post(cfg.getMfaRequiredUrl(), (req, res, ctx) =>
+ res(ctx.json({ required: false }))
+ ),
+ ],
+ connectionDiagnostic: [
+ rest.post(cfg.getConnectionDiagnosticUrl(), (req, res, ctx) =>
+ res(
+ ctx.json({
+ id: '1234',
+ success: true,
+ traces: [
+ {
+ traceType: 'rbac node',
+ status: 'success',
+ details: 'Everything is a-okay.',
+ },
+ ],
+ })
+ )
+ ),
+ ],
+ },
+ },
+ },
};
initialize();
-const node = nodes[0];
-
+const node = { ...nodes[0] };
+node.sshLogins = [
+ ...node.sshLogins,
+ 'george_washington_really_long_name_testing',
+];
const agentStepProps = {
prevStep: () => {},
nextStep: () => {},
agentMeta: { resourceName: node.hostname, node, agentMatcherLabels: [] },
};
-export const SingleLogin = () => {
- return (
-
-
-
- );
-};
+export const SingleLogin = () => (
+
+
+
+);
SingleLogin.parameters = {
msw: {
- handlers: [
- rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
- res(ctx.json({}))
- ),
- rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
- res(ctx.json({ logins: ['foo'] }))
- ),
- ],
+ handlers: {
+ connectMyComputerLogins: [
+ rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
+ res(ctx.json({ logins: ['foo'] }))
+ ),
+ ],
+ },
},
};
@@ -74,14 +112,22 @@ export const MultipleLogins = () => {
MultipleLogins.parameters = {
msw: {
- handlers: [
- rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
- res(ctx.json({}))
- ),
- rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
- res(ctx.json({ logins: ['foo', 'bar', 'baz'] }))
- ),
- ],
+ handlers: {
+ connectMyComputerLogins: [
+ rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
+ res(
+ ctx.json({
+ logins: [
+ 'foo',
+ 'bar',
+ 'baz',
+ 'czesława_maria_de_domo_cieślak_primo_voto_gospodarek_secundo_voto_kowalczyk',
+ ],
+ })
+ )
+ ),
+ ],
+ },
},
};
@@ -95,14 +141,13 @@ export const NoLogins = () => {
NoLogins.parameters = {
msw: {
- handlers: [
- rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
- res(ctx.json({}))
- ),
- rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
- res(ctx.json({ logins: [] }))
- ),
- ],
+ handlers: {
+ connectMyComputerLogins: [
+ rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
+ res(ctx.json({ logins: [] }))
+ ),
+ ],
+ },
},
};
@@ -116,15 +161,16 @@ export const NoRole = () => {
NoRole.parameters = {
msw: {
- handlers: [
- rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
- res(ctx.json({}))
- ),
- rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
- // TODO Check how our error responses look like.
- res(ctx.status(404), ctx.text('Whoops no role found'))
- ),
- ],
+ handlers: {
+ connectMyComputerLogins: [
+ rest.get(cfg.api.connectMyComputerLoginsPath, (req, res, ctx) =>
+ res(
+ ctx.status(404),
+ ctx.json({ error: { message: 'No role found' } })
+ )
+ ),
+ ],
+ },
},
};
@@ -138,11 +184,13 @@ export const ReloadUserProcessing = () => {
ReloadUserProcessing.parameters = {
msw: {
- handlers: [
- rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
- res(ctx.delay('infinite'))
- ),
- ],
+ handlers: {
+ renewToken: [
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res(ctx.delay('infinite'))
+ ),
+ ],
+ },
},
};
@@ -156,46 +204,54 @@ export const ReloadUserError = () => {
ReloadUserError.parameters = {
msw: {
- handlers: [
+ handlers: {
// The first handler returns an error immediately. Subsequent requests return after a delay so
// that we can show a spinner after clicking on "Retry".
- rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
- res.once(
- ctx.status(500),
- ctx.json({ message: 'Could not renew session' })
- )
- ),
- rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
- res(
- ctx.delay(1000),
- ctx.status(500),
- ctx.json({ message: 'Could not renew session' })
- )
- ),
- ],
+ renewToken: [
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res.once(
+ ctx.status(500),
+ ctx.json({ message: 'Could not renew session' })
+ )
+ ),
+ rest.post(cfg.api.webRenewTokenPath, (req, res, ctx) =>
+ res(
+ ctx.delay(1000),
+ ctx.status(500),
+ ctx.json({ error: { message: 'Could not renew session' } })
+ )
+ ),
+ ],
+ },
},
};
const Provider = ({ children }) => {
const ctx = createTeleportContext();
-
- const preferences = makeDefaultUserPreferences();
- const updatePreferences = () => Promise.resolve();
- const getClusterPinnedResources = () => Promise.resolve([]);
- const updateClusterPinnedResources = () => Promise.resolve();
+ const discoverCtx: DiscoverContextState = {
+ ...agentStepProps,
+ currentStep: 0,
+ onSelectResource: () => null,
+ resourceSpec: undefined,
+ exitFlow: () => null,
+ viewConfig: null,
+ indexedViews: [],
+ setResourceSpec: () => null,
+ updateAgentMeta: () => null,
+ emitErrorEvent: () => null,
+ emitEvent: () => null,
+ eventState: null,
+ };
return (
-
-
- {children}
-
+
+
+ {children}
+
);
};
diff --git a/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx
index ba14c03b6ea77..8fd761b437ab7 100644
--- a/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx
+++ b/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx
@@ -1,11 +1,11 @@
/**
- * Copyright 2023 Gravitational, Inc
+ * Copyright 2022 Gravitational, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,40 +14,56 @@
* limitations under the License.
*/
-import React, { useEffect, useCallback, useRef } from 'react';
-
+import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
ButtonPrimary,
ButtonSecondary,
- Flex,
- Indicator,
Text,
+ Box,
+ LabelInput,
+ Indicator,
Link,
} from 'design';
-import * as Icons from 'design/Icon';
+import Select from 'shared/components/Select';
import { useAsync } from 'shared/hooks/useAsync';
-import { MenuLogin } from 'shared/components/MenuLogin';
+import * as Icons from 'design/Icon';
import * as connectMyComputer from 'shared/connectMyComputer';
import cfg from 'teleport/config';
import useTeleport from 'teleport/useTeleport';
+import ReAuthenticate from 'teleport/components/ReAuthenticate';
+import { openNewTab } from 'teleport/lib/util';
import {
+ useConnectionDiagnostic,
+ Header,
ActionButtons,
+ HeaderSubtitle,
+ ConnectionDiagnosticResult,
StyledBox,
- Header,
TextIcon,
} from 'teleport/Discover/Shared';
-import { openNewTab } from 'teleport/lib/util';
-import { Node, sortNodeLogins } from 'teleport/services/nodes';
+import { sortNodeLogins } from 'teleport/services/nodes';
import { ApiError } from 'teleport/services/api/parseError';
import { NodeMeta } from '../../useDiscover';
+import type { Option } from 'shared/components/Select';
import type { AgentStepProps } from '../../types';
+import type { MfaAuthnResponse } from 'teleport/services/mfa';
-export const TestConnection = (props: AgentStepProps) => {
+export function TestConnection(props: AgentStepProps) {
const { userService, storeUser } = useTeleport();
- const meta = props.agentMeta as NodeMeta;
+ const {
+ runConnectionDiagnostic,
+ attempt: connectionDiagAttempt,
+ diagnosis,
+ nextStep,
+ canTestConnection,
+ showMfaDialog,
+ cancelMfaDialog,
+ } = useConnectionDiagnostic();
+ const node = (props.agentMeta as NodeMeta).node;
+ const [selectedLoginOpt, setSelectedLoginOpt] = useState