mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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}
|
||||
`;
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user