KEYCLOAK-11550: Signing In page
This commit is contained in:
parent
812b69af13
commit
210fd92d23
9 changed files with 401 additions and 139 deletions
|
@ -57,6 +57,145 @@ public class AccountCredentialResource {
|
|||
// models.forEach(c -> c.setSecretData(null));
|
||||
// return models.stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList());
|
||||
// }
|
||||
|
||||
private static class CredentialContainer {
|
||||
// ** These first three attributes can be ordinary UI text or a key into
|
||||
// a localized message bundle. Typically, it will be a key, but
|
||||
// the UI will work just fine if you don't care about localization
|
||||
// and you want to just send UI text.
|
||||
//
|
||||
// Also, the ${} shown in Apicurio is not needed.
|
||||
private String category; // **
|
||||
private String type; // **
|
||||
private String helptext; // **
|
||||
private boolean enabled;
|
||||
private String createAction;
|
||||
private String updateAction;
|
||||
private boolean removeable;
|
||||
private List<CredentialModel> userCredentials;
|
||||
|
||||
public CredentialContainer(String category, String type, String helptext, boolean enabled, String createAction, String updateAction, boolean removeable,List<CredentialModel> userCredentials) {
|
||||
this.category = category;
|
||||
this.type = type;
|
||||
this.helptext = helptext;
|
||||
this.enabled = enabled;
|
||||
this.createAction = createAction;
|
||||
this.updateAction = updateAction;
|
||||
this.removeable = removeable;
|
||||
this.userCredentials = userCredentials;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getHelptext() {
|
||||
return helptext;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public String getCreateAction() {
|
||||
return createAction;
|
||||
}
|
||||
|
||||
public String getUpdateAction() {
|
||||
return updateAction;
|
||||
}
|
||||
|
||||
public boolean isRemoveable() {
|
||||
return removeable;
|
||||
}
|
||||
|
||||
public List<CredentialModel> getUserCredentials() {
|
||||
return userCredentials;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
public List<CredentialContainer> dummyCredentialTypes(){
|
||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
List<CredentialModel> models = session.userCredentialManager().getStoredCredentials(realm, user);
|
||||
|
||||
List<CredentialModel> passwordUserCreds = new java.util.ArrayList<>();
|
||||
passwordUserCreds.add(models.get(0));
|
||||
|
||||
List<CredentialModel> otpUserCreds = new java.util.ArrayList<>();
|
||||
if (models.size() > 1) otpUserCreds.add(models.get(1));
|
||||
if (models.size() > 2) otpUserCreds.add(models.get(2));
|
||||
|
||||
List<CredentialModel> webauthnUserCreds = new java.util.ArrayList<>();
|
||||
CredentialModel webauthnCred = new CredentialModel();
|
||||
webauthnCred.setId("bogus-id");
|
||||
webauthnCred.setUserLabel("yubikey key");
|
||||
webauthnCred.setCreatedDate(1579122652382L);
|
||||
webauthnUserCreds.add(webauthnCred);
|
||||
|
||||
List<CredentialModel> webauthnStrongUserCreds = new java.util.ArrayList<>();
|
||||
CredentialModel webauthnStrongCred = new CredentialModel();
|
||||
webauthnStrongCred.setId("bogus-id-for-webauthnStrong");
|
||||
webauthnStrongCred.setUserLabel("My very strong key with required PIN");
|
||||
webauthnStrongCred.setCreatedDate(1579122652382L);
|
||||
webauthnUserCreds.add(webauthnStrongCred);
|
||||
|
||||
CredentialContainer password = new CredentialContainer(
|
||||
"password",
|
||||
"password",
|
||||
"passwordHelptext",
|
||||
true,
|
||||
null, // no create action
|
||||
"UPDATE_PASSWORD",
|
||||
false,
|
||||
passwordUserCreds
|
||||
);
|
||||
CredentialContainer otp = new CredentialContainer(
|
||||
"two-factor",
|
||||
"otp",
|
||||
"otpHelptext",
|
||||
true,
|
||||
"CONFIGURE_TOTP",
|
||||
null, // no update action
|
||||
true,
|
||||
otpUserCreds
|
||||
);
|
||||
CredentialContainer webAuthn = new CredentialContainer(
|
||||
"two-factor",
|
||||
"webauthn",
|
||||
"webauthnHelptext",
|
||||
true,
|
||||
"CONFIGURE_WEBAUTHN",
|
||||
null, // no update action
|
||||
true,
|
||||
webauthnUserCreds
|
||||
);
|
||||
CredentialContainer passwordless = new CredentialContainer(
|
||||
"passwordless",
|
||||
"webauthn-passwordless",
|
||||
"webauthn-passwordlessHelptext",
|
||||
true,
|
||||
"CONFIGURE_WEBAUTHN_STRONG",
|
||||
null, // no update action
|
||||
true,
|
||||
webauthnStrongUserCreds
|
||||
);
|
||||
|
||||
List<CredentialContainer> dummyCreds = new java.util.ArrayList<>();
|
||||
dummyCreds.add(password);
|
||||
dummyCreds.add(otp);
|
||||
dummyCreds.add(webAuthn);
|
||||
dummyCreds.add(passwordless);
|
||||
|
||||
return dummyCreds;
|
||||
}
|
||||
//
|
||||
//
|
||||
// @GET
|
||||
|
@ -72,17 +211,17 @@ public class AccountCredentialResource {
|
|||
// .collect(Collectors.toList());
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Remove a credential for a user
|
||||
// *
|
||||
// */
|
||||
// @Path("{credentialId}")
|
||||
// @DELETE
|
||||
// @NoCache
|
||||
// public void removeCredential(final @PathParam("credentialId") String credentialId) {
|
||||
// auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||
// session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
||||
// }
|
||||
/**
|
||||
* Remove a credential for a user
|
||||
*
|
||||
*/
|
||||
@Path("{credentialId}")
|
||||
@DELETE
|
||||
@NoCache
|
||||
public void removeCredential(final @PathParam("credentialId") String credentialId) {
|
||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||
session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// * Update a credential label for a user
|
||||
|
|
|
@ -59,16 +59,21 @@ unLink=Unlink Account
|
|||
# Signing In Page
|
||||
signingIn=Signing In
|
||||
signingInSubMessage=Configure ways to sign in.
|
||||
twoFactorEnabled=Two-factor authentication is enabled.
|
||||
twoFactorDisabled=Two-factor authentication is disabled.
|
||||
twoFactorAuth=Two-Factor Authentication
|
||||
mobileAuthDefault=Mobile Authenticator (Default)
|
||||
removeMobileAuth=Remove Mobile Authenticator
|
||||
stopMobileAuth=Stop using Mobile Authenticator?
|
||||
setUp=Set Up
|
||||
credentialCreatedAt=Created
|
||||
successRemovedMessage={0} was removed.
|
||||
stopUsingCred=Stop using {0}?
|
||||
removeCred=Remove {0}
|
||||
setUpNew=Set up {0}
|
||||
notSetUp={0} is not set up.
|
||||
two-factor=Two-Factor Authentication
|
||||
passwordless=Passwordless
|
||||
lastUpdate=Last Update
|
||||
unknown=Unknown
|
||||
otp=One Time Password
|
||||
webauthn=WebAuthn
|
||||
webauthn-passwordless=WebAuthn Passwordless
|
||||
otpHelptext=A one-time password (OTP), is a password that is valid for only one login session.
|
||||
webauthnHelptext=WebAuthn lets web applications authenticate users without storing their passwords on servers.
|
||||
webauthn-passwordlessHelptext=I need help with this help text. Any suggestions?
|
||||
|
||||
# Applications page
|
||||
applicationsPageTitle=Applications
|
||||
|
|
|
@ -104,7 +104,7 @@ export class AccountServiceClient {
|
|||
}
|
||||
|
||||
private makeConfig(endpoint: string, config: AxiosRequestConfig = {}): Promise<AxiosRequestConfig> {
|
||||
return new Promise( (resolve: ConfigResolve, reject: ErrorReject) => {
|
||||
return new Promise( (resolve: ConfigResolve) => {
|
||||
this.kcSvc.getToken()
|
||||
.then( (token: string) => {
|
||||
resolve( {
|
||||
|
|
|
@ -39,38 +39,38 @@ export class ContentAlert extends React.Component<ContentAlertProps, ContentAler
|
|||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static success(message: string): void {
|
||||
ContentAlert.instance.postAlert(message, 'success');
|
||||
public static success(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert('success', message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static danger(message: string): void {
|
||||
ContentAlert.instance.postAlert(message, 'danger');
|
||||
public static danger(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert('danger', message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static warning(message: string): void {
|
||||
ContentAlert.instance.postAlert(message, 'warning');
|
||||
public static warning(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert('warning', message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static info(message: string): void {
|
||||
ContentAlert.instance.postAlert(message, 'info');
|
||||
public static info(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert('info', message, params);
|
||||
}
|
||||
|
||||
private hideAlert = () => {
|
||||
this.setState({isVisible: false});
|
||||
}
|
||||
|
||||
private postAlert = (message: string, variant: AlertVariant) => {
|
||||
private postAlert = (variant: AlertVariant, message: string, params?: string[]) => {
|
||||
this.setState({isVisible: true,
|
||||
message: Msg.localize(message),
|
||||
message: Msg.localize(message, params),
|
||||
variant});
|
||||
|
||||
if (variant !== 'danger') {
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
GridItem,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { InfoAltIcon, CheckIcon, LinkIcon, BuilderImageIcon } from '@patternfly/react-icons';
|
||||
import { InfoAltIcon, CheckIcon, BuilderImageIcon } from '@patternfly/react-icons';
|
||||
import { ContentPage } from '../ContentPage';
|
||||
import { ContinueCancelModal } from '../../widgets/ContinueCancelModal';
|
||||
import { AccountServiceClient } from '../../account-service/account.service';
|
||||
|
@ -110,6 +110,8 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
|
|||
<ContentPage title={Msg.localize('applicationsPageTitle')}>
|
||||
<DataList id="applications-list" aria-label={Msg.localize('applicationsPageTitle')}>
|
||||
{this.state.applications.map((application: Application, appIndex: number) => {
|
||||
const appUrl: string = application.userConsentRequired ? application.baseUrl : '/auth' + application.baseUrl;
|
||||
|
||||
return (
|
||||
<DataListItem key={'application-' + appIndex} aria-labelledby="applications-list" isExpanded={this.state.isRowOpen[appIndex]}>
|
||||
<DataListItemRow>
|
||||
|
@ -132,7 +134,10 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
|
|||
{application.inUse ? Msg.localize('inUse') : Msg.localize('notInUse')}
|
||||
</DataListCell>,
|
||||
<DataListCell width={4} key={'baseUrl-' + appIndex}>
|
||||
<a href={application.userConsentRequired ? application.baseUrl : '/auth' + application.baseUrl} target="_blank"><LinkIcon /> {application.baseUrl}</a>
|
||||
<button className="pf-c-button pf-m-link" type="button" onClick={() => window.open(appUrl)}>
|
||||
<span className="pf-c-button__icon">
|
||||
<i className="fas fa-link" aria-hidden="true"></i>
|
||||
</span>{application.baseUrl}</button>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -158,7 +158,7 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
|
|||
return moment(time * 1000).format('LLLL');
|
||||
}
|
||||
|
||||
private elementId(item: string, session: Session) : string {
|
||||
private elementId(item: string, session: Session): string {
|
||||
return `session-${session.id.substring(0,7)}-${item}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,12 +25,12 @@ import {
|
|||
DataListAction,
|
||||
DataListItemCells,
|
||||
DataListCell,
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
Stack,
|
||||
StackItem,
|
||||
Switch,
|
||||
Title,
|
||||
TitleLevel
|
||||
TitleLevel,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import {AIACommand} from '../../util/AIACommand';
|
||||
|
@ -48,14 +48,36 @@ interface PasswordDetails {
|
|||
lastUpdate: number;
|
||||
}
|
||||
|
||||
type CredCategory = 'password' | 'two-factor' | 'passwordless';
|
||||
type CredType = string;
|
||||
type CredTypeMap = Map<CredType, CredentialContainer>;
|
||||
type CredContainerMap = Map<CredCategory, CredTypeMap>;
|
||||
|
||||
interface UserCredential {
|
||||
id: string;
|
||||
type: string;
|
||||
userLabel: string;
|
||||
createdDate: number;
|
||||
strCreatedDate?: string;
|
||||
}
|
||||
|
||||
// A CredentialContainer is unique by combo of credential type and credential category
|
||||
interface CredentialContainer {
|
||||
category: CredCategory;
|
||||
type: CredType;
|
||||
helptext?: string;
|
||||
createAction: string;
|
||||
updateAction: string;
|
||||
removeable: boolean;
|
||||
userCredentials: UserCredential[];
|
||||
}
|
||||
|
||||
interface SigningInPageProps extends RouteComponentProps {
|
||||
}
|
||||
|
||||
interface SigningInPageState {
|
||||
twoFactorEnabled: boolean;
|
||||
twoFactorEnabledText: string;
|
||||
isTotpConfigured: boolean;
|
||||
lastPasswordUpdate?: number;
|
||||
// Credential containers organized by category then type
|
||||
credentialContainers: CredContainerMap;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,116 +90,205 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
|||
public constructor(props: SigningInPageProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
twoFactorEnabled: true,
|
||||
twoFactorEnabledText: Msg.localize('twoFactorEnabled'),
|
||||
isTotpConfigured: features.isTotpConfigured,
|
||||
}
|
||||
this.setLastPwdUpdate();
|
||||
credentialContainers: new Map()
|
||||
}
|
||||
|
||||
private setLastPwdUpdate(): void {
|
||||
AccountServiceClient.Instance.doGet("/credentials/password")
|
||||
.then((response: AxiosResponse<PasswordDetails>) => {
|
||||
if (response.data.lastUpdate) {
|
||||
const lastUpdate: number = response.data.lastUpdate;
|
||||
this.setState({lastPasswordUpdate: lastUpdate});
|
||||
this.getCredentialContainers();
|
||||
}
|
||||
|
||||
private getCredentialContainers(): void {
|
||||
AccountServiceClient.Instance.doGet("/credentials")
|
||||
.then((response: AxiosResponse<CredentialContainer[]>) => {
|
||||
|
||||
const allContainers: CredContainerMap = new Map();
|
||||
response.data.forEach(container => {
|
||||
let categoryMap = allContainers.get(container.category);
|
||||
if (!categoryMap) {
|
||||
categoryMap = new Map();
|
||||
allContainers.set(container.category, categoryMap);
|
||||
}
|
||||
categoryMap.set(container.type, container);
|
||||
});
|
||||
|
||||
this.setState({credentialContainers: allContainers});
|
||||
console.log({allContainers})
|
||||
});
|
||||
}
|
||||
|
||||
private handleTwoFactorSwitch = () => {
|
||||
if (this.state.twoFactorEnabled) {
|
||||
this.setState({twoFactorEnabled: false, twoFactorEnabledText: Msg.localize('twoFactorDisabled')})
|
||||
} else {
|
||||
this.setState({twoFactorEnabled: true, twoFactorEnabledText: Msg.localize('twoFactorEnabled')})
|
||||
}
|
||||
}
|
||||
|
||||
private handleRemoveTOTP = () => {
|
||||
AccountServiceClient.Instance.doDelete("/totp/remove")
|
||||
private handleRemove = (credentialId: string, userLabel: string) => {
|
||||
AccountServiceClient.Instance.doDelete("/credentials/" + credentialId)
|
||||
.then(() => {
|
||||
this.setState({isTotpConfigured: false});
|
||||
ContentAlert.success('successTotpRemovedMessage');
|
||||
this.getCredentialContainers();
|
||||
ContentAlert.success('successRemovedMessage', [userLabel]);
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let lastPwdUpdate: string = Msg.localize('unknown');
|
||||
if (this.state.lastPasswordUpdate) {
|
||||
lastPwdUpdate = moment(this.state.lastPasswordUpdate).format('LLL');
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentPage title="signingIn"
|
||||
introMessage="signingInSubMessage">
|
||||
<Stack gutter='md'>
|
||||
<StackItem isFilled>
|
||||
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
||||
<strong><Msg msgKey='password'/></strong>
|
||||
</Title>
|
||||
<DataList aria-label='foo'>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key='password'><Msg msgKey='password'/></DataListCell>,
|
||||
<DataListCell key='lastPwdUpdate'><strong><Msg msgKey='lastUpdate'/>: </strong>{lastPwdUpdate}</DataListCell>,
|
||||
<DataListCell key='spacer'/>
|
||||
]}/>
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id='setPasswordAction'>
|
||||
<Button variant='primary'onClick={()=> this.updatePassword.execute()}><Msg msgKey='update'/></Button>
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
</DataList>
|
||||
</StackItem>
|
||||
<StackItem isFilled>
|
||||
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
||||
<strong><Msg msgKey='twoFactorAuth'/></strong>
|
||||
</Title>
|
||||
<DataList aria-label='foo'>
|
||||
<DataListItemRow>
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id='twoFactorOnOff'>
|
||||
<Switch
|
||||
aria-label='twoFactorSwitch'
|
||||
label={this.state.twoFactorEnabledText}
|
||||
isChecked={this.state.twoFactorEnabled}
|
||||
onClick={this.handleTwoFactorSwitch}
|
||||
/>
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key='mobileAuth'><Msg msgKey='mobileAuthDefault'/></DataListCell>
|
||||
]}/>
|
||||
{!this.state.isTotpConfigured &&
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id='setMobileAuthAction'>
|
||||
<Button isDisabled={!this.state.twoFactorEnabled} variant='primary' onClick={()=> this.setUpTOTP.execute()}><Msg msgKey='setUp'/></Button>
|
||||
</DataListAction>
|
||||
}
|
||||
{this.state.isTotpConfigured &&
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id='setMobileAuthAction'>
|
||||
<ContinueCancelModal buttonTitle='remove'
|
||||
isDisabled={!this.state.twoFactorEnabled}
|
||||
modalTitle={Msg.localize('removeMobileAuth')}
|
||||
modalMessage={Msg.localize('stopMobileAuth')}
|
||||
onContinue={this.handleRemoveTOTP}
|
||||
/>
|
||||
</DataListAction>
|
||||
}
|
||||
</DataListItemRow>
|
||||
</DataList>
|
||||
</StackItem>
|
||||
<StackItem isFilled>
|
||||
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
||||
<strong><Msg msgKey='passwordless'/></strong>
|
||||
</Title>
|
||||
</StackItem>
|
||||
{this.renderCategories()}
|
||||
</Stack>
|
||||
</ContentPage>
|
||||
);
|
||||
}
|
||||
|
||||
private renderCategories(): React.ReactNode {
|
||||
return (<> {
|
||||
Array.from(this.state.credentialContainers.keys()).map(category => (
|
||||
<StackItem key={category} isFilled>
|
||||
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
||||
<strong><Msg msgKey={category}/></strong>
|
||||
</Title>
|
||||
<DataList aria-label='foo'>
|
||||
{this.renderTypes(this.state.credentialContainers.get(category)!)}
|
||||
</DataList>
|
||||
</StackItem>
|
||||
))
|
||||
|
||||
}</>)
|
||||
}
|
||||
|
||||
private renderTypes(credTypeMap: CredTypeMap): React.ReactNode {
|
||||
return (<> {
|
||||
Array.from(credTypeMap.keys()).map((credType: CredType, index: number, typeArray: string[]) => ([
|
||||
this.renderCredTypeTitle(credTypeMap.get(credType)!),
|
||||
this.renderUserCredentials(credTypeMap, credType),
|
||||
this.renderEmptyRow(credTypeMap.get(credType)!.type, index === typeArray.length - 1)
|
||||
]))
|
||||
}</>)
|
||||
}
|
||||
|
||||
private renderEmptyRow(type: string, isLast: boolean): React.ReactNode {
|
||||
if (isLast) return; // don't put empty row at the end
|
||||
|
||||
return (
|
||||
<DataListItem aria-labelledby={'empty-list-item-' + type}>
|
||||
<DataListItemRow key={'empty-row-' + type}>
|
||||
<DataListItemCells dataListCells={[<DataListCell></DataListCell>]}/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
)
|
||||
}
|
||||
|
||||
private renderUserCredentials(credTypeMap: CredTypeMap, credType: CredType): React.ReactNode {
|
||||
const userCredentials: UserCredential[] = credTypeMap.get(credType)!.userCredentials;
|
||||
const removeable: boolean = credTypeMap.get(credType)!.removeable;
|
||||
const updateAction: string = credTypeMap.get(credType)!.updateAction;
|
||||
const type: string = credTypeMap.get(credType)!.type;
|
||||
|
||||
if (userCredentials.length === 0) {
|
||||
const localizedType = Msg.localize(type);
|
||||
return (
|
||||
<DataListItem aria-labelledby='no-credentials-list-item'>
|
||||
<DataListItemRow key='no-credentials-list-item-row'>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell/>,
|
||||
<strong><Msg msgKey='notSetUp' params={[localizedType]}/></strong>,
|
||||
<DataListCell/>
|
||||
]}/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
}
|
||||
|
||||
userCredentials.forEach(credential => {
|
||||
if (!credential.userLabel) credential.userLabel = Msg.localize(credential.type);
|
||||
credential.strCreatedDate = moment(credential.createdDate).format('LLL');
|
||||
});
|
||||
|
||||
let updateAIA: AIACommand;
|
||||
if (updateAction) updateAIA = new AIACommand(updateAction, this.props.location.pathname);
|
||||
|
||||
return (
|
||||
<React.Fragment key='userCredentials'> {
|
||||
userCredentials.map(credential => (
|
||||
<DataListItem aria-labelledby={'credential-list-item-' + credential.userLabel}>
|
||||
<DataListItemRow key={'userCredentialRow-' + credential.id}>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key={'userLabel-' + credential.id}>{credential.userLabel}</DataListCell>,
|
||||
<DataListCell key={'created-' + credential.id}><strong><Msg msgKey='credentialCreatedAt'/>: </strong>{credential.strCreatedDate}</DataListCell>,
|
||||
<DataListCell key={'spacer-' + credential.id}/>
|
||||
]}/>
|
||||
|
||||
<CredentialAction credential={credential}
|
||||
removeable={removeable}
|
||||
updateAction={updateAIA}
|
||||
credRemover={this.handleRemove}/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
))
|
||||
}
|
||||
</React.Fragment>)
|
||||
}
|
||||
|
||||
private renderCredTypeTitle(credContainer: CredentialContainer): React.ReactNode {
|
||||
if (!credContainer.createAction) return;
|
||||
|
||||
const setupAction: AIACommand = new AIACommand(credContainer.createAction, this.props.location.pathname);
|
||||
const credContainerType: string = Msg.localize(credContainer.type);
|
||||
|
||||
return (
|
||||
<DataListItem aria-labelledby={'type-datalistitem-' + credContainer.type}>
|
||||
<DataListItemRow key={'credTitleRow-' + credContainer.type}>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell width={5} key={'credTypeTitle-' + credContainer.type}>
|
||||
<Title headingLevel={TitleLevel.h3} size='2xl'>
|
||||
<strong><Msg msgKey={credContainer.type}/></strong>
|
||||
</Title>
|
||||
<Msg msgKey={credContainer.helptext}/>
|
||||
</DataListCell>,
|
||||
|
||||
]}/>
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'setUpAction-' + credContainer.type}>
|
||||
<button className="pf-c-button pf-m-link" type="button" onClick={()=> setupAction.execute()}>
|
||||
<span className="pf-c-button__icon">
|
||||
<i className="fas fa-plus-circle" aria-hidden="true"></i>
|
||||
</span>
|
||||
<Msg msgKey='setUpNew' params={[credContainerType]}/>
|
||||
</button>
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
)
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
type CredRemover = (credentialId: string, userLabel: string) => void;
|
||||
interface CredentialActionProps {credential: UserCredential;
|
||||
removeable: boolean;
|
||||
updateAction: AIACommand;
|
||||
credRemover: CredRemover;};
|
||||
class CredentialAction extends React.Component<CredentialActionProps> {
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.props.updateAction) {
|
||||
return (
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'updateAction-' + this.props.credential.id}>
|
||||
<Button variant='primary'onClick={()=> this.props.updateAction.execute()}><Msg msgKey='update'/></Button>
|
||||
</DataListAction>
|
||||
)
|
||||
}
|
||||
|
||||
if (this.props.removeable) {
|
||||
const userLabel: string = this.props.credential.userLabel;
|
||||
return (
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'removeAction-' + this.props.credential.id }>
|
||||
<ContinueCancelModal buttonTitle='remove'
|
||||
modalTitle={Msg.localize('removeCred', [userLabel])}
|
||||
modalMessage={Msg.localize('stopUsingCred', [userLabel])}
|
||||
onContinue={() => this.props.credRemover(this.props.credential.id, userLabel)}
|
||||
/>
|
||||
</DataListAction>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SigningInPageWithRouter = withRouter(SigningInPage);
|
||||
export { SigningInPageWithRouter as SigningInPage};
|
|
@ -19,7 +19,7 @@ import * as React from 'react';
|
|||
declare const l18nMsg: {[key: string]: string};
|
||||
|
||||
export interface MsgProps {
|
||||
readonly msgKey: string;
|
||||
readonly msgKey: string | undefined;
|
||||
readonly params?: string[];
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,9 @@ export class Msg extends React.Component<MsgProps> {
|
|||
);
|
||||
}
|
||||
|
||||
public static localize(msgKey: string, params?: string[]): string {
|
||||
public static localize(msgKey: string | undefined, params?: string[]): string {
|
||||
if (msgKey === undefined) return '';
|
||||
|
||||
let message: string = l18nMsg[this.processKey(msgKey)];
|
||||
if (message === undefined) message = msgKey;
|
||||
|
||||
|
|
|
@ -1333,7 +1333,7 @@
|
|||
'./icons/playstation-icon.js': '@empty',
|
||||
'./icons/plug-icon.js': '@empty',
|
||||
'./icons/plugged-icon.js': '@empty',
|
||||
'./icons/plus-circle-icon.js': '@empty',
|
||||
//'./icons/plus-circle-icon.js': '@empty',
|
||||
'./icons/plus-icon.js': '@empty',
|
||||
'./icons/plus-square-icon.js': '@empty',
|
||||
'./icons/podcast-icon.js': '@empty',
|
||||
|
|
Loading…
Reference in a new issue