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}
/>
+
.
*/
-import React from 'react';
import styled from 'styled-components';
import { Flex, ButtonText, Box } from 'design';
import * as Alerts from 'design/Alert';
@@ -25,13 +24,13 @@ import { Attempt } from 'shared/hooks/useAsync';
import * as types from 'teleterm/ui/services/clusters/types';
-import { PromptWebauthn } from './PromptWebauthn';
+import { PromptPasswordless } from './PromptPasswordless';
import PromptSsoStatus from './PromptSsoStatus';
import { FormPasswordless } from './FormPasswordless';
import { FormSso } from './FormSso';
import { FormLocal } from './FormLocal';
-import type { WebauthnLogin } from '../useClusterLogin';
+import type { PasswordlessLoginState } from '../useClusterLogin';
import type { PrimaryAuthType } from 'shared/services';
import type { StepComponentProps } from 'design/StepSlider';
@@ -42,11 +41,13 @@ export default function LoginForm(props: Props) {
authProviders,
localAuthEnabled = true,
shouldPromptSsoStatus,
- webauthnLogin,
+ passwordlessLoginState,
} = props;
- if (webauthnLogin) {
- return ;
+ if (passwordlessLoginState) {
+ return (
+
+ );
}
if (shouldPromptSsoStatus) {
@@ -260,7 +261,7 @@ type LoginAttempt = Attempt;
export type Props = types.AuthSettings & {
shouldPromptSsoStatus: boolean;
- webauthnLogin: WebauthnLogin;
+ passwordlessLoginState: PasswordlessLoginState;
loginAttempt: LoginAttempt;
clearLoginAttempt(): void;
primaryAuthType: PrimaryAuthType;
diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/PromptWebauthn.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/PromptPasswordless.tsx
similarity index 95%
rename from web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/PromptWebauthn.tsx
rename to web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/PromptPasswordless.tsx
index 48bd5734d457c..83f3128172130 100644
--- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/PromptWebauthn.tsx
+++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/PromptPasswordless.tsx
@@ -23,13 +23,17 @@ import Validation from 'shared/components/Validation';
import styled from 'styled-components';
-import LinearProgress from 'teleterm/ui/components/LinearProgress';
+import { LinearProgress } from 'teleterm/ui/components/LinearProgress';
import svgHardwareKey from './hardware.svg';
-import type { WebauthnLogin } from '../../useClusterLogin';
+import type { PasswordlessLoginState } from '../../useClusterLogin';
-export function PromptWebauthn(props: Props) {
+type Props = PasswordlessLoginState & {
+ onCancel(): void;
+};
+
+export function PromptPasswordless(props: Props) {
const { prompt } = props;
return (
@@ -195,7 +199,7 @@ const requiredLength = value => () => {
if (!value || value.length < 4) {
return {
valid: false,
- message: 'pin must be at least 4 characters',
+ message: 'PIN must be at least 4 characters',
};
}
@@ -203,7 +207,3 @@ const requiredLength = value => () => {
valid: true,
};
};
-
-export type Props = WebauthnLogin & {
- onCancel(): void;
-};
diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/hardware.svg b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/hardware.svg
similarity index 100%
rename from web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/hardware.svg
rename to web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/hardware.svg
diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/index.ts b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/index.ts
similarity index 92%
rename from web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/index.ts
rename to web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/index.ts
index f6cf4a63fc775..b95ea30cc3507 100644
--- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/index.ts
+++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/index.ts
@@ -16,4 +16,4 @@
* along with this program. If not, see .
*/
-export { PromptWebauthn } from './PromptWebauthn';
+export { PromptPasswordless } from './PromptPasswordless';
diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptSsoStatus/PromptSsoStatus.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptSsoStatus/PromptSsoStatus.tsx
index 83b25f9869915..419dbb533f874 100644
--- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptSsoStatus/PromptSsoStatus.tsx
+++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/PromptSsoStatus/PromptSsoStatus.tsx
@@ -18,7 +18,7 @@
import { Box, ButtonSecondary, Text, Flex } from 'design';
-import LinearProgress from 'teleterm/ui/components/LinearProgress';
+import { LinearProgress } from 'teleterm/ui/components/LinearProgress';
export default function PromptSsoStatus(props: Props) {
return (
diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts
index 957d9d68e6966..fd500a191edb4 100644
--- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts
+++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts
@@ -34,11 +34,12 @@ export default function useClusterLogin(props: Props) {
const loggedInUserName =
props.prefill.username || cluster.loggedInUser?.name || null;
const [shouldPromptSsoStatus, promptSsoStatus] = useState(false);
- const [webauthnLogin, setWebauthnLogin] = useState();
+ const [passwordlessLoginState, setPasswordlessLoginState] =
+ useState();
- const [initAttempt, init] = useAsync(async () => {
- return await clustersService.getAuthSettings(clusterUri);
- });
+ const [initAttempt, init] = useAsync(() =>
+ clustersService.getAuthSettings(clusterUri)
+ );
const [loginAttempt, login, setAttempt] = useAsync(
(params: types.LoginParams) => {
@@ -66,7 +67,6 @@ export default function useClusterLogin(props: Props) {
);
const onLoginWithLocal = (username: string, password: string) => {
- setWebauthnLogin({ prompt: 'tap' });
login({
kind: 'local',
clusterUri,
@@ -79,16 +79,16 @@ export default function useClusterLogin(props: Props) {
login({
kind: 'passwordless',
clusterUri,
- onPromptCallback: (prompt: types.WebauthnLoginPrompt) => {
- const newLogin: WebauthnLogin = {
+ onPromptCallback: (prompt: types.PasswordlessLoginPrompt) => {
+ const newState: PasswordlessLoginState = {
prompt: prompt.type,
processing: false,
};
if (prompt.type === 'pin') {
- newLogin.onUserResponse = (pin: string) => {
- setWebauthnLogin({
- ...newLogin,
+ newState.onUserResponse = (pin: string) => {
+ setPasswordlessLoginState({
+ ...newState,
// prevent user from clicking on submit buttons more than once
processing: true,
});
@@ -97,12 +97,12 @@ export default function useClusterLogin(props: Props) {
}
if (prompt.type === 'credential') {
- newLogin.loginUsernames = prompt.data.credentials.map(
+ newState.loginUsernames = prompt.data.credentials.map(
c => c.username
);
- newLogin.onUserResponse = (index: number) => {
- setWebauthnLogin({
- ...newLogin,
+ newState.onUserResponse = (index: number) => {
+ setPasswordlessLoginState({
+ ...newState,
// prevent user from clicking on multiple usernames
processing: true,
});
@@ -110,7 +110,7 @@ export default function useClusterLogin(props: Props) {
};
}
- setWebauthnLogin(newLogin);
+ setPasswordlessLoginState(newState);
},
});
};
@@ -146,7 +146,7 @@ export default function useClusterLogin(props: Props) {
useEffect(() => {
if (loginAttempt.status !== 'processing') {
- setWebauthnLogin(null);
+ setPasswordlessLoginState(null);
promptSsoStatus(false);
}
@@ -157,7 +157,7 @@ export default function useClusterLogin(props: Props) {
return {
shouldPromptSsoStatus,
- webauthnLogin,
+ passwordlessLoginState,
title: cluster?.name,
loggedInUserName,
onLoginWithLocal,
@@ -181,9 +181,11 @@ export type Props = {
prefill: { username: string };
};
-export type WebauthnLogin = {
- prompt: types.WebauthnLoginPrompt['type'];
- // The below fields are only ever used for passwordless login flow.
+export type PasswordlessLoginState = {
+ /**
+ * prompt describes the current step, or prompt, shown to the user during the passwordless login.
+ */
+ prompt: types.PasswordlessLoginPrompt['type'];
processing?: boolean;
loginUsernames?: string[];
onUserResponse?(val: number | string): void;
diff --git a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx
index 08e74016cce95..6e7b0fbeb74e7 100644
--- a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx
+++ b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx
@@ -37,8 +37,8 @@ import * as Icons from 'design/Icon';
import { P, P3 } from 'design/Text/Text';
-import LinearProgress from 'teleterm/ui/components/LinearProgress';
-import svgHardwareKey from 'teleterm/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/hardware.svg';
+import { LinearProgress } from 'teleterm/ui/components/LinearProgress';
+import svgHardwareKey from 'teleterm/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/hardware.svg';
import type * as tsh from 'teleterm/services/tshd/types';
diff --git a/web/packages/teleterm/src/ui/ModalsHost/modals/HardwareKeys/Touch.tsx b/web/packages/teleterm/src/ui/ModalsHost/modals/HardwareKeys/Touch.tsx
index a25e1f42dc5fd..f3a2646402784 100644
--- a/web/packages/teleterm/src/ui/ModalsHost/modals/HardwareKeys/Touch.tsx
+++ b/web/packages/teleterm/src/ui/ModalsHost/modals/HardwareKeys/Touch.tsx
@@ -21,8 +21,8 @@ import { Flex, Image, P2 } from 'design';
import { PromptHardwareKeyTouchRequest } from 'gen-proto-ts/teleport/lib/teleterm/v1/tshd_events_service_pb';
-import svgHardwareKey from 'teleterm/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/hardware.svg';
-import LinearProgress from 'teleterm/ui/components/LinearProgress';
+import svgHardwareKey from 'teleterm/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/hardware.svg';
+import { LinearProgress } from 'teleterm/ui/components/LinearProgress';
import { CommonHeader } from './CommonHeader';
diff --git a/web/packages/teleterm/src/ui/ModalsHost/modals/ReAuthenticate/ReAuthenticate.tsx b/web/packages/teleterm/src/ui/ModalsHost/modals/ReAuthenticate/ReAuthenticate.tsx
index 9343313eeeeda..aa8386031fed7 100644
--- a/web/packages/teleterm/src/ui/ModalsHost/modals/ReAuthenticate/ReAuthenticate.tsx
+++ b/web/packages/teleterm/src/ui/ModalsHost/modals/ReAuthenticate/ReAuthenticate.tsx
@@ -46,8 +46,8 @@ import { Option } from 'shared/components/Select';
import { assertUnreachable } from 'shared/utils/assertUnreachable';
import { useAppContext } from 'teleterm/ui/appContextProvider';
-import LinearProgress from 'teleterm/ui/components/LinearProgress';
-import svgHardwareKey from 'teleterm/ui/ClusterConnect/ClusterLogin/FormLogin/PromptWebauthn/hardware.svg';
+import { LinearProgress } from 'teleterm/ui/components/LinearProgress';
+import svgHardwareKey from 'teleterm/ui/ClusterConnect/ClusterLogin/FormLogin/PromptPasswordless/hardware.svg';
import { useLogger } from 'teleterm/ui/hooks/useLogger';
import { routing } from 'teleterm/ui/uri';
diff --git a/web/packages/teleterm/src/ui/Search/pickers/ResultList.tsx b/web/packages/teleterm/src/ui/Search/pickers/ResultList.tsx
index 8618b67d5e33e..ff9137f36ba5e 100644
--- a/web/packages/teleterm/src/ui/Search/pickers/ResultList.tsx
+++ b/web/packages/teleterm/src/ui/Search/pickers/ResultList.tsx
@@ -28,7 +28,7 @@ import { IconProps } from 'design/Icon/Icon';
import styled, { css } from 'styled-components';
import { Attempt } from 'shared/hooks/useAsync';
-import LinearProgress from 'teleterm/ui/components/LinearProgress';
+import { LinearProgress } from 'teleterm/ui/components/LinearProgress';
import { AddWindowEventListener } from '../SearchContext';
diff --git a/web/packages/teleterm/src/ui/Tabs/TabItem.tsx b/web/packages/teleterm/src/ui/Tabs/TabItem.tsx
index 037c94e12e0a3..9d397c2d5bb4b 100644
--- a/web/packages/teleterm/src/ui/Tabs/TabItem.tsx
+++ b/web/packages/teleterm/src/ui/Tabs/TabItem.tsx
@@ -21,7 +21,7 @@ import styled from 'styled-components';
import * as Icons from 'design/Icon';
import { ButtonIcon, Text } from 'design';
-import LinearProgress from 'teleterm/ui/components/LinearProgress';
+import { LinearProgress } from 'teleterm/ui/components/LinearProgress';
import { useTabDnD } from './useTabDnD';
diff --git a/web/packages/teleterm/src/ui/components/LinearProgress/LinearProgress.tsx b/web/packages/teleterm/src/ui/components/LinearProgress/LinearProgress.tsx
index a0755edaaae74..00bf10aa1a0d7 100644
--- a/web/packages/teleterm/src/ui/components/LinearProgress/LinearProgress.tsx
+++ b/web/packages/teleterm/src/ui/components/LinearProgress/LinearProgress.tsx
@@ -16,38 +16,42 @@
* along with this program. If not, see .
*/
-import React from 'react';
import styled from 'styled-components';
-interface LinearProgressProps {
- transparentBackground?: boolean;
-}
-
-const LinearProgress = (props: LinearProgressProps) => {
+export const LinearProgress = ({
+ transparentBackground = false,
+ absolute = true,
+ hidden = false,
+}) => {
return (
-
+
);
};
-const StyledProgress = styled.div<{ transparentBackground?: boolean }>`
+const Wrapper = styled.div<{ $absolute: boolean; $hidden: boolean }>`
+ ${props =>
+ props.$absolute &&
+ `
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ `}
+ ${props => props.$hidden && `visibility: hidden;`}
+`;
+
+const StyledProgress = styled.div<{ $transparentBackground: boolean }>`
position: relative;
overflow: hidden;
display: block;
height: 1px;
z-index: 0;
background-color: ${props =>
- props.transparentBackground
+ props.$transparentBackground
? 'transparent'
: props.theme.colors.levels.surface};
@@ -80,5 +84,3 @@ const StyledProgress = styled.div<{ transparentBackground?: boolean }>`
}
}
`;
-
-export default LinearProgress;
diff --git a/web/packages/teleterm/src/ui/components/LinearProgress/index.ts b/web/packages/teleterm/src/ui/components/LinearProgress/index.ts
index f126c653db3d1..2a1d34f2abb2e 100644
--- a/web/packages/teleterm/src/ui/components/LinearProgress/index.ts
+++ b/web/packages/teleterm/src/ui/components/LinearProgress/index.ts
@@ -16,5 +16,4 @@
* along with this program. If not, see .
*/
-import LinearProgress from './LinearProgress';
-export default LinearProgress;
+export { LinearProgress } from './LinearProgress';
diff --git a/web/packages/teleterm/src/ui/services/clusters/types.ts b/web/packages/teleterm/src/ui/services/clusters/types.ts
index 4329c4b942cac..7f65015cd2f7d 100644
--- a/web/packages/teleterm/src/ui/services/clusters/types.ts
+++ b/web/packages/teleterm/src/ui/services/clusters/types.ts
@@ -44,7 +44,7 @@ export interface LoginSsoParams {
export interface LoginPasswordlessParams {
kind: 'passwordless';
clusterUri: uri.RootClusterUri;
- onPromptCallback(res: WebauthnLoginPrompt): void;
+ onPromptCallback(res: PasswordlessLoginPrompt): void;
}
export type LoginParams =
@@ -54,22 +54,15 @@ export type LoginParams =
export type LoginPasswordlessRequest = tsh.LoginPasswordlessRequest;
-export type WebauthnLoginPrompt =
- | WebauthnLoginTapPrompt
- | WebauthnLoginRetapPrompt
- | WebauthnLoginPinPrompt
- | WebauthnLoginCredentialPrompt;
-export type WebauthnLoginTapPrompt = { type: 'tap' };
-export type WebauthnLoginRetapPrompt = { type: 'retap' };
-export type WebauthnLoginPinPrompt = {
- type: 'pin';
- onUserResponse(pin: string): void;
-};
-export type WebauthnLoginCredentialPrompt = {
- type: 'credential';
- data: { credentials: tsh.CredentialInfo[] };
- onUserResponse(index: number): void;
-};
+export type PasswordlessLoginPrompt =
+ | { type: 'tap' }
+ | { type: 'retap' }
+ | { type: 'pin'; onUserResponse(pin: string): void }
+ | {
+ type: 'credential';
+ data: { credentials: tsh.CredentialInfo[] };
+ onUserResponse(index: number): void;
+ };
export interface AuthSettings extends tsh.AuthSettings {
authType: AuthType;