diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx index f7c2e53087b12..191ed831ab224 100644 --- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx @@ -16,17 +16,16 @@ * along with this program. If not, see . */ -import React, { PropsWithChildren } from 'react'; +import { FC, PropsWithChildren } from 'react'; import { Box } from 'design'; -import { Attempt, makeErrorAttempt } from 'shared/hooks/useAsync'; +import { + Attempt, + makeErrorAttempt, + makeProcessingAttempt, +} from 'shared/hooks/useAsync'; import * as types from 'teleterm/ui/services/clusters/types'; -import { - appUri, - makeDatabaseGateway, - makeKubeGateway, -} from 'teleterm/services/tshd/testHelpers'; import { ClusterLoginPresentation, @@ -67,44 +66,11 @@ function makeProps(): ClusterLoginPresentationProps { onLoginWithPasswordless: () => Promise.resolve<[void, Error]>([null, null]), onLoginWithSso: () => null, clearLoginAttempt: () => null, - webauthnLogin: null, + passwordlessLoginState: null, reason: undefined, }; } -export const Err = () => { - const props = makeProps(); - props.initAttempt = makeErrorAttempt(new Error('some error message')); - - return ( - - - - ); -}; - -export const Processing = () => { - const props = makeProps(); - props.initAttempt.status = 'processing'; - - return ( - - - - ); -}; - -export const LocalDisabled = () => { - const props = makeProps(); - props.initAttempt.data.localAuthEnabled = false; - - return ( - - - - ); -}; - export const LocalOnly = () => { const props = makeProps(); props.initAttempt.data.allowPasswordless = false; @@ -116,14 +82,9 @@ export const LocalOnly = () => { ); }; -export const LocalOnlyWithReasonGatewayCertExpiredWithDbGateway = () => { +export const InitErr = () => { const props = makeProps(); - props.initAttempt.data.allowPasswordless = false; - props.reason = { - kind: 'reason.gateway-cert-expired', - targetUri: dbGateway.targetUri, - gateway: dbGateway, - }; + props.initAttempt = makeErrorAttempt(new Error('some error message')); return ( @@ -132,14 +93,9 @@ export const LocalOnlyWithReasonGatewayCertExpiredWithDbGateway = () => { ); }; -export const LocalOnlyWithReasonGatewayCertExpiredWithKubeGateway = () => { +export const InitProcessing = () => { const props = makeProps(); - props.initAttempt.data.allowPasswordless = false; - props.reason = { - kind: 'reason.gateway-cert-expired', - targetUri: kubeGateway.targetUri, - gateway: kubeGateway, - }; + props.initAttempt.status = 'processing'; return ( @@ -148,14 +104,9 @@ export const LocalOnlyWithReasonGatewayCertExpiredWithKubeGateway = () => { ); }; -export const LocalOnlyWithReasonGatewayCertExpiredWithoutGateway = () => { +export const LocalDisabled = () => { const props = makeProps(); - props.initAttempt.data.allowPasswordless = false; - props.reason = { - kind: 'reason.gateway-cert-expired', - targetUri: dbGateway.targetUri, - gateway: undefined, - }; + props.initAttempt.data.localAuthEnabled = false; return ( @@ -164,14 +115,14 @@ export const LocalOnlyWithReasonGatewayCertExpiredWithoutGateway = () => { ); }; -export const LocalOnlyWithReasonVnetCertExpired = () => { +// The password field is empty in this story as there's no way to change the value of a controlled +// input without touching the internals of React. +// https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-change-or-input-event-in-react-js +export const LocalProcessing = () => { const props = makeProps(); props.initAttempt.data.allowPasswordless = false; - props.reason = { - kind: 'reason.vnet-cert-expired', - targetUri: appUri, - publicAddr: 'tcp-app.teleport.example.com', - }; + props.loginAttempt = makeProcessingAttempt(); + props.loggedInUserName = 'alice'; return ( @@ -277,7 +228,7 @@ export const SsoWithNoProvidersConfigured = () => { export const HardwareTapPrompt = () => { const props = makeProps(); props.loginAttempt.status = 'processing'; - props.webauthnLogin = { + props.passwordlessLoginState = { prompt: 'tap', }; return ( @@ -290,7 +241,7 @@ export const HardwareTapPrompt = () => { export const HardwarePinPrompt = () => { const props = makeProps(); props.loginAttempt.status = 'processing'; - props.webauthnLogin = { + props.passwordlessLoginState = { prompt: 'pin', }; return ( @@ -303,7 +254,7 @@ export const HardwarePinPrompt = () => { export const HardwareRetapPrompt = () => { const props = makeProps(); props.loginAttempt.status = 'processing'; - props.webauthnLogin = { + props.passwordlessLoginState = { prompt: 'retap', }; return ( @@ -316,7 +267,7 @@ export const HardwareRetapPrompt = () => { export const HardwareCredentialPrompt = () => { const props = makeProps(); props.loginAttempt.status = 'processing'; - props.webauthnLogin = { + props.passwordlessLoginState = { prompt: 'credential', loginUsernames: [ 'apple', @@ -338,7 +289,7 @@ export const HardwareCredentialPrompt = () => { export const HardwareCredentialPromptProcessing = () => { const props = makeProps(); props.loginAttempt.status = 'processing'; - props.webauthnLogin = { + props.passwordlessLoginState = { prompt: 'credential', loginUsernames: [ 'apple', @@ -350,7 +301,7 @@ export const HardwareCredentialPromptProcessing = () => { 'strawberry', ], }; - props.webauthnLogin.processing = true; + props.passwordlessLoginState.processing = true; return ( @@ -368,7 +319,7 @@ export const SsoPrompt = () => { ); }; -const TestContainer: React.FC = ({ children }) => ( +const TestContainer: FC = ({ children }) => ( <> Bordered box is not part of the component = ({ children }) => ( ); - -const dbGateway = makeDatabaseGateway({ - uri: '/gateways/gateway1', - targetName: 'postgres', - targetUri: '/clusters/teleport-local/dbs/postgres', - targetUser: 'alice', - targetSubresourceName: '', - localAddress: 'localhost', - localPort: '59116', - protocol: 'postgres', -}); - -const kubeGateway = makeKubeGateway({ - uri: '/gateways/gateway2', - targetName: 'minikube', - targetUri: '/clusters/teleport-local/kubes/minikube', - targetSubresourceName: '', - localAddress: 'localhost', - localPort: '59117', -}); diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx index 5f80e5f5565a9..1909beaf3ebdd 100644 --- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.tsx @@ -53,7 +53,7 @@ export function ClusterLoginPresentation({ onAbort, loggedInUserName, shouldPromptSsoStatus, - webauthnLogin, + passwordlessLoginState, reason, }: ClusterLoginPresentationProps) { return ( @@ -94,7 +94,7 @@ export function ClusterLoginPresentation({ loginAttempt={loginAttempt} clearLoginAttempt={clearLoginAttempt} shouldPromptSsoStatus={shouldPromptSsoStatus} - webauthnLogin={webauthnLogin} + passwordlessLoginState={passwordlessLoginState} /> )} diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLoginReason.story.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLoginReason.story.tsx new file mode 100644 index 0000000000000..e6e53becffc97 --- /dev/null +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLoginReason.story.tsx @@ -0,0 +1,171 @@ +/** + * 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 . + */ + +import { FC, PropsWithChildren } from 'react'; +import { Box } from 'design'; +import { Attempt } from 'shared/hooks/useAsync'; + +import * as types from 'teleterm/ui/services/clusters/types'; +import { + appUri, + makeDatabaseGateway, + makeKubeGateway, +} from 'teleterm/services/tshd/testHelpers'; + +import { + ClusterLoginPresentation, + ClusterLoginPresentationProps, +} from './ClusterLogin'; + +export default { + title: 'Teleterm/ModalsHost/ClusterLogin/Reason', +}; + +export const GatewayCertExpiredWithDbGateway = () => { + const props = makeProps(); + props.initAttempt.data.allowPasswordless = false; + props.reason = { + kind: 'reason.gateway-cert-expired', + targetUri: dbGateway.targetUri, + gateway: dbGateway, + }; + + return ( + + + + ); +}; + +export const GatewayCertExpiredWithKubeGateway = () => { + const props = makeProps(); + props.initAttempt.data.allowPasswordless = false; + props.reason = { + kind: 'reason.gateway-cert-expired', + targetUri: kubeGateway.targetUri, + gateway: kubeGateway, + }; + + return ( + + + + ); +}; + +export const GatewayCertExpiredWithoutGateway = () => { + const props = makeProps(); + props.initAttempt.data.allowPasswordless = false; + props.reason = { + kind: 'reason.gateway-cert-expired', + targetUri: dbGateway.targetUri, + gateway: undefined, + }; + + return ( + + + + ); +}; + +export const VnetCertExpired = () => { + const props = makeProps(); + props.initAttempt.data.allowPasswordless = false; + props.reason = { + kind: 'reason.vnet-cert-expired', + targetUri: appUri, + publicAddr: 'tcp-app.teleport.example.com', + }; + + return ( + + + + ); +}; + +function makeProps(): ClusterLoginPresentationProps { + return { + shouldPromptSsoStatus: false, + title: 'localhost', + loginAttempt: { + status: '', + statusText: '', + } as Attempt, + init: () => null, + initAttempt: { + status: 'success', + statusText: '', + data: { + localAuthEnabled: true, + authProviders: [], + type: '', + hasMessageOfTheDay: false, + allowPasswordless: true, + localConnectorName: '', + authType: 'local', + } as types.AuthSettings, + } as const, + + loggedInUserName: null, + onCloseDialog: () => null, + onAbort: () => null, + onLoginWithLocal: () => Promise.resolve<[void, Error]>([null, null]), + onLoginWithPasswordless: () => Promise.resolve<[void, Error]>([null, null]), + onLoginWithSso: () => null, + clearLoginAttempt: () => null, + passwordlessLoginState: null, + reason: undefined, + }; +} + +const TestContainer: FC = ({ children }) => ( + <> + Bordered box is not part of the component + props.theme.colors.levels.elevated}; + background: ${props => props.theme.colors.levels.surface}; + `} + > + {children} + + +); + +const dbGateway = makeDatabaseGateway({ + uri: '/gateways/gateway1', + targetName: 'postgres', + targetUri: '/clusters/teleport-local/dbs/postgres', + targetUser: 'alice', + targetSubresourceName: '', + localAddress: 'localhost', + localPort: '59116', + protocol: 'postgres', +}); + +const kubeGateway = makeKubeGateway({ + uri: '/gateways/gateway2', + targetName: 'minikube', + targetUri: '/clusters/teleport-local/kubes/minikube', + targetSubresourceName: '', + localAddress: 'localhost', + localPort: '59117', +}); diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx index ef189c5d326d1..4a25bdf50f093 100644 --- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx @@ -24,6 +24,8 @@ import FieldInput from 'shared/components/FieldInput'; import { requiredField } from 'shared/components/Validation/rules'; import { useRefAutoFocus } from 'shared/hooks'; +import { LinearProgress } from 'teleterm/ui/components/LinearProgress'; + import type { Props } from '../FormLogin'; export const FormLocal = ({ @@ -83,6 +85,7 @@ export const FormLocal = ({ width="100%" disabled={isProcessing} /> +