Transient sessions: UX improvements
Closes: #24279 Signed-off-by: Hynek Mlnarik <hmlnarik@redhat.com>
This commit is contained in:
parent
bbe69fce0b
commit
5ec394b258
8 changed files with 74 additions and 6 deletions
4
.github/workflows/js-ci.yml
vendored
4
.github/workflows/js-ci.yml
vendored
|
@ -211,7 +211,7 @@ jobs:
|
||||||
- name: Start Keycloak server
|
- name: Start Keycloak server
|
||||||
run: |
|
run: |
|
||||||
tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz
|
tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz
|
||||||
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=declarative-user-profile &> ~/server.log &
|
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=declarative-user-profile,transient-users &> ~/server.log &
|
||||||
env:
|
env:
|
||||||
KEYCLOAK_ADMIN: admin
|
KEYCLOAK_ADMIN: admin
|
||||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||||
|
@ -294,7 +294,7 @@ jobs:
|
||||||
- name: Start Keycloak server
|
- name: Start Keycloak server
|
||||||
run: |
|
run: |
|
||||||
tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz
|
tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz
|
||||||
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz,declarative-user-profile &> ~/server.log &
|
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz,declarative-user-profile,transient-users &> ~/server.log &
|
||||||
env:
|
env:
|
||||||
KEYCLOAK_ADMIN: admin
|
KEYCLOAK_ADMIN: admin
|
||||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||||
|
|
|
@ -337,6 +337,15 @@ describe("Identity provider test", () => {
|
||||||
advancedSettings.clickTrustEmailSwitch();
|
advancedSettings.clickTrustEmailSwitch();
|
||||||
advancedSettings.clickAccountLinkingOnlySwitch();
|
advancedSettings.clickAccountLinkingOnlySwitch();
|
||||||
advancedSettings.clickHideOnLoginPageSwitch();
|
advancedSettings.clickHideOnLoginPageSwitch();
|
||||||
|
advancedSettings.assertDoNotImportUsersSwitchTurnedOn(false);
|
||||||
|
advancedSettings.assertSyncModeShown(true);
|
||||||
|
advancedSettings.clickdoNotStoreUsersSwitch();
|
||||||
|
advancedSettings.assertDoNotImportUsersSwitchTurnedOn(true);
|
||||||
|
advancedSettings.assertSyncModeShown(false);
|
||||||
|
advancedSettings.clickdoNotStoreUsersSwitch();
|
||||||
|
advancedSettings.assertDoNotImportUsersSwitchTurnedOn(false);
|
||||||
|
advancedSettings.assertSyncModeShown(true);
|
||||||
|
|
||||||
advancedSettings.clickEssentialClaimSwitch();
|
advancedSettings.clickEssentialClaimSwitch();
|
||||||
advancedSettings.typeClaimNameInput("claim-name");
|
advancedSettings.typeClaimNameInput("claim-name");
|
||||||
advancedSettings.typeClaimValueInput("claim-value");
|
advancedSettings.typeClaimValueInput("claim-value");
|
||||||
|
|
|
@ -65,6 +65,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
#promptSelect = "#prompt";
|
#promptSelect = "#prompt";
|
||||||
#disableUserInfoSwitch = "#disableUserInfo";
|
#disableUserInfoSwitch = "#disableUserInfo";
|
||||||
#trustEmailSwitch = "#trustEmail";
|
#trustEmailSwitch = "#trustEmail";
|
||||||
|
#doNotStoreUsers = "#doNotStoreUsers";
|
||||||
#accountLinkingOnlySwitch = "#accountLinkingOnly";
|
#accountLinkingOnlySwitch = "#accountLinkingOnly";
|
||||||
#hideOnLoginPageSwitch = "#hideOnLoginPage";
|
#hideOnLoginPageSwitch = "#hideOnLoginPage";
|
||||||
#firstLoginFlowSelect = "#firstBrokerLoginFlowAlias";
|
#firstLoginFlowSelect = "#firstBrokerLoginFlowAlias";
|
||||||
|
@ -149,6 +150,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clickdoNotStoreUsersSwitch() {
|
||||||
|
cy.get(this.#doNotStoreUsers).parent().click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public typeClaimNameInput(text: string) {
|
public typeClaimNameInput(text: string) {
|
||||||
cy.get(this.#claimNameInput).type(text).blur();
|
cy.get(this.#claimNameInput).type(text).blur();
|
||||||
return this;
|
return this;
|
||||||
|
@ -273,6 +279,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public assertDoNotImportUsersSwitchTurnedOn(isOn: boolean) {
|
||||||
|
super.assertSwitchStateOn(cy.get(this.#doNotStoreUsers).parent(), isOn);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public assertEssentialClaimSwitchTurnedOn(isOn: boolean) {
|
public assertEssentialClaimSwitchTurnedOn(isOn: boolean) {
|
||||||
super.assertSwitchStateOn(
|
super.assertSwitchStateOn(
|
||||||
cy.get(this.#essentialClaimSwitch).parent(),
|
cy.get(this.#essentialClaimSwitch).parent(),
|
||||||
|
@ -310,6 +321,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public assertSyncModeShown(isShown: boolean) {
|
||||||
|
cy.get(this.#syncModeSelect).should(isShown ? "exist" : "not.exist");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public assertClientAssertSigAlgSelectOptionEqual(
|
public assertClientAssertSigAlgSelectOptionEqual(
|
||||||
clientAssertionSigningAlg: ClientAssertionSigningAlg,
|
clientAssertionSigningAlg: ClientAssertionSigningAlg,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -2890,6 +2890,8 @@ startBySearchingAUser=Start by searching for users
|
||||||
times.days=Days
|
times.days=Days
|
||||||
selectALocale=Select a locale
|
selectALocale=Select a locale
|
||||||
clientsClientScopesHelp=The scopes associated with this resource.
|
clientsClientScopesHelp=The scopes associated with this resource.
|
||||||
|
transientUser=Transient
|
||||||
|
transientUserTooltip=This user not stored in Keycloak database. It is constructed solely from data provided by the originating identity provider.
|
||||||
error-empty=Please specify value of '{{0}}'.
|
error-empty=Please specify value of '{{0}}'.
|
||||||
error-invalid-blank=Please specify value of '{{0}}'.
|
error-invalid-blank=Please specify value of '{{0}}'.
|
||||||
error-invalid-date='{{0}}' is invalid date.
|
error-invalid-date='{{0}}' is invalid date.
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { CubesIcon } from "@patternfly/react-icons";
|
||||||
import { ReactNode, useMemo, useState } from "react";
|
import { ReactNode, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useMatch, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { adminClient } from "../admin-client";
|
import { adminClient } from "../admin-client";
|
||||||
import { toClient } from "../clients/routes/Client";
|
import { toClient } from "../clients/routes/Client";
|
||||||
|
@ -25,7 +26,9 @@ import {
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
import { useWhoAmI } from "../context/whoami/WhoAmI";
|
import { useWhoAmI } from "../context/whoami/WhoAmI";
|
||||||
import { keycloak } from "../keycloak";
|
import { keycloak } from "../keycloak";
|
||||||
import { toUser } from "../user/routes/User";
|
import { toUser, UserRoute } from "../user/routes/User";
|
||||||
|
import { toUsers } from "../user/routes/Users";
|
||||||
|
import { isLightweightUser } from "../user/utils";
|
||||||
import useFormatDate from "../utils/useFormatDate";
|
import useFormatDate from "../utils/useFormatDate";
|
||||||
|
|
||||||
export type ColumnName =
|
export type ColumnName =
|
||||||
|
@ -80,11 +83,13 @@ export default function SessionsTable({
|
||||||
}: SessionsTableProps) {
|
}: SessionsTableProps) {
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
const { whoAmI } = useWhoAmI();
|
const { whoAmI } = useWhoAmI();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { addError } = useAlerts();
|
const { addError } = useAlerts();
|
||||||
const formatDate = useFormatDate();
|
const formatDate = useFormatDate();
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const refresh = () => setKey((value) => value + 1);
|
const refresh = () => setKey((value) => value + 1);
|
||||||
|
const isOnUserPage = !!useMatch(UserRoute.path);
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
const defaultColumns: Field<UserSessionRepresentation>[] = [
|
const defaultColumns: Field<UserSessionRepresentation>[] = [
|
||||||
|
@ -130,7 +135,11 @@ export default function SessionsTable({
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
await adminClient.users.logout({ id: logoutUser! });
|
await adminClient.users.logout({ id: logoutUser! });
|
||||||
refresh();
|
if (isOnUserPage && isLightweightUser(logoutUser)) {
|
||||||
|
navigate(toUsers({ realm: realm }));
|
||||||
|
} else {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addError("logoutAllSessionsError", error);
|
addError("logoutAllSessionsError", error);
|
||||||
}
|
}
|
||||||
|
@ -142,6 +151,8 @@ export default function SessionsTable({
|
||||||
|
|
||||||
if (session.userId === whoAmI.getUserId()) {
|
if (session.userId === whoAmI.getUserId()) {
|
||||||
await keycloak.logout({ redirectUri: "" });
|
await keycloak.logout({ redirectUri: "" });
|
||||||
|
} else if (isOnUserPage && isLightweightUser(session.userId)) {
|
||||||
|
navigate(toUsers({ realm: realm }));
|
||||||
} else {
|
} else {
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,13 @@ import {
|
||||||
AlertVariant,
|
AlertVariant,
|
||||||
ButtonVariant,
|
ButtonVariant,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
|
Label,
|
||||||
PageSection,
|
PageSection,
|
||||||
Tab,
|
Tab,
|
||||||
TabTitleText,
|
TabTitleText,
|
||||||
|
Tooltip,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
import { InfoCircleIcon } from "@patternfly/react-icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -44,6 +47,7 @@ import {
|
||||||
} from "./form-state";
|
} from "./form-state";
|
||||||
import { UserParams, UserTab, toUser } from "./routes/User";
|
import { UserParams, UserTab, toUser } from "./routes/User";
|
||||||
import { toUsers } from "./routes/Users";
|
import { toUsers } from "./routes/Users";
|
||||||
|
import { isLightweightUser } from "./utils";
|
||||||
|
|
||||||
import "./user-section.css";
|
import "./user-section.css";
|
||||||
|
|
||||||
|
@ -63,6 +67,7 @@ export default function EditUser() {
|
||||||
useState<UserProfileMetadata>();
|
useState<UserProfileMetadata>();
|
||||||
const [refreshCount, setRefreshCount] = useState(0);
|
const [refreshCount, setRefreshCount] = useState(0);
|
||||||
const refresh = () => setRefreshCount((count) => count + 1);
|
const refresh = () => setRefreshCount((count) => count + 1);
|
||||||
|
const lightweightUser = isLightweightUser(user?.id);
|
||||||
|
|
||||||
const toTab = (tab: UserTab) =>
|
const toTab = (tab: UserTab) =>
|
||||||
toUser({
|
toUser({
|
||||||
|
@ -141,7 +146,11 @@ export default function EditUser() {
|
||||||
continueButtonVariant: ButtonVariant.danger,
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
await adminClient.users.del({ id: user!.id! });
|
if (lightweightUser) {
|
||||||
|
await adminClient.users.logout({ id: user!.id! });
|
||||||
|
} else {
|
||||||
|
await adminClient.users.del({ id: user!.id! });
|
||||||
|
}
|
||||||
addAlert(t("userDeletedSuccess"), AlertVariant.success);
|
addAlert(t("userDeletedSuccess"), AlertVariant.success);
|
||||||
navigate(toUsers({ realm: realmName }));
|
navigate(toUsers({ realm: realmName }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -183,6 +192,24 @@ export default function EditUser() {
|
||||||
titleKey={user.username!}
|
titleKey={user.username!}
|
||||||
className="kc-username-view-header"
|
className="kc-username-view-header"
|
||||||
divider={false}
|
divider={false}
|
||||||
|
badges={
|
||||||
|
lightweightUser
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
text: (
|
||||||
|
<Tooltip content={t("transientUserTooltip")}>
|
||||||
|
<Label
|
||||||
|
data-testid="user-details-label-transient-user"
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
>
|
||||||
|
{t("transientUser")}
|
||||||
|
</Label>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
dropdownItems={[
|
dropdownItems={[
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="impersonate"
|
key="impersonate"
|
||||||
|
|
|
@ -25,3 +25,6 @@ export const fieldName = (attribute: UserProfileAttributeMetadata) =>
|
||||||
isRootAttribute(attribute.name)
|
isRootAttribute(attribute.name)
|
||||||
? attribute.name
|
? attribute.name
|
||||||
: `attributes.${attribute.name}`;
|
: `attributes.${attribute.name}`;
|
||||||
|
|
||||||
|
export const isLightweightUser = (userId?: string) =>
|
||||||
|
userId?.startsWith("lightweight-");
|
||||||
|
|
|
@ -40,7 +40,7 @@ async function startServer() {
|
||||||
[
|
[
|
||||||
"start-dev",
|
"start-dev",
|
||||||
"--http-port=8180",
|
"--http-port=8180",
|
||||||
"--features=account3,admin-fine-grained-authz,declarative-user-profile",
|
"--features=account3,admin-fine-grained-authz,declarative-user-profile,transient-users",
|
||||||
...keycloakArgs,
|
...keycloakArgs,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue