Connect: support MOTD (#64310)

* Add `message_of_the_day` to `AuthSettings`, remove unused `has_message_of_the_day`

* Support passing `whiteSpace` to `Text`

* Show MOTD before login

* Add tests and stories

* Remove `minHeight={0}`

* Move `MessageOfTheDay` to `ClusterLogin`

* Minor code style fix
This commit is contained in:
Grzegorz Zdunek
2026-03-12 09:27:35 +00:00
committed by GitHub
parent bccf22368f
commit 90eabf6ee7
14 changed files with 277 additions and 104 deletions
@@ -108,10 +108,6 @@ type AuthSettings struct {
LocalAuthEnabled bool `protobuf:"varint,1,opt,name=local_auth_enabled,json=localAuthEnabled,proto3" json:"local_auth_enabled,omitempty"`
// auth_providers contains a list of auth providers
AuthProviders []*AuthProvider `protobuf:"bytes,4,rep,name=auth_providers,json=authProviders,proto3" json:"auth_providers,omitempty"`
// has_message_of_the_day is a flag indicating that the cluster has MOTD
// banner text that must be retrieved, displayed and acknowledged by
// the user.
HasMessageOfTheDay bool `protobuf:"varint,5,opt,name=has_message_of_the_day,json=hasMessageOfTheDay,proto3" json:"has_message_of_the_day,omitempty"`
// auth_type is the authentication type e.g. "local", "github", "saml", "oidc"
AuthType string `protobuf:"bytes,6,opt,name=auth_type,json=authType,proto3" json:"auth_type,omitempty"`
// allow_passwordless is true if passwordless logins are allowed.
@@ -122,8 +118,10 @@ type AuthSettings struct {
// against the version of the server.
ClientVersionStatus ClientVersionStatus `protobuf:"varint,9,opt,name=client_version_status,json=clientVersionStatus,proto3,enum=teleport.lib.teleterm.v1.ClientVersionStatus" json:"client_version_status,omitempty"`
Versions *Versions `protobuf:"bytes,10,opt,name=versions,proto3" json:"versions,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// message_of_the_day is a message shown before login that the user must acknowledge.
MessageOfTheDay string `protobuf:"bytes,11,opt,name=message_of_the_day,json=messageOfTheDay,proto3" json:"message_of_the_day,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthSettings) Reset() {
@@ -170,13 +168,6 @@ func (x *AuthSettings) GetAuthProviders() []*AuthProvider {
return nil
}
func (x *AuthSettings) GetHasMessageOfTheDay() bool {
if x != nil {
return x.HasMessageOfTheDay
}
return false
}
func (x *AuthSettings) GetAuthType() string {
if x != nil {
return x.AuthType
@@ -212,6 +203,13 @@ func (x *AuthSettings) GetVersions() *Versions {
return nil
}
func (x *AuthSettings) GetMessageOfTheDay() string {
if x != nil {
return x.MessageOfTheDay
}
return ""
}
// AuthProvider describes a way of authentication that is supported by the server. Auth provider is
// referred to as "auth connector" on the backend.
type AuthProvider struct {
@@ -346,17 +344,17 @@ var File_teleport_lib_teleterm_v1_auth_settings_proto protoreflect.FileDescripto
const file_teleport_lib_teleterm_v1_auth_settings_proto_rawDesc = "" +
"\n" +
",teleport/lib/teleterm/v1/auth_settings.proto\x12\x18teleport.lib.teleterm.v1\"\x8a\x04\n" +
",teleport/lib/teleterm/v1/auth_settings.proto\x12\x18teleport.lib.teleterm.v1\"\xa1\x04\n" +
"\fAuthSettings\x12,\n" +
"\x12local_auth_enabled\x18\x01 \x01(\bR\x10localAuthEnabled\x12M\n" +
"\x0eauth_providers\x18\x04 \x03(\v2&.teleport.lib.teleterm.v1.AuthProviderR\rauthProviders\x122\n" +
"\x16has_message_of_the_day\x18\x05 \x01(\bR\x12hasMessageOfTheDay\x12\x1b\n" +
"\x0eauth_providers\x18\x04 \x03(\v2&.teleport.lib.teleterm.v1.AuthProviderR\rauthProviders\x12\x1b\n" +
"\tauth_type\x18\x06 \x01(\tR\bauthType\x12-\n" +
"\x12allow_passwordless\x18\a \x01(\bR\x11allowPasswordless\x120\n" +
"\x14local_connector_name\x18\b \x01(\tR\x12localConnectorName\x12a\n" +
"\x15client_version_status\x18\t \x01(\x0e2-.teleport.lib.teleterm.v1.ClientVersionStatusR\x13clientVersionStatus\x12>\n" +
"\bversions\x18\n" +
" \x01(\v2\".teleport.lib.teleterm.v1.VersionsR\bversionsJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04R\rsecond_factorR\rpreferred_mfa\"Y\n" +
" \x01(\v2\".teleport.lib.teleterm.v1.VersionsR\bversions\x12+\n" +
"\x12message_of_the_day\x18\v \x01(\tR\x0fmessageOfTheDayJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04J\x04\b\x05\x10\x06R\rsecond_factorR\rpreferred_mfaR\x16has_message_of_the_day\"Y\n" +
"\fAuthProvider\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12!\n" +
+15 -17
View File
@@ -48,14 +48,6 @@ export interface AuthSettings {
* @generated from protobuf field: repeated teleport.lib.teleterm.v1.AuthProvider auth_providers = 4;
*/
authProviders: AuthProvider[];
/**
* has_message_of_the_day is a flag indicating that the cluster has MOTD
* banner text that must be retrieved, displayed and acknowledged by
* the user.
*
* @generated from protobuf field: bool has_message_of_the_day = 5;
*/
hasMessageOfTheDay: boolean;
/**
* auth_type is the authentication type e.g. "local", "github", "saml", "oidc"
*
@@ -85,6 +77,12 @@ export interface AuthSettings {
* @generated from protobuf field: teleport.lib.teleterm.v1.Versions versions = 10;
*/
versions?: Versions;
/**
* message_of_the_day is a message shown before login that the user must acknowledge.
*
* @generated from protobuf field: string message_of_the_day = 11;
*/
messageOfTheDay: string;
}
/**
* AuthProvider describes a way of authentication that is supported by the server. Auth provider is
@@ -178,23 +176,23 @@ class AuthSettings$Type extends MessageType<AuthSettings> {
super("teleport.lib.teleterm.v1.AuthSettings", [
{ no: 1, name: "local_auth_enabled", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 4, name: "auth_providers", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => AuthProvider },
{ no: 5, name: "has_message_of_the_day", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 6, name: "auth_type", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 7, name: "allow_passwordless", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 8, name: "local_connector_name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 9, name: "client_version_status", kind: "enum", T: () => ["teleport.lib.teleterm.v1.ClientVersionStatus", ClientVersionStatus, "CLIENT_VERSION_STATUS_"] },
{ no: 10, name: "versions", kind: "message", T: () => Versions }
{ no: 10, name: "versions", kind: "message", T: () => Versions },
{ no: 11, name: "message_of_the_day", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<AuthSettings>): AuthSettings {
const message = globalThis.Object.create((this.messagePrototype!));
message.localAuthEnabled = false;
message.authProviders = [];
message.hasMessageOfTheDay = false;
message.authType = "";
message.allowPasswordless = false;
message.localConnectorName = "";
message.clientVersionStatus = 0;
message.messageOfTheDay = "";
if (value !== undefined)
reflectionMergePartial<AuthSettings>(this, message, value);
return message;
@@ -210,9 +208,6 @@ class AuthSettings$Type extends MessageType<AuthSettings> {
case /* repeated teleport.lib.teleterm.v1.AuthProvider auth_providers */ 4:
message.authProviders.push(AuthProvider.internalBinaryRead(reader, reader.uint32(), options));
break;
case /* bool has_message_of_the_day */ 5:
message.hasMessageOfTheDay = reader.bool();
break;
case /* string auth_type */ 6:
message.authType = reader.string();
break;
@@ -228,6 +223,9 @@ class AuthSettings$Type extends MessageType<AuthSettings> {
case /* teleport.lib.teleterm.v1.Versions versions */ 10:
message.versions = Versions.internalBinaryRead(reader, reader.uint32(), options, message.versions);
break;
case /* string message_of_the_day */ 11:
message.messageOfTheDay = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -246,9 +244,6 @@ class AuthSettings$Type extends MessageType<AuthSettings> {
/* repeated teleport.lib.teleterm.v1.AuthProvider auth_providers = 4; */
for (let i = 0; i < message.authProviders.length; i++)
AuthProvider.internalBinaryWrite(message.authProviders[i], writer.tag(4, WireType.LengthDelimited).fork(), options).join();
/* bool has_message_of_the_day = 5; */
if (message.hasMessageOfTheDay !== false)
writer.tag(5, WireType.Varint).bool(message.hasMessageOfTheDay);
/* string auth_type = 6; */
if (message.authType !== "")
writer.tag(6, WireType.LengthDelimited).string(message.authType);
@@ -264,6 +259,9 @@ class AuthSettings$Type extends MessageType<AuthSettings> {
/* teleport.lib.teleterm.v1.Versions versions = 10; */
if (message.versions)
Versions.internalBinaryWrite(message.versions, writer.tag(10, WireType.LengthDelimited).fork(), options).join();
/* string message_of_the_day = 11; */
if (message.messageOfTheDay !== "")
writer.tag(11, WireType.LengthDelimited).string(message.messageOfTheDay);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -140,6 +140,7 @@ func (s *Handler) GetAuthSettings(ctx context.Context, req *api.GetAuthSettingsR
AuthType: preferences.AuthType,
AllowPasswordless: preferences.AllowPasswordless,
LocalConnectorName: preferences.LocalConnectorName,
MessageOfTheDay: preferences.MOTD,
}
for _, provider := range preferences.Providers {
@@ -35,10 +35,8 @@ message AuthSettings {
// auth_providers contains a list of auth providers
repeated AuthProvider auth_providers = 4;
// has_message_of_the_day is a flag indicating that the cluster has MOTD
// banner text that must be retrieved, displayed and acknowledged by
// the user.
bool has_message_of_the_day = 5;
reserved "has_message_of_the_day";
reserved 5; // has_message_of_the_day
// auth_type is the authentication type e.g. "local", "github", "saml", "oidc"
string auth_type = 6;
// allow_passwordless is true if passwordless logins are allowed.
@@ -49,6 +47,8 @@ message AuthSettings {
// against the version of the server.
ClientVersionStatus client_version_status = 9;
Versions versions = 10;
// message_of_the_day is a message shown before login that the user must acknowledge.
string message_of_the_day = 11;
}
// AuthProvider describes a way of authentication that is supported by the server. Auth provider is
+4
View File
@@ -36,6 +36,8 @@ import {
TextAlignProps,
typography,
TypographyProps,
whiteSpace,
WhiteSpaceProps,
} from 'design/system';
import { shouldForwardTypographyProp } from 'design/system/typography';
import { fontWeights } from 'design/theme/typography';
@@ -51,6 +53,7 @@ export type TextProps<E extends React.ElementType = 'div'> =
SpaceProps &
ColorProps &
TextAlignProps &
WhiteSpaceProps &
FontWeightProps &
FontFamilyProps;
@@ -65,6 +68,7 @@ const Text = styled.div.withConfig({
${space}
${color}
${textAlign}
${whiteSpace}
${fontWeight}
${fontFamily}
`;
+9
View File
@@ -125,6 +125,15 @@ export interface BoxShadowProps<TLength = TLengthStyledSystem> {
boxShadow?: ResponsiveValue<Property.Gap<TLength>>;
}
export const whiteSpace = style({
prop: 'whiteSpace',
cssProperty: 'white-space',
});
export interface WhiteSpaceProps {
whiteSpace?: Property.WhiteSpace;
}
export {
alignItems,
type AlignItemsProps,
@@ -66,7 +66,7 @@ export class MockTshClient implements TshdClient {
new MockedUnaryCall({
localAuthEnabled: true,
authProviders: [],
hasMessageOfTheDay: false,
messageOfTheDay: '',
authType: 'local',
allowPasswordless: false,
localConnectorName: '',
@@ -375,7 +375,7 @@ export const makeAuthSettings = (
): AuthSettings => ({
localAuthEnabled: true,
authProviders: [],
hasMessageOfTheDay: false,
messageOfTheDay: '',
authType: 'local',
allowPasswordless: false,
localConnectorName: '',
@@ -33,6 +33,7 @@ const meta: Meta<StoryProps> = {
args: {
compatibility: 'compatible',
showUpdate: true,
showMessageOfTheDay: false,
},
};
export default meta;
@@ -26,6 +26,7 @@ import { TshdClient } from 'teleterm/services/tshd';
import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient';
import {
makeAuthSettings,
makeDatabaseGateway,
makeRootCluster,
} from 'teleterm/services/tshd/testHelpers';
import { AppUpdaterContextProvider } from 'teleterm/ui/AppUpdater';
@@ -80,7 +81,7 @@ it('shows go to updates button in compatibility warning if there are clusters pr
new MockedUnaryCall({
localAuthEnabled: true,
authProviders: [],
hasMessageOfTheDay: false,
messageOfTheDay: '',
authType: 'local',
allowPasswordless: false,
localConnectorName: '',
@@ -208,3 +209,79 @@ it('shows two separate prompt texts during SSO login', async () => {
resolveSyncClusterPromise();
});
});
it('requires acknowledging MOTD before showing the login form', async () => {
const user = userEvent.setup();
const cluster = makeRootCluster();
const appContext = new MockAppContext();
appContext.addRootCluster(cluster);
jest.spyOn(appContext.tshd, 'getAuthSettings').mockReturnValue(
new MockedUnaryCall(
makeAuthSettings({
messageOfTheDay: 'Authorized use only.',
})
)
);
render(
<MockAppContextProvider appContext={appContext}>
<AppUpdaterContextProvider>
<ClusterLogin
clusterUri={cluster.uri}
onCancel={() => {}}
prefill={{ username: 'alice' }}
reason={undefined}
/>
</AppUpdaterContextProvider>
</MockAppContextProvider>
);
expect(await screen.findByText(/Authorized use only\./)).toBeVisible();
expect(screen.getByRole('button', { name: 'Acknowledge' })).toBeVisible();
expect(screen.queryByLabelText('Password')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Acknowledge' }));
expect(await screen.findByLabelText('Password')).toBeVisible();
expect(screen.queryByText(/Authorized use only\./)).not.toBeInTheDocument();
});
it('does not show relogin reason when MOTD is visible', async () => {
const user = userEvent.setup();
const cluster = makeRootCluster();
const appContext = new MockAppContext();
appContext.addRootCluster(cluster);
jest.spyOn(appContext.tshd, 'getAuthSettings').mockReturnValue(
new MockedUnaryCall(
makeAuthSettings({
messageOfTheDay: 'Authorized use only.',
})
)
);
render(
<MockAppContextProvider appContext={appContext}>
<AppUpdaterContextProvider>
<ClusterLogin
clusterUri={cluster.uri}
onCancel={() => {}}
prefill={{ username: 'alice' }}
reason={{
kind: 'reason.gateway-cert-expired',
gateway: makeDatabaseGateway(),
targetUri: '',
}}
/>
</AppUpdaterContextProvider>
</MockAppContextProvider>
);
expect(await screen.findByText(/Authorized use only\./)).toBeVisible();
expect(screen.queryByText(/You tried to connect to/)).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Acknowledge' }));
expect(
await screen.findByText(/You tried to connect to/)
).toBeInTheDocument();
});
@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react';
import React, { useState } from 'react';
import {
Box,
@@ -25,6 +25,7 @@ import {
Flex,
H2,
Indicator,
P2,
StepSlider,
Text,
} from 'design';
@@ -54,6 +55,58 @@ export function ClusterLogin(props: Props & { reason: ClusterConnectReason }) {
export const ClusterLoginPresentation = (
props: ClusterLoginPresentationProps
) => {
const [motdAcknowledged, setMotdAcknowledged] = useState(false);
const showMotd =
props.initAttempt.status === 'success' &&
!!props.initAttempt.data.messageOfTheDay &&
!motdAcknowledged;
if (
props.initAttempt.status === '' ||
props.initAttempt.status === 'processing'
) {
return (
<>
<LoginHeader cluster={props.title} onClose={props.onCloseDialog} />
<Box px={outermostPadding} textAlign="center">
<Indicator delay="none" />
</Box>
</>
);
}
if (props.initAttempt.status === 'error') {
return (
<>
<LoginHeader cluster={props.title} onClose={props.onCloseDialog} />
<Flex px={outermostPadding} flexDirection="column" gap={3}>
<Alerts.Danger
details={props.initAttempt.statusText}
margin={0}
width="100%"
>
Unable to retrieve cluster auth preferences
</Alerts.Danger>
<ButtonPrimary autoFocus={true} size="large" onClick={props.init}>
Retry
</ButtonPrimary>
</Flex>
</>
);
}
if (showMotd) {
return (
<>
<LoginHeader cluster={props.title} onClose={props.onCloseDialog} />
<MessageOfTheDay
message={props.initAttempt.data.messageOfTheDay}
onAcknowledge={() => setMotdAcknowledged(true)}
/>
</>
);
}
return (
<StepSlider
flows={loginViews}
@@ -63,6 +116,7 @@ export const ClusterLoginPresentation = (
// Instead, the entire dialog should be scrollable.
flex-shrink: 0;
`}
authSettings={props.initAttempt.data}
{...props}
/>
);
@@ -72,10 +126,15 @@ export type ClusterLoginPresentationProps = State & {
reason: ClusterConnectReason;
};
export type ClusterLoginFormProps = Omit<State, 'initAttempt' | 'init'> &
StepComponentProps & {
reason: ClusterConnectReason;
authSettings: AuthSettings;
};
function ClusterLoginForm({
title,
initAttempt,
init,
authSettings,
loginAttempt,
clearLoginAttempt,
onLoginWithLocal,
@@ -98,71 +157,39 @@ function ClusterLoginForm({
quitAndInstallAppUpdate,
downloadAppUpdate,
checkForAppUpdates,
}: ClusterLoginPresentationProps & StepComponentProps) {
}: ClusterLoginFormProps) {
return (
<Flex ref={refCallback} flexDirection="column">
<DialogHeader px={outermostPadding}>
<H2>
Log in to <b>{title}</b>
</H2>
<ButtonIcon ml="auto" p={3} onClick={onCloseDialog} aria-label="Close">
<Icons.Cross size="medium" />
</ButtonIcon>
</DialogHeader>
<LoginHeader cluster={title} onClose={onCloseDialog} />
<DialogContent mb={0} gap={2}>
{reason && (
<Box px={outermostPadding}>
<Reason reason={reason} />
</Box>
)}
{initAttempt.status === 'error' && (
<Flex
px={outermostPadding}
flexDirection="column"
alignItems="flex-start"
gap={3}
>
<Alerts.Danger
details={initAttempt.statusText}
margin={0}
width="100%"
>
Unable to retrieve cluster auth preferences
</Alerts.Danger>
<ButtonPrimary onClick={init}>Retry</ButtonPrimary>
</Flex>
)}
{initAttempt.status === 'processing' && (
<Box px={outermostPadding} textAlign="center">
<Indicator delay="none" />
</Box>
)}
{initAttempt.status === 'success' && (
<LoginForm
authSettings={initAttempt.data}
primaryAuthType={getPrimaryAuthType(initAttempt.data)}
loggedInUserName={loggedInUserName}
onLoginWithSso={onLoginWithSso}
onLoginWithPasswordless={onLoginWithPasswordless}
onLogin={onLoginWithLocal}
onAbort={onAbort}
loginAttempt={loginAttempt}
clearLoginAttempt={clearLoginAttempt}
ssoPrompt={ssoPrompt}
passwordlessLoginState={passwordlessLoginState}
shouldSkipVersionCheck={shouldSkipVersionCheck}
disableVersionCheck={disableVersionCheck}
platform={platform}
checkForAppUpdates={checkForAppUpdates}
changeAppUpdatesManagingCluster={changeAppUpdatesManagingCluster}
appUpdateEvent={appUpdateEvent}
cancelAppUpdateDownload={cancelAppUpdateDownload}
downloadAppUpdate={downloadAppUpdate}
quitAndInstallAppUpdate={quitAndInstallAppUpdate}
switchToAppUpdateDetails={next}
/>
)}
<LoginForm
authSettings={authSettings}
primaryAuthType={getPrimaryAuthType(authSettings)}
loggedInUserName={loggedInUserName}
onLoginWithSso={onLoginWithSso}
onLoginWithPasswordless={onLoginWithPasswordless}
onLogin={onLoginWithLocal}
onAbort={onAbort}
loginAttempt={loginAttempt}
clearLoginAttempt={clearLoginAttempt}
ssoPrompt={ssoPrompt}
passwordlessLoginState={passwordlessLoginState}
shouldSkipVersionCheck={shouldSkipVersionCheck}
disableVersionCheck={disableVersionCheck}
platform={platform}
checkForAppUpdates={checkForAppUpdates}
changeAppUpdatesManagingCluster={changeAppUpdatesManagingCluster}
appUpdateEvent={appUpdateEvent}
cancelAppUpdateDownload={cancelAppUpdateDownload}
downloadAppUpdate={downloadAppUpdate}
quitAndInstallAppUpdate={quitAndInstallAppUpdate}
switchToAppUpdateDetails={next}
/>
</DialogContent>
</Flex>
);
@@ -267,3 +294,37 @@ const getTargetDesc = (reason: ClusterConnectReason): React.ReactNode => {
}
}
};
function LoginHeader(props: { cluster: string; onClose(): void }) {
return (
<DialogHeader px={outermostPadding}>
<H2>
Log in to <b>{props.cluster}</b>
</H2>
<ButtonIcon ml="auto" p={3} onClick={props.onClose} aria-label="Close">
<Icons.Cross size="medium" />
</ButtonIcon>
</DialogHeader>
);
}
function MessageOfTheDay(props: { message: string; onAcknowledge(): void }) {
return (
<>
{/* Make the internal container scrollable, so that the acknowledge button is always visible. */}
<Box mb={3} maxHeight="400px" overflow="auto">
<P2 whiteSpace="pre-wrap" px={outermostPadding}>
{props.message}
</P2>
</Box>
<ButtonPrimary
size="large"
mx={outermostPadding}
autoFocus
onClick={props.onAcknowledge}
>
Acknowledge
</ButtonPrimary>
</>
);
}
@@ -37,6 +37,8 @@ const meta: Meta<StoryProps> = {
argTypes: compatibilityArgType,
args: {
compatibility: 'compatible',
showUpdate: true,
showMessageOfTheDay: false,
},
};
export default meta;
@@ -36,6 +36,7 @@ export const TestContainer: FC<PropsWithChildren> = ({ children }) => (
export interface StoryProps {
compatibility: 'compatible' | 'client-too-old' | 'client-too-new';
showUpdate: boolean;
showMessageOfTheDay: boolean;
}
export const compatibilityArgType: ArgTypes<StoryProps> = {
@@ -48,6 +49,10 @@ export const compatibilityArgType: ArgTypes<StoryProps> = {
type: 'boolean',
description: 'Show app update',
},
showMessageOfTheDay: {
type: 'boolean',
description: 'Show Message Of The Day (MOTD)',
},
};
export function makeProps(
@@ -65,7 +70,24 @@ export function makeProps(
initAttempt: {
status: 'success',
statusText: '',
data: makeAuthSettings(),
data: makeAuthSettings({
messageOfTheDay: storyProps.showMessageOfTheDay
? 'Authorized use only.\nBy continuing you consent to monitoring.\n' +
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod ' +
'tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,' +
'quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' +
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu ' +
'fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in ' +
'culpa qui officia deserunt mollit anim id est laborum.' +
'Proin ultrices leo ac nulla finibus, vitae accumsan mauris molestie. Vivamus ' +
'vitae massa sed odio lobortis blandit a vel neque. Aenean ultrices facilisis erat, ' +
'viverra rutrum ex varius sit amet. Phasellus fermentum facilisis cursus. ' +
'Cras ac turpis tellus. Cras vitae dictum dolor. Duis pretium molestie tortor sagittis ' +
'commodo. Aliquam non urna interdum, dignissim risus sit amet, congue erat. Quisque ' +
'vestibulum augue vitae libero fermentum, sed laoreet justo malesuada. ' +
'Donec quis augue nec lectus commodo commodo'
: '',
}),
},
loggedInUserName: null,
@@ -339,9 +339,9 @@ it('shows a login modal when a request to a cluster from the current workspace f
// Verify that the login modal was shown after typing in the search box.
await waitFor(() => {
expect(screen.getByTestId('Modal')).toBeInTheDocument();
// Wait for the login field to show up in the modal.
expect(screen.getByTestId('Modal')).toHaveTextContent('Username');
});
expect(screen.getByTestId('Modal')).toHaveTextContent('Log in to');
// Verify that the search bar stays open after closing the modal.
await user.click(screen.getByLabelText('Close'));