Add toggle for user profile to realm settings (#1717)
This commit is contained in:
parent
88601cf118
commit
f0fc136672
13 changed files with 236 additions and 158 deletions
2
.github/workflows/cypress.yml
vendored
2
.github/workflows/cypress.yml
vendored
|
@ -53,7 +53,7 @@ jobs:
|
||||||
working-directory: ${{ env.KEYCLOAK_SERVER_BIN }}
|
working-directory: ${{ env.KEYCLOAK_SERVER_BIN }}
|
||||||
run: |
|
run: |
|
||||||
./add-user-keycloak.sh --user admin --password admin
|
./add-user-keycloak.sh --user admin --password admin
|
||||||
./standalone.sh -Dkeycloak.profile.feature.admin2=enabled &
|
./standalone.sh -Dkeycloak.profile.feature.admin2=enabled -Dkeycloak.profile.feature.declarative_user_profile=enabled &
|
||||||
|
|
||||||
- name: Configure Keycloak Server
|
- name: Configure Keycloak Server
|
||||||
working-directory: ${{ env.KEYCLOAK_SERVER_BIN }}
|
working-directory: ${{ env.KEYCLOAK_SERVER_BIN }}
|
||||||
|
|
|
@ -88,11 +88,13 @@ describe("Realm settings events tab tests", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
it("Enable user events", () => {
|
it("Enable user events", () => {
|
||||||
cy.intercept("GET", `/auth/admin/realms/${realmName}/keys`).as("load");
|
cy.intercept("GET", `/auth/admin/realms/${realmName}/events/config`).as(
|
||||||
|
"load"
|
||||||
|
);
|
||||||
sidebarPage.goToRealmSettings();
|
sidebarPage.goToRealmSettings();
|
||||||
cy.findByTestId("rs-realm-events-tab").click();
|
cy.findByTestId("rs-realm-events-tab").click();
|
||||||
cy.findByTestId("rs-events-tab").click();
|
cy.findByTestId("rs-events-tab").click();
|
||||||
cy.wait(["@load"]);
|
cy.wait("@load");
|
||||||
realmSettingsPage
|
realmSettingsPage
|
||||||
.toggleSwitch(realmSettingsPage.enableEvents)
|
.toggleSwitch(realmSettingsPage.enableEvents)
|
||||||
.save(realmSettingsPage.eventsUserSave);
|
.save(realmSettingsPage.eventsUserSave);
|
||||||
|
|
|
@ -37,6 +37,15 @@ describe("Realm settings tabs tests", () => {
|
||||||
masthead.checkNotificationMessage("Realm successfully updated");
|
masthead.checkNotificationMessage("Realm successfully updated");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows the 'user profile' tab if enabled", () => {
|
||||||
|
sidebarPage.goToRealmSettings();
|
||||||
|
cy.findByTestId(realmSettingsPage.userProfileTab).should("not.exist");
|
||||||
|
realmSettingsPage.toggleSwitch(realmSettingsPage.profileEnabledSwitch);
|
||||||
|
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
|
||||||
|
masthead.checkNotificationMessage("Realm successfully updated");
|
||||||
|
cy.findByTestId(realmSettingsPage.userProfileTab).should("exist");
|
||||||
|
});
|
||||||
|
|
||||||
it("Go to login tab", () => {
|
it("Go to login tab", () => {
|
||||||
sidebarPage.goToRealmSettings();
|
sidebarPage.goToRealmSettings();
|
||||||
cy.findByTestId("rs-login-tab").click();
|
cy.findByTestId("rs-login-tab").click();
|
||||||
|
@ -79,10 +88,7 @@ describe("Realm settings tabs tests", () => {
|
||||||
|
|
||||||
it("Go to themes tab", () => {
|
it("Go to themes tab", () => {
|
||||||
sidebarPage.goToRealmSettings();
|
sidebarPage.goToRealmSettings();
|
||||||
cy.intercept(`/auth/admin/realms/${realmName}/keys`).as("load");
|
|
||||||
|
|
||||||
cy.findByTestId("rs-themes-tab").click();
|
cy.findByTestId("rs-themes-tab").click();
|
||||||
cy.wait(["@load"]);
|
|
||||||
|
|
||||||
realmSettingsPage.selectLoginThemeType("keycloak");
|
realmSettingsPage.selectLoginThemeType("keycloak");
|
||||||
realmSettingsPage.selectAccountThemeType("keycloak");
|
realmSettingsPage.selectAccountThemeType("keycloak");
|
||||||
|
|
|
@ -5,6 +5,7 @@ export default class RealmSettingsPage {
|
||||||
generalSaveBtn = "general-tab-save";
|
generalSaveBtn = "general-tab-save";
|
||||||
themesSaveBtn = "themes-tab-save";
|
themesSaveBtn = "themes-tab-save";
|
||||||
loginTab = "rs-login-tab";
|
loginTab = "rs-login-tab";
|
||||||
|
userProfileTab = "rs-user-profile-tab";
|
||||||
selectLoginTheme = "#kc-login-theme";
|
selectLoginTheme = "#kc-login-theme";
|
||||||
loginThemeList = "#kc-login-theme + ul";
|
loginThemeList = "#kc-login-theme + ul";
|
||||||
selectAccountTheme = "#kc-account-theme";
|
selectAccountTheme = "#kc-account-theme";
|
||||||
|
@ -53,6 +54,7 @@ export default class RealmSettingsPage {
|
||||||
supportedLocalesToggle = "#kc-l-supported-locales";
|
supportedLocalesToggle = "#kc-l-supported-locales";
|
||||||
emailSaveBtn = "email-tab-save";
|
emailSaveBtn = "email-tab-save";
|
||||||
managedAccessSwitch = "user-managed-access-switch";
|
managedAccessSwitch = "user-managed-access-switch";
|
||||||
|
profileEnabledSwitch = "user-profile-enabled-switch";
|
||||||
userRegSwitch = "user-reg-switch";
|
userRegSwitch = "user-reg-switch";
|
||||||
forgotPwdSwitch = "forgot-pw-switch";
|
forgotPwdSwitch = "forgot-pw-switch";
|
||||||
rememberMeSwitch = "remember-me-switch";
|
rememberMeSwitch = "remember-me-switch";
|
||||||
|
@ -769,7 +771,12 @@ export default class RealmSettingsPage {
|
||||||
|
|
||||||
shouldCancelEditingExecutor() {
|
shouldCancelEditingExecutor() {
|
||||||
cy.get(this.clientProfileTwo).click();
|
cy.get(this.clientProfileTwo).click();
|
||||||
|
|
||||||
|
cy.intercept("/auth/admin/realms/master/client-policies/profiles*").as(
|
||||||
|
"profilesFetch"
|
||||||
|
);
|
||||||
cy.findByTestId(this.editExecutor).first().click();
|
cy.findByTestId(this.editExecutor).first().click();
|
||||||
|
cy.wait("@profilesFetch");
|
||||||
|
|
||||||
cy.findByTestId(this.addExecutorCancelBtn).click();
|
cy.findByTestId(this.addExecutorCancelBtn).click();
|
||||||
cy.get('ul[class*="pf-c-data-list"]').should(
|
cy.get('ul[class*="pf-c-data-list"]').should(
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
|
|
||||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||||
import { getBaseUrl } from "../util";
|
import { getBaseUrl } from "../util";
|
||||||
|
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
|
||||||
import { useAdminClient } from "../context/auth/AdminClient";
|
import { useAdminClient } from "../context/auth/AdminClient";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
import { FormAccess } from "../components/form-access/FormAccess";
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
|
@ -42,6 +43,7 @@ export const RealmSettingsGeneralTab = ({
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isDirty },
|
formState: { isDirty },
|
||||||
} = useFormContext();
|
} = useFormContext();
|
||||||
|
const isFeatureEnabled = useIsFeatureEnabled();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const baseUrl = getBaseUrl(adminClient);
|
const baseUrl = getBaseUrl(adminClient);
|
||||||
|
@ -166,6 +168,35 @@ export const RealmSettingsGeneralTab = ({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
{isFeatureEnabled(Feature.DeclarativeUserProfile) && (
|
||||||
|
<FormGroup
|
||||||
|
hasNoPaddingTop
|
||||||
|
label={t("userProfileEnabled")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="realm-settings-help:userProfileEnabled"
|
||||||
|
fieldLabelId="realm-settings:userProfileEnabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="kc-user-profile-enabled"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="attributes.userProfileEnabled"
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Switch
|
||||||
|
id="kc-user-profile-enabled"
|
||||||
|
data-testid="user-profile-enabled-switch"
|
||||||
|
label={t("common:on")}
|
||||||
|
labelOff={t("common:off")}
|
||||||
|
isChecked={value === "true"}
|
||||||
|
onChange={(value) => onChange(value.toString())}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("endpoints")}
|
label={t("endpoints")}
|
||||||
labelIcon={
|
labelIcon={
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm, useWatch } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
AlertVariant,
|
AlertVariant,
|
||||||
|
@ -51,8 +51,9 @@ import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||||
import { UserRegistration } from "./UserRegistration";
|
import { UserRegistration } from "./UserRegistration";
|
||||||
import { toDashboard } from "../dashboard/routes/Dashboard";
|
import { toDashboard } from "../dashboard/routes/Dashboard";
|
||||||
import environment from "../environment";
|
import environment from "../environment";
|
||||||
import { UserProfileTab } from "./UserProfileTab";
|
|
||||||
import helpUrls from "../help-urls";
|
import helpUrls from "../help-urls";
|
||||||
|
import { UserProfileTab } from "./user-profile/UserProfileTab";
|
||||||
|
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
|
||||||
|
|
||||||
type RealmSettingsHeaderProps = {
|
type RealmSettingsHeaderProps = {
|
||||||
onChange: (value: boolean) => void;
|
onChange: (value: boolean) => void;
|
||||||
|
@ -175,6 +176,7 @@ export const RealmSettingsTabs = ({
|
||||||
const { realm: realmName } = useRealm();
|
const { realm: realmName } = useRealm();
|
||||||
const { refresh: refreshRealms } = useRealms();
|
const { refresh: refreshRealms } = useRealms();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const isFeatureEnabled = useIsFeatureEnabled();
|
||||||
|
|
||||||
const kpComponentTypes =
|
const kpComponentTypes =
|
||||||
useServerInfo().componentTypes?.[KEY_PROVIDER_TYPE] ?? [];
|
useServerInfo().componentTypes?.[KEY_PROVIDER_TYPE] ?? [];
|
||||||
|
@ -221,6 +223,12 @@ export const RealmSettingsTabs = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userProfileEnabled = useWatch({
|
||||||
|
control,
|
||||||
|
name: "attributes.userProfileEnabled",
|
||||||
|
defaultValue: "false",
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Controller
|
<Controller
|
||||||
|
@ -239,7 +247,7 @@ export const RealmSettingsTabs = ({
|
||||||
/>
|
/>
|
||||||
<PageSection variant="light" className="pf-u-p-0">
|
<PageSection variant="light" className="pf-u-p-0">
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<KeycloakTabs isBox>
|
<KeycloakTabs isBox mountOnEnter>
|
||||||
<Tab
|
<Tab
|
||||||
eventKey="general"
|
eventKey="general"
|
||||||
title={<TabTitleText>{t("general")}</TabTitleText>}
|
title={<TabTitleText>{t("general")}</TabTitleText>}
|
||||||
|
@ -423,15 +431,20 @@ export const RealmSettingsTabs = ({
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
{isFeatureEnabled(Feature.DeclarativeUserProfile) &&
|
||||||
eventKey="userProfile"
|
userProfileEnabled === "true" && (
|
||||||
data-testid="rs-user-profile-tab"
|
<Tab
|
||||||
title={
|
eventKey="userProfile"
|
||||||
<TabTitleText>{t("realm-settings:userProfile")}</TabTitleText>
|
data-testid="rs-user-profile-tab"
|
||||||
}
|
title={
|
||||||
>
|
<TabTitleText>
|
||||||
<UserProfileTab />
|
{t("realm-settings:userProfile")}
|
||||||
</Tab>
|
</TabTitleText>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<UserProfileTab />
|
||||||
|
</Tab>
|
||||||
|
)}
|
||||||
<Tab
|
<Tab
|
||||||
eventKey="userRegistration"
|
eventKey="userRegistration"
|
||||||
title={<TabTitleText>{t("userRegistration")}</TabTitleText>}
|
title={<TabTitleText>{t("userRegistration")}</TabTitleText>}
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
import type ClientProfilesRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfilesRepresentation";
|
|
||||||
import { CodeEditor, Language } from "@patternfly/react-code-editor";
|
|
||||||
import {
|
|
||||||
ActionGroup,
|
|
||||||
AlertVariant,
|
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
PageSection,
|
|
||||||
Tab,
|
|
||||||
Tabs,
|
|
||||||
TabTitleText,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
import type { editor } from "monaco-editor";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
|
||||||
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
|
||||||
import { prettyPrintJSON } from "../util";
|
|
||||||
|
|
||||||
export const UserProfileTab = () => {
|
|
||||||
const adminClient = useAdminClient();
|
|
||||||
const { realm } = useRealm();
|
|
||||||
const { t } = useTranslation("realm-settings");
|
|
||||||
const { addAlert, addError } = useAlerts();
|
|
||||||
const [activeTab, setActiveTab] = useState("attributes");
|
|
||||||
const [profiles, setProfiles] = useState<ClientProfilesRepresentation>();
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
const [refreshCount, setRefreshCount] = useState(0);
|
|
||||||
|
|
||||||
useFetch(
|
|
||||||
() =>
|
|
||||||
adminClient.clientPolicies.listProfiles({
|
|
||||||
includeGlobalProfiles: true,
|
|
||||||
realm,
|
|
||||||
}),
|
|
||||||
(profiles) => setProfiles(profiles),
|
|
||||||
[refreshCount]
|
|
||||||
);
|
|
||||||
|
|
||||||
async function onSave(updatedProfiles: ClientProfilesRepresentation) {
|
|
||||||
setIsSaving(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await adminClient.clientPolicies.createProfiles({
|
|
||||||
...updatedProfiles,
|
|
||||||
realm,
|
|
||||||
});
|
|
||||||
|
|
||||||
setRefreshCount(refreshCount + 1);
|
|
||||||
addAlert(t("userProfileSuccess"), AlertVariant.success);
|
|
||||||
} catch (error) {
|
|
||||||
addError("realm-settings:userProfileError", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs
|
|
||||||
activeKey={activeTab}
|
|
||||||
onSelect={(_, key) => setActiveTab(key.toString())}
|
|
||||||
mountOnEnter
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
eventKey="attributes"
|
|
||||||
title={<TabTitleText>{t("attributes")}</TabTitleText>}
|
|
||||||
></Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="attributesGroup"
|
|
||||||
title={<TabTitleText>{t("attributesGroup")}</TabTitleText>}
|
|
||||||
></Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="jsonEditor"
|
|
||||||
title={<TabTitleText>{t("jsonEditor")}</TabTitleText>}
|
|
||||||
>
|
|
||||||
<JsonEditorTab
|
|
||||||
profiles={profiles}
|
|
||||||
onSave={onSave}
|
|
||||||
isSaving={isSaving}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type JsonEditorTabProps = {
|
|
||||||
profiles?: ClientProfilesRepresentation;
|
|
||||||
onSave: (profiles: ClientProfilesRepresentation) => void;
|
|
||||||
isSaving: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const JsonEditorTab = ({ profiles, onSave, isSaving }: JsonEditorTabProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { addError } = useAlerts();
|
|
||||||
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();
|
|
||||||
|
|
||||||
useEffect(() => resetCode(), [profiles, editor]);
|
|
||||||
|
|
||||||
function resetCode() {
|
|
||||||
editor?.setValue(profiles ? prettyPrintJSON(profiles) : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
const value = editor?.getValue();
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
onSave(JSON.parse(value));
|
|
||||||
} catch (error) {
|
|
||||||
addError("realm-settings:invalidJsonError", error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageSection variant="light">
|
|
||||||
<CodeEditor
|
|
||||||
language={Language.json}
|
|
||||||
height="30rem"
|
|
||||||
onEditorDidMount={(editor) => setEditor(editor)}
|
|
||||||
isLanguageLabelVisible
|
|
||||||
/>
|
|
||||||
<Form>
|
|
||||||
<ActionGroup>
|
|
||||||
<Button variant="primary" onClick={save} isDisabled={isSaving}>
|
|
||||||
{t("common:save")}
|
|
||||||
</Button>
|
|
||||||
<Button variant="link" onClick={resetCode} isDisabled={isSaving}>
|
|
||||||
{t("common:revert")}
|
|
||||||
</Button>
|
|
||||||
</ActionGroup>
|
|
||||||
</Form>
|
|
||||||
</PageSection>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -12,6 +12,7 @@ export default {
|
||||||
"Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.",
|
"Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.",
|
||||||
userManagedAccess:
|
userManagedAccess:
|
||||||
"If enabled, users are allowed to manage their resources and permissions using the Account Management Console.",
|
"If enabled, users are allowed to manage their resources and permissions using the Account Management Console.",
|
||||||
|
userProfileEnabled: "If enabled, allows managing user profiles.",
|
||||||
endpoints: "Shows the configuration of the protocol endpoints",
|
endpoints: "Shows the configuration of the protocol endpoints",
|
||||||
loginTheme:
|
loginTheme:
|
||||||
"Select theme for login, OTP, grant, registration and forgot password pages.",
|
"Select theme for login, OTP, grant, registration and forgot password pages.",
|
||||||
|
|
|
@ -162,6 +162,7 @@ export default {
|
||||||
},
|
},
|
||||||
placeholderText: "Select one",
|
placeholderText: "Select one",
|
||||||
userManagedAccess: "User-managed access",
|
userManagedAccess: "User-managed access",
|
||||||
|
userProfileEnabled: "User Profile Enabled",
|
||||||
endpoints: "Endpoints",
|
endpoints: "Endpoints",
|
||||||
openIDEndpointConfiguration: "OpenID Endpoint Configuration",
|
openIDEndpointConfiguration: "OpenID Endpoint Configuration",
|
||||||
samlIdentityProviderMetadata: "SAML 2.0 Identity Provider Metadata",
|
samlIdentityProviderMetadata: "SAML 2.0 Identity Provider Metadata",
|
||||||
|
|
67
src/realm-settings/user-profile/JsonEditorTab.tsx
Normal file
67
src/realm-settings/user-profile/JsonEditorTab.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import type ClientProfilesRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfilesRepresentation";
|
||||||
|
import type UserProfileConfig from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
|
||||||
|
import { CodeEditor, Language } from "@patternfly/react-code-editor";
|
||||||
|
import { ActionGroup, Button, Form, PageSection } from "@patternfly/react-core";
|
||||||
|
import type { editor } from "monaco-editor";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
import { prettyPrintJSON } from "../../util";
|
||||||
|
|
||||||
|
type JsonEditorTabProps = {
|
||||||
|
config?: UserProfileConfig;
|
||||||
|
onSave: (profiles: ClientProfilesRepresentation) => void;
|
||||||
|
isSaving: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JsonEditorTab = ({
|
||||||
|
config,
|
||||||
|
onSave,
|
||||||
|
isSaving,
|
||||||
|
}: JsonEditorTabProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { addError } = useAlerts();
|
||||||
|
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();
|
||||||
|
|
||||||
|
useEffect(() => resetCode(), [config, editor]);
|
||||||
|
|
||||||
|
function resetCode() {
|
||||||
|
editor?.setValue(config ? prettyPrintJSON(config) : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
const value = editor?.getValue();
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
onSave(JSON.parse(value));
|
||||||
|
} catch (error) {
|
||||||
|
addError("realm-settings:invalidJsonError", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection variant="light">
|
||||||
|
<CodeEditor
|
||||||
|
language={Language.json}
|
||||||
|
height="30rem"
|
||||||
|
onEditorDidMount={(editor) => setEditor(editor)}
|
||||||
|
isLanguageLabelVisible
|
||||||
|
/>
|
||||||
|
<Form>
|
||||||
|
<ActionGroup>
|
||||||
|
<Button variant="primary" onClick={save} isDisabled={isSaving}>
|
||||||
|
{t("common:save")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="link" onClick={resetCode} isDisabled={isSaving}>
|
||||||
|
{t("common:revert")}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</Form>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
};
|
67
src/realm-settings/user-profile/UserProfileTab.tsx
Normal file
67
src/realm-settings/user-profile/UserProfileTab.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import type ClientProfilesRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfilesRepresentation";
|
||||||
|
import type UserProfileConfig from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
|
||||||
|
import { AlertVariant, Tab, Tabs, TabTitleText } from "@patternfly/react-core";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
|
import { JsonEditorTab } from "./JsonEditorTab";
|
||||||
|
|
||||||
|
export const UserProfileTab = () => {
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
const { realm } = useRealm();
|
||||||
|
const { t } = useTranslation("realm-settings");
|
||||||
|
const { addAlert, addError } = useAlerts();
|
||||||
|
const [activeTab, setActiveTab] = useState("attributes");
|
||||||
|
const [config, setConfig] = useState<UserProfileConfig>();
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [refreshCount, setRefreshCount] = useState(0);
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
() => adminClient.users.getProfile({ realm }),
|
||||||
|
(config) => setConfig(config),
|
||||||
|
[refreshCount]
|
||||||
|
);
|
||||||
|
|
||||||
|
async function onSave(updatedProfiles: ClientProfilesRepresentation) {
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await adminClient.clientPolicies.createProfiles({
|
||||||
|
...updatedProfiles,
|
||||||
|
realm,
|
||||||
|
});
|
||||||
|
|
||||||
|
setRefreshCount(refreshCount + 1);
|
||||||
|
addAlert(t("userProfileSuccess"), AlertVariant.success);
|
||||||
|
} catch (error) {
|
||||||
|
addError("realm-settings:userProfileError", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onSelect={(_, key) => setActiveTab(key.toString())}
|
||||||
|
mountOnEnter
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
eventKey="attributes"
|
||||||
|
title={<TabTitleText>{t("attributes")}</TabTitleText>}
|
||||||
|
></Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="attributesGroup"
|
||||||
|
title={<TabTitleText>{t("attributesGroup")}</TabTitleText>}
|
||||||
|
></Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="jsonEditor"
|
||||||
|
title={<TabTitleText>{t("jsonEditor")}</TabTitleText>}
|
||||||
|
>
|
||||||
|
<JsonEditorTab config={config} onSave={onSave} isSaving={isSaving} />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
21
src/utils/useIsFeatureEnabled.ts
Normal file
21
src/utils/useIsFeatureEnabled.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||||
|
|
||||||
|
export enum Feature {
|
||||||
|
DeclarativeUserProfile = "DECLARATIVE_USER_PROFILE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useIsFeatureEnabled() {
|
||||||
|
const { profileInfo } = useServerInfo();
|
||||||
|
|
||||||
|
const experimentalFeatures = profileInfo?.experimentalFeatures ?? [];
|
||||||
|
const previewFeatures = profileInfo?.previewFeatures ?? [];
|
||||||
|
const disabledFilters = profileInfo?.disabledFeatures ?? [];
|
||||||
|
const allFeatures = [...experimentalFeatures, ...previewFeatures];
|
||||||
|
const enabledFeatures = allFeatures.filter(
|
||||||
|
(feature) => !disabledFilters.includes(feature)
|
||||||
|
);
|
||||||
|
|
||||||
|
return function isFeatureEnabled(feature: Feature) {
|
||||||
|
return enabledFeatures.includes(feature);
|
||||||
|
};
|
||||||
|
}
|
|
@ -66,7 +66,8 @@ const run = () => {
|
||||||
addProc.on("exit", () => {
|
addProc.on("exit", () => {
|
||||||
const proc = spawn(path.join(serverPath, "bin", `standalone${extension}`), [
|
const proc = spawn(path.join(serverPath, "bin", `standalone${extension}`), [
|
||||||
"-Djboss.socket.binding.port-offset=100",
|
"-Djboss.socket.binding.port-offset=100",
|
||||||
"-Dprofile.feature.newadmin=enabled",
|
"-Dkeycloak.profile.feature.admin2=enabled",
|
||||||
|
"-Dkeycloak.profile.feature.declarative_user_profile=enabled",
|
||||||
...args,
|
...args,
|
||||||
]);
|
]);
|
||||||
proc.stdout.on("data", (data) => {
|
proc.stdout.on("data", (data) => {
|
||||||
|
|
Loading…
Reference in a new issue