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")
.removeable(true);
UserModel user = metadataContext.getUser();
if (user != null && !isConfiguredFor(session.getContext().getRealm(), user, getType())) {
builder.createAction(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name());
} else {
builder.updateAction(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name());
}
builder.createAction(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name());
return builder.build(session);
}

View file

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

View file

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

View file

@ -1357,7 +1357,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
loginPage.login("realm-admin", "password");
Assert.assertTrue(applicationsPage.isCurrent());
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();
}

View file

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

View file

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

View file

@ -28,6 +28,6 @@ public class PageNotFound extends AbstractLoggedInPage {
@Override
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>
*/
public class SigningInTest extends BaseAccountPageTest {
public static final String PASSWORD_LABEL = "My Password";
public static final String PASSWORD_LABEL = "My password";
@Page
private SigningInPage signingInPage;
@ -91,8 +91,8 @@ public class SigningInTest extends BaseAccountPageTest {
testContext.setTestRealmReps(emptyList()); // reimport realm after this test
assertThat(signingInPage.getCategoriesCount(), is(2));
assertThat(signingInPage.getCategoryTitle("basic-authentication"), is("Basic Authentication"));
assertThat(signingInPage.getCategoryTitle("two-factor"), is("Two-Factor Authentication"));
assertThat(signingInPage.getCategoryTitle("basic-authentication"), is("Basic authentication"));
assertThat(signingInPage.getCategoryTitle("two-factor"), is("Two-factor authentication"));
}
@Test
@ -160,7 +160,7 @@ public class SigningInTest extends BaseAccountPageTest {
signingInPage.assertCurrent();
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 label2 = "OTP is inconvenient";

View file

@ -3,11 +3,16 @@ doCancel=Cancel
doLogOutAllSessions=Log out all sessions
doRemove=Remove
doAdd=Add
doSignOut=Sign Out
doSignOut=Sign out
doLogIn=Log In
doLink=Link
noAccessMessage=Access not allowed
personalInfoSidebarTitle=Personal info
accountSecuritySidebarTitle=Account security
signingInSidebarTitle=Signing in
deviceActivitySidebarTitle=Device activity
linkedAccountsSidebarTitle=Linked accounts
editAccountHtmlTitle=Edit Account
personalInfoHtmlTitle=Personal Info
@ -19,7 +24,7 @@ sessionsHtmlTitle=Sessions
accountManagementTitle=Keycloak Account Management
authenticatorTitle=Authenticator
applicationsHtmlTitle=Applications
linkedAccountsHtmlTitle=Linked Accounts
linkedAccountsHtmlTitle=Linked accounts
accountManagementWelcomeMessage=Welcome to Keycloak Account Management
personalInfoIntroMessage=Manage your basic information
@ -32,7 +37,7 @@ updatePasswordTitle=Update 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.
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
email=Email
@ -94,7 +99,7 @@ role_offline-access=Offline access
role_uma_authorization=Obtain permissions
client_account=Account
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_realm-management=Realm Management
client_broker=Broker
@ -120,7 +125,7 @@ applications=Applications
account=Account
federatedIdentity=Federated Identity
authenticator=Authenticator
device-activity=Device Activity
device-activity=Device activity
sessions=Sessions
log=Log

View file

@ -48,7 +48,7 @@
<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="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??>
<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-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-label-default=Recovery codes
# WebAuthn
webauthn-display-name=Security Key

View file

@ -112,8 +112,10 @@
</#list>
</#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/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"/>
</head>
@ -181,18 +183,23 @@
</div>
<div class="pf-c-page__header-tools">
<#if referrer?has_content && referrer_uri?has_content>
<div class="pf-c-page__header-tools-group pf-m-icons">
<a id="landingReferrerLink" href="${referrer_uri}" id="referrer" tabindex="0"><span class="pf-icon pf-icon-arrow"></span>${msg("backTo",referrerName)}</a>
<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}" 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>
</#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="landingSignOutButton" tabindex="0" style="display:none" onclick="keycloak.logout();" class="pf-c-button pf-m-primary" type="button">${msg("doSignOut")}</button>
</div>
<!-- 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 -->
<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>
@ -200,7 +207,7 @@
<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>
<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>
</#if>
@ -220,45 +227,51 @@
</header>
<main role="main" class="pf-c-page__main">
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content" id="landingWelcomeMessage">
<h1>${msg("accountManagementWelcomeMessage")}</h1>
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<#assign content=theme.apply("content.json")?eval>
<#list content as item>
<div class="pf-l-gallery__item pf-c-card" id="landing-${item.id}">
<div>
<div class="pf-c-card__header pf-c-content">
<h2>
<#if item.icon??>
<i class="pf-icon ${item.icon}"></i>&nbsp;
<#elseif item.iconSvg??>
<img src="${item.iconSvg}" alt="icon"/>&nbsp;
</#if>
${msg(item.label)}
</h2>
<#if item.descriptionLabel??>
<p>${msg(item.descriptionLabel)}</p>
</#if>
</div>
<div class="pf-c-card__body pf-c-content">
<#if item.content??>
<#list item.content as sub>
<div id="landing-${sub.id}">
<a onclick="toggleReact(); window.location.hash='${sub.path}'">${msg(sub.label)}</a>
</div>
</#list>
<#else>
<a id="landing-${item.id}" onclick="toggleReact(); window.location.hash = '${item.path}'">${msg(item.label)}</a>
</#if>
</div>
<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">
<h1>${msg("accountManagementWelcomeMessage")}</h1>
</div>
</div>
</#list>
</div>
</div>
</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">
<#assign content=theme.apply("content.json")?eval>
<#list content as item>
<div class="pf-l-gallery__item" id="landing-${item.id}">
<div class="pf-c-card pf-m-full-height">
<div>
<div class="pf-c-card__title pf-c-content">
<h2 class="pf-u-display-flex pf-u-w-100 pf-u-flex-direction-column">
<#if item.icon??>
<i class="pf-icon ${item.icon}"></i>
<#elseif item.iconSvg??>
<img src="${item.iconSvg}" alt="icon"/>
</#if>
${msg(item.label)}
</h2>
</div>
<div class="pf-c-card__body">
<#if item.descriptionLabel??>
<p class="pf-u-mb-md">${msg(item.descriptionLabel)}</p>
</#if>
<#if item.content??>
<#list item.content as sub>
<div id="landing-${sub.id}">
<a onclick="toggleReact(); window.location.hash='${sub.path}'">${msg(sub.label)}</a>
</div>
</#list>
<#else>
<a id="landing-${item.id}" onclick="toggleReact(); window.location.hash = '${item.path}'">${msg(item.label)}</a>
</#if>
</div>
</div>
</div>
</div>
</#list>
</div>
</div>
</section>
</main>
</div>

View file

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

View file

@ -3,7 +3,7 @@
"id": "personal-info",
"path": "personal-info",
"icon": "pf-icon-user",
"label": "personalInfoHtmlTitle",
"label": "personalInfoSidebarTitle",
"descriptionLabel": "personalInfoIntroMessage",
"modulePath": "/content/account-page/AccountPage.js",
"componentName": "AccountPage"
@ -11,27 +11,27 @@
{
"id": "security",
"icon": "pf-icon-security",
"label": "accountSecurityTitle",
"label": "accountSecuritySidebarTitle",
"descriptionLabel": "accountSecurityIntroMessage",
"content": [
{
"id": "signingin",
"path": "security/signingin",
"label": "signingIn",
"label": "signingInSidebarTitle",
"modulePath": "/content/signingin-page/SigningInPage.js",
"componentName": "SigningInPage"
},
{
"id": "device-activity",
"path": "security/device-activity",
"label": "device-activity",
"label": "deviceActivitySidebarTitle",
"modulePath": "/content/device-activity-page/DeviceActivityPage.js",
"componentName": "DeviceActivityPage"
},
{
"id": "linked-accounts",
"path": "security/linked-accounts",
"label": "linkedAccountsHtmlTitle",
"label": "linkedAccountsSidebarTitle",
"modulePath": "/content/linked-accounts-page/LinkedAccountsPage.js",
"componentName": "LinkedAccountsPage",
"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 {
height: 35px;
}
@ -12,5 +13,114 @@
width: 120px;
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 {PageNav} from './PageNav';
import {PageToolbar} from './PageToolbar';
import {PageHeaderTool} from './PageHeaderTool';
import {makeRoutes} from './ContentPages';
import {
Brand,
Page,
PageHeader,
PageSection,
PageSidebar,
PageSidebar
} from '@patternfly/react-core';
import { KeycloakContext } from './keycloak-service/KeycloakContext';
@ -61,11 +60,11 @@ export class App extends React.Component<AppProps> {
const username = (
<span style={{marginLeft: '10px'}} id="loggedInUser">{loggedInUserName()}</span>
);
const Header = (
<PageHeader
logo={<a id="brandLink" href={brandUrl}><Brand src={brandImg} alt="Logo" className="brand"/></a>}
toolbar={<PageToolbar/>}
avatar={username}
headerTools={<PageHeaderTool/>}
showNavToggle
/>
);
@ -73,13 +72,9 @@ export class App extends React.Component<AppProps> {
const Sidebar = <PageSidebar nav={<PageNav/>} />;
return (
<span style={{ height: '100%'}}>
<Page header={Header} sidebar={Sidebar} isManagedSidebar>
<PageSection>
{makeRoutes()}
</PageSection>
</Page>
</span>
<Page header={Header} sidebar={Sidebar} isManagedSidebar>
{makeRoutes()}
</Page>
);
}
};
};

View file

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

View file

@ -110,4 +110,4 @@ flattenContent(content).forEach((item: ContentItem) => {
Promise.all(moduleLoaders).then(() => {
const domContainer = document.querySelector('#main_react_container');
ReactDOM.render(e(Main), domContainer);
});
});

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 {
const kebabDropdownItems = [];
if (this.hasReferrer) {
kebabDropdownItems.push(
<ReferrerDropdownItem key='referrerDropdownItem'/>
)
}
kebabDropdownItems.push(<LogoutDropdownItem key='LogoutDropdownItem'/>);
return (
<Toolbar>
{this.hasReferrer &&
<ToolbarGroup key='referrerGroup'>
<ToolbarGroup key='referrerGroup' alignment={{default:"alignRight"}}>
<ToolbarItem className="pf-m-icons" key='referrer'>
<ReferrerLink/>
</ToolbarItem>
</ToolbarGroup>
}
<ToolbarGroup key='secondGroup'>
<ToolbarGroup key='secondGroup' alignment={{default:"alignRight"}}>
<ToolbarItem className="pf-m-icons" key='logout'>
<LogoutButton/>
</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>
</Toolbar>
);

View file

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

View file

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

View file

@ -14,7 +14,21 @@
* limitations under the License.
*/
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 { AccountServiceContext } from '../../account-service/AccountServiceContext';
@ -144,133 +158,188 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
public render(): React.ReactNode {
const fields: FormFields = this.state.formFields;
return (
<ContentPage title="personalInfoHtmlTitle"
introMessage="personalSubMessage">
<Form isHorizontal onSubmit={event => this.handleSubmit(event)}>
{!this.isRegistrationEmailAsUsername &&
<FormGroup
label={Msg.localize('username')}
isRequired
fieldId="user-name"
helperTextInvalid={this.state.errors.username}
isValid={this.state.errors.username === ''}
>
{this.isEditUserNameAllowed && <this.UsernameInput />}
{!this.isEditUserNameAllowed && <this.RestrictedUsernameInput />}
</FormGroup>
}
<FormGroup
label={Msg.localize('email')}
isRequired
fieldId="email-address"
helperTextInvalid={this.state.errors.email}
isValid={this.state.errors.email === ''}
<ContentPage
title="personalInfoHtmlTitle"
introMessage="personalSubMessage"
>
<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"
>
<TextInput
isRequired
type="email"
id="email-address"
name="email"
maxLength={254}
value={fields.email}
onChange={this.handleChange}
isValid={this.state.errors.email === ''}
>
</TextInput>
</FormGroup>
<FormGroup
label={Msg.localize('firstName')}
isRequired
fieldId="first-name"
helperTextInvalid={this.state.errors.firstName}
isValid={this.state.errors.firstName === ''}
>
<TextInput
isRequired
type="text"
id="first-name"
name="firstName"
maxLength={254}
value={fields.firstName}
onChange={this.handleChange}
isValid={this.state.errors.firstName === ''}
>
</TextInput>
</FormGroup>
<FormGroup
label={Msg.localize('lastName')}
isRequired
fieldId="last-name"
helperTextInvalid={this.state.errors.lastName}
isValid={this.state.errors.lastName === ''}
>
<TextInput
isRequired
type="text"
id="last-name"
name="lastName"
maxLength={254}
value={fields.lastName}
onChange={this.handleChange}
isValid={this.state.errors.lastName === ''}
>
</TextInput>
</FormGroup>
{features.isInternationalizationEnabled && <FormGroup
label={Msg.localize('selectLocale')}
isRequired
fieldId="locale"
>
<LocaleSelector id="locale-selector"
value={fields.attributes!.locale || ''}
onChange={value => this.setState({
errors: this.state.errors,
formFields: { ...this.state.formFields, attributes: { ...this.state.formFields.attributes, locale: [value] }}
})}
/>
</FormGroup>}
<ActionGroup>
<Button
type="submit"
id="save-btn"
variant="primary"
isDisabled={Object.values(this.state.errors).filter(e => e !== '').length !== 0}
>
<Msg msgKey="doSave" />
</Button>
<Button
id="cancel-btn"
variant="secondary"
onClick={this.handleCancel}
>
<Msg msgKey="doCancel" />
</Button>
</ActionGroup>
</Form>
{ this.isDeleteAccountAllowed &&
<div id="delete-account" style={{marginTop:"30px"}}>
<Expandable toggleText={Msg.localize('deleteAccount')}>
<Grid gutter={"sm"}>
<GridItem span={6}>
<p>
<Msg msgKey="deleteAccountWarning" />
</p>
</GridItem>
<GridItem span={4}>
<KeycloakContext.Consumer>
{ (keycloak: KeycloakService) => (
<Button id="delete-account-btn" variant="danger" onClick={() => this.handleDelete(keycloak)} className="delete-button"><Msg msgKey="doDelete" /></Button>
{!this.isRegistrationEmailAsUsername && (
<FormGroup
label={Msg.localize("username")}
fieldId="user-name"
helperTextInvalid={this.state.errors.username}
validated={
this.state.errors.username !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
>
{this.isEditUserNameAllowed && <this.UsernameInput />}
{!this.isEditUserNameAllowed && (
<this.RestrictedUsernameInput />
)}
</KeycloakContext.Consumer>
</GridItem>
<GridItem span={2}>
</GridItem>
</Grid>
</Expandable>
</div>}
</ContentPage>
</FormGroup>
)}
<FormGroup
label={Msg.localize("email")}
fieldId="email-address"
helperTextInvalid={this.state.errors.email}
validated={
this.state.errors.email !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
>
<TextInput
isRequired
type="email"
id="email-address"
name="email"
maxLength={254}
value={fields.email}
onChange={this.handleChange}
validated={
this.state.errors.email !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
></TextInput>
</FormGroup>
<FormGroup
label={Msg.localize("firstName")}
fieldId="first-name"
helperTextInvalid={this.state.errors.firstName}
validated={
this.state.errors.firstName !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
>
<TextInput
isRequired
type="text"
id="first-name"
name="firstName"
maxLength={254}
value={fields.firstName}
onChange={this.handleChange}
validated={
this.state.errors.firstName !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
></TextInput>
</FormGroup>
<FormGroup
label={Msg.localize("lastName")}
fieldId="last-name"
helperTextInvalid={this.state.errors.lastName}
validated={
this.state.errors.lastName !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
>
<TextInput
isRequired
type="text"
id="last-name"
name="lastName"
maxLength={254}
value={fields.lastName}
onChange={this.handleChange}
validated={
this.state.errors.lastName !== ""
? ValidatedOptions.error
: ValidatedOptions.default
}
></TextInput>
</FormGroup>
{features.isInternationalizationEnabled && (
<FormGroup
label={Msg.localize("selectLocale")}
isRequired
fieldId="locale"
>
<LocaleSelector
id="locale-selector"
value={fields.attributes!.locale || ""}
onChange={(value) =>
this.setState({
errors: this.state.errors,
formFields: {
...this.state.formFields,
attributes: {
...this.state.formFields.attributes,
locale: [value],
},
},
})
}
/>
</FormGroup>
)}
<ActionGroup>
<Button
type="submit"
id="save-btn"
variant="primary"
isDisabled={
Object.values(this.state.errors).filter((e) => e !== "")
.length !== 0
}
>
<Msg msgKey="doSave" />
</Button>
<Button
id="cancel-btn"
variant="link"
onClick={this.handleCancel}
>
<Msg msgKey="doCancel" />
</Button>
</ActionGroup>
</Form>
{this.isDeleteAccountAllowed && (
<div id="delete-account" style={{ marginTop: "30px" }}>
<ExpandableSection toggleText="Delete Account">
<Grid hasGutter>
<GridItem span={6}>
<p>
<Msg msgKey="deleteAccountWarning" />
</p>
</GridItem>
<GridItem span={4}>
<KeycloakContext.Consumer>
{(keycloak: KeycloakService) => (
<Button
id="delete-account-btn"
variant="danger"
onClick={() => this.handleDelete(keycloak)}
className="delete-button"
>
<Msg msgKey="doDelete" />
</Button>
)}
</KeycloakContext.Consumer>
</GridItem>
<GridItem span={2}></GridItem>
</Grid>
</ExpandableSection>
</div>
)}
</PageSection>
</ContentPage>
);
}
@ -283,14 +352,14 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
maxLength={254}
value={this.state.formFields.username}
onChange={this.handleChange}
isValid={this.state.errors.username === ''}
>
validated={this.state.errors.username !== '' ? ValidatedOptions.error : ValidatedOptions.default}
>
</TextInput>
);
private RestrictedUsernameInput = () => (
<TextInput
isDisabled
isReadOnly
type="text"
id="user-name"
name="username"
@ -298,4 +367,4 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
>
</TextInput>
);
};
};

View file

@ -23,12 +23,12 @@ import {Msg} from '../../widgets/Msg';
import {
Title,
TitleLevel,
Button,
EmptyState,
EmptyStateVariant,
EmptyStateIcon,
EmptyStateBody
EmptyStateBody,
TitleSizes
} from '@patternfly/react-core';
import { PassportIcon } from '@patternfly/react-icons';
import { KeycloakService } from '../../keycloak-service/keycloak.service';
@ -67,7 +67,7 @@ class ApplicationInitiatedActionPage extends React.Component<AppInitiatedActionP
return (
<EmptyState variant={EmptyStateVariant.full}>
<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}/>
</Title>
<EmptyStateBody>
@ -88,4 +88,4 @@ class ApplicationInitiatedActionPage extends React.Component<AppInitiatedActionP
// Note that the class name is not exported above. To get access to the router,
// we use withRouter() and export a different name.
export const AppInitiatedActionPage = withRouter(ApplicationInitiatedActionPage);
export const AppInitiatedActionPage = withRouter(ApplicationInitiatedActionPage);

View file

@ -24,12 +24,23 @@ import {
DataListToggle,
DataListContent,
DataListItemCells,
DescriptionList,
DescriptionListTerm,
DescriptionListGroup,
DescriptionListDescription,
Grid,
GridItem,
Button,
PageSection,
PageSectionVariants,
Stack,
StackItem,
SplitItem,
Split,
TextContent
} 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 { ContinueCancelModal } from '../../widgets/ContinueCancelModal';
import { HttpResponse } from '../../account-service/account.service';
@ -118,106 +129,122 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
public render(): React.ReactNode {
return (
<ContentPage title={Msg.localize('applicationsPageTitle')}>
<DataList id="applications-list" aria-label={Msg.localize('applicationsPageTitle')} isCompact>
<DataListItem id="applications-list-header" aria-labelledby="Columns names">
<DataListItemRow>
// invisible toggle allows headings to line up properly
<span style={{ visibility: 'hidden' }}>
<DataListToggle
isExpanded={false}
id='applications-list-header-invisible-toggle'
aria-controls="hidden"
/>
</span>
<DataListItemCells
dataListCells={[
<DataListCell key='applications-list-client-id-header' width={2}>
<strong><Msg msgKey='applicationName' /></strong>
</DataListCell>,
<DataListCell key='applications-list-app-type-header' width={2}>
<strong><Msg msgKey='applicationType' /></strong>
</DataListCell>,
<DataListCell key='applications-list-status' width={2}>
<strong><Msg msgKey='status' /></strong>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
{this.state.applications.map((application: Application, appIndex: number) => {
return (
<DataListItem id={this.elementId("client-id", application)} key={'application-' + appIndex} aria-labelledby="applications-list" isExpanded={this.state.isRowOpen[appIndex]}>
<ContentPage
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">
<DataListItemRow>
<DataListToggle
onClick={() => this.onToggle(appIndex)}
isExpanded={this.state.isRowOpen[appIndex]}
id={this.elementId('toggle', application)}
aria-controls={this.elementId("expandable", application)}
/>
// invisible toggle allows headings to line up properly
<span style={{ visibility: 'hidden', height: 55 }}>
<DataListToggle
isExpanded={false}
id='applications-list-header-invisible-toggle'
aria-controls="hidden"
/>
</span>
<DataListItemCells
dataListCells={[
<DataListCell id={this.elementId('name', application)} width={2} key={'app-' + appIndex}>
<Button component="a" variant="link" onClick={() => window.open(application.effectiveUrl)}>
{application.clientName || application.clientId} <ExternalLinkAltIcon/>
</Button>
<DataListCell key='applications-list-client-id-header' width={2} className="pf-u-pt-md">
<strong><Msg msgKey='applicationName' /></strong>
</DataListCell>,
<DataListCell id={this.elementId('internal', application)} width={2} key={'internal-' + appIndex}>
{application.userConsentRequired ? Msg.localize('thirdPartyApp') : Msg.localize('internalApp')}
{application.offlineAccess ? ', ' + Msg.localize('offlineAccess') : ''}
<DataListCell key='applications-list-app-type-header' width={2} className="pf-u-pt-md">
<strong><Msg msgKey='applicationType' /></strong>
</DataListCell>,
<DataListCell key='applications-list-status' width={2} className="pf-u-pt-md">
<strong><Msg msgKey='status' /></strong>
</DataListCell>,
<DataListCell id={this.elementId('status', application)} width={2} key={'status-' + appIndex}>
{application.inUse ? Msg.localize('inUse') : Msg.localize('notInUse')}
</DataListCell>
]}
/>
</DataListItemRow>
<DataListContent
noPadding={false}
aria-label={Msg.localize('applicationDetails')}
id={this.elementId("expandable", application)}
isHidden={!this.state.isRowOpen[appIndex]}
>
<Grid sm={6} md={6} lg={6}>
<div className='pf-c-content'>
<GridItem><strong>{Msg.localize('client') + ': '}</strong> {application.clientId}</GridItem>
</DataListItem>
{this.state.applications.map((application: Application, appIndex: number) => {
return (
<DataListItem id={this.elementId("client-id", application)} key={'application-' + appIndex} aria-labelledby="applications-list" isExpanded={this.state.isRowOpen[appIndex]}>
<DataListItemRow className="pf-u-align-items-center">
<DataListToggle
onClick={() => this.onToggle(appIndex)}
isExpanded={this.state.isRowOpen[appIndex]}
id={this.elementId('toggle', application)}
aria-controls={this.elementId("expandable", application)}
/>
<DataListItemCells
className="pf-u-align-items-center"
dataListCells={[
<DataListCell id={this.elementId('name', application)} width={2} key={'app-' + appIndex}>
<Button className="pf-u-pl-0 title-case" component="a" variant="link" onClick={() => window.open(application.effectiveUrl)}>
{application.clientName || application.clientId} <ExternalLinkAltIcon/>
</Button>
</DataListCell>,
<DataListCell id={this.elementId('internal', application)} width={2} key={'internal-' + appIndex}>
{application.userConsentRequired ? Msg.localize('thirdPartyApp') : Msg.localize('internalApp')}
{application.offlineAccess ? ', ' + Msg.localize('offlineAccess') : ''}
</DataListCell>,
<DataListCell id={this.elementId('status', application)} width={2} key={'status-' + appIndex}>
{application.inUse ? Msg.localize('inUse') : Msg.localize('notInUse')}
</DataListCell>
]}
/>
</DataListItemRow>
<DataListContent
className="pf-u-pl-35xl"
hasNoPadding={false}
aria-label={Msg.localize('applicationDetails')}
id={this.elementId("expandable", application)}
isHidden={!this.state.isRowOpen[appIndex]}
>
<DescriptionList>
<DescriptionListGroup>
<DescriptionListTerm>{Msg.localize('client')}</DescriptionListTerm>
<DescriptionListDescription>{application.clientId}</DescriptionListDescription>
</DescriptionListGroup>
{application.description &&
<GridItem><strong>{Msg.localize('description') + ': '}</strong> {application.description}</GridItem>
<DescriptionListGroup>
<DescriptionListTerm>{Msg.localize('description')}</DescriptionListTerm>
<DescriptionListDescription>{application.description}</DescriptionListDescription>
</DescriptionListGroup>
}
{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 &&
<React.Fragment>
<GridItem span={12}>
<strong>Has access to:</strong>
</GridItem>
{application.consent.grantedScopes.map((scope: GrantedScope, scopeIndex: number) => {
return (
<React.Fragment key={'scope-' + scopeIndex} >
<GridItem offset={1}><CheckIcon /> {scope.name}</GridItem>
</React.Fragment>
)
})}
{application.tosUri && <GridItem><strong>{Msg.localize('termsOfService') + ': '}</strong>{application.tosUri}</GridItem>}
{application.policyUri && <GridItem><strong>{Msg.localize('policy') + ': '}</strong>{application.policyUri}</GridItem>}
<GridItem><strong>{Msg.localize('accessGrantedOn') + ': '}</strong>
{new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}).format(application.consent.createDate)}
</GridItem>
<DescriptionListGroup>
<DescriptionListTerm>Has access to</DescriptionListTerm>
{application.consent.grantedScopes.map((scope: GrantedScope, scopeIndex: number) => {
return (
<React.Fragment key={'scope-' + scopeIndex} >
<DescriptionListDescription><CheckIcon /> {scope.name}</DescriptionListDescription>
</React.Fragment>
)
})}
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{Msg.localize('accessGrantedOn') + ': '}</DescriptionListTerm>
<DescriptionListDescription>
{new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}).format(application.consent.createDate)}
</DescriptionListDescription>
</DescriptionListGroup>
</React.Fragment>
}
</div>
{application.logoUri && <div className='pf-c-content'><img src={application.logoUri} /></div> }
</Grid>
{(application.consent || application.offlineAccess) &&
<Grid gutter='sm'>
</DescriptionList>
{(application.consent || application.offlineAccess) &&
<Grid hasGutter>
<hr />
<GridItem>
<React.Fragment>
@ -233,12 +260,14 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
</GridItem>
<GridItem><InfoAltIcon /> {Msg.localize('infoMessage')}</GridItem>
</Grid>
}
</DataListContent>
</DataListItem>
)
})}
</DataList>
}
</DataListContent>
</DataListItem>
)
})}
</DataList>
</Stack>
</PageSection>
</ContentPage>
);
}

View file

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

View file

@ -33,4 +33,4 @@ export class ForbiddenPage extends React.Component {
</EmptyMessageState>
);
}
};
};

View file

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

View file

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

View file

@ -18,7 +18,18 @@ import * as React from 'react';
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 {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 {
return (
<Tab id={title} eventKey={eventKey} title={Msg.localize(title)}>
<Stack gutter="md">
<Stack hasGutter>
<StackItem isFilled><span/></StackItem>
<StackItem isFilled>
<Level gutter='md'>
<Level hasGutter>
<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>
</Level>
</StackItem>
@ -191,24 +202,26 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
public render(): React.ReactNode {
return (
<ContentPage title="resources" onRefresh={this.fetchInitialResources.bind(this)}>
<Tabs isFilled activeKey={this.state.activeTabKey} onSelect={this.handleTabClick}>
{this.makeTab(0, 'myResources', this.state.myResources, false)}
{this.makeTab(1, 'sharedwithMe', this.state.sharedWithMe, true)}
</Tabs>
<PageSection variant={PageSectionVariants.light}>
<Tabs activeKey={this.state.activeTabKey} onSelect={this.handleTabClick}>
{this.makeTab(0, 'myResources', this.state.myResources, false)}
{this.makeTab(1, 'sharedwithMe', this.state.sharedWithMe, true)}
</Tabs>
<Level gutter='md'>
<LevelItem>
{this.hasPrevious() && <Button onClick={this.handlePreviousClick}>&lt;<Msg msgKey='previousPage'/></Button>}
</LevelItem>
<Level hasGutter>
<LevelItem>
{this.hasPrevious() && <Button onClick={this.handlePreviousClick}>&lt;<Msg msgKey='previousPage'/></Button>}
</LevelItem>
<LevelItem>
{this.hasPrevious() && <Button onClick={this.handleFirstPageClick}><Msg msgKey='firstPage'/></Button>}
</LevelItem>
<LevelItem>
{this.hasPrevious() && <Button onClick={this.handleFirstPageClick}><Msg msgKey='firstPage'/></Button>}
</LevelItem>
<LevelItem>
{this.hasNext() && <Button onClick={this.handleNextClick}><Msg msgKey='nextPage'/>&gt;</Button>}
</LevelItem>
</Level>
<LevelItem>
{this.hasNext() && <Button onClick={this.handleNextClick}><Msg msgKey='nextPage'/>&gt;</Button>}
</LevelItem>
</Level>
</PageSection>
</ContentPage>
);
}

View file

@ -27,7 +27,8 @@ import {
DataListCell,
Chip,
Split,
SplitItem
SplitItem,
ModalVariant
} from '@patternfly/react-core';
import { UserCheckIcon } from '@patternfly/react-icons';
@ -35,7 +36,7 @@ import { HttpResponse } from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { Msg } from '../../widgets/Msg';
import { ContentAlert } from '../ContentAlert';
import { Resource, Scope, Permission } from './resource-model';
import { Resource, Scope, Permission, Permissions } from './resource-model';
interface PermissionRequestProps {
@ -48,7 +49,7 @@ interface 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;
context: React.ContextType<typeof AccountServiceContext>;
@ -107,7 +108,7 @@ export class PermissionRequest extends React.Component<PermissionRequestProps, P
<Modal
id={`modal-${id}`}
title={Msg.localize('permissionRequests') + ' - ' + this.props.resource.name}
isLarge={true}
variant={ModalVariant.large}
isOpen={this.state.isOpen}
onClose={this.handleToggleDialog}
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>)}
</DataListCell>,
<DataListCell key={`actions${i}`}>
<Split gutter="sm">
<Split hasGutter>
<SplitItem>
<Button
id={`accept-${i}-${id}`}
@ -176,4 +177,4 @@ export class PermissionRequest extends React.Component<PermissionRequestProps, P
</React.Fragment>
);
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,39 +14,46 @@
* 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 {
Button,
DataList,
DataListAction,
DataListItemCells,
DataListCell,
DataListItem,
DataListItemRow,
Stack,
StackItem,
Title,
TitleLevel,
DataListActionVisibility,
Dropdown,
DropdownPosition,
KebabToggle,
} from '@patternfly/react-core';
Alert,
Button,
DataList,
DataListAction,
DataListItemCells,
DataListCell,
DataListItem,
DataListItemRow,
EmptyState,
EmptyStateVariant,
EmptyStateBody,
Split,
SplitItem,
Title,
Dropdown,
DropdownPosition,
KebabToggle,
PageSection,
PageSectionVariants
} from "@patternfly/react-core";
import {AIACommand} from '../../util/AIACommand';
import TimeUtil from '../../util/TimeUtil';
import {HttpResponse, AccountServiceClient} from '../../account-service/account.service';
import {AccountServiceContext} from '../../account-service/AccountServiceContext';
import {ContinueCancelModal} from '../../widgets/ContinueCancelModal';
import {Features} from '../../widgets/features';
import {Msg} from '../../widgets/Msg';
import {ContentPage} from '../ContentPage';
import {ContentAlert} from '../ContentAlert';
import { KeycloakContext } from '../../keycloak-service/KeycloakContext';
import { KeycloakService } from '../../keycloak-service/keycloak.service';
import { css } from '@patternfly/react-styles';
import { AIACommand } from "../../util/AIACommand";
import TimeUtil from "../../util/TimeUtil";
import {
HttpResponse,
AccountServiceClient,
} from "../../account-service/account.service";
import { AccountServiceContext } from "../../account-service/AccountServiceContext";
import { ContinueCancelModal } from "../../widgets/ContinueCancelModal";
import { Features } from "../../widgets/features";
import { Msg } from "../../widgets/Msg";
import { ContentPage } from "../ContentPage";
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;
@ -55,7 +62,7 @@ interface PasswordDetails {
lastUpdate: number;
}
type CredCategory = 'password' | 'two-factor' | 'passwordless';
type CredCategory = "password" | "two-factor" | "passwordless";
type CredType = string;
type CredTypeMap = Map<CredType, CredentialContainer>;
type CredContainerMap = Map<CredCategory, CredTypeMap>;
@ -89,8 +96,7 @@ interface CredentialContainer {
open: boolean;
}
interface SigningInPageProps extends RouteComponentProps {
}
interface SigningInPageProps extends RouteComponentProps {}
interface SigningInPageState {
// Credential containers organized by category then type
@ -100,28 +106,33 @@ interface SigningInPageState {
/**
* @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;
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);
this.context = context;
this.state = {
credentialContainers: new Map(),
}
};
this.getCredentialContainers();
}
private getCredentialContainers(): void {
this.context!.doGet("/credentials")
.then((response: HttpResponse<CredentialContainer[]>) => {
this.context!.doGet("/credentials").then(
(response: HttpResponse<CredentialContainer[]>) => {
const allContainers: CredContainerMap = new Map();
const containers: CredentialContainer[] = response.data || [];
containers.forEach(container => {
containers.forEach((container) => {
let categoryMap = allContainers.get(container.category);
if (!categoryMap) {
categoryMap = new Map();
@ -130,63 +141,83 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
categoryMap.set(container.type, container);
});
this.setState({credentialContainers: allContainers});
console.log({allContainers})
});
this.setState({ credentialContainers: allContainers });
}
);
}
private handleRemove = (credentialId: string, userLabel: string) => {
this.context!.doDelete("/credentials/" + credentialId)
.then(() => {
this.context!.doDelete("/credentials/" + credentialId).then(() => {
this.getCredentialContainers();
ContentAlert.success('successRemovedMessage', [userLabel]);
ContentAlert.success("successRemovedMessage", [userLabel]);
});
}
};
public static credElementId(credType: CredType, credId: string, item: string): string {
return `${credType}-${item}-${credId.substring(0,8)}`;
public static credElementId(
credType: CredType,
credId: string,
item: string
): string {
return `${credType}-${item}-${credId.substring(0, 8)}`;
}
public render(): React.ReactNode {
return (
<ContentPage title="signingIn"
introMessage="signingInSubMessage">
<Stack gutter='md'>
{this.renderCategories()}
</Stack>
<ContentPage title="signingIn" introMessage="signingInSubMessage">
{this.renderCategories()}
</ContentPage>
);
}
private renderCategories(): React.ReactNode {
return (<> {
Array.from(this.state.credentialContainers.keys()).map(category => (
<StackItem key={category} isFilled>
<Title id={`${category}-categ-title`} headingLevel={TitleLevel.h2} size='2xl'>
<strong><Msg msgKey={category}/></strong>
</Title>
<DataList aria-label='foo'>
{this.renderTypes(this.state.credentialContainers.get(category)!)}
</DataList>
</StackItem>
))
}</>)
return Array.from(this.state.credentialContainers.keys()).map(
(category) => (
<PageSection key={category} variant={PageSectionVariants.light}>
<Title
id={`${category}-categ-title`}
headingLevel="h2"
size="xl"
>
<Msg msgKey={category} />
</Title>
{this.renderTypes(category!)}
</PageSection>
)
)
}
private renderTypes(credTypeMap: CredTypeMap): React.ReactNode {
private renderTypes(category: CredCategory): React.ReactNode {
let credTypeMap: CredTypeMap = this.state.credentialContainers.get(
category
)!;
return (
<KeycloakContext.Consumer>
{ keycloak => (
<>{
Array.from(credTypeMap.keys()).map((credType: CredType, index: number, typeArray: string[]) => ([
this.renderCredTypeTitle(credTypeMap.get(credType)!, keycloak!),
this.renderUserCredentials(credTypeMap, credType, keycloak!),
this.renderEmptyRow(credTypeMap.get(credType)!.type, index === typeArray.length - 1)
]))
}</>
)}
</KeycloakContext.Consumer>
<KeycloakContext.Consumer>
{(keycloak) => (
<>
{Array.from(
credTypeMap.keys()
).map(
(
credType: CredType,
index: number,
typeArray: string[]
) => [
this.renderCredTypeTitle(
credTypeMap.get(credType)!,
keycloak!,
category
),
this.renderUserCredentials(
credTypeMap,
credType,
keycloak!
),
]
)}
</>
)}
</KeycloakContext.Consumer>
);
}
@ -194,15 +225,21 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
if (isLast) return; // don't put empty row at the end
return (
<DataListItem aria-labelledby={'empty-list-item-' + type}>
<DataListItemRow key={'empty-row-' + type}>
<DataListItemCells dataListCells={[<DataListCell></DataListCell>]}/>
<DataListItem aria-labelledby={"empty-list-item-" + type}>
<DataListItemRow key={"empty-row-" + type}>
<DataListItemCells
dataListCells={[<DataListCell></DataListCell>]}
/>
</DataListItemRow>
</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 userCredentialMetadatas: CredMetadata[] = credContainer.userCredentialMetadatas;
const removeable: boolean = credContainer.removeable;
@ -212,17 +249,22 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
if (!userCredentialMetadatas || userCredentialMetadatas.length === 0) {
const localizedDisplayName = Msg.localize(displayName);
return (
<DataListItem key='no-credentials-list-item' aria-labelledby='no-credentials-list-item'>
<DataListItemRow key='no-credentials-list-item-row'>
<DataList aria-label={Msg.localize('notSetUp', [localizedDisplayName])} className="pf-u-mb-xl">
<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
dataListCells={[
<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'}/>
]}
/>
]}/>
</DataListItemRow>
</DataListItem>
</DataList>
);
}
@ -239,139 +281,191 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
updateAIA = new AIACommand(keycloak, credContainer.updateAction);
}
let maxWidth = { maxWidth: 689 } as React.CSSProperties;
return (
<React.Fragment key='userCredentialMetadatas'> {
userCredentialMetadatas.map(credentialMetadata => (
<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}>
<DataListItemCells dataListCells={this.credentialRowCells(credentialMetadata, type)}/>
<CredentialAction
<>
{(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}>
<DataListItemRow key={'userCredentialRow-' + credentialMetadata.credential.id} className="pf-u-align-items-center">
<DataListItemCells dataListCells={this.credentialRowCells(credentialMetadata, type)}/>
<CredentialAction
credential={credentialMetadata.credential}
removeable={removeable}
updateAction={updateAIA}
credRemover={this.handleRemove}
/>
</DataListItemRow>
</DataListItem>
/>
</DataListItemRow>
</DataListItem>
</DataList>
</>
))
}
</React.Fragment>)
} </React.Fragment>
)
}
private credentialRowCells(credMetadata: CredMetadata, type: string): React.ReactNode[] {
const credRowCells: React.ReactNode[] = [];
const credential = credMetadata.credential;
const infoMessage = credMetadata.infoMessage ? JSON.parse(credMetadata.infoMessage) : null;
const warningMessageTitle = credMetadata.warningMessageTitle ? JSON.parse(credMetadata.warningMessageTitle) : null;
const warningMessageDescription = credMetadata.warningMessageDescription ? JSON.parse(credMetadata.warningMessageDescription) : null;
let maxWidth = { "--pf-u-max-width--MaxWidth": "300px" } as React.CSSProperties;
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}
{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>
);
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(<DataListCell key={'spacer-' + credential.id}/>);
credRowCells.push(
<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;
}
private renderCredTypeTitle(credContainer: CredentialContainer, keycloak: KeycloakService): React.ReactNode {
if (!credContainer.hasOwnProperty('helptext') && !credContainer.hasOwnProperty('createAction')) return;
private renderCredTypeTitle(
credContainer: CredentialContainer,
keycloak: KeycloakService,
category: CredCategory
): React.ReactNode {
if (
!credContainer.hasOwnProperty("helptext") &&
!credContainer.hasOwnProperty("createAction")
)
return;
let setupAction: AIACommand;
if (credContainer.createAction) {
setupAction = new AIACommand(keycloak, credContainer.createAction);
}
const credContainerDisplayName: string = Msg.localize(credContainer.displayName);
const credContainerDisplayName: string = Msg.localize(
credContainer.displayName
);
return (
<React.Fragment key={'credTypeTitle-' + credContainer.type}>
<DataListItem aria-labelledby={'type-datalistitem-' + credContainer.type}>
<DataListItemRow key={'credTitleRow-' + credContainer.type}>
<DataListItemCells
dataListCells={[
<DataListCell width={5} key={'credTypeTitle-' + credContainer.type}>
<Title headingLevel={TitleLevel.h3} size='2xl'>
<strong id={`${credContainer.type}-cred-title`}><Msg msgKey={credContainer.displayName}/></strong>
</Title>
<span id={`${credContainer.type}-cred-help`}>
{credContainer.helptext && <Msg msgKey={credContainer.helptext}/>}
<React.Fragment key={"credTypeTitle-" + credContainer.type}>
<Split className="pf-u-mt-lg pf-u-mb-lg">
<SplitItem>
<Title
headingLevel="h3"
size="md"
className="pf-u-mb-md"
>
<span className="cred-title pf-u-display-block" id={`${credContainer.type}-cred-title`}>
<Msg msgKey={credContainer.displayName} />
</span>
</Title>
<span id={`${credContainer.type}-cred-help`}>
{credContainer.helptext && (
<Msg msgKey={credContainer.helptext} />
)}
</span>
</SplitItem>
<SplitItem isFilled>
{credContainer.createAction && (
<div
id={"mob-setUpAction-" + credContainer.type}
className="pf-u-display-none-on-lg pf-u-float-right"
>
<Dropdown
isPlain
position={DropdownPosition.right}
toggle={
<KebabToggle
onToggle={(isOpen) => {
credContainer.open = isOpen;
this.setState({
credentialContainers: new Map(
this.state.credentialContainers
),
});
}}
/>
}
isOpen={credContainer.open}
dropdownItems={[
<button
id={`mob-${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>,
]}
/>
</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>
</DataListCell>,
]}/>
{credContainer.createAction &&
<DataListAction
aria-labelledby='create'
aria-label='create action'
id={'mob-setUpAction-' + credContainer.type}
className={DataListActionVisibility.hiddenOnLg}
>
<Dropdown
isPlain
position={DropdownPosition.right}
toggle={<KebabToggle onToggle={isOpen => {
credContainer.open = isOpen;
this.setState({ credentialContainers: new Map(this.state.credentialContainers) });
}} />}
isOpen={credContainer.open}
dropdownItems={[
<button id={`mob-${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>]}
/>
</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">
<i className="fas fa-plus-circle" aria-hidden="true"></i>
</span>
<Msg msgKey='setUpNew' params={[credContainerDisplayName]}/>
</button>
</DataListAction>}
</DataListItemRow>
</DataListItem>
<Msg
msgKey="setUpNew"
params={[credContainerDisplayName]}
/>
</button>
</div>
)}
</SplitItem>
</Split>
</React.Fragment>
)
);
}
};
}
type CredRemover = (credentialId: string, userLabel: string) => void;
interface CredentialActionProps {
credential: UserCredential;
removeable: boolean;
@ -383,29 +477,49 @@ class CredentialAction extends React.Component<CredentialActionProps> {
render(): React.ReactNode {
if (this.props.updateAction) {
return (
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'updateAction-' + this.props.credential.id}>
<Button id={`${SigningInPage.credElementId(this.props.credential.type, this.props.credential.id, 'update')}`} variant='primary'onClick={()=> this.props.updateAction.execute()}><Msg msgKey='update'/></Button>
<DataListAction
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>
)
);
}
if (this.props.removeable) {
const userLabel: string = this.props.credential.userLabel;
return (
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'removeAction-' + this.props.credential.id }>
<ContinueCancelModal buttonTitle='remove'
<DataListAction
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')}`}
modalTitle={Msg.localize('removeCred', [userLabel])}
modalMessage={Msg.localize('stopUsingCred', [userLabel])}
onContinue={() => this.props.credRemover(this.props.credential.id, userLabel)}
/>
</DataListAction>
)
);
}
return (<></>)
return <></>;
}
}
const SigningInPageWithRouter = withRouter(SigningInPage);
export { SigningInPageWithRouter as SigningInPage};
export { SigningInPageWithRouter as SigningInPage };

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -10,25 +10,28 @@
"check-types:watch": "npm run check-types -- -w",
"lint": "eslint ./app/**/*.ts*",
"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",
"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-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-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-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": [],
"author": "Stan Silvert",
"license": "Apache-2.0",
"dependencies": {
"@patternfly/react-core": "^3.153.3",
"@patternfly/react-icons": "^3.15.16",
"@patternfly/react-styles": "^3.7.14",
"@patternfly/patternfly": "^4.125.3",
"@patternfly/react-core": "^4.147.0",
"@patternfly/react-icons": "^4.11.8",
"@patternfly/react-styles": "^4.11.8",
"react": "npm:@pika/react@^16.13.1",
"react-dom": "npm:@pika/react-dom@^16.13.1",
"react-router-dom": "^4.3.1"
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/compat-data": "^7.9.0",
"@babel/core": "^7.8.7",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/preset-env": "^7.13.15",
@ -41,6 +44,7 @@
"@typescript-eslint/eslint-plugin": "^1.4.2",
"@typescript-eslint/parser": "^1.4.2",
"babel-eslint": "^9.0.0",
"chokidar": "^3.5.3",
"eslint": "^5.15.1",
"eslint-config-react-app": "^3.0.8",
"eslint-plugin-flowtype": "^2.50.3",

View file

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

View file

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