Updates patternfly libs and fixes breaking changes (#10748)

adding nvmrc

CIAM-1048 Device Activity screen PF updates

CIAM-1046: Personal Info sub-header update

Updates SigningInPage to use EmptyState component when there are no credentials.

rearanged some components used in signing in page

Displays ApplicationPage content in description list.

Updates refresh link on ContentPage, updates Resources screen.

CIAM-1049 Linked Accounts screen PF updates

CIAM-1043-General upstream updates

Updates AccountPage to display form errors.

fix: display Set up Authenticator Application link on large viewport

fix(page structure): rearranges page sections

CIAM-1254/Personal info PF4 updates & Sidebar text updates

updating layouts

updating layout on Signing in and Linked acounts

adding patternfly-additions

adding patternfly-addons styles

Updates Application page based on designs feedback.

moving page description

Updates status label on Applications page to be capitalized.

Updates the copy-fonts script for keycloak.v2 to copy all font directories instead of one.

update Personal info screen - set max width of 600px for form input fields

update Personal info - remove required indicator from input fields

General updates (#2)

* removed the extra lines being shown

* tweaked general spacing

* general alignment and spacer application

* refactor to get proper alignments without css globals

* forgot to add the conditional on displaying the set up buttons

* try and adjust the alignments

Co-authored-by: zwitter <zwitter@redhat.com>

resolve merge conflicts

Device activity updates (#4)

* update text to sentence case

* update device info columns to be dynamic across various viewport sizes

* update signed in device layout

* update based on feedback

Co-authored-by: Jon Szeto <jszeto@redhat.com>

Linked accounts update (#3)

* linked accounts screen - updated icons & Linked/Unlinked Login Providers layout & update text to sentence case

Co-authored-by: Jon Szeto <jszeto@redhat.com>

fixing ts errors

cleaning up fonts and messages

final review updates

message update for Back to admin console link

fixing capitalization on 2fa

updating landing page welcome message

fix: reposition Back to... link

adjusting size for confirm modal

updating spacing and alignment issues

updating resources page

removing unused header class

fixes ts issues and updates node version to match the themes install

npm updates

fixing pf addons

adding chokidar to get babel:watch working

fixing issues from pull request feedback

fixing tests

fixes signingin page test

fixing tests

Co-authored-by: Tyler Andor <tandor@highereducation.com>
This commit is contained in:
Tyler Andor 2022-04-06 05:00:38 -06:00 committed by GitHub
parent a68e8ba0c8
commit caebe50d7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 4910 additions and 5240 deletions

View file

@ -62,11 +62,7 @@ public class RecoveryAuthnCodesCredentialProvider
.helpText("recovery-authn-codes-help-text").iconCssClass("kcAuthenticatorRecoveryAuthnCodesClass") .helpText("recovery-authn-codes-help-text").iconCssClass("kcAuthenticatorRecoveryAuthnCodesClass")
.removeable(true); .removeable(true);
UserModel user = metadataContext.getUser(); UserModel user = metadataContext.getUser();
if (user != null && !isConfiguredFor(session.getContext().getRealm(), user, getType())) {
builder.createAction(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name()); builder.createAction(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name());
} else {
builder.updateAction(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name());
}
return builder.build(session); return builder.build(session);
} }

View file

@ -43,7 +43,7 @@ public class AccountManagement extends AuthRealm implements PageWithLogOutAction
@FindBy(xpath = "//a[@id='referer']") @FindBy(xpath = "//a[@id='referer']")
private WebElement backToRefererLink; private WebElement backToRefererLink;
@FindBy(linkText = "Sign Out") @FindBy(linkText = "Sign out")
private WebElement signOutLink; private WebElement signOutLink;
@FindBy(linkText = "Account") @FindBy(linkText = "Account")

View file

@ -26,7 +26,7 @@ import org.openqa.selenium.support.FindBy;
*/ */
public abstract class AbstractAccountPage extends AbstractPage { public abstract class AbstractAccountPage extends AbstractPage {
@FindBy(linkText = "Sign Out") @FindBy(linkText = "Sign out")
private WebElement logoutLink; private WebElement logoutLink;
@FindBy(id = "kc-current-locale-link") @FindBy(id = "kc-current-locale-link")

View file

@ -1357,7 +1357,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
loginPage.login("realm-admin", "password"); loginPage.login("realm-admin", "password");
Assert.assertTrue(applicationsPage.isCurrent()); Assert.assertTrue(applicationsPage.isCurrent());
Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications(); Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
Assert.assertThat(apps.keySet(), hasItems("Admin CLI", "Security Admin Console")); Assert.assertThat(apps.keySet(), hasItems("Admin CLI", "security admin console"));
events.clear(); events.clear();
} }

View file

@ -139,11 +139,11 @@ public class DeviceActivityPage extends AbstractLoggedInPage {
} }
public String getStarted() { public String getStarted() {
return getTextFromItem("started").split("Started at ")[1]; return getTextFromItem("started").split("Started ")[1];
} }
public String getExpires() { public String getExpires() {
return getTextFromItem("expires").split("Expires at ")[1]; return getTextFromItem("expires").split("Expires ")[1];
} }
public boolean isSignOutDisplayed() { public boolean isSignOutDisplayed() {

View file

@ -104,11 +104,11 @@ public class LinkedAccountsPage extends AbstractLoggedInPage {
} }
public boolean hasSocialLoginBadge() { public boolean hasSocialLoginBadge() {
return getTextFromElement(badgeElement).equals("Social Login"); return getTextFromElement(badgeElement).equals("Social login");
} }
public boolean hasSystemDefinedBadge() { public boolean hasSystemDefinedBadge() {
return getTextFromElement(badgeElement).equals("System Defined"); return getTextFromElement(badgeElement).equals("System defined");
} }
public boolean hasSocialIcon() { public boolean hasSocialIcon() {

View file

@ -28,6 +28,6 @@ public class PageNotFound extends AbstractLoggedInPage {
@Override @Override
public boolean isCurrent() { public boolean isCurrent() {
return driver.getPageSource().contains("Page Not Found"); return driver.getPageSource().contains("Page not found");
} }
} }

View file

@ -50,7 +50,7 @@ import static org.keycloak.testsuite.util.WaitUtils.pause;
* @author Vaclav Muzikar <vmuzikar@redhat.com> * @author Vaclav Muzikar <vmuzikar@redhat.com>
*/ */
public class SigningInTest extends BaseAccountPageTest { public class SigningInTest extends BaseAccountPageTest {
public static final String PASSWORD_LABEL = "My Password"; public static final String PASSWORD_LABEL = "My password";
@Page @Page
private SigningInPage signingInPage; private SigningInPage signingInPage;
@ -91,8 +91,8 @@ public class SigningInTest extends BaseAccountPageTest {
testContext.setTestRealmReps(emptyList()); // reimport realm after this test testContext.setTestRealmReps(emptyList()); // reimport realm after this test
assertThat(signingInPage.getCategoriesCount(), is(2)); assertThat(signingInPage.getCategoriesCount(), is(2));
assertThat(signingInPage.getCategoryTitle("basic-authentication"), is("Basic Authentication")); assertThat(signingInPage.getCategoryTitle("basic-authentication"), is("Basic authentication"));
assertThat(signingInPage.getCategoryTitle("two-factor"), is("Two-Factor Authentication")); assertThat(signingInPage.getCategoryTitle("two-factor"), is("Two-factor authentication"));
} }
@Test @Test
@ -160,7 +160,7 @@ public class SigningInTest extends BaseAccountPageTest {
signingInPage.assertCurrent(); signingInPage.assertCurrent();
assertThat(otpCredentialType.isSetUp(), is(false)); assertThat(otpCredentialType.isSetUp(), is(false));
assertThat(otpCredentialType.getTitle(), is("Authenticator Application")); assertThat(otpCredentialType.getTitle(), is("authenticator application"));
final String label1 = "OTP is secure"; final String label1 = "OTP is secure";
final String label2 = "OTP is inconvenient"; final String label2 = "OTP is inconvenient";

View file

@ -3,11 +3,16 @@ doCancel=Cancel
doLogOutAllSessions=Log out all sessions doLogOutAllSessions=Log out all sessions
doRemove=Remove doRemove=Remove
doAdd=Add doAdd=Add
doSignOut=Sign Out doSignOut=Sign out
doLogIn=Log In doLogIn=Log In
doLink=Link doLink=Link
noAccessMessage=Access not allowed noAccessMessage=Access not allowed
personalInfoSidebarTitle=Personal info
accountSecuritySidebarTitle=Account security
signingInSidebarTitle=Signing in
deviceActivitySidebarTitle=Device activity
linkedAccountsSidebarTitle=Linked accounts
editAccountHtmlTitle=Edit Account editAccountHtmlTitle=Edit Account
personalInfoHtmlTitle=Personal Info personalInfoHtmlTitle=Personal Info
@ -19,7 +24,7 @@ sessionsHtmlTitle=Sessions
accountManagementTitle=Keycloak Account Management accountManagementTitle=Keycloak Account Management
authenticatorTitle=Authenticator authenticatorTitle=Authenticator
applicationsHtmlTitle=Applications applicationsHtmlTitle=Applications
linkedAccountsHtmlTitle=Linked Accounts linkedAccountsHtmlTitle=Linked accounts
accountManagementWelcomeMessage=Welcome to Keycloak Account Management accountManagementWelcomeMessage=Welcome to Keycloak Account Management
personalInfoIntroMessage=Manage your basic information personalInfoIntroMessage=Manage your basic information
@ -32,7 +37,7 @@ updatePasswordTitle=Update Password
updatePasswordMessageTitle=Make sure you choose a strong password updatePasswordMessageTitle=Make sure you choose a strong password
updatePasswordMessage=A strong password contains a mix of numbers, letters, and symbols. It is hard to guess, does not resemble a real word, and is only used for this account. updatePasswordMessage=A strong password contains a mix of numbers, letters, and symbols. It is hard to guess, does not resemble a real word, and is only used for this account.
personalSubTitle=Your Personal Info personalSubTitle=Your Personal Info
personalSubMessage=Manage this basic information: your first name, last name and email personalSubMessage=Manage your basic information.
authenticatorCode=One-time code authenticatorCode=One-time code
email=Email email=Email
@ -94,7 +99,7 @@ role_offline-access=Offline access
role_uma_authorization=Obtain permissions role_uma_authorization=Obtain permissions
client_account=Account client_account=Account
client_account-console=Account Console client_account-console=Account Console
client_security-admin-console=Security Admin Console client_security-admin-console=security admin console
client_admin-cli=Admin CLI client_admin-cli=Admin CLI
client_realm-management=Realm Management client_realm-management=Realm Management
client_broker=Broker client_broker=Broker
@ -120,7 +125,7 @@ applications=Applications
account=Account account=Account
federatedIdentity=Federated Identity federatedIdentity=Federated Identity
authenticator=Authenticator authenticator=Authenticator
device-activity=Device Activity device-activity=Device activity
sessions=Sessions sessions=Sessions
log=Log log=Log

View file

@ -48,7 +48,7 @@
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-recovery-codes-settings-form" method="post"> <form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-recovery-codes-settings-form" method="post">
<input type="hidden" name="generatedRecoveryAuthnCodes" value="${recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesAsString}" /> <input type="hidden" name="generatedRecoveryAuthnCodes" value="${recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesAsString}" />
<input type="hidden" name="generatedAt" value="${recoveryAuthnCodesConfigBean.generatedAt?c}" /> <input type="hidden" name="generatedAt" value="${recoveryAuthnCodesConfigBean.generatedAt?c}" />
<input type="hidden" name="userLabel" value=" " /> <input type="hidden" id="userLabel" name="userLabel" value="${msg("recovery-codes-label-default")}" />
<#if isAppInitiatedAction??> <#if isAppInitiatedAction??>
<input type="submit" <input type="submit"

View file

@ -442,6 +442,7 @@ recovery-codes-action-cancel=Cancel setup
recovery-codes-download-file-header=Keep these recovery codes somewhere safe. recovery-codes-download-file-header=Keep these recovery codes somewhere safe.
recovery-codes-download-file-description=Recovery codes are single-use passcodes that allow you to log in to your account if you do not have access to your authenticator. recovery-codes-download-file-description=Recovery codes are single-use passcodes that allow you to log in to your account if you do not have access to your authenticator.
recovery-codes-download-file-date= These codes were generated on recovery-codes-download-file-date= These codes were generated on
recovery-codes-label-default=Recovery codes
# WebAuthn # WebAuthn
webauthn-display-name=Security Key webauthn-display-name=Security Key

View file

@ -112,8 +112,10 @@
</#list> </#list>
</#if> </#if>
<link rel="stylesheet" type="text/css" href="${resourceCommonUrl}/web_modules/@patternfly/react-core/dist/styles/base.css"/> <link rel="stylesheet" type="text/css" href="${resourceCommonUrl}/web_modules/@patternfly/react-core/dist/styles/base.css"/>
<link rel="stylesheet" type="text/css" href="${resourceCommonUrl}/web_modules/@patternfly/react-core/dist/styles/app.css"/> <link rel="stylesheet" type="text/css" href="${resourceCommonUrl}/web_modules/@patternfly/react-core/dist/styles/app.css"/>
<link rel="stylesheet" type="text/css" href="${resourceCommonUrl}/web_modules/@patternfly/patternfly/patternfly-addons.css"/>
<link href="${resourceUrl}/public/layout.css" rel="stylesheet"/> <link href="${resourceUrl}/public/layout.css" rel="stylesheet"/>
</head> </head>
@ -181,18 +183,23 @@
</div> </div>
<div class="pf-c-page__header-tools"> <div class="pf-c-page__header-tools">
<#if referrer?has_content && referrer_uri?has_content> <#if referrer?has_content && referrer_uri?has_content>
<div class="pf-c-page__header-tools-group pf-m-icons"> <div class="pf-c-page__header-tools-group pf-m-icons pf-u-display-none pf-u-display-flex-on-md">
<a id="landingReferrerLink" href="${referrer_uri}" id="referrer" tabindex="0"><span class="pf-icon pf-icon-arrow"></span>${msg("backTo",referrerName)}</a> <a id="landingReferrerLink" href="${referrer_uri}" class="pf-c-button pf-m-link" tabindex="0">
<span class="pf-c-button__icon pf-m-start">
<i class="pf-icon pf-icon-arrow" aria-hidden="true"></i>
</span>
${msg("backToAdminConsole")}
</a>
</div> </div>
</#if> </#if>
<div class="pf-c-page__header-tools-group pf-m-icons"> <div class="pf-c-page__header-tools-group pf-m-icons pf-u-display-none pf-u-display-flex-on-md pf-u-mr-md">
<button id="landingSignInButton" tabindex="0" style="display:none" onclick="keycloak.login();" class="pf-c-button pf-m-primary" type="button">${msg("doSignIn")}</button> <button id="landingSignInButton" tabindex="0" style="display:none" onclick="keycloak.login();" class="pf-c-button pf-m-primary" type="button">${msg("doSignIn")}</button>
<button id="landingSignOutButton" tabindex="0" style="display:none" onclick="keycloak.logout();" class="pf-c-button pf-m-primary" type="button">${msg("doSignOut")}</button> <button id="landingSignOutButton" tabindex="0" style="display:none" onclick="keycloak.logout();" class="pf-c-button pf-m-primary" type="button">${msg("doSignOut")}</button>
</div> </div>
<!-- Kebab for mobile --> <!-- Kebab for mobile -->
<div class="pf-c-page__header-tools-group"> <div class="pf-c-page__header-tools-group pf-u-display-none-on-md">
<div id="landingMobileKebab" class="pf-c-dropdown pf-m-mobile" onclick="toggleMobileDropdown();"> <!-- pf-m-expanded --> <div id="landingMobileKebab" class="pf-c-dropdown pf-m-mobile" onclick="toggleMobileDropdown();"> <!-- pf-m-expanded -->
<button aria-label="Actions" tabindex="0" id="landingMobileKebabButton" class="pf-c-dropdown__toggle pf-m-plain" type="button" aria-expanded="true" aria-haspopup="true"> <button aria-label="Actions" tabindex="0" id="landingMobileKebabButton" class="pf-c-dropdown__toggle pf-m-plain" type="button" aria-expanded="true" aria-haspopup="true">
<svg fill="currentColor" height="1em" width="1em" viewBox="0 0 192 512" aria-hidden="true" role="img" style="vertical-align: -0.125em;"><path d="M96 184c39.8 0 72 32.2 72 72s-32.2 72-72 72-72-32.2-72-72 32.2-72 72-72zM24 80c0 39.8 32.2 72 72 72s72-32.2 72-72S135.8 8 96 8 24 40.2 24 80zm0 352c0 39.8 32.2 72 72 72s72-32.2 72-72-32.2-72-72-72-72 32.2-72 72z" transform=""></path></svg> <svg fill="currentColor" height="1em" width="1em" viewBox="0 0 192 512" aria-hidden="true" role="img" style="vertical-align: -0.125em;"><path d="M96 184c39.8 0 72 32.2 72 72s-32.2 72-72 72-72-32.2-72-72 32.2-72 72-72zM24 80c0 39.8 32.2 72 72 72s72-32.2 72-72S135.8 8 96 8 24 40.2 24 80zm0 352c0 39.8 32.2 72 72 72s72-32.2 72-72-32.2-72-72-72-72 32.2-72 72z" transform=""></path></svg>
@ -200,7 +207,7 @@
<ul id="landingMobileDropdown" aria-labelledby="landingMobileKebabButton" class="pf-c-dropdown__menu pf-m-align-right" role="menu" style="display:none"> <ul id="landingMobileDropdown" aria-labelledby="landingMobileKebabButton" class="pf-c-dropdown__menu pf-m-align-right" role="menu" style="display:none">
<#if referrer?has_content && referrer_uri?has_content> <#if referrer?has_content && referrer_uri?has_content>
<li role="none"> <li role="none">
<a id="landingMobileReferrerLink" href="${referrer_uri}" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${msg("backTo",referrerName)}</a> <a id="landingMobileReferrerLink" href="${referrer_uri}" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${msg("backToAdminConsole")}</a>
</li> </li>
</#if> </#if>
@ -220,31 +227,35 @@
</header> </header>
<main role="main" class="pf-c-page__main"> <main role="main" class="pf-c-page__main">
<section class="pf-c-page__main-section pf-m-light"> <section class="pf-c-page__main-section pf-m-limit-width pf-m-light pf-m-shadow-bottom">
<div class="pf-c-page__main-body">
<div class="pf-c-content" id="landingWelcomeMessage"> <div class="pf-c-content" id="landingWelcomeMessage">
<h1>${msg("accountManagementWelcomeMessage")}</h1> <h1>${msg("accountManagementWelcomeMessage")}</h1>
</div> </div>
</div>
</section> </section>
<section class="pf-c-page__main-section"> <section class="pf-c-page__main-section pf-m-limit-width pf-m-overflow-scroll">
<div class="pf-c-page__main-body">
<div class="pf-l-gallery pf-m-gutter"> <div class="pf-l-gallery pf-m-gutter">
<#assign content=theme.apply("content.json")?eval> <#assign content=theme.apply("content.json")?eval>
<#list content as item> <#list content as item>
<div class="pf-l-gallery__item pf-c-card" id="landing-${item.id}"> <div class="pf-l-gallery__item" id="landing-${item.id}">
<div class="pf-c-card pf-m-full-height">
<div> <div>
<div class="pf-c-card__header pf-c-content"> <div class="pf-c-card__title pf-c-content">
<h2> <h2 class="pf-u-display-flex pf-u-w-100 pf-u-flex-direction-column">
<#if item.icon??> <#if item.icon??>
<i class="pf-icon ${item.icon}"></i>&nbsp; <i class="pf-icon ${item.icon}"></i>
<#elseif item.iconSvg??> <#elseif item.iconSvg??>
<img src="${item.iconSvg}" alt="icon"/>&nbsp; <img src="${item.iconSvg}" alt="icon"/>
</#if> </#if>
${msg(item.label)} ${msg(item.label)}
</h2> </h2>
<#if item.descriptionLabel??>
<p>${msg(item.descriptionLabel)}</p>
</#if>
</div> </div>
<div class="pf-c-card__body pf-c-content"> <div class="pf-c-card__body">
<#if item.descriptionLabel??>
<p class="pf-u-mb-md">${msg(item.descriptionLabel)}</p>
</#if>
<#if item.content??> <#if item.content??>
<#list item.content as sub> <#list item.content as sub>
<div id="landing-${sub.id}"> <div id="landing-${sub.id}">
@ -257,8 +268,10 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</#list> </#list>
</div> </div>
</div>
</section> </section>
</main> </main>
</div> </div>

View file

@ -1,13 +1,14 @@
# Put new messages for Account Console Here # Put new messages for Account Console Here
# Feel free to use any existing messages from the base theme # Feel free to use any existing messages from the base theme
pageNotFound=Page Not Found pageNotFound=Page not found
forbidden=Forbidden forbidden=Forbidden
needAccessRights=You do not have access rights to this request. Contact your administrator. needAccessRights=You do not have access rights to this request. Contact your administrator.
invalidRoute={0} is not a valid route. invalidRoute={0} is not a valid route.
actionRequiresIDP=This action requires redirection to your identity provider. actionRequiresIDP=This action requires redirection to your identity provider.
actionNotDefined=No Action defined actionNotDefined=No action defined
continue=Continue continue=Continue
refreshPage=Refresh the page refreshPage=Refresh the page
refresh=Refresh
done=Done done=Done
cancel=Cancel cancel=Cancel
remove=Remove remove=Remove
@ -15,38 +16,46 @@ update=Update
loadingMessage=Account Console loading ... loadingMessage=Account Console loading ...
unknownUser=Anonymous unknownUser=Anonymous
fullName={0} {1} fullName={0} {1}
allFieldsRequired=All fields are required.
selectLocale=Select a locale selectLocale=Select a locale
doSignIn=Sign In doSignIn=Sign in
# Device Activity Page backToAdminConsole=Back to admin console
signedInDevices=Signed In Devices accountManagementWelcomeMessage=Welcome to Keycloak account management
signedInDevicesExplanation=Sign out any device that is unfamiliar.
# Personal info page
personalInfoHtmlTitle=Personal info
# Device activity page
signedInDevices=Signed in devices
signedInDevicesExplanation=Sign out of any unfamiliar devices.
signOutWarning=Sign out the session? signOutWarning=Sign out the session?
signOutAllDevices=Sign Out All Devices signOutAllDevices=Sign out all devices
signOutAllDevicesWarning=This action will sign out all the devices that have signed in to your account, including the current device you are using. signOutAllDevicesWarning=This action will sign out all the devices that have signed in to your account, including the current device you are using.
recentlyUsedDevices=Recently Used Devices recentlyUsedDevices=Recently used devices
recentlyUsedDevicesExplanation=Devices used in the last month, but not currently logged in. recentlyUsedDevicesExplanation=Devices used in the last month, but not currently logged in.
lastAccess=Last Access lastAccess=Last access
unknownOperatingSystem=Unknown Operating System unknownOperatingSystem=Unknown operating system
currentDevice=Current Device currentDevice=Current device
currentSession=Current Session currentSession=Current session
signedOutSession=Signed out {0}/{1} signedOutSession=Signed out {0}/{1}
lastAccessedOn=Last accessed on lastAccessedOn=Last accessed
clients=Clients clients=Clients
startedAt=Started at started=Started
expiresAt=Expires at expires=Expires
ipAddress=IP Address ipAddress=IP address
# Resources Page # Resources page
resourceName=Resource Name resourceName=Resource name
nextPage=Next nextPage=Next
previousPage=Previous previousPage=Previous
firstPage=First Page firstPage=First page
resourceSharedWith=Resource is shared with {0} resourceSharedWith=Resource is shared with {0}
and=\ and {0} other users and=\ and {0} other users
add=Add add=Add
share=Share share=Share
shareWith=Share with
edit=Edit edit=Edit
close=Close close=Close
unShare=Unshare all unShare=Unshare all
@ -57,75 +66,81 @@ resourceAlreadyShared=Resource is already shared with this user.
resourceNotShared=This resource is not shared. resourceNotShared=This resource is not shared.
permissionRequests=Permission requests permissionRequests=Permission requests
permissions=Permissions permissions=Permissions
selectPermissions=Select the permissions
unShareAllConfirm=Are you sure you want to completely remove all shares? unShareAllConfirm=Are you sure you want to completely remove all shares?
userNotFound=No user found with name or email {0} userNotFound=No user found with name or email {0}
# Linked Accounts Page # Linked accounts page
linkedAccountsTitle=Linked Accounts linkedAccountsTitle=Linked accounts
linkedAccountsIntroMessage=Manage logins through third-party accounts. linkedAccountsIntroMessage=Manage logins through third-party accounts.
linkedLoginProviders=Linked Login Providers linkedLoginProviders=Linked login providers
unlinkedLoginProviders=Unlinked Login Providers unlinkedLoginProviders=Unlinked login providers
linkedEmpty=No Linked Providers linkedEmpty=No linked providers
unlinkedEmpty=No Unlinked Providers unlinkedEmpty=No unlinked providers
socialLogin=Social Login socialLogin=Social login
systemDefined=System Defined systemDefined=System defined
link=Link Account link=Link account
unLink=Unlink Account unLink=Unlink account
# Signing In Page # Signing in page
signingIn=Signing In signingIn=Signing in
signingInSubMessage=Configure ways to sign in. signingInSubMessage=Configure ways to sign in.
credentialCreatedAt=Created credentialCreatedAt=Created
successRemovedMessage={0} was removed. successRemovedMessage={0} was removed.
stopUsingCred=Stop using {0}? stopUsingCred=Stop using {0}?
changePassword=Change password
removeCred=Remove {0} removeCred=Remove {0}
setUpNew=Set up {0} setUpNew=Set up {0}
removeCredAriaLabel=Remove credential
updateCredAriaLabel=Update credential
notSetUp={0} is not set up. notSetUp={0} is not set up.
two-factor=Two-Factor Authentication two-factor=Two-factor authentication
passwordless=Passwordless passwordless=Passwordless
unknown=Unknown unknown=Unknown
password-display-name=Password password-display-name=Password
password-help-text=Log in by entering your password. password-help-text=Log in by entering your password.
password=My Password password=My password
otp-display-name=Authenticator Application otp-display-name=authenticator application
otp-help-text=Enter a verification code from authenticator application. otp-help-text=Enter a verification code from authenticator application.
recovery-authn-code=My Recovery Authentication Codes recovery-authn-code=My recovery authentication codes
recovery-authn-codes-display-name=Recovery Authentication Codes recovery-authn-codes-display-name=Recovery authentication codes
recovery-authn-codes-help-text=These codes can be used to regain your access in case your other 2FA means are not available. recovery-authn-codes-help-text=These codes can be used to regain your access in case your other 2FA means are not available.
recovery-codes-number-used={0} recovery codes used recovery-codes-number-used={0} recovery codes used
recovery-codes-number-remaining={0} recovery codes remaining recovery-codes-number-remaining={0} recovery codes remaining
recovery-codes-generate-new-codes=Generate new codes to ensure access to your account recovery-codes-generate-new-codes=Generate new codes to ensure access to your account
webauthn-display-name=Security Key webauthn-display-name=Security key
webauthn-help-text=Use your security key to sign in. webauthn-help-text=Use your security key to log in.
webauthn-passwordless-display-name=Security Key webauthn-passwordless-display-name=Security key
webauthn-passwordless-help-text=Use your security key for passwordless sign in. webauthn-passwordless-help-text=Use your security key for passwordless log in.
basic-authentication=Basic Authentication basic-authentication=Basic authentication
invalidRequestMessage=Invalid Request invalidRequestMessage=Invalid request
# Applications page # Applications page
applicationsPageTitle=Applications applicationsPageTitle=Applications
internalApp=Internal internalApp=Internal
thirdPartyApp=Third-party thirdPartyApp=Third-party
offlineAccess=Offline Access offlineAccess=Offline access
inUse=In use inUse=In use
notInUse=Not in use notInUse=Not in use
applicationDetails=Application Details applicationDetails=Application details
client=Client client=Client
description=Description description=Description
baseUrl=URL baseUrl=URL
accessGrantedOn=Access granted on accessGrantedOn=Access granted on
removeButton=Remove access removeButton=Remove access
removeModalTitle=Remove Access removeModalTitle=Remove access
removeModalMessage=This will remove the currently granted access permission for {0}. You will need to grant access again if you want to use this app. removeModalMessage=This will remove the currently granted access permission for {0}. You will need to grant access again if you want to use this app.
confirmButton=Confirm confirmButton=Confirm
infoMessage=By clicking 'Remove Access', you will remove granted permissions of this application. This application will no longer use your information. infoMessage=By clicking 'Remove Access', you will remove granted permissions of this application. This application will no longer use your information.
termsOfService=Terms of service termsOfService=Terms of service
policy=Privacy policy policy=Privacy policy
applicationType=Application type
status=Status
#Delete Account page #Delete account page
doDelete=Delete doDelete=Delete
deleteAccountSummary=Deleting your account will erase all your data and log you out immediately. deleteAccountSummary=Deleting your account will erase all your data and log you out immediately.
deleteAccount=Delete Account deleteAccount=Delete account
deleteAccountWarning=This is irreversible. All your data will be permanently destroyed, and irretrievable. deleteAccountWarning=This is irreversible. All your data will be permanently destroyed, and irretrievable.
error-invalid-value=''{0}'' has invalid value. error-invalid-value=''{0}'' has invalid value.

View file

@ -3,7 +3,7 @@
"id": "personal-info", "id": "personal-info",
"path": "personal-info", "path": "personal-info",
"icon": "pf-icon-user", "icon": "pf-icon-user",
"label": "personalInfoHtmlTitle", "label": "personalInfoSidebarTitle",
"descriptionLabel": "personalInfoIntroMessage", "descriptionLabel": "personalInfoIntroMessage",
"modulePath": "/content/account-page/AccountPage.js", "modulePath": "/content/account-page/AccountPage.js",
"componentName": "AccountPage" "componentName": "AccountPage"
@ -11,27 +11,27 @@
{ {
"id": "security", "id": "security",
"icon": "pf-icon-security", "icon": "pf-icon-security",
"label": "accountSecurityTitle", "label": "accountSecuritySidebarTitle",
"descriptionLabel": "accountSecurityIntroMessage", "descriptionLabel": "accountSecurityIntroMessage",
"content": [ "content": [
{ {
"id": "signingin", "id": "signingin",
"path": "security/signingin", "path": "security/signingin",
"label": "signingIn", "label": "signingInSidebarTitle",
"modulePath": "/content/signingin-page/SigningInPage.js", "modulePath": "/content/signingin-page/SigningInPage.js",
"componentName": "SigningInPage" "componentName": "SigningInPage"
}, },
{ {
"id": "device-activity", "id": "device-activity",
"path": "security/device-activity", "path": "security/device-activity",
"label": "device-activity", "label": "deviceActivitySidebarTitle",
"modulePath": "/content/device-activity-page/DeviceActivityPage.js", "modulePath": "/content/device-activity-page/DeviceActivityPage.js",
"componentName": "DeviceActivityPage" "componentName": "DeviceActivityPage"
}, },
{ {
"id": "linked-accounts", "id": "linked-accounts",
"path": "security/linked-accounts", "path": "security/linked-accounts",
"label": "linkedAccountsHtmlTitle", "label": "linkedAccountsSidebarTitle",
"modulePath": "/content/linked-accounts-page/LinkedAccountsPage.js", "modulePath": "/content/linked-accounts-page/LinkedAccountsPage.js",
"componentName": "LinkedAccountsPage", "componentName": "LinkedAccountsPage",
"hidden": "!features.isLinkedAccountsEnabled" "hidden": "!features.isLinkedAccountsEnabled"

View file

@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<style>
.faac274b-9d49-4ba1-9ef0-610572d38128 {
fill: #1877f2;
}
.f55f6e4a-14c2-4793-a1d5-db4b028479c8 {
fill: #fff;
}
</style>
</defs>
<g id="b36e017a-ce19-4905-9b48-48066e87bbf5" data-name="New stuff">
<g>
<rect class="faac274b-9d49-4ba1-9ef0-610572d38128" width="32" height="32" rx="1.19"/>
<path class="f55f6e4a-14c2-4793-a1d5-db4b028479c8" d="M26.93,16.07a10.93,10.93,0,1,0-12.64,10.8V19.23H11.52V16.07h2.77V13.66c0-2.74,1.63-4.26,4.13-4.26a16.32,16.32,0,0,1,2.45.22v2.69H19.49A1.58,1.58,0,0,0,17.71,14v2.05h3l-.48,3.16H17.71v7.64a10.94,10.94,0,0,0,9.22-10.8Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 730 B

View file

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<style>
.b42551c7-a511-4544-a371-f6e3883f7abd {
fill: #fff;
fill-rule: evenodd;
}
</style>
</defs>
<g id="f00b56ab-1ecb-4d94-b704-cf429e3c78a4" data-name="GitHub">
<g>
<rect width="32" height="32" rx="1.19"/>
<path class="b42551c7-a511-4544-a371-f6e3883f7abd" d="M16,5.13a11.06,11.06,0,0,0-3.5,21.55c.56.1.76-.24.76-.53s0-1,0-1.88c-3.07.67-3.72-1.48-3.72-1.48a2.91,2.91,0,0,0-1.23-1.62c-1-.69.08-.67.08-.67a2.32,2.32,0,0,1,1.69,1.14,2.36,2.36,0,0,0,3.22.92,2.36,2.36,0,0,1,.7-1.48c-2.45-.28-5-1.23-5-5.47a4.29,4.29,0,0,1,1.13-3,4,4,0,0,1,.11-2.93s.93-.3,3,1.13a10.55,10.55,0,0,1,5.54,0c2.11-1.43,3-1.13,3-1.13a4,4,0,0,1,.11,2.93,4.25,4.25,0,0,1,1.13,3c0,4.25-2.58,5.18-5.05,5.46a2.62,2.62,0,0,1,.75,2.05c0,1.47,0,2.67,0,3s.2.64.76.53A11.06,11.06,0,0,0,16,5.13Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 912 B

View file

@ -0,0 +1,77 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32">
<defs>
<style>
.e5ef51ce-3313-4c04-b152-23646fd2b306 {
fill: none;
clip-rule: evenodd;
}
.ef16f3ef-fb03-4a62-9cce-d65767c17c4c {
fill: #ededed;
}
.e0f4cb03-a97e-447b-8c37-60d897bac484 {
clip-path: url(#fe0cecb0-916b-4b9d-828f-63ec94884d2e);
}
.afb39849-fc2d-48aa-a4a4-070e36b84e51 {
fill: #3e82f1;
}
.a2ee00a5-b156-4157-adc5-b40fd1ee5ebd {
clip-path: url(#e022c77c-5a12-4caf-aaed-c796166a8562);
}
.a025e47e-f997-4bd7-8964-88ada0c7c12f {
fill: #32a753;
}
.b4e7c98a-856b-4280-8aaa-b9f157cf22ea {
clip-path: url(#b1e1e904-5b74-4ac6-8dc5-11a864f7f8ec);
}
.b0bf74a2-e960-4e68-948c-0e3dfb1bcd2a {
fill: #f9bb00;
}
.ba55bfd1-dca2-4617-b45c-128fdf3bf3b4 {
clip-path: url(#bcdf0102-4e91-4218-8945-e2893d757d6d);
}
.fae83dfa-2d64-4e2a-9cd9-b00b32869370 {
fill: #e74235;
}
</style>
<clipPath id="fe0cecb0-916b-4b9d-828f-63ec94884d2e">
<path class="e5ef51ce-3313-4c04-b152-23646fd2b306" d="M25.85,16.23a12.53,12.53,0,0,0-.18-2.06H16.2v3.89h5.41a4.58,4.58,0,0,1-2,3v2.53h3.25a9.81,9.81,0,0,0,3-7.39Zm0,0"/>
</clipPath>
<clipPath id="e022c77c-5a12-4caf-aaed-c796166a8562">
<path class="e5ef51ce-3313-4c04-b152-23646fd2b306" d="M16.2,26.05a9.61,9.61,0,0,0,6.65-2.43L19.6,21.09a6.06,6.06,0,0,1-9-3.18H7.22v2.6a10,10,0,0,0,9,5.54Zm0,0"/>
</clipPath>
<clipPath id="b1e1e904-5b74-4ac6-8dc5-11a864f7f8ec">
<path class="e5ef51ce-3313-4c04-b152-23646fd2b306" d="M10.58,17.91a5.86,5.86,0,0,1,0-3.82v-2.6H7.22a10,10,0,0,0,0,9l3.36-2.6Zm0,0"/>
</clipPath>
<clipPath id="bcdf0102-4e91-4218-8945-e2893d757d6d">
<path class="e5ef51ce-3313-4c04-b152-23646fd2b306" d="M16.2,10A5.39,5.39,0,0,1,20,11.45l2.89-2.88A9.7,9.7,0,0,0,16.2,6a10,10,0,0,0-9,5.54l3.36,2.6A6,6,0,0,1,16.2,10Zm0,0"/>
</clipPath>
</defs>
<g id="a06b7517-c1f4-49d5-aa34-cee2287c4769" data-name="Google">
<g id="fbe0f094-aaab-4b03-a7bf-452a2c250397" data-name="Full color">
<rect class="ef16f3ef-fb03-4a62-9cce-d65767c17c4c" width="32" height="32" rx="1.19"/>
<g>
<g class="e0f4cb03-a97e-447b-8c37-60d897bac484">
<rect class="afb39849-fc2d-48aa-a4a4-070e36b84e51" x="10.62" y="8.59" width="20.81" height="20.61"/>
</g>
<g class="a2ee00a5-b156-4157-adc5-b40fd1ee5ebd">
<rect class="a025e47e-f997-4bd7-8964-88ada0c7c12f" x="1.64" y="12.33" width="26.8" height="19.31"/>
</g>
<g class="b4e7c98a-856b-4280-8aaa-b9f157cf22ea">
<rect class="b0bf74a2-e960-4e68-948c-0e3dfb1bcd2a" x="0.57" y="5.9" width="15.59" height="20.19"/>
</g>
<g class="ba55bfd1-dca2-4617-b45c-128fdf3bf3b4">
<rect class="fae83dfa-2d64-4e2a-9cd9-b00b32869370" x="1.64" y="0.37" width="26.87" height="19.31"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<style>
.ac4e1fda-441f-45c3-89b5-eabc007f54ca {
fill: #e1306c;
}
.a7c172c4-b213-4581-937c-bb042fd94b3e {
fill: #fff;
}
</style>
</defs>
<g id="a864479b-0561-4117-aa66-744aa86c5250" data-name="Instagram">
<g id="bb9ebb9e-2d15-4e72-a42a-7eff89494a26" data-name="Black and white">
<rect class="ac4e1fda-441f-45c3-89b5-eabc007f54ca" width="32" height="32" rx="1.19"/>
</g>
<g>
<path class="a7c172c4-b213-4581-937c-bb042fd94b3e" d="M16,6.09c-2.69,0-3,0-4.09.06a7.18,7.18,0,0,0-2.4.46,5,5,0,0,0-2.9,2.9,7.18,7.18,0,0,0-.46,2.4C6.1,13,6.09,13.31,6.09,16s0,3,.06,4.09a7.18,7.18,0,0,0,.46,2.4,5,5,0,0,0,2.9,2.9,7.18,7.18,0,0,0,2.4.46c1.06,0,1.4.06,4.09.06s3,0,4.09-.06a7.18,7.18,0,0,0,2.4-.46,5,5,0,0,0,2.9-2.9,7.18,7.18,0,0,0,.46-2.4c0-1.06.06-1.4.06-4.09s0-3-.06-4.09a7.18,7.18,0,0,0-.46-2.4,5,5,0,0,0-2.9-2.9,7.18,7.18,0,0,0-2.4-.46C19,6.1,18.69,6.09,16,6.09Zm0,1.79c2.65,0,3,0,4,0a5.73,5.73,0,0,1,1.84.34A3,3,0,0,1,23,9a3,3,0,0,1,.75,1.14A5.73,5.73,0,0,1,24.07,12c0,1,.05,1.35.05,4s0,3-.05,4a5.73,5.73,0,0,1-.34,1.84,3.38,3.38,0,0,1-1.89,1.89,5.73,5.73,0,0,1-1.84.34c-1,0-1.35.05-4,.05s-3,0-4-.05a5.73,5.73,0,0,1-1.84-.34A3,3,0,0,1,9,23a3,3,0,0,1-.75-1.14A5.73,5.73,0,0,1,7.93,20c0-1,0-1.35,0-4s0-3,0-4a5.73,5.73,0,0,1,.34-1.84A3,3,0,0,1,9,9a3,3,0,0,1,1.14-.75A5.73,5.73,0,0,1,12,7.93c1,0,1.35,0,4,0"/>
<path class="a7c172c4-b213-4581-937c-bb042fd94b3e" d="M16,19.3A3.3,3.3,0,1,1,19.3,16,3.3,3.3,0,0,1,16,19.3Zm0-8.39A5.09,5.09,0,1,0,21.09,16,5.09,5.09,0,0,0,16,10.91Z"/>
<path class="a7c172c4-b213-4581-937c-bb042fd94b3e" d="M22.48,10.71a1.19,1.19,0,1,1-1.19-1.19,1.19,1.19,0,0,1,1.19,1.19Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<style>
.a975829d-e85a-4fdc-b46a-16eba47b4e8e {
fill: #2867b2;
}
.b17912fc-cab5-4688-935b-8aa15b250003 {
fill: #fff;
}
</style>
</defs>
<g id="adf29cd2-f5a2-4433-a836-7d0b8cd64c5d" data-name="LinkedIn">
<g>
<rect class="a975829d-e85a-4fdc-b46a-16eba47b4e8e" width="32" height="32" rx="1.19"/>
<g>
<polygon class="b17912fc-cab5-4688-935b-8aa15b250003" points="10.47 25.5 6.46 25.5 6.46 12.59 10.47 12.59 10.47 25.5 10.47 25.5"/>
<path class="b17912fc-cab5-4688-935b-8aa15b250003" d="M8.46,10.83A2.33,2.33,0,1,1,10.79,8.5a2.33,2.33,0,0,1-2.33,2.33Z"/>
<path class="b17912fc-cab5-4688-935b-8aa15b250003" d="M25.5,25.5h-4V19.22c0-1.49,0-3.42-2.09-3.42S17,17.43,17,19.11V25.5H13V12.59h3.84v1.76h.06a4.21,4.21,0,0,1,3.8-2.08c4.06,0,4.81,2.67,4.81,6.15V25.5Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 951 B

View file

@ -0,0 +1,34 @@
<svg id="bf5a9f86-a166-4609-aea2-c789db71fd48" data-name="Microsoft" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<style>
.fe82c64e-6d2d-42d3-aeec-ae7ea0741e78 {
fill: #ededed;
}
.be49cf2d-e5c3-4773-b2c7-b422380b776a {
fill: #7fba00;
}
.a9f66f41-846e-499a-af6d-5503813bb401 {
fill: #ffb900;
}
.a8084a6b-0b46-4437-8e12-638c29ccbea1 {
fill: #f25022;
}
.bb872b1d-e833-4ba3-99af-3b105ed602a0 {
fill: #00a4ef;
}
</style>
</defs>
<g id="e0b7e64b-db07-46e0-9f19-72b142175f6b" data-name="Full color">
<rect class="fe82c64e-6d2d-42d3-aeec-ae7ea0741e78" width="32" height="32" rx="1.19"/>
<g>
<rect class="be49cf2d-e5c3-4773-b2c7-b422380b776a" x="16.47" y="6.5" width="9.03" height="9.03" rx="0.1"/>
<rect class="a9f66f41-846e-499a-af6d-5503813bb401" x="16.47" y="16.47" width="9.03" height="9.03" rx="0.1"/>
<rect class="a8084a6b-0b46-4437-8e12-638c29ccbea1" x="6.5" y="6.5" width="9.03" height="9.03" rx="0.1"/>
<rect class="bb872b1d-e833-4ba3-99af-3b105ed602a0" x="6.5" y="16.47" width="9.03" height="9.03" rx="0.1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.321739,0,0,0.321739,-0.234299,0.0972928)">
<path d="M64.38,23.5C67.556,24.99 70.436,27.044 72.88,29.56L88.62,23.83C80.176,12.044 66.537,5.037 52.039,5.037C27.353,5.037 7.039,25.351 7.039,50.037C7.039,51.183 7.083,52.328 7.17,53.47L22.91,47.74C24.033,32.597 36.806,20.737 51.99,20.737C56.273,20.737 60.503,21.68 64.38,23.5" style="fill:rgb(238,0,0);fill-rule:nonzero;"/>
<path d="M16,58.19L1,63.63C2.371,69.099 4.744,74.267 8,78.87L23.7,73.16C19.696,69.029 16.99,63.813 15.92,58.16" style="fill:rgb(238,0,0);fill-rule:nonzero;"/>
<path d="M81.16,52.25C80.904,55.75 80.009,59.173 78.52,62.35C71.736,76.878 54.205,83.258 39.67,76.49C36.487,75.027 33.599,72.994 31.15,70.49L15.45,76.21C23.88,88.016 37.524,95.039 52.032,95.039C69.504,95.039 85.459,84.851 92.81,69C96.075,61.982 97.454,54.233 96.81,46.52L81.16,52.25Z" style="fill:rgb(238,0,0);fill-rule:nonzero;"/>
<path d="M85,33L70,38.45C72.851,43.476 74.153,49.236 73.74,55L89.44,49.29C88.992,43.63 87.486,38.104 85,33" style="fill:rgb(238,0,0);fill-rule:nonzero;"/>
<path d="M29.46,45.36L13.72,51.09C13.94,53.604 14.368,56.096 15,58.54L30,53.1C29.501,50.552 29.346,47.949 29.54,45.36" style="fill:rgb(204,0,0);fill-rule:nonzero;"/>
<path d="M99,28C97.903,25.724 96.619,23.543 95.16,21.48L79.43,27.18C81.238,29.04 82.791,31.132 84.05,33.4L99,28Z" style="fill:rgb(204,0,0);fill-rule:nonzero;"/>
<path d="M15.45,76.17C16.671,77.879 18.007,79.503 19.45,81.03L36.54,74.79C34.578,73.561 32.769,72.105 31.15,70.45L15.45,76.17ZM96.86,46.54L81.16,52.25C80.985,54.545 80.539,56.81 79.83,59L96.92,52.76C97.04,50.684 97.02,48.603 96.86,46.53" style="fill:rgb(204,0,0);fill-rule:nonzero;"/>
<path d="M29.4,48.52C29.36,47.466 29.38,46.411 29.46,45.36L13.72,51.09C13.8,52.09 13.93,53.09 14.08,54.09L29.4,48.52Z" style="fill:rgb(163,0,0);fill-rule:nonzero;"/>
<path d="M96.72,23.82C96.22,23.01 95.72,22.22 95.16,21.44L79.43,27.18C80.116,27.884 80.764,28.626 81.37,29.4L96.72,23.82Z" style="fill:rgb(163,0,0);fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<style>
.f425009f-d653-4243-b52f-e6e7efdbf57a {
fill: #4d4d4d;
}
.bac6a0a4-fc2d-4f36-9ded-667acc6f4b52 {
fill: #bcbbbb;
}
.b9d59bf2-7616-43cd-a672-04b81955bad9 {
fill: #f48024;
}
</style>
</defs>
<g id="b393dd85-54a9-4370-beab-fda62d40e5d8" data-name="StackOverflow">
<g id="beef34ed-ad1e-45a9-8dc6-781d71c5dd19" data-name="Full color">
<rect class="f425009f-d653-4243-b52f-e6e7efdbf57a" width="32" height="32" rx="1.19"/>
<g>
<polygon class="bac6a0a4-fc2d-4f36-9ded-667acc6f4b52" points="21.29 23.44 21.29 18.82 23.34 18.82 23.34 25.5 7.94 25.5 7.94 18.82 9.99 18.82 9.99 23.44 21.29 23.44"/>
<path class="b9d59bf2-7616-43cd-a672-04b81955bad9" d="M11.53,21.9h8.22V20.36H11.53ZM19,6.5l-1.39,1,5.08,6.83,1.39-1Zm-4.21,4L21.29,16l1.08-1.28L15.85,9.22Zm-2.16,3.8,7.7,3.59L21,16.36l-7.7-3.59Zm7.18,5.53.36-1.51-8.27-1.72-.35,1.7Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<style>
.b48eca1a-55ea-4187-8747-2a0a7c0905ff {
fill: #1da1f2;
}
.a0fa0208-710f-4171-b935-86383a49537b {
fill: #fff;
}
</style>
</defs>
<g id="e43e3b03-e423-44e8-9d4d-c8c1f8f88064" data-name="Twitter">
<g>
<rect class="b48eca1a-55ea-4187-8747-2a0a7c0905ff" width="32" height="32" rx="1.19"/>
<path class="a0fa0208-710f-4171-b935-86383a49537b" d="M24.42,11.5c0,.18,0,.37,0,.56A12.25,12.25,0,0,1,5.58,22.37,8.72,8.72,0,0,0,12,20.59a4.3,4.3,0,0,1-4-3,4.28,4.28,0,0,0,.81.07,4.22,4.22,0,0,0,1.13-.15A4.29,4.29,0,0,1,6.43,13.3v-.05a4.4,4.4,0,0,0,2,.54A4.31,4.31,0,0,1,7,8a12.26,12.26,0,0,0,8.88,4.5,4,4,0,0,1-.11-1,4.3,4.3,0,0,1,7.44-3,8.59,8.59,0,0,0,2.73-1A4.31,4.31,0,0,1,24.09,10a8.86,8.86,0,0,0,2.47-.68,8.72,8.72,0,0,1-2.14,2.23Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 895 B

View file

@ -1,3 +1,4 @@
/* Globals */
.brand { .brand {
height: 35px; height: 35px;
} }
@ -12,5 +13,114 @@
width: 120px; width: 120px;
height: 50px; height: 50px;
} }
}
/* Linked Accounts screen */
.idp-icon-social {
width: 32px;
height: 32px;
}
#github-idp-icon-social {
background-image: url(../img/socialmedia/socialmedia_icons_github_transparent.svg);
}
#linkedin-idp-icon-social {
background-image: url(../img/socialmedia/socialmedia_icons_linkedin_transparent.svg);
}
#facebook-idp-icon-social {
background-image: url(../img/socialmedia/socialmedia_icons_facebook_transparent.svg);
}
#google-idp-icon-social {
background-image: url(../img/socialmedia/socialmedia_icons_google_transparent.svg);
}
#microsoft-idp-icon-social {
background-image: url(../img/socialmedia/socialmedia_icons_microsoft_transparent.svg);
}
#instagram-idp-icon-social {
background-image: url(../img/socialmedia/socialmedia_icons_instagram_transparent.svg);
}
#stackoverflow-idp-icon-social {
background-image: url(../img/socialmedia/socialmedia_icons_stack_transparent.svg);
}
#twitter-idp-icon-social {
background-image: url(../img/socialmedia/socialmedia_icons_twitter_transparent.svg);
}
#openshift-idp-icon-social {
background-image: url(../img/socialmedia/socialmedia_icons_openshift_transparent.svg);
}
/* Account Page screen */
.personal-info-form .pf-c-form__group-control {
max-width: 600px;
}
/* Device Activity screen */
.signed-in-device-list .pf-c-data-list__item-row {
--pf-c-data-list__item-row--PaddingRight: 0;
--pf-c-data-list__item-row--PaddingLeft: 0;
}
.signed-in-device-list .pf-c-data-list__expandable-content-body {
--pf-c-data-list__expandable-content-body--PaddingRight: 0;
}
.signed-in-device-grid {
grid-template-columns: auto repeat(11, [col-start] 1fr);
}
.signed-in-device-list.pf-c-data-list {
--pf-c-data-list--sm--BorderTopWidth: 0;
}
.pf-c-data-list__item {
--pf-c-data-list__item--BorderBottomWidth: 1px;
--pf-c-data-list__item--BorderBottomColor: var(--pf-global--BorderColor--100);
}
.signed-in-device-list.pf-c-data-list {
--pf-c-data-list--BorderTopWidth: 0;
}
@media (min-width: 576px) {
.pf-c-data-list__item {
position: relative;
display: flex;
flex-direction: column;
background-color: var(--pf-c-data-list__item--BackgroundColor);
border-bottom: var(--pf-c-data-list__item--BorderBottomWidth) solid var(--pf-c-data-list__item--BorderBottomColor);
}
.signed-in-device-list.pf-c-data-list {
--pf-c-data-list--BorderTopWidth: 0;
}
}
@media (min-width: 768px) {
.signed-in-device-list .pf-c-description-list {
--pf-c-description-list--GridTemplateColumns--count: 5;
}
}
/* Signing in screen */
.title-case:first-letter,
.cred-title:first-letter,
#otp-not-set-up .pf-c-empty-state__body:first-letter {
text-transform: capitalize
}
/* Applications screen */
#applications-list-header .pf-c-data-list__item-content {
--pf-c-data-list__item-content--md--PaddingBottom: 1rem;
}
.pf-u-pl-35xl {
padding-left: 4.5rem;
} }

View file

@ -0,0 +1 @@
v16.13.0

View file

@ -19,15 +19,14 @@ import * as React from 'react';
import {KeycloakService} from './keycloak-service/keycloak.service'; import {KeycloakService} from './keycloak-service/keycloak.service';
import {PageNav} from './PageNav'; import {PageNav} from './PageNav';
import {PageToolbar} from './PageToolbar'; import {PageHeaderTool} from './PageHeaderTool';
import {makeRoutes} from './ContentPages'; import {makeRoutes} from './ContentPages';
import { import {
Brand, Brand,
Page, Page,
PageHeader, PageHeader,
PageSection, PageSidebar
PageSidebar,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { KeycloakContext } from './keycloak-service/KeycloakContext'; import { KeycloakContext } from './keycloak-service/KeycloakContext';
@ -61,11 +60,11 @@ export class App extends React.Component<AppProps> {
const username = ( const username = (
<span style={{marginLeft: '10px'}} id="loggedInUser">{loggedInUserName()}</span> <span style={{marginLeft: '10px'}} id="loggedInUser">{loggedInUserName()}</span>
); );
const Header = ( const Header = (
<PageHeader <PageHeader
logo={<a id="brandLink" href={brandUrl}><Brand src={brandImg} alt="Logo" className="brand"/></a>} logo={<a id="brandLink" href={brandUrl}><Brand src={brandImg} alt="Logo" className="brand"/></a>}
toolbar={<PageToolbar/>} headerTools={<PageHeaderTool/>}
avatar={username}
showNavToggle showNavToggle
/> />
); );
@ -73,13 +72,9 @@ export class App extends React.Component<AppProps> {
const Sidebar = <PageSidebar nav={<PageNav/>} />; const Sidebar = <PageSidebar nav={<PageNav/>} />;
return ( return (
<span style={{ height: '100%'}}>
<Page header={Header} sidebar={Sidebar} isManagedSidebar> <Page header={Header} sidebar={Sidebar} isManagedSidebar>
<PageSection>
{makeRoutes()} {makeRoutes()}
</PageSection>
</Page> </Page>
</span>
); );
} }
}; };

View file

@ -85,7 +85,8 @@ function createNavItems(activePage: PageDef, contentParam: ContentItem[], groupN
groupId={item.groupId} groupId={item.groupId}
key={item.groupId} key={item.groupId}
title={Msg.localize(item.label, item.labelParams)} title={Msg.localize(item.label, item.labelParams)}
isExpanded={isChildOf(item, activePage)}> isExpanded={isChildOf(item, activePage)}
>
{createNavItems(activePage, item.content, groupNum + 1)} {createNavItems(activePage, item.content, groupNum + 1)}
</NavExpandable> </NavExpandable>
} else { } else {

View file

@ -0,0 +1,27 @@
import * as React from 'react';
import {PageHeaderTools} from '@patternfly/react-core';
import {ReferrerLink} from './widgets/ReferrerLink';
import {LogoutButton} from './widgets/Logout';
declare const referrerName: string;
export class PageHeaderTool extends React.Component {
private hasReferrer: boolean = typeof referrerName !== 'undefined';
public render(): React.ReactNode {
return (
<PageHeaderTools>
{this.hasReferrer &&
<div className="pf-c-page__header-tools-group">
<ReferrerLink/>
</div>
}
<div className="pf-c-page__header-tools-group">
<LogoutButton/>
</div>
</PageHeaderTools>
);
}
}

View file

@ -46,39 +46,20 @@ export class PageToolbar extends React.Component<PageToolbarProps, PageToolbarSt
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
const kebabDropdownItems = [];
if (this.hasReferrer) {
kebabDropdownItems.push(
<ReferrerDropdownItem key='referrerDropdownItem'/>
)
}
kebabDropdownItems.push(<LogoutDropdownItem key='LogoutDropdownItem'/>);
return ( return (
<Toolbar> <Toolbar>
{this.hasReferrer && {this.hasReferrer &&
<ToolbarGroup key='referrerGroup'> <ToolbarGroup key='referrerGroup' alignment={{default:"alignRight"}}>
<ToolbarItem className="pf-m-icons" key='referrer'> <ToolbarItem className="pf-m-icons" key='referrer'>
<ReferrerLink/> <ReferrerLink/>
</ToolbarItem> </ToolbarItem>
</ToolbarGroup> </ToolbarGroup>
} }
<ToolbarGroup key='secondGroup'> <ToolbarGroup key='secondGroup' alignment={{default:"alignRight"}}>
<ToolbarItem className="pf-m-icons" key='logout'> <ToolbarItem className="pf-m-icons" key='logout'>
<LogoutButton/> <LogoutButton/>
</ToolbarItem> </ToolbarItem>
<ToolbarItem key='kebab' className="pf-m-mobile">
<Dropdown
isPlain
position="right"
toggle={<KebabToggle id="mobileKebab" onToggle={this.onKebabDropdownToggle} />}
isOpen={this.state.isKebabDropdownOpen}
dropdownItems={kebabDropdownItems}
/>
</ToolbarItem>
</ToolbarGroup> </ToolbarGroup>
</Toolbar> </Toolbar>
); );

View file

@ -97,7 +97,7 @@ export class ContentAlert extends React.Component<ContentAlertProps, ContentAler
isLiveRegion isLiveRegion
variant={variant} variant={variant}
title={message} title={message}
action={ actionClose={
<AlertActionCloseButton <AlertActionCloseButton
title={message} title={message}
variantLabel={`${variant} alert`} variantLabel={`${variant} alert`}

View file

@ -15,8 +15,8 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import {Button, Grid, GridItem, Title, Tooltip} from '@patternfly/react-core'; import {Button, Grid, GridItem, Text, Title, Tooltip, Card, CardBody, Stack, StackItem, PageSection, TextContent, PageSectionVariants, SplitItem, Split} from '@patternfly/react-core';
import {RedoIcon} from '@patternfly/react-icons'; import {RedoIcon, SyncAltIcon} from '@patternfly/react-icons';
import {Msg} from '../widgets/Msg'; import {Msg} from '../widgets/Msg';
import {ContentAlert} from './ContentAlert'; import {ContentAlert} from './ContentAlert';
@ -41,25 +41,39 @@ export class ContentPage extends React.Component<ContentPageProps> {
return ( return (
<React.Fragment> <React.Fragment>
<ContentAlert /> <ContentAlert />
<section id="page-heading" className="pf-c-page__main-section pf-m-light">
<Grid> <PageSection variant={PageSectionVariants.light} className="pf-u-pb-xs">
<GridItem span={11}><Title headingLevel='h1' size='3xl'><strong><Msg msgKey={this.props.title}/></strong></Title></GridItem> <Split>
{this.props.onRefresh && <SplitItem isFilled>
<GridItem span={1}> <TextContent>
<Tooltip content={<Msg msgKey='refreshPage'/>}> <Title headingLevel="h1" size="2xl" className="pf-u-mb-xl">
<Button aria-describedby="refresh page" id='refresh-page' variant='plain' onClick={this.props.onRefresh}> <Msg msgKey={this.props.title} />
<RedoIcon size='sm'/> </Title>
{this.props.introMessage && (
<Text component="p">
<Msg msgKey={this.props.introMessage} />
</Text>
)}
</TextContent>
</SplitItem>
{this.props.onRefresh && (
<SplitItem>
<Tooltip content={<Msg msgKey="refreshPage" />}>
<Button
aria-label={Msg.localize('refreshPage')}
id="refresh-page"
variant="link"
onClick={this.props.onRefresh}
icon={<SyncAltIcon />}
>
<Msg msgKey="refresh" />
</Button> </Button>
</Tooltip> </Tooltip>
</GridItem> </SplitItem>
} )}
{this.props.introMessage && <GridItem span={12}> <Msg msgKey={this.props.introMessage}/></GridItem>} </Split>
</Grid> </PageSection>
</section>
<section className="pf-c-page__main-section pf-m-no-padding-mobile">
{this.props.children} {this.props.children}
</section>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -14,7 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
import * as React from 'react'; import * as React from 'react';
import { ActionGroup, Button, Form, FormGroup, TextInput, Grid, GridItem, Expandable} from '@patternfly/react-core'; import { ActionGroup,
Button,
Form,
FormGroup,
TextInput,
Grid,
GridItem,
ExpandableSection,
ValidatedOptions,
PageSection,
PageSectionVariants,
Text,
TextVariants,
TextContent
} from '@patternfly/react-core';
import { HttpResponse } from '../../account-service/account.service'; import { HttpResponse } from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext'; import { AccountServiceContext } from '../../account-service/AccountServiceContext';
@ -144,27 +158,46 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
public render(): React.ReactNode { public render(): React.ReactNode {
const fields: FormFields = this.state.formFields; const fields: FormFields = this.state.formFields;
return ( return (
<ContentPage title="personalInfoHtmlTitle" <ContentPage
introMessage="personalSubMessage"> title="personalInfoHtmlTitle"
<Form isHorizontal onSubmit={event => this.handleSubmit(event)}> introMessage="personalSubMessage"
{!this.isRegistrationEmailAsUsername && >
<PageSection isFilled variant={PageSectionVariants.light}>
<TextContent className="pf-u-mb-lg">
<Text component={TextVariants.small}>
{Msg.localize('allFieldsRequired')}
</Text>
</TextContent>
<Form
onSubmit={(event) => this.handleSubmit(event)}
className="personal-info-form"
>
{!this.isRegistrationEmailAsUsername && (
<FormGroup <FormGroup
label={Msg.localize('username')} label={Msg.localize("username")}
isRequired
fieldId="user-name" fieldId="user-name"
helperTextInvalid={this.state.errors.username} helperTextInvalid={this.state.errors.username}
isValid={this.state.errors.username === ''} validated={
this.state.errors.username !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
> >
{this.isEditUserNameAllowed && <this.UsernameInput />} {this.isEditUserNameAllowed && <this.UsernameInput />}
{!this.isEditUserNameAllowed && <this.RestrictedUsernameInput />} {!this.isEditUserNameAllowed && (
<this.RestrictedUsernameInput />
)}
</FormGroup> </FormGroup>
} )}
<FormGroup <FormGroup
label={Msg.localize('email')} label={Msg.localize("email")}
isRequired
fieldId="email-address" fieldId="email-address"
helperTextInvalid={this.state.errors.email} helperTextInvalid={this.state.errors.email}
isValid={this.state.errors.email === ''} validated={
this.state.errors.email !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
> >
<TextInput <TextInput
isRequired isRequired
@ -174,16 +207,22 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
maxLength={254} maxLength={254}
value={fields.email} value={fields.email}
onChange={this.handleChange} onChange={this.handleChange}
isValid={this.state.errors.email === ''} validated={
> this.state.errors.email !== ""
</TextInput> ? ValidatedOptions.error
: ValidatedOptions.default
}
></TextInput>
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={Msg.localize('firstName')} label={Msg.localize("firstName")}
isRequired
fieldId="first-name" fieldId="first-name"
helperTextInvalid={this.state.errors.firstName} helperTextInvalid={this.state.errors.firstName}
isValid={this.state.errors.firstName === ''} validated={
this.state.errors.firstName !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
> >
<TextInput <TextInput
isRequired isRequired
@ -193,16 +232,22 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
maxLength={254} maxLength={254}
value={fields.firstName} value={fields.firstName}
onChange={this.handleChange} onChange={this.handleChange}
isValid={this.state.errors.firstName === ''} validated={
> this.state.errors.firstName !== ""
</TextInput> ? ValidatedOptions.error
: ValidatedOptions.default
}
></TextInput>
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={Msg.localize('lastName')} label={Msg.localize("lastName")}
isRequired
fieldId="last-name" fieldId="last-name"
helperTextInvalid={this.state.errors.lastName} helperTextInvalid={this.state.errors.lastName}
isValid={this.state.errors.lastName === ''} validated={
this.state.errors.lastName !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
> >
<TextInput <TextInput
isRequired isRequired
@ -212,35 +257,52 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
maxLength={254} maxLength={254}
value={fields.lastName} value={fields.lastName}
onChange={this.handleChange} onChange={this.handleChange}
isValid={this.state.errors.lastName === ''} validated={
> this.state.errors.lastName !== ""
</TextInput> ? ValidatedOptions.error
: ValidatedOptions.default
}
></TextInput>
</FormGroup> </FormGroup>
{features.isInternationalizationEnabled && <FormGroup {features.isInternationalizationEnabled && (
label={Msg.localize('selectLocale')} <FormGroup
label={Msg.localize("selectLocale")}
isRequired isRequired
fieldId="locale" fieldId="locale"
> >
<LocaleSelector id="locale-selector" <LocaleSelector
value={fields.attributes!.locale || ''} id="locale-selector"
onChange={value => this.setState({ value={fields.attributes!.locale || ""}
onChange={(value) =>
this.setState({
errors: this.state.errors, errors: this.state.errors,
formFields: { ...this.state.formFields, attributes: { ...this.state.formFields.attributes, locale: [value] }} formFields: {
})} ...this.state.formFields,
attributes: {
...this.state.formFields.attributes,
locale: [value],
},
},
})
}
/> />
</FormGroup>} </FormGroup>
)}
<ActionGroup> <ActionGroup>
<Button <Button
type="submit" type="submit"
id="save-btn" id="save-btn"
variant="primary" variant="primary"
isDisabled={Object.values(this.state.errors).filter(e => e !== '').length !== 0} isDisabled={
Object.values(this.state.errors).filter((e) => e !== "")
.length !== 0
}
> >
<Msg msgKey="doSave" /> <Msg msgKey="doSave" />
</Button> </Button>
<Button <Button
id="cancel-btn" id="cancel-btn"
variant="secondary" variant="link"
onClick={this.handleCancel} onClick={this.handleCancel}
> >
<Msg msgKey="doCancel" /> <Msg msgKey="doCancel" />
@ -248,10 +310,10 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
</ActionGroup> </ActionGroup>
</Form> </Form>
{ this.isDeleteAccountAllowed && {this.isDeleteAccountAllowed && (
<div id="delete-account" style={{ marginTop: "30px" }}> <div id="delete-account" style={{ marginTop: "30px" }}>
<Expandable toggleText={Msg.localize('deleteAccount')}> <ExpandableSection toggleText="Delete Account">
<Grid gutter={"sm"}> <Grid hasGutter>
<GridItem span={6}> <GridItem span={6}>
<p> <p>
<Msg msgKey="deleteAccountWarning" /> <Msg msgKey="deleteAccountWarning" />
@ -260,16 +322,23 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
<GridItem span={4}> <GridItem span={4}>
<KeycloakContext.Consumer> <KeycloakContext.Consumer>
{(keycloak: KeycloakService) => ( {(keycloak: KeycloakService) => (
<Button id="delete-account-btn" variant="danger" onClick={() => this.handleDelete(keycloak)} className="delete-button"><Msg msgKey="doDelete" /></Button> <Button
id="delete-account-btn"
variant="danger"
onClick={() => this.handleDelete(keycloak)}
className="delete-button"
>
<Msg msgKey="doDelete" />
</Button>
)} )}
</KeycloakContext.Consumer> </KeycloakContext.Consumer>
</GridItem> </GridItem>
<GridItem span={2}> <GridItem span={2}></GridItem>
</GridItem>
</Grid> </Grid>
</ExpandableSection>
</Expandable> </div>
</div>} )}
</PageSection>
</ContentPage> </ContentPage>
); );
} }
@ -283,14 +352,14 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
maxLength={254} maxLength={254}
value={this.state.formFields.username} value={this.state.formFields.username}
onChange={this.handleChange} onChange={this.handleChange}
isValid={this.state.errors.username === ''} validated={this.state.errors.username !== '' ? ValidatedOptions.error : ValidatedOptions.default}
> >
</TextInput> </TextInput>
); );
private RestrictedUsernameInput = () => ( private RestrictedUsernameInput = () => (
<TextInput <TextInput
isDisabled isReadOnly
type="text" type="text"
id="user-name" id="user-name"
name="username" name="username"

View file

@ -23,12 +23,12 @@ import {Msg} from '../../widgets/Msg';
import { import {
Title, Title,
TitleLevel,
Button, Button,
EmptyState, EmptyState,
EmptyStateVariant, EmptyStateVariant,
EmptyStateIcon, EmptyStateIcon,
EmptyStateBody EmptyStateBody,
TitleSizes
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { PassportIcon } from '@patternfly/react-icons'; import { PassportIcon } from '@patternfly/react-icons';
import { KeycloakService } from '../../keycloak-service/keycloak.service'; import { KeycloakService } from '../../keycloak-service/keycloak.service';
@ -67,7 +67,7 @@ class ApplicationInitiatedActionPage extends React.Component<AppInitiatedActionP
return ( return (
<EmptyState variant={EmptyStateVariant.full}> <EmptyState variant={EmptyStateVariant.full}>
<EmptyStateIcon icon={PassportIcon} /> <EmptyStateIcon icon={PassportIcon} />
<Title headingLevel={TitleLevel.h5} size="lg"> <Title headingLevel="h5" size={TitleSizes.lg}>
<Msg msgKey={this.props.pageDef.label} params={this.props.pageDef.labelParams}/> <Msg msgKey={this.props.pageDef.label} params={this.props.pageDef.labelParams}/>
</Title> </Title>
<EmptyStateBody> <EmptyStateBody>

View file

@ -24,12 +24,23 @@ import {
DataListToggle, DataListToggle,
DataListContent, DataListContent,
DataListItemCells, DataListItemCells,
DescriptionList,
DescriptionListTerm,
DescriptionListGroup,
DescriptionListDescription,
Grid, Grid,
GridItem, GridItem,
Button, Button,
PageSection,
PageSectionVariants,
Stack,
StackItem,
SplitItem,
Split,
TextContent
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { InfoAltIcon, CheckIcon, BuilderImageIcon, ExternalLinkAltIcon } from '@patternfly/react-icons'; import { InfoAltIcon, CheckIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
import { ContentPage } from '../ContentPage'; import { ContentPage } from '../ContentPage';
import { ContinueCancelModal } from '../../widgets/ContinueCancelModal'; import { ContinueCancelModal } from '../../widgets/ContinueCancelModal';
import { HttpResponse } from '../../account-service/account.service'; import { HttpResponse } from '../../account-service/account.service';
@ -118,12 +129,18 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
public render(): React.ReactNode { public render(): React.ReactNode {
return ( return (
<ContentPage title={Msg.localize('applicationsPageTitle')}> <ContentPage
<DataList id="applications-list" aria-label={Msg.localize('applicationsPageTitle')} isCompact> title={Msg.localize('applicationsPageTitle')}
introMessage="Manage your application permissions."
>
<PageSection isFilled variant={PageSectionVariants.light}>
<Stack hasGutter>
<DataList id="applications-list" aria-label={Msg.localize('applicationsPageTitle')}>
<DataListItem id="applications-list-header" aria-labelledby="Columns names"> <DataListItem id="applications-list-header" aria-labelledby="Columns names">
<DataListItemRow> <DataListItemRow>
// invisible toggle allows headings to line up properly // invisible toggle allows headings to line up properly
<span style={{ visibility: 'hidden' }}> <span style={{ visibility: 'hidden', height: 55 }}>
<DataListToggle <DataListToggle
isExpanded={false} isExpanded={false}
id='applications-list-header-invisible-toggle' id='applications-list-header-invisible-toggle'
@ -132,13 +149,13 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
</span> </span>
<DataListItemCells <DataListItemCells
dataListCells={[ dataListCells={[
<DataListCell key='applications-list-client-id-header' width={2}> <DataListCell key='applications-list-client-id-header' width={2} className="pf-u-pt-md">
<strong><Msg msgKey='applicationName' /></strong> <strong><Msg msgKey='applicationName' /></strong>
</DataListCell>, </DataListCell>,
<DataListCell key='applications-list-app-type-header' width={2}> <DataListCell key='applications-list-app-type-header' width={2} className="pf-u-pt-md">
<strong><Msg msgKey='applicationType' /></strong> <strong><Msg msgKey='applicationType' /></strong>
</DataListCell>, </DataListCell>,
<DataListCell key='applications-list-status' width={2}> <DataListCell key='applications-list-status' width={2} className="pf-u-pt-md">
<strong><Msg msgKey='status' /></strong> <strong><Msg msgKey='status' /></strong>
</DataListCell>, </DataListCell>,
]} ]}
@ -148,7 +165,7 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
{this.state.applications.map((application: Application, appIndex: number) => { {this.state.applications.map((application: Application, appIndex: number) => {
return ( return (
<DataListItem id={this.elementId("client-id", application)} key={'application-' + appIndex} aria-labelledby="applications-list" isExpanded={this.state.isRowOpen[appIndex]}> <DataListItem id={this.elementId("client-id", application)} key={'application-' + appIndex} aria-labelledby="applications-list" isExpanded={this.state.isRowOpen[appIndex]}>
<DataListItemRow> <DataListItemRow className="pf-u-align-items-center">
<DataListToggle <DataListToggle
onClick={() => this.onToggle(appIndex)} onClick={() => this.onToggle(appIndex)}
isExpanded={this.state.isRowOpen[appIndex]} isExpanded={this.state.isRowOpen[appIndex]}
@ -156,9 +173,10 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
aria-controls={this.elementId("expandable", application)} aria-controls={this.elementId("expandable", application)}
/> />
<DataListItemCells <DataListItemCells
className="pf-u-align-items-center"
dataListCells={[ dataListCells={[
<DataListCell id={this.elementId('name', application)} width={2} key={'app-' + appIndex}> <DataListCell id={this.elementId('name', application)} width={2} key={'app-' + appIndex}>
<Button component="a" variant="link" onClick={() => window.open(application.effectiveUrl)}> <Button className="pf-u-pl-0 title-case" component="a" variant="link" onClick={() => window.open(application.effectiveUrl)}>
{application.clientName || application.clientId} <ExternalLinkAltIcon/> {application.clientName || application.clientId} <ExternalLinkAltIcon/>
</Button> </Button>
</DataListCell>, </DataListCell>,
@ -172,36 +190,46 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
]} ]}
/> />
</DataListItemRow> </DataListItemRow>
<DataListContent <DataListContent
noPadding={false} className="pf-u-pl-35xl"
hasNoPadding={false}
aria-label={Msg.localize('applicationDetails')} aria-label={Msg.localize('applicationDetails')}
id={this.elementId("expandable", application)} id={this.elementId("expandable", application)}
isHidden={!this.state.isRowOpen[appIndex]} isHidden={!this.state.isRowOpen[appIndex]}
> >
<Grid sm={6} md={6} lg={6}> <DescriptionList>
<div className='pf-c-content'> <DescriptionListGroup>
<GridItem><strong>{Msg.localize('client') + ': '}</strong> {application.clientId}</GridItem> <DescriptionListTerm>{Msg.localize('client')}</DescriptionListTerm>
<DescriptionListDescription>{application.clientId}</DescriptionListDescription>
</DescriptionListGroup>
{application.description && {application.description &&
<GridItem><strong>{Msg.localize('description') + ': '}</strong> {application.description}</GridItem> <DescriptionListGroup>
<DescriptionListTerm>{Msg.localize('description')}</DescriptionListTerm>
<DescriptionListDescription>{application.description}</DescriptionListDescription>
</DescriptionListGroup>
} }
{application.effectiveUrl && {application.effectiveUrl &&
<GridItem><strong>URL: </strong> <span id={this.elementId('effectiveurl', application)}>{application.effectiveUrl.split('"')}</span></GridItem> <DescriptionListGroup>
<DescriptionListTerm>URL</DescriptionListTerm>
<DescriptionListDescription>{application.effectiveUrl.split('"')}</DescriptionListDescription>
</DescriptionListGroup>
} }
{application.consent && {application.consent &&
<React.Fragment> <React.Fragment>
<GridItem span={12}> <DescriptionListGroup>
<strong>Has access to:</strong> <DescriptionListTerm>Has access to</DescriptionListTerm>
</GridItem>
{application.consent.grantedScopes.map((scope: GrantedScope, scopeIndex: number) => { {application.consent.grantedScopes.map((scope: GrantedScope, scopeIndex: number) => {
return ( return (
<React.Fragment key={'scope-' + scopeIndex} > <React.Fragment key={'scope-' + scopeIndex} >
<GridItem offset={1}><CheckIcon /> {scope.name}</GridItem> <DescriptionListDescription><CheckIcon /> {scope.name}</DescriptionListDescription>
</React.Fragment> </React.Fragment>
) )
})} })}
{application.tosUri && <GridItem><strong>{Msg.localize('termsOfService') + ': '}</strong>{application.tosUri}</GridItem>} </DescriptionListGroup>
{application.policyUri && <GridItem><strong>{Msg.localize('policy') + ': '}</strong>{application.policyUri}</GridItem>} <DescriptionListGroup>
<GridItem><strong>{Msg.localize('accessGrantedOn') + ': '}</strong> <DescriptionListTerm>{Msg.localize('accessGrantedOn') + ': '}</DescriptionListTerm>
<DescriptionListDescription>
{new Intl.DateTimeFormat(locale, { {new Intl.DateTimeFormat(locale, {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
@ -210,14 +238,13 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
minute: 'numeric', minute: 'numeric',
second: 'numeric' second: 'numeric'
}).format(application.consent.createDate)} }).format(application.consent.createDate)}
</GridItem> </DescriptionListDescription>
</DescriptionListGroup>
</React.Fragment> </React.Fragment>
} }
</div> </DescriptionList>
{application.logoUri && <div className='pf-c-content'><img src={application.logoUri} /></div> }
</Grid>
{(application.consent || application.offlineAccess) && {(application.consent || application.offlineAccess) &&
<Grid gutter='sm'> <Grid hasGutter>
<hr /> <hr />
<GridItem> <GridItem>
<React.Fragment> <React.Fragment>
@ -239,6 +266,8 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
) )
})} })}
</DataList> </DataList>
</Stack>
</PageSection>
</ContentPage> </ContentPage>
); );
} }

View file

@ -21,28 +21,30 @@ import { AccountServiceContext } from '../../account-service/AccountServiceConte
import TimeUtil from '../../util/TimeUtil'; import TimeUtil from '../../util/TimeUtil';
import { import {
Bullseye, Button,
DataList, DataList,
DataListItem, DataListItem,
DataListItemRow, DataListItemRow,
DataListCell, DataListContent,
DataListItemCells, DescriptionList,
DescriptionListTerm,
DescriptionListDescription,
DescriptionListGroup,
Grid, Grid,
GridItem, GridItem,
Stack, Label,
StackItem PageSection,
PageSectionVariants,
Title,
Tooltip,
SplitItem,
Split
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { import {
AmazonIcon, DesktopIcon,
ChromeIcon, MobileAltIcon,
EdgeIcon, SyncAltIcon,
FirefoxIcon,
GlobeIcon,
InternetExplorerIcon,
OperaIcon,
SafariIcon,
YandexInternationalIcon,
} from '@patternfly/react-icons'; } from '@patternfly/react-icons';
import {Msg} from '../../widgets/Msg'; import {Msg} from '../../widgets/Msg';
@ -161,22 +163,15 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
return TimeUtil.format(time * 1000); return TimeUtil.format(time * 1000);
} }
private elementId(item: string, session: Session): string { private elementId(item: string, session: Session, element: string='session'): string {
return `session-${session.id.substring(0,7)}-${item}`; return `${element}-${session.id.substring(0,7)}-${item}`;
} }
private findBrowserIcon(session: Session): React.ReactNode { private findDeviceTypeIcon(session: Session, device: Device): React.ReactNode {
const browserName: string = session.browser.toLowerCase(); const deviceType: boolean = device.mobile;
if (browserName.includes("chrom")) return (<ChromeIcon id={this.elementId('icon-chrome', session)} size='lg'/>); // chrome or chromium if (deviceType === true) return (<MobileAltIcon id={this.elementId('icon-mobile', session, 'device')} />);
if (browserName.includes("firefox")) return (<FirefoxIcon id={this.elementId('icon-firefox', session)} size='lg'/>);
if (browserName.includes("edge")) return (<EdgeIcon id={this.elementId('icon-edge', session)} size='lg'/>);
if (browserName.startsWith("ie/")) return (<InternetExplorerIcon id={this.elementId('icon-ie', session)} size='lg'/>);
if (browserName.includes("safari")) return (<SafariIcon id={this.elementId('icon-safari', session)} size='lg'/>);
if (browserName.includes("opera")) return (<OperaIcon id={this.elementId('icon-opera', session)} size='lg'/>);
if (browserName.includes("yandex")) return (<YandexInternationalIcon id={this.elementId('icon-yandex', session)} size='lg'/>);
if (browserName.includes("amazon")) return (<AmazonIcon id={this.elementId('icon-amazon', session)} size='lg'/>);
return (<GlobeIcon id={this.elementId('icon-default', session)} size='lg'/>); return (<DesktopIcon id={this.elementId('icon-desktop', session, 'device')} />);
} }
private findOS(device: Device): string { private findOS(device: Device): string {
@ -220,74 +215,62 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
public render(): React.ReactNode { public render(): React.ReactNode {
return ( return (
<ContentPage title="device-activity" onRefresh={this.fetchDevices.bind(this)}> <ContentPage
<Stack gutter="md"> title="device-activity"
<StackItem isFilled> introMessage="signedInDevicesExplanation"
<DataList aria-label={Msg.localize('signedInDevices')}> >
<DataListItem key="SignedInDevicesHeader" aria-labelledby="signedInDevicesTitle" isExpanded={false}> <PageSection isFilled variant={PageSectionVariants.light}>
<DataListItemRow> <Split hasGutter className="pf-u-mb-lg">
<DataListItemCells <SplitItem isFilled>
dataListCells={[ <div id="signedInDevicesTitle" className="pf-c-content"><Title headingLevel="h2" size="xl"><Msg msgKey="signedInDevices"/></Title></div>
<DataListCell key='signedInDevicesTitle' width={4}> </SplitItem>
<div id="signedInDevicesTitle" className="pf-c-content"> <SplitItem>
<h2><Msg msgKey="signedInDevices"/></h2> <Tooltip content={<Msg msgKey="refreshPage" />}>
<p> <Button
<Msg msgKey="signedInDevicesExplanation"/> aria-describedby="refresh page"
</p> id="refresh-page"
</div> variant="link"
</DataListCell>, onClick={this.fetchDevices.bind(this)}
icon={<SyncAltIcon />}
>
Refresh
</Button>
</Tooltip>
</SplitItem>
<SplitItem>
<KeycloakContext.Consumer> <KeycloakContext.Consumer>
{ (keycloak: KeycloakService) => ( { (keycloak: KeycloakService) => (
<DataListCell key='signOutAllButton' width={1}> this.isShowSignOutAll(this.state.devices) &&
{this.isShowSignOutAll(this.state.devices) &&
<ContinueCancelModal buttonTitle='signOutAllDevices' <ContinueCancelModal buttonTitle='signOutAllDevices'
buttonId='sign-out-all' buttonId='sign-out-all'
modalTitle='signOutAllDevices' modalTitle='signOutAllDevices'
modalMessage='signOutAllDevicesWarning' modalMessage='signOutAllDevicesWarning'
onContinue={() => this.signOutAll(keycloak)} onContinue={() => this.signOutAll(keycloak)}
/> />
}
</DataListCell>
)} )}
</KeycloakContext.Consumer> </KeycloakContext.Consumer>
]} </SplitItem>
/> </Split>
</DataListItemRow> <DataList className="signed-in-device-list" aria-label={Msg.localize('signedInDevices')}>
</DataListItem> <DataListItem aria-labelledby='sessions' id='device-activity-sessions'>
<DataListItem aria-labelledby='sessions'>
<DataListItemRow>
<Grid gutter='sm'>
<GridItem span={12} /> {/* <-- top spacing */}
{this.state.devices.map((device: Device, deviceIndex: number) => { {this.state.devices.map((device: Device, deviceIndex: number) => {
return ( return (
<React.Fragment> <React.Fragment>
{device.sessions.map((session: Session, sessionIndex: number) => { {device.sessions.map((session: Session, sessionIndex: number) => {
return ( return (
<React.Fragment key={'device-' + deviceIndex + '-session-' + sessionIndex}> <React.Fragment key={'device-' + deviceIndex + '-session-' + sessionIndex}>
<DataListItemRow>
<GridItem md={3}> <DataListContent aria-label="device-sessions-content" isHidden={false} className="pf-u-flex-grow-1">
<Stack> <Grid className="signed-in-device-grid" hasGutter>
<StackItem isFilled={false}> <GridItem className="device-icon" span={1} rowSpan={2}>
<Bullseye>{this.findBrowserIcon(session)}</Bullseye> <span>{this.findDeviceTypeIcon(session, device)}</span>
</StackItem>
<StackItem isFilled={false}>
<Bullseye id={this.elementId('ip', session)}>{session.ipAddress}</Bullseye>
</StackItem>
{session.current &&
<StackItem isFilled={false}>
<Bullseye id={this.elementId('current-badge', session)}><strong className='pf-c-badge pf-m-read'><Msg msgKey="currentSession" /></strong></Bullseye>
</StackItem>
}
</Stack>
</GridItem> </GridItem>
<GridItem md={9}> <GridItem sm={8} md={9} span={10}>
{!session.browser.toLowerCase().includes('unknown') && <span id={this.elementId('browser', session)} className="pf-u-mr-md">{this.findOS(device)} {this.findOSVersion(device)} / {session.browser}</span>
<p id={this.elementId('browser', session)}><strong>{session.browser} / {this.findOS(device)} {this.findOSVersion(device)}</strong></p>} {session.current &&
<p id={this.elementId('last-access', session)}><strong>{Msg.localize('lastAccessedOn')}</strong> {this.time(session.lastAccess)}</p> <Label color="green"><Msg msgKey="currentSession" /></Label>}
<p id={this.elementId('clients', session)}><strong>{Msg.localize('clients')}</strong> {this.makeClientsString(session.clients)}</p> </GridItem>
<p id={this.elementId('started', session)}><strong>{Msg.localize('startedAt')}</strong> {this.time(session.started)}</p> <GridItem className="pf-u-text-align-right" sm={3} md={2} span={1}>
<p id={this.elementId('expires', session)}><strong>{Msg.localize('expiresAt')}</strong> {this.time(session.expires)}</p>
{!session.current && {!session.current &&
<ContinueCancelModal buttonTitle='doSignOut' <ContinueCancelModal buttonTitle='doSignOut'
buttonId={this.elementId('sign-out', session)} buttonId={this.elementId('sign-out', session)}
@ -297,23 +280,43 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
onContinue={() => this.signOutSession(device, session)} onContinue={() => this.signOutSession(device, session)}
/> />
} }
</GridItem> </GridItem>
<GridItem span={11}>
<DescriptionList columnModifier={{ sm: '2Col', lg: '3Col' }}>
<DescriptionListGroup>
<DescriptionListTerm>{Msg.localize('ipAddress')}</DescriptionListTerm>
<DescriptionListDescription>{session.ipAddress}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{Msg.localize('lastAccessedOn')}</DescriptionListTerm>
<DescriptionListDescription>{this.time(session.lastAccess)}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{Msg.localize('clients')}</DescriptionListTerm>
<DescriptionListDescription>{this.makeClientsString(session.clients)}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{Msg.localize('started')}</DescriptionListTerm>
<DescriptionListDescription>{this.time(session.started)}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{Msg.localize('expires')}</DescriptionListTerm>
<DescriptionListDescription>{this.time(session.expires)}</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</GridItem>
</Grid>
</DataListContent>
</DataListItemRow>
</React.Fragment> </React.Fragment>
); );
})} })}
</React.Fragment> </React.Fragment>
) )
})} })}
<GridItem span={12} /> {/* <-- bottom spacing */}
</Grid>
</DataListItemRow>
</DataListItem> </DataListItem>
</DataList> </DataList>
</StackItem> </PageSection>
</Stack>
</ContentPage> </ContentPage>
); );
} }

View file

@ -18,35 +18,31 @@ import * as React from 'react';
import {withRouter, RouteComponentProps} from 'react-router-dom'; import {withRouter, RouteComponentProps} from 'react-router-dom';
import { import {
Badge,
Button, Button,
DataList, DataList,
DataListAction, DataListAction,
DataListItemCells, DataListItemCells,
DataListCell, DataListCell,
DataListItemRow, DataListItemRow,
Divider,
Label,
PageSection,
PageSectionVariants,
Split,
SplitItem,
Stack, Stack,
StackItem, StackItem,
Title, Title,
TitleLevel,
DataListItem, DataListItem,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { import {
BitbucketIcon, BitbucketIcon,
CubeIcon, CubeIcon,
FacebookIcon,
GithubIcon,
GitlabIcon, GitlabIcon,
GoogleIcon,
InstagramIcon,
LinkIcon, LinkIcon,
LinkedinIcon,
MicrosoftIcon,
OpenshiftIcon, OpenshiftIcon,
PaypalIcon, PaypalIcon,
StackOverflowIcon,
TwitterIcon,
UnlinkIcon UnlinkIcon
} from '@patternfly/react-icons'; } from '@patternfly/react-icons';
@ -128,25 +124,26 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
return ( return (
<ContentPage title={Msg.localize('linkedAccountsTitle')} introMessage={Msg.localize('linkedAccountsIntroMessage')}> <ContentPage title={Msg.localize('linkedAccountsTitle')} introMessage={Msg.localize('linkedAccountsIntroMessage')}>
<Stack gutter='md'> <PageSection isFilled variant={PageSectionVariants.light}>
<StackItem isFilled> <Stack hasGutter>
<Title headingLevel={TitleLevel.h2} size='2xl'> <StackItem>
<Title headingLevel="h2" className="pf-u-mb-lg" size='xl'>
<Msg msgKey='linkedLoginProviders'/> <Msg msgKey='linkedLoginProviders'/>
</Title> </Title>
<DataList id="linked-idps" aria-label='foo'> <DataList id="linked-idps" aria-label={Msg.localize('linkedLoginProviders')}>
{this.makeRows(this.state.linkedAccounts, true)} {this.makeRows(this.state.linkedAccounts, true)}
</DataList> </DataList>
</StackItem> </StackItem>
<StackItem isFilled/> <StackItem>
<StackItem isFilled> <Title headingLevel="h2" className="pf-u-mt-xl pf-u-mb-lg" size='xl'>
<Title headingLevel={TitleLevel.h2} size='2xl'>
<Msg msgKey='unlinkedLoginProviders'/> <Msg msgKey='unlinkedLoginProviders'/>
</Title> </Title>
<DataList id="unlinked-idps" aria-label='foo'> <DataList id="unlinked-idps" aria-label={Msg.localize('unlinkedLoginProviders')}>
{this.makeRows(this.state.unLinkedAccounts, false)} {this.makeRows(this.state.unLinkedAccounts, false)}
</DataList> </DataList>
</StackItem> </StackItem>
</Stack> </Stack>
</PageSection>
</ContentPage> </ContentPage>
); );
} }
@ -160,10 +157,10 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
} }
return ( return (
<DataListItem key='emptyItem' aria-labelledby="empty-item"> <DataListItem key='emptyItem' aria-labelledby={Msg.localize('isEmptyMessage')}>
<DataListItemRow key='emptyRow'> <DataListItemRow key='emptyRow'>
<DataListItemCells dataListCells={[ <DataListItemCells dataListCells={[
<DataListCell key='empty'><strong>{isEmptyMessage}</strong></DataListCell> <DataListCell key='empty'>{isEmptyMessage}</DataListCell>
]}/> ]}/>
</DataListItemRow> </DataListItemRow>
</DataListItem> </DataListItem>
@ -179,15 +176,28 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
<> { <> {
accounts.map( (account: LinkedAccount) => ( accounts.map( (account: LinkedAccount) => (
<DataListItem id={`${account.providerAlias}-idp`} key={account.providerName} aria-labelledby="simple-item1"> <DataListItem id={`${account.providerAlias}-idp`} key={account.providerName} aria-labelledby={Msg.localize('linkedAccountsTitle')}>
<DataListItemRow key={account.providerName}> <DataListItemRow key={account.providerName}>
<DataListItemCells <DataListItemCells
dataListCells={[ dataListCells={[
<DataListCell key='idp'><Stack><StackItem isFilled>{this.findIcon(account)}</StackItem><StackItem id={`${account.providerAlias}-idp-name`} isFilled><h2><strong>{account.displayName}</strong></h2></StackItem></Stack></DataListCell>, <DataListCell key='idp'>
<DataListCell key='badge'><Stack><StackItem isFilled/><StackItem id={`${account.providerAlias}-idp-badge`} isFilled>{this.badge(account)}</StackItem></Stack></DataListCell>, <Split>
<DataListCell key='username'><Stack><StackItem isFilled/><StackItem id={`${account.providerAlias}-idp-username`} isFilled>{account.linkedUsername}</StackItem></Stack></DataListCell>, <SplitItem className="pf-u-mr-sm">{this.findIcon(account)}</SplitItem>
<SplitItem className="pf-u-my-xs" isFilled><span id={`${account.providerAlias}-idp-name`}>{account.displayName}</span></SplitItem>
</Split>
</DataListCell>,
<DataListCell key='label'>
<Split>
<SplitItem className="pf-u-my-xs" isFilled><span id={`${account.providerAlias}-idp-label`}>{this.label(account)}</span></SplitItem>
</Split>
</DataListCell>,
<DataListCell key='username' width={5}>
<Split>
<SplitItem className="pf-u-my-xs" isFilled><span id={`${account.providerAlias}-idp-username`}>{account.linkedUsername}</span></SplitItem>
</Split>
</DataListCell>,
]}/> ]}/>
<DataListAction aria-labelledby='foo' aria-label='foo action' id='setPasswordAction'> <DataListAction aria-labelledby={Msg.localize('link')} aria-label={Msg.localize('unLink')} id='setPasswordAction'>
{isLinked && <Button id={`${account.providerAlias}-idp-unlink`} variant='link' onClick={() => this.unLinkAccount(account)}><UnlinkIcon size='sm'/> <Msg msgKey='unLink'/></Button>} {isLinked && <Button id={`${account.providerAlias}-idp-unlink`} variant='link' onClick={() => this.unLinkAccount(account)}><UnlinkIcon size='sm'/> <Msg msgKey='unLink'/></Button>}
{!isLinked && <Button id={`${account.providerAlias}-idp-link`} variant='link' onClick={() => this.linkAccount(account)}><LinkIcon size='sm'/> <Msg msgKey='link'/></Button>} {!isLinked && <Button id={`${account.providerAlias}-idp-link`} variant='link' onClick={() => this.linkAccount(account)}><LinkIcon size='sm'/> <Msg msgKey='link'/></Button>}
</DataListAction> </DataListAction>
@ -200,30 +210,31 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
) )
} }
private badge(account: LinkedAccount): React.ReactNode { private label(account: LinkedAccount): React.ReactNode {
if (account.social) { if (account.social) {
return (<Badge><Msg msgKey='socialLogin'/></Badge>); return (<Label color="blue"><Msg msgKey='socialLogin'/></Label>);
} }
return (<Badge style={{backgroundColor: "green"}} ><Msg msgKey='systemDefined'/></Badge>); return (<Label color="green"><Msg msgKey='systemDefined'/></Label>);
} }
private findIcon(account: LinkedAccount): React.ReactNode { private findIcon(account: LinkedAccount): React.ReactNode {
const socialIconId = `${account.providerAlias}-idp-icon-social`; const socialIconId = `${account.providerAlias}-idp-icon-social`;
if (account.providerName.toLowerCase().includes('github')) return (<GithubIcon id={socialIconId} size='xl'/>); console.log(account);
if (account.providerName.toLowerCase().includes('linkedin')) return (<LinkedinIcon id={socialIconId} size='xl'/>); switch (true) {
if (account.providerName.toLowerCase().includes('facebook')) return (<FacebookIcon id={socialIconId} size='xl'/>); case account.providerName.toLowerCase().includes('bitbucket'):
if (account.providerName.toLowerCase().includes('google')) return (<GoogleIcon id={socialIconId} size='xl'/>); return <BitbucketIcon id={socialIconId} size='lg'/>;
if (account.providerName.toLowerCase().includes('instagram')) return (<InstagramIcon id={socialIconId} size='xl'/>); case account.providerName.toLowerCase().includes('openshift'):
if (account.providerName.toLowerCase().includes('microsoft')) return (<MicrosoftIcon id={socialIconId} size='xl'/>); return <div className="idp-icon-social" id="openshift-idp-icon-social" />;
if (account.providerName.toLowerCase().includes('bitbucket')) return (<BitbucketIcon id={socialIconId} size='xl'/>); case account.providerName.toLowerCase().includes('gitlab'):
if (account.providerName.toLowerCase().includes('twitter')) return (<TwitterIcon id={socialIconId} size='xl'/>); return <GitlabIcon id={socialIconId} size='lg'/>;
if (account.providerName.toLowerCase().includes('openshift')) return (<OpenshiftIcon id={socialIconId} size='xl'/>); case account.providerName.toLowerCase().includes('paypal'):
if (account.providerName.toLowerCase().includes('gitlab')) return (<GitlabIcon id={socialIconId} size='xl'/>); return <PaypalIcon id={socialIconId} size='lg'/>;
if (account.providerName.toLowerCase().includes('paypal')) return (<PaypalIcon id={socialIconId} size='xl'/>); case (account.providerName !== '' && account.social):
if (account.providerName.toLowerCase().includes('stackoverflow')) return (<StackOverflowIcon id={socialIconId} size='xl'/>); return <div className="idp-icon-social" id={socialIconId}/>;
default:
return (<CubeIcon id={`${account.providerAlias}-idp-icon-default`} size='xl'/>); return <CubeIcon id={`${account.providerAlias}-idp-icon-default`} size='lg'/>;
}
} }
}; };

View file

@ -22,11 +22,12 @@ import {
Form, Form,
FormGroup, FormGroup,
TextInput, TextInput,
InputGroup InputGroup,
ModalVariant
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { OkIcon } from '@patternfly/react-icons'; import { OkIcon } from '@patternfly/react-icons';
import { Resource, Permission, Scope } from './resource-model'; import { Resource, Permission, Permissions, Scope } from './resource-model';
import { Msg } from '../../widgets/Msg'; import { Msg } from '../../widgets/Msg';
import { AccountServiceContext } from '../../account-service/AccountServiceContext'; import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { ContentAlert } from '../ContentAlert'; import { ContentAlert } from '../ContentAlert';
@ -45,7 +46,7 @@ interface EditTheResourceState {
} }
export class EditTheResource extends React.Component<EditTheResourceProps, EditTheResourceState> { export class EditTheResource extends React.Component<EditTheResourceProps, EditTheResourceState> {
protected static defaultProps = { permissions: [] }; protected static defaultProps:Permissions = { permissions: [] };
static contextType = AccountServiceContext; static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>; context: React.ContextType<typeof AccountServiceContext>;
@ -91,7 +92,7 @@ export class EditTheResource extends React.Component<EditTheResourceProps, EditT
<Modal <Modal
title={'Edit the resource - ' + this.props.resource.name} title={'Edit the resource - ' + this.props.resource.name}
isLarge variant={ModalVariant.large}
isOpen={this.state.isOpen} isOpen={this.state.isOpen}
onClose={this.handleToggleDialog} onClose={this.handleToggleDialog}
actions={[ actions={[

View file

@ -18,7 +18,18 @@ import * as React from 'react';
import parse from '../../util/ParseLink'; import parse from '../../util/ParseLink';
import { Button, Level, LevelItem, Stack, StackItem, Tab, Tabs, TextInput } from '@patternfly/react-core'; import {
Button,
Level,
LevelItem,
PageSection,
PageSectionVariants,
Stack,
StackItem,
Tab,
Tabs,
TextInput
} from '@patternfly/react-core';
import {HttpResponse} from '../../account-service/account.service'; import {HttpResponse} from '../../account-service/account.service';
import {AccountServiceContext} from '../../account-service/AccountServiceContext'; import {AccountServiceContext} from '../../account-service/AccountServiceContext';
@ -170,12 +181,12 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
private makeTab(eventKey: number, title: string, resources: PaginatedResources, sharedResourcesTab: boolean): React.ReactNode { private makeTab(eventKey: number, title: string, resources: PaginatedResources, sharedResourcesTab: boolean): React.ReactNode {
return ( return (
<Tab id={title} eventKey={eventKey} title={Msg.localize(title)}> <Tab id={title} eventKey={eventKey} title={Msg.localize(title)}>
<Stack gutter="md"> <Stack hasGutter>
<StackItem isFilled><span/></StackItem> <StackItem isFilled><span/></StackItem>
<StackItem isFilled> <StackItem isFilled>
<Level gutter='md'> <Level hasGutter>
<LevelItem> <LevelItem>
<TextInput value={this.state.nameFilter} onChange={this.handleFilterRequest} id={'filter-' + title} type="text" placeholder={Msg.localize('filterByName')} /> <TextInput value={this.state.nameFilter} onChange={this.handleFilterRequest} id={'filter-' + title} type="text" placeholder={Msg.localize('filterByName')} iconVariant="search"/>
</LevelItem> </LevelItem>
</Level> </Level>
</StackItem> </StackItem>
@ -191,12 +202,13 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
public render(): React.ReactNode { public render(): React.ReactNode {
return ( return (
<ContentPage title="resources" onRefresh={this.fetchInitialResources.bind(this)}> <ContentPage title="resources" onRefresh={this.fetchInitialResources.bind(this)}>
<Tabs isFilled activeKey={this.state.activeTabKey} onSelect={this.handleTabClick}> <PageSection variant={PageSectionVariants.light}>
<Tabs activeKey={this.state.activeTabKey} onSelect={this.handleTabClick}>
{this.makeTab(0, 'myResources', this.state.myResources, false)} {this.makeTab(0, 'myResources', this.state.myResources, false)}
{this.makeTab(1, 'sharedwithMe', this.state.sharedWithMe, true)} {this.makeTab(1, 'sharedwithMe', this.state.sharedWithMe, true)}
</Tabs> </Tabs>
<Level gutter='md'> <Level hasGutter>
<LevelItem> <LevelItem>
{this.hasPrevious() && <Button onClick={this.handlePreviousClick}>&lt;<Msg msgKey='previousPage'/></Button>} {this.hasPrevious() && <Button onClick={this.handlePreviousClick}>&lt;<Msg msgKey='previousPage'/></Button>}
</LevelItem> </LevelItem>
@ -209,6 +221,7 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
{this.hasNext() && <Button onClick={this.handleNextClick}><Msg msgKey='nextPage'/>&gt;</Button>} {this.hasNext() && <Button onClick={this.handleNextClick}><Msg msgKey='nextPage'/>&gt;</Button>}
</LevelItem> </LevelItem>
</Level> </Level>
</PageSection>
</ContentPage> </ContentPage>
); );
} }

View file

@ -27,7 +27,8 @@ import {
DataListCell, DataListCell,
Chip, Chip,
Split, Split,
SplitItem SplitItem,
ModalVariant
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { UserCheckIcon } from '@patternfly/react-icons'; import { UserCheckIcon } from '@patternfly/react-icons';
@ -35,7 +36,7 @@ import { HttpResponse } from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext'; import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { Msg } from '../../widgets/Msg'; import { Msg } from '../../widgets/Msg';
import { ContentAlert } from '../ContentAlert'; import { ContentAlert } from '../ContentAlert';
import { Resource, Scope, Permission } from './resource-model'; import { Resource, Scope, Permission, Permissions } from './resource-model';
interface PermissionRequestProps { interface PermissionRequestProps {
@ -48,7 +49,7 @@ interface PermissionRequestState {
} }
export class PermissionRequest extends React.Component<PermissionRequestProps, PermissionRequestState> { export class PermissionRequest extends React.Component<PermissionRequestProps, PermissionRequestState> {
protected static defaultProps = { permissions: [], row: 0 }; protected static defaultProps:Permissions = { permissions: [], row: 0 };
static contextType = AccountServiceContext; static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>; context: React.ContextType<typeof AccountServiceContext>;
@ -107,7 +108,7 @@ export class PermissionRequest extends React.Component<PermissionRequestProps, P
<Modal <Modal
id={`modal-${id}`} id={`modal-${id}`}
title={Msg.localize('permissionRequests') + ' - ' + this.props.resource.name} title={Msg.localize('permissionRequests') + ' - ' + this.props.resource.name}
isLarge={true} variant={ModalVariant.large}
isOpen={this.state.isOpen} isOpen={this.state.isOpen}
onClose={this.handleToggleDialog} onClose={this.handleToggleDialog}
actions={[ actions={[
@ -146,7 +147,7 @@ export class PermissionRequest extends React.Component<PermissionRequestProps, P
{(shareRequest.scopes as Scope[]).map((scope, j) => <Chip key={j} isReadOnly>{scope}</Chip>)} {(shareRequest.scopes as Scope[]).map((scope, j) => <Chip key={j} isReadOnly>{scope}</Chip>)}
</DataListCell>, </DataListCell>,
<DataListCell key={`actions${i}`}> <DataListCell key={`actions${i}`}>
<Split gutter="sm"> <Split hasGutter>
<SplitItem> <SplitItem>
<Button <Button
id={`accept-${i}-${id}`} id={`accept-${i}-${id}`}

View file

@ -2,6 +2,7 @@ import * as React from 'react';
import { Select, SelectOption, SelectVariant, SelectOptionObject } from '@patternfly/react-core'; import { Select, SelectOption, SelectVariant, SelectOptionObject } from '@patternfly/react-core';
import { Scope } from './resource-model'; import { Scope } from './resource-model';
import { Msg } from '../../widgets/Msg';
interface PermissionSelectState { interface PermissionSelectState {
selected: ScopeValue[]; selected: ScopeValue[];
@ -86,19 +87,19 @@ export class PermissionSelect extends React.Component<PermissionSelectProps, Per
return ( return (
<div> <div>
<span id={titleId} hidden> <span id={titleId} hidden>
Select the permissions <Msg msgKey='selectPermissions' />
</span> </span>
<Select <Select
direction={this.props.direction || 'down'} direction={this.props.direction || 'down'}
variant={SelectVariant.typeaheadMulti} variant={SelectVariant.typeaheadMulti}
ariaLabelTypeAhead="Select the permissions" typeAheadAriaLabel={Msg.localize("selectPermissions")}
onToggle={this.onToggle} onToggle={this.onToggle}
onSelect={this.onSelect} onSelect={this.onSelect}
onClear={this.clearSelection} onClear={this.clearSelection}
selections={selected} selections={selected}
isExpanded={isExpanded} isOpen={isExpanded}
ariaLabelledBy={titleId} aria-labelledby={titleId}
placeholderText="Select the permissions" placeholderText={Msg.localize("selectPermissions")}
> >
{this.state.scopes} {this.state.scopes}
</Select> </Select>

View file

@ -28,7 +28,6 @@ import {
LevelItem, LevelItem,
Button, Button,
DataListAction, DataListAction,
DataListActionVisibility,
Dropdown, Dropdown,
DropdownPosition, DropdownPosition,
DropdownItem, DropdownItem,
@ -169,7 +168,7 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
]} ]}
/> />
<DataListAction <DataListAction
className={DataListActionVisibility.hiddenOnLg} visibility={{ lg: 'hidden' }}
aria-labelledby="check-action-item3 check-action-action3" aria-labelledby="check-action-item3 check-action-action3"
id="check-action-action3" id="check-action-action3"
aria-label="Actions" aria-label="Actions"
@ -246,7 +245,7 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
</DataListAction> </DataListAction>
<DataListAction <DataListAction
id={`actions-${row}`} id={`actions-${row}`}
className={css(DataListActionVisibility.visibleOnLg, DataListActionVisibility.hidden)} visibility={{ default: 'hidden', lg: 'visible' }}
aria-labelledby="Row actions" aria-labelledby="Row actions"
aria-label="Actions" aria-label="Actions"
> >
@ -319,15 +318,14 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
]} ]}
/> />
</DataListAction> </DataListAction>
</DataListItemRow> </DataListItemRow>
<DataListContent <DataListContent
noPadding={false} hasNoPadding={false}
aria-label="Session Details" aria-label="Session Details"
id={'ex-expand' + row} id={'ex-expand' + row}
isHidden={!this.state.isRowOpen[row]} isHidden={!this.state.isRowOpen[row]}
> >
<Level gutter='md'> <Level hasGutter>
<LevelItem><span /></LevelItem> <LevelItem><span /></LevelItem>
<LevelItem id={'shared-with-user-message-' + row}>{this.sharedWithUsersMessage(row)}</LevelItem> <LevelItem id={'shared-with-user-message-' + row}>{this.sharedWithUsersMessage(row)}</LevelItem>
<LevelItem><span /></LevelItem> <LevelItem><span /></LevelItem>

View file

@ -20,7 +20,6 @@ import {
Button, Button,
Chip, Chip,
ChipGroup, ChipGroup,
ChipGroupToolbarItem,
Form, Form,
FormGroup, FormGroup,
Gallery, Gallery,
@ -28,7 +27,8 @@ import {
Modal, Modal,
Stack, Stack,
StackItem, StackItem,
TextInput TextInput,
ModalVariant
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { AccountServiceContext } from '../../account-service/AccountServiceContext'; import { AccountServiceContext } from '../../account-service/AccountServiceContext';
@ -57,7 +57,7 @@ interface ShareTheResourceState {
* @author Stan Silvert ssilvert@redhat.com (C) 2019 Red Hat Inc. * @author Stan Silvert ssilvert@redhat.com (C) 2019 Red Hat Inc.
*/ */
export class ShareTheResource extends React.Component<ShareTheResourceProps, ShareTheResourceState> { export class ShareTheResource extends React.Component<ShareTheResourceProps, ShareTheResourceState> {
protected static defaultProps = {permissions: []}; protected static defaultProps:any = {permissions: []};
static contextType = AccountServiceContext; static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>; context: React.ContextType<typeof AccountServiceContext>;
@ -166,7 +166,7 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
<Modal <Modal
title={'Share the resource - ' + this.props.resource.name} title={'Share the resource - ' + this.props.resource.name}
isLarge={true} variant={ModalVariant.large}
isOpen={this.state.isOpen} isOpen={this.state.isOpen}
onClose={this.handleToggleDialog} onClose={this.handleToggleDialog}
actions={[ actions={[
@ -178,7 +178,7 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
</Button> </Button>
]} ]}
> >
<Stack gutter='md'> <Stack hasGutter>
<StackItem isFilled> <StackItem isFilled>
<Form> <Form>
<FormGroup <FormGroup
@ -187,13 +187,11 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
helperTextInvalid={Msg.localize('resourceAlreadyShared')} helperTextInvalid={Msg.localize('resourceAlreadyShared')}
fieldId="username" fieldId="username"
isRequired isRequired
isValid={!this.isAlreadyShared()}
> >
<Gallery gutter='sm'> <Gallery hasGutter>
<GalleryItem> <GalleryItem>
<TextInput <TextInput
value={this.state.usernameInput} value={this.state.usernameInput}
isValid={!this.isAlreadyShared()}
id="username" id="username"
aria-describedby="username-helper" aria-describedby="username-helper"
placeholder="Username or email" placeholder="Username or email"
@ -208,14 +206,12 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
</GalleryItem> </GalleryItem>
</Gallery> </Gallery>
<ChipGroup withToolbar> <ChipGroup categoryName={Msg.localize('shareWith')}>
<ChipGroupToolbarItem key='users-selected' categoryName='Share with '>
{this.state.usernames.map((currentChip: string) => ( {this.state.usernames.map((currentChip: string) => (
<Chip key={currentChip} onClick={() => this.handleDeleteUsername(currentChip)}> <Chip key={currentChip} onClick={() => this.handleDeleteUsername(currentChip)}>
{currentChip} {currentChip}
</Chip> </Chip>
))} ))}
</ChipGroupToolbarItem>
</ChipGroup> </ChipGroup>
</FormGroup> </FormGroup>
<FormGroup <FormGroup

View file

@ -23,7 +23,6 @@ import {
DataListCell, DataListCell,
DataListItemCells, DataListItemCells,
ChipGroup, ChipGroup,
ChipGroupToolbarItem,
Chip Chip
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { RepositoryIcon } from '@patternfly/react-icons'; import { RepositoryIcon } from '@patternfly/react-icons';
@ -85,8 +84,7 @@ export class SharedResourcesTable extends AbstractResourcesTable<ResourcesTableS
</DataListCell>, </DataListCell>,
<DataListCell key={'permissions-' + row} width={2}> <DataListCell key={'permissions-' + row} width={2}>
{ resource.scopes.length > 0 && { resource.scopes.length > 0 &&
<ChipGroup withToolbar> <ChipGroup categoryName={Msg.localize('permissions')}>
<ChipGroupToolbarItem key='permissions' categoryName={Msg.localize('permissions')}>
{ {
resource.scopes.map(scope => ( resource.scopes.map(scope => (
<Chip key={scope.name} isReadOnly> <Chip key={scope.name} isReadOnly>
@ -94,13 +92,11 @@ export class SharedResourcesTable extends AbstractResourcesTable<ResourcesTableS
</Chip> </Chip>
)) ))
} }
</ChipGroupToolbarItem>
</ChipGroup>} </ChipGroup>}
</DataListCell>, </DataListCell>,
<DataListCell key={'pending-' + row} width={2}> <DataListCell key={'pending-' + row} width={2}>
{resource.shareRequests.length > 0 && {resource.shareRequests.length > 0 &&
<ChipGroup withToolbar> <ChipGroup categoryName={Msg.localize('pending')}>
<ChipGroupToolbarItem key='permissions' categoryName={Msg.localize('pending')}>
{ {
(resource.shareRequests[0].scopes as Scope[]).map(scope => ( (resource.shareRequests[0].scopes as Scope[]).map(scope => (
<Chip key={scope.name} isReadOnly> <Chip key={scope.name} isReadOnly>
@ -108,7 +104,6 @@ export class SharedResourcesTable extends AbstractResourcesTable<ResourcesTableS
</Chip> </Chip>
)) ))
} }
</ChipGroupToolbarItem>
</ChipGroup> </ChipGroup>
} }
</DataListCell> </DataListCell>

View file

@ -38,3 +38,8 @@ export interface Permission {
scopes: Scope[] | string[]; // this should be Scope[] - fix API scopes: Scope[] | string[]; // this should be Scope[] - fix API
username: string; username: string;
} }
export interface Permissions {
permissions: Permission[];
row?: number;
}

View file

@ -14,10 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import * as React from 'react'; import * as React from "react";
import {withRouter, RouteComponentProps} from 'react-router-dom'; import { withRouter, RouteComponentProps } from "react-router-dom";
import { import {
Alert,
Button, Button,
DataList, DataList,
DataListAction, DataListAction,
@ -25,28 +26,34 @@ import {
DataListCell, DataListCell,
DataListItem, DataListItem,
DataListItemRow, DataListItemRow,
Stack, EmptyState,
StackItem, EmptyStateVariant,
EmptyStateBody,
Split,
SplitItem,
Title, Title,
TitleLevel,
DataListActionVisibility,
Dropdown, Dropdown,
DropdownPosition, DropdownPosition,
KebabToggle, KebabToggle,
} from '@patternfly/react-core'; PageSection,
PageSectionVariants
} from "@patternfly/react-core";
import {AIACommand} from '../../util/AIACommand'; import { AIACommand } from "../../util/AIACommand";
import TimeUtil from '../../util/TimeUtil'; import TimeUtil from "../../util/TimeUtil";
import {HttpResponse, AccountServiceClient} from '../../account-service/account.service'; import {
import {AccountServiceContext} from '../../account-service/AccountServiceContext'; HttpResponse,
import {ContinueCancelModal} from '../../widgets/ContinueCancelModal'; AccountServiceClient,
import {Features} from '../../widgets/features'; } from "../../account-service/account.service";
import {Msg} from '../../widgets/Msg'; import { AccountServiceContext } from "../../account-service/AccountServiceContext";
import {ContentPage} from '../ContentPage'; import { ContinueCancelModal } from "../../widgets/ContinueCancelModal";
import {ContentAlert} from '../ContentAlert'; import { Features } from "../../widgets/features";
import { KeycloakContext } from '../../keycloak-service/KeycloakContext'; import { Msg } from "../../widgets/Msg";
import { KeycloakService } from '../../keycloak-service/keycloak.service'; import { ContentPage } from "../ContentPage";
import { css } from '@patternfly/react-styles'; import { ContentAlert } from "../ContentAlert";
import { KeycloakContext } from "../../keycloak-service/KeycloakContext";
import { KeycloakService } from "../../keycloak-service/keycloak.service";
import { css } from "@patternfly/react-styles";
declare const features: Features; declare const features: Features;
@ -55,7 +62,7 @@ interface PasswordDetails {
lastUpdate: number; lastUpdate: number;
} }
type CredCategory = 'password' | 'two-factor' | 'passwordless'; type CredCategory = "password" | "two-factor" | "passwordless";
type CredType = string; type CredType = string;
type CredTypeMap = Map<CredType, CredentialContainer>; type CredTypeMap = Map<CredType, CredentialContainer>;
type CredContainerMap = Map<CredCategory, CredTypeMap>; type CredContainerMap = Map<CredCategory, CredTypeMap>;
@ -89,8 +96,7 @@ interface CredentialContainer {
open: boolean; open: boolean;
} }
interface SigningInPageProps extends RouteComponentProps { interface SigningInPageProps extends RouteComponentProps {}
}
interface SigningInPageState { interface SigningInPageState {
// Credential containers organized by category then type // Credential containers organized by category then type
@ -100,28 +106,33 @@ interface SigningInPageState {
/** /**
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc. * @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
*/ */
class SigningInPage extends React.Component<SigningInPageProps, SigningInPageState> { class SigningInPage extends React.Component<
SigningInPageProps,
SigningInPageState
> {
static contextType = AccountServiceContext; static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>; context: React.ContextType<typeof AccountServiceContext>;
public constructor(props: SigningInPageProps, context: React.ContextType<typeof AccountServiceContext>) { public constructor(
props: SigningInPageProps,
context: React.ContextType<typeof AccountServiceContext>
) {
super(props); super(props);
this.context = context; this.context = context;
this.state = { this.state = {
credentialContainers: new Map(), credentialContainers: new Map(),
} };
this.getCredentialContainers(); this.getCredentialContainers();
} }
private getCredentialContainers(): void { private getCredentialContainers(): void {
this.context!.doGet("/credentials") this.context!.doGet("/credentials").then(
.then((response: HttpResponse<CredentialContainer[]>) => { (response: HttpResponse<CredentialContainer[]>) => {
const allContainers: CredContainerMap = new Map(); const allContainers: CredContainerMap = new Map();
const containers: CredentialContainer[] = response.data || []; const containers: CredentialContainer[] = response.data || [];
containers.forEach(container => { containers.forEach((container) => {
let categoryMap = allContainers.get(container.category); let categoryMap = allContainers.get(container.category);
if (!categoryMap) { if (!categoryMap) {
categoryMap = new Map(); categoryMap = new Map();
@ -131,60 +142,80 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
}); });
this.setState({ credentialContainers: allContainers }); this.setState({ credentialContainers: allContainers });
console.log({allContainers}) }
}); );
} }
private handleRemove = (credentialId: string, userLabel: string) => { private handleRemove = (credentialId: string, userLabel: string) => {
this.context!.doDelete("/credentials/" + credentialId) this.context!.doDelete("/credentials/" + credentialId).then(() => {
.then(() => {
this.getCredentialContainers(); this.getCredentialContainers();
ContentAlert.success('successRemovedMessage', [userLabel]); ContentAlert.success("successRemovedMessage", [userLabel]);
}); });
} };
public static credElementId(credType: CredType, credId: string, item: string): string { public static credElementId(
credType: CredType,
credId: string,
item: string
): string {
return `${credType}-${item}-${credId.substring(0, 8)}`; return `${credType}-${item}-${credId.substring(0, 8)}`;
} }
public render(): React.ReactNode { public render(): React.ReactNode {
return ( return (
<ContentPage title="signingIn" <ContentPage title="signingIn" introMessage="signingInSubMessage">
introMessage="signingInSubMessage">
<Stack gutter='md'>
{this.renderCategories()} {this.renderCategories()}
</Stack>
</ContentPage> </ContentPage>
); );
} }
private renderCategories(): React.ReactNode { private renderCategories(): React.ReactNode {
return (<> { return Array.from(this.state.credentialContainers.keys()).map(
Array.from(this.state.credentialContainers.keys()).map(category => ( (category) => (
<StackItem key={category} isFilled> <PageSection key={category} variant={PageSectionVariants.light}>
<Title id={`${category}-categ-title`} headingLevel={TitleLevel.h2} size='2xl'> <Title
<strong><Msg msgKey={category}/></strong> id={`${category}-categ-title`}
headingLevel="h2"
size="xl"
>
<Msg msgKey={category} />
</Title> </Title>
<DataList aria-label='foo'> {this.renderTypes(category!)}
{this.renderTypes(this.state.credentialContainers.get(category)!)} </PageSection>
</DataList> )
</StackItem> )
))
}</>)
} }
private renderTypes(credTypeMap: CredTypeMap): React.ReactNode { private renderTypes(category: CredCategory): React.ReactNode {
let credTypeMap: CredTypeMap = this.state.credentialContainers.get(
category
)!;
return ( return (
<KeycloakContext.Consumer> <KeycloakContext.Consumer>
{ keycloak => ( {(keycloak) => (
<>{ <>
Array.from(credTypeMap.keys()).map((credType: CredType, index: number, typeArray: string[]) => ([ {Array.from(
this.renderCredTypeTitle(credTypeMap.get(credType)!, keycloak!), credTypeMap.keys()
this.renderUserCredentials(credTypeMap, credType, keycloak!), ).map(
this.renderEmptyRow(credTypeMap.get(credType)!.type, index === typeArray.length - 1) (
])) credType: CredType,
}</> index: number,
typeArray: string[]
) => [
this.renderCredTypeTitle(
credTypeMap.get(credType)!,
keycloak!,
category
),
this.renderUserCredentials(
credTypeMap,
credType,
keycloak!
),
]
)}
</>
)} )}
</KeycloakContext.Consumer> </KeycloakContext.Consumer>
); );
@ -194,15 +225,21 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
if (isLast) return; // don't put empty row at the end if (isLast) return; // don't put empty row at the end
return ( return (
<DataListItem aria-labelledby={'empty-list-item-' + type}> <DataListItem aria-labelledby={"empty-list-item-" + type}>
<DataListItemRow key={'empty-row-' + type}> <DataListItemRow key={"empty-row-" + type}>
<DataListItemCells dataListCells={[<DataListCell></DataListCell>]}/> <DataListItemCells
dataListCells={[<DataListCell></DataListCell>]}
/>
</DataListItemRow> </DataListItemRow>
</DataListItem> </DataListItem>
) );
} }
private renderUserCredentials(credTypeMap: CredTypeMap, credType: CredType, keycloak: KeycloakService): React.ReactNode { private renderUserCredentials(
credTypeMap: CredTypeMap,
credType: CredType,
keycloak: KeycloakService
): React.ReactNode {
const credContainer: CredentialContainer = credTypeMap.get(credType)!; const credContainer: CredentialContainer = credTypeMap.get(credType)!;
const userCredentialMetadatas: CredMetadata[] = credContainer.userCredentialMetadatas; const userCredentialMetadatas: CredMetadata[] = credContainer.userCredentialMetadatas;
const removeable: boolean = credContainer.removeable; const removeable: boolean = credContainer.removeable;
@ -212,17 +249,22 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
if (!userCredentialMetadatas || userCredentialMetadatas.length === 0) { if (!userCredentialMetadatas || userCredentialMetadatas.length === 0) {
const localizedDisplayName = Msg.localize(displayName); const localizedDisplayName = Msg.localize(displayName);
return ( return (
<DataListItem key='no-credentials-list-item' aria-labelledby='no-credentials-list-item'> <DataList aria-label={Msg.localize('notSetUp', [localizedDisplayName])} className="pf-u-mb-xl">
<DataListItemRow key='no-credentials-list-item-row'> <DataListItem key='no-credentials-list-item' aria-labelledby={Msg.localize('notSetUp', [localizedDisplayName])}>
<DataListItemRow key='no-credentials-list-item-row' className="pf-u-align-items-center">
<DataListItemCells <DataListItemCells
dataListCells={[ dataListCells={[
<DataListCell key={'no-credentials-cell-0'}/>, <DataListCell key={'no-credentials-cell-0'}/>,
<strong id={`${type}-not-set-up`} key={'no-credentials-cell-1'}><Msg msgKey='notSetUp' params={[localizedDisplayName]}/></strong>, <EmptyState id={`${type}-not-set-up`} key={'no-credentials-cell-1'} variant={EmptyStateVariant.xs}>
<EmptyStateBody>
<Msg msgKey='notSetUp' params={[localizedDisplayName]}/>
</EmptyStateBody>
</EmptyState>,
<DataListCell key={'no-credentials-cell-2'}/> <DataListCell key={'no-credentials-cell-2'}/>
]} ]}/>
/>
</DataListItemRow> </DataListItemRow>
</DataListItem> </DataListItem>
</DataList>
); );
} }
@ -239,11 +281,24 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
updateAIA = new AIACommand(keycloak, credContainer.updateAction); updateAIA = new AIACommand(keycloak, credContainer.updateAction);
} }
let maxWidth = { maxWidth: 689 } as React.CSSProperties;
return ( return (
<React.Fragment key='userCredentialMetadatas'> { <React.Fragment key='userCredentialMetadatas'> {
userCredentialMetadatas.map(credentialMetadata => ( userCredentialMetadatas.map(credentialMetadata => (
<>
{(credentialMetadata.infoMessage && !credentialMetadata.warningMessageTitle && !credentialMetadata.warningMessageDescription) &&
<Alert variant="default" className="pf-u-mb-md" isInline isPlain title={Msg.localize(JSON.parse(credentialMetadata.infoMessage).key, JSON.parse(credentialMetadata.infoMessage).parameters)} />
}
{(credentialMetadata.warningMessageTitle && credentialMetadata.warningMessageDescription) &&
<Alert variant="warning" className="pf-u-mb-md" isInline title={Msg.localize(JSON.parse(credentialMetadata.warningMessageTitle).key, JSON.parse(credentialMetadata.warningMessageTitle).parameters)} style={maxWidth}>
<p>{Msg.localize(JSON.parse(credentialMetadata.warningMessageDescription).key, JSON.parse(credentialMetadata.warningMessageDescription).parameters)}</p>
</Alert>
}
<DataList aria-label="user credential" className="pf-u-mb-xl">
<DataListItem id={`${SigningInPage.credElementId(type, credentialMetadata.credential.id, 'row')}`} key={'credential-list-item-' + credentialMetadata.credential.id} aria-labelledby={'credential-list-item-' + credentialMetadata.credential.userLabel}> <DataListItem id={`${SigningInPage.credElementId(type, credentialMetadata.credential.id, 'row')}`} key={'credential-list-item-' + credentialMetadata.credential.id} aria-labelledby={'credential-list-item-' + credentialMetadata.credential.userLabel}>
<DataListItemRow key={'userCredentialRow-' + credentialMetadata.credential.id}> <DataListItemRow key={'userCredentialRow-' + credentialMetadata.credential.id} className="pf-u-align-items-center">
<DataListItemCells dataListCells={this.credentialRowCells(credentialMetadata, type)}/> <DataListItemCells dataListCells={this.credentialRowCells(credentialMetadata, type)}/>
<CredentialAction <CredentialAction
credential={credentialMetadata.credential} credential={credentialMetadata.credential}
@ -253,125 +308,164 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
/> />
</DataListItemRow> </DataListItemRow>
</DataListItem> </DataListItem>
</DataList>
</>
)) ))
} } </React.Fragment>
</React.Fragment>) )
} }
private credentialRowCells(credMetadata: CredMetadata, type: string): React.ReactNode[] { private credentialRowCells(credMetadata: CredMetadata, type: string): React.ReactNode[] {
const credRowCells: React.ReactNode[] = []; const credRowCells: React.ReactNode[] = [];
const credential = credMetadata.credential; const credential = credMetadata.credential;
const infoMessage = credMetadata.infoMessage ? JSON.parse(credMetadata.infoMessage) : null; let maxWidth = { "--pf-u-max-width--MaxWidth": "300px" } as React.CSSProperties;
const warningMessageTitle = credMetadata.warningMessageTitle ? JSON.parse(credMetadata.warningMessageTitle) : null;
const warningMessageDescription = credMetadata.warningMessageDescription ? JSON.parse(credMetadata.warningMessageDescription) : null;
credRowCells.push( credRowCells.push(
<DataListCell id={`${SigningInPage.credElementId(type, credential.id, 'label')}`} key={'userLabel-' + credential.id}> <DataListCell id={`${SigningInPage.credElementId(type, credential.id, 'label')}`} key={'userLabel-' + credential.id} className="pf-u-max-width" style={maxWidth}>
{credential.userLabel} {credential.userLabel}
{infoMessage &&
<div>{Msg.localize(infoMessage.key, infoMessage.parameters)}</div>
}
{warningMessageTitle &&
<>
<br />
<div className="pf-c-alert pf-m-warning pf-m-inline" aria-label="Success alert">
<div className="pf-c-alert__icon">
<i className="pficon-warning-triangle-o" aria-hidden="true"></i>
</div>
<h4 className="pf-c-alert__title">
<span className="pf-screen-reader">Warning alert:</span>
{Msg.localize(warningMessageTitle.key, warningMessageTitle.parameters)}
</h4>
{credMetadata.warningMessageDescription &&
<div className="pf-c-alert__description">
{Msg.localize(warningMessageDescription.key, warningMessageDescription.parameters)}
</div>
}
</div>
</>
}
</DataListCell> </DataListCell>
); );
if (credential.strCreatedDate) { if (credential.strCreatedDate) {
credRowCells.push(<DataListCell id={`${SigningInPage.credElementId(type, credential.id, 'created-at')}`} key={'created-' + credential.id}><strong><Msg msgKey='credentialCreatedAt'/>: </strong>{credential.strCreatedDate}</DataListCell>); credRowCells.push(
credRowCells.push(<DataListCell key={'spacer-' + credential.id}/>); <DataListCell
id={`${SigningInPage.credElementId(
type,
credential.id,
"created-at"
)}`}
key={"created-" + credential.id}
>
<strong className="pf-u-mr-md">
<Msg msgKey="credentialCreatedAt" />{" "}
</strong>
{credential.strCreatedDate}
</DataListCell>
);
credRowCells.push(<DataListCell key={"spacer-" + credential.id} />);
} }
return credRowCells; return credRowCells;
} }
private renderCredTypeTitle(credContainer: CredentialContainer, keycloak: KeycloakService): React.ReactNode { private renderCredTypeTitle(
if (!credContainer.hasOwnProperty('helptext') && !credContainer.hasOwnProperty('createAction')) return; credContainer: CredentialContainer,
keycloak: KeycloakService,
category: CredCategory
): React.ReactNode {
if (
!credContainer.hasOwnProperty("helptext") &&
!credContainer.hasOwnProperty("createAction")
)
return;
let setupAction: AIACommand; let setupAction: AIACommand;
if (credContainer.createAction) { if (credContainer.createAction) {
setupAction = new AIACommand(keycloak, credContainer.createAction); setupAction = new AIACommand(keycloak, credContainer.createAction);
} }
const credContainerDisplayName: string = Msg.localize(credContainer.displayName);
const credContainerDisplayName: string = Msg.localize(
credContainer.displayName
);
return ( return (
<React.Fragment key={'credTypeTitle-' + credContainer.type}> <React.Fragment key={"credTypeTitle-" + credContainer.type}>
<DataListItem aria-labelledby={'type-datalistitem-' + credContainer.type}> <Split className="pf-u-mt-lg pf-u-mb-lg">
<DataListItemRow key={'credTitleRow-' + credContainer.type}> <SplitItem>
<DataListItemCells <Title
dataListCells={[ headingLevel="h3"
<DataListCell width={5} key={'credTypeTitle-' + credContainer.type}> size="md"
<Title headingLevel={TitleLevel.h3} size='2xl'> className="pf-u-mb-md"
<strong id={`${credContainer.type}-cred-title`}><Msg msgKey={credContainer.displayName}/></strong> >
<span className="cred-title pf-u-display-block" id={`${credContainer.type}-cred-title`}>
<Msg msgKey={credContainer.displayName} />
</span>
</Title> </Title>
<span id={`${credContainer.type}-cred-help`}> <span id={`${credContainer.type}-cred-help`}>
{credContainer.helptext && <Msg msgKey={credContainer.helptext}/>} {credContainer.helptext && (
<Msg msgKey={credContainer.helptext} />
)}
</span> </span>
</DataListCell>, </SplitItem>
]}/> <SplitItem isFilled>
{credContainer.createAction && {credContainer.createAction && (
<DataListAction <div
aria-labelledby='create' id={"mob-setUpAction-" + credContainer.type}
aria-label='create action' className="pf-u-display-none-on-lg pf-u-float-right"
id={'mob-setUpAction-' + credContainer.type}
className={DataListActionVisibility.hiddenOnLg}
> >
<Dropdown <Dropdown
isPlain isPlain
position={DropdownPosition.right} position={DropdownPosition.right}
toggle={<KebabToggle onToggle={isOpen => { toggle={
<KebabToggle
onToggle={(isOpen) => {
credContainer.open = isOpen; credContainer.open = isOpen;
this.setState({ credentialContainers: new Map(this.state.credentialContainers) }); this.setState({
}} />} credentialContainers: new Map(
this.state.credentialContainers
),
});
}}
/>
}
isOpen={credContainer.open} isOpen={credContainer.open}
dropdownItems={[ dropdownItems={[
<button id={`mob-${credContainer.type}-set-up`} className="pf-c-button pf-m-link" type="button" onClick={() => setupAction.execute()}> <button
<span className="pf-c-button__icon"> id={`mob-${credContainer.type}-set-up`}
<i className="fas fa-plus-circle" aria-hidden="true"></i> className="pf-c-button pf-m-link"
</span> type="button"
<Msg msgKey='setUpNew' params={[credContainerDisplayName]} /> onClick={() =>
</button>]} setupAction.execute()
/> }
</DataListAction>}
{credContainer.createAction &&
<DataListAction
aria-labelledby='create'
aria-label='create action'
id={'setUpAction-' + credContainer.type}
className={css(DataListActionVisibility.visibleOnLg, DataListActionVisibility.hidden)}
> >
<button id={`${credContainer.type}-set-up`} className="pf-c-button pf-m-link" type="button" onClick={()=> setupAction.execute()}>
<span className="pf-c-button__icon"> <span className="pf-c-button__icon">
<i className="fas fa-plus-circle" aria-hidden="true"></i> <i
className="fas fa-plus-circle"
aria-hidden="true"
></i>
</span> </span>
<Msg msgKey='setUpNew' params={[credContainerDisplayName]}/> <Msg
msgKey="setUpNew"
params={[
credContainerDisplayName,
]}
/>
</button>,
]}
/>
</div>
)}
{credContainer.createAction && (
<div
id={"setUpAction-" + credContainer.type}
className="pf-u-display-none pf-u-display-inline-flex-on-lg pf-u-float-right"
>
<button
id={`${credContainer.type}-set-up`}
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={[credContainerDisplayName]}
/>
</button> </button>
</DataListAction>} </div>
</DataListItemRow> )}
</DataListItem> </SplitItem>
</Split>
</React.Fragment> </React.Fragment>
) );
}
} }
};
type CredRemover = (credentialId: string, userLabel: string) => void; type CredRemover = (credentialId: string, userLabel: string) => void;
interface CredentialActionProps { interface CredentialActionProps {
credential: UserCredential; credential: UserCredential;
removeable: boolean; removeable: boolean;
@ -383,27 +477,47 @@ class CredentialAction extends React.Component<CredentialActionProps> {
render(): React.ReactNode { render(): React.ReactNode {
if (this.props.updateAction) { if (this.props.updateAction) {
return ( return (
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'updateAction-' + this.props.credential.id}> <DataListAction
<Button id={`${SigningInPage.credElementId(this.props.credential.type, this.props.credential.id, 'update')}`} variant='primary'onClick={()=> this.props.updateAction.execute()}><Msg msgKey='update'/></Button> aria-labelledby={Msg.localize('updateCredAriaLabel')}
aria-label={Msg.localize('updateCredAriaLabel')}
id={"updateAction-" + this.props.credential.id}
>
<Button
variant="secondary"
id={`${SigningInPage.credElementId(
this.props.credential.type,
this.props.credential.id,
"update"
)}`}
onClick={() => this.props.updateAction.execute()}
>
<Msg msgKey="update" />
</Button>
</DataListAction> </DataListAction>
) );
} }
if (this.props.removeable) { if (this.props.removeable) {
const userLabel: string = this.props.credential.userLabel; const userLabel: string = this.props.credential.userLabel;
return ( return (
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'removeAction-' + this.props.credential.id }> <DataListAction
<ContinueCancelModal buttonTitle='remove' aria-label={Msg.localize('removeCredAriaLabel')}
aria-labelledby={Msg.localize('removeCredAriaLabel')}
id={'removeAction-' + this.props.credential.id }
>
<ContinueCancelModal
buttonTitle='remove'
buttonVariant='danger'
buttonId={`${SigningInPage.credElementId(this.props.credential.type, this.props.credential.id, 'remove')}`} buttonId={`${SigningInPage.credElementId(this.props.credential.type, this.props.credential.id, 'remove')}`}
modalTitle={Msg.localize('removeCred', [userLabel])} modalTitle={Msg.localize('removeCred', [userLabel])}
modalMessage={Msg.localize('stopUsingCred', [userLabel])} modalMessage={Msg.localize('stopUsingCred', [userLabel])}
onContinue={() => this.props.credRemover(this.props.credential.id, userLabel)} onContinue={() => this.props.credRemover(this.props.credential.id, userLabel)}
/> />
</DataListAction> </DataListAction>
) );
} }
return (<></>) return <></>;
} }
} }

View file

@ -15,7 +15,7 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { Modal, Button, ButtonProps } from '@patternfly/react-core'; import { Modal, ModalVariant, Button, ButtonProps } from '@patternfly/react-core';
import {Msg} from './Msg'; import {Msg} from './Msg';
/** /**
@ -34,7 +34,6 @@ interface ContinueCancelModalProps {
onContinue: () => void; onContinue: () => void;
onClose?: () => void; onClose?: () => void;
isDisabled?: boolean; isDisabled?: boolean;
isLarge?: boolean;
} }
interface ContinueCancelModalState { interface ContinueCancelModalState {
@ -52,8 +51,7 @@ export class ContinueCancelModal extends React.Component<ContinueCancelModalProp
buttonVariant: 'primary', buttonVariant: 'primary',
modalContinueButtonLabel: 'continue', modalContinueButtonLabel: 'continue',
modalCancelButtonLabel: 'doCancel', modalCancelButtonLabel: 'doCancel',
isDisabled: false, isDisabled: false
isSmall: true
}; };
public constructor(props: ContinueCancelModalProps) { public constructor(props: ContinueCancelModalProps) {
@ -87,15 +85,16 @@ export class ContinueCancelModal extends React.Component<ContinueCancelModalProp
{this.props.render && this.props.render(this.handleModalToggle)} {this.props.render && this.props.render(this.handleModalToggle)}
<Modal <Modal
{...this.props} {...this.props}
variant={ModalVariant.small}
title={Msg.localize(this.props.modalTitle)} title={Msg.localize(this.props.modalTitle)}
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={this.handleModalToggle} onClose={this.handleModalToggle}
actions={[ actions={[
<Button id='modal-cancel' key="cancel" variant="secondary" onClick={this.handleModalToggle}>
<Msg msgKey={this.props.modalCancelButtonLabel!}/>
</Button>,
<Button id='modal-confirm' key="confirm" variant="primary" onClick={this.handleContinue}> <Button id='modal-confirm' key="confirm" variant="primary" onClick={this.handleContinue}>
<Msg msgKey={this.props.modalContinueButtonLabel!}/> <Msg msgKey={this.props.modalContinueButtonLabel!}/>
</Button>,
<Button id='modal-cancel' key="cancel" variant="secondary" onClick={this.handleModalToggle}>
<Msg msgKey={this.props.modalCancelButtonLabel!}/>
</Button> </Button>
]} ]}
> >

View file

@ -20,15 +20,14 @@ import {
EmptyStateVariant, EmptyStateVariant,
Title, Title,
EmptyStateIcon, EmptyStateIcon,
TitleLevel,
EmptyStateBody, EmptyStateBody,
IconProps,
} from '@patternfly/react-core' } from '@patternfly/react-core'
import { Msg } from './Msg'; import { Msg } from './Msg';
import {SVGIconProps} from '@patternfly/react-icons/dist/esm/createIcon';
export interface EmptyMessageStateProps { export interface EmptyMessageStateProps {
icon: React.FunctionComponent<IconProps>; icon: React.ComponentType<SVGIconProps>;
messageKey: string; messageKey: string;
} }
@ -41,7 +40,7 @@ export default class EmptyMessageState extends React.Component<EmptyMessageState
return ( return (
<EmptyState variant={EmptyStateVariant.full}> <EmptyState variant={EmptyStateVariant.full}>
<EmptyStateIcon icon={this.props.icon} /> <EmptyStateIcon icon={this.props.icon} />
<Title headingLevel={TitleLevel.h5} size="lg"> <Title headingLevel="h5" size="lg">
<Msg msgKey={this.props.messageKey} /> <Msg msgKey={this.props.messageKey} />
</Title> </Title>
<EmptyStateBody> <EmptyStateBody>

File diff suppressed because it is too large Load diff

View file

@ -10,25 +10,28 @@
"check-types:watch": "npm run check-types -- -w", "check-types:watch": "npm run check-types -- -w",
"lint": "eslint ./app/**/*.ts*", "lint": "eslint ./app/**/*.ts*",
"move-web_modules": "shx mv web_modules ../../../keycloak/common/resources", "move-web_modules": "shx mv web_modules ../../../keycloak/common/resources",
"copy-pf-resources": "npm run move-app-css && npm run copy-base-css && npm run copy-fonts && npm run copy-pficon", "copy-pf-resources": "npm run move-app-css && npm run copy-base-css && npm run copy-fonts && npm run copy-pficon && npm run copy-addons",
"move-app-css": "shx mkdir -p ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles && shx mv app.css ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles", "move-app-css": "shx mkdir -p ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles && shx mv app.css ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles",
"copy-base-css": "shx mkdir -p ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles && shx cp node_modules/@patternfly/react-core/dist/styles/base.css ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles", "copy-base-css": "shx mkdir -p ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles && shx cp node_modules/@patternfly/react-core/dist/styles/base.css ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles",
"copy-fonts": "shx mkdir -p ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles/assets/fonts/overpass-webfont && shx cp node_modules/@patternfly/react-core/dist/styles/assets/fonts/overpass-webfont/overpass*.woff2 ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles/assets/fonts/overpass-webfont", "copy-fonts": "shx mkdir -p ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles/assets/fonts && shx cp -r node_modules/@patternfly/react-core/dist/styles/assets/fonts/* ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles/assets/fonts/",
"copy-pficon": "shx mkdir -p ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles/assets/pficon && shx cp node_modules/@patternfly/react-core/dist/styles/assets/pficon/pficon.woff2 ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles/assets/pficon" "copy-pficon": "shx mkdir -p ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles/assets/pficon && shx cp node_modules/@patternfly/react-core/dist/styles/assets/pficon/pficon.woff2 ../../../keycloak/common/resources/web_modules/@patternfly/react-core/dist/styles/assets/pficon",
"copy-addons": "shx mkdir -p ../../../keycloak/common/resources/web_modules/@patternfly/patternfly && shx cp node_modules/@patternfly/patternfly/patternfly-addons.css ../../../keycloak/common/resources/web_modules/@patternfly/patternfly"
}, },
"keywords": [], "keywords": [],
"author": "Stan Silvert", "author": "Stan Silvert",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@patternfly/react-core": "^3.153.3", "@patternfly/patternfly": "^4.125.3",
"@patternfly/react-icons": "^3.15.16", "@patternfly/react-core": "^4.147.0",
"@patternfly/react-styles": "^3.7.14", "@patternfly/react-icons": "^4.11.8",
"@patternfly/react-styles": "^4.11.8",
"react": "npm:@pika/react@^16.13.1", "react": "npm:@pika/react@^16.13.1",
"react-dom": "npm:@pika/react-dom@^16.13.1", "react-dom": "npm:@pika/react-dom@^16.13.1",
"react-router-dom": "^4.3.1" "react-router-dom": "^4.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.8.4", "@babel/cli": "^7.8.4",
"@babel/compat-data": "^7.9.0",
"@babel/core": "^7.8.7", "@babel/core": "^7.8.7",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/preset-env": "^7.13.15", "@babel/preset-env": "^7.13.15",
@ -41,6 +44,7 @@
"@typescript-eslint/eslint-plugin": "^1.4.2", "@typescript-eslint/eslint-plugin": "^1.4.2",
"@typescript-eslint/parser": "^1.4.2", "@typescript-eslint/parser": "^1.4.2",
"babel-eslint": "^9.0.0", "babel-eslint": "^9.0.0",
"chokidar": "^3.5.3",
"eslint": "^5.15.1", "eslint": "^5.15.1",
"eslint-config-react-app": "^3.0.8", "eslint-config-react-app": "^3.0.8",
"eslint-plugin-flowtype": "^2.50.3", "eslint-plugin-flowtype": "^2.50.3",

View file

@ -11,7 +11,8 @@
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true, "strictNullChecks": true,
"jsx": "react", "jsx": "react",
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true,
"skipLibCheck": true
}, },
"include": [ "include": [
"./app/**/*.ts?" "./app/**/*.ts?"

View file

@ -1,6 +1,8 @@
parent=base parent=base
deprecatedMode=false deprecatedMode=false
scripts=welcome-page-scripts.js scripts=welcome-page-scripts.js
developmentMode=false developmentMode=false
# This is the logo in upper lefthand corner. # This is the logo in upper lefthand corner.