Actually, take all the themes
This commit is contained in:
279
keycloak.v2/account/index.ftl
Normal file
279
keycloak.v2/account/index.ftl
Normal file
@ -0,0 +1,279 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${msg("accountManagementTitle")}</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<script>
|
||||
<#if properties.developmentMode?has_content && properties.developmentMode == "true">
|
||||
var developmentMode = true;
|
||||
var reactRuntime = 'react.development.js';
|
||||
var reactDOMRuntime = 'react-dom.development.js';
|
||||
var reactRouterRuntime = 'react-router-dom.js';
|
||||
<#else>
|
||||
var developmentMode = false;
|
||||
var reactRuntime = 'react.production.min.js';
|
||||
var reactDOMRuntime = 'react-dom.production.min.js';
|
||||
var reactRouterRuntime = 'react-router-dom.min.js';
|
||||
</#if>
|
||||
var authUrl = '${authUrl}';
|
||||
var baseUrl = '${baseUrl}';
|
||||
var realm = '${realm.name}';
|
||||
var resourceUrl = '${resourceUrl}';
|
||||
var isReactLoading = false;
|
||||
|
||||
<#if properties.logo?has_content>
|
||||
var brandImg = resourceUrl + '${properties.logo}';
|
||||
<#else>
|
||||
var brandImg = resourceUrl + '/public/logo.svg';
|
||||
</#if>
|
||||
|
||||
<#if properties.logoUrl?has_content>
|
||||
var brandUrl = '${properties.logoUrl}';
|
||||
<#else>
|
||||
var brandUrl = baseUrl;
|
||||
</#if>
|
||||
|
||||
var features = {
|
||||
isRegistrationEmailAsUsername : ${realm.registrationEmailAsUsername?c},
|
||||
isEditUserNameAllowed : ${realm.editUsernameAllowed?c},
|
||||
isInternationalizationEnabled : ${realm.isInternationalizationEnabled()?c},
|
||||
isLinkedAccountsEnabled : ${realm.identityFederationEnabled?c},
|
||||
isEventsEnabled : ${isEventsEnabled?c},
|
||||
isMyResourcesEnabled : ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
|
||||
isTotpConfigured : ${isTotpConfigured?c},
|
||||
deleteAccountAllowed : ${deleteAccountAllowed?c}
|
||||
}
|
||||
|
||||
var availableLocales = [];
|
||||
<#list supportedLocales as locale, label>
|
||||
availableLocales.push({locale : '${locale}', label : '${label}'});
|
||||
</#list>
|
||||
|
||||
<#if referrer??>
|
||||
var referrer = '${referrer}';
|
||||
var referrerName = '${referrerName}';
|
||||
var referrerUri = '${referrer_uri}'.replace('&', '&');
|
||||
</#if>
|
||||
|
||||
<#if msg??>
|
||||
var locale = '${locale}';
|
||||
var l18nMsg = JSON.parse('${msgJSON?no_esc}');
|
||||
<#else>
|
||||
var locale = 'en';
|
||||
var l18Msg = {};
|
||||
</#if>
|
||||
</script>
|
||||
|
||||
<#if properties.favIcon?has_content>
|
||||
<link rel="icon" href="${resourceUrl}${properties.favIcon}" type="image/x-icon"/>
|
||||
<#else>
|
||||
<link rel="icon" href="${resourceUrl}/public/favicon.ico" type="image/x-icon"/>
|
||||
</#if>
|
||||
|
||||
<script src="${authUrl}js/keycloak.js"></script>
|
||||
|
||||
<#if properties.developmentMode?has_content && properties.developmentMode == "true">
|
||||
<!-- Don't use this in production: -->
|
||||
<script src="${resourceUrl}/node_modules/react/umd/react.development.js" crossorigin></script>
|
||||
<script src="${resourceUrl}/node_modules/react-dom/umd/react-dom.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
|
||||
</#if>
|
||||
|
||||
<#if properties.extensions?has_content>
|
||||
<#list properties.extensions?split(' ') as script>
|
||||
<#if properties.developmentMode?has_content && properties.developmentMode == "true">
|
||||
<script type="text/babel" src="${resourceUrl}/${script}"></script>
|
||||
<#else>
|
||||
<script type="text/javascript" src="${resourceUrl}/${script}"></script>
|
||||
</#if>
|
||||
</#list>
|
||||
</#if>
|
||||
|
||||
<#if properties.scripts?has_content>
|
||||
<#list properties.scripts?split(' ') as script>
|
||||
<script type="text/javascript" src="${resourceUrl}/${script}"></script>
|
||||
</#list>
|
||||
</#if>
|
||||
|
||||
<script>
|
||||
var content = <#include "resources/content.json"/>
|
||||
</script>
|
||||
|
||||
<#if properties.styles?has_content>
|
||||
<#list properties.styles?split(' ') as style>
|
||||
<link href="${resourceUrl}/${style}" rel="stylesheet"/>
|
||||
</#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 href="${resourceUrl}/public/layout.css" rel="stylesheet"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
const keycloak = Keycloak({
|
||||
authServerUrl: authUrl,
|
||||
realm: realm,
|
||||
clientId: 'account-console'
|
||||
});
|
||||
keycloak.init({onLoad: 'check-sso', pkceMethod: 'S256', promiseType: 'native'}).then((authenticated) => {
|
||||
isReactLoading = true;
|
||||
toggleReact();
|
||||
if (!keycloak.authenticated) {
|
||||
document.getElementById("landingSignInButton").style.display='inline';
|
||||
document.getElementById("landingSignInLink").style.display='inline';
|
||||
} else {
|
||||
document.getElementById("landingSignOutButton").style.display='inline';
|
||||
document.getElementById("landingSignOutLink").style.display='inline';
|
||||
document.getElementById("landingLoggedInUser").innerHTML = loggedInUserName('${msg("unknownUser")}', '${msg("fullName")}');
|
||||
}
|
||||
|
||||
loadjs("/Main.js");
|
||||
}).catch(() => {
|
||||
alert('failed to initialize keycloak');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="main_react_container" style="display:none;height:100%"></div>
|
||||
|
||||
<div id="spinner_screen" style="display:block; height:100%">
|
||||
<div style="width: 320px; height: 328px; text-align: center; position: absolute; top:0; bottom: 0; left: 0; right: 0; margin: auto;">
|
||||
<#if properties.logo?has_content>
|
||||
<img src="${resourceUrl}${properties.logo}" alt="Logo" class="brand">
|
||||
<#else>
|
||||
<img src="${resourceUrl}/public/logo.svg" alt="Logo" class="brand">
|
||||
</#if>
|
||||
<p>${msg("loadingMessage")}</p>
|
||||
<div >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(255, 255, 255); display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<path d="M10 50A40 40 0 0 0 90 50A40 42 0 0 1 10 50" fill="#5DBCD2" stroke="none" transform="rotate(16.3145 50 51)">
|
||||
<animateTransform attributeName="transform" type="rotate" dur="1s" repeatCount="indefinite" keyTimes="0;1" values="0 50 51;360 50 51"></animateTransform>
|
||||
</path>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="welcomeScreen" style="display:none;height:100%">
|
||||
<div class="pf-c-page" id="page-layout-default-nav">
|
||||
<header role="banner" class="pf-c-page__header">
|
||||
<div class="pf-c-page__header-brand">
|
||||
<#if properties.logoUrl?has_content>
|
||||
<a id="landingLogo" class="pf-c-page__header-brand-link" href="${properties.logoUrl}">
|
||||
<#else>
|
||||
<a id="landingLogo" class="pf-c-page__header-brand-link" href="${baseUrl}">
|
||||
</#if>
|
||||
<#if properties.logo?has_content>
|
||||
<img class="pf-c-brand brand" src="${resourceUrl}${properties.logo}" alt="Logo">
|
||||
<#else>
|
||||
<img class="pf-c-brand brand" src="${resourceUrl}/public/logo.svg" alt="Logo">
|
||||
</#if>
|
||||
</a>
|
||||
</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>
|
||||
</#if>
|
||||
|
||||
<div class="pf-c-page__header-tools-group pf-m-icons">
|
||||
<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 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>
|
||||
</button>
|
||||
<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>
|
||||
</li>
|
||||
</#if>
|
||||
|
||||
<li id="landingSignInLink" role="none" style="display:none">
|
||||
<a onclick="keycloak.login();" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${msg("doLogIn")}</a>
|
||||
</li>
|
||||
<li id="landingSignOutLink" role="none" style="display:none">
|
||||
<a onclick="keycloak.logout();" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${msg("doSignOut")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="landingLoggedInUser"></span>
|
||||
|
||||
</div> <!-- end header tools -->
|
||||
</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>
|
||||
<#elseif item.iconSvg??>
|
||||
<img src="${item.iconSvg}" alt="icon"/>
|
||||
</#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>
|
||||
</div>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const removeHidden = (content) => {
|
||||
content.forEach(c => {
|
||||
if (c.hidden && eval(c.hidden)) {
|
||||
document.getElementById('landing-' + c.id).remove();
|
||||
}
|
||||
if (c.content) removeHidden(c.content);
|
||||
});
|
||||
}
|
||||
removeHidden(content);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
121
keycloak.v2/account/messages/messages_en.properties
Normal file
121
keycloak.v2/account/messages/messages_en.properties
Normal file
@ -0,0 +1,121 @@
|
||||
# Put new messages for Account Console Here
|
||||
# Feel free to use any existing messages from the base theme
|
||||
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
|
||||
continue=Continue
|
||||
refreshPage=Refresh the page
|
||||
done=Done
|
||||
cancel=Cancel
|
||||
remove=Remove
|
||||
update=Update
|
||||
loadingMessage=Account Console loading ...
|
||||
unknownUser=Anonymous
|
||||
fullName={0} {1}
|
||||
|
||||
selectLocale=Select a locale
|
||||
doSignIn=Sign In
|
||||
|
||||
# Device Activity Page
|
||||
signedInDevices=Signed In Devices
|
||||
signedInDevicesExplanation=Sign out any device that is unfamiliar.
|
||||
signOutWarning=Sign out the session?
|
||||
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
|
||||
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
|
||||
signedOutSession=Signed out {0}/{1}
|
||||
lastAccessedOn=Last accessed on
|
||||
clients=Clients
|
||||
startedAt=Started at
|
||||
expiresAt=Expires at
|
||||
ipAddress=IP Address
|
||||
|
||||
# Resources Page
|
||||
resourceName=Resource Name
|
||||
nextPage=Next
|
||||
previousPage=Previous
|
||||
firstPage=First Page
|
||||
resourceSharedWith=Resource is shared with {0}
|
||||
and=\ and {0} other users
|
||||
add=Add
|
||||
share=Share
|
||||
edit=Edit
|
||||
close=Close
|
||||
unShare=Unshare all
|
||||
shareSuccess=Resource successfully shared.
|
||||
unShareSuccess=Resource successfully un-shared.
|
||||
updateSuccess=Resource successfully updated.
|
||||
resourceAlreadyShared=Resource is already shared with this user.
|
||||
resourceNotShared=This resource is not shared.
|
||||
permissionRequests=Permission requests
|
||||
permissions=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
|
||||
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
|
||||
|
||||
# Signing In Page
|
||||
signingIn=Signing In
|
||||
signingInSubMessage=Configure ways to sign in.
|
||||
credentialCreatedAt=Created
|
||||
successRemovedMessage={0} was removed.
|
||||
stopUsingCred=Stop using {0}?
|
||||
removeCred=Remove {0}
|
||||
setUpNew=Set up {0}
|
||||
notSetUp={0} is not set up.
|
||||
two-factor=Two-Factor Authentication
|
||||
passwordless=Passwordless
|
||||
unknown=Unknown
|
||||
password-display-name=Password
|
||||
password-help-text=Log in by entering your password.
|
||||
password=My Password
|
||||
otp-display-name=Authenticator Application
|
||||
otp-help-text=Enter a verification code from authenticator application.
|
||||
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
|
||||
inUse=In use
|
||||
notInUse=Not in use
|
||||
applicationDetails=Application Details
|
||||
client=Client
|
||||
description=Description
|
||||
baseUrl=URL
|
||||
accessGrantedOn=Access granted on
|
||||
removeButton=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.
|
||||
|
||||
#Delete Account page
|
||||
doDelete=Delete
|
||||
deleteAccountSummary=Deleting your account will erase all your data and log you out immediately.
|
||||
deleteAccount=Delete Account
|
||||
deleteAccountWarning=This is irreversible. All your data will be permanently destroyed, and irretrievable.
|
14
keycloak.v2/account/resources/.gitignore
vendored
Normal file
14
keycloak.v2/account/resources/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# ignore typescript-generated files
|
||||
*.js
|
||||
*.js.map
|
||||
|
||||
# ignore log files
|
||||
*.log
|
||||
|
||||
# Don't ignore these
|
||||
!WelcomePageScripts.js
|
||||
!content.json
|
||||
|
||||
public/app.css
|
||||
public/base.css
|
||||
public/assets/
|
60
keycloak.v2/account/resources/content.json
Normal file
60
keycloak.v2/account/resources/content.json
Normal file
@ -0,0 +1,60 @@
|
||||
[
|
||||
{
|
||||
"id": "personal-info",
|
||||
"path": "personal-info",
|
||||
"icon": "pf-icon-user",
|
||||
"label": "personalInfoHtmlTitle",
|
||||
"descriptionLabel": "personalInfoIntroMessage",
|
||||
"modulePath": "/content/account-page/AccountPage.js",
|
||||
"componentName": "AccountPage"
|
||||
},
|
||||
{
|
||||
"id": "security",
|
||||
"icon": "pf-icon-security",
|
||||
"label": "accountSecurityTitle",
|
||||
"descriptionLabel": "accountSecurityIntroMessage",
|
||||
"content": [
|
||||
{
|
||||
"id": "signingin",
|
||||
"path": "security/signingin",
|
||||
"label": "signingIn",
|
||||
"modulePath": "/content/signingin-page/SigningInPage.js",
|
||||
"componentName": "SigningInPage"
|
||||
},
|
||||
{
|
||||
"id": "device-activity",
|
||||
"path": "security/device-activity",
|
||||
"label": "device-activity",
|
||||
"modulePath": "/content/device-activity-page/DeviceActivityPage.js",
|
||||
"componentName": "DeviceActivityPage"
|
||||
},
|
||||
{
|
||||
"id": "linked-accounts",
|
||||
"path": "security/linked-accounts",
|
||||
"label": "linkedAccountsHtmlTitle",
|
||||
"modulePath": "/content/linked-accounts-page/LinkedAccountsPage.js",
|
||||
"componentName": "LinkedAccountsPage",
|
||||
"hidden": "!features.isLinkedAccountsEnabled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "applications",
|
||||
"icon": "pf-icon-applications",
|
||||
"path": "applications",
|
||||
"label": "applications",
|
||||
"descriptionLabel": "applicationsIntroMessage",
|
||||
"modulePath": "/content/applications-page/ApplicationsPage.js",
|
||||
"componentName": "ApplicationsPage"
|
||||
},
|
||||
{
|
||||
"id": "resources",
|
||||
"icon": "pf-icon-repository",
|
||||
"path": "resources",
|
||||
"label": "resources",
|
||||
"descriptionLabel": "resourceIntroMessage",
|
||||
"modulePath": "/content/my-resources-page/MyResourcesPage.js",
|
||||
"componentName": "MyResourcesPage",
|
||||
"hidden": "!features.isMyResourcesEnabled"
|
||||
}
|
||||
]
|
BIN
keycloak.v2/account/resources/public/favicon.ico
Normal file
BIN
keycloak.v2/account/resources/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 627 B |
16
keycloak.v2/account/resources/public/layout.css
Normal file
16
keycloak.v2/account/resources/public/layout.css
Normal file
@ -0,0 +1,16 @@
|
||||
.brand {
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
width: 150px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.delete-button {
|
||||
width: 120px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
}
|
1
keycloak.v2/account/resources/public/logo.svg
Normal file
1
keycloak.v2/account/resources/public/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
19
keycloak.v2/account/src/.babelrc
Normal file
19
keycloak.v2/account/src/.babelrc
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"plugins": [
|
||||
[
|
||||
"snowpack/assets/babel-plugin.js",
|
||||
{
|
||||
"webModulesUrl": "../common/keycloak/web_modules",
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
{}
|
||||
]
|
||||
],
|
||||
"presets": [
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-typescript"
|
||||
]
|
||||
}
|
32
keycloak.v2/account/src/.eslintrc.js
Normal file
32
keycloak.v2/account/src/.eslintrc.js
Normal file
@ -0,0 +1,32 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
},
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'react-app'],
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly',
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
impliedStrict: true
|
||||
},
|
||||
ecmaVersion: 2019,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [
|
||||
'react',
|
||||
],
|
||||
rules: {
|
||||
"no-useless-constructor": "off",
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-parameter-properties": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"no-restricted-properties": "off"
|
||||
},
|
||||
};
|
2
keycloak.v2/account/src/.gitignore
vendored
Normal file
2
keycloak.v2/account/src/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Do not commit, installed at compile time
|
||||
node_modules
|
85
keycloak.v2/account/src/app/App.tsx
Normal file
85
keycloak.v2/account/src/app/App.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {KeycloakService} from './keycloak-service/keycloak.service';
|
||||
|
||||
import {PageNav} from './PageNav';
|
||||
import {PageToolbar} from './PageToolbar';
|
||||
import {makeRoutes} from './ContentPages';
|
||||
|
||||
import {
|
||||
Brand,
|
||||
Page,
|
||||
PageHeader,
|
||||
PageSection,
|
||||
PageSidebar,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { KeycloakContext } from './keycloak-service/KeycloakContext';
|
||||
|
||||
declare function toggleReact(): void;
|
||||
declare function isWelcomePage(): boolean;
|
||||
declare function loggedInUserName(): string;
|
||||
|
||||
declare const brandImg: string;
|
||||
declare const brandUrl: string;
|
||||
|
||||
export interface AppProps {};
|
||||
export class App extends React.Component<AppProps> {
|
||||
static contextType = KeycloakContext;
|
||||
context: React.ContextType<typeof KeycloakContext>;
|
||||
|
||||
public constructor(props: AppProps, context: React.ContextType<typeof KeycloakContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
toggleReact();
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
toggleReact();
|
||||
|
||||
// check login
|
||||
if (!this.context!.authenticated() && !isWelcomePage()) {
|
||||
this.context!.login();
|
||||
}
|
||||
|
||||
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}
|
||||
showNavToggle
|
||||
/>
|
||||
);
|
||||
|
||||
const Sidebar = <PageSidebar nav={<PageNav/>} />;
|
||||
|
||||
return (
|
||||
<span style={{ height: '100%'}}>
|
||||
<Page header={Header} sidebar={Sidebar} isManagedSidebar>
|
||||
<PageSection>
|
||||
{makeRoutes()}
|
||||
</PageSection>
|
||||
</Page>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
174
keycloak.v2/account/src/app/ContentPages.tsx
Normal file
174
keycloak.v2/account/src/app/ContentPages.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Route, Switch} from 'react-router-dom';
|
||||
import {NavItem, NavExpandable} from '@patternfly/react-core';
|
||||
import {Msg} from './widgets/Msg';
|
||||
import {PageNotFound} from './content/page-not-found/PageNotFound';
|
||||
import { ForbiddenPage } from './content/forbidden-page/ForbiddenPage';
|
||||
|
||||
export interface ContentItem {
|
||||
id?: string;
|
||||
label: string;
|
||||
labelParams?: string[];
|
||||
hidden?: string;
|
||||
groupId: string; // computed value
|
||||
itemId: string; // computed value
|
||||
};
|
||||
|
||||
export interface Expansion extends ContentItem {
|
||||
content: ContentItem[];
|
||||
}
|
||||
|
||||
export interface PageDef extends ContentItem {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ComponentPageDef extends PageDef {
|
||||
component: React.ComponentType;
|
||||
}
|
||||
|
||||
export interface ModulePageDef extends PageDef {
|
||||
modulePath: string;
|
||||
componentName: string;
|
||||
module: React.Component; // computed value
|
||||
}
|
||||
|
||||
export function isModulePageDef(item: ContentItem): item is ModulePageDef {
|
||||
return (item as ModulePageDef).modulePath !== undefined;
|
||||
}
|
||||
|
||||
export function isExpansion(contentItem: ContentItem): contentItem is Expansion {
|
||||
return (contentItem as Expansion).content !== undefined;
|
||||
}
|
||||
|
||||
declare const content: ContentItem[];
|
||||
|
||||
function groupId(group: number): string {
|
||||
return 'grp-' + group;
|
||||
}
|
||||
|
||||
function itemId(group: number, item: number): string {
|
||||
return 'grp-' + group + '_itm-' + item;
|
||||
}
|
||||
|
||||
function isChildOf(parent: Expansion, child: PageDef): boolean {
|
||||
for (var item of parent.content) {
|
||||
if (isExpansion(item) && isChildOf(item, child)) return true;
|
||||
if (parent.groupId === child.groupId) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function createNavItems(activePage: PageDef, contentParam: ContentItem[], groupNum: number): React.ReactNode {
|
||||
if (typeof content === 'undefined') return (<React.Fragment/>);
|
||||
|
||||
const links: React.ReactElement[] = contentParam.map((item: ContentItem) => {
|
||||
const navLinkId = `nav-link-${item.id}`;
|
||||
if (isExpansion(item)) {
|
||||
return <NavExpandable id={navLinkId}
|
||||
groupId={item.groupId}
|
||||
key={item.groupId}
|
||||
title={Msg.localize(item.label, item.labelParams)}
|
||||
isExpanded={isChildOf(item, activePage)}>
|
||||
{createNavItems(activePage, item.content, groupNum + 1)}
|
||||
</NavExpandable>
|
||||
} else {
|
||||
const page: PageDef = item as PageDef;
|
||||
return <NavItem id={navLinkId}
|
||||
groupId={item.groupId}
|
||||
itemId={item.itemId}
|
||||
key={item.itemId}
|
||||
to={'#/' + page.path}
|
||||
isActive={activePage.itemId === item.itemId}
|
||||
type="button">
|
||||
{Msg.localize(page.label, page.labelParams)}
|
||||
</NavItem>
|
||||
}
|
||||
});
|
||||
|
||||
return (<React.Fragment>{links}</React.Fragment>);
|
||||
}
|
||||
|
||||
export function makeNavItems(activePage: PageDef): React.ReactNode {
|
||||
console.log({activePage});
|
||||
return createNavItems(activePage, content, 0);
|
||||
}
|
||||
|
||||
function setIds(contentParam: ContentItem[], groupNum: number): number {
|
||||
if (typeof contentParam === 'undefined') return groupNum;
|
||||
let expansionGroupNum = groupNum;
|
||||
|
||||
for (let i = 0; i < contentParam.length; i++) {
|
||||
const item: ContentItem = contentParam[i];
|
||||
if (isExpansion(item)) {
|
||||
item.itemId = itemId(groupNum, i);
|
||||
expansionGroupNum = expansionGroupNum + 1;
|
||||
item.groupId = groupId(expansionGroupNum);
|
||||
expansionGroupNum = setIds(item.content, expansionGroupNum);
|
||||
console.log('currentGroup=' + (expansionGroupNum));
|
||||
} else {
|
||||
item.groupId = groupId(groupNum);
|
||||
item.itemId = itemId(groupNum, i);
|
||||
}
|
||||
};
|
||||
|
||||
return expansionGroupNum;
|
||||
}
|
||||
|
||||
export function initGroupAndItemIds(): void {
|
||||
setIds(content, 0);
|
||||
console.log({content});
|
||||
}
|
||||
|
||||
// get rid of Expansions and put all PageDef items into a single array
|
||||
export function flattenContent(pageDefs: ContentItem[]): PageDef[] {
|
||||
const flat: PageDef[] = [];
|
||||
|
||||
for (let item of pageDefs) {
|
||||
if (isExpansion(item)) {
|
||||
flat.push(...flattenContent(item.content));
|
||||
} else {
|
||||
flat.push(item as PageDef);
|
||||
}
|
||||
}
|
||||
|
||||
return flat;
|
||||
}
|
||||
|
||||
export function makeRoutes(): React.ReactNode {
|
||||
if (typeof content === 'undefined') return (<span/>);
|
||||
|
||||
const pageDefs: PageDef[] = flattenContent(content);
|
||||
|
||||
const routes: React.ReactElement<Route>[] = pageDefs.map((page: PageDef) => {
|
||||
if (isModulePageDef(page)) {
|
||||
const node: React.ReactNode = React.createElement(page.module[page.componentName], {'pageDef': page});
|
||||
return <Route key={page.itemId} path={'/' + page.path} exact render={() => node} />;
|
||||
} else {
|
||||
const pageDef: ComponentPageDef = page as ComponentPageDef;
|
||||
return <Route key={page.itemId} path={'/' + page.path} exact component={pageDef.component}/>;
|
||||
}
|
||||
});
|
||||
|
||||
return (<Switch>
|
||||
{routes}
|
||||
<Route path="/forbidden" component={ForbiddenPage}/>
|
||||
<Route component={PageNotFound}/>
|
||||
</Switch>);
|
||||
}
|
113
keycloak.v2/account/src/app/Main.tsx
Normal file
113
keycloak.v2/account/src/app/Main.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
import {HashRouter} from 'react-router-dom';
|
||||
|
||||
import {App} from './App';
|
||||
import {ContentItem, ModulePageDef, flattenContent, initGroupAndItemIds, isExpansion, isModulePageDef} from './ContentPages';
|
||||
|
||||
import { KeycloakClient, KeycloakService } from './keycloak-service/keycloak.service';
|
||||
import { KeycloakContext } from './keycloak-service/KeycloakContext';
|
||||
import { AccountServiceClient } from './account-service/account.service';
|
||||
import { AccountServiceContext } from './account-service/AccountServiceContext';
|
||||
|
||||
declare const keycloak: KeycloakClient;
|
||||
|
||||
declare let isReactLoading: boolean;
|
||||
declare function toggleReact(): void;
|
||||
declare const features: { [key: string]: boolean; };
|
||||
|
||||
export interface MainProps {}
|
||||
export class Main extends React.Component<MainProps> {
|
||||
|
||||
public constructor(props: MainProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
isReactLoading = false;
|
||||
toggleReact();
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const keycloakService = new KeycloakService(keycloak);
|
||||
return (
|
||||
<HashRouter>
|
||||
<KeycloakContext.Provider value={keycloakService}>
|
||||
<AccountServiceContext.Provider value={new AccountServiceClient(keycloakService)}>
|
||||
<App/>
|
||||
</AccountServiceContext.Provider>
|
||||
</KeycloakContext.Provider>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
declare const resourceUrl: string;
|
||||
declare let content: ContentItem[];
|
||||
const e = React.createElement;
|
||||
|
||||
function removeHidden(items: ContentItem[]): ContentItem[] {
|
||||
const visible: ContentItem[] = [];
|
||||
|
||||
for (let item of items) {
|
||||
if (item.hidden && eval(item.hidden)) continue;
|
||||
|
||||
if (isExpansion(item)) {
|
||||
visible.push(item);
|
||||
item.content = removeHidden(item.content);
|
||||
if (item.content.length === 0) {
|
||||
visible.pop(); // remove empty expansion
|
||||
}
|
||||
} else {
|
||||
visible.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
content = removeHidden(content);
|
||||
initGroupAndItemIds();
|
||||
|
||||
function loadModule(modulePage: ModulePageDef): Promise<ModulePageDef> {
|
||||
return new Promise ((resolve, reject) => {
|
||||
console.log('loading: ' + resourceUrl + modulePage.modulePath);
|
||||
import(resourceUrl + modulePage.modulePath).then( (module: React.Component) => {
|
||||
modulePage.module = module;
|
||||
resolve(modulePage);
|
||||
}).catch((error: Error) => {
|
||||
console.warn('Unable to load ' + modulePage.label + ' because ' + error.message);
|
||||
reject(modulePage);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const moduleLoaders: Promise<ModulePageDef>[] = [];
|
||||
flattenContent(content).forEach((item: ContentItem) => {
|
||||
if (isModulePageDef(item)) {
|
||||
moduleLoaders.push(loadModule(item));
|
||||
}
|
||||
});
|
||||
|
||||
// load content modules and start
|
||||
Promise.all(moduleLoaders).then(() => {
|
||||
const domContainer = document.querySelector('#main_react_container');
|
||||
ReactDOM.render(e(Main), domContainer);
|
||||
});
|
61
keycloak.v2/account/src/app/PageNav.tsx
Normal file
61
keycloak.v2/account/src/app/PageNav.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {withRouter, RouteComponentProps} from 'react-router-dom';
|
||||
import {Nav, NavList} from '@patternfly/react-core';
|
||||
|
||||
import {makeNavItems, flattenContent, ContentItem, PageDef} from './ContentPages';
|
||||
|
||||
declare const content: ContentItem[];
|
||||
|
||||
export interface PageNavProps extends RouteComponentProps {}
|
||||
|
||||
export interface PageNavState {}
|
||||
|
||||
class PageNavigation extends React.Component<PageNavProps, PageNavState> {
|
||||
|
||||
public constructor(props: PageNavProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
private findActiveItem(): PageDef {
|
||||
const currentPath: string = this.props.location.pathname;
|
||||
const items: PageDef[] = flattenContent(content);
|
||||
const firstItem = items[0];
|
||||
for (let item of items) {
|
||||
const itemPath: string = '/' + item.path;
|
||||
if (itemPath === currentPath) {
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
return firstItem;
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const activeItem: PageDef = this.findActiveItem();
|
||||
return (
|
||||
<Nav>
|
||||
<NavList>
|
||||
{makeNavItems(activeItem)}
|
||||
</NavList>
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const PageNav = withRouter(PageNavigation);
|
86
keycloak.v2/account/src/app/PageToolbar.tsx
Normal file
86
keycloak.v2/account/src/app/PageToolbar.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {Dropdown, KebabToggle, Toolbar, ToolbarGroup, ToolbarItem} from '@patternfly/react-core';
|
||||
|
||||
import {ReferrerDropdownItem} from './widgets/ReferrerDropdownItem';
|
||||
import {ReferrerLink} from './widgets/ReferrerLink';
|
||||
import {Features} from './widgets/features';
|
||||
import {LogoutButton,LogoutDropdownItem} from './widgets/Logout';
|
||||
|
||||
declare const referrerName: string;
|
||||
declare const features: Features;
|
||||
|
||||
interface PageToolbarProps {}
|
||||
interface PageToolbarState {isKebabDropdownOpen: boolean}
|
||||
export class PageToolbar extends React.Component<PageToolbarProps, PageToolbarState> {
|
||||
private hasReferrer: boolean = typeof referrerName !== 'undefined';
|
||||
|
||||
public constructor(props: PageToolbarProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isKebabDropdownOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
private onKebabDropdownToggle = (isKebabDropdownOpen: boolean) => {
|
||||
this.setState({
|
||||
isKebabDropdownOpen
|
||||
});
|
||||
};
|
||||
|
||||
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'>
|
||||
<ToolbarItem className="pf-m-icons" key='referrer'>
|
||||
<ReferrerLink/>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
}
|
||||
|
||||
<ToolbarGroup key='secondGroup'>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { AccountServiceClient } from './account.service';
|
||||
|
||||
export const AccountServiceContext = React.createContext<AccountServiceClient | undefined>(undefined);
|
154
keycloak.v2/account/src/app/account-service/account.service.ts
Normal file
154
keycloak.v2/account/src/app/account-service/account.service.ts
Normal file
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat Inc. and/or its affiliates and other contributors
|
||||
* as indicated by the @author tags. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
import {KeycloakService} from '../keycloak-service/keycloak.service';
|
||||
import {ContentAlert} from '../content/ContentAlert';
|
||||
|
||||
declare const baseUrl: string;
|
||||
|
||||
type ConfigResolve = (config: RequestInit) => void;
|
||||
|
||||
export interface HttpResponse<T = {}> extends Response {
|
||||
data?: T;
|
||||
}
|
||||
|
||||
export interface RequestInitWithParams extends RequestInit {
|
||||
params?: {[name: string]: string | number};
|
||||
}
|
||||
|
||||
export class AccountServiceError extends Error {
|
||||
constructor(public response: HttpResponse) {
|
||||
super(response.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
|
||||
*/
|
||||
export class AccountServiceClient {
|
||||
private kcSvc: KeycloakService;
|
||||
private accountUrl: string;
|
||||
|
||||
public constructor(keycloakService: KeycloakService) {
|
||||
this.kcSvc = keycloakService;
|
||||
this.accountUrl = this.kcSvc.authServerUrl() + 'realms/' + this.kcSvc.realm() + '/account';
|
||||
}
|
||||
|
||||
public async doGet<T>(endpoint: string,
|
||||
config?: RequestInitWithParams): Promise<HttpResponse<T>> {
|
||||
return this.doRequest(endpoint, {...config, method: 'get'});
|
||||
}
|
||||
|
||||
public async doDelete<T>(endpoint: string,
|
||||
config?: RequestInitWithParams): Promise<HttpResponse<T>> {
|
||||
return this.doRequest(endpoint, {...config, method: 'delete'});
|
||||
}
|
||||
|
||||
public async doPost<T>(endpoint: string,
|
||||
body: string | {},
|
||||
config?: RequestInitWithParams): Promise<HttpResponse<T>> {
|
||||
return this.doRequest(endpoint, {...config, body: JSON.stringify(body), method: 'post'});
|
||||
}
|
||||
|
||||
public async doPut<T>(endpoint: string,
|
||||
body: string | {},
|
||||
config?: RequestInitWithParams): Promise<HttpResponse<T>> {
|
||||
return this.doRequest(endpoint, {...config, body: JSON.stringify(body), method: 'put'});
|
||||
}
|
||||
|
||||
public async doRequest<T>(endpoint: string,
|
||||
config?: RequestInitWithParams): Promise<HttpResponse<T>> {
|
||||
|
||||
const response: HttpResponse<T> = await fetch(this.makeUrl(endpoint, config).toString(),
|
||||
await this.makeConfig(config));
|
||||
|
||||
try {
|
||||
response.data = await response.json();
|
||||
} catch (e) {} // ignore. Might be empty
|
||||
|
||||
if (!response.ok) {
|
||||
this.handleError(response);
|
||||
throw new AccountServiceError(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private handleError(response: HttpResponse): void {
|
||||
if (response !== null && response.status === 401) {
|
||||
if (this.kcSvc.authenticated() && !this.kcSvc.audiencePresent()) {
|
||||
// authenticated and the audience is not present => not allowed
|
||||
window.location.href = baseUrl + '#/forbidden';
|
||||
} else {
|
||||
// session timed out?
|
||||
this.kcSvc.login();
|
||||
}
|
||||
}
|
||||
|
||||
if (response !== null && response.status === 403) {
|
||||
window.location.href = baseUrl + '#/forbidden';
|
||||
}
|
||||
|
||||
if (response !== null && response.data != null) {
|
||||
ContentAlert.danger(
|
||||
`${response.statusText}: ${response.data['errorMessage'] ? response.data['errorMessage'] : ''} ${response.data['error'] ? response.data['error'] : ''}`
|
||||
);
|
||||
} else {
|
||||
ContentAlert.danger(response.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
private makeUrl(endpoint: string, config?: RequestInitWithParams): URL {
|
||||
if (endpoint.startsWith('http')) return new URL(endpoint);
|
||||
const url = new URL(this.accountUrl + endpoint);
|
||||
|
||||
// add request params
|
||||
if (config && config.hasOwnProperty('params')) {
|
||||
const params: {[name: string]: string} = config.params as {} || {};
|
||||
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
private makeConfig(config: RequestInit = {}): Promise<RequestInit> {
|
||||
return new Promise( (resolve: ConfigResolve) => {
|
||||
this.kcSvc.getToken()
|
||||
.then( (token: string) => {
|
||||
resolve( {
|
||||
...config,
|
||||
headers: {'Content-Type': 'application/json',
|
||||
...config.headers,
|
||||
Authorization: 'Bearer ' + token}
|
||||
});
|
||||
}).catch(() => {
|
||||
this.kcSvc.login();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
|
||||
event.promise.catch(error => {
|
||||
if (error instanceof AccountServiceError) {
|
||||
// We already handled the error. Ignore unhandled rejection.
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
112
keycloak.v2/account/src/app/content/ContentAlert.tsx
Normal file
112
keycloak.v2/account/src/app/content/ContentAlert.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Alert, AlertActionCloseButton, AlertGroup, AlertVariant } from '@patternfly/react-core';
|
||||
import { Msg } from '../widgets/Msg';
|
||||
|
||||
interface ContentAlertProps { }
|
||||
|
||||
interface ContentAlertState {
|
||||
alerts: {
|
||||
key: number;
|
||||
message: string;
|
||||
variant: AlertVariant;
|
||||
}[];
|
||||
}
|
||||
export class ContentAlert extends React.Component<ContentAlertProps, ContentAlertState> {
|
||||
private static instance: ContentAlert;
|
||||
|
||||
private constructor(props: ContentAlertProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
alerts: []
|
||||
};
|
||||
ContentAlert.instance = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static success(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert(AlertVariant.success, message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static danger(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert(AlertVariant.danger, message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static warning(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert(AlertVariant.warning, message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message A literal text message or localization key.
|
||||
*/
|
||||
public static info(message: string, params?: string[]): void {
|
||||
ContentAlert.instance.postAlert(AlertVariant.info, message, params);
|
||||
}
|
||||
|
||||
private hideAlert = (key: number) => {
|
||||
this.setState({ alerts: [...this.state.alerts.filter(el => el.key !== key)] });
|
||||
}
|
||||
|
||||
private getUniqueId = () => (new Date().getTime());
|
||||
|
||||
private postAlert = (variant: AlertVariant, message: string, params?: string[]) => {
|
||||
const alerts = this.state.alerts;
|
||||
const key = this.getUniqueId();
|
||||
alerts.push({
|
||||
key,
|
||||
message: Msg.localize(message, params),
|
||||
variant
|
||||
});
|
||||
this.setState({ alerts });
|
||||
|
||||
if (variant !== AlertVariant.danger) {
|
||||
setTimeout(() => this.hideAlert(key), 8000);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<AlertGroup isToast aria-live="assertive">
|
||||
{this.state.alerts.map(({ key, variant, message }) => (
|
||||
<Alert
|
||||
aria-details={message}
|
||||
isLiveRegion
|
||||
variant={variant}
|
||||
title={message}
|
||||
action={
|
||||
<AlertActionCloseButton
|
||||
title={message}
|
||||
variantLabel={`${variant} alert`}
|
||||
onClose={() => this.hideAlert(key)}
|
||||
/>
|
||||
}
|
||||
key={key} />
|
||||
))}
|
||||
</AlertGroup>
|
||||
);
|
||||
}
|
||||
}
|
66
keycloak.v2/account/src/app/content/ContentPage.tsx
Normal file
66
keycloak.v2/account/src/app/content/ContentPage.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Button, Grid, GridItem, Title, Tooltip} from '@patternfly/react-core';
|
||||
import {RedoIcon} from '@patternfly/react-icons';
|
||||
|
||||
import {Msg} from '../widgets/Msg';
|
||||
import {ContentAlert} from './ContentAlert';
|
||||
|
||||
interface ContentPageProps {
|
||||
title: string; // Literal title or key into message bundle
|
||||
introMessage?: string; // Literal message or key into message bundle
|
||||
onRefresh?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2019 Red Hat Inc.
|
||||
*/
|
||||
export class ContentPage extends React.Component<ContentPageProps> {
|
||||
|
||||
public constructor(props: ContentPageProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<section className="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
{this.props.children}
|
||||
</section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
301
keycloak.v2/account/src/app/content/account-page/AccountPage.tsx
Normal file
301
keycloak.v2/account/src/app/content/account-page/AccountPage.tsx
Normal file
@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { ActionGroup, Button, Form, FormGroup, TextInput, Grid, GridItem, Expandable} from '@patternfly/react-core';
|
||||
|
||||
import { HttpResponse } from '../../account-service/account.service';
|
||||
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
|
||||
import { Features } from '../../widgets/features';
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import { ContentPage } from '../ContentPage';
|
||||
import { ContentAlert } from '../ContentAlert';
|
||||
import { LocaleSelector } from '../../widgets/LocaleSelectors';
|
||||
import { KeycloakContext } from '../../keycloak-service/KeycloakContext';
|
||||
import { KeycloakService } from '../../keycloak-service/keycloak.service';
|
||||
import { AIACommand } from '../../util/AIACommand';
|
||||
|
||||
declare const features: Features;
|
||||
declare const locale: string;
|
||||
|
||||
interface AccountPageProps {
|
||||
}
|
||||
|
||||
interface FormFields {
|
||||
readonly username?: string;
|
||||
readonly firstName?: string;
|
||||
readonly lastName?: string;
|
||||
readonly email?: string;
|
||||
attributes?: { locale?: [string] };
|
||||
}
|
||||
|
||||
interface AccountPageState {
|
||||
readonly errors: FormFields;
|
||||
readonly formFields: FormFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
|
||||
*/
|
||||
export class AccountPage extends React.Component<AccountPageProps, AccountPageState> {
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
private isRegistrationEmailAsUsername: boolean = features.isRegistrationEmailAsUsername;
|
||||
private isEditUserNameAllowed: boolean = features.isEditUserNameAllowed;
|
||||
private isDeleteAccountAllowed: boolean = features.deleteAccountAllowed;
|
||||
private readonly DEFAULT_STATE: AccountPageState = {
|
||||
errors: {
|
||||
username: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: ''
|
||||
},
|
||||
formFields: {
|
||||
username: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
attributes: {}
|
||||
}
|
||||
};
|
||||
|
||||
public state: AccountPageState = this.DEFAULT_STATE;
|
||||
|
||||
public constructor(props: AccountPageProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
|
||||
this.fetchPersonalInfo();
|
||||
}
|
||||
|
||||
private fetchPersonalInfo(): void {
|
||||
this.context!.doGet<FormFields>("/")
|
||||
.then((response: HttpResponse<FormFields>) => {
|
||||
this.setState(this.DEFAULT_STATE);
|
||||
const formFields = response.data;
|
||||
if (!formFields!.attributes) {
|
||||
formFields!.attributes = { locale: [locale] };
|
||||
}
|
||||
else if (!formFields!.attributes.locale) {
|
||||
formFields!.attributes.locale = [locale];
|
||||
}
|
||||
|
||||
this.setState({...{ formFields: formFields as FormFields }});
|
||||
});
|
||||
}
|
||||
|
||||
private handleCancel = (): void => {
|
||||
this.fetchPersonalInfo();
|
||||
}
|
||||
|
||||
private handleChange = (value: string, event: React.FormEvent<HTMLInputElement>) => {
|
||||
const target = event.currentTarget;
|
||||
const name = target.name;
|
||||
|
||||
this.setState({
|
||||
errors: { ...this.state.errors, [name]: target.validationMessage },
|
||||
formFields: { ...this.state.formFields, [name]: value }
|
||||
});
|
||||
}
|
||||
|
||||
private handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
|
||||
event.preventDefault();
|
||||
const form = event.target as HTMLFormElement;
|
||||
const isValid = form.checkValidity();
|
||||
if (isValid) {
|
||||
const reqData: FormFields = { ...this.state.formFields };
|
||||
this.context!.doPost<void>("/", reqData)
|
||||
.then(() => {
|
||||
ContentAlert.success('accountUpdatedMessage');
|
||||
if (locale !== this.state.formFields.attributes!.locale![0]) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const formData = new FormData(form);
|
||||
const validationMessages = Array.from(formData.keys()).reduce((acc, key) => {
|
||||
acc[key] = form.elements[key].validationMessage
|
||||
return acc
|
||||
}, {});
|
||||
this.setState({
|
||||
errors: { ...validationMessages },
|
||||
formFields: this.state.formFields
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private handleDelete = (keycloak: KeycloakService): void => {
|
||||
new AIACommand(keycloak, "delete_account").execute();
|
||||
}
|
||||
|
||||
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 === ''}
|
||||
>
|
||||
<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="Delete Account">
|
||||
<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>
|
||||
)}
|
||||
</KeycloakContext.Consumer>
|
||||
</GridItem>
|
||||
<GridItem span={2}>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
</Expandable>
|
||||
</div>}
|
||||
</ContentPage>
|
||||
);
|
||||
}
|
||||
|
||||
private UsernameInput = () => (
|
||||
<TextInput
|
||||
isRequired
|
||||
type="text"
|
||||
id="user-name"
|
||||
name="username"
|
||||
maxLength={254}
|
||||
value={this.state.formFields.username}
|
||||
onChange={this.handleChange}
|
||||
isValid={this.state.errors.username === ''}
|
||||
>
|
||||
</TextInput>
|
||||
);
|
||||
|
||||
private RestrictedUsernameInput = () => (
|
||||
<TextInput
|
||||
isDisabled
|
||||
type="text"
|
||||
id="user-name"
|
||||
name="username"
|
||||
value={this.state.formFields.username}
|
||||
>
|
||||
</TextInput>
|
||||
);
|
||||
};
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {withRouter, RouteComponentProps} from 'react-router-dom';
|
||||
|
||||
import {AIACommand} from '../../util/AIACommand';
|
||||
import {PageDef} from '../../ContentPages';
|
||||
import {Msg} from '../../widgets/Msg';
|
||||
|
||||
import {
|
||||
Title,
|
||||
TitleLevel,
|
||||
Button,
|
||||
EmptyState,
|
||||
EmptyStateVariant,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody
|
||||
} from '@patternfly/react-core';
|
||||
import { PassportIcon } from '@patternfly/react-icons';
|
||||
import { KeycloakService } from '../../keycloak-service/keycloak.service';
|
||||
import { KeycloakContext } from '../../keycloak-service/KeycloakContext';
|
||||
|
||||
// Note: This class demonstrates two features of the ContentPages framework:
|
||||
// 1) The PageDef is available as a React property.
|
||||
// 2) You can add additional custom properties to the PageDef. In this case,
|
||||
// we add a value called kcAction in content.js and access it by extending the
|
||||
// PageDef interface.
|
||||
interface ActionPageDef extends PageDef {
|
||||
kcAction: string;
|
||||
}
|
||||
|
||||
// Extend RouteComponentProps to get access to router information such as
|
||||
// the hash-routed path associated with this page. See this.props.location.pathname
|
||||
// as used below.
|
||||
interface AppInitiatedActionPageProps extends RouteComponentProps {
|
||||
pageDef: ActionPageDef;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
class ApplicationInitiatedActionPage extends React.Component<AppInitiatedActionPageProps> {
|
||||
|
||||
public constructor(props: AppInitiatedActionPageProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
private handleClick = (keycloak: KeycloakService): void => {
|
||||
new AIACommand(keycloak, this.props.pageDef.kcAction).execute();
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<EmptyState variant={EmptyStateVariant.full}>
|
||||
<EmptyStateIcon icon={PassportIcon} />
|
||||
<Title headingLevel={TitleLevel.h5} size="lg">
|
||||
<Msg msgKey={this.props.pageDef.label} params={this.props.pageDef.labelParams}/>
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
<Msg msgKey="actionRequiresIDP"/>
|
||||
</EmptyStateBody>
|
||||
<KeycloakContext.Consumer>
|
||||
{ keycloak => (
|
||||
<Button variant="primary"
|
||||
onClick={() => this.handleClick(keycloak!)}
|
||||
target="_blank"><Msg msgKey="continue"/></Button>
|
||||
)}
|
||||
</KeycloakContext.Consumer>
|
||||
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
DataList,
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DataListCell,
|
||||
DataListToggle,
|
||||
DataListContent,
|
||||
DataListItemCells,
|
||||
Grid,
|
||||
GridItem,
|
||||
Button,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { InfoAltIcon, CheckIcon, BuilderImageIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
import { ContentPage } from '../ContentPage';
|
||||
import { ContinueCancelModal } from '../../widgets/ContinueCancelModal';
|
||||
import { HttpResponse } from '../../account-service/account.service';
|
||||
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
|
||||
declare const locale: string;
|
||||
|
||||
export interface ApplicationsPageProps {
|
||||
}
|
||||
|
||||
export interface ApplicationsPageState {
|
||||
isRowOpen: boolean[];
|
||||
applications: Application[];
|
||||
}
|
||||
|
||||
export interface GrantedScope {
|
||||
displayTest: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Consent {
|
||||
createDate: number;
|
||||
grantedScopes: GrantedScope[];
|
||||
lastUpdatedDate: number;
|
||||
}
|
||||
|
||||
interface Application {
|
||||
effectiveUrl: string;
|
||||
clientId: string;
|
||||
clientName: string;
|
||||
consent: Consent;
|
||||
description: string;
|
||||
inUse: boolean;
|
||||
offlineAccess: boolean;
|
||||
userConsentRequired: boolean;
|
||||
scope: string[];
|
||||
}
|
||||
|
||||
export class ApplicationsPage extends React.Component<ApplicationsPageProps, ApplicationsPageState> {
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
|
||||
public constructor(props: ApplicationsPageProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
this.state = {
|
||||
isRowOpen: [],
|
||||
applications: []
|
||||
};
|
||||
|
||||
this.fetchApplications();
|
||||
}
|
||||
|
||||
private removeConsent = (clientId: string) => {
|
||||
this.context!.doDelete("/applications/" + clientId + "/consent")
|
||||
.then(() => {
|
||||
this.fetchApplications();
|
||||
});
|
||||
}
|
||||
|
||||
private onToggle = (row: number): void => {
|
||||
const newIsRowOpen: boolean[] = this.state.isRowOpen;
|
||||
newIsRowOpen[row] = !newIsRowOpen[row];
|
||||
this.setState({ isRowOpen: newIsRowOpen });
|
||||
};
|
||||
|
||||
private fetchApplications(): void {
|
||||
this.context!.doGet<Application[]>("/applications")
|
||||
.then((response: HttpResponse<Application[]>) => {
|
||||
const applications = response.data || [];
|
||||
this.setState({
|
||||
isRowOpen: new Array(applications.length).fill(false),
|
||||
applications: applications
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private elementId(item: string, application: Application): string {
|
||||
return `application-${item}-${application.clientId}`;
|
||||
}
|
||||
|
||||
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]}>
|
||||
<DataListItemRow>
|
||||
<DataListToggle
|
||||
onClick={() => this.onToggle(appIndex)}
|
||||
isExpanded={this.state.isRowOpen[appIndex]}
|
||||
id={this.elementId('toggle', application)}
|
||||
aria-controls={this.elementId("expandable", application)}
|
||||
/>
|
||||
<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>,
|
||||
<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
|
||||
noPadding={false}
|
||||
aria-label={Msg.localize('applicationDetails')}
|
||||
id={this.elementId("expandable", application)}
|
||||
isHidden={!this.state.isRowOpen[appIndex]}
|
||||
>
|
||||
<Grid sm={12} md={12} lg={12}>
|
||||
<div className='pf-c-content'>
|
||||
<GridItem><strong>{Msg.localize('client') + ': '}</strong> {application.clientId}</GridItem>
|
||||
{application.description &&
|
||||
<GridItem><strong>{Msg.localize('description') + ': '}</strong> {application.description}</GridItem>
|
||||
}
|
||||
<GridItem><strong>URL: </strong> <span id={this.elementId('effectiveurl', application)}>{application.effectiveUrl.split('"')}</span></GridItem>
|
||||
{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>
|
||||
)
|
||||
})}
|
||||
<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>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
</Grid>
|
||||
{(application.consent || application.offlineAccess) &&
|
||||
<Grid gutter='sm'>
|
||||
<hr />
|
||||
<GridItem>
|
||||
<React.Fragment>
|
||||
<ContinueCancelModal
|
||||
buttonTitle={Msg.localize('removeButton')} // required
|
||||
buttonVariant='secondary' // defaults to 'primary'
|
||||
modalTitle={Msg.localize('removeModalTitle')} // required
|
||||
modalMessage={Msg.localize('removeModalMessage', [application.clientId])}
|
||||
modalContinueButtonLabel={Msg.localize('confirmButton')} // defaults to 'Continue'
|
||||
onContinue={() => this.removeConsent(application.clientId)} // required
|
||||
/>
|
||||
</React.Fragment>
|
||||
</GridItem>
|
||||
<GridItem><InfoAltIcon /> {Msg.localize('infoMessage')}</GridItem>
|
||||
</Grid>
|
||||
}
|
||||
</DataListContent>
|
||||
</DataListItem>
|
||||
)
|
||||
})}
|
||||
</DataList>
|
||||
</ContentPage>
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
export interface AuthenticatorPageProps {
|
||||
}
|
||||
|
||||
export class AuthenticatorPage extends React.Component<AuthenticatorPageProps> {
|
||||
|
||||
public constructor(props: AuthenticatorPageProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div>
|
||||
<h2>Hello Authenticator Page</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {HttpResponse} from '../../account-service/account.service';
|
||||
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
|
||||
import TimeUtil from '../../util/TimeUtil';
|
||||
|
||||
import {
|
||||
Bullseye,
|
||||
DataList,
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DataListCell,
|
||||
DataListItemCells,
|
||||
Grid,
|
||||
GridItem,
|
||||
Stack,
|
||||
StackItem
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import {
|
||||
AmazonIcon,
|
||||
ChromeIcon,
|
||||
EdgeIcon,
|
||||
FirefoxIcon,
|
||||
GlobeIcon,
|
||||
InternetExplorerIcon,
|
||||
OperaIcon,
|
||||
SafariIcon,
|
||||
YandexInternationalIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
import {Msg} from '../../widgets/Msg';
|
||||
import {ContinueCancelModal} from '../../widgets/ContinueCancelModal';
|
||||
import { KeycloakService } from '../../keycloak-service/keycloak.service';
|
||||
import { KeycloakContext } from '../../keycloak-service/KeycloakContext';
|
||||
|
||||
import {ContentPage} from '../ContentPage';
|
||||
import { ContentAlert } from '../ContentAlert';
|
||||
|
||||
export interface DeviceActivityPageProps {
|
||||
}
|
||||
|
||||
export interface DeviceActivityPageState {
|
||||
devices: Device[];
|
||||
}
|
||||
|
||||
interface Device {
|
||||
browser: string;
|
||||
current: boolean;
|
||||
device: string;
|
||||
ipAddress: string;
|
||||
lastAccess: number;
|
||||
mobile: boolean;
|
||||
os: string;
|
||||
osVersion: string;
|
||||
sessions: Session[];
|
||||
}
|
||||
|
||||
interface Session {
|
||||
browser: string;
|
||||
current: boolean;
|
||||
clients: Client[];
|
||||
expires: number;
|
||||
id: string;
|
||||
ipAddress: string;
|
||||
lastAccess: number;
|
||||
started: number;
|
||||
}
|
||||
|
||||
interface Client {
|
||||
clientId: string;
|
||||
clientName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2019 Red Hat Inc.
|
||||
*/
|
||||
export class DeviceActivityPage extends React.Component<DeviceActivityPageProps, DeviceActivityPageState> {
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
|
||||
public constructor(props: DeviceActivityPageProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
|
||||
this.state = {
|
||||
devices: []
|
||||
};
|
||||
|
||||
this.fetchDevices();
|
||||
}
|
||||
|
||||
private signOutAll = (keycloakService: KeycloakService) => {
|
||||
this.context!.doDelete("/sessions")
|
||||
.then( () => {
|
||||
keycloakService.logout();
|
||||
});
|
||||
}
|
||||
|
||||
private signOutSession = (device: Device, session: Session) => {
|
||||
this.context!.doDelete("/sessions/" + session.id)
|
||||
.then (() => {
|
||||
this.fetchDevices();
|
||||
ContentAlert.success('signedOutSession', [session.browser, device.os]);
|
||||
});
|
||||
}
|
||||
|
||||
private fetchDevices(): void {
|
||||
this.context!.doGet<Device[]>("/sessions/devices")
|
||||
.then((response: HttpResponse<Device[]>) => {
|
||||
console.log({response});
|
||||
|
||||
let devices: Device[] = this.moveCurrentToTop(response.data as Device[]);
|
||||
|
||||
this.setState({
|
||||
devices: devices
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// current device and session should display at the top of their respective lists
|
||||
private moveCurrentToTop(devices: Device[]): Device[] {
|
||||
let currentDevice: Device = devices[0];
|
||||
|
||||
devices.forEach((device: Device, index: number) => {
|
||||
if (device.current) {
|
||||
currentDevice = device;
|
||||
devices.splice(index, 1);
|
||||
devices.unshift(device);
|
||||
}
|
||||
});
|
||||
|
||||
currentDevice.sessions.forEach((session: Session, index: number) => {
|
||||
if (session.current) {
|
||||
const currentSession: Session[] = currentDevice.sessions.splice(index, 1);
|
||||
currentDevice.sessions.unshift(currentSession[0]);
|
||||
}
|
||||
});
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
private time(time: number): string {
|
||||
return TimeUtil.format(time * 1000);
|
||||
}
|
||||
|
||||
private elementId(item: string, session: Session): string {
|
||||
return `session-${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'/>);
|
||||
|
||||
return (<GlobeIcon id={this.elementId('icon-default', session)} size='lg'/>);
|
||||
}
|
||||
|
||||
private findOS(device: Device): string {
|
||||
if (device.os.toLowerCase().includes('unknown')) return Msg.localize('unknownOperatingSystem');
|
||||
|
||||
return device.os;
|
||||
}
|
||||
|
||||
private findOSVersion(device: Device): string {
|
||||
if (device.osVersion.toLowerCase().includes('unknown')) return '';
|
||||
|
||||
return device.osVersion;
|
||||
}
|
||||
|
||||
private makeClientsString(clients: Client[]): string {
|
||||
let clientsString = "";
|
||||
clients.forEach( (client: Client, index: number) => {
|
||||
let clientName: string;
|
||||
if (client.hasOwnProperty('clientName') && (client.clientName !== undefined) && (client.clientName !== '')) {
|
||||
clientName = Msg.localize(client.clientName);
|
||||
} else {
|
||||
clientName = client.clientId;
|
||||
}
|
||||
|
||||
clientsString += clientName;
|
||||
|
||||
if (clients.length > index + 1) clientsString += ', ';
|
||||
})
|
||||
|
||||
return clientsString;
|
||||
}
|
||||
|
||||
private isShowSignOutAll(devices: Device[]): boolean {
|
||||
if (devices.length === 0) return false;
|
||||
if (devices.length > 1) return true;
|
||||
if (devices[0].sessions.length > 1) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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) => {
|
||||
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>
|
||||
</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>
|
||||
{!session.current &&
|
||||
<ContinueCancelModal buttonTitle='doSignOut'
|
||||
buttonId={this.elementId('sign-out', session)}
|
||||
modalTitle='doSignOut'
|
||||
buttonVariant='secondary'
|
||||
modalMessage='signOutWarning'
|
||||
onContinue={() => this.signOutSession(device, session)}
|
||||
/>
|
||||
}
|
||||
|
||||
</GridItem>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
<GridItem span={12} /> {/* <-- bottom spacing */}
|
||||
</Grid>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
</DataList>
|
||||
</StackItem>
|
||||
|
||||
</Stack>
|
||||
</ContentPage>
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
|
||||
import { WarningTriangleIcon } from '@patternfly/react-icons';
|
||||
import {Msg} from '../../widgets/Msg';
|
||||
import EmptyMessageState from '../../widgets/EmptyMessageState';
|
||||
|
||||
|
||||
export class ForbiddenPage extends React.Component {
|
||||
|
||||
public constructor() {
|
||||
super({});
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<EmptyMessageState icon={WarningTriangleIcon} messageKey="forbidden">
|
||||
<Msg msgKey="needAccessRights"/>
|
||||
</EmptyMessageState>
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {withRouter, RouteComponentProps} from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DataList,
|
||||
DataListAction,
|
||||
DataListItemCells,
|
||||
DataListCell,
|
||||
DataListItemRow,
|
||||
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';
|
||||
|
||||
import {HttpResponse} from '../../account-service/account.service';
|
||||
import {AccountServiceContext} from '../../account-service/AccountServiceContext';
|
||||
import {Msg} from '../../widgets/Msg';
|
||||
import {ContentPage} from '../ContentPage';
|
||||
import {createRedirect} from '../../util/RedirectUri';
|
||||
|
||||
interface LinkedAccount {
|
||||
connected: boolean;
|
||||
social: boolean;
|
||||
providerAlias: string;
|
||||
providerName: string;
|
||||
displayName: string;
|
||||
linkedUsername: string;
|
||||
}
|
||||
|
||||
interface LinkedAccountsPageProps extends RouteComponentProps {
|
||||
}
|
||||
|
||||
interface LinkedAccountsPageState {
|
||||
linkedAccounts: LinkedAccount[];
|
||||
unLinkedAccounts: LinkedAccount[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, LinkedAccountsPageState> {
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
|
||||
public constructor(props: LinkedAccountsPageProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
|
||||
this.state = {
|
||||
linkedAccounts: [],
|
||||
unLinkedAccounts: []
|
||||
}
|
||||
|
||||
this.getLinkedAccounts();
|
||||
}
|
||||
|
||||
private getLinkedAccounts(): void {
|
||||
this.context!.doGet<LinkedAccount[]>("/linked-accounts")
|
||||
.then((response: HttpResponse<LinkedAccount[]>) => {
|
||||
console.log({response});
|
||||
const linkedAccounts = response.data!.filter((account) => account.connected);
|
||||
const unLinkedAccounts = response.data!.filter((account) => !account.connected);
|
||||
this.setState({linkedAccounts: linkedAccounts, unLinkedAccounts: unLinkedAccounts});
|
||||
});
|
||||
}
|
||||
|
||||
private unLinkAccount(account: LinkedAccount): void {
|
||||
const url = '/linked-accounts/' + account.providerName;
|
||||
|
||||
this.context!.doDelete<void>(url)
|
||||
.then((response: HttpResponse<void>) => {
|
||||
console.log({response});
|
||||
this.getLinkedAccounts();
|
||||
});
|
||||
}
|
||||
|
||||
private linkAccount(account: LinkedAccount): void {
|
||||
const url = '/linked-accounts/' + account.providerName;
|
||||
|
||||
const redirectUri: string = createRedirect(this.props.location.pathname);
|
||||
|
||||
this.context!.doGet<{accountLinkUri: string}>(url, { params: {providerId: account.providerName, redirectUri}})
|
||||
.then((response: HttpResponse<{accountLinkUri: string}>) => {
|
||||
console.log({response});
|
||||
window.location.href = response.data!.accountLinkUri;
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
|
||||
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>
|
||||
</ContentPage>
|
||||
);
|
||||
}
|
||||
|
||||
private emptyRow(isLinked: boolean): React.ReactNode {
|
||||
let isEmptyMessage = '';
|
||||
if (isLinked) {
|
||||
isEmptyMessage = Msg.localize('linkedEmpty');
|
||||
} else {
|
||||
isEmptyMessage = Msg.localize('unlinkedEmpty');
|
||||
}
|
||||
|
||||
return (
|
||||
<DataListItem key='emptyItem' aria-labelledby="empty-item">
|
||||
<DataListItemRow key='emptyRow'>
|
||||
<DataListItemCells dataListCells={[
|
||||
<DataListCell key='empty'><strong>{isEmptyMessage}</strong></DataListCell>
|
||||
]}/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
)
|
||||
}
|
||||
|
||||
private makeRows(accounts: LinkedAccount[], isLinked: boolean): React.ReactNode {
|
||||
if (accounts.length === 0) {
|
||||
return this.emptyRow(isLinked);
|
||||
}
|
||||
|
||||
return (
|
||||
<> {
|
||||
|
||||
accounts.map( (account: LinkedAccount) => (
|
||||
<DataListItem id={`${account.providerAlias}-idp`} key={account.providerName} aria-labelledby="simple-item1">
|
||||
<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>,
|
||||
]}/>
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' 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>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
))
|
||||
|
||||
} </>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
private badge(account: LinkedAccount): React.ReactNode {
|
||||
if (account.social) {
|
||||
return (<Badge><Msg msgKey='socialLogin'/></Badge>);
|
||||
}
|
||||
|
||||
return (<Badge style={{backgroundColor: "green"}} ><Msg msgKey='systemDefined'/></Badge>);
|
||||
}
|
||||
|
||||
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 LinkedAccountsPagewithRouter = withRouter(LinkedAccountsPage);
|
||||
export {LinkedAccountsPagewithRouter as LinkedAccountsPage};
|
@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import { Permission, PaginatedResources, Client } from './resource-model';
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
|
||||
export interface ResourcesTableProps {
|
||||
resources: PaginatedResources;
|
||||
}
|
||||
|
||||
export interface ResourcesTableState {
|
||||
permissions: Map<number, Permission[]>;
|
||||
}
|
||||
|
||||
export abstract class AbstractResourcesTable<S extends ResourcesTableState> extends React.Component<ResourcesTableProps, S> {
|
||||
|
||||
protected hasPermissions(row: number): boolean {
|
||||
return (this.state.permissions.has(row)) && (this.state.permissions.get(row)!.length > 0);
|
||||
}
|
||||
|
||||
private firstUser(row: number): string {
|
||||
if (!this.hasPermissions(row)) return 'ERROR!!!!'; // should never happen
|
||||
|
||||
return this.state.permissions.get(row)![0].username;
|
||||
}
|
||||
|
||||
protected numOthers(row: number): number {
|
||||
if (!this.hasPermissions(row)) return -1; // should never happen
|
||||
|
||||
return this.state.permissions.get(row)!.length - 1;
|
||||
}
|
||||
|
||||
public sharedWithUsersMessage(row: number): React.ReactNode {
|
||||
if (!this.hasPermissions(row)) return (<React.Fragment><Msg msgKey='resourceNotShared' /></React.Fragment>);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Msg msgKey='resourceSharedWith'>
|
||||
<strong>{this.firstUser(row)}</strong>
|
||||
</Msg>
|
||||
{this.numOthers(row) > 0 && <Msg msgKey='and'>
|
||||
<strong>{this.numOthers(row)}</strong>
|
||||
</Msg>}.
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
protected getClientName(client: Client): string {
|
||||
if (client.hasOwnProperty('name') && client.name !== null && client.name !== '') {
|
||||
return Msg.localize(client.name!);
|
||||
} else {
|
||||
return client.clientId;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Form,
|
||||
FormGroup,
|
||||
TextInput,
|
||||
InputGroup
|
||||
} from '@patternfly/react-core';
|
||||
import { OkIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { Resource, Permission, Scope } from './resource-model';
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
|
||||
import { ContentAlert } from '../ContentAlert';
|
||||
import { PermissionSelect } from './PermissionSelect';
|
||||
|
||||
interface EditTheResourceProps {
|
||||
resource: Resource;
|
||||
permissions: Permission[];
|
||||
onClose: () => void;
|
||||
children: (toggle: () => void) => void;
|
||||
}
|
||||
|
||||
interface EditTheResourceState {
|
||||
changed: boolean[];
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export class EditTheResource extends React.Component<EditTheResourceProps, EditTheResourceState> {
|
||||
protected static defaultProps = { permissions: [] };
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
|
||||
public constructor(props: EditTheResourceProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
|
||||
this.state = {
|
||||
changed: [],
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
private clearState(): void {
|
||||
this.setState({});
|
||||
}
|
||||
|
||||
private handleToggleDialog = () => {
|
||||
if (this.state.isOpen) {
|
||||
this.setState({ isOpen: false });
|
||||
this.props.onClose();
|
||||
} else {
|
||||
this.clearState();
|
||||
this.setState({ isOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
private updateChanged = (row: number) => {
|
||||
const changed = this.state.changed;
|
||||
changed[row] = !changed[row];
|
||||
this.setState({ changed });
|
||||
}
|
||||
|
||||
async savePermission(permission: Permission): Promise<void> {
|
||||
await this.context!.doPut(`/resources/${this.props.resource._id}/permissions`, [permission]);
|
||||
ContentAlert.success(Msg.localize('updateSuccess'));
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.props.children(this.handleToggleDialog)}
|
||||
|
||||
<Modal
|
||||
title={'Edit the resource - ' + this.props.resource.name}
|
||||
isLarge
|
||||
isOpen={this.state.isOpen}
|
||||
onClose={this.handleToggleDialog}
|
||||
actions={[
|
||||
<Button key="done" variant="link" id="done" onClick={this.handleToggleDialog}>
|
||||
<Msg msgKey='done' />
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form isHorizontal>
|
||||
{this.props.permissions.map((p, row) => (
|
||||
<React.Fragment>
|
||||
<FormGroup
|
||||
fieldId={`username-${row}`}
|
||||
label={Msg.localize('User')}
|
||||
>
|
||||
<TextInput id={`username-${row}`} type="text" value={p.username} isDisabled />
|
||||
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
fieldId={`permissions-${row}`}
|
||||
label={Msg.localize('permissions')}
|
||||
isRequired
|
||||
>
|
||||
<InputGroup>
|
||||
<PermissionSelect
|
||||
scopes={this.props.resource.scopes}
|
||||
selected={(p.scopes as string[]).map(s => new Scope(s))}
|
||||
direction={row === this.props.permissions.length - 1 ? "up" : "down"}
|
||||
onSelect={selection => {
|
||||
p.scopes = selection.map(s => s.name);
|
||||
this.updateChanged(row);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
id={`save-${row}`}
|
||||
isDisabled={!this.state.changed[row]}
|
||||
onClick={() => this.savePermission(p)}
|
||||
>
|
||||
<OkIcon />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Form>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import parse from '../../util/ParseLink';
|
||||
|
||||
import { Button, Level, LevelItem, Stack, StackItem, Tab, Tabs, TextInput } from '@patternfly/react-core';
|
||||
|
||||
import {HttpResponse} from '../../account-service/account.service';
|
||||
import {AccountServiceContext} from '../../account-service/AccountServiceContext';
|
||||
|
||||
import { PaginatedResources, Resource, Scope, Permission } from './resource-model';
|
||||
import {ResourcesTable} from './ResourcesTable';
|
||||
import {ContentPage} from '../ContentPage';
|
||||
import {Msg} from '../../widgets/Msg';
|
||||
import { SharedResourcesTable } from './SharedResourcesTable';
|
||||
|
||||
export interface MyResourcesPageProps {
|
||||
}
|
||||
|
||||
export interface MyResourcesPageState {
|
||||
activeTabKey: number;
|
||||
isModalOpen: boolean;
|
||||
nameFilter: string;
|
||||
myResources: PaginatedResources;
|
||||
sharedWithMe: PaginatedResources;
|
||||
}
|
||||
|
||||
const MY_RESOURCES_TAB = 0;
|
||||
const SHARED_WITH_ME_TAB = 1;
|
||||
|
||||
export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyResourcesPageState> {
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
private first = 0;
|
||||
private max = 5;
|
||||
|
||||
public constructor(props: MyResourcesPageProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
|
||||
this.state = {
|
||||
activeTabKey: MY_RESOURCES_TAB,
|
||||
nameFilter: '',
|
||||
isModalOpen: false,
|
||||
myResources: {nextUrl: '', prevUrl: '', data: []},
|
||||
sharedWithMe: {nextUrl: '', prevUrl: '', data: []}
|
||||
};
|
||||
|
||||
this.fetchInitialResources();
|
||||
}
|
||||
|
||||
private isSharedWithMeTab(): boolean {
|
||||
return this.state.activeTabKey === SHARED_WITH_ME_TAB;
|
||||
}
|
||||
|
||||
private hasNext(): boolean {
|
||||
if (this.isSharedWithMeTab()) {
|
||||
return (this.state.sharedWithMe.nextUrl !== null) && (this.state.sharedWithMe.nextUrl !== '');
|
||||
} else {
|
||||
return (this.state.myResources.nextUrl !== null) && (this.state.myResources.nextUrl !== '');
|
||||
}
|
||||
}
|
||||
|
||||
private hasPrevious(): boolean {
|
||||
if (this.isSharedWithMeTab()) {
|
||||
return (this.state.sharedWithMe.prevUrl !== null) && (this.state.sharedWithMe.prevUrl !== '');
|
||||
} else {
|
||||
return (this.state.myResources.prevUrl !== null) && (this.state.myResources.prevUrl !== '');
|
||||
}
|
||||
}
|
||||
|
||||
private fetchInitialResources(): void {
|
||||
if (this.isSharedWithMeTab()) {
|
||||
this.fetchResources("/resources/shared-with-me");
|
||||
} else {
|
||||
this.fetchResources("/resources", {first: this.first, max: this.max});
|
||||
}
|
||||
}
|
||||
|
||||
private fetchFilteredResources(params: Record<string, string|number>): void {
|
||||
if (this.isSharedWithMeTab()) {
|
||||
this.fetchResources("/resources/shared-with-me", params);
|
||||
} else {
|
||||
this.fetchResources("/resources", {...params, first: this.first, max: this.max});
|
||||
}
|
||||
}
|
||||
|
||||
private fetchResources(url: string, extraParams?: Record<string, string|number>): void {
|
||||
this.context!.doGet<Resource[]>(url, {params: extraParams})
|
||||
.then((response: HttpResponse<Resource[]>) => {
|
||||
const resources: Resource[] = response.data || [];
|
||||
resources.forEach((resource: Resource) => resource.shareRequests = []);
|
||||
|
||||
// serialize the Scope objects from JSON so that toString() will work.
|
||||
resources.forEach((resource: Resource) => resource.scopes = resource.scopes.map(this.makeScopeObj));
|
||||
|
||||
if (this.isSharedWithMeTab()) {
|
||||
this.setState({sharedWithMe: this.parseResourceResponse(response)}, this.fetchPending);
|
||||
} else {
|
||||
this.setState({myResources: this.parseResourceResponse(response)}, this.fetchPermissionRequests);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private makeScopeObj = (scope: Scope): Scope => {
|
||||
return new Scope(scope.name, scope.displayName);
|
||||
}
|
||||
|
||||
private fetchPermissionRequests = () => {
|
||||
this.state.myResources.data.forEach((resource: Resource) => {
|
||||
this.fetchShareRequests(resource);
|
||||
});
|
||||
}
|
||||
|
||||
private fetchShareRequests(resource: Resource): void {
|
||||
this.context!.doGet('/resources/' + resource._id + '/permissions/requests')
|
||||
.then((response: HttpResponse<Permission[]>) => {
|
||||
resource.shareRequests = response.data || [];
|
||||
if (resource.shareRequests.length > 0) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fetchPending = async () => {
|
||||
const response: HttpResponse<Resource[]> = await this.context!.doGet(`/resources/pending-requests`);
|
||||
const resources: Resource[] = response.data || [];
|
||||
resources.forEach((pendingRequest: Resource) => {
|
||||
this.state.sharedWithMe.data.forEach(resource => {
|
||||
if (resource._id === pendingRequest._id) {
|
||||
resource.shareRequests = [{username: 'me', scopes: pendingRequest.scopes}]
|
||||
this.forceUpdate();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private parseResourceResponse(response: HttpResponse<Resource[]>): PaginatedResources {
|
||||
const links: string | undefined = response.headers.get('link') || undefined;
|
||||
const parsed = parse(links);
|
||||
|
||||
let next = '';
|
||||
let prev = '';
|
||||
|
||||
if (parsed !== null) {
|
||||
if (parsed.next) next = parsed.next;
|
||||
if (parsed.prev) prev = parsed.prev;
|
||||
}
|
||||
|
||||
const resources: Resource[] = response.data || [];
|
||||
|
||||
return {nextUrl: next, prevUrl: prev, data: resources};
|
||||
}
|
||||
|
||||
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">
|
||||
<StackItem isFilled><span/></StackItem>
|
||||
<StackItem isFilled>
|
||||
<Level gutter='md'>
|
||||
<LevelItem>
|
||||
<TextInput value={this.state.nameFilter} onChange={this.handleFilterRequest} id={'filter-' + title} type="text" placeholder={Msg.localize('filterByName')} />
|
||||
</LevelItem>
|
||||
</Level>
|
||||
</StackItem>
|
||||
<StackItem isFilled>
|
||||
{!sharedResourcesTab && <ResourcesTable resources={resources}/>}
|
||||
{sharedResourcesTab && <SharedResourcesTable resources={resources}/>}
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</Tab>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<Level gutter='md'>
|
||||
<LevelItem>
|
||||
{this.hasPrevious() && <Button onClick={this.handlePreviousClick}><<Msg msgKey='previousPage'/></Button>}
|
||||
</LevelItem>
|
||||
|
||||
<LevelItem>
|
||||
{this.hasPrevious() && <Button onClick={this.handleFirstPageClick}><Msg msgKey='firstPage'/></Button>}
|
||||
</LevelItem>
|
||||
|
||||
<LevelItem>
|
||||
{this.hasNext() && <Button onClick={this.handleNextClick}><Msg msgKey='nextPage'/>></Button>}
|
||||
</LevelItem>
|
||||
</Level>
|
||||
</ContentPage>
|
||||
);
|
||||
}
|
||||
|
||||
private handleFilterRequest = (value: string) => {
|
||||
this.setState({nameFilter: value});
|
||||
this.fetchFilteredResources({name: value});
|
||||
}
|
||||
|
||||
private clearNextPrev(): void {
|
||||
const newMyResources: PaginatedResources = this.state.myResources;
|
||||
newMyResources.nextUrl = '';
|
||||
newMyResources.prevUrl = '';
|
||||
this.setState({myResources: newMyResources});
|
||||
}
|
||||
|
||||
private handleFirstPageClick = () => {
|
||||
this.fetchInitialResources();
|
||||
}
|
||||
|
||||
private handleNextClick = () => {
|
||||
if (this.isSharedWithMeTab()) {
|
||||
this.fetchResources(this.state.sharedWithMe.nextUrl);
|
||||
} else {
|
||||
this.fetchResources(this.state.myResources.nextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private handlePreviousClick = () => {
|
||||
if (this.isSharedWithMeTab()) {
|
||||
this.fetchResources(this.state.sharedWithMe.prevUrl);
|
||||
} else {
|
||||
this.fetchResources(this.state.myResources.prevUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private handleTabClick = (event: React.MouseEvent<HTMLInputElement>, tabIndex: number) => {
|
||||
if (this.state.activeTabKey === tabIndex) return;
|
||||
|
||||
this.setState({
|
||||
nameFilter: '',
|
||||
activeTabKey: tabIndex
|
||||
}, () => {this.fetchInitialResources()});
|
||||
};
|
||||
};
|
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Text,
|
||||
Badge,
|
||||
DataListItem,
|
||||
DataList,
|
||||
TextVariants,
|
||||
DataListItemRow,
|
||||
DataListItemCells,
|
||||
DataListCell,
|
||||
Chip,
|
||||
Split,
|
||||
SplitItem
|
||||
} from '@patternfly/react-core';
|
||||
import { UserCheckIcon } from '@patternfly/react-icons';
|
||||
|
||||
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';
|
||||
|
||||
|
||||
interface PermissionRequestProps {
|
||||
resource: Resource;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface PermissionRequestState {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export class PermissionRequest extends React.Component<PermissionRequestProps, PermissionRequestState> {
|
||||
protected static defaultProps = { permissions: [], row: 0 };
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
|
||||
public constructor(props: PermissionRequestProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
private handleApprove = async (shareRequest: Permission, index: number) => {
|
||||
this.handle(shareRequest.username, shareRequest.scopes as Scope[], true);
|
||||
this.props.resource.shareRequests.splice(index, 1);
|
||||
};
|
||||
|
||||
private handleDeny = async (shareRequest: Permission, index: number) => {
|
||||
this.handle(shareRequest.username, shareRequest.scopes as Scope[]);
|
||||
this.props.resource.shareRequests.splice(index, 1)
|
||||
};
|
||||
|
||||
private handle = async (username: string, scopes: Scope[], approve: boolean = false) => {
|
||||
const id = this.props.resource._id
|
||||
this.handleToggleDialog();
|
||||
|
||||
const permissionsRequest: HttpResponse<Permission[]> = await this.context!.doGet(`/resources/${id}/permissions`);
|
||||
const permissions = permissionsRequest.data || [];
|
||||
const foundPermission = permissions.find(p => p.username === username);
|
||||
const userScopes = foundPermission ? (foundPermission.scopes as Scope[]): [];
|
||||
if (approve) {
|
||||
userScopes.push(...scopes);
|
||||
}
|
||||
try {
|
||||
await this.context!.doPut(`/resources/${id}/permissions`, [{ username: username, scopes: userScopes }] )
|
||||
ContentAlert.success(Msg.localize('shareSuccess'));
|
||||
this.props.onClose();
|
||||
} catch (e) {
|
||||
console.error('Could not update permissions', e.error);
|
||||
}
|
||||
};
|
||||
|
||||
private handleToggleDialog = () => {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const id = `shareRequest-${this.props.resource.name.replace(/\s/, '-')}`;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button id={id} variant="link" onClick={this.handleToggleDialog}>
|
||||
<UserCheckIcon size="lg" />
|
||||
<Badge>{this.props.resource.shareRequests.length}</Badge>
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
id={`modal-${id}`}
|
||||
title={Msg.localize('permissionRequests') + ' - ' + this.props.resource.name}
|
||||
isLarge={true}
|
||||
isOpen={this.state.isOpen}
|
||||
onClose={this.handleToggleDialog}
|
||||
actions={[
|
||||
<Button id={`close-${id}`} key="close" variant="link" onClick={this.handleToggleDialog}>
|
||||
<Msg msgKey="close" />
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<DataList aria-label={Msg.localize('permissionRequests')}>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key='permissions-name-header' width={5}>
|
||||
<strong>Requestor</strong>
|
||||
</DataListCell>,
|
||||
<DataListCell key='permissions-requested-header' width={5}>
|
||||
<strong><Msg msgKey='permissionRequests' /></strong>
|
||||
</DataListCell>,
|
||||
<DataListCell key='permission-request-header' width={5}>
|
||||
</DataListCell>
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
{this.props.resource.shareRequests.map((shareRequest, i) =>
|
||||
<DataListItem key={i} aria-labelledby="requestor">
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell id={`requestor${i}`} key={`requestor${i}`}>
|
||||
<span>
|
||||
{shareRequest.firstName} {shareRequest.lastName} {shareRequest.lastName ? '' : shareRequest.username}
|
||||
</span><br />
|
||||
<Text component={TextVariants.small}>{shareRequest.email}</Text>
|
||||
</DataListCell>,
|
||||
<DataListCell id={`permissions${i}`} key={`permissions${i}`}>
|
||||
{(shareRequest.scopes as Scope[]).map((scope, j) => <Chip key={j} isReadOnly>{scope}</Chip>)}
|
||||
</DataListCell>,
|
||||
<DataListCell key={`actions${i}`}>
|
||||
<Split gutter="sm">
|
||||
<SplitItem>
|
||||
<Button
|
||||
id={`accept-${i}-${id}`}
|
||||
onClick={() => this.handleApprove(shareRequest, i)}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Button
|
||||
id={`deny-${i}-${id}`}
|
||||
variant="danger"
|
||||
onClick={() => this.handleDeny(shareRequest, i)}
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</DataListCell>
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
)}
|
||||
</DataList>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Select, SelectOption, SelectVariant, SelectOptionObject } from '@patternfly/react-core';
|
||||
import { Scope } from './resource-model';
|
||||
|
||||
interface PermissionSelectState {
|
||||
selected: ScopeValue[];
|
||||
isExpanded: boolean;
|
||||
scopes: JSX.Element[];
|
||||
}
|
||||
|
||||
interface PermissionSelectProps {
|
||||
scopes: Scope[];
|
||||
selected?: Scope[];
|
||||
direction?: 'up' | 'down';
|
||||
onSelect: (selected: Scope[]) => void;
|
||||
}
|
||||
|
||||
class ScopeValue implements SelectOptionObject {
|
||||
value: Scope;
|
||||
constructor(value: Scope) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.value.displayName ? this.value.displayName : this.value.name;
|
||||
}
|
||||
|
||||
compareTo(selectOption: Scope): boolean {
|
||||
return selectOption.name === this.value.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class PermissionSelect extends React.Component<PermissionSelectProps, PermissionSelectState> {
|
||||
constructor(props: PermissionSelectProps) {
|
||||
super(props);
|
||||
|
||||
let values: ScopeValue[] = [];
|
||||
if (this.props.selected) {
|
||||
values = this.props.selected!.map(s => new ScopeValue(s))
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isExpanded: false,
|
||||
selected: values,
|
||||
scopes: this.props.scopes.map((option, index) => (
|
||||
<SelectOption key={index} value={values.find(s => s.compareTo(option)) || new ScopeValue(option)} />
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
private onSelect = (_event: React.MouseEvent | React.ChangeEvent, selection: ScopeValue): void => {
|
||||
const { selected } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
if (selected.includes(selection)) {
|
||||
this.setState(
|
||||
prevState => ({ selected: prevState.selected.filter(item => item !== selection) }),
|
||||
() => onSelect(this.state.selected.map(sv => sv.value))
|
||||
);
|
||||
} else {
|
||||
this.setState(
|
||||
prevState => ({ selected: [...prevState.selected, selection] }),
|
||||
() => onSelect(this.state.selected.map(sv => sv.value))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private onToggle = (isExpanded: boolean) => {
|
||||
this.setState({
|
||||
isExpanded
|
||||
});
|
||||
}
|
||||
|
||||
private clearSelection = () => {
|
||||
this.setState({
|
||||
selected: [],
|
||||
isExpanded: false
|
||||
});
|
||||
this.props.onSelect([]);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isExpanded, selected } = this.state;
|
||||
const titleId = 'permission-id';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span id={titleId} hidden>
|
||||
Select the permissions
|
||||
</span>
|
||||
<Select
|
||||
direction={this.props.direction || 'down'}
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
ariaLabelTypeAhead="Select the permissions"
|
||||
onToggle={this.onToggle}
|
||||
onSelect={this.onSelect}
|
||||
onClear={this.clearSelection}
|
||||
selections={selected}
|
||||
isExpanded={isExpanded}
|
||||
ariaLabelledBy={titleId}
|
||||
placeholderText="Select the permissions"
|
||||
>
|
||||
{this.state.scopes}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,341 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
DataList,
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DataListCell,
|
||||
DataListToggle,
|
||||
DataListContent,
|
||||
DataListItemCells,
|
||||
Level,
|
||||
LevelItem,
|
||||
Button,
|
||||
DataListAction,
|
||||
DataListActionVisibility,
|
||||
Dropdown,
|
||||
DropdownPosition,
|
||||
DropdownItem,
|
||||
KebabToggle
|
||||
} from '@patternfly/react-core';
|
||||
import { css } from '@patternfly/react-styles';
|
||||
|
||||
import { Remove2Icon, RepositoryIcon, ShareAltIcon, EditAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { HttpResponse } from '../../account-service/account.service';
|
||||
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
|
||||
import { PermissionRequest } from "./PermissionRequest";
|
||||
import { ShareTheResource } from "./ShareTheResource";
|
||||
import { Permission, Resource } from "./resource-model";
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import { ResourcesTableState, ResourcesTableProps, AbstractResourcesTable } from './AbstractResourceTable';
|
||||
import { EditTheResource } from './EditTheResource';
|
||||
import { ContentAlert } from '../ContentAlert';
|
||||
import EmptyMessageState from '../../widgets/EmptyMessageState';
|
||||
import { ContinueCancelModal } from '../../widgets/ContinueCancelModal';
|
||||
|
||||
export interface CollapsibleResourcesTableState extends ResourcesTableState {
|
||||
isRowOpen: boolean[];
|
||||
contextOpen: boolean[];
|
||||
isModalActive: boolean;
|
||||
}
|
||||
|
||||
export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesTableState> {
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
|
||||
public constructor(props: ResourcesTableProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
|
||||
this.state = {
|
||||
isRowOpen: [],
|
||||
contextOpen: [],
|
||||
isModalActive: false,
|
||||
permissions: new Map()
|
||||
}
|
||||
}
|
||||
|
||||
private onToggle = (row: number): void => {
|
||||
const newIsRowOpen: boolean[] = this.state.isRowOpen;
|
||||
newIsRowOpen[row] = !newIsRowOpen[row];
|
||||
if (newIsRowOpen[row]) this.fetchPermissions(this.props.resources.data[row], row);
|
||||
this.setState({ isRowOpen: newIsRowOpen });
|
||||
};
|
||||
|
||||
private onContextToggle = (row: number, isOpen: boolean): void => {
|
||||
if (this.state.isModalActive) return;
|
||||
const data = this.props.resources.data;
|
||||
const contextOpen = this.state.contextOpen;
|
||||
contextOpen[row] = isOpen;
|
||||
if (isOpen) {
|
||||
const index = row > data.length ? row - data.length - 1 : row;
|
||||
this.fetchPermissions(data[index], index);
|
||||
}
|
||||
this.setState({ contextOpen });
|
||||
}
|
||||
|
||||
private fetchPermissions(resource: Resource, row: number): void {
|
||||
this.context!.doGet(`/resources/${resource._id}/permissions`)
|
||||
.then((response: HttpResponse<Permission[]>) => {
|
||||
const newPermissions: Map<number, Permission[]> = new Map(this.state.permissions);
|
||||
newPermissions.set(row, response.data || []);
|
||||
this.setState({ permissions: newPermissions });
|
||||
});
|
||||
}
|
||||
|
||||
private removeShare(resource: Resource, row: number): Promise<void> {
|
||||
const permissions = this.state.permissions.get(row)!.map(a => ({ username: a.username, scopes: [] }));
|
||||
return this.context!.doPut(`/resources/${resource._id}/permissions`, permissions)
|
||||
.then(() => {
|
||||
ContentAlert.success(Msg.localize('unShareSuccess'));
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.props.resources.data.length === 0) {
|
||||
return (
|
||||
<EmptyMessageState icon={RepositoryIcon} messageKey="notHaveAnyResource"/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DataList aria-label={Msg.localize('resources')} id="resourcesList">
|
||||
<DataListItem key='resource-header' aria-labelledby='resource-header'>
|
||||
<DataListItemRow>
|
||||
// invisible toggle allows headings to line up properly
|
||||
<span style={{ visibility: 'hidden' }}>
|
||||
<DataListToggle
|
||||
isExpanded={false}
|
||||
id='resource-header-invisible-toggle'
|
||||
aria-controls="ex-expand1"
|
||||
/>
|
||||
</span>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key='resource-name-header' width={5}>
|
||||
<strong><Msg msgKey='resourceName' /></strong>
|
||||
</DataListCell>,
|
||||
<DataListCell key='application-name-header' width={5}>
|
||||
<strong><Msg msgKey='application' /></strong>
|
||||
</DataListCell>,
|
||||
<DataListCell key='permission-request-header' width={5}>
|
||||
<strong><Msg msgKey='permissionRequests' /></strong>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
{this.props.resources.data.map((resource: Resource, row: number) => (
|
||||
<DataListItem key={'resource-' + row} aria-labelledby={resource.name} isExpanded={this.state.isRowOpen[row]}>
|
||||
<DataListItemRow>
|
||||
<DataListToggle
|
||||
onClick={() => this.onToggle(row)}
|
||||
isExpanded={this.state.isRowOpen[row]}
|
||||
id={'resourceToggle-' + row}
|
||||
aria-controls="ex-expand1"
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell id={'resourceName-' + row} key={'resourceName-' + row} width={5}>
|
||||
<Msg msgKey={resource.name} />
|
||||
</DataListCell>,
|
||||
<DataListCell id={'resourceClient-' + row} key={'resourceClient-' + row} width={5}>
|
||||
<a href={resource.client.baseUrl}>{this.getClientName(resource.client)}</a>
|
||||
</DataListCell>,
|
||||
<DataListCell id={'resourceRequests-' + row} key={'permissionRequests-' + row} width={5}>
|
||||
{resource.shareRequests.length > 0 &&
|
||||
<PermissionRequest
|
||||
resource={resource}
|
||||
onClose={() => this.fetchPermissions(resource, row)}
|
||||
></PermissionRequest>
|
||||
}
|
||||
</DataListCell>
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
className={DataListActionVisibility.hiddenOnLg}
|
||||
aria-labelledby="check-action-item3 check-action-action3"
|
||||
id="check-action-action3"
|
||||
aria-label="Actions"
|
||||
>
|
||||
<Dropdown
|
||||
isPlain
|
||||
position={DropdownPosition.right}
|
||||
onSelect={() => this.setState({ isModalActive: true })}
|
||||
toggle={<KebabToggle onToggle={isOpen => this.onContextToggle(row + this.props.resources.data.length + 1, isOpen)} />}
|
||||
isOpen={this.state.contextOpen[row + this.props.resources.data.length + 1]}
|
||||
dropdownItems={[
|
||||
<ShareTheResource
|
||||
resource={resource}
|
||||
permissions={this.state.permissions.get(row)!}
|
||||
sharedWithUsersMsg={this.sharedWithUsersMessage(row)}
|
||||
onClose={() => {
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row + this.props.resources.data.length + 1, false);
|
||||
this.fetchPermissions(resource, row + this.props.resources.data.length + 1);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{
|
||||
(toggle: () => void) => (
|
||||
<DropdownItem id={'mob-share-' + row} key="mob-share" onClick={toggle}>
|
||||
<ShareAltIcon /> <Msg msgKey="share"/>
|
||||
</DropdownItem>)
|
||||
}
|
||||
</ShareTheResource>,
|
||||
<EditTheResource
|
||||
resource={resource}
|
||||
permissions={this.state.permissions.get(row)!}
|
||||
onClose={() => {
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row + this.props.resources.data.length + 1, false);
|
||||
this.fetchPermissions(resource, row + this.props.resources.data.length + 1);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{
|
||||
(toggle: () => void) => (
|
||||
<DropdownItem
|
||||
id={'mob-edit-' + row} key="mob-edit"
|
||||
isDisabled={this.numOthers(row) < 0}
|
||||
onClick={toggle}
|
||||
>
|
||||
<EditAltIcon /> <Msg msgKey="edit"/>
|
||||
</DropdownItem>)
|
||||
}
|
||||
</EditTheResource>,
|
||||
<ContinueCancelModal
|
||||
render={(toggle: () => void) => (
|
||||
<DropdownItem
|
||||
id={'mob-remove-' + row}
|
||||
key="mob-remove"
|
||||
isDisabled={this.numOthers(row) < 0}
|
||||
onClick={toggle}
|
||||
>
|
||||
<Remove2Icon /> <Msg msgKey="unShare"/>
|
||||
</DropdownItem>
|
||||
)}
|
||||
modalTitle="unShare"
|
||||
modalMessage="unShareAllConfirm"
|
||||
onClose={() =>
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row + this.props.resources.data.length + 1, false);
|
||||
})
|
||||
}
|
||||
onContinue={() => this.removeShare(resource, row)
|
||||
.then(() => this.fetchPermissions(resource, row + this.props.resources.data.length + 1))}
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
</DataListAction>
|
||||
<DataListAction
|
||||
id={`actions-${row}`}
|
||||
className={css(DataListActionVisibility.visibleOnLg, DataListActionVisibility.hidden)}
|
||||
aria-labelledby="Row actions"
|
||||
aria-label="Actions"
|
||||
>
|
||||
<ShareTheResource
|
||||
resource={resource}
|
||||
permissions={this.state.permissions.get(row)!}
|
||||
sharedWithUsersMsg={this.sharedWithUsersMessage(row)}
|
||||
onClose={() => this.fetchPermissions(resource, row)}
|
||||
>
|
||||
{
|
||||
(toggle: () => void) => (
|
||||
<Button id={`share-${row}`} variant="link" onClick={toggle}>
|
||||
<ShareAltIcon /> <Msg msgKey="share"/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</ShareTheResource>
|
||||
<Dropdown
|
||||
id={`action-menu-${row}`}
|
||||
isPlain
|
||||
position={DropdownPosition.right}
|
||||
toggle={<KebabToggle onToggle={isOpen => this.onContextToggle(row, isOpen)} />}
|
||||
onSelect={() => this.setState({ isModalActive: true })}
|
||||
isOpen={this.state.contextOpen[row]}
|
||||
dropdownItems={[
|
||||
<EditTheResource
|
||||
resource={resource}
|
||||
permissions={this.state.permissions.get(row)!}
|
||||
onClose={() => {
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row, false);
|
||||
this.fetchPermissions(resource, row);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{
|
||||
(toggle: () => void) => (
|
||||
<DropdownItem
|
||||
id={'edit-' + row}
|
||||
key="edit"
|
||||
component="button"
|
||||
isDisabled={this.numOthers(row) < 0}
|
||||
onClick={toggle}
|
||||
>
|
||||
<EditAltIcon /> <Msg msgKey="edit"/>
|
||||
</DropdownItem>)
|
||||
}
|
||||
</EditTheResource>,
|
||||
<ContinueCancelModal
|
||||
render={(toggle: () => void) => (
|
||||
<DropdownItem
|
||||
id={'remove-' + row}
|
||||
key="remove"
|
||||
component="button"
|
||||
isDisabled={this.numOthers(row) < 0}
|
||||
onClick={toggle}
|
||||
>
|
||||
<Remove2Icon /> <Msg msgKey="unShare"/>
|
||||
</DropdownItem>
|
||||
)}
|
||||
modalTitle="unShare"
|
||||
modalMessage='unShareAllConfirm'
|
||||
onClose={() =>
|
||||
this.setState({ isModalActive: false }, () => {
|
||||
this.onContextToggle(row, false);
|
||||
})
|
||||
}
|
||||
onContinue={() => this.removeShare(resource, row).then(() => this.fetchPermissions(resource, row))}
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
</DataListAction>
|
||||
|
||||
</DataListItemRow>
|
||||
<DataListContent
|
||||
noPadding={false}
|
||||
aria-label="Session Details"
|
||||
id={'ex-expand' + row}
|
||||
isHidden={!this.state.isRowOpen[row]}
|
||||
>
|
||||
<Level gutter='md'>
|
||||
<LevelItem><span /></LevelItem>
|
||||
<LevelItem id={'shared-with-user-message-' + row}>{this.sharedWithUsersMessage(row)}</LevelItem>
|
||||
<LevelItem><span /></LevelItem>
|
||||
</Level>
|
||||
</DataListContent>
|
||||
</DataListItem>
|
||||
))}
|
||||
</DataList>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
ChipGroupToolbarItem,
|
||||
Form,
|
||||
FormGroup,
|
||||
Gallery,
|
||||
GalleryItem,
|
||||
Modal,
|
||||
Stack,
|
||||
StackItem,
|
||||
TextInput
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
|
||||
import { Resource, Permission, Scope } from './resource-model';
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import {ContentAlert} from '../ContentAlert';
|
||||
import { PermissionSelect } from './PermissionSelect';
|
||||
|
||||
interface ShareTheResourceProps {
|
||||
resource: Resource;
|
||||
permissions: Permission[];
|
||||
sharedWithUsersMsg: React.ReactNode;
|
||||
onClose: () => void;
|
||||
children: (toggle: () => void) => void;
|
||||
}
|
||||
|
||||
interface ShareTheResourceState {
|
||||
isOpen: boolean;
|
||||
permissionsSelected: Scope[];
|
||||
permissionsUnSelected: Scope[];
|
||||
usernames: string[];
|
||||
usernameInput: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2019 Red Hat Inc.
|
||||
*/
|
||||
export class ShareTheResource extends React.Component<ShareTheResourceProps, ShareTheResourceState> {
|
||||
protected static defaultProps = {permissions: []};
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
|
||||
public constructor(props: ShareTheResourceProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
permissionsSelected: [],
|
||||
permissionsUnSelected: this.props.resource.scopes,
|
||||
usernames: [],
|
||||
usernameInput: ''
|
||||
};
|
||||
}
|
||||
|
||||
private clearState(): void {
|
||||
this.setState({
|
||||
permissionsSelected: [],
|
||||
permissionsUnSelected: this.props.resource.scopes,
|
||||
usernames: [],
|
||||
usernameInput: ''
|
||||
});
|
||||
}
|
||||
|
||||
private handleAddPermission = () => {
|
||||
const rscId: string = this.props.resource._id;
|
||||
const newPermissions: string[] = [];
|
||||
|
||||
for (const permission of this.state.permissionsSelected) {
|
||||
newPermissions.push(permission.name);
|
||||
}
|
||||
|
||||
const permissions = [];
|
||||
|
||||
for (const username of this.state.usernames) {
|
||||
permissions.push({username: username, scopes: newPermissions});
|
||||
}
|
||||
|
||||
this.handleToggleDialog();
|
||||
|
||||
this.context!.doPut(`/resources/${rscId}/permissions`, permissions)
|
||||
.then(() => {
|
||||
ContentAlert.success('shareSuccess');
|
||||
this.props.onClose();
|
||||
})
|
||||
};
|
||||
|
||||
private handleToggleDialog = () => {
|
||||
if (this.state.isOpen) {
|
||||
this.setState({isOpen: false});
|
||||
this.props.onClose();
|
||||
} else {
|
||||
this.clearState();
|
||||
this.setState({isOpen: true});
|
||||
}
|
||||
};
|
||||
|
||||
private handleUsernameChange = (username: string) => {
|
||||
this.setState({usernameInput: username});
|
||||
}
|
||||
|
||||
private handleAddUsername = async () => {
|
||||
if ((this.state.usernameInput !== '') && (!this.state.usernames.includes(this.state.usernameInput))) {
|
||||
const response = await this.context!.doGet<{username: string}>(`/resources/${this.props.resource._id}/user`, { params: { value: this.state.usernameInput } });
|
||||
if (response.data && response.data.username) {
|
||||
this.setState({ usernameInput: '', usernames: [...this.state.usernames, this.state.usernameInput] });
|
||||
} else {
|
||||
ContentAlert.info('userNotFound', [this.state.usernameInput]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleEnterKeyInAddField = (event: React.KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.handleAddUsername();
|
||||
}
|
||||
}
|
||||
|
||||
private handleDeleteUsername = (username: string) => {
|
||||
const newUsernames: string[] = this.state.usernames.filter(user => user !== username);
|
||||
this.setState({usernames: newUsernames});
|
||||
}
|
||||
|
||||
private isAddDisabled(): boolean {
|
||||
return this.state.usernameInput === '' || this.isAlreadyShared();
|
||||
}
|
||||
|
||||
private isAlreadyShared(): boolean {
|
||||
for (let permission of this.props.permissions) {
|
||||
if (permission.username === this.state.usernameInput) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private isFormInvalid(): boolean {
|
||||
return (this.state.usernames.length === 0) || (this.state.permissionsSelected.length === 0);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.props.children(this.handleToggleDialog)}
|
||||
|
||||
<Modal
|
||||
title={'Share the resource - ' + this.props.resource.name}
|
||||
isLarge={true}
|
||||
isOpen={this.state.isOpen}
|
||||
onClose={this.handleToggleDialog}
|
||||
actions={[
|
||||
<Button key="cancel" variant="link" onClick={this.handleToggleDialog}>
|
||||
<Msg msgKey='cancel'/>
|
||||
</Button>,
|
||||
<Button key="confirm" variant="primary" id="done" onClick={this.handleAddPermission} isDisabled={this.isFormInvalid()}>
|
||||
<Msg msgKey='done'/>
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Stack gutter='md'>
|
||||
<StackItem isFilled>
|
||||
<Form>
|
||||
<FormGroup
|
||||
label="Add users to share your resource with"
|
||||
type="string"
|
||||
helperTextInvalid={Msg.localize('resourceAlreadyShared')}
|
||||
fieldId="username"
|
||||
isRequired
|
||||
isValid={!this.isAlreadyShared()}
|
||||
>
|
||||
<Gallery gutter='sm'>
|
||||
<GalleryItem>
|
||||
<TextInput
|
||||
value={this.state.usernameInput}
|
||||
isValid={!this.isAlreadyShared()}
|
||||
id="username"
|
||||
aria-describedby="username-helper"
|
||||
placeholder="Username or email"
|
||||
onChange={this.handleUsernameChange}
|
||||
onKeyPress={this.handleEnterKeyInAddField}
|
||||
/>
|
||||
</GalleryItem>
|
||||
<GalleryItem>
|
||||
<Button key="add-user" variant="primary" id="add" onClick={this.handleAddUsername} isDisabled={this.isAddDisabled()}>
|
||||
<Msg msgKey="add"/>
|
||||
</Button>
|
||||
</GalleryItem>
|
||||
|
||||
</Gallery>
|
||||
<ChipGroup withToolbar>
|
||||
<ChipGroupToolbarItem key='users-selected' categoryName='Share with '>
|
||||
{this.state.usernames.map((currentChip: string) => (
|
||||
<Chip key={currentChip} onClick={() => this.handleDeleteUsername(currentChip)}>
|
||||
{currentChip}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroupToolbarItem>
|
||||
</ChipGroup>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label=""
|
||||
fieldId="permissions-selected"
|
||||
>
|
||||
<PermissionSelect
|
||||
scopes={this.state.permissionsUnSelected}
|
||||
onSelect={selection => this.setState({ permissionsSelected: selection })}
|
||||
direction="up"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</StackItem>
|
||||
<StackItem isFilled><br/></StackItem>
|
||||
<StackItem isFilled>
|
||||
{this.props.sharedWithUsersMsg}
|
||||
</StackItem>
|
||||
|
||||
</Stack>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
DataList,
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DataListCell,
|
||||
DataListItemCells,
|
||||
ChipGroup,
|
||||
ChipGroupToolbarItem,
|
||||
Chip
|
||||
} from '@patternfly/react-core';
|
||||
import { RepositoryIcon } from '@patternfly/react-icons';
|
||||
|
||||
|
||||
import { PaginatedResources, Resource, Scope } from "./resource-model";
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import { AbstractResourcesTable, ResourcesTableState } from './AbstractResourceTable';
|
||||
import EmptyMessageState from '../../widgets/EmptyMessageState';
|
||||
|
||||
export interface ResourcesTableProps {
|
||||
resources: PaginatedResources;
|
||||
noResourcesMessage: string;
|
||||
}
|
||||
|
||||
export class SharedResourcesTable extends AbstractResourcesTable<ResourcesTableState> {
|
||||
|
||||
public constructor(props: ResourcesTableProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
permissions: new Map()
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.props.resources.data.length === 0) {
|
||||
return (
|
||||
<EmptyMessageState icon={RepositoryIcon} messageKey="noResourcesSharedWithYou"/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DataList aria-label={Msg.localize('resources')} id="sharedResourcesList">
|
||||
<DataListItem key='resource-header' aria-labelledby='resource-header'>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key='resource-name-header' width={2}>
|
||||
<strong><Msg msgKey='resourceName'/></strong>
|
||||
</DataListCell>,
|
||||
<DataListCell key='application-name-header' width={2}>
|
||||
<strong><Msg msgKey='application'/></strong>
|
||||
</DataListCell>,
|
||||
<DataListCell key='permission-header' width={2}/>,
|
||||
<DataListCell key='requests-header' width={2}/>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
{this.props.resources.data.map((resource: Resource, row: number) => (
|
||||
<DataListItem key={'resource-' + row} aria-labelledby={resource.name}>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key={'resourceName-' + row} width={2}>
|
||||
<Msg msgKey={resource.name}/>
|
||||
</DataListCell>,
|
||||
<DataListCell key={'resourceClient-' + row} width={2}>
|
||||
<a href={resource.client.baseUrl}>{this.getClientName(resource.client)}</a>
|
||||
</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>}
|
||||
</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>
|
||||
}
|
||||
</DataListCell>
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
))}
|
||||
</DataList>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
export interface Resource {
|
||||
_id: string;
|
||||
name: string;
|
||||
client: Client;
|
||||
scopes: Scope[];
|
||||
uris: string[];
|
||||
shareRequests: Permission[];
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
baseUrl: string;
|
||||
clientId: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export class Scope {
|
||||
public constructor(public name: string, public displayName?: string) {}
|
||||
|
||||
public toString(): string {
|
||||
if (this.hasOwnProperty('displayName') && (this.displayName)) {
|
||||
return this.displayName;
|
||||
} else {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PaginatedResources {
|
||||
nextUrl: string;
|
||||
prevUrl: string;
|
||||
data: Resource[];
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
scopes: Scope[] | string[]; // this should be Scope[] - fix API
|
||||
username: string;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { WarningTriangleIcon } from '@patternfly/react-icons';
|
||||
import {withRouter, RouteComponentProps} from 'react-router-dom';
|
||||
import {Msg} from '../../widgets/Msg';
|
||||
import EmptyMessageState from '../../widgets/EmptyMessageState';
|
||||
|
||||
export interface PageNotFoundProps extends RouteComponentProps {}
|
||||
|
||||
class PgNotFound extends React.Component<PageNotFoundProps> {
|
||||
|
||||
public constructor(props: PageNotFoundProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<EmptyMessageState icon={WarningTriangleIcon} messageKey="pageNotFound">
|
||||
<Msg msgKey="invalidRoute" params={[this.props.location.pathname]} />
|
||||
</EmptyMessageState>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const PageNotFound = withRouter(PgNotFound);
|
@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
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';
|
||||
|
||||
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;
|
||||
|
||||
interface PasswordDetails {
|
||||
registered: boolean;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
type CredCategory = 'password' | 'two-factor' | 'passwordless';
|
||||
type CredType = string;
|
||||
type CredTypeMap = Map<CredType, CredentialContainer>;
|
||||
type CredContainerMap = Map<CredCategory, CredTypeMap>;
|
||||
|
||||
interface UserCredential {
|
||||
id: string;
|
||||
type: string;
|
||||
userLabel: string;
|
||||
createdDate?: number;
|
||||
strCreatedDate?: string;
|
||||
}
|
||||
|
||||
// A CredentialContainer is unique by combo of credential type and credential category
|
||||
interface CredentialContainer {
|
||||
category: CredCategory;
|
||||
type: CredType;
|
||||
displayName: string;
|
||||
helptext?: string;
|
||||
createAction?: string;
|
||||
updateAction?: string;
|
||||
removeable: boolean;
|
||||
userCredentials: UserCredential[];
|
||||
}
|
||||
|
||||
interface SigningInPageProps extends RouteComponentProps {
|
||||
}
|
||||
|
||||
interface SigningInPageState {
|
||||
// Credential containers organized by category then type
|
||||
credentialContainers: CredContainerMap;
|
||||
toggle: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
|
||||
*/
|
||||
class SigningInPage extends React.Component<SigningInPageProps, SigningInPageState> {
|
||||
static contextType = AccountServiceContext;
|
||||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
|
||||
public constructor(props: SigningInPageProps, context: React.ContextType<typeof AccountServiceContext>) {
|
||||
super(props);
|
||||
this.context = context;
|
||||
|
||||
this.state = {
|
||||
credentialContainers: new Map(),
|
||||
toggle: false
|
||||
}
|
||||
|
||||
this.getCredentialContainers();
|
||||
}
|
||||
|
||||
private getCredentialContainers(): void {
|
||||
this.context!.doGet("/credentials")
|
||||
.then((response: HttpResponse<CredentialContainer[]>) => {
|
||||
|
||||
const allContainers: CredContainerMap = new Map();
|
||||
const containers: CredentialContainer[] = response.data || [];
|
||||
containers.forEach(container => {
|
||||
let categoryMap = allContainers.get(container.category);
|
||||
if (!categoryMap) {
|
||||
categoryMap = new Map();
|
||||
allContainers.set(container.category, categoryMap);
|
||||
}
|
||||
categoryMap.set(container.type, container);
|
||||
});
|
||||
|
||||
this.setState({credentialContainers: allContainers});
|
||||
console.log({allContainers})
|
||||
});
|
||||
}
|
||||
|
||||
private handleRemove = (credentialId: string, userLabel: string) => {
|
||||
this.context!.doDelete("/credentials/" + credentialId)
|
||||
.then(() => {
|
||||
this.getCredentialContainers();
|
||||
ContentAlert.success('successRemovedMessage', [userLabel]);
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
))
|
||||
|
||||
}</>)
|
||||
}
|
||||
|
||||
private renderTypes(credTypeMap: CredTypeMap): React.ReactNode {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
private renderEmptyRow(type: string, isLast: boolean): React.ReactNode {
|
||||
if (isLast) return; // don't put empty row at the end
|
||||
|
||||
return (
|
||||
<DataListItem aria-labelledby={'empty-list-item-' + type}>
|
||||
<DataListItemRow key={'empty-row-' + type}>
|
||||
<DataListItemCells dataListCells={[<DataListCell></DataListCell>]}/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
)
|
||||
}
|
||||
|
||||
private renderUserCredentials(credTypeMap: CredTypeMap, credType: CredType, keycloak: KeycloakService): React.ReactNode {
|
||||
const credContainer: CredentialContainer = credTypeMap.get(credType)!;
|
||||
const userCredentials: UserCredential[] = credContainer.userCredentials;
|
||||
const removeable: boolean = credContainer.removeable;
|
||||
const type: string = credContainer.type;
|
||||
const displayName: string = credContainer.displayName;
|
||||
|
||||
if (!userCredentials || userCredentials.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'>
|
||||
<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>,
|
||||
<DataListCell key={'no-credentials-cell-2'}/>
|
||||
]}/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
}
|
||||
|
||||
userCredentials.forEach(credential => {
|
||||
if (!credential.userLabel) credential.userLabel = Msg.localize(credential.type);
|
||||
if (credential.hasOwnProperty('createdDate') && credential.createdDate && credential.createdDate! > 0) {
|
||||
credential.strCreatedDate = TimeUtil.format(credential.createdDate as number);
|
||||
}
|
||||
});
|
||||
|
||||
let updateAIA: AIACommand;
|
||||
if (credContainer.updateAction) {
|
||||
updateAIA = new AIACommand(keycloak, credContainer.updateAction);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key='userCredentials'> {
|
||||
userCredentials.map(credential => (
|
||||
<DataListItem id={`${SigningInPage.credElementId(type, credential.id, 'row')}`} key={'credential-list-item-' + credential.id} aria-labelledby={'credential-list-item-' + credential.userLabel}>
|
||||
<DataListItemRow key={'userCredentialRow-' + credential.id}>
|
||||
<DataListItemCells dataListCells={this.credentialRowCells(credential, type)}/>
|
||||
|
||||
<CredentialAction credential={credential}
|
||||
removeable={removeable}
|
||||
updateAction={updateAIA}
|
||||
credRemover={this.handleRemove}/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
))
|
||||
}
|
||||
</React.Fragment>)
|
||||
}
|
||||
|
||||
private credentialRowCells(credential: UserCredential, type: string): React.ReactNode[] {
|
||||
const credRowCells: React.ReactNode[] = [];
|
||||
credRowCells.push(<DataListCell id={`${SigningInPage.credElementId(type, credential.id, 'label')}`} key={'userLabel-' + credential.id}>{credential.userLabel}</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}/>);
|
||||
}
|
||||
|
||||
return credRowCells;
|
||||
}
|
||||
|
||||
private renderCredTypeTitle(credContainer: CredentialContainer, keycloak: KeycloakService): 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);
|
||||
|
||||
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}/>}
|
||||
</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 => this.setState({ toggle: isOpen })} />}
|
||||
isOpen={this.state.toggle}
|
||||
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>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
type CredRemover = (credentialId: string, userLabel: string) => void;
|
||||
interface CredentialActionProps {credential: UserCredential;
|
||||
removeable: boolean;
|
||||
updateAction: AIACommand;
|
||||
credRemover: CredRemover;};
|
||||
class CredentialAction extends React.Component<CredentialActionProps> {
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.props.updateAction) {
|
||||
return (
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'updateAction-' + this.props.credential.id}>
|
||||
<Button id={`${SigningInPage.credElementId(this.props.credential.type, this.props.credential.id, 'update')}`} variant='primary'onClick={()=> this.props.updateAction.execute()}><Msg msgKey='update'/></Button>
|
||||
</DataListAction>
|
||||
)
|
||||
}
|
||||
|
||||
if (this.props.removeable) {
|
||||
const userLabel: string = this.props.credential.userLabel;
|
||||
return (
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'removeAction-' + this.props.credential.id }>
|
||||
<ContinueCancelModal buttonTitle='remove'
|
||||
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 (<></>)
|
||||
}
|
||||
}
|
||||
|
||||
const SigningInPageWithRouter = withRouter(SigningInPage);
|
||||
export { SigningInPageWithRouter as SigningInPage};
|
@ -0,0 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { KeycloakService } from './keycloak.service';
|
||||
|
||||
export const KeycloakContext = React.createContext<KeycloakService | undefined>(undefined);
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2017 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {KeycloakLoginOptions} from "../../../../../../../../../../adapters/oidc/js/src/main/resources/keycloak";
|
||||
|
||||
declare const baseUrl: string;
|
||||
export type KeycloakClient = Keycloak.KeycloakInstance;
|
||||
|
||||
export class KeycloakService {
|
||||
private keycloakAuth: KeycloakClient;
|
||||
|
||||
public constructor(keycloak: KeycloakClient) {
|
||||
this.keycloakAuth = keycloak;
|
||||
}
|
||||
|
||||
public authenticated(): boolean {
|
||||
return this.keycloakAuth.authenticated ? this.keycloakAuth.authenticated : false;
|
||||
}
|
||||
|
||||
public audiencePresent(): boolean {
|
||||
if (this.keycloakAuth.tokenParsed) {
|
||||
const audience = this.keycloakAuth.tokenParsed['aud'];
|
||||
return audience === 'account' || (Array.isArray(audience) && audience.indexOf('account') >= 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public login(options?: KeycloakLoginOptions): void {
|
||||
this.keycloakAuth.login(options);
|
||||
}
|
||||
|
||||
public logout(redirectUri: string = baseUrl): void {
|
||||
this.keycloakAuth.logout({redirectUri: redirectUri});
|
||||
}
|
||||
|
||||
public account(): void {
|
||||
this.keycloakAuth.accountManagement();
|
||||
}
|
||||
|
||||
public authServerUrl(): string | undefined {
|
||||
const authServerUrl = this.keycloakAuth.authServerUrl;
|
||||
return authServerUrl!.charAt(authServerUrl!.length - 1) === '/' ? authServerUrl : authServerUrl + '/';
|
||||
}
|
||||
|
||||
public realm(): string | undefined {
|
||||
return this.keycloakAuth.realm;
|
||||
}
|
||||
|
||||
public getToken(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (this.keycloakAuth.token) {
|
||||
this.keycloakAuth
|
||||
.updateToken(5)
|
||||
.success(() => {
|
||||
resolve(this.keycloakAuth.token as string);
|
||||
})
|
||||
.error(() => {
|
||||
reject('Failed to refresh token');
|
||||
});
|
||||
} else {
|
||||
reject('Not logged in');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
32
keycloak.v2/account/src/app/util/AIACommand.ts
Normal file
32
keycloak.v2/account/src/app/util/AIACommand.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {KeycloakService} from '../keycloak-service/keycloak.service';
|
||||
|
||||
/**
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
export class AIACommand {
|
||||
|
||||
constructor(private keycloak: KeycloakService, private action: string) {}
|
||||
|
||||
public execute(): void {
|
||||
this.keycloak.login({
|
||||
action: this.action,
|
||||
})
|
||||
|
||||
}
|
||||
}
|
22
keycloak.v2/account/src/app/util/ParseLink.ts
Normal file
22
keycloak.v2/account/src/app/util/ParseLink.ts
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
export interface Links {
|
||||
prev?: string;
|
||||
next?: string;
|
||||
}
|
||||
|
||||
function parse(linkHeader: string | undefined): Links {
|
||||
if (!linkHeader) return {};
|
||||
const links = linkHeader.split(/,\s*</);
|
||||
return links.reduce<Links>((acc: Links, link: string): Links => {
|
||||
const matcher = link.match(/<?([^>]*)>(.*)/);
|
||||
if (!matcher) return {};
|
||||
const linkUrl = matcher[1];
|
||||
const rel = matcher[2].match(/\s*(.+)\s*=\s*"?([^"]+)"?/);
|
||||
if (rel) {
|
||||
acc[rel[2]] = linkUrl;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export default parse;
|
42
keycloak.v2/account/src/app/util/RedirectUri.ts
Normal file
42
keycloak.v2/account/src/app/util/RedirectUri.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare const baseUrl: string;
|
||||
declare const referrer: string;
|
||||
declare const referrerUri: string;
|
||||
|
||||
/**
|
||||
* Create a redirect uri that can return to this application with referrer and referrer_uri intact.
|
||||
*
|
||||
* @param currentLocation The ReactRouter location to return to.
|
||||
*
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
export const createRedirect = (currentLocation: string): string => {
|
||||
let redirectUri: string = baseUrl;
|
||||
|
||||
if (typeof referrer !== 'undefined') {
|
||||
// '_hash_' is a workaround for when uri encoding is not
|
||||
// sufficient to escape the # character properly.
|
||||
// The problem is that both the redirect and the application URL contain a hash.
|
||||
// The browser will consider anything after the first hash to be client-side. So
|
||||
// it sees the hash in the redirect param and stops.
|
||||
redirectUri += "?referrer=" + referrer + "&referrer_uri=" + referrerUri.replace('#', '_hash_');
|
||||
}
|
||||
|
||||
return encodeURIComponent(redirectUri) + encodeURIComponent("/#" + currentLocation);
|
||||
}
|
41
keycloak.v2/account/src/app/util/TimeUtil.ts
Normal file
41
keycloak.v2/account/src/app/util/TimeUtil.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare const locale: string;
|
||||
|
||||
/**
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
class TimeUtil {
|
||||
private options = { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
|
||||
private formatter: Intl.DateTimeFormat;
|
||||
|
||||
constructor() {
|
||||
try {
|
||||
this.formatter = new Intl.DateTimeFormat(locale, this.options);
|
||||
} catch(e) {
|
||||
// unknown locale falling back to English
|
||||
this.formatter = new Intl.DateTimeFormat('en', this.options);
|
||||
}
|
||||
}
|
||||
|
||||
format(time: number): string {
|
||||
return this.formatter.format(time);
|
||||
}
|
||||
}
|
||||
|
||||
const TimeUtilInstance: TimeUtil = new TimeUtil();
|
||||
export default TimeUtilInstance as TimeUtil;
|
108
keycloak.v2/account/src/app/widgets/ContinueCancelModal.tsx
Normal file
108
keycloak.v2/account/src/app/widgets/ContinueCancelModal.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Modal, Button, ButtonProps } from '@patternfly/react-core';
|
||||
import {Msg} from './Msg';
|
||||
|
||||
/**
|
||||
* For any of these properties that are strings, you can
|
||||
* pass in a localization key instead of a static string.
|
||||
*/
|
||||
interface ContinueCancelModalProps {
|
||||
buttonTitle?: string;
|
||||
buttonVariant?: ButtonProps['variant'];
|
||||
buttonId?: string;
|
||||
render?(toggle: () => void): React.ReactNode;
|
||||
modalTitle: string;
|
||||
modalMessage?: string;
|
||||
modalContinueButtonLabel?: string;
|
||||
modalCancelButtonLabel?: string;
|
||||
onContinue: () => void;
|
||||
onClose?: () => void;
|
||||
isDisabled?: boolean;
|
||||
isLarge?: boolean;
|
||||
}
|
||||
|
||||
interface ContinueCancelModalState {
|
||||
isModalOpen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class renders a button that provides a continue/cancel modal dialog when clicked. If the user selects 'Continue'
|
||||
* then the onContinue function is executed.
|
||||
*
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2019 Red Hat Inc.
|
||||
*/
|
||||
export class ContinueCancelModal extends React.Component<ContinueCancelModalProps, ContinueCancelModalState> {
|
||||
protected static defaultProps = {
|
||||
buttonVariant: 'primary',
|
||||
modalContinueButtonLabel: 'continue',
|
||||
modalCancelButtonLabel: 'doCancel',
|
||||
isDisabled: false,
|
||||
isSmall: true
|
||||
};
|
||||
|
||||
public constructor(props: ContinueCancelModalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
private handleModalToggle = () => {
|
||||
this.setState(({ isModalOpen }) => ({
|
||||
isModalOpen: !isModalOpen
|
||||
}));
|
||||
if (this.props.onClose) this.props.onClose();
|
||||
};
|
||||
|
||||
private handleContinue = () => {
|
||||
this.handleModalToggle();
|
||||
this.props.onContinue();
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const { isModalOpen } = this.state;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!this.props.render &&
|
||||
<Button id={this.props.buttonId} variant={this.props.buttonVariant} onClick={this.handleModalToggle} isDisabled={this.props.isDisabled}>
|
||||
<Msg msgKey={this.props.buttonTitle!}/>
|
||||
</Button>}
|
||||
{this.props.render && this.props.render(this.handleModalToggle)}
|
||||
<Modal
|
||||
{...this.props}
|
||||
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>
|
||||
]}
|
||||
>
|
||||
{ !this.props.modalMessage && this.props.children}
|
||||
{ this.props.modalMessage && <Msg msgKey={this.props.modalMessage}/>}
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
53
keycloak.v2/account/src/app/widgets/EmptyMessageState.tsx
Normal file
53
keycloak.v2/account/src/app/widgets/EmptyMessageState.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateVariant,
|
||||
Title,
|
||||
EmptyStateIcon,
|
||||
TitleLevel,
|
||||
EmptyStateBody,
|
||||
IconProps,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
import { Msg } from './Msg';
|
||||
|
||||
export interface EmptyMessageStateProps {
|
||||
icon: React.FunctionComponent<IconProps>;
|
||||
messageKey: string;
|
||||
}
|
||||
|
||||
export default class EmptyMessageState extends React.Component<EmptyMessageStateProps, {}> {
|
||||
constructor(props: EmptyMessageStateProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EmptyState variant={EmptyStateVariant.full}>
|
||||
<EmptyStateIcon icon={this.props.icon} />
|
||||
<Title headingLevel={TitleLevel.h5} size="lg">
|
||||
<Msg msgKey={this.props.messageKey} />
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{this.props.children}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
}
|
57
keycloak.v2/account/src/app/widgets/LocaleSelectors.tsx
Normal file
57
keycloak.v2/account/src/app/widgets/LocaleSelectors.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
FormSelect,
|
||||
FormSelectOption,
|
||||
FormSelectProps
|
||||
} from '@patternfly/react-core';
|
||||
import { Msg } from './Msg';
|
||||
|
||||
interface AvailableLocale {
|
||||
locale: string;
|
||||
label: string;
|
||||
};
|
||||
declare const availableLocales: [AvailableLocale];
|
||||
|
||||
interface LocaleSelectorProps extends Omit<FormSelectProps, 'children'> { }
|
||||
interface LocaleSelectorState { }
|
||||
export class LocaleSelector extends React.Component<LocaleSelectorProps, LocaleSelectorState> {
|
||||
|
||||
constructor(props: LocaleSelectorProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<FormSelect
|
||||
id="locale-select"
|
||||
value={this.props.value}
|
||||
onChange={(value, event) => { if (this.props.onChange) this.props.onChange(value, event) }}
|
||||
aria-label={Msg.localize('selectLocale')}
|
||||
>
|
||||
{availableLocales.map((locale, index) =>
|
||||
<FormSelectOption
|
||||
key={index}
|
||||
value={locale.locale}
|
||||
label={locale.label}
|
||||
/>)
|
||||
}
|
||||
</FormSelect>
|
||||
);
|
||||
}
|
||||
}
|
56
keycloak.v2/account/src/app/widgets/Logout.tsx
Normal file
56
keycloak.v2/account/src/app/widgets/Logout.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {Msg} from './Msg';
|
||||
import {KeycloakService} from '../keycloak-service/keycloak.service';
|
||||
import { KeycloakContext } from '../keycloak-service/KeycloakContext';
|
||||
|
||||
import {Button, DropdownItem} from '@patternfly/react-core';
|
||||
|
||||
function handleLogout(keycloak: KeycloakService): void {
|
||||
keycloak.logout();
|
||||
}
|
||||
|
||||
interface LogoutProps {}
|
||||
export class LogoutButton extends React.Component<LogoutProps> {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<KeycloakContext.Consumer>
|
||||
{ keycloak => (
|
||||
<Button id="signOutButton" onClick={() => handleLogout(keycloak!)}><Msg msgKey="doSignOut"/></Button>
|
||||
)}
|
||||
</KeycloakContext.Consumer>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface LogoutDropdownItemProps {}
|
||||
export class LogoutDropdownItem extends React.Component<LogoutDropdownItemProps> {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<KeycloakContext.Consumer>
|
||||
{ keycloak => (
|
||||
<DropdownItem id="signOutLink" key="logout" onClick={() => handleLogout(keycloak!)}>
|
||||
{Msg.localize('doSignOut')}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</KeycloakContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
84
keycloak.v2/account/src/app/widgets/Msg.tsx
Normal file
84
keycloak.v2/account/src/app/widgets/Msg.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
declare const l18nMsg: {[key: string]: string};
|
||||
|
||||
export interface MsgProps {
|
||||
readonly msgKey: string;
|
||||
readonly params?: string[];
|
||||
}
|
||||
|
||||
export class Msg extends React.Component<MsgProps> {
|
||||
|
||||
public constructor(props: MsgProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.props.children) {
|
||||
return Msg.localizeWithChildren(this.props.msgKey, this.props.children);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>{Msg.localize(this.props.msgKey, this.props.params)}</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private static localizeWithChildren(msgKey: string, children: React.ReactNode): React.ReactNode {
|
||||
const message: string = l18nMsg[this.processKey(msgKey)];
|
||||
const parts = message.split(/\{\{param_\d*}}/);
|
||||
const count = React.Children.count(children);
|
||||
return React.Children.map(children, (child, i) =>
|
||||
[parts[i], child, count === i + 1 ? parts[count] : '']
|
||||
);
|
||||
}
|
||||
|
||||
public static localize(msgKey: string, params?: string[]): string {
|
||||
let message: string = l18nMsg[this.processKey(msgKey)];
|
||||
if (message === undefined) message = msgKey;
|
||||
|
||||
if ((params !== undefined) && (params.length > 0)) {
|
||||
params.forEach((value: string, index: number) => {
|
||||
value = this.processParam(value);
|
||||
message = message.replace('{{param_'+ index + '}}', value);
|
||||
})
|
||||
}
|
||||
|
||||
return unescape(message);
|
||||
}
|
||||
|
||||
// if the message key has Freemarker syntax, remove it
|
||||
private static processKey(msgKey: string): string {
|
||||
if (!(msgKey.startsWith('${') && msgKey.endsWith('}'))) return msgKey;
|
||||
|
||||
// remove Freemarker syntax
|
||||
return msgKey.substring(2, msgKey.length - 1);
|
||||
}
|
||||
|
||||
// if the param has Freemarker syntax, try to look up its value
|
||||
private static processParam(param: string): string {
|
||||
if (!(param.startsWith('${') && param.endsWith('}'))) return param;
|
||||
|
||||
// remove Freemarker syntax
|
||||
const key: string = param.substring(2, param.length - 1);
|
||||
|
||||
let value: string = l18nMsg[key];
|
||||
if (value === undefined) return param;
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
47
keycloak.v2/account/src/app/widgets/ReferrerDropdownItem.tsx
Normal file
47
keycloak.v2/account/src/app/widgets/ReferrerDropdownItem.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {Msg} from '../widgets/Msg';
|
||||
|
||||
import {DropdownItem} from '@patternfly/react-core';
|
||||
import {ArrowIcon} from '@patternfly/react-icons';
|
||||
|
||||
declare const referrerName: string;
|
||||
declare const referrerUri: string;
|
||||
|
||||
export interface ReferrerDropdownItemProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
|
||||
*/
|
||||
export class ReferrerDropdownItem extends React.Component<ReferrerDropdownItemProps> {
|
||||
|
||||
public constructor(props: ReferrerDropdownItemProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
|
||||
return (
|
||||
<DropdownItem id="referrerMobileLink" href={referrerUri}>
|
||||
<ArrowIcon /> {Msg.localize('backTo', [referrerName])}
|
||||
</DropdownItem>
|
||||
);
|
||||
}
|
||||
};
|
48
keycloak.v2/account/src/app/widgets/ReferrerLink.tsx
Normal file
48
keycloak.v2/account/src/app/widgets/ReferrerLink.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {Msg} from './Msg';
|
||||
|
||||
import {ArrowIcon} from '@patternfly/react-icons';
|
||||
|
||||
declare const referrerName: string;
|
||||
declare const referrerUri: string;
|
||||
|
||||
export interface ReferrerLinkProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
|
||||
*/
|
||||
export class ReferrerLink extends React.Component<ReferrerLinkProps> {
|
||||
|
||||
public constructor(props: ReferrerLinkProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
// '_hash_' is a workaround for when uri encoding is not
|
||||
// sufficient to escape the # character properly.
|
||||
// See AppInitiatedActionPage for more details.
|
||||
<a id="referrerLink" href={referrerUri.replace('_hash_', '#')}>
|
||||
<ArrowIcon/> <Msg msgKey="backTo" params={[referrerName]}/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
29
keycloak.v2/account/src/app/widgets/features.ts
Normal file
29
keycloak.v2/account/src/app/widgets/features.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface Features {
|
||||
isRegistrationEmailAsUsername: boolean;
|
||||
isEditUserNameAllowed: boolean;
|
||||
isInternationalizationEnabled: boolean;
|
||||
isLinkedAccountsEnabled: boolean;
|
||||
isEventsEnabled: boolean;
|
||||
isMyResourcesEnabled: boolean;
|
||||
isTotpConfigured: boolean;
|
||||
deleteAccountAllowed: boolean;
|
||||
}
|
||||
|
||||
|
7
keycloak.v2/account/src/eslint.cmd
Normal file
7
keycloak.v2/account/src/eslint.cmd
Normal file
@ -0,0 +1,7 @@
|
||||
@IF EXIST "%~dp0\node.exe" (
|
||||
"%~dp0\node.exe" "%~dp0\node_modules\eslint\bin\eslint.js" %*
|
||||
) ELSE (
|
||||
@SETLOCAL
|
||||
@SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
node "%~dp0\node_modules\eslint\bin\eslint.js" %*
|
||||
)
|
56
keycloak.v2/account/src/package.json
Normal file
56
keycloak.v2/account/src/package.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "keycloak.v2",
|
||||
"version": "1.0.0",
|
||||
"description": "keycloak account management written in React",
|
||||
"scripts": {
|
||||
"build": "snowpack --optimize && npm run check-types && npm run babel && npm run move-web_modules && npm run copy-pf-resources",
|
||||
"babel": "babel --source-maps --extensions \".js,.ts,.tsx\" app/ --out-dir ../resources/",
|
||||
"babel:watch": "npm run babel -- --watch",
|
||||
"check-types": "tsc --noImplicitAny --strictNullChecks --jsx react -p ./",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"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/core": "^7.8.7",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.7",
|
||||
"@babel/preset-react": "^7.8.3",
|
||||
"@babel/preset-typescript": "^7.8.3",
|
||||
"@types/node": "^13.9.8",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^1.4.2",
|
||||
"@typescript-eslint/parser": "^1.4.2",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"eslint": "^5.15.1",
|
||||
"eslint-config-react-app": "^3.0.8",
|
||||
"eslint-plugin-flowtype": "^2.50.3",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.1",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"rollup-plugin-postcss": "^2.5.0",
|
||||
"shx": "^0.3.2",
|
||||
"snowpack": "^1.7.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"repository": {}
|
||||
}
|
11
keycloak.v2/account/src/snowpack.config.js
Normal file
11
keycloak.v2/account/src/snowpack.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
const postcss = require('rollup-plugin-postcss');
|
||||
|
||||
module.exports = {
|
||||
rollup: {
|
||||
plugins: [
|
||||
postcss({
|
||||
extract: 'app.css'
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
22
keycloak.v2/account/src/tsconfig.json
Normal file
22
keycloak.v2/account/src/tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"noEmit": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": [ "es2015", "dom", "dom.iterable" ],
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"jsx": "react",
|
||||
"suppressImplicitAnyIndexErrors": true
|
||||
},
|
||||
"include": [
|
||||
"./app/**/*.ts?"
|
||||
],
|
||||
"files": [
|
||||
"../../../../../../../../adapters/oidc/js/src/main/resources/keycloak.d.ts"
|
||||
]
|
||||
}
|
16
keycloak.v2/account/theme.properties
Normal file
16
keycloak.v2/account/theme.properties
Normal file
@ -0,0 +1,16 @@
|
||||
parent=base
|
||||
deprecatedMode=false
|
||||
scripts=welcome-page-scripts.js
|
||||
developmentMode=false
|
||||
|
||||
# This is the logo in upper lefthand corner.
|
||||
# It must be a relative path.
|
||||
logo=/public/logo.svg
|
||||
|
||||
# This is the link followed when clicking on the logo.
|
||||
# It can be any valid URL, including an external site.
|
||||
logoUrl=./
|
||||
|
||||
# This is the icon for the account console.
|
||||
# It must be a relative path.
|
||||
favIcon=/public/favicon.ico
|
Reference in New Issue
Block a user