Remove Account Console version 2 (#27510)
Closes #19664 Signed-off-by: Jon Koops <jonkoops@gmail.com>
1
.github/CODEOWNERS
vendored
|
@ -43,5 +43,4 @@
|
||||||
/js/ @keycloak/ui-maintainers
|
/js/ @keycloak/ui-maintainers
|
||||||
/adapters/oidc/js/ @keycloak/ui-maintainers
|
/adapters/oidc/js/ @keycloak/ui-maintainers
|
||||||
/rest/admin-ui-ext/ @keycloak/ui-maintainers
|
/rest/admin-ui-ext/ @keycloak/ui-maintainers
|
||||||
/themes/src/main/resources/theme/keycloak.v2/account/ @keycloak/ui-maintainers
|
|
||||||
/testsuite/integration-arquillian/tests/other/base-ui/ @keycloak/ui-maintainers
|
/testsuite/integration-arquillian/tests/other/base-ui/ @keycloak/ui-maintainers
|
||||||
|
|
37
.github/workflows/ci.yml
vendored
|
@ -578,42 +578,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
job-id: fips-integration-tests-${{ matrix.mode }}
|
job-id: fips-integration-tests-${{ matrix.mode }}
|
||||||
|
|
||||||
account-console-integration-tests:
|
|
||||||
name: Account Console IT
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
timeout-minutes: 75
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
browser: [chrome]
|
|
||||||
fail-fast: false
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- id: integration-test-setup
|
|
||||||
name: Integration test setup
|
|
||||||
uses: ./.github/actions/integration-test-setup
|
|
||||||
|
|
||||||
- name: Run Account Console IT
|
|
||||||
run: ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=**.account2.**,!SigningInTest#passwordlessWebAuthnTest,!SigningInTest#twoFactorWebAuthnTest -Dbrowser=${{ matrix.browser }} "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -f testsuite/integration-arquillian/tests/other/base-ui/pom.xml 2>&1 | misc/log/trimmer.sh
|
|
||||||
|
|
||||||
- name: Upload JVM Heapdumps
|
|
||||||
if: always()
|
|
||||||
uses: ./.github/actions/upload-heapdumps
|
|
||||||
|
|
||||||
- uses: ./.github/actions/upload-flaky-tests
|
|
||||||
name: Upload flaky tests
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
with:
|
|
||||||
job-name: Account Console IT
|
|
||||||
|
|
||||||
- name: Surefire reports
|
|
||||||
if: always()
|
|
||||||
uses: ./.github/actions/archive-surefire-reports
|
|
||||||
with:
|
|
||||||
job-id: account-console-integration-tests-${{ matrix.browser }}
|
|
||||||
|
|
||||||
forms-integration-tests:
|
forms-integration-tests:
|
||||||
name: Forms IT
|
name: Forms IT
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -797,7 +761,6 @@ jobs:
|
||||||
- clustering-integration-tests
|
- clustering-integration-tests
|
||||||
- fips-unit-tests
|
- fips-unit-tests
|
||||||
- fips-integration-tests
|
- fips-integration-tests
|
||||||
- account-console-integration-tests
|
|
||||||
- forms-integration-tests
|
- forms-integration-tests
|
||||||
- webauthn-integration-tests
|
- webauthn-integration-tests
|
||||||
- sssd-unit-tests
|
- sssd-unit-tests
|
||||||
|
|
|
@ -50,8 +50,6 @@ public class Profile {
|
||||||
|
|
||||||
ACCOUNT_API("Account Management REST API", Type.DEFAULT),
|
ACCOUNT_API("Account Management REST API", Type.DEFAULT),
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
ACCOUNT2("Account Console version 2", Type.DEPRECATED, Feature.ACCOUNT_API),
|
|
||||||
ACCOUNT3("Account Console version 3", Type.DEFAULT, Feature.ACCOUNT_API),
|
ACCOUNT3("Account Console version 3", Type.DEFAULT, Feature.ACCOUNT_API),
|
||||||
|
|
||||||
ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW),
|
ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW),
|
||||||
|
|
|
@ -27,7 +27,7 @@ public class ProfileTest {
|
||||||
private static final Profile.Feature DISABLED_BY_DEFAULT_FEATURE = Profile.Feature.DOCKER;
|
private static final Profile.Feature DISABLED_BY_DEFAULT_FEATURE = Profile.Feature.DOCKER;
|
||||||
private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ;
|
private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ;
|
||||||
private static final Profile.Feature EXPERIMENTAL_FEATURE = Profile.Feature.DYNAMIC_SCOPES;
|
private static final Profile.Feature EXPERIMENTAL_FEATURE = Profile.Feature.DYNAMIC_SCOPES;
|
||||||
private static Profile.Feature DEPRECATED_FEATURE = Profile.Feature.ACCOUNT2;
|
private static Profile.Feature DEPRECATED_FEATURE = Profile.Feature.LINKEDIN_OAUTH;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
@ -94,7 +94,7 @@ public class ProfileTest {
|
||||||
properties.setProperty("keycloak.profile.feature.account3", "disabled");
|
properties.setProperty("keycloak.profile.feature.account3", "disabled");
|
||||||
properties.setProperty("keycloak.profile.feature.account_api", "disabled");
|
properties.setProperty("keycloak.profile.feature.account_api", "disabled");
|
||||||
Profile.configure(new PropertiesProfileConfigResolver(properties));
|
Profile.configure(new PropertiesProfileConfigResolver(properties));
|
||||||
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2));
|
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.ACCOUNT3));
|
||||||
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.ACCOUNT_API));
|
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.ACCOUNT_API));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,9 +149,9 @@ public class ProfileTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testKeys() {
|
public void testKeys() {
|
||||||
Assert.assertEquals("account2", Profile.Feature.ACCOUNT2.getKey());
|
Assert.assertEquals("account3", Profile.Feature.ACCOUNT3.getKey());
|
||||||
Assert.assertEquals("account2", Profile.Feature.ACCOUNT2.getUnversionedKey());
|
Assert.assertEquals("account3", Profile.Feature.ACCOUNT3.getUnversionedKey());
|
||||||
Assert.assertEquals("account2:v1", Profile.Feature.ACCOUNT2.getVersionedKey());
|
Assert.assertEquals("account3:v1", Profile.Feature.ACCOUNT3.getVersionedKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -49,7 +49,7 @@ public class FeaturePropertyMappersTest {
|
||||||
@Test
|
@Test
|
||||||
public void testValidFeatures() {
|
public void testValidFeatures() {
|
||||||
FeaturePropertyMappers.validateEnabledFeature("preview");
|
FeaturePropertyMappers.validateEnabledFeature("preview");
|
||||||
FeaturePropertyMappers.validateEnabledFeature(Feature.ACCOUNT2.getVersionedKey());
|
FeaturePropertyMappers.validateEnabledFeature(Feature.ACCOUNT3.getVersionedKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,14 +47,14 @@ public class OptionValidationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Launch({"build", "--features", "account2", "account3"})
|
@Launch({"build", "--features", "linkedin-oauth", "account3"})
|
||||||
public void failMultipleMultiOptionValue(LaunchResult result) {
|
public void failMultipleMultiOptionValue(LaunchResult result) {
|
||||||
CLIResult cliResult = (CLIResult) result;
|
CLIResult cliResult = (CLIResult) result;
|
||||||
assertThat(cliResult.getErrorOutput(), containsString("Option '--features' (feature) expects one or more comma separated values without whitespace. Expected values are: "));
|
assertThat(cliResult.getErrorOutput(), containsString("Option '--features' (feature) expects one or more comma separated values without whitespace. Expected values are: "));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Launch({"build", "--features", "xyz,account2"})
|
@Launch({"build", "--features", "xyz,account3"})
|
||||||
public void failInvalidMultiOptionValue(LaunchResult result) {
|
public void failInvalidMultiOptionValue(LaunchResult result) {
|
||||||
CLIResult cliResult = (CLIResult) result;
|
CLIResult cliResult = (CLIResult) result;
|
||||||
assertThat(cliResult.getErrorOutput(), containsString("xyz is an unrecognized feature, it should be one of"));
|
assertThat(cliResult.getErrorOutput(), containsString("xyz is an unrecognized feature, it should be one of"));
|
||||||
|
|
|
@ -48,10 +48,6 @@ public interface ThemeSelectorProvider extends Provider {
|
||||||
return DEFAULT_V3;
|
return DEFAULT_V3;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((type == Theme.Type.ACCOUNT) && Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2)) {
|
|
||||||
return DEFAULT_V2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((type == Theme.Type.ADMIN) && Profile.isFeatureEnabled(Profile.Feature.ADMIN2)) {
|
if ((type == Theme.Type.ADMIN) && Profile.isFeatureEnabled(Profile.Feature.ADMIN2)) {
|
||||||
return DEFAULT_V2;
|
return DEFAULT_V2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,15 +227,12 @@ public class ServerInfoAdminResource {
|
||||||
|
|
||||||
private LinkedList<String> filterThemes(Theme.Type type, LinkedList<String> themeNames) {
|
private LinkedList<String> filterThemes(Theme.Type type, LinkedList<String> themeNames) {
|
||||||
LinkedList<String> filteredNames = new LinkedList<>(themeNames);
|
LinkedList<String> filteredNames = new LinkedList<>(themeNames);
|
||||||
|
|
||||||
boolean filterAccountV2 = (type == Theme.Type.ACCOUNT) &&
|
|
||||||
!Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2);
|
|
||||||
boolean filterAdminV2 = (type == Theme.Type.ADMIN) &&
|
boolean filterAdminV2 = (type == Theme.Type.ADMIN) &&
|
||||||
!Profile.isFeatureEnabled(Profile.Feature.ADMIN2);
|
!Profile.isFeatureEnabled(Profile.Feature.ADMIN2);
|
||||||
boolean filterLoginV2 = (type == Theme.Type.LOGIN) &&
|
boolean filterLoginV2 = (type == Theme.Type.LOGIN) &&
|
||||||
!Profile.isFeatureEnabled(Profile.Feature.LOGIN2);
|
!Profile.isFeatureEnabled(Profile.Feature.LOGIN2);
|
||||||
|
|
||||||
if (filterAccountV2 || filterAdminV2 || filterLoginV2) {
|
if (filterAdminV2 || filterLoginV2) {
|
||||||
filteredNames.remove("keycloak.v2");
|
filteredNames.remove("keycloak.v2");
|
||||||
filteredNames.remove("rh-sso.v2");
|
filteredNames.remove("rh-sso.v2");
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
package org.keycloak.testsuite.util;
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
import org.jboss.arquillian.graphene.wait.ElementBuilder;
|
import org.jboss.arquillian.graphene.wait.ElementBuilder;
|
||||||
import org.keycloak.common.Profile;
|
|
||||||
import org.keycloak.testsuite.ProfileAssume;
|
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.TimeoutException;
|
import org.openqa.selenium.TimeoutException;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
@ -147,12 +145,6 @@ public final class WaitUtils {
|
||||||
log.warn("URL seems unstable! (Some redirect are probably still in progress)");
|
log.warn("URL seems unstable! (Some redirect are probably still in progress)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
ProfileAssume.isFeatureEnabled(Profile.Feature.ACCOUNT2) && currentUrl.matches("^[^\\/]+:\\/\\/[^\\/]+\\/auth\\/realms\\/[^\\/]+\\/account\\/.*$") // check for new Account Console URL
|
|
||||||
) {
|
|
||||||
pause(2000); // TODO rework this temporary workaround once KEYCLOAK-11201 and/or KEYCLOAK-8181 are fixed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void waitForModalFadeIn() {
|
public static void waitForModalFadeIn() {
|
||||||
|
|
|
@ -16,39 +16,12 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.adapter.example.fuse;
|
package org.keycloak.testsuite.adapter.example.fuse;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.*;
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
|
|
||||||
import static org.keycloak.testsuite.utils.fuse.FuseUtils.assertCommand;
|
|
||||||
import static org.keycloak.testsuite.utils.fuse.FuseUtils.getCommandOutput;
|
|
||||||
import static org.keycloak.testsuite.utils.io.IOUtil.loadRealm;
|
|
||||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
|
||||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import javax.management.InstanceNotFoundException;
|
|
||||||
import javax.management.MBeanException;
|
|
||||||
import javax.management.MBeanServerConnection;
|
|
||||||
import javax.management.ObjectName;
|
|
||||||
import javax.management.ReflectionException;
|
|
||||||
import javax.management.remote.JMXConnector;
|
|
||||||
import javax.management.remote.JMXConnectorFactory;
|
|
||||||
import javax.management.remote.JMXServiceURL;
|
|
||||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Assume;
|
import org.junit.Assume;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.common.Profile;
|
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
|
import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
|
||||||
|
@ -59,21 +32,49 @@ import org.keycloak.testsuite.adapter.page.fuse.CustomerListing;
|
||||||
import org.keycloak.testsuite.adapter.page.fuse.CustomerPortalFuseExample;
|
import org.keycloak.testsuite.adapter.page.fuse.CustomerPortalFuseExample;
|
||||||
import org.keycloak.testsuite.adapter.page.fuse.ProductPortalFuseExample;
|
import org.keycloak.testsuite.adapter.page.fuse.ProductPortalFuseExample;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
|
||||||
import org.keycloak.testsuite.auth.page.AuthRealm;
|
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||||
import org.keycloak.testsuite.pages.LogoutConfirmPage;
|
|
||||||
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
|
|
||||||
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
|
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
|
||||||
|
import org.keycloak.testsuite.pages.LogoutConfirmPage;
|
||||||
import org.keycloak.testsuite.util.DroneUtils;
|
import org.keycloak.testsuite.util.DroneUtils;
|
||||||
import org.keycloak.testsuite.util.JavascriptBrowser;
|
import org.keycloak.testsuite.util.JavascriptBrowser;
|
||||||
import org.keycloak.testsuite.util.WaitUtils;
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
|
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
|
||||||
import org.keycloak.testsuite.utils.fuse.FuseUtils.Result;
|
import org.keycloak.testsuite.utils.fuse.FuseUtils.Result;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
|
import javax.management.InstanceNotFoundException;
|
||||||
|
import javax.management.MBeanException;
|
||||||
|
import javax.management.MBeanServerConnection;
|
||||||
|
import javax.management.ObjectName;
|
||||||
|
import javax.management.ReflectionException;
|
||||||
|
import javax.management.remote.JMXConnector;
|
||||||
|
import javax.management.remote.JMXConnectorFactory;
|
||||||
|
import javax.management.remote.JMXServiceURL;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.allOf;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
|
||||||
|
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
||||||
|
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
|
||||||
|
import static org.keycloak.testsuite.utils.fuse.FuseUtils.assertCommand;
|
||||||
|
import static org.keycloak.testsuite.utils.fuse.FuseUtils.getCommandOutput;
|
||||||
|
import static org.keycloak.testsuite.utils.io.IOUtil.loadRealm;
|
||||||
|
|
||||||
@AppServerContainer(ContainerConstants.APP_SERVER_FUSE63)
|
@AppServerContainer(ContainerConstants.APP_SERVER_FUSE63)
|
||||||
@AppServerContainer(ContainerConstants.APP_SERVER_FUSE7X)
|
@AppServerContainer(ContainerConstants.APP_SERVER_FUSE7X)
|
||||||
@DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228)
|
|
||||||
public class FuseAdapterTest extends AbstractExampleAdapterTest {
|
public class FuseAdapterTest extends AbstractExampleAdapterTest {
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,6 @@ import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLo
|
||||||
/**
|
/**
|
||||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||||
*/
|
*/
|
||||||
@EnableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true)
|
|
||||||
public abstract class AbstractAccountTest extends AbstractUiTest {
|
public abstract class AbstractAccountTest extends AbstractUiTest {
|
||||||
public static final String ACCOUNT_THEME_NAME_KC = "keycloak.v2";
|
public static final String ACCOUNT_THEME_NAME_KC = "keycloak.v2";
|
||||||
public static final DateTimeFormatter DEFAULT_TIME_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy 'at' h:mm a", Locale.ENGLISH);
|
public static final DateTimeFormatter DEFAULT_TIME_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy 'at' h:mm a", Locale.ENGLISH);
|
||||||
|
|
|
@ -142,12 +142,6 @@
|
||||||
<module>sssd</module>
|
<module>sssd</module>
|
||||||
</modules>
|
</modules>
|
||||||
</profile>
|
</profile>
|
||||||
<profile>
|
|
||||||
<id>base-ui</id>
|
|
||||||
<modules>
|
|
||||||
<module>base-ui</module>
|
|
||||||
</modules>
|
|
||||||
</profile>
|
|
||||||
<profile>
|
<profile>
|
||||||
<id>springboot</id>
|
<id>springboot</id>
|
||||||
<modules>
|
<modules>
|
||||||
|
|
|
@ -11,15 +11,6 @@ git add package.json pnpm-lock.yaml
|
||||||
cd -
|
cd -
|
||||||
```
|
```
|
||||||
|
|
||||||
## For account console v2
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd src/main/resources/theme/keycloak.v2/account/src
|
|
||||||
pnpm update --latest --interactive
|
|
||||||
git add package.json pnpm-lock.yaml
|
|
||||||
cd -
|
|
||||||
```
|
|
||||||
|
|
||||||
## License Information
|
## License Information
|
||||||
|
|
||||||
Make sure to enter license information for new dependencies, as specified in `docs/dependency-license-information.md`. Javascript dependencies are included as `other` elements.
|
Make sure to enter license information for new dependencies, as specified in `docs/dependency-license-information.md`. Javascript dependencies are included as `other` elements.
|
||||||
|
|
|
@ -14,20 +14,11 @@
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<dir.common>src/main/resources/theme/keycloak/common/resources</dir.common>
|
<dir.common>src/main/resources/theme/keycloak/common/resources</dir.common>
|
||||||
<dir.account2>src/main/resources/theme/keycloak.v2/account/src</dir.account2>
|
|
||||||
<!-- ignore folders for incremental builds by the maven build cache plugin -->
|
<!-- ignore folders for incremental builds by the maven build cache plugin -->
|
||||||
<maven.build.cache.exclude.value.1>src/main/resources/theme/keycloak/common/resources/node_modules</maven.build.cache.exclude.value.1>
|
<maven.build.cache.exclude.value.1>src/main/resources/theme/keycloak/common/resources/node_modules</maven.build.cache.exclude.value.1>
|
||||||
<maven.build.cache.exclude.value.2>src/main/resources/theme/keycloak/common/resources/vendor</maven.build.cache.exclude.value.2>
|
<maven.build.cache.exclude.value.2>src/main/resources/theme/keycloak/common/resources/vendor</maven.build.cache.exclude.value.2>
|
||||||
<maven.build.cache.exclude.value.3>src/main/resources/theme/keycloak.v2/account/src/node_modules</maven.build.cache.exclude.value.3>
|
<maven.build.cache.exclude.value.3>src/main/resources/theme/keycloak.v2/welcome/node_modules</maven.build.cache.exclude.value.3>
|
||||||
<maven.build.cache.exclude.value.4>src/main/resources/theme/keycloak.v2/account/src/web_modules</maven.build.cache.exclude.value.4>
|
<maven.build.cache.exclude.value.4>src/main/resources/theme/keycloak.v2/welcome/resources/vendor</maven.build.cache.exclude.value.4>
|
||||||
<maven.build.cache.exclude.value.6>src/main/resources/theme/keycloak.v2/account/resources</maven.build.cache.exclude.value.6>
|
|
||||||
<maven.build.cache.exclude.glob.6>*.js</maven.build.cache.exclude.glob.6>
|
|
||||||
<maven.build.cache.exclude.value.7>src/main/resources/theme/keycloak.v2/account/resources</maven.build.cache.exclude.value.7>
|
|
||||||
<maven.build.cache.exclude.glob.7>*.js.map</maven.build.cache.exclude.glob.7>
|
|
||||||
<maven.build.cache.exclude.value.8>src/main/resources/theme/keycloak.v2/account/resources/public/app.css</maven.build.cache.exclude.value.8>
|
|
||||||
<maven.build.cache.exclude.value.9>src/main/resources/theme/keycloak.v2/account/web_modules</maven.build.cache.exclude.value.9>
|
|
||||||
<maven.build.cache.exclude.value.10>src/main/resources/theme/keycloak.v2/welcome/node_modules</maven.build.cache.exclude.value.10>
|
|
||||||
<maven.build.cache.exclude.value.11>src/main/resources/theme/keycloak.v2/welcome/resources/vendor</maven.build.cache.exclude.value.11>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
@ -86,8 +77,6 @@
|
||||||
<exclude>**/Gruntfile.js</exclude>
|
<exclude>**/Gruntfile.js</exclude>
|
||||||
<exclude>**/Gemfile*</exclude>
|
<exclude>**/Gemfile*</exclude>
|
||||||
<exclude>**/.*</exclude>
|
<exclude>**/.*</exclude>
|
||||||
|
|
||||||
<exclude>**/keycloak.v2/account/src/**</exclude>
|
|
||||||
</excludes>
|
</excludes>
|
||||||
</resource>
|
</resource>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -123,61 +112,6 @@
|
||||||
</build>
|
</build>
|
||||||
</profile>
|
</profile>
|
||||||
|
|
||||||
<profile>
|
|
||||||
<id>account2</id>
|
|
||||||
<activation>
|
|
||||||
<property>
|
|
||||||
<name>!skipAccount2</name>
|
|
||||||
</property>
|
|
||||||
</activation>
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-clean-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<filesets>
|
|
||||||
<fileset>
|
|
||||||
<directory>${dir.account2}/web_modules</directory>
|
|
||||||
</fileset>
|
|
||||||
</filesets>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>com.github.eirslett</groupId>
|
|
||||||
<artifactId>frontend-maven-plugin</artifactId>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>install-node-and-pnpm-account2</id>
|
|
||||||
<goals>
|
|
||||||
<goal>install-node-and-pnpm</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
<execution>
|
|
||||||
<id>pnpm-install-account2</id>
|
|
||||||
<goals>
|
|
||||||
<goal>pnpm</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<arguments>${pnpm.args.install}</arguments>
|
|
||||||
<workingDirectory>${dir.account2}</workingDirectory>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
<execution>
|
|
||||||
<id>run-build-account2</id>
|
|
||||||
<goals>
|
|
||||||
<goal>pnpm</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<arguments>run build</arguments>
|
|
||||||
<workingDirectory>${dir.account2}</workingDirectory>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</profile>
|
|
||||||
|
|
||||||
<profile>
|
<profile>
|
||||||
<id>common</id>
|
<id>common</id>
|
||||||
<activation>
|
<activation>
|
||||||
|
|
|
@ -7,6 +7,6 @@
|
||||||
"types": [ "login", "common", "email", "welcome" ]
|
"types": [ "login", "common", "email", "welcome" ]
|
||||||
}, {
|
}, {
|
||||||
"name" : "keycloak.v2",
|
"name" : "keycloak.v2",
|
||||||
"types": [ "account", "admin", "login" ]
|
"types": [ "login" ]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,293 +0,0 @@
|
||||||
<!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},
|
|
||||||
isMyResourcesEnabled : ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
|
|
||||||
deleteAccountAllowed : ${deleteAccountAllowed?c},
|
|
||||||
updateEmailFeatureEnabled: ${updateEmailFeatureEnabled?c},
|
|
||||||
updateEmailActionEnabled: ${updateEmailActionEnabled?c},
|
|
||||||
isViewGroupsEnabled : ${isViewGroupsEnabled?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}';
|
|
||||||
<#outputformat "JavaScript">
|
|
||||||
var l18nMsg = JSON.parse('${msgJSON?js_string}');
|
|
||||||
</#outputformat>
|
|
||||||
<#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>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="${resourceCommonUrl}/node_modules/@patternfly/react-core/dist/styles/base.css"/>
|
|
||||||
<link rel="stylesheet" href="${resourceCommonUrl}/node_modules/@patternfly/patternfly/patternfly-addons.css"/>
|
|
||||||
<link rel="stylesheet" href="${resourceUrl}/public/app.css"/>
|
|
||||||
<link rel="stylesheet" href="${resourceUrl}/public/layout.css"/>
|
|
||||||
|
|
||||||
<#if properties.styles?has_content>
|
|
||||||
<#list properties.styles?split(' ') as style>
|
|
||||||
<link href="${resourceUrl}/${style}" rel="stylesheet"/>
|
|
||||||
</#list>
|
|
||||||
</#if>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var keycloak = new Keycloak({
|
|
||||||
authServerUrl: authUrl,
|
|
||||||
realm: realm,
|
|
||||||
clientId: 'account-console'
|
|
||||||
});
|
|
||||||
keycloak.init({onLoad: 'check-sso'}).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" role="progressbar" 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>
|
|
||||||
</svg>
|
|
||||||
</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 pf-u-display-none pf-u-display-flex-on-md">
|
|
||||||
<a id="landingReferrerLink" href="${referrer_uri}" class="pf-c-button pf-m-link" tabindex="0">
|
|
||||||
<span class="pf-c-button__icon pf-m-start">
|
|
||||||
<i class="pf-icon pf-icon-arrow" aria-hidden="true"></i>
|
|
||||||
</span>
|
|
||||||
${msg("backTo",referrerName)}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</#if>
|
|
||||||
|
|
||||||
<div class="pf-c-page__header-tools-group pf-m-icons pf-u-display-none pf-u-display-flex-on-md pf-u-mr-md">
|
|
||||||
<button id="landingSignInButton" tabindex="0" style="display:none" onclick="keycloak.login();" class="pf-c-button pf-m-primary" type="button">${msg("doSignIn")}</button>
|
|
||||||
<button id="landingSignOutButton" tabindex="0" style="display:none" onclick="keycloak.logout();" class="pf-c-button pf-m-primary" type="button">${msg("doSignOut")}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Kebab for mobile -->
|
|
||||||
<div class="pf-c-page__header-tools-group pf-u-display-none-on-md">
|
|
||||||
<div id="landingMobileKebab" class="pf-c-dropdown pf-m-mobile" onclick="toggleMobileDropdown();"> <!-- pf-m-expanded -->
|
|
||||||
<button aria-label="Actions" tabindex="0" id="landingMobileKebabButton" class="pf-c-dropdown__toggle pf-m-plain" type="button" aria-expanded="true" aria-haspopup="true">
|
|
||||||
<svg fill="currentColor" height="1em" width="1em" viewBox="0 0 192 512" aria-hidden="true" role="img" style="vertical-align: -0.125em;"><path d="M96 184c39.8 0 72 32.2 72 72s-32.2 72-72 72-72-32.2-72-72 32.2-72 72-72zM24 80c0 39.8 32.2 72 72 72s72-32.2 72-72S135.8 8 96 8 24 40.2 24 80zm0 352c0 39.8 32.2 72 72 72s72-32.2 72-72-32.2-72-72-72-72 32.2-72 72z" transform=""></path></svg>
|
|
||||||
</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-limit-width pf-m-light pf-m-shadow-bottom">
|
|
||||||
<div class="pf-c-page__main-body">
|
|
||||||
<div class="pf-c-content" id="landingWelcomeMessage">
|
|
||||||
<h1>${msg("accountManagementWelcomeMessage")}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="pf-c-page__main-section pf-m-limit-width pf-m-overflow-scroll">
|
|
||||||
<div class="pf-c-page__main-body">
|
|
||||||
<div class="pf-l-gallery pf-m-gutter">
|
|
||||||
<#assign content=theme.apply("content.json")?eval>
|
|
||||||
<#list content as item>
|
|
||||||
<div class="pf-l-gallery__item" id="landing-${item.id}">
|
|
||||||
<div class="pf-c-card pf-m-full-height">
|
|
||||||
<div>
|
|
||||||
<div class="pf-c-card__title pf-c-content">
|
|
||||||
<h2 class="pf-u-display-flex pf-u-w-100 pf-u-flex-direction-column">
|
|
||||||
<#if item.icon??>
|
|
||||||
<i class="pf-icon ${item.icon}"></i>
|
|
||||||
<#elseif item.iconSvg??>
|
|
||||||
<img src="${item.iconSvg}" alt="icon"/>
|
|
||||||
</#if>
|
|
||||||
${msg(item.label)}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
<#if item.descriptionLabel??>
|
|
||||||
<p class="pf-u-mb-md">${msg(item.descriptionLabel)}</p>
|
|
||||||
</#if>
|
|
||||||
<#if item.content??>
|
|
||||||
<#list item.content as sub>
|
|
||||||
<div id="landing-${sub.id}">
|
|
||||||
<a onclick="toggleReact(); window.location.hash='${sub.path}'">${msg(sub.label)}</a>
|
|
||||||
</div>
|
|
||||||
</#list>
|
|
||||||
<#else>
|
|
||||||
<a id="landing-${item.id}" onclick="toggleReact(); window.location.hash = '${item.path}'">${msg(item.label)}</a>
|
|
||||||
</#if>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</#list>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
|
@ -1,175 +0,0 @@
|
||||||
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
|
|
||||||
refresh=Refresh
|
|
||||||
done=Done
|
|
||||||
cancel=Cancel
|
|
||||||
remove=Remove
|
|
||||||
update=Update
|
|
||||||
loadingMessage=Account Console loading ...
|
|
||||||
unknownUser=Anonymous
|
|
||||||
fullName={0} {1}
|
|
||||||
allFieldsRequired=All fields are required.
|
|
||||||
|
|
||||||
selectLocale=Select a locale
|
|
||||||
doSignIn=Sign in
|
|
||||||
|
|
||||||
backToAdminConsole=Back to admin console
|
|
||||||
accountManagementWelcomeMessage=Welcome to Keycloak account management
|
|
||||||
|
|
||||||
# Personal info page
|
|
||||||
personalInfoHtmlTitle=Personal info
|
|
||||||
|
|
||||||
# Device activity page
|
|
||||||
signedInDevices=Signed in devices
|
|
||||||
signedInDevicesExplanation=Sign out of any unfamiliar devices.
|
|
||||||
signOutWarning=Sign out the session?
|
|
||||||
signOutAllDevices=Sign out all devices
|
|
||||||
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
|
|
||||||
clients=Clients
|
|
||||||
started=Started
|
|
||||||
expires=Expires
|
|
||||||
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
|
|
||||||
shareWith=Share with
|
|
||||||
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
|
|
||||||
selectPermissions=Select the permissions
|
|
||||||
unShareAllConfirm=Are you sure you want to completely remove all shares?
|
|
||||||
userNotFound=No user found with name or email {0}
|
|
||||||
|
|
||||||
# Linked accounts page
|
|
||||||
linkedAccountsTitle=Linked accounts
|
|
||||||
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}?
|
|
||||||
changePassword=Change password
|
|
||||||
removeCred=Remove {0}
|
|
||||||
setUpNew=Set up {0}
|
|
||||||
removeCredAriaLabel=Remove credential
|
|
||||||
updateCredAriaLabel=Update credential
|
|
||||||
notSetUp={0} is not set up.
|
|
||||||
two-factor=Two-factor authentication
|
|
||||||
passwordless=Passwordless
|
|
||||||
unknown=Unknown
|
|
||||||
password-display-name=Password
|
|
||||||
password-help-text=Sign in by entering your password.
|
|
||||||
password=My password
|
|
||||||
otp-display-name=authenticator application
|
|
||||||
otp-help-text=Enter a verification code from authenticator application.
|
|
||||||
recovery-authn-code=My recovery authentication codes
|
|
||||||
recovery-authn-codes-display-name=Recovery authentication codes
|
|
||||||
recovery-authn-codes-help-text=These codes can be used to regain your access in case your other 2FA means are not available.
|
|
||||||
recovery-codes-number-used={0} recovery codes used
|
|
||||||
recovery-codes-number-remaining={0} recovery codes remaining
|
|
||||||
recovery-codes-generate-new-codes=Generate new codes to ensure access to your account
|
|
||||||
webauthn-display-name=Passkey
|
|
||||||
webauthn-help-text=Use your Passkey to sign in.
|
|
||||||
webauthn-passwordless-display-name=Passkey
|
|
||||||
webauthn-passwordless-help-text=Use your Passkey for passwordless sign in.
|
|
||||||
basic-authentication=Basic authentication
|
|
||||||
invalidRequestMessage=Invalid request
|
|
||||||
|
|
||||||
# Applications page
|
|
||||||
applicationsPageTitle=Applications
|
|
||||||
applicationsPageSubTitle=Manage your application permissions
|
|
||||||
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.
|
|
||||||
termsOfService=Terms of service
|
|
||||||
policy=Privacy policy
|
|
||||||
applicationType=Application type
|
|
||||||
status=Status
|
|
||||||
logo=Logo
|
|
||||||
hasAccessTo=Has access to
|
|
||||||
|
|
||||||
#Delete account page
|
|
||||||
doDelete=Delete
|
|
||||||
deleteAccountSummary=Deleting your account will erase all your data and sign you out immediately.
|
|
||||||
deleteAccount=Delete account
|
|
||||||
deleteAccountWarning=This is irreversible. All your data will be permanently destroyed, and irretrievable.
|
|
||||||
|
|
||||||
error-invalid-value=''{0}'' has invalid value.
|
|
||||||
error-invalid-blank=Please specify value of ''{0}''.
|
|
||||||
error-empty=Please specify value of ''{0}''.
|
|
||||||
error-invalid-length=''{0}'' must have a length between {1} and {2}.
|
|
||||||
error-invalid-length-too-short=''{0}'' must have minimal length of {1}.
|
|
||||||
error-invalid-length-too-long=''{0}'' must have maximal length of {2}.
|
|
||||||
error-invalid-email=Invalid email address.
|
|
||||||
error-invalid-number=''{0}'' is invalid number.
|
|
||||||
error-number-out-of-range=''{0}'' must be a number between {1} and {2}.
|
|
||||||
error-number-out-of-range-too-small=''{0}'' must have minimal value of {1}.
|
|
||||||
error-number-out-of-range-too-big=''{0}'' must have maximal value of {2}.
|
|
||||||
error-pattern-no-match=''{0}'' doesn''t match required format.
|
|
||||||
error-invalid-uri=''{0}'' is invalid URL.
|
|
||||||
error-invalid-uri-scheme=''{0}'' has invalid URL scheme.
|
|
||||||
error-invalid-uri-fragment=''{0}'' is invalid URL fragment.
|
|
||||||
error-user-attribute-required=Please specify ''{0}''.
|
|
||||||
error-invalid-date=''{0}'' is invalid date.
|
|
||||||
error-username-invalid-character=''{0}'' contains invalid character.
|
|
||||||
error-person-name-invalid-character='{0}' contains invalid character.
|
|
||||||
|
|
||||||
updateEmail=Update email
|
|
||||||
|
|
||||||
#groups
|
|
||||||
groupLabel=Groups
|
|
||||||
groupDescriptionLabel=View groups that you are associated with
|
|
||||||
path=Path
|
|
||||||
directMembership=Direct membership
|
|
||||||
noGroups=No groups
|
|
||||||
noGroupsText=You are not joined in any group
|
|
|
@ -1,12 +0,0 @@
|
||||||
# ignore typescript-generated files
|
|
||||||
*.js
|
|
||||||
*.js.map
|
|
||||||
|
|
||||||
# ignore log files
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Don't ignore these
|
|
||||||
!WelcomePageScripts.js
|
|
||||||
!content.json
|
|
||||||
|
|
||||||
public/app.css
|
|
|
@ -1,70 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "personal-info",
|
|
||||||
"path": "personal-info",
|
|
||||||
"icon": "pf-icon-user",
|
|
||||||
"label": "personalInfoSidebarTitle",
|
|
||||||
"descriptionLabel": "personalInfoIntroMessage",
|
|
||||||
"modulePath": "/content/account-page/AccountPage.js",
|
|
||||||
"componentName": "AccountPage"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "security",
|
|
||||||
"icon": "pf-icon-security",
|
|
||||||
"label": "accountSecuritySidebarTitle",
|
|
||||||
"descriptionLabel": "accountSecurityIntroMessage",
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"id": "signingin",
|
|
||||||
"path": "security/signingin",
|
|
||||||
"label": "signingInSidebarTitle",
|
|
||||||
"modulePath": "/content/signingin-page/SigningInPage.js",
|
|
||||||
"componentName": "SigningInPage"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "device-activity",
|
|
||||||
"path": "security/device-activity",
|
|
||||||
"label": "deviceActivitySidebarTitle",
|
|
||||||
"modulePath": "/content/device-activity-page/DeviceActivityPage.js",
|
|
||||||
"componentName": "DeviceActivityPage"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "linked-accounts",
|
|
||||||
"path": "security/linked-accounts",
|
|
||||||
"label": "linkedAccountsSidebarTitle",
|
|
||||||
"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": "groups",
|
|
||||||
"path": "groups",
|
|
||||||
"icon": "pf-icon-server-group",
|
|
||||||
"label": "groupLabel",
|
|
||||||
"descriptionLabel": "groupDescriptionLabel",
|
|
||||||
"modulePath": "/content/group-page/GroupsPage.js",
|
|
||||||
"componentName": "GroupsPage",
|
|
||||||
"hidden": "!features.isViewGroupsEnabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "resources",
|
|
||||||
"icon": "pf-icon-repository",
|
|
||||||
"path": "resources",
|
|
||||||
"label": "resources",
|
|
||||||
"descriptionLabel": "resourceIntroMessage",
|
|
||||||
"modulePath": "/content/my-resources-page/MyResourcesPage.js",
|
|
||||||
"componentName": "MyResourcesPage",
|
|
||||||
"hidden": "!features.isMyResourcesEnabled"
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.faac274b-9d49-4ba1-9ef0-610572d38128 {
|
|
||||||
fill: #1877f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.f55f6e4a-14c2-4793-a1d5-db4b028479c8 {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g id="b36e017a-ce19-4905-9b48-48066e87bbf5" data-name="New stuff">
|
|
||||||
<g>
|
|
||||||
<rect class="faac274b-9d49-4ba1-9ef0-610572d38128" width="32" height="32" rx="1.19"/>
|
|
||||||
<path class="f55f6e4a-14c2-4793-a1d5-db4b028479c8" d="M26.93,16.07a10.93,10.93,0,1,0-12.64,10.8V19.23H11.52V16.07h2.77V13.66c0-2.74,1.63-4.26,4.13-4.26a16.32,16.32,0,0,1,2.45.22v2.69H19.49A1.58,1.58,0,0,0,17.71,14v2.05h3l-.48,3.16H17.71v7.64a10.94,10.94,0,0,0,9.22-10.8Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 730 B |
|
@ -1,16 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.b42551c7-a511-4544-a371-f6e3883f7abd {
|
|
||||||
fill: #fff;
|
|
||||||
fill-rule: evenodd;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g id="f00b56ab-1ecb-4d94-b704-cf429e3c78a4" data-name="GitHub">
|
|
||||||
<g>
|
|
||||||
<rect width="32" height="32" rx="1.19"/>
|
|
||||||
<path class="b42551c7-a511-4544-a371-f6e3883f7abd" d="M16,5.13a11.06,11.06,0,0,0-3.5,21.55c.56.1.76-.24.76-.53s0-1,0-1.88c-3.07.67-3.72-1.48-3.72-1.48a2.91,2.91,0,0,0-1.23-1.62c-1-.69.08-.67.08-.67a2.32,2.32,0,0,1,1.69,1.14,2.36,2.36,0,0,0,3.22.92,2.36,2.36,0,0,1,.7-1.48c-2.45-.28-5-1.23-5-5.47a4.29,4.29,0,0,1,1.13-3,4,4,0,0,1,.11-2.93s.93-.3,3,1.13a10.55,10.55,0,0,1,5.54,0c2.11-1.43,3-1.13,3-1.13a4,4,0,0,1,.11,2.93,4.25,4.25,0,0,1,1.13,3c0,4.25-2.58,5.18-5.05,5.46a2.62,2.62,0,0,1,.75,2.05c0,1.47,0,2.67,0,3s.2.64.76.53A11.06,11.06,0,0,0,16,5.13Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 912 B |
|
@ -1,77 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.e5ef51ce-3313-4c04-b152-23646fd2b306 {
|
|
||||||
fill: none;
|
|
||||||
clip-rule: evenodd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ef16f3ef-fb03-4a62-9cce-d65767c17c4c {
|
|
||||||
fill: #ededed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.e0f4cb03-a97e-447b-8c37-60d897bac484 {
|
|
||||||
clip-path: url(#fe0cecb0-916b-4b9d-828f-63ec94884d2e);
|
|
||||||
}
|
|
||||||
|
|
||||||
.afb39849-fc2d-48aa-a4a4-070e36b84e51 {
|
|
||||||
fill: #3e82f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.a2ee00a5-b156-4157-adc5-b40fd1ee5ebd {
|
|
||||||
clip-path: url(#e022c77c-5a12-4caf-aaed-c796166a8562);
|
|
||||||
}
|
|
||||||
|
|
||||||
.a025e47e-f997-4bd7-8964-88ada0c7c12f {
|
|
||||||
fill: #32a753;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b4e7c98a-856b-4280-8aaa-b9f157cf22ea {
|
|
||||||
clip-path: url(#b1e1e904-5b74-4ac6-8dc5-11a864f7f8ec);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b0bf74a2-e960-4e68-948c-0e3dfb1bcd2a {
|
|
||||||
fill: #f9bb00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ba55bfd1-dca2-4617-b45c-128fdf3bf3b4 {
|
|
||||||
clip-path: url(#bcdf0102-4e91-4218-8945-e2893d757d6d);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fae83dfa-2d64-4e2a-9cd9-b00b32869370 {
|
|
||||||
fill: #e74235;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<clipPath id="fe0cecb0-916b-4b9d-828f-63ec94884d2e">
|
|
||||||
<path class="e5ef51ce-3313-4c04-b152-23646fd2b306" d="M25.85,16.23a12.53,12.53,0,0,0-.18-2.06H16.2v3.89h5.41a4.58,4.58,0,0,1-2,3v2.53h3.25a9.81,9.81,0,0,0,3-7.39Zm0,0"/>
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="e022c77c-5a12-4caf-aaed-c796166a8562">
|
|
||||||
<path class="e5ef51ce-3313-4c04-b152-23646fd2b306" d="M16.2,26.05a9.61,9.61,0,0,0,6.65-2.43L19.6,21.09a6.06,6.06,0,0,1-9-3.18H7.22v2.6a10,10,0,0,0,9,5.54Zm0,0"/>
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="b1e1e904-5b74-4ac6-8dc5-11a864f7f8ec">
|
|
||||||
<path class="e5ef51ce-3313-4c04-b152-23646fd2b306" d="M10.58,17.91a5.86,5.86,0,0,1,0-3.82v-2.6H7.22a10,10,0,0,0,0,9l3.36-2.6Zm0,0"/>
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id="bcdf0102-4e91-4218-8945-e2893d757d6d">
|
|
||||||
<path class="e5ef51ce-3313-4c04-b152-23646fd2b306" d="M16.2,10A5.39,5.39,0,0,1,20,11.45l2.89-2.88A9.7,9.7,0,0,0,16.2,6a10,10,0,0,0-9,5.54l3.36,2.6A6,6,0,0,1,16.2,10Zm0,0"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<g id="a06b7517-c1f4-49d5-aa34-cee2287c4769" data-name="Google">
|
|
||||||
<g id="fbe0f094-aaab-4b03-a7bf-452a2c250397" data-name="Full color">
|
|
||||||
<rect class="ef16f3ef-fb03-4a62-9cce-d65767c17c4c" width="32" height="32" rx="1.19"/>
|
|
||||||
<g>
|
|
||||||
<g class="e0f4cb03-a97e-447b-8c37-60d897bac484">
|
|
||||||
<rect class="afb39849-fc2d-48aa-a4a4-070e36b84e51" x="10.62" y="8.59" width="20.81" height="20.61"/>
|
|
||||||
</g>
|
|
||||||
<g class="a2ee00a5-b156-4157-adc5-b40fd1ee5ebd">
|
|
||||||
<rect class="a025e47e-f997-4bd7-8964-88ada0c7c12f" x="1.64" y="12.33" width="26.8" height="19.31"/>
|
|
||||||
</g>
|
|
||||||
<g class="b4e7c98a-856b-4280-8aaa-b9f157cf22ea">
|
|
||||||
<rect class="b0bf74a2-e960-4e68-948c-0e3dfb1bcd2a" x="0.57" y="5.9" width="15.59" height="20.19"/>
|
|
||||||
</g>
|
|
||||||
<g class="ba55bfd1-dca2-4617-b45c-128fdf3bf3b4">
|
|
||||||
<rect class="fae83dfa-2d64-4e2a-9cd9-b00b32869370" x="1.64" y="0.37" width="26.87" height="19.31"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3 KiB |
|
@ -1,23 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.ac4e1fda-441f-45c3-89b5-eabc007f54ca {
|
|
||||||
fill: #e1306c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.a7c172c4-b213-4581-937c-bb042fd94b3e {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g id="a864479b-0561-4117-aa66-744aa86c5250" data-name="Instagram">
|
|
||||||
<g id="bb9ebb9e-2d15-4e72-a42a-7eff89494a26" data-name="Black and white">
|
|
||||||
<rect class="ac4e1fda-441f-45c3-89b5-eabc007f54ca" width="32" height="32" rx="1.19"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="a7c172c4-b213-4581-937c-bb042fd94b3e" d="M16,6.09c-2.69,0-3,0-4.09.06a7.18,7.18,0,0,0-2.4.46,5,5,0,0,0-2.9,2.9,7.18,7.18,0,0,0-.46,2.4C6.1,13,6.09,13.31,6.09,16s0,3,.06,4.09a7.18,7.18,0,0,0,.46,2.4,5,5,0,0,0,2.9,2.9,7.18,7.18,0,0,0,2.4.46c1.06,0,1.4.06,4.09.06s3,0,4.09-.06a7.18,7.18,0,0,0,2.4-.46,5,5,0,0,0,2.9-2.9,7.18,7.18,0,0,0,.46-2.4c0-1.06.06-1.4.06-4.09s0-3-.06-4.09a7.18,7.18,0,0,0-.46-2.4,5,5,0,0,0-2.9-2.9,7.18,7.18,0,0,0-2.4-.46C19,6.1,18.69,6.09,16,6.09Zm0,1.79c2.65,0,3,0,4,0a5.73,5.73,0,0,1,1.84.34A3,3,0,0,1,23,9a3,3,0,0,1,.75,1.14A5.73,5.73,0,0,1,24.07,12c0,1,.05,1.35.05,4s0,3-.05,4a5.73,5.73,0,0,1-.34,1.84,3.38,3.38,0,0,1-1.89,1.89,5.73,5.73,0,0,1-1.84.34c-1,0-1.35.05-4,.05s-3,0-4-.05a5.73,5.73,0,0,1-1.84-.34A3,3,0,0,1,9,23a3,3,0,0,1-.75-1.14A5.73,5.73,0,0,1,7.93,20c0-1,0-1.35,0-4s0-3,0-4a5.73,5.73,0,0,1,.34-1.84A3,3,0,0,1,9,9a3,3,0,0,1,1.14-.75A5.73,5.73,0,0,1,12,7.93c1,0,1.35,0,4,0"/>
|
|
||||||
<path class="a7c172c4-b213-4581-937c-bb042fd94b3e" d="M16,19.3A3.3,3.3,0,1,1,19.3,16,3.3,3.3,0,0,1,16,19.3Zm0-8.39A5.09,5.09,0,1,0,21.09,16,5.09,5.09,0,0,0,16,10.91Z"/>
|
|
||||||
<path class="a7c172c4-b213-4581-937c-bb042fd94b3e" d="M22.48,10.71a1.19,1.19,0,1,1-1.19-1.19,1.19,1.19,0,0,1,1.19,1.19Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -1,23 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.a975829d-e85a-4fdc-b46a-16eba47b4e8e {
|
|
||||||
fill: #2867b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b17912fc-cab5-4688-935b-8aa15b250003 {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g id="adf29cd2-f5a2-4433-a836-7d0b8cd64c5d" data-name="LinkedIn">
|
|
||||||
<g>
|
|
||||||
<rect class="a975829d-e85a-4fdc-b46a-16eba47b4e8e" width="32" height="32" rx="1.19"/>
|
|
||||||
<g>
|
|
||||||
<polygon class="b17912fc-cab5-4688-935b-8aa15b250003" points="10.47 25.5 6.46 25.5 6.46 12.59 10.47 12.59 10.47 25.5 10.47 25.5"/>
|
|
||||||
<path class="b17912fc-cab5-4688-935b-8aa15b250003" d="M8.46,10.83A2.33,2.33,0,1,1,10.79,8.5a2.33,2.33,0,0,1-2.33,2.33Z"/>
|
|
||||||
<path class="b17912fc-cab5-4688-935b-8aa15b250003" d="M25.5,25.5h-4V19.22c0-1.49,0-3.42-2.09-3.42S17,17.43,17,19.11V25.5H13V12.59h3.84v1.76h.06a4.21,4.21,0,0,1,3.8-2.08c4.06,0,4.81,2.67,4.81,6.15V25.5Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 951 B |
|
@ -1,34 +0,0 @@
|
||||||
<svg id="bf5a9f86-a166-4609-aea2-c789db71fd48" data-name="Microsoft" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.fe82c64e-6d2d-42d3-aeec-ae7ea0741e78 {
|
|
||||||
fill: #ededed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.be49cf2d-e5c3-4773-b2c7-b422380b776a {
|
|
||||||
fill: #7fba00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.a9f66f41-846e-499a-af6d-5503813bb401 {
|
|
||||||
fill: #ffb900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.a8084a6b-0b46-4437-8e12-638c29ccbea1 {
|
|
||||||
fill: #f25022;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bb872b1d-e833-4ba3-99af-3b105ed602a0 {
|
|
||||||
fill: #00a4ef;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g id="e0b7e64b-db07-46e0-9f19-72b142175f6b" data-name="Full color">
|
|
||||||
<rect class="fe82c64e-6d2d-42d3-aeec-ae7ea0741e78" width="32" height="32" rx="1.19"/>
|
|
||||||
<g>
|
|
||||||
<rect class="be49cf2d-e5c3-4773-b2c7-b422380b776a" x="16.47" y="6.5" width="9.03" height="9.03" rx="0.1"/>
|
|
||||||
<rect class="a9f66f41-846e-499a-af6d-5503813bb401" x="16.47" y="16.47" width="9.03" height="9.03" rx="0.1"/>
|
|
||||||
<rect class="a8084a6b-0b46-4437-8e12-638c29ccbea1" x="6.5" y="6.5" width="9.03" height="9.03" rx="0.1"/>
|
|
||||||
<rect class="bb872b1d-e833-4ba3-99af-3b105ed602a0" x="6.5" y="16.47" width="9.03" height="9.03" rx="0.1"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1,15 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(0.321739,0,0,0.321739,-0.234299,0.0972928)">
|
|
||||||
<path d="M64.38,23.5C67.556,24.99 70.436,27.044 72.88,29.56L88.62,23.83C80.176,12.044 66.537,5.037 52.039,5.037C27.353,5.037 7.039,25.351 7.039,50.037C7.039,51.183 7.083,52.328 7.17,53.47L22.91,47.74C24.033,32.597 36.806,20.737 51.99,20.737C56.273,20.737 60.503,21.68 64.38,23.5" style="fill:rgb(238,0,0);fill-rule:nonzero;"/>
|
|
||||||
<path d="M16,58.19L1,63.63C2.371,69.099 4.744,74.267 8,78.87L23.7,73.16C19.696,69.029 16.99,63.813 15.92,58.16" style="fill:rgb(238,0,0);fill-rule:nonzero;"/>
|
|
||||||
<path d="M81.16,52.25C80.904,55.75 80.009,59.173 78.52,62.35C71.736,76.878 54.205,83.258 39.67,76.49C36.487,75.027 33.599,72.994 31.15,70.49L15.45,76.21C23.88,88.016 37.524,95.039 52.032,95.039C69.504,95.039 85.459,84.851 92.81,69C96.075,61.982 97.454,54.233 96.81,46.52L81.16,52.25Z" style="fill:rgb(238,0,0);fill-rule:nonzero;"/>
|
|
||||||
<path d="M85,33L70,38.45C72.851,43.476 74.153,49.236 73.74,55L89.44,49.29C88.992,43.63 87.486,38.104 85,33" style="fill:rgb(238,0,0);fill-rule:nonzero;"/>
|
|
||||||
<path d="M29.46,45.36L13.72,51.09C13.94,53.604 14.368,56.096 15,58.54L30,53.1C29.501,50.552 29.346,47.949 29.54,45.36" style="fill:rgb(204,0,0);fill-rule:nonzero;"/>
|
|
||||||
<path d="M99,28C97.903,25.724 96.619,23.543 95.16,21.48L79.43,27.18C81.238,29.04 82.791,31.132 84.05,33.4L99,28Z" style="fill:rgb(204,0,0);fill-rule:nonzero;"/>
|
|
||||||
<path d="M15.45,76.17C16.671,77.879 18.007,79.503 19.45,81.03L36.54,74.79C34.578,73.561 32.769,72.105 31.15,70.45L15.45,76.17ZM96.86,46.54L81.16,52.25C80.985,54.545 80.539,56.81 79.83,59L96.92,52.76C97.04,50.684 97.02,48.603 96.86,46.53" style="fill:rgb(204,0,0);fill-rule:nonzero;"/>
|
|
||||||
<path d="M29.4,48.52C29.36,47.466 29.38,46.411 29.46,45.36L13.72,51.09C13.8,52.09 13.93,53.09 14.08,54.09L29.4,48.52Z" style="fill:rgb(163,0,0);fill-rule:nonzero;"/>
|
|
||||||
<path d="M96.72,23.82C96.22,23.01 95.72,22.22 95.16,21.44L79.43,27.18C80.116,27.884 80.764,28.626 81.37,29.4L96.72,23.82Z" style="fill:rgb(163,0,0);fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.5 KiB |
|
@ -1,26 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.f425009f-d653-4243-b52f-e6e7efdbf57a {
|
|
||||||
fill: #4d4d4d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bac6a0a4-fc2d-4f36-9ded-667acc6f4b52 {
|
|
||||||
fill: #bcbbbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b9d59bf2-7616-43cd-a672-04b81955bad9 {
|
|
||||||
fill: #f48024;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g id="b393dd85-54a9-4370-beab-fda62d40e5d8" data-name="StackOverflow">
|
|
||||||
<g id="beef34ed-ad1e-45a9-8dc6-781d71c5dd19" data-name="Full color">
|
|
||||||
<rect class="f425009f-d653-4243-b52f-e6e7efdbf57a" width="32" height="32" rx="1.19"/>
|
|
||||||
<g>
|
|
||||||
<polygon class="bac6a0a4-fc2d-4f36-9ded-667acc6f4b52" points="21.29 23.44 21.29 18.82 23.34 18.82 23.34 25.5 7.94 25.5 7.94 18.82 9.99 18.82 9.99 23.44 21.29 23.44"/>
|
|
||||||
<path class="b9d59bf2-7616-43cd-a672-04b81955bad9" d="M11.53,21.9h8.22V20.36H11.53ZM19,6.5l-1.39,1,5.08,6.83,1.39-1Zm-4.21,4L21.29,16l1.08-1.28L15.85,9.22Zm-2.16,3.8,7.7,3.59L21,16.36l-7.7-3.59Zm7.18,5.53.36-1.51-8.27-1.72-.35,1.7Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1 KiB |
|
@ -1,19 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.b48eca1a-55ea-4187-8747-2a0a7c0905ff {
|
|
||||||
fill: #1da1f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.a0fa0208-710f-4171-b935-86383a49537b {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g id="e43e3b03-e423-44e8-9d4d-c8c1f8f88064" data-name="Twitter">
|
|
||||||
<g>
|
|
||||||
<rect class="b48eca1a-55ea-4187-8747-2a0a7c0905ff" width="32" height="32" rx="1.19"/>
|
|
||||||
<path class="a0fa0208-710f-4171-b935-86383a49537b" d="M24.42,11.5c0,.18,0,.37,0,.56A12.25,12.25,0,0,1,5.58,22.37,8.72,8.72,0,0,0,12,20.59a4.3,4.3,0,0,1-4-3,4.28,4.28,0,0,0,.81.07,4.22,4.22,0,0,0,1.13-.15A4.29,4.29,0,0,1,6.43,13.3v-.05a4.4,4.4,0,0,0,2,.54A4.31,4.31,0,0,1,7,8a12.26,12.26,0,0,0,8.88,4.5,4,4,0,0,1-.11-1,4.3,4.3,0,0,1,7.44-3,8.59,8.59,0,0,0,2.73-1A4.31,4.31,0,0,1,24.09,10a8.86,8.86,0,0,0,2.47-.68,8.72,8.72,0,0,1-2.14,2.23Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 895 B |
Before Width: | Height: | Size: 627 B |
|
@ -1,84 +0,0 @@
|
||||||
/* Globals */
|
|
||||||
.brand {
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-button {
|
|
||||||
width: 150px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 320px) {
|
|
||||||
.delete-button {
|
|
||||||
width: 120px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Account Page screen */
|
|
||||||
.personal-info-form .pf-c-form__group-control {
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Device Activity screen */
|
|
||||||
.signed-in-device-list .pf-c-data-list__item-row {
|
|
||||||
--pf-c-data-list__item-row--PaddingRight: 0;
|
|
||||||
--pf-c-data-list__item-row--PaddingLeft: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signed-in-device-list .pf-c-data-list__expandable-content-body {
|
|
||||||
--pf-c-data-list__expandable-content-body--PaddingRight: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signed-in-device-grid {
|
|
||||||
grid-template-columns: auto repeat(11, [col-start] 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signed-in-device-list.pf-c-data-list {
|
|
||||||
--pf-c-data-list--sm--BorderTopWidth: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pf-c-data-list__item {
|
|
||||||
--pf-c-data-list__item--BorderBottomWidth: 1px;
|
|
||||||
--pf-c-data-list__item--BorderBottomColor: var(--pf-global--BorderColor--100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signed-in-device-list.pf-c-data-list {
|
|
||||||
--pf-c-data-list--BorderTopWidth: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
|
||||||
.pf-c-data-list__item {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--pf-c-data-list__item--BackgroundColor);
|
|
||||||
border-bottom: var(--pf-c-data-list__item--BorderBottomWidth) solid var(--pf-c-data-list__item--BorderBottomColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signed-in-device-list.pf-c-data-list {
|
|
||||||
--pf-c-data-list--BorderTopWidth: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.signed-in-device-list .pf-c-description-list {
|
|
||||||
--pf-c-description-list--GridTemplateColumns--count: 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Signing in screen */
|
|
||||||
.title-case:first-letter,
|
|
||||||
.cred-title:first-letter,
|
|
||||||
#otp-not-set-up .pf-c-empty-state__body:first-letter {
|
|
||||||
text-transform: capitalize
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Applications screen */
|
|
||||||
#applications-list-header .pf-c-data-list__item-content {
|
|
||||||
--pf-c-data-list__item-content--md--PaddingBottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pf-u-pl-35xl {
|
|
||||||
padding-left: 4.5rem;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 22 KiB |
|
@ -1,89 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
var isWelcomePage = function () {
|
|
||||||
var winHash = window.location.hash;
|
|
||||||
return winHash === '#/';
|
|
||||||
};
|
|
||||||
|
|
||||||
var toggleReact = function () {
|
|
||||||
var welcomeScreen = document.getElementById("welcomeScreen");
|
|
||||||
var spinnerScreen = document.getElementById("spinner_screen");
|
|
||||||
var reactScreen = document.getElementById("main_react_container");
|
|
||||||
|
|
||||||
if (!isWelcomePage() && !isReactLoading) {
|
|
||||||
if (welcomeScreen) welcomeScreen.style.display = 'none';
|
|
||||||
if (spinnerScreen) spinnerScreen.style.display = 'none';
|
|
||||||
if (reactScreen) reactScreen.style.display = 'block';
|
|
||||||
if (reactScreen) reactScreen.style.height = '100%';
|
|
||||||
} else if (!isWelcomePage() && isReactLoading) {
|
|
||||||
if (welcomeScreen) welcomeScreen.style.display = 'none';
|
|
||||||
if (reactScreen) reactScreen.style.display = 'none';
|
|
||||||
if (spinnerScreen) spinnerScreen.style.display = 'block';
|
|
||||||
if (spinnerScreen) spinnerScreen.style.height = '100%';
|
|
||||||
} else {
|
|
||||||
if (reactScreen) reactScreen.style.display = 'none';
|
|
||||||
if (spinnerScreen) spinnerScreen.style.display = 'none';
|
|
||||||
if (welcomeScreen) welcomeScreen.style.display = 'block';
|
|
||||||
if (welcomeScreen) welcomeScreen.style.height = '100%';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function loggedInUserName() {
|
|
||||||
let userName = l18nMsg['unknownUser'];
|
|
||||||
if (keycloak.tokenParsed) {
|
|
||||||
const givenName = keycloak.tokenParsed.given_name;
|
|
||||||
const familyName = keycloak.tokenParsed.family_name;
|
|
||||||
const preferredUsername = keycloak.tokenParsed.preferred_username;
|
|
||||||
if (givenName && familyName) {
|
|
||||||
userName = [givenName, familyName].reduce((acc, value, index) =>
|
|
||||||
acc.replace('{{param_'+ index + '}}', value), l18nMsg['fullName']
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
userName = (givenName || familyName) || preferredUsername || userName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sanitize(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitize(dirtyString) {
|
|
||||||
let element = document.createElement("span");
|
|
||||||
element.textContent = dirtyString;
|
|
||||||
return element.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
var toggleMobileDropdown = function () {
|
|
||||||
var mobileDropdown = document.getElementById("landingMobileDropdown");
|
|
||||||
var mobileKebab = document.getElementById("landingMobileKebab");
|
|
||||||
var mobileKebabButton = document.getElementById("landingMobileKebabButton");
|
|
||||||
if (mobileDropdown.style.display === 'none') {
|
|
||||||
mobileDropdown.style.display = 'block';
|
|
||||||
mobileKebab.classList.add("pf-m-expanded");
|
|
||||||
mobileKebabButton.setAttribute("aria-expanded", "true");
|
|
||||||
} else {
|
|
||||||
mobileDropdown.style.display = 'none';
|
|
||||||
mobileKebab.classList.remove("pf-m-expanded");
|
|
||||||
mobileKebabButton.setAttribute("aria-expanded", "false");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var loadjs = function (url, loadListener) {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.src = resourceUrl + url;
|
|
||||||
script.type = "module";
|
|
||||||
if (loadListener)
|
|
||||||
script.addEventListener("load", loadListener);
|
|
||||||
document.head.appendChild(script);
|
|
||||||
};
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"snowpack/assets/babel-plugin.js",
|
|
||||||
{
|
|
||||||
"webModulesUrl": "./keycloak.v2/web_modules",
|
|
||||||
"moduleResolution": "node"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
{}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-react",
|
|
||||||
"@babel/preset-typescript"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
v16.13.0
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 {PageHeaderTool} from './PageHeaderTool';
|
|
||||||
import {makeRoutes} from './ContentPages';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Brand,
|
|
||||||
Page,
|
|
||||||
PageHeader,
|
|
||||||
PageSidebar
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { KeycloakContext } from './keycloak-service/KeycloakContext';
|
|
||||||
|
|
||||||
declare function toggleReact(): void;
|
|
||||||
declare function isWelcomePage(): boolean;
|
|
||||||
|
|
||||||
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 Header = (
|
|
||||||
<PageHeader
|
|
||||||
logo={<a id="brandLink" href={brandUrl}><Brand src={brandImg} alt="Logo" className="brand"/></a>}
|
|
||||||
headerTools={<PageHeaderTool/>}
|
|
||||||
showNavToggle
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Sidebar = <PageSidebar nav={<PageNav/>} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page header={Header} sidebar={Sidebar} isManagedSidebar>
|
|
||||||
{makeRoutes()}
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,175 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>);
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
/*
|
|
||||||
* 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);
|
|
||||||
});
|
|
|
@ -1,32 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import {PageHeaderTools} from '@patternfly/react-core';
|
|
||||||
import {ReferrerLink} from './widgets/ReferrerLink';
|
|
||||||
import {LogoutButton} from './widgets/Logout';
|
|
||||||
|
|
||||||
declare const referrerName: string;
|
|
||||||
declare function loggedInUserName(): string;
|
|
||||||
|
|
||||||
export class PageHeaderTool extends React.Component {
|
|
||||||
private hasReferrer: boolean = typeof referrerName !== 'undefined';
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const username = loggedInUserName();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageHeaderTools>
|
|
||||||
{this.hasReferrer &&
|
|
||||||
<div className="pf-c-page__header-tools-group">
|
|
||||||
<ReferrerLink/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="pf-c-page__header-tools-group">
|
|
||||||
<LogoutButton/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span style={{marginLeft: '10px'}} id="loggedInUser">{username}</span>
|
|
||||||
</PageHeaderTools>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
* 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);
|
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 {
|
|
||||||
return (
|
|
||||||
<Toolbar>
|
|
||||||
{this.hasReferrer &&
|
|
||||||
<ToolbarGroup key='referrerGroup' alignment={{default:"alignRight"}}>
|
|
||||||
<ToolbarItem className="pf-m-icons" key='referrer'>
|
|
||||||
<ReferrerLink/>
|
|
||||||
</ToolbarItem>
|
|
||||||
</ToolbarGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<ToolbarGroup key='secondGroup' alignment={{default:"alignRight"}}>
|
|
||||||
<ToolbarItem className="pf-m-icons" key='logout'>
|
|
||||||
<LogoutButton/>
|
|
||||||
</ToolbarItem>
|
|
||||||
</ToolbarGroup>
|
|
||||||
</Toolbar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import { AccountServiceClient } from './account.service';
|
|
||||||
|
|
||||||
export const AccountServiceContext = React.createContext<AccountServiceClient | undefined>(undefined);
|
|
|
@ -1,158 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 = unknown> 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) {
|
|
||||||
if (response.data['errors'] != null) {
|
|
||||||
for(let err of response.data['errors'])
|
|
||||||
ContentAlert.danger(err['errorMessage'], err['params']);
|
|
||||||
} else {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,112 +0,0 @@
|
||||||
/*
|
|
||||||
* 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}
|
|
||||||
actionClose={
|
|
||||||
<AlertActionCloseButton
|
|
||||||
title={message}
|
|
||||||
variantLabel={`${variant} alert`}
|
|
||||||
onClose={() => this.hideAlert(key)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
key={key} />
|
|
||||||
))}
|
|
||||||
</AlertGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* 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, Text, Title, Tooltip, Card, CardBody, Stack, StackItem, PageSection, TextContent, PageSectionVariants, SplitItem, Split} from '@patternfly/react-core';
|
|
||||||
import {RedoIcon, SyncAltIcon} from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
import {Msg} from '../widgets/Msg';
|
|
||||||
import {ContentAlert} from './ContentAlert';
|
|
||||||
|
|
||||||
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 />
|
|
||||||
|
|
||||||
<PageSection variant={PageSectionVariants.light} className="pf-u-pb-xs">
|
|
||||||
<Split>
|
|
||||||
<SplitItem isFilled>
|
|
||||||
<TextContent>
|
|
||||||
<Title headingLevel="h1" size="2xl" className="pf-u-mb-xl">
|
|
||||||
<Msg msgKey={this.props.title} />
|
|
||||||
</Title>
|
|
||||||
{this.props.introMessage && (
|
|
||||||
<Text component="p">
|
|
||||||
<Msg msgKey={this.props.introMessage} />
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</TextContent>
|
|
||||||
</SplitItem>
|
|
||||||
{this.props.onRefresh && (
|
|
||||||
<SplitItem>
|
|
||||||
<Tooltip content={<Msg msgKey="refreshPage" />}>
|
|
||||||
<Button
|
|
||||||
aria-label={Msg.localize('refreshPage')}
|
|
||||||
id="refresh-page"
|
|
||||||
variant="link"
|
|
||||||
onClick={this.props.onRefresh}
|
|
||||||
icon={<SyncAltIcon />}
|
|
||||||
>
|
|
||||||
<Msg msgKey="refresh" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</SplitItem>
|
|
||||||
)}
|
|
||||||
</Split>
|
|
||||||
</PageSection>
|
|
||||||
{this.props.children}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,406 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
InputGroup,
|
|
||||||
Grid,
|
|
||||||
GridItem,
|
|
||||||
ExpandableSection,
|
|
||||||
ValidatedOptions,
|
|
||||||
PageSection,
|
|
||||||
PageSectionVariants,
|
|
||||||
Text,
|
|
||||||
TextVariants,
|
|
||||||
TextContent
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { HttpResponse } from '../../account-service/account.service';
|
|
||||||
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
|
|
||||||
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';
|
|
||||||
import {ExternalLinkSquareAltIcon} from "@patternfly/react-icons";
|
|
||||||
|
|
||||||
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 isUpdateEmailFeatureEnabled: boolean = features.updateEmailFeatureEnabled;
|
|
||||||
private isUpdateEmailActionEnabled: boolean = features.updateEmailActionEnabled;
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleEmailUpdate = (keycloak: KeycloakService): void => {
|
|
||||||
new AIACommand(keycloak, "UPDATE_EMAIL").execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const fields: FormFields = this.state.formFields;
|
|
||||||
return (
|
|
||||||
<ContentPage
|
|
||||||
title="personalInfoHtmlTitle"
|
|
||||||
introMessage="personalSubMessage"
|
|
||||||
>
|
|
||||||
<PageSection isFilled variant={PageSectionVariants.light}>
|
|
||||||
<TextContent className="pf-u-mb-lg">
|
|
||||||
<Text component={TextVariants.small}>
|
|
||||||
{Msg.localize('allFieldsRequired')}
|
|
||||||
</Text>
|
|
||||||
</TextContent>
|
|
||||||
<Form
|
|
||||||
onSubmit={(event) => this.handleSubmit(event)}
|
|
||||||
className="personal-info-form"
|
|
||||||
>
|
|
||||||
{!this.isRegistrationEmailAsUsername && fields.username != undefined && (
|
|
||||||
<FormGroup
|
|
||||||
label={Msg.localize("username")}
|
|
||||||
fieldId="user-name"
|
|
||||||
helperTextInvalid={this.state.errors.username}
|
|
||||||
validated={
|
|
||||||
this.state.errors.username !== ""
|
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{this.isEditUserNameAllowed && <this.UsernameInput />}
|
|
||||||
{!this.isEditUserNameAllowed && (
|
|
||||||
<this.RestrictedUsernameInput />
|
|
||||||
)}
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{!this.isUpdateEmailFeatureEnabled && <FormGroup
|
|
||||||
label={Msg.localize('email')}
|
|
||||||
fieldId="email-address"
|
|
||||||
helperTextInvalid={this.state.errors.email}
|
|
||||||
validated={
|
|
||||||
this.state.errors.email !== ""
|
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
isRequired
|
|
||||||
type="email"
|
|
||||||
id="email-address"
|
|
||||||
name="email"
|
|
||||||
maxLength={254}
|
|
||||||
value={fields.email}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
validated={
|
|
||||||
this.state.errors.email !== ""
|
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
|
||||||
></TextInput>
|
|
||||||
</FormGroup> }
|
|
||||||
{this.isUpdateEmailFeatureEnabled && <FormGroup
|
|
||||||
label={Msg.localize('email')}
|
|
||||||
fieldId="email-address"
|
|
||||||
>
|
|
||||||
<InputGroup>
|
|
||||||
<TextInput
|
|
||||||
isDisabled
|
|
||||||
type="email"
|
|
||||||
id="email-address"
|
|
||||||
name="email"
|
|
||||||
value={fields.email}
|
|
||||||
>
|
|
||||||
</TextInput>
|
|
||||||
{this.isUpdateEmailActionEnabled && (!this.isRegistrationEmailAsUsername || this.isEditUserNameAllowed) &&
|
|
||||||
<KeycloakContext.Consumer>
|
|
||||||
{ (keycloak) => (
|
|
||||||
<Button id="update-email-btn"
|
|
||||||
variant="link"
|
|
||||||
onClick={() => this.handleEmailUpdate(keycloak!)}
|
|
||||||
icon={<ExternalLinkSquareAltIcon/>}
|
|
||||||
iconPosition="right">
|
|
||||||
<Msg msgKey="updateEmail" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</KeycloakContext.Consumer>
|
|
||||||
}
|
|
||||||
</InputGroup>
|
|
||||||
</FormGroup> }
|
|
||||||
<FormGroup
|
|
||||||
label={Msg.localize("firstName")}
|
|
||||||
fieldId="first-name"
|
|
||||||
helperTextInvalid={this.state.errors.firstName}
|
|
||||||
validated={
|
|
||||||
this.state.errors.firstName !== ""
|
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
isRequired
|
|
||||||
type="text"
|
|
||||||
id="first-name"
|
|
||||||
name="firstName"
|
|
||||||
maxLength={254}
|
|
||||||
value={fields.firstName}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
validated={
|
|
||||||
this.state.errors.firstName !== ""
|
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
|
||||||
></TextInput>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup
|
|
||||||
label={Msg.localize("lastName")}
|
|
||||||
fieldId="last-name"
|
|
||||||
helperTextInvalid={this.state.errors.lastName}
|
|
||||||
validated={
|
|
||||||
this.state.errors.lastName !== ""
|
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
isRequired
|
|
||||||
type="text"
|
|
||||||
id="last-name"
|
|
||||||
name="lastName"
|
|
||||||
maxLength={254}
|
|
||||||
value={fields.lastName}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
validated={
|
|
||||||
this.state.errors.lastName !== ""
|
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
|
||||||
></TextInput>
|
|
||||||
</FormGroup>
|
|
||||||
{features.isInternationalizationEnabled && (
|
|
||||||
<FormGroup
|
|
||||||
label={Msg.localize("selectLocale")}
|
|
||||||
isRequired
|
|
||||||
fieldId="locale"
|
|
||||||
>
|
|
||||||
<LocaleSelector
|
|
||||||
id="locale-selector"
|
|
||||||
value={fields.attributes!.locale || ""}
|
|
||||||
onChange={(value) =>
|
|
||||||
this.setState({
|
|
||||||
errors: this.state.errors,
|
|
||||||
formFields: {
|
|
||||||
...this.state.formFields,
|
|
||||||
attributes: {
|
|
||||||
...this.state.formFields.attributes,
|
|
||||||
locale: [value],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
<ActionGroup>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
id="save-btn"
|
|
||||||
variant="primary"
|
|
||||||
isDisabled={
|
|
||||||
Object.values(this.state.errors).filter((e) => e !== "")
|
|
||||||
.length !== 0
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Msg msgKey="doSave" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
id="cancel-btn"
|
|
||||||
variant="link"
|
|
||||||
onClick={this.handleCancel}
|
|
||||||
>
|
|
||||||
<Msg msgKey="doCancel" />
|
|
||||||
</Button>
|
|
||||||
</ActionGroup>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
{this.isDeleteAccountAllowed && (
|
|
||||||
<div id="delete-account" style={{ marginTop: "30px" }}>
|
|
||||||
<ExpandableSection toggleText={Msg.localize("deleteAccount")}>
|
|
||||||
<Grid hasGutter>
|
|
||||||
<GridItem span={6}>
|
|
||||||
<p>
|
|
||||||
<Msg msgKey="deleteAccountWarning" />
|
|
||||||
</p>
|
|
||||||
</GridItem>
|
|
||||||
<GridItem span={4}>
|
|
||||||
<KeycloakContext.Consumer>
|
|
||||||
{(keycloak) => (
|
|
||||||
<Button
|
|
||||||
id="delete-account-btn"
|
|
||||||
variant="danger"
|
|
||||||
onClick={() => this.handleDelete(keycloak!)}
|
|
||||||
className="delete-button"
|
|
||||||
>
|
|
||||||
<Msg msgKey="doDelete" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</KeycloakContext.Consumer>
|
|
||||||
</GridItem>
|
|
||||||
<GridItem span={2}></GridItem>
|
|
||||||
</Grid>
|
|
||||||
</ExpandableSection>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</PageSection>
|
|
||||||
</ContentPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private UsernameInput = () => (
|
|
||||||
<TextInput
|
|
||||||
isRequired
|
|
||||||
type="text"
|
|
||||||
id="user-name"
|
|
||||||
name="username"
|
|
||||||
maxLength={254}
|
|
||||||
value={this.state.formFields.username}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
validated={this.state.errors.username !== '' ? ValidatedOptions.error : ValidatedOptions.default}
|
|
||||||
>
|
|
||||||
</TextInput>
|
|
||||||
);
|
|
||||||
|
|
||||||
private RestrictedUsernameInput = () => (
|
|
||||||
<TextInput
|
|
||||||
isReadOnly
|
|
||||||
type="text"
|
|
||||||
id="user-name"
|
|
||||||
name="username"
|
|
||||||
value={this.state.formFields.username}
|
|
||||||
>
|
|
||||||
</TextInput>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,91 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
Button,
|
|
||||||
EmptyState,
|
|
||||||
EmptyStateVariant,
|
|
||||||
EmptyStateIcon,
|
|
||||||
EmptyStateBody,
|
|
||||||
TitleSizes
|
|
||||||
} 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="h5" size={TitleSizes.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);
|
|
|
@ -1,294 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
DescriptionList,
|
|
||||||
DescriptionListTerm,
|
|
||||||
DescriptionListGroup,
|
|
||||||
DescriptionListDescription,
|
|
||||||
Grid,
|
|
||||||
GridItem,
|
|
||||||
Button,
|
|
||||||
PageSection,
|
|
||||||
PageSectionVariants,
|
|
||||||
Stack,
|
|
||||||
StackItem,
|
|
||||||
SplitItem,
|
|
||||||
Split,
|
|
||||||
TextContent
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { InfoAltIcon, CheckIcon, 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[];
|
|
||||||
logoUri: string;
|
|
||||||
policyUri: string;
|
|
||||||
tosUri: 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/" + encodeURIComponent(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')}
|
|
||||||
introMessage={Msg.localize('applicationsPageSubTitle')}
|
|
||||||
>
|
|
||||||
<PageSection isFilled variant={PageSectionVariants.light}>
|
|
||||||
|
|
||||||
<Stack hasGutter>
|
|
||||||
<DataList id="applications-list" aria-label={Msg.localize('applicationsPageTitle')}>
|
|
||||||
<DataListItem id="applications-list-header" aria-labelledby="Columns names">
|
|
||||||
<DataListItemRow>
|
|
||||||
// invisible toggle allows headings to line up properly
|
|
||||||
<span style={{ visibility: 'hidden', height: 55 }}>
|
|
||||||
<DataListToggle
|
|
||||||
isExpanded={false}
|
|
||||||
id='applications-list-header-invisible-toggle'
|
|
||||||
aria-controls="hidden"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<DataListItemCells
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell key='applications-list-client-id-header' width={2} className="pf-u-pt-md">
|
|
||||||
<strong><Msg msgKey='applicationName' /></strong>
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key='applications-list-app-type-header' width={2} className="pf-u-pt-md">
|
|
||||||
<strong><Msg msgKey='applicationType' /></strong>
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key='applications-list-status' width={2} className="pf-u-pt-md">
|
|
||||||
<strong><Msg msgKey='status' /></strong>
|
|
||||||
</DataListCell>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</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 className="pf-u-align-items-center">
|
|
||||||
<DataListToggle
|
|
||||||
onClick={() => this.onToggle(appIndex)}
|
|
||||||
isExpanded={this.state.isRowOpen[appIndex]}
|
|
||||||
id={this.elementId('toggle', application)}
|
|
||||||
aria-controls={this.elementId("expandable", application)}
|
|
||||||
/>
|
|
||||||
<DataListItemCells
|
|
||||||
className="pf-u-align-items-center"
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell id={this.elementId('name', application)} width={2} key={'app-' + appIndex}>
|
|
||||||
<Button className="pf-u-pl-0 title-case" component="a" variant="link" onClick={() => window.open(application.effectiveUrl)}>
|
|
||||||
{application.clientName || application.clientId} <ExternalLinkAltIcon/>
|
|
||||||
</Button>
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell id={this.elementId('internal', application)} width={2} key={'internal-' + appIndex}>
|
|
||||||
{application.userConsentRequired ? Msg.localize('thirdPartyApp') : Msg.localize('internalApp')}
|
|
||||||
{application.offlineAccess ? ', ' + Msg.localize('offlineAccess') : ''}
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell id={this.elementId('status', application)} width={2} key={'status-' + appIndex}>
|
|
||||||
{application.inUse ? Msg.localize('inUse') : Msg.localize('notInUse')}
|
|
||||||
</DataListCell>
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</DataListItemRow>
|
|
||||||
|
|
||||||
<DataListContent
|
|
||||||
className="pf-u-pl-35xl"
|
|
||||||
hasNoPadding={false}
|
|
||||||
aria-label={Msg.localize('applicationDetails')}
|
|
||||||
id={this.elementId("expandable", application)}
|
|
||||||
isHidden={!this.state.isRowOpen[appIndex]}
|
|
||||||
>
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('client')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription>{application.clientId}</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
{application.description &&
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('description')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription>{application.description}</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
}
|
|
||||||
{application.effectiveUrl &&
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>URL</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription id={this.elementId("effectiveurl", application)}>
|
|
||||||
{application.effectiveUrl.split('"')}
|
|
||||||
</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
}
|
|
||||||
{application.consent &&
|
|
||||||
<React.Fragment>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('hasAccessTo')}</DescriptionListTerm>
|
|
||||||
{application.consent.grantedScopes.map((scope: GrantedScope, scopeIndex: number) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={'scope-' + scopeIndex} >
|
|
||||||
<DescriptionListDescription><CheckIcon />{Msg.localize(scope.name)}</DescriptionListDescription>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</DescriptionListGroup>
|
|
||||||
{application.tosUri &&
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('termsOfService')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription>{application.tosUri}</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
}
|
|
||||||
{application.policyUri &&
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('policy')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription>{application.policyUri }</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
}
|
|
||||||
{application.logoUri &&
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('logo')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription><img src={application.logoUri} /></DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
}
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('accessGrantedOn') + ': '}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription>
|
|
||||||
{new Intl.DateTimeFormat(locale, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
second: 'numeric'
|
|
||||||
}).format(application.consent.createDate)}
|
|
||||||
</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
{(application.consent || application.offlineAccess) &&
|
|
||||||
<Grid hasGutter>
|
|
||||||
<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>
|
|
||||||
</Stack>
|
|
||||||
</PageSection>
|
|
||||||
</ContentPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,323 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 {
|
|
||||||
Button,
|
|
||||||
DataList,
|
|
||||||
DataListItem,
|
|
||||||
DataListItemRow,
|
|
||||||
DataListContent,
|
|
||||||
DescriptionList,
|
|
||||||
DescriptionListTerm,
|
|
||||||
DescriptionListDescription,
|
|
||||||
DescriptionListGroup,
|
|
||||||
Grid,
|
|
||||||
GridItem,
|
|
||||||
Label,
|
|
||||||
PageSection,
|
|
||||||
PageSectionVariants,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SplitItem,
|
|
||||||
Split
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DesktopIcon,
|
|
||||||
MobileAltIcon,
|
|
||||||
SyncAltIcon,
|
|
||||||
} 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/" + encodeURIComponent(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, element: string='session'): string {
|
|
||||||
return `${element}-${session.id.substring(0,7)}-${item}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private findDeviceTypeIcon(session: Session, device: Device): React.ReactNode {
|
|
||||||
const deviceType: boolean = device.mobile;
|
|
||||||
if (deviceType === true) return (<MobileAltIcon id={this.elementId('icon-mobile', session, 'device')} />);
|
|
||||||
|
|
||||||
return (<DesktopIcon id={this.elementId('icon-desktop', session, 'device')} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
introMessage="signedInDevicesExplanation"
|
|
||||||
>
|
|
||||||
<PageSection isFilled variant={PageSectionVariants.light}>
|
|
||||||
<Split hasGutter className="pf-u-mb-lg">
|
|
||||||
<SplitItem isFilled>
|
|
||||||
<div id="signedInDevicesTitle" className="pf-c-content"><Title headingLevel="h2" size="xl"><Msg msgKey="signedInDevices"/></Title></div>
|
|
||||||
</SplitItem>
|
|
||||||
<SplitItem>
|
|
||||||
<Tooltip content={<Msg msgKey="refreshPage" />}>
|
|
||||||
<Button
|
|
||||||
aria-describedby="refresh page"
|
|
||||||
id="refresh-page"
|
|
||||||
variant="link"
|
|
||||||
onClick={this.fetchDevices.bind(this)}
|
|
||||||
icon={<SyncAltIcon />}
|
|
||||||
>
|
|
||||||
<Msg msgKey="refresh"/>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</SplitItem>
|
|
||||||
<SplitItem>
|
|
||||||
<KeycloakContext.Consumer>
|
|
||||||
{ (keycloak) => (
|
|
||||||
this.isShowSignOutAll(this.state.devices) &&
|
|
||||||
<ContinueCancelModal buttonTitle='signOutAllDevices'
|
|
||||||
buttonId='sign-out-all'
|
|
||||||
modalTitle='signOutAllDevices'
|
|
||||||
modalMessage='signOutAllDevicesWarning'
|
|
||||||
onContinue={() => this.signOutAll(keycloak!)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</KeycloakContext.Consumer>
|
|
||||||
</SplitItem>
|
|
||||||
</Split>
|
|
||||||
<DataList className="signed-in-device-list" aria-label={Msg.localize('signedInDevices')}>
|
|
||||||
<DataListItem aria-labelledby='sessions' id='device-activity-sessions'>
|
|
||||||
{this.state.devices.map((device: Device, deviceIndex: number) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
{device.sessions.map((session: Session, sessionIndex: number) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={'device-' + deviceIndex + '-session-' + sessionIndex}>
|
|
||||||
<DataListItemRow>
|
|
||||||
<DataListContent aria-label="device-sessions-content" isHidden={false} className="pf-u-flex-grow-1">
|
|
||||||
<Grid id={this.elementId("item",session)} className="signed-in-device-grid" hasGutter>
|
|
||||||
<GridItem className="device-icon" span={1} rowSpan={2}>
|
|
||||||
<span>{this.findDeviceTypeIcon(session, device)}</span>
|
|
||||||
</GridItem>
|
|
||||||
<GridItem sm={8} md={9} span={10}>
|
|
||||||
<span id={this.elementId('browser', session)} className="pf-u-mr-md session-title">{this.findOS(device)} {this.findOSVersion(device)} / {session.browser}</span>
|
|
||||||
{session.current &&
|
|
||||||
<Label color="green" id={this.elementId('current-badge', session)}><Msg msgKey="currentSession" /></Label>}
|
|
||||||
</GridItem>
|
|
||||||
<GridItem className="pf-u-text-align-right" sm={3} md={2} span={1}>
|
|
||||||
{!session.current &&
|
|
||||||
<ContinueCancelModal buttonTitle='doSignOut'
|
|
||||||
buttonId={this.elementId('sign-out', session)}
|
|
||||||
modalTitle='doSignOut'
|
|
||||||
buttonVariant='secondary'
|
|
||||||
modalMessage='signOutWarning'
|
|
||||||
onContinue={() => this.signOutSession(device, session)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</GridItem>
|
|
||||||
<GridItem span={11}>
|
|
||||||
<DescriptionList columnModifier={{ sm: '2Col', lg: '3Col' }}>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('ipAddress')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription id={this.elementId('ip', session)}>{session.ipAddress}</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('lastAccessedOn')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription id={this.elementId('last-access', session)}>{this.time(session.lastAccess)}</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('clients')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription id={this.elementId('clients', session)}>{this.makeClientsString(session.clients)}</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('started')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription id={this.elementId('started', session)}>{this.time(session.started)}</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{Msg.localize('expires')}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription id={this.elementId('expires', session)}>{this.time(session.expires)}</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
</DescriptionList>
|
|
||||||
</GridItem>
|
|
||||||
</Grid>
|
|
||||||
</DataListContent>
|
|
||||||
</DataListItemRow>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</DataListItem>
|
|
||||||
</DataList>
|
|
||||||
</PageSection>
|
|
||||||
</ContentPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,168 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
DataList,
|
|
||||||
DataListItem,
|
|
||||||
DataListItemRow,
|
|
||||||
DataListCell,
|
|
||||||
DataListItemCells,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { ContentPage } from '../ContentPage';
|
|
||||||
import { HttpResponse } from '../../account-service/account.service';
|
|
||||||
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
|
|
||||||
import { Msg } from '../../widgets/Msg';
|
|
||||||
|
|
||||||
export interface GroupsPageProps {
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GroupsPageState {
|
|
||||||
groups: Group[];
|
|
||||||
directGroups: Group[];
|
|
||||||
isDirectMembership: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Group {
|
|
||||||
id?: string;
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GroupsPage extends React.Component<GroupsPageProps, GroupsPageState> {
|
|
||||||
static contextType = AccountServiceContext;
|
|
||||||
context: React.ContextType<typeof AccountServiceContext>;
|
|
||||||
|
|
||||||
public constructor(props: GroupsPageProps, context: React.ContextType<typeof AccountServiceContext>) {
|
|
||||||
super(props);
|
|
||||||
this.context = context;
|
|
||||||
this.state = {
|
|
||||||
groups: [],
|
|
||||||
directGroups: [],
|
|
||||||
isDirectMembership: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.fetchGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchGroups(): void {
|
|
||||||
this.context!.doGet<Group[]>("/groups")
|
|
||||||
.then((response: HttpResponse<Group[]>) => {
|
|
||||||
const directGroups = response.data || [];
|
|
||||||
const groups = [...directGroups];
|
|
||||||
const groupsPaths = directGroups.map(s => s.path);
|
|
||||||
directGroups.forEach((el) => this.getParents(el, groups, groupsPaths))
|
|
||||||
this.setState({
|
|
||||||
groups: groups,
|
|
||||||
directGroups: directGroups
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getParents(el: Group, groups: Group[], groupsPaths: string[]): void {
|
|
||||||
const parentPath = el.path.slice(0, el.path.lastIndexOf('/'));
|
|
||||||
if (parentPath && (groupsPaths.indexOf(parentPath) === -1)) {
|
|
||||||
|
|
||||||
el = {
|
|
||||||
name: parentPath.slice(parentPath.lastIndexOf('/')+1),
|
|
||||||
path: parentPath
|
|
||||||
};
|
|
||||||
groups.push(el);
|
|
||||||
groupsPaths.push(parentPath);
|
|
||||||
|
|
||||||
this.getParents(el, groups, groupsPaths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private changeDirectMembership = (checked: boolean,event: React.FormEvent<HTMLInputElement> )=> {
|
|
||||||
this.setState({
|
|
||||||
isDirectMembership: checked
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private emptyGroup(): React.ReactNode {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataListItem key='emptyItem' aria-labelledby="empty-item">
|
|
||||||
<DataListItemRow key='emptyRow'>
|
|
||||||
<DataListItemCells dataListCells={[
|
|
||||||
<DataListCell key='empty'><strong><Msg msgKey='noGroupsText' /></strong></DataListCell>
|
|
||||||
]} />
|
|
||||||
</DataListItemRow>
|
|
||||||
</DataListItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderGroupList(group: Group, appIndex: number): React.ReactNode {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataListItem id={`${appIndex}-group`} key={'group-' + appIndex} aria-labelledby="groups-list" >
|
|
||||||
<DataListItemRow>
|
|
||||||
<DataListItemCells
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell id={`${appIndex}-group-name`} width={2} key={'name-' + appIndex}>
|
|
||||||
{group.name}
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell id={`${appIndex}-group-path`} width={2} key={'path-' + appIndex}>
|
|
||||||
{group.path}
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell id={`${appIndex}-group-directMembership`} width={2} key={'directMembership-' + appIndex}>
|
|
||||||
<Checkbox id={`${appIndex}-checkbox-directMembership`} isChecked={group.id != null} isDisabled={true} />
|
|
||||||
</DataListCell>
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</DataListItemRow>
|
|
||||||
|
|
||||||
</DataListItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
return (
|
|
||||||
<ContentPage title={Msg.localize('groupLabel')}>
|
|
||||||
<DataList id="groups-list" aria-label={Msg.localize('groupLabel')} isCompact>
|
|
||||||
<DataListItem id="groups-list-header" aria-labelledby="Columns names">
|
|
||||||
<DataListItemRow >
|
|
||||||
<DataListItemCells
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell key='directMembership-header' >
|
|
||||||
<Checkbox
|
|
||||||
label={Msg.localize('directMembership')}
|
|
||||||
id="directMembership-checkbox"
|
|
||||||
isChecked={this.state.isDirectMembership}
|
|
||||||
onChange={this.changeDirectMembership}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</DataListCell>
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</DataListItemRow>
|
|
||||||
</DataListItem>
|
|
||||||
<DataListItem id="groups-list-header" aria-labelledby="Columns names">
|
|
||||||
<DataListItemRow >
|
|
||||||
<DataListItemCells
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell key='group-name-header' width={2}>
|
|
||||||
<strong><Msg msgKey='Name' /></strong>
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key='group-path-header' width={2}>
|
|
||||||
<strong><Msg msgKey='path' /></strong>
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key='group-direct-membership-header' width={2}>
|
|
||||||
<strong><Msg msgKey='directMembership' /></strong>
|
|
||||||
</DataListCell>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</DataListItemRow>
|
|
||||||
</DataListItem>
|
|
||||||
{this.state.groups.length === 0
|
|
||||||
? this.emptyGroup()
|
|
||||||
: (this.state.isDirectMembership ? this.state.directGroups.map((group: Group, appIndex: number) =>
|
|
||||||
this.renderGroupList(group, appIndex)
|
|
||||||
) : this.state.groups.map((group: Group, appIndex: number) =>
|
|
||||||
this.renderGroupList(group, appIndex)))}
|
|
||||||
</DataList>
|
|
||||||
</ContentPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,267 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 {
|
|
||||||
Button,
|
|
||||||
DataList,
|
|
||||||
DataListAction,
|
|
||||||
DataListItemCells,
|
|
||||||
DataListCell,
|
|
||||||
DataListItemRow,
|
|
||||||
Label,
|
|
||||||
PageSection,
|
|
||||||
PageSectionVariants,
|
|
||||||
Split,
|
|
||||||
SplitItem,
|
|
||||||
Stack,
|
|
||||||
StackItem,
|
|
||||||
Title,
|
|
||||||
DataListItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import {
|
|
||||||
BitbucketIcon,
|
|
||||||
CubeIcon,
|
|
||||||
GitlabIcon,
|
|
||||||
LinkIcon,
|
|
||||||
OpenshiftIcon,
|
|
||||||
PaypalIcon,
|
|
||||||
UnlinkIcon,
|
|
||||||
FacebookIcon,
|
|
||||||
GoogleIcon,
|
|
||||||
InstagramIcon,
|
|
||||||
MicrosoftIcon,
|
|
||||||
TwitterIcon,
|
|
||||||
StackOverflowIcon,
|
|
||||||
LinkedinIcon,
|
|
||||||
GithubIcon
|
|
||||||
} 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')}>
|
|
||||||
<PageSection isFilled variant={PageSectionVariants.light}>
|
|
||||||
<Stack hasGutter>
|
|
||||||
<StackItem>
|
|
||||||
<Title headingLevel="h2" className="pf-u-mb-lg" size='xl'>
|
|
||||||
<Msg msgKey='linkedLoginProviders'/>
|
|
||||||
</Title>
|
|
||||||
<DataList id="linked-idps" aria-label={Msg.localize('linkedLoginProviders')}>
|
|
||||||
{this.makeRows(this.state.linkedAccounts, true)}
|
|
||||||
</DataList>
|
|
||||||
</StackItem>
|
|
||||||
<StackItem>
|
|
||||||
<Title headingLevel="h2" className="pf-u-mt-xl pf-u-mb-lg" size='xl'>
|
|
||||||
<Msg msgKey='unlinkedLoginProviders'/>
|
|
||||||
</Title>
|
|
||||||
<DataList id="unlinked-idps" aria-label={Msg.localize('unlinkedLoginProviders')}>
|
|
||||||
{this.makeRows(this.state.unLinkedAccounts, false)}
|
|
||||||
</DataList>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
</PageSection>
|
|
||||||
</ContentPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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={Msg.localize('isEmptyMessage')}>
|
|
||||||
<DataListItemRow key='emptyRow'>
|
|
||||||
<DataListItemCells dataListCells={[
|
|
||||||
<DataListCell key='empty'>{isEmptyMessage}</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={Msg.localize('linkedAccountsTitle')}>
|
|
||||||
<DataListItemRow key={account.providerName}>
|
|
||||||
<DataListItemCells
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell key='idp'>
|
|
||||||
<Split>
|
|
||||||
<SplitItem className="pf-u-mr-sm">{this.findIcon(account)}</SplitItem>
|
|
||||||
<SplitItem className="pf-u-my-xs" isFilled><span id={`${account.providerAlias}-idp-name`}>{account.displayName}</span></SplitItem>
|
|
||||||
</Split>
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key='label'>
|
|
||||||
<Split>
|
|
||||||
<SplitItem className="pf-u-my-xs" isFilled><span id={`${account.providerAlias}-idp-label`}>{this.label(account)}</span></SplitItem>
|
|
||||||
</Split>
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key='username' width={5}>
|
|
||||||
<Split>
|
|
||||||
<SplitItem className="pf-u-my-xs" isFilled><span id={`${account.providerAlias}-idp-username`}>{account.linkedUsername}</span></SplitItem>
|
|
||||||
</Split>
|
|
||||||
</DataListCell>,
|
|
||||||
]}/>
|
|
||||||
<DataListAction aria-labelledby={Msg.localize('link')} aria-label={Msg.localize('unLink')} id='setPasswordAction'>
|
|
||||||
{isLinked && <Button id={`${account.providerAlias}-idp-unlink`} variant='link' onClick={() => this.unLinkAccount(account)}><UnlinkIcon size='sm'/> <Msg msgKey='unLink'/></Button>}
|
|
||||||
{!isLinked && <Button id={`${account.providerAlias}-idp-link`} variant='link' onClick={() => this.linkAccount(account)}><LinkIcon size='sm'/> <Msg msgKey='link'/></Button>}
|
|
||||||
</DataListAction>
|
|
||||||
</DataListItemRow>
|
|
||||||
</DataListItem>
|
|
||||||
))
|
|
||||||
|
|
||||||
} </>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private label(account: LinkedAccount): React.ReactNode {
|
|
||||||
if (account.social) {
|
|
||||||
return (<Label color="blue"><Msg msgKey='socialLogin'/></Label>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<Label color="green"><Msg msgKey='systemDefined'/></Label>);
|
|
||||||
}
|
|
||||||
|
|
||||||
private findIcon(account: LinkedAccount): React.ReactNode {
|
|
||||||
const socialIconId = `${account.providerAlias}-idp-icon-social`;
|
|
||||||
console.log(account);
|
|
||||||
switch (true) {
|
|
||||||
case account.providerName.toLowerCase().includes('linkedin'):
|
|
||||||
return <LinkedinIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('facebook'):
|
|
||||||
return <FacebookIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('google'):
|
|
||||||
return <GoogleIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('instagram'):
|
|
||||||
return <InstagramIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('microsoft'):
|
|
||||||
return <MicrosoftIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('bitbucket'):
|
|
||||||
return <BitbucketIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('twitter'):
|
|
||||||
return <TwitterIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('openshift'):
|
|
||||||
// return <div className="idp-icon-social" id="openshift-idp-icon-social" />;
|
|
||||||
return <OpenshiftIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('gitlab'):
|
|
||||||
return <GitlabIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('github'):
|
|
||||||
return <GithubIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('paypal'):
|
|
||||||
return <PaypalIcon id={socialIconId} size='lg'/>;
|
|
||||||
case account.providerName.toLowerCase().includes('stackoverflow'):
|
|
||||||
return <StackOverflowIcon id={socialIconId} size='lg'/>;
|
|
||||||
case (account.providerName !== '' && account.social):
|
|
||||||
return <div className="idp-icon-social" id={socialIconId}/>;
|
|
||||||
default:
|
|
||||||
return <CubeIcon id={`${account.providerAlias}-idp-icon-default`} size='lg'/>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const LinkedAccountsPagewithRouter = withRouter(LinkedAccountsPage);
|
|
||||||
export {LinkedAccountsPagewithRouter as LinkedAccountsPage};
|
|
|
@ -1,53 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,146 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
ModalVariant
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { OkIcon } from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
import { Resource, Permission, Permissions, 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 = { 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/${encodeURIComponent(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}
|
|
||||||
variant={ModalVariant.large}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,269 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
PageSection,
|
|
||||||
PageSectionVariants,
|
|
||||||
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<Permission[]>('/resources/' + resource._id + '/permissions/requests')
|
|
||||||
.then((response) => {
|
|
||||||
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 hasGutter>
|
|
||||||
<StackItem isFilled><span/></StackItem>
|
|
||||||
<StackItem isFilled>
|
|
||||||
<Level hasGutter>
|
|
||||||
<LevelItem>
|
|
||||||
<TextInput value={this.state.nameFilter} onChange={this.handleFilterRequest} id={'filter-' + title} type="text" placeholder={Msg.localize('filterByName')} iconVariant="search"/>
|
|
||||||
</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)}>
|
|
||||||
<PageSection variant={PageSectionVariants.light}>
|
|
||||||
<Tabs activeKey={this.state.activeTabKey} onSelect={(event, index) => this.handleTabClick(index as number)}>
|
|
||||||
{this.makeTab(0, 'myResources', this.state.myResources, false)}
|
|
||||||
{this.makeTab(1, 'sharedwithMe', this.state.sharedWithMe, true)}
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<Level hasGutter>
|
|
||||||
<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>
|
|
||||||
</PageSection>
|
|
||||||
</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 = (tabIndex: number) => {
|
|
||||||
if (this.state.activeTabKey === tabIndex) return;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
nameFilter: '',
|
|
||||||
activeTabKey: tabIndex
|
|
||||||
}, () => {this.fetchInitialResources()});
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,180 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
ModalVariant
|
|
||||||
} 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, Permissions } 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 = { 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/${encodeURIComponent(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/${encodeURIComponent(id)}/permissions`, [{ username: username, scopes: userScopes }] )
|
|
||||||
ContentAlert.success(Msg.localize('shareSuccess'));
|
|
||||||
this.props.onClose();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Could not update permissions', (e as any).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}
|
|
||||||
variant={ModalVariant.large}
|
|
||||||
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 hasGutter>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { Select, SelectOption, SelectVariant, SelectOptionObject } from '@patternfly/react-core';
|
|
||||||
import { Scope } from './resource-model';
|
|
||||||
import { Msg } from '../../widgets/Msg';
|
|
||||||
|
|
||||||
interface PermissionSelectState {
|
|
||||||
selected: ScopeValue[];
|
|
||||||
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, value: string | SelectOptionObject): void => {
|
|
||||||
const { selected } = this.state;
|
|
||||||
const { onSelect } = this.props;
|
|
||||||
|
|
||||||
if (!(value instanceof ScopeValue)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selected.includes(value)) {
|
|
||||||
this.setState(
|
|
||||||
prevState => ({ selected: prevState.selected.filter(item => item !== value) }),
|
|
||||||
() => onSelect(this.state.selected.map(sv => sv.value))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.setState(
|
|
||||||
prevState => ({ selected: [...prevState.selected, value] }),
|
|
||||||
() => 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>
|
|
||||||
<Msg msgKey='selectPermissions' />
|
|
||||||
</span>
|
|
||||||
<Select
|
|
||||||
maxHeight={300}
|
|
||||||
direction={this.props.direction || 'down'}
|
|
||||||
variant={SelectVariant.typeaheadMulti}
|
|
||||||
typeAheadAriaLabel={Msg.localize("selectPermissions")}
|
|
||||||
onToggle={this.onToggle}
|
|
||||||
onSelect={this.onSelect}
|
|
||||||
onClear={this.clearSelection}
|
|
||||||
selections={selected}
|
|
||||||
isOpen={isExpanded}
|
|
||||||
aria-labelledby={titleId}
|
|
||||||
placeholderText={Msg.localize("selectPermissions")}
|
|
||||||
menuAppendTo="parent"
|
|
||||||
>
|
|
||||||
{this.state.scopes}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,339 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
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<Permission[]>(`/resources/${encodeURIComponent(resource._id)}/permissions`)
|
|
||||||
.then((response) => {
|
|
||||||
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/${encodeURIComponent(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
|
|
||||||
visibility={{ lg: 'hidden' }}
|
|
||||||
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}`}
|
|
||||||
visibility={{ default: 'hidden', lg: 'visible' }}
|
|
||||||
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
|
|
||||||
hasNoPadding={false}
|
|
||||||
aria-label="Session Details"
|
|
||||||
id={'ex-expand' + row}
|
|
||||||
isHidden={!this.state.isRowOpen[row]}
|
|
||||||
>
|
|
||||||
<Level hasGutter>
|
|
||||||
<LevelItem><span /></LevelItem>
|
|
||||||
<LevelItem id={'shared-with-user-message-' + row}>{this.sharedWithUsersMessage(row)}</LevelItem>
|
|
||||||
<LevelItem><span /></LevelItem>
|
|
||||||
</Level>
|
|
||||||
</DataListContent>
|
|
||||||
</DataListItem>
|
|
||||||
))}
|
|
||||||
</DataList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,239 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
Form,
|
|
||||||
FormGroup,
|
|
||||||
Gallery,
|
|
||||||
GalleryItem,
|
|
||||||
Modal,
|
|
||||||
Stack,
|
|
||||||
StackItem,
|
|
||||||
TextInput,
|
|
||||||
ModalVariant
|
|
||||||
} 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:any = {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/${encodeURIComponent(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/${encodeURIComponent(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}
|
|
||||||
variant={ModalVariant.large}
|
|
||||||
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 hasGutter>
|
|
||||||
<StackItem isFilled>
|
|
||||||
<Form>
|
|
||||||
<FormGroup
|
|
||||||
label="Add users to share your resource with"
|
|
||||||
type="string"
|
|
||||||
helperTextInvalid={Msg.localize('resourceAlreadyShared')}
|
|
||||||
fieldId="username"
|
|
||||||
isRequired
|
|
||||||
>
|
|
||||||
<Gallery hasGutter>
|
|
||||||
<GalleryItem>
|
|
||||||
<TextInput
|
|
||||||
value={this.state.usernameInput}
|
|
||||||
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 categoryName={Msg.localize('shareWith')}>
|
|
||||||
{this.state.usernames.map((currentChip: string) => (
|
|
||||||
<Chip key={currentChip} onClick={() => this.handleDeleteUsername(currentChip)}>
|
|
||||||
{currentChip}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
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 categoryName={Msg.localize('permissions')}>
|
|
||||||
{
|
|
||||||
resource.scopes.map(scope => (
|
|
||||||
<Chip key={scope.name} isReadOnly>
|
|
||||||
{scope.displayName || scope.name}
|
|
||||||
</Chip>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ChipGroup>}
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key={'pending-' + row} width={2}>
|
|
||||||
{resource.shareRequests.length > 0 &&
|
|
||||||
<ChipGroup categoryName={Msg.localize('pending')}>
|
|
||||||
{
|
|
||||||
(resource.shareRequests[0].scopes as Scope[]).map(scope => (
|
|
||||||
<Chip key={scope.name} isReadOnly>
|
|
||||||
{scope.displayName || scope.name}
|
|
||||||
</Chip>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
</DataListCell>
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</DataListItemRow>
|
|
||||||
</DataListItem>
|
|
||||||
))}
|
|
||||||
</DataList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Permissions {
|
|
||||||
permissions: Permission[];
|
|
||||||
row?: number;
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
* 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);
|
|
|
@ -1,525 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 {
|
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
DataList,
|
|
||||||
DataListAction,
|
|
||||||
DataListItemCells,
|
|
||||||
DataListCell,
|
|
||||||
DataListItem,
|
|
||||||
DataListItemRow,
|
|
||||||
EmptyState,
|
|
||||||
EmptyStateVariant,
|
|
||||||
EmptyStateBody,
|
|
||||||
Split,
|
|
||||||
SplitItem,
|
|
||||||
Title,
|
|
||||||
Dropdown,
|
|
||||||
DropdownPosition,
|
|
||||||
KebabToggle,
|
|
||||||
PageSection,
|
|
||||||
PageSectionVariants
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
|
|
||||||
import { AIACommand } from "../../util/AIACommand";
|
|
||||||
import TimeUtil from "../../util/TimeUtil";
|
|
||||||
import {
|
|
||||||
HttpResponse,
|
|
||||||
AccountServiceClient,
|
|
||||||
} from "../../account-service/account.service";
|
|
||||||
import { AccountServiceContext } from "../../account-service/AccountServiceContext";
|
|
||||||
import { ContinueCancelModal } from "../../widgets/ContinueCancelModal";
|
|
||||||
import { Features } from "../../widgets/features";
|
|
||||||
import { Msg } from "../../widgets/Msg";
|
|
||||||
import { ContentPage } from "../ContentPage";
|
|
||||||
import { ContentAlert } from "../ContentAlert";
|
|
||||||
import { KeycloakContext } from "../../keycloak-service/KeycloakContext";
|
|
||||||
import { KeycloakService } from "../../keycloak-service/keycloak.service";
|
|
||||||
import { css } from "@patternfly/react-styles";
|
|
||||||
|
|
||||||
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 CredMetadata {
|
|
||||||
infoMessage?: string;
|
|
||||||
warningMessageTitle?: string;
|
|
||||||
warningMessageDescription?: string;
|
|
||||||
credential: UserCredential;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserCredential {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
userLabel: string;
|
|
||||||
createdDate?: number;
|
|
||||||
strCreatedDate?: string;
|
|
||||||
credentialData?: 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;
|
|
||||||
userCredentialMetadatas: CredMetadata[];
|
|
||||||
open: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SigningInPageProps extends RouteComponentProps {}
|
|
||||||
|
|
||||||
interface SigningInPageState {
|
|
||||||
// Credential containers organized by category then type
|
|
||||||
credentialContainers: CredContainerMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getCredentialContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCredentialContainers(): void {
|
|
||||||
this.context!.doGet<CredentialContainer[]>("/credentials").then(
|
|
||||||
(response) => {
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRemove = (credentialId: string, userLabel: string) => {
|
|
||||||
this.context!.doDelete("/credentials/" + encodeURIComponent(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">
|
|
||||||
{this.renderCategories()}
|
|
||||||
</ContentPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderCategories(): React.ReactNode {
|
|
||||||
return Array.from(this.state.credentialContainers.keys()).map(
|
|
||||||
(category) => (
|
|
||||||
<PageSection key={category} variant={PageSectionVariants.light}>
|
|
||||||
<Title
|
|
||||||
id={`${category}-categ-title`}
|
|
||||||
headingLevel="h2"
|
|
||||||
size="xl"
|
|
||||||
>
|
|
||||||
<Msg msgKey={category} />
|
|
||||||
</Title>
|
|
||||||
{this.renderTypes(category!)}
|
|
||||||
</PageSection>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderTypes(category: CredCategory): React.ReactNode {
|
|
||||||
let credTypeMap: CredTypeMap = this.state.credentialContainers.get(
|
|
||||||
category
|
|
||||||
)!;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<KeycloakContext.Consumer>
|
|
||||||
{(keycloak) => (
|
|
||||||
<>
|
|
||||||
{Array.from(
|
|
||||||
credTypeMap.keys()
|
|
||||||
).map(
|
|
||||||
(
|
|
||||||
credType: CredType,
|
|
||||||
index: number,
|
|
||||||
typeArray: string[]
|
|
||||||
) => [
|
|
||||||
this.renderCredTypeTitle(
|
|
||||||
credTypeMap.get(credType)!,
|
|
||||||
keycloak!,
|
|
||||||
category
|
|
||||||
),
|
|
||||||
this.renderUserCredentials(
|
|
||||||
credTypeMap,
|
|
||||||
credType,
|
|
||||||
keycloak!
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</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 userCredentialMetadatas: CredMetadata[] = credContainer.userCredentialMetadatas;
|
|
||||||
const removeable: boolean = credContainer.removeable;
|
|
||||||
const type: string = credContainer.type;
|
|
||||||
const displayName: string = credContainer.displayName;
|
|
||||||
|
|
||||||
if (!userCredentialMetadatas || userCredentialMetadatas.length === 0) {
|
|
||||||
const localizedDisplayName = Msg.localize(displayName);
|
|
||||||
return (
|
|
||||||
<DataList aria-label={Msg.localize('notSetUp', [localizedDisplayName])} className="pf-u-mb-xl">
|
|
||||||
<DataListItem key='no-credentials-list-item' aria-labelledby={Msg.localize('notSetUp', [localizedDisplayName])}>
|
|
||||||
<DataListItemRow key='no-credentials-list-item-row' className="pf-u-align-items-center">
|
|
||||||
<DataListItemCells
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell key={'no-credentials-cell-0'}/>,
|
|
||||||
<EmptyState id={`${type}-not-set-up`} key={'no-credentials-cell-1'} variant={EmptyStateVariant.xs}>
|
|
||||||
<EmptyStateBody>
|
|
||||||
<Msg msgKey='notSetUp' params={[localizedDisplayName]}/>
|
|
||||||
</EmptyStateBody>
|
|
||||||
</EmptyState>,
|
|
||||||
<DataListCell key={'no-credentials-cell-2'}/>
|
|
||||||
]}/>
|
|
||||||
</DataListItemRow>
|
|
||||||
</DataListItem>
|
|
||||||
</DataList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
userCredentialMetadatas.forEach(credentialMetadata => {
|
|
||||||
let credential = credentialMetadata.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxWidth = { maxWidth: 689 } as React.CSSProperties;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment key='userCredentialMetadatas'> {
|
|
||||||
userCredentialMetadatas.map(credentialMetadata => (
|
|
||||||
<>
|
|
||||||
{(credentialMetadata.infoMessage && !credentialMetadata.warningMessageTitle && !credentialMetadata.warningMessageDescription) &&
|
|
||||||
<Alert variant="default" className="pf-u-mb-md" isInline isPlain title={Msg.localize(JSON.parse(credentialMetadata.infoMessage).key, JSON.parse(credentialMetadata.infoMessage).parameters)} />
|
|
||||||
}
|
|
||||||
{(credentialMetadata.warningMessageTitle && credentialMetadata.warningMessageDescription) &&
|
|
||||||
<Alert variant="warning" className="pf-u-mb-md" isInline title={Msg.localize(JSON.parse(credentialMetadata.warningMessageTitle).key, JSON.parse(credentialMetadata.warningMessageTitle).parameters)} style={maxWidth}>
|
|
||||||
|
|
||||||
<p>{Msg.localize(JSON.parse(credentialMetadata.warningMessageDescription).key, JSON.parse(credentialMetadata.warningMessageDescription).parameters)}</p>
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
<DataList aria-label="user credential" className="pf-u-mb-xl">
|
|
||||||
<DataListItem id={`${SigningInPage.credElementId(type, credentialMetadata.credential.id, 'row')}`} key={'credential-list-item-' + credentialMetadata.credential.id} aria-labelledby={'credential-list-item-' + credentialMetadata.credential.userLabel}>
|
|
||||||
<DataListItemRow key={'userCredentialRow-' + credentialMetadata.credential.id} className="pf-u-align-items-center">
|
|
||||||
<DataListItemCells dataListCells={this.credentialRowCells(credentialMetadata, type)}/>
|
|
||||||
<CredentialAction
|
|
||||||
credential={credentialMetadata.credential}
|
|
||||||
removeable={removeable}
|
|
||||||
updateAction={updateAIA}
|
|
||||||
credRemover={this.handleRemove}
|
|
||||||
/>
|
|
||||||
</DataListItemRow>
|
|
||||||
</DataListItem>
|
|
||||||
</DataList>
|
|
||||||
</>
|
|
||||||
))
|
|
||||||
} </React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private credentialRowCells(credMetadata: CredMetadata, type: string): React.ReactNode[] {
|
|
||||||
const credRowCells: React.ReactNode[] = [];
|
|
||||||
const credential = credMetadata.credential;
|
|
||||||
let maxWidth = { "--pf-u-max-width--MaxWidth": "300px" } as React.CSSProperties;
|
|
||||||
credRowCells.push(
|
|
||||||
<DataListCell id={`${SigningInPage.credElementId(type, credential.id, 'label')}`} key={'userLabel-' + credential.id} className="pf-u-max-width" style={maxWidth}>
|
|
||||||
{credential.userLabel}
|
|
||||||
</DataListCell>
|
|
||||||
);
|
|
||||||
if (credential.strCreatedDate) {
|
|
||||||
credRowCells.push(
|
|
||||||
<DataListCell
|
|
||||||
id={`${SigningInPage.credElementId(
|
|
||||||
type,
|
|
||||||
credential.id,
|
|
||||||
"created-at"
|
|
||||||
)}`}
|
|
||||||
key={"created-" + credential.id}
|
|
||||||
>
|
|
||||||
<strong className="pf-u-mr-md">
|
|
||||||
<Msg msgKey="credentialCreatedAt" />{" "}
|
|
||||||
</strong>
|
|
||||||
{credential.strCreatedDate}
|
|
||||||
</DataListCell>
|
|
||||||
);
|
|
||||||
credRowCells.push(<DataListCell key={"spacer-" + credential.id} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return credRowCells;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderCredTypeTitle(
|
|
||||||
credContainer: CredentialContainer,
|
|
||||||
keycloak: KeycloakService,
|
|
||||||
category: CredCategory
|
|
||||||
): React.ReactNode {
|
|
||||||
|
|
||||||
if (
|
|
||||||
!credContainer.hasOwnProperty("helptext") &&
|
|
||||||
!credContainer.hasOwnProperty("createAction")
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let setupAction: AIACommand;
|
|
||||||
if (credContainer.createAction) {
|
|
||||||
setupAction = new AIACommand(keycloak, credContainer.createAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
const credContainerDisplayName: string = Msg.localize(
|
|
||||||
credContainer.displayName
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<React.Fragment key={"credTypeTitle-" + credContainer.type}>
|
|
||||||
<Split className="pf-u-mt-lg pf-u-mb-lg">
|
|
||||||
<SplitItem>
|
|
||||||
<Title
|
|
||||||
headingLevel="h3"
|
|
||||||
size="md"
|
|
||||||
className="pf-u-mb-md"
|
|
||||||
>
|
|
||||||
<span className="cred-title pf-u-display-block" id={`${credContainer.type}-cred-title`}>
|
|
||||||
<Msg msgKey={credContainer.displayName} />
|
|
||||||
</span>
|
|
||||||
</Title>
|
|
||||||
<span id={`${credContainer.type}-cred-help`}>
|
|
||||||
{credContainer.helptext && (
|
|
||||||
<Msg msgKey={credContainer.helptext} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</SplitItem>
|
|
||||||
|
|
||||||
<SplitItem isFilled>
|
|
||||||
{credContainer.createAction && (
|
|
||||||
<div
|
|
||||||
id={"mob-setUpAction-" + credContainer.type}
|
|
||||||
className="pf-u-display-none-on-lg pf-u-float-right"
|
|
||||||
>
|
|
||||||
<Dropdown
|
|
||||||
isPlain
|
|
||||||
position={DropdownPosition.right}
|
|
||||||
toggle={
|
|
||||||
<KebabToggle
|
|
||||||
onToggle={(isOpen) => {
|
|
||||||
credContainer.open = isOpen;
|
|
||||||
this.setState({
|
|
||||||
credentialContainers: new Map(
|
|
||||||
this.state.credentialContainers
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isOpen={credContainer.open}
|
|
||||||
dropdownItems={[
|
|
||||||
<button
|
|
||||||
id={`mob-${credContainer.type}-set-up`}
|
|
||||||
className="pf-c-button pf-m-link"
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setupAction.execute()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="pf-c-button__icon">
|
|
||||||
<i
|
|
||||||
className="fa fa-plus-circle"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</span>
|
|
||||||
<Msg
|
|
||||||
msgKey="setUpNew"
|
|
||||||
params={[
|
|
||||||
credContainerDisplayName,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</button>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{credContainer.createAction && (
|
|
||||||
<div
|
|
||||||
id={"setUpAction-" + credContainer.type}
|
|
||||||
className="pf-u-display-none pf-u-display-inline-flex-on-lg pf-u-float-right"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
id={`${credContainer.type}-set-up`}
|
|
||||||
className="pf-c-button pf-m-link"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setupAction.execute()}
|
|
||||||
>
|
|
||||||
<span className="pf-c-button__icon">
|
|
||||||
<i
|
|
||||||
className="fa fa-plus-circle"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</span>
|
|
||||||
<Msg
|
|
||||||
msgKey="setUpNew"
|
|
||||||
params={[credContainerDisplayName]}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SplitItem>
|
|
||||||
</Split>
|
|
||||||
</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={Msg.localize('updateCredAriaLabel')}
|
|
||||||
aria-label={Msg.localize('updateCredAriaLabel')}
|
|
||||||
id={"updateAction-" + this.props.credential.id}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
id={`${SigningInPage.credElementId(
|
|
||||||
this.props.credential.type,
|
|
||||||
this.props.credential.id,
|
|
||||||
"update"
|
|
||||||
)}`}
|
|
||||||
onClick={() => this.props.updateAction.execute()}
|
|
||||||
>
|
|
||||||
<Msg msgKey="update" />
|
|
||||||
</Button>
|
|
||||||
</DataListAction>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.removeable) {
|
|
||||||
const userLabel: string = this.props.credential.userLabel;
|
|
||||||
return (
|
|
||||||
<DataListAction
|
|
||||||
aria-label={Msg.localize('removeCredAriaLabel')}
|
|
||||||
aria-labelledby={Msg.localize('removeCredAriaLabel')}
|
|
||||||
id={'removeAction-' + this.props.credential.id }
|
|
||||||
>
|
|
||||||
<ContinueCancelModal
|
|
||||||
buttonTitle='remove'
|
|
||||||
buttonVariant='danger'
|
|
||||||
buttonId={`${SigningInPage.credElementId(this.props.credential.type, this.props.credential.id, 'remove')}`}
|
|
||||||
modalTitle={Msg.localize('removeCred', [userLabel])}
|
|
||||||
modalMessage={Msg.localize('stopUsingCred', [userLabel])}
|
|
||||||
onContinue={() => this.props.credRemover(this.props.credential.id, userLabel)}
|
|
||||||
/>
|
|
||||||
</DataListAction>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SigningInPageWithRouter = withRouter(SigningInPage);
|
|
||||||
export { SigningInPageWithRouter as SigningInPage };
|
|
|
@ -1,4 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import { KeycloakService } from './keycloak.service';
|
|
||||||
|
|
||||||
export const KeycloakContext = React.createContext<KeycloakService | undefined>(undefined);
|
|
|
@ -1,78 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 Keycloak, { KeycloakLoginOptions } from "../../../../../../../../../../js/libs/keycloak-js";
|
|
||||||
|
|
||||||
declare const baseUrl: string;
|
|
||||||
export type KeycloakClient = Keycloak;
|
|
||||||
|
|
||||||
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)
|
|
||||||
.then(() => {
|
|
||||||
resolve(this.keycloakAuth.token as string);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
reject('Failed to refresh token');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject('Not logged in');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
|
|
||||||
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;
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
* 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);
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* 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: Intl.DateTimeFormatOptions = { 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;
|
|
|
@ -1,107 +0,0 @@
|
||||||
/*
|
|
||||||
* 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, ModalVariant, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
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}
|
|
||||||
variant={ModalVariant.small}
|
|
||||||
title={Msg.localize(this.props.modalTitle)}
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={this.handleModalToggle}
|
|
||||||
actions={[
|
|
||||||
<Button id='modal-confirm' key="confirm" variant="primary" onClick={this.handleContinue}>
|
|
||||||
<Msg msgKey={this.props.modalContinueButtonLabel!}/>
|
|
||||||
</Button>,
|
|
||||||
<Button id='modal-cancel' key="cancel" variant="secondary" onClick={this.handleModalToggle}>
|
|
||||||
<Msg msgKey={this.props.modalCancelButtonLabel!}/>
|
|
||||||
</Button>
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{ !this.props.modalMessage && this.props.children}
|
|
||||||
{ this.props.modalMessage && <Msg msgKey={this.props.modalMessage}/>}
|
|
||||||
</Modal>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,
|
|
||||||
EmptyStateBody,
|
|
||||||
} from '@patternfly/react-core'
|
|
||||||
|
|
||||||
import { Msg } from './Msg';
|
|
||||||
import {SVGIconProps} from '@patternfly/react-icons/dist/esm/createIcon';
|
|
||||||
|
|
||||||
export interface EmptyMessageStateProps {
|
|
||||||
icon: React.ComponentType<SVGIconProps>;
|
|
||||||
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="h5" size="lg">
|
|
||||||
<Msg msgKey={this.props.messageKey} />
|
|
||||||
</Title>
|
|
||||||
<EmptyStateBody>
|
|
||||||
{this.props.children}
|
|
||||||
</EmptyStateBody>
|
|
||||||
</EmptyState>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
updateEmailFeatureEnabled: boolean;
|
|
||||||
updateEmailActionEnabled: boolean;
|
|
||||||
isViewGroupsEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
{
|
|
||||||
"name": "keycloak.v2",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "keycloak account management written in React",
|
|
||||||
"scripts": {
|
|
||||||
"build": "snowpack --optimize && pnpm run check-types && pnpm run babel && pnpm run move-web_modules",
|
|
||||||
"babel": "babel --source-maps --extensions \".js,.ts,.tsx\" app/ --out-dir ../resources/",
|
|
||||||
"babel:watch": "pnpm run babel -- --watch",
|
|
||||||
"check-types": "tsc",
|
|
||||||
"check-types:watch": "pnpm run check-types -- -w",
|
|
||||||
"move-web_modules": "shx mv web_modules ../resources"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "Stan Silvert",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@patternfly/patternfly": "^4.224.5",
|
|
||||||
"@patternfly/react-core": "^4.278.0",
|
|
||||||
"@patternfly/react-icons": "^4.93.7",
|
|
||||||
"@patternfly/react-styles": "^4.92.8",
|
|
||||||
"react": "npm:@pika/react@^16.13.1",
|
|
||||||
"react-dom": "npm:@pika/react-dom@^16.13.1",
|
|
||||||
"react-router-dom": "^4.3.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/cli": "^7.18.9",
|
|
||||||
"@babel/compat-data": "^7.18.8",
|
|
||||||
"@babel/core": "^7.18.9",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
||||||
"@babel/preset-env": "^7.18.9",
|
|
||||||
"@babel/preset-react": "^7.18.6",
|
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
|
||||||
"@types/node": "^18.0.6",
|
|
||||||
"@types/react": "^16.9.23",
|
|
||||||
"@types/react-dom": "^16.9.5",
|
|
||||||
"@types/react-router-dom": "^4.3.1",
|
|
||||||
"chokidar": "^3.5.3",
|
|
||||||
"rollup-plugin-postcss": "^2.5.0",
|
|
||||||
"shx": "^0.3.4",
|
|
||||||
"snowpack": "^1.7.1",
|
|
||||||
"typescript": "^5.2.2"
|
|
||||||
},
|
|
||||||
"packageManager": "pnpm@8.10.0"
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
const postcss = require('rollup-plugin-postcss');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
rollup: {
|
|
||||||
plugins: [
|
|
||||||
postcss({
|
|
||||||
extract: '../resources/public/app.css'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../../../../../../../js/tsconfig.json",
|
|
||||||
"include": ["app"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"suppressImplicitAnyIndexErrors": true,
|
|
||||||
"ignoreDeprecations": "5.0",
|
|
||||||
"jsx": "react"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
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
|
|