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