Localization tabs (#25532)
* Add new localization tabs to Administration Console Closes #23057 Signed-off-by: Agnieszka <agancarc@redhat.com> Signed-off-by: Jon Koops <jonkoops@gmail.com> * css cleanup Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * css cleanup Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> --------- Signed-off-by: Agnieszka <agancarc@redhat.com> Signed-off-by: Jon Koops <jonkoops@gmail.com> Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> Co-authored-by: Jon Koops <jonkoops@gmail.com> Co-authored-by: Agnieszka Gancarczyk <agancarc@redhat.com>
This commit is contained in:
parent
ddcaa6dcbf
commit
768231d950
13 changed files with 1579 additions and 66 deletions
|
@ -89,12 +89,6 @@ describe("Realm settings events tab tests", () => {
|
|||
return this;
|
||||
};
|
||||
|
||||
const addBundle = () => {
|
||||
realmSettingsPage.addKeyValuePair("key_" + uuid(), "value_" + uuid());
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
it("Enable user events", () => {
|
||||
cy.intercept("GET", `/admin/realms/${realmName}/events/config`).as("load");
|
||||
sidebarPage.goToRealmSettings();
|
||||
|
@ -240,33 +234,31 @@ describe("Realm settings events tab tests", () => {
|
|||
realmSettingsPage.checkKeyPublic();
|
||||
});
|
||||
|
||||
it("add locale", () => {
|
||||
it("Realm header settings", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.findByTestId("rs-security-defenses-tab").click();
|
||||
cy.findByTestId("headers-form-tab-save").should("be.disabled");
|
||||
cy.get("#xFrameOptions").clear().type("DENY");
|
||||
cy.findByTestId("headers-form-tab-save").should("be.enabled").click();
|
||||
|
||||
cy.findByTestId("rs-localization-tab").click();
|
||||
cy.findByTestId("internationalization-disabled").click({ force: true });
|
||||
masthead.checkNotificationMessage("Realm successfully updated");
|
||||
});
|
||||
|
||||
cy.get(realmSettingsPage.supportedLocalesTypeahead)
|
||||
.click()
|
||||
.get(".pf-c-select__menu-item")
|
||||
.contains("Danish")
|
||||
.click();
|
||||
cy.get("#kc-l-supported-locales").click();
|
||||
it("Brute force detection", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.findAllByTestId("rs-security-defenses-tab").click();
|
||||
cy.get("#pf-tab-20-bruteForce").click();
|
||||
|
||||
cy.intercept("GET", `/admin/realms/${realmName}/localization/en*`).as(
|
||||
"load",
|
||||
);
|
||||
cy.findByTestId("brute-force-tab-save").should("be.disabled");
|
||||
|
||||
cy.findByTestId("localization-tab-save").click();
|
||||
cy.wait("@load");
|
||||
cy.get("#bruteForceProtected").click({ force: true });
|
||||
cy.findByTestId("waitIncrementSeconds").type("1");
|
||||
cy.findByTestId("maxFailureWaitSeconds").type("1");
|
||||
cy.findByTestId("maxDeltaTimeSeconds").type("1");
|
||||
cy.findByTestId("minimumQuickLoginWaitSeconds").type("1");
|
||||
|
||||
addBundle();
|
||||
|
||||
masthead.checkNotificationMessage(
|
||||
"Success! The message bundle has been added.",
|
||||
);
|
||||
realmSettingsPage.setDefaultLocale("Danish");
|
||||
cy.findByTestId("localization-tab-save").click();
|
||||
cy.findByTestId("brute-force-tab-save").should("be.enabled").click();
|
||||
masthead.checkNotificationMessage("Realm successfully updated");
|
||||
});
|
||||
|
||||
it("add session data", () => {
|
||||
|
|
|
@ -34,6 +34,12 @@ describe("Realm settings tabs tests", () => {
|
|||
await adminClient.deleteRealm(realmName);
|
||||
});
|
||||
|
||||
const addBundle = () => {
|
||||
realmSettingsPage.addKeyValuePair("123", "abc");
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
it("shows the 'user profile' tab if enabled", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.findByTestId(realmSettingsPage.userProfileTab).should("not.exist");
|
||||
|
@ -167,6 +173,109 @@ describe("Realm settings tabs tests", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Go to localization tab", () => {
|
||||
it("Locales tab - Add locale", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
realmSettingsPage.goToLocalizationTab();
|
||||
realmSettingsPage.goToLocalizationLocalesSubTab();
|
||||
|
||||
cy.findByTestId("internationalization-disabled").click({ force: true });
|
||||
|
||||
cy.get(realmSettingsPage.supportedLocalesTypeahead)
|
||||
.click()
|
||||
.get(".pf-c-select__menu-item")
|
||||
.contains("Danish")
|
||||
.click();
|
||||
cy.get("#kc-l-supported-locales").click();
|
||||
|
||||
cy.intercept("GET", `/admin/realms/${realmName}/localization/en*`).as(
|
||||
"load",
|
||||
);
|
||||
|
||||
cy.findByTestId("localization-tab-save").click();
|
||||
cy.wait("@load");
|
||||
|
||||
masthead.checkNotificationMessage("Realm successfully updated");
|
||||
});
|
||||
|
||||
it("Realm Overrides - Add and delete bundle", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
realmSettingsPage.goToLocalizationTab();
|
||||
realmSettingsPage.goToLocalizationRealmOverridesSubTab();
|
||||
|
||||
addBundle();
|
||||
|
||||
masthead.checkNotificationMessage(
|
||||
"Success! The message bundle has been added.",
|
||||
);
|
||||
|
||||
cy.findByTestId("editable-rows-table")
|
||||
.contains("td", "123")
|
||||
.should("be.visible");
|
||||
|
||||
cy.findByTestId("realmOverrides-deleteKebabToggle").click();
|
||||
cy.findByTestId("confirm").click();
|
||||
masthead.checkNotificationMessage(
|
||||
"Successfully removed message(s) from the bundle.",
|
||||
);
|
||||
});
|
||||
|
||||
it("Realm Overrides - Search for and delete bundle", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
realmSettingsPage.goToLocalizationTab();
|
||||
realmSettingsPage.goToLocalizationRealmOverridesSubTab();
|
||||
|
||||
addBundle();
|
||||
|
||||
cy.get('input[aria-label="Search"]').type("123");
|
||||
|
||||
cy.findByTestId("editable-rows-table")
|
||||
.contains("td", "123")
|
||||
.should("be.visible");
|
||||
|
||||
cy.findByTestId("selectAll").click();
|
||||
cy.get('[data-testid="toolbar-deleteBtn"] button').click();
|
||||
cy.findByTestId("delete-selected-bundleBtn").click();
|
||||
cy.findByTestId("confirm").click();
|
||||
masthead.checkNotificationMessage(
|
||||
"Successfully removed message(s) from the bundle.",
|
||||
);
|
||||
});
|
||||
|
||||
it("Realm Overrides - Edit and cancel edit message bundle", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
realmSettingsPage.goToLocalizationTab();
|
||||
realmSettingsPage.goToLocalizationRealmOverridesSubTab();
|
||||
|
||||
addBundle();
|
||||
|
||||
cy.findByTestId("editUserLabelBtn-0").click();
|
||||
cy.findByTestId("editUserLabelCancelBtn-0").click();
|
||||
|
||||
cy.findByTestId("editUserLabelBtn-0").click();
|
||||
cy.findByTestId("editUserLabelInput-0").click().clear().type("def");
|
||||
cy.findByTestId("editUserLabelAcceptBtn-0").click();
|
||||
|
||||
cy.findByTestId("editable-rows-table")
|
||||
.contains("td", "def")
|
||||
.should("be.visible");
|
||||
|
||||
cy.findByTestId("realmOverrides-deleteKebabToggle").click();
|
||||
cy.findByTestId("confirm").click();
|
||||
|
||||
masthead.checkNotificationMessage(
|
||||
"Successfully removed message(s) from the bundle.",
|
||||
);
|
||||
});
|
||||
|
||||
it("Effective Message Bundles - Check before search message", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
realmSettingsPage.goToLocalizationTab();
|
||||
realmSettingsPage.goToLocalizationEffectiveMessageBundlesSubTab();
|
||||
cy.contains("h1", "Search for effective messages");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility tests for realm settings", () => {
|
||||
beforeEach(() => {
|
||||
loginPage.logIn();
|
||||
|
@ -226,18 +335,32 @@ describe("Realm settings tabs tests", () => {
|
|||
cy.checkA11y();
|
||||
});
|
||||
|
||||
it("Check a11y violations on localization tab", () => {
|
||||
it("Check a11y violations on localization locales sub tab", () => {
|
||||
realmSettingsPage.goToLocalizationTab();
|
||||
realmSettingsPage.goToLocalizationLocalesSubTab();
|
||||
cy.checkA11y();
|
||||
});
|
||||
|
||||
it("Check a11y violations on localization tab/ adding message bundle", () => {
|
||||
it("Check a11y violations on localization realm overrides sub tab", () => {
|
||||
realmSettingsPage.goToLocalizationTab();
|
||||
realmSettingsPage.goToLocalizationRealmOverridesSubTab();
|
||||
cy.checkA11y();
|
||||
});
|
||||
|
||||
it("Check a11y violations on localization realm overrides sub tab/ adding message bundle", () => {
|
||||
realmSettingsPage.goToLocalizationTab();
|
||||
realmSettingsPage.goToLocalizationRealmOverridesSubTab();
|
||||
cy.findByTestId("add-bundle-button").click();
|
||||
cy.checkA11y();
|
||||
modalUtils.cancelModal();
|
||||
});
|
||||
|
||||
it("Check a11y violations on localization effective message bundles sub tab", () => {
|
||||
realmSettingsPage.goToLocalizationTab();
|
||||
realmSettingsPage.goToLocalizationEffectiveMessageBundlesSubTab();
|
||||
cy.checkA11y();
|
||||
});
|
||||
|
||||
it("Check a11y violations on security defenses tab", () => {
|
||||
realmSettingsPage.goToSecurityDefensesTab();
|
||||
cy.checkA11y();
|
||||
|
|
|
@ -238,6 +238,10 @@ export default class RealmSettingsPage extends CommonPage {
|
|||
#port = "#kc-port";
|
||||
|
||||
#publicKeyBtn = ".kc-keys-list > tbody > tr > td > .button-wrapper > button";
|
||||
#localizationLocalesSubTab = "rs-localization-locales-tab";
|
||||
#localizationRealmOverridesSubTab = "rs-localization-realm-overrides-tab";
|
||||
#localizationEffectiveMessageBundlesSubTab =
|
||||
"rs-localization-effective-message-bundles-tab";
|
||||
#realmSettingsEventsTab = new RealmSettingsEventsTab();
|
||||
#realmId = 'input[aria-label="Copyable input"]';
|
||||
#securityDefensesHeadersSaveBtn = "headers-form-tab-save";
|
||||
|
@ -456,7 +460,7 @@ export default class RealmSettingsPage extends CommonPage {
|
|||
cy.findByTestId(this.keyInput).type(key);
|
||||
cy.findByTestId(this.valueInput).type(value);
|
||||
|
||||
cy.findByTestId(this.confirmAddBundle).click();
|
||||
cy.findByTestId(this.confirmAddBundle).click({ force: true });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -1260,6 +1264,21 @@ export default class RealmSettingsPage extends CommonPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
goToLocalizationLocalesSubTab() {
|
||||
cy.findByTestId(this.#localizationLocalesSubTab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
goToLocalizationRealmOverridesSubTab() {
|
||||
cy.findByTestId(this.#localizationRealmOverridesSubTab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
goToLocalizationEffectiveMessageBundlesSubTab() {
|
||||
cy.findByTestId(this.#localizationEffectiveMessageBundlesSubTab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
goToSecurityDefensesTab() {
|
||||
cy.findByTestId(this.securityDefensesTab).click();
|
||||
return this;
|
||||
|
|
|
@ -2404,7 +2404,7 @@ id=ID
|
|||
join=Join
|
||||
clientUpdaterSourceGroupsHelp=The condition checks the group of the entity who tries to create/update the client to determine whether the policy is applied.
|
||||
idTokenEncryptionContentEncryptionAlgorithmHelp=JWA Algorithm used for content encryption in encrypting ID tokens. This option is needed just if you want encrypted ID tokens. If left empty, ID Tokens are just signed, but not encrypted.
|
||||
messageBundleDescription=You can edit the supported locales. If you haven't selected supported locales yet, you can only edit the English locale.
|
||||
messageBundleDescription=You can only edit the supported locales. If you haven't selected supported locales yet, you can only edit the English locale.
|
||||
saveEventListenersError=Error saving event listener\: {{error}}
|
||||
scopesHelp=The scopes to be sent when asking for authorization. It can be a space-separated list of scopes. Defaults to 'openid'.
|
||||
multivalued.tooltip=Indicates if attribute supports multiple values. If true, the list of all values of this attribute will be set as claim. If false, just first value will be set as claim
|
||||
|
@ -2941,6 +2941,28 @@ invalidEmailMessage='{{0}}': Invalid email address.
|
|||
missingLastNameMessage='{{0}}': Please specify last name.
|
||||
missingEmailMessage='{{0}}': Please specify email.
|
||||
missingPasswordMessage='{{0}}': Please specify password.
|
||||
locales=Locales
|
||||
realmOverrides=Realm overrides
|
||||
realmOverridesHelp=You can only edit the supported locales. If you haven't selected supported locale yet, you only can edit English locale.
|
||||
effectiveMessageBundles=Effective message bundles
|
||||
effectiveMessageBundlesHelp=You can search for effective message bundles based on themes, features, language, and free text.
|
||||
deleteMessageBundle=Delete message bundle {{key}}
|
||||
deleteAllMessagesBundleSuccess=Successfully removed message(s) from the bundle.
|
||||
deleteAllMessagesBundleError=Error removing message(s) from the bundle, {{error}}
|
||||
noRealmOverridesSearchResultsInstructions=Click on the search bar above to search for realm overrides
|
||||
emptyEffectiveMessageBundles=Search for effective messages
|
||||
emptyEffectiveMessageBundlesInstructions=You can search for the effective messages you want by theme, feature and language in the search box above.
|
||||
searchForEffectiveMessageBundles=Search for message bundle
|
||||
selectAll=Select all
|
||||
theme=Theme
|
||||
themeType=Theme Type
|
||||
language=Language
|
||||
hasWords=Has words
|
||||
deleteConfirmMessageBundle=Delete message bundle?
|
||||
messageBundleDeleteConfirmDialog=Are you sure you want to permanently delete {{count}} selected message bundles
|
||||
selectTheme=Select theme
|
||||
selectThemeType=Select theme type
|
||||
selectLanguage=Select language
|
||||
referral=Referral
|
||||
referralHelp=Specifies if LDAP referrals should be followed or ignored. Please note that enabling referrals can slow down authentication as it allows the LDAP server to decide which other LDAP servers to use. This could potentially include untrusted servers.
|
||||
authenticatorRefConfig.value.help=Add a custom reference name for the authenticator. When this authenticator is successfully completed during an authentication flow, the Authentication Method Reference (AMR) protocol mapper will use this value to populate the amr claim of the generated tokens. Note, the AMR protocol must be configured for the given client to populate the AMR claim
|
||||
|
|
|
@ -45,7 +45,7 @@ import { useRealm } from "../context/realm-context/RealmContext";
|
|||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||
import { useWhoAmI } from "../context/whoami/WhoAmI";
|
||||
import { DEFAULT_LOCALE } from "../i18n/i18n";
|
||||
import { convertToFormValues } from "../util";
|
||||
import { convertToFormValues, localeToDisplayName } from "../util";
|
||||
import { useFetch } from "../utils/useFetch";
|
||||
import useLocaleSort, { mapByKey } from "../utils/useLocaleSort";
|
||||
import { AddMessageBundleModal } from "./AddMessageBundleModal";
|
||||
|
@ -74,16 +74,6 @@ export type BundleForm = {
|
|||
messageBundle: KeyValueType;
|
||||
};
|
||||
|
||||
const localeToDisplayName = (locale: string, displayLocale: string) => {
|
||||
try {
|
||||
return new Intl.DisplayNames([displayLocale], { type: "language" }).of(
|
||||
locale,
|
||||
);
|
||||
} catch (error) {
|
||||
return locale;
|
||||
}
|
||||
};
|
||||
|
||||
export const LocalizationTab = ({ save, realm }: LocalizationTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [addMessageBundleModalOpen, setAddMessageBundleModalOpen] =
|
||||
|
|
|
@ -33,7 +33,7 @@ import { convertFormValuesToObject, convertToFormValues } from "../util";
|
|||
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
|
||||
import { RealmSettingsEmailTab } from "./EmailTab";
|
||||
import { RealmSettingsGeneralTab } from "./GeneralTab";
|
||||
import { LocalizationTab } from "./LocalizationTab";
|
||||
import { LocalizationTab } from "./localization/LocalizationTab";
|
||||
import { RealmSettingsLoginTab } from "./LoginTab";
|
||||
import { PartialExportDialog } from "./PartialExport";
|
||||
import { PartialImportDialog } from "./PartialImport";
|
||||
|
@ -340,12 +340,7 @@ export const RealmSettingsTabs = ({
|
|||
data-testid="rs-localization-tab"
|
||||
{...localizationTab}
|
||||
>
|
||||
<LocalizationTab
|
||||
key={key}
|
||||
refresh={refresh}
|
||||
save={save}
|
||||
realm={realm}
|
||||
/>
|
||||
<LocalizationTab key={key} save={save} realm={realm} />
|
||||
</Tab>
|
||||
<Tab
|
||||
title={<TabTitleText>{t("securityDefences")}</TabTitleText>}
|
||||
|
|
|
@ -0,0 +1,505 @@
|
|||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
Divider,
|
||||
Dropdown,
|
||||
DropdownToggle,
|
||||
Flex,
|
||||
FlexItem,
|
||||
Form,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
import { pickBy } from "lodash-es";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { adminClient } from "../../admin-client";
|
||||
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
|
||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
import { useWhoAmI } from "../../context/whoami/WhoAmI";
|
||||
import { DEFAULT_LOCALE } from "../../i18n/i18n";
|
||||
import { localeToDisplayName } from "../../util";
|
||||
|
||||
type EffectiveMessageBundlesProps = {
|
||||
defaultSupportedLocales: string[];
|
||||
};
|
||||
|
||||
type EffectiveMessageBundlesSearchForm = {
|
||||
theme: string;
|
||||
themeType: string;
|
||||
locale: string;
|
||||
hasWords: string[];
|
||||
};
|
||||
|
||||
const defaultValues: EffectiveMessageBundlesSearchForm = {
|
||||
theme: "",
|
||||
themeType: "",
|
||||
locale: "",
|
||||
hasWords: [],
|
||||
};
|
||||
|
||||
export const EffectiveMessageBundles = ({
|
||||
defaultSupportedLocales,
|
||||
}: EffectiveMessageBundlesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { realm } = useRealm();
|
||||
const serverInfo = useServerInfo();
|
||||
const { whoAmI } = useWhoAmI();
|
||||
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false);
|
||||
const [searchPerformed, setSearchPerformed] = useState(false);
|
||||
const [selectThemesOpen, setSelectThemesOpen] = useState(false);
|
||||
const [selectThemeTypeOpen, setSelectThemeTypeOpen] = useState(false);
|
||||
const [selectLanguageOpen, setSelectLanguageOpen] = useState(false);
|
||||
const [activeFilters, setActiveFilters] = useState<
|
||||
Partial<EffectiveMessageBundlesSearchForm>
|
||||
>({});
|
||||
const themes = serverInfo.themes;
|
||||
const themeKeys = themes
|
||||
? Object.keys(themes).sort((a, b) => a.localeCompare(b))
|
||||
: [];
|
||||
const themeTypes = Object.values(themes!)
|
||||
.flatMap((theme) => theme.map((item) => item.name))
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
const filterLabels: Record<keyof EffectiveMessageBundlesSearchForm, string> =
|
||||
{
|
||||
theme: t("theme"),
|
||||
themeType: t("themeType"),
|
||||
locale: t("language"),
|
||||
hasWords: t("hasWords"),
|
||||
};
|
||||
|
||||
const {
|
||||
getValues,
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
control,
|
||||
} = useForm<EffectiveMessageBundlesSearchForm>({
|
||||
mode: "onChange",
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const loader = async () => {
|
||||
try {
|
||||
const filter = getValues();
|
||||
const messages = await adminClient.serverInfo.findEffectiveMessageBundles(
|
||||
{
|
||||
realm,
|
||||
...filter,
|
||||
locale: filter.locale || DEFAULT_LOCALE,
|
||||
source: true,
|
||||
},
|
||||
);
|
||||
|
||||
return filter.hasWords.length > 0
|
||||
? messages.filter((m) =>
|
||||
filter.hasWords.some((f) => m.value.includes(f)),
|
||||
)
|
||||
: messages;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
function submitSearch() {
|
||||
setSearchDropdownOpen(false);
|
||||
commitFilters();
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
reset();
|
||||
commitFilters();
|
||||
}
|
||||
|
||||
function removeFilter(key: keyof EffectiveMessageBundlesSearchForm) {
|
||||
const formValues: EffectiveMessageBundlesSearchForm = { ...getValues() };
|
||||
delete formValues[key];
|
||||
|
||||
reset({ ...defaultValues, ...formValues });
|
||||
commitFilters();
|
||||
}
|
||||
|
||||
function removeFilterValue(
|
||||
key: keyof EffectiveMessageBundlesSearchForm,
|
||||
valueToRemove: string,
|
||||
) {
|
||||
const formValues = getValues();
|
||||
const fieldValue = formValues[key];
|
||||
const newFieldValue = Array.isArray(fieldValue)
|
||||
? fieldValue.filter((val) => val !== valueToRemove)
|
||||
: fieldValue;
|
||||
|
||||
reset({ ...formValues, [key]: newFieldValue });
|
||||
commitFilters();
|
||||
}
|
||||
|
||||
function commitFilters() {
|
||||
const newFilters: Partial<EffectiveMessageBundlesSearchForm> = pickBy(
|
||||
getValues(),
|
||||
(value) => value !== "" || (Array.isArray(value) && value.length > 0),
|
||||
);
|
||||
|
||||
setActiveFilters(newFilters);
|
||||
setKey(key + 1);
|
||||
}
|
||||
|
||||
const effectiveMessageBunldesSearchFormDisplay = () => {
|
||||
return (
|
||||
<Flex
|
||||
direction={{ default: "column" }}
|
||||
spaceItems={{ default: "spaceItemsNone" }}
|
||||
>
|
||||
<FlexItem>
|
||||
<Dropdown
|
||||
id="effective-message-bundles-search-select"
|
||||
data-testid="EffectiveMessageBundlesSearchSelector"
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
data-testid="effectiveMessageBundlesSearchSelectorToggle"
|
||||
onToggle={(isOpen) => setSearchDropdownOpen(isOpen)}
|
||||
>
|
||||
{t("searchForEffectiveMessageBundles")}
|
||||
</DropdownToggle>
|
||||
}
|
||||
isOpen={searchDropdownOpen}
|
||||
>
|
||||
<Form
|
||||
isHorizontal
|
||||
className="pf-c-form pf-u-mx-lg pf-u-mb-lg"
|
||||
data-testid="effectiveMessageBundlesSearchForm"
|
||||
>
|
||||
<FormGroup label={t("theme")} fieldId="kc-theme">
|
||||
<Controller
|
||||
name="theme"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
name="theme"
|
||||
data-testid="effective_message_bundles-theme-searchField"
|
||||
chipGroupProps={{
|
||||
numChips: 1,
|
||||
expandedText: t("hide"),
|
||||
collapsedText: t("showRemaining"),
|
||||
}}
|
||||
variant={SelectVariant.single}
|
||||
typeAheadAriaLabel="Select"
|
||||
onToggle={(isOpen) => setSelectThemesOpen(isOpen)}
|
||||
selections={field.value}
|
||||
onSelect={(_, selectedValue) => {
|
||||
field.onChange(selectedValue.toString());
|
||||
}}
|
||||
onClear={(theme) => {
|
||||
theme.stopPropagation();
|
||||
field.onChange("");
|
||||
}}
|
||||
isOpen={selectThemesOpen}
|
||||
aria-labelledby={t("theme")}
|
||||
chipGroupComponent={
|
||||
<ChipGroup>
|
||||
<Chip
|
||||
key={field.value}
|
||||
onClick={(theme) => {
|
||||
theme.stopPropagation();
|
||||
field.onChange("");
|
||||
}}
|
||||
>
|
||||
{field.value}
|
||||
</Chip>
|
||||
</ChipGroup>
|
||||
}
|
||||
>
|
||||
{[
|
||||
<SelectOption
|
||||
key="theme_placeholder"
|
||||
value="Select theme"
|
||||
label={t("selectTheme")}
|
||||
className="kc__effective_message_bundles_search_theme__placeholder"
|
||||
isDisabled
|
||||
/>,
|
||||
].concat(
|
||||
themeKeys.map((option) => (
|
||||
<SelectOption key={option} value={option} />
|
||||
)),
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={t("themeType")} fieldId="kc-themeType">
|
||||
<Controller
|
||||
name="themeType"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
name="themeType"
|
||||
data-testid="effective-message-bundles-feature-searchField"
|
||||
chipGroupProps={{
|
||||
numChips: 1,
|
||||
expandedText: t("hide"),
|
||||
collapsedText: t("showRemaining"),
|
||||
}}
|
||||
variant={SelectVariant.single}
|
||||
typeAheadAriaLabel="Select"
|
||||
onToggle={(isOpen) => setSelectThemeTypeOpen(isOpen)}
|
||||
selections={field.value}
|
||||
onSelect={(_, selectedValue) => {
|
||||
field.onChange(selectedValue.toString());
|
||||
}}
|
||||
onClear={(themeType) => {
|
||||
themeType.stopPropagation();
|
||||
field.onChange("");
|
||||
}}
|
||||
isOpen={selectThemeTypeOpen}
|
||||
aria-labelledby={t("themeType")}
|
||||
chipGroupComponent={
|
||||
<ChipGroup>
|
||||
<Chip
|
||||
key={field.value}
|
||||
onClick={(themeType) => {
|
||||
themeType.stopPropagation();
|
||||
field.onChange("");
|
||||
}}
|
||||
>
|
||||
{field.value}
|
||||
</Chip>
|
||||
</ChipGroup>
|
||||
}
|
||||
>
|
||||
{[
|
||||
<SelectOption
|
||||
key="themeType_placeholder"
|
||||
value="Select theme type"
|
||||
label={t("selectThemeType")}
|
||||
className="pf-m-plain"
|
||||
isDisabled
|
||||
/>,
|
||||
].concat(
|
||||
themeTypes.map((option) => (
|
||||
<SelectOption key={option} value={option} />
|
||||
)),
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={t("language")} fieldId="kc-language">
|
||||
<Controller
|
||||
name="locale"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
name="language"
|
||||
data-testid="effective-message-bundles-language-searchField"
|
||||
chipGroupProps={{
|
||||
numChips: 1,
|
||||
expandedText: t("hide"),
|
||||
collapsedText: t("showRemaining"),
|
||||
}}
|
||||
variant={SelectVariant.single}
|
||||
typeAheadAriaLabel="Select"
|
||||
onToggle={(isOpen) => setSelectLanguageOpen(isOpen)}
|
||||
selections={field.value}
|
||||
onSelect={(_, selectedValue) => {
|
||||
field.onChange(selectedValue.toString());
|
||||
}}
|
||||
onClear={(language) => {
|
||||
language.stopPropagation();
|
||||
field.onChange("");
|
||||
}}
|
||||
isOpen={selectLanguageOpen}
|
||||
aria-labelledby="language"
|
||||
chipGroupComponent={
|
||||
<ChipGroup>
|
||||
{field.value ? (
|
||||
<Chip
|
||||
key={field.value}
|
||||
onClick={(language) => {
|
||||
language.stopPropagation();
|
||||
field.onChange("");
|
||||
}}
|
||||
>
|
||||
{localeToDisplayName(
|
||||
field.value,
|
||||
whoAmI.getLocale(),
|
||||
)}
|
||||
</Chip>
|
||||
) : null}
|
||||
</ChipGroup>
|
||||
}
|
||||
>
|
||||
{[
|
||||
<SelectOption
|
||||
key="language_placeholder"
|
||||
value="Select language"
|
||||
label={t("selectLanguage")}
|
||||
className="pf-m-plain"
|
||||
isDisabled
|
||||
/>,
|
||||
].concat(
|
||||
defaultSupportedLocales.map((option) => (
|
||||
<SelectOption key={option} value={option}>
|
||||
{localeToDisplayName(option, whoAmI.getLocale())}
|
||||
</SelectOption>
|
||||
)),
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={t("hasWords")} fieldId="kc-hasWords">
|
||||
<Controller
|
||||
name="hasWords"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<KeycloakTextInput
|
||||
id="kc-hasWords"
|
||||
data-testid="effective-message-bundles-hasWords-searchField"
|
||||
value={field.value.join(" ")}
|
||||
onChange={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const words = target.value
|
||||
.split(" ")
|
||||
.map((word) => word.trim());
|
||||
field.onChange(words);
|
||||
}}
|
||||
/>
|
||||
<ChipGroup>
|
||||
{field.value.map((word, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const newWords = field.value.filter(
|
||||
(_, i) => i !== index,
|
||||
);
|
||||
field.onChange(newWords);
|
||||
}}
|
||||
>
|
||||
{word}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => {
|
||||
setSearchPerformed(true);
|
||||
submitSearch();
|
||||
}}
|
||||
data-testid="search-effective-message-bundles-btn"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("search")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={resetSearch}
|
||||
data-testid="reset-search-effective-message-bundles-btn"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
</Dropdown>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
{Object.entries(activeFilters).length > 0 && (
|
||||
<div className="keycloak__searchChips pf-u-ml-md">
|
||||
{Object.entries(activeFilters).map((filter) => {
|
||||
const [key, value] = filter as [
|
||||
keyof EffectiveMessageBundlesSearchForm,
|
||||
string | string[],
|
||||
];
|
||||
|
||||
return (
|
||||
<ChipGroup
|
||||
className="pf-u-mt-md pf-u-mr-md"
|
||||
key={key}
|
||||
categoryName={filterLabels[key]}
|
||||
isClosable
|
||||
onClick={() => removeFilter(key)}
|
||||
>
|
||||
{typeof value === "string" ? (
|
||||
<Chip isReadOnly>
|
||||
{key === "locale"
|
||||
? localeToDisplayName(value, whoAmI.getLocale())
|
||||
: value}
|
||||
</Chip>
|
||||
) : (
|
||||
value.map((entry) => (
|
||||
<Chip
|
||||
key={entry}
|
||||
onClick={() => removeFilterValue(key, entry)}
|
||||
>
|
||||
{entry}
|
||||
</Chip>
|
||||
))
|
||||
)}
|
||||
</ChipGroup>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
if (!searchPerformed) {
|
||||
return (
|
||||
<>
|
||||
<div className="pf-u-py-lg pf-u-pl-md">
|
||||
{effectiveMessageBunldesSearchFormDisplay()}
|
||||
</div>
|
||||
<Divider />
|
||||
<ListEmptyState
|
||||
message={t("emptyEffectiveMessageBundles")}
|
||||
instructions={t("emptyEffectiveMessageBundlesInstructions")}
|
||||
isSearchVariant
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
ariaLabelKey="effectiveMessageBundles"
|
||||
toolbarItem={effectiveMessageBunldesSearchFormDisplay()}
|
||||
columns={[
|
||||
{
|
||||
name: "key",
|
||||
displayKey: "key",
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
displayKey: "value",
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("noSearchResults")}
|
||||
instructions={t("noSearchResultsInstructions")}
|
||||
/>
|
||||
}
|
||||
isSearching={Object.keys(activeFilters).length > 0}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,275 @@
|
|||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Switch,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
Tabs,
|
||||
} from "@patternfly/react-core";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HelpItem } from "ui-shared";
|
||||
import { FormAccess } from "../../components/form/FormAccess";
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
import { useWhoAmI } from "../../context/whoami/WhoAmI";
|
||||
import { DEFAULT_LOCALE } from "../../i18n/i18n";
|
||||
import { convertToFormValues, localeToDisplayName } from "../../util";
|
||||
import { EffectiveMessageBundles } from "./EffectiveMessageBundles";
|
||||
import { RealmOverrides } from "./RealmOverrides";
|
||||
|
||||
type LocalizationTabProps = {
|
||||
save: (realm: RealmRepresentation) => void;
|
||||
realm: RealmRepresentation;
|
||||
};
|
||||
|
||||
export const LocalizationTab = ({ save, realm }: LocalizationTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { whoAmI } = useWhoAmI();
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const { setValue, control, handleSubmit, formState } = useForm();
|
||||
const [supportedLocalesOpen, setSupportedLocalesOpen] = useState(false);
|
||||
const [defaultLocaleOpen, setDefaultLocaleOpen] = useState(false);
|
||||
|
||||
const defaultSupportedLocales = realm.supportedLocales?.length
|
||||
? realm.supportedLocales
|
||||
: [DEFAULT_LOCALE];
|
||||
|
||||
const themeTypes = useServerInfo().themes!;
|
||||
const allLocales = useMemo(() => {
|
||||
const locales = Object.values(themeTypes).flatMap((theme) =>
|
||||
theme.flatMap(({ locales }) => (locales ? locales : [])),
|
||||
);
|
||||
return Array.from(new Set(locales));
|
||||
}, [themeTypes]);
|
||||
|
||||
const setupForm = () => {
|
||||
convertToFormValues(realm, setValue);
|
||||
setValue("supportedLocales", defaultSupportedLocales);
|
||||
};
|
||||
|
||||
useEffect(setupForm, []);
|
||||
|
||||
const watchSupportedLocales: string[] = useWatch({
|
||||
control,
|
||||
name: "supportedLocales",
|
||||
defaultValue: defaultSupportedLocales,
|
||||
});
|
||||
|
||||
const internationalizationEnabled = useWatch({
|
||||
control,
|
||||
name: "internationalizationEnabled",
|
||||
defaultValue: realm.internationalizationEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onSelect={(_, key) => setActiveTab(key as number)}
|
||||
>
|
||||
<Tab
|
||||
id="locales"
|
||||
eventKey={0}
|
||||
title={<TabTitleText>{t("locales")}</TabTitleText>}
|
||||
data-testid="rs-localization-locales-tab"
|
||||
>
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
role="manage-realm"
|
||||
className="pf-u-mt-lg pf-u-ml-md"
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<FormGroup
|
||||
label={t("internationalization")}
|
||||
fieldId="kc-internationalization"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("internationalizationHelp")}
|
||||
fieldLabelId="internationalization"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="internationalizationEnabled"
|
||||
control={control}
|
||||
defaultValue={realm.internationalizationEnabled}
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
id="kc-l-internationalization"
|
||||
className="pf-u-mt-sm"
|
||||
label={t("enabled")}
|
||||
labelOff={t("disabled")}
|
||||
isChecked={field.value}
|
||||
data-testid={
|
||||
field.value
|
||||
? "internationalization-enabled"
|
||||
: "internationalization-disabled"
|
||||
}
|
||||
onChange={field.onChange}
|
||||
aria-label={t("internationalization")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{internationalizationEnabled && (
|
||||
<>
|
||||
<FormGroup
|
||||
label={t("supportedLocales")}
|
||||
fieldId="kc-l-supported-locales"
|
||||
>
|
||||
<Controller
|
||||
name="supportedLocales"
|
||||
control={control}
|
||||
defaultValue={defaultSupportedLocales}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
toggleId="kc-l-supported-locales"
|
||||
onToggle={(open) => {
|
||||
setSupportedLocalesOpen(open);
|
||||
}}
|
||||
onSelect={(_, v) => {
|
||||
const option = v as string;
|
||||
if (field.value.includes(option)) {
|
||||
field.onChange(
|
||||
field.value.filter(
|
||||
(item: string) => item !== option,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
field.onChange([...field.value, option]);
|
||||
}
|
||||
}}
|
||||
onClear={() => {
|
||||
field.onChange([]);
|
||||
}}
|
||||
selections={field.value}
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
aria-label={t("supportedLocales")}
|
||||
isOpen={supportedLocalesOpen}
|
||||
placeholderText={t("selectLocales")}
|
||||
>
|
||||
{allLocales.map((locale) => (
|
||||
<SelectOption
|
||||
selected={field.value.includes(locale)}
|
||||
key={locale}
|
||||
value={locale}
|
||||
>
|
||||
{localeToDisplayName(locale, whoAmI.getLocale())}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("defaultLocale")}
|
||||
fieldId="kc-l-default-locale"
|
||||
>
|
||||
<Controller
|
||||
name="defaultLocale"
|
||||
control={control}
|
||||
defaultValue={DEFAULT_LOCALE}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
toggleId="kc-default-locale"
|
||||
onToggle={() => setDefaultLocaleOpen(!defaultLocaleOpen)}
|
||||
onSelect={(_, value) => {
|
||||
field.onChange(value as string);
|
||||
setDefaultLocaleOpen(false);
|
||||
}}
|
||||
selections={
|
||||
field.value
|
||||
? localeToDisplayName(field.value, whoAmI.getLocale())
|
||||
: realm.defaultLocale !== ""
|
||||
? localeToDisplayName(
|
||||
realm.defaultLocale || DEFAULT_LOCALE,
|
||||
whoAmI.getLocale(),
|
||||
)
|
||||
: t("placeholderText")
|
||||
}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("defaultLocale")}
|
||||
isOpen={defaultLocaleOpen}
|
||||
placeholderText={t("placeholderText")}
|
||||
data-testid="select-default-locale"
|
||||
>
|
||||
{watchSupportedLocales.map((locale, idx) => (
|
||||
<SelectOption
|
||||
key={`default-locale-${idx}`}
|
||||
value={locale}
|
||||
>
|
||||
{localeToDisplayName(locale, whoAmI.getLocale())}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
isDisabled={!formState.isDirty}
|
||||
type="submit"
|
||||
data-testid="localization-tab-save"
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={setupForm}>
|
||||
{t("revert")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="realm-overrides"
|
||||
eventKey={1}
|
||||
title={
|
||||
<TabTitleText>
|
||||
{t("realmOverrides")}{" "}
|
||||
<HelpItem
|
||||
fieldLabelId="realm-overrides"
|
||||
helpText={t("realmOverridesHelp")}
|
||||
noVerticalAlign={false}
|
||||
unWrap
|
||||
/>
|
||||
</TabTitleText>
|
||||
}
|
||||
data-testid="rs-localization-realm-overrides-tab"
|
||||
>
|
||||
<RealmOverrides
|
||||
internationalizationEnabled={internationalizationEnabled}
|
||||
watchSupportedLocales={watchSupportedLocales}
|
||||
realm={realm}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="effective-message-bundles"
|
||||
eventKey={2}
|
||||
title={
|
||||
<TabTitleText>
|
||||
{t("effectiveMessageBundles")}
|
||||
<HelpItem
|
||||
fieldLabelId="effective-message-bundles"
|
||||
helpText={t("effectiveMessageBundlesHelp")}
|
||||
noVerticalAlign={false}
|
||||
unWrap
|
||||
/>
|
||||
</TabTitleText>
|
||||
}
|
||||
data-testid="rs-localization-effective-message-bundles-tab"
|
||||
>
|
||||
<EffectiveMessageBundles
|
||||
defaultSupportedLocales={defaultSupportedLocales}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,571 @@
|
|||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Divider,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
Form,
|
||||
FormGroup,
|
||||
KebabToggle,
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
import {
|
||||
CheckIcon,
|
||||
PencilAltIcon,
|
||||
SearchIcon,
|
||||
TimesIcon,
|
||||
} from "@patternfly/react-icons";
|
||||
import {
|
||||
IRow,
|
||||
IRowCell,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from "@patternfly/react-table";
|
||||
import RealmRepresentation from "libs/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import { cloneDeep, isEqual, uniqWith } from "lodash-es";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { adminClient } from "../../admin-client";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||
import { KeyValueType } from "../../components/key-value-form/key-value-convert";
|
||||
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
|
||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||
import { PaginatingTableToolbar } from "../../components/table-toolbar/PaginatingTableToolbar";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useWhoAmI } from "../../context/whoami/WhoAmI";
|
||||
import { DEFAULT_LOCALE } from "../../i18n/i18n";
|
||||
import { localeToDisplayName } from "../../util";
|
||||
import { AddMessageBundleModal } from "../AddMessageBundleModal";
|
||||
|
||||
type RealmOverridesProps = {
|
||||
internationalizationEnabled: boolean;
|
||||
watchSupportedLocales: string[];
|
||||
realm: RealmRepresentation;
|
||||
};
|
||||
|
||||
type EditStatesType = { [key: number]: boolean };
|
||||
|
||||
export type BundleForm = {
|
||||
key: string;
|
||||
value: string;
|
||||
messageBundle: KeyValueType;
|
||||
};
|
||||
|
||||
export enum RowEditAction {
|
||||
Save = "save",
|
||||
Cancel = "cancel",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
}
|
||||
|
||||
export const RealmOverrides = ({
|
||||
internationalizationEnabled,
|
||||
watchSupportedLocales,
|
||||
realm,
|
||||
}: RealmOverridesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [addMessageBundleModalOpen, setAddMessageBundleModalOpen] =
|
||||
useState(false);
|
||||
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
|
||||
const [messageBundles, setMessageBundles] = useState<[string, string][]>([]);
|
||||
const [selectMenuLocale, setSelectMenuLocale] = useState(DEFAULT_LOCALE);
|
||||
const [kebabOpen, setKebabOpen] = useState(false);
|
||||
const { getValues, handleSubmit } = useForm();
|
||||
const [selectMenuValueSelected, setSelectMenuValueSelected] = useState(false);
|
||||
const [tableRows, setTableRows] = useState<IRow[]>([]);
|
||||
const [tableKey, setTableKey] = useState(0);
|
||||
const [max, setMax] = useState(10);
|
||||
const [first, setFirst] = useState(0);
|
||||
const [filter, setFilter] = useState("");
|
||||
const bundleForm = useForm<BundleForm>({ mode: "onChange" });
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { realm: currentRealm } = useRealm();
|
||||
const { whoAmI } = useWhoAmI();
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
const [areAllRowsSelected, setAreAllRowsSelected] = useState(false);
|
||||
const [editStates, setEditStates] = useState<EditStatesType>({});
|
||||
const [formValue, setFormValue] = useState("");
|
||||
const refreshTable = () => {
|
||||
setTableKey(tableKey + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLocalizationTexts = async () => {
|
||||
try {
|
||||
let result = await adminClient.realms.getRealmLocalizationTexts({
|
||||
first,
|
||||
max,
|
||||
realm: realm.realm!,
|
||||
selectedLocale:
|
||||
selectMenuLocale ||
|
||||
getValues("defaultLocale") ||
|
||||
whoAmI.getLocale(),
|
||||
});
|
||||
|
||||
if (filter) {
|
||||
const searchInBundles = (idx: number) => {
|
||||
return Object.entries(result).filter((i) =>
|
||||
i[idx].includes(filter),
|
||||
);
|
||||
};
|
||||
|
||||
const filtered = uniqWith(
|
||||
searchInBundles(0).concat(searchInBundles(1)),
|
||||
isEqual,
|
||||
);
|
||||
|
||||
result = Object.fromEntries(filtered);
|
||||
}
|
||||
|
||||
return Object.entries(result).slice(first, first + max);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
fetchLocalizationTexts().then((bundles) => {
|
||||
setMessageBundles(bundles);
|
||||
|
||||
const updatedRows: IRow[] = bundles.map(
|
||||
(messageBundle): IRow => ({
|
||||
rowEditBtnAriaLabel: () =>
|
||||
t("rowEditBtnAriaLabel", {
|
||||
messageBundle: messageBundle[1],
|
||||
}),
|
||||
rowSaveBtnAriaLabel: () =>
|
||||
t("rowSaveBtnAriaLabel", {
|
||||
messageBundle: messageBundle[1],
|
||||
}),
|
||||
rowCancelBtnAriaLabel: () =>
|
||||
t("rowCancelBtnAriaLabel", {
|
||||
messageBundle: messageBundle[1],
|
||||
}),
|
||||
cells: [
|
||||
{
|
||||
title: messageBundle[0],
|
||||
props: {
|
||||
value: messageBundle[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: messageBundle[1],
|
||||
props: {
|
||||
value: messageBundle[1],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
setTableRows(updatedRows);
|
||||
});
|
||||
}, [tableKey, first, max, filter]);
|
||||
|
||||
const handleModalToggle = () => {
|
||||
setAddMessageBundleModalOpen(!addMessageBundleModalOpen);
|
||||
};
|
||||
|
||||
const options = [
|
||||
<SelectGroup label={t("defaultLocale")} key="group1">
|
||||
<SelectOption key={DEFAULT_LOCALE} value={DEFAULT_LOCALE}>
|
||||
{localeToDisplayName(DEFAULT_LOCALE, whoAmI.getDisplayName())}
|
||||
</SelectOption>
|
||||
</SelectGroup>,
|
||||
<Divider key="divider" />,
|
||||
<SelectGroup label={t("supportedLocales")} key="group2">
|
||||
{watchSupportedLocales.map((locale) => (
|
||||
<SelectOption key={locale} value={locale}>
|
||||
{localeToDisplayName(locale, whoAmI.getLocale())}
|
||||
</SelectOption>
|
||||
))}
|
||||
</SelectGroup>,
|
||||
];
|
||||
|
||||
const addKeyValue = async (pair: KeyValueType): Promise<void> => {
|
||||
try {
|
||||
await adminClient.realms.addLocalization(
|
||||
{
|
||||
realm: currentRealm!,
|
||||
selectedLocale:
|
||||
selectMenuLocale || getValues("defaultLocale") || DEFAULT_LOCALE,
|
||||
key: pair.key,
|
||||
},
|
||||
pair.value,
|
||||
);
|
||||
|
||||
adminClient.setConfig({
|
||||
realmName: currentRealm!,
|
||||
});
|
||||
refreshTable();
|
||||
bundleForm.setValue("key", "");
|
||||
bundleForm.setValue("value", "");
|
||||
addAlert(t("addMessageBundleSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError(t("addMessageBundleError"), error);
|
||||
}
|
||||
};
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "deleteConfirmMessageBundle",
|
||||
messageKey: t("messageBundleDeleteConfirmDialog", {
|
||||
count: selectedRowKeys.length,
|
||||
}),
|
||||
continueButtonLabel: "delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
for (const key of selectedRowKeys) {
|
||||
await adminClient.realms.deleteRealmLocalizationTexts({
|
||||
realm: currentRealm!,
|
||||
selectedLocale: selectMenuLocale,
|
||||
key: key,
|
||||
});
|
||||
}
|
||||
setAreAllRowsSelected(false);
|
||||
setSelectedRowKeys([]);
|
||||
refreshTable();
|
||||
addAlert(t("deleteAllMessagesBundleSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError("deleteAllMessagesBundleError", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleRowSelect = (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
rowIndex: number,
|
||||
) => {
|
||||
const selectedKey = (tableRows[rowIndex].cells?.[0] as IRowCell).props
|
||||
.value;
|
||||
if (event.target.checked) {
|
||||
setSelectedRowKeys((prevSelected) => [...prevSelected, selectedKey]);
|
||||
} else {
|
||||
setSelectedRowKeys((prevSelected) =>
|
||||
prevSelected.filter((key) => key !== selectedKey),
|
||||
);
|
||||
}
|
||||
|
||||
setAreAllRowsSelected(
|
||||
tableRows.length ===
|
||||
selectedRowKeys.length + (event.target.checked ? 1 : -1),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleSelectAllRows = () => {
|
||||
if (areAllRowsSelected) {
|
||||
setSelectedRowKeys([]);
|
||||
} else {
|
||||
setSelectedRowKeys(
|
||||
tableRows.map((row) => (row.cells?.[0] as IRowCell).props.value),
|
||||
);
|
||||
}
|
||||
setAreAllRowsSelected(!areAllRowsSelected);
|
||||
};
|
||||
|
||||
const isRowSelected = (key: any) => {
|
||||
return selectedRowKeys.includes(key);
|
||||
};
|
||||
|
||||
const onSubmit = async (inputValue: string, rowIndex: number) => {
|
||||
const newRows = cloneDeep(tableRows);
|
||||
|
||||
const newRow = cloneDeep(newRows[rowIndex]);
|
||||
(newRow.cells?.[1] as IRowCell).props.value = inputValue;
|
||||
newRows[rowIndex] = newRow;
|
||||
|
||||
try {
|
||||
const key = (newRow.cells?.[0] as IRowCell).props.value;
|
||||
const value = (newRow.cells?.[1] as IRowCell).props.value;
|
||||
|
||||
await adminClient.realms.addLocalization(
|
||||
{
|
||||
realm: realm.realm!,
|
||||
selectedLocale:
|
||||
selectMenuLocale || getValues("defaultLocale") || DEFAULT_LOCALE,
|
||||
key,
|
||||
},
|
||||
value,
|
||||
);
|
||||
|
||||
addAlert(t("updateMessageBundleSuccess"), AlertVariant.success);
|
||||
setTableRows(newRows);
|
||||
} catch (error) {
|
||||
addAlert(t("updateMessageBundleError"), AlertVariant.danger);
|
||||
}
|
||||
|
||||
setEditStates((prevEditStates) => ({
|
||||
...prevEditStates,
|
||||
[rowIndex]: false,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
{addMessageBundleModalOpen && (
|
||||
<AddMessageBundleModal
|
||||
handleModalToggle={handleModalToggle}
|
||||
save={(pair: any) => {
|
||||
addKeyValue(pair);
|
||||
handleModalToggle();
|
||||
}}
|
||||
form={bundleForm}
|
||||
/>
|
||||
)}
|
||||
<PaginatingTableToolbar
|
||||
count={messageBundles.length}
|
||||
first={first}
|
||||
max={max}
|
||||
onNextClick={setFirst}
|
||||
onPreviousClick={setFirst}
|
||||
onPerPageSelect={(first, max) => {
|
||||
setFirst(first);
|
||||
setMax(max);
|
||||
}}
|
||||
inputGroupName={"search"}
|
||||
inputGroupOnEnter={(search) => {
|
||||
setFilter(search);
|
||||
setFirst(0);
|
||||
setMax(10);
|
||||
}}
|
||||
inputGroupPlaceholder={t("searchForMessageBundle")}
|
||||
toolbarItem={
|
||||
<>
|
||||
<Button
|
||||
data-testid="add-bundle-button"
|
||||
onClick={() => setAddMessageBundleModalOpen(true)}
|
||||
>
|
||||
{t("addMessageBundle")}
|
||||
</Button>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
toggle={
|
||||
<KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />
|
||||
}
|
||||
isOpen={kebabOpen}
|
||||
isPlain
|
||||
data-testid="toolbar-deleteBtn"
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="action"
|
||||
component="button"
|
||||
data-testid="delete-selected-bundleBtn"
|
||||
isDisabled={
|
||||
messageBundles.length === 0 ||
|
||||
selectedRowKeys.length === 0
|
||||
}
|
||||
onClick={() => {
|
||||
toggleDeleteDialog();
|
||||
setKebabOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
searchTypeComponent={
|
||||
<ToolbarItem>
|
||||
<Select
|
||||
width={180}
|
||||
isOpen={filterDropdownOpen}
|
||||
className="kc-filter-by-locale-select"
|
||||
variant={SelectVariant.single}
|
||||
isDisabled={!internationalizationEnabled}
|
||||
onToggle={(isExpanded) => setFilterDropdownOpen(isExpanded)}
|
||||
onSelect={(_, value) => {
|
||||
setSelectMenuLocale(value.toString());
|
||||
setSelectMenuValueSelected(true);
|
||||
refreshTable();
|
||||
setFilterDropdownOpen(false);
|
||||
}}
|
||||
selections={
|
||||
selectMenuValueSelected
|
||||
? localeToDisplayName(selectMenuLocale, whoAmI.getLocale())
|
||||
: realm.defaultLocale !== ""
|
||||
? localeToDisplayName(DEFAULT_LOCALE, whoAmI.getLocale())
|
||||
: t("placeholderText")
|
||||
}
|
||||
>
|
||||
{options}
|
||||
</Select>
|
||||
</ToolbarItem>
|
||||
}
|
||||
>
|
||||
{messageBundles.length === 0 && !filter && (
|
||||
<ListEmptyState
|
||||
hasIcon
|
||||
message={t("noMessageBundles")}
|
||||
instructions={t("noMessageBundlesInstructions")}
|
||||
onPrimaryAction={handleModalToggle}
|
||||
/>
|
||||
)}
|
||||
{messageBundles.length === 0 && filter && (
|
||||
<ListEmptyState
|
||||
hasIcon
|
||||
icon={SearchIcon}
|
||||
isSearchVariant
|
||||
message={t("noSearchResults")}
|
||||
instructions={t("noRealmOverridesSearchResultsInstructions")}
|
||||
/>
|
||||
)}
|
||||
{messageBundles.length !== 0 && (
|
||||
<Table
|
||||
aria-label={t("editableRowsTable")}
|
||||
data-testid="editable-rows-table"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th className="pf-u-px-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={t("selectAll")}
|
||||
checked={areAllRowsSelected}
|
||||
onChange={toggleSelectAllRows}
|
||||
data-testid="selectAll"
|
||||
/>
|
||||
</Th>
|
||||
<Th className="pf-u-py-lg">{t("key")}</Th>
|
||||
<Th className="pf-u-py-lg">{t("value")}</Th>
|
||||
<Th aria-hidden="true" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{tableRows.map((row, rowIndex) => (
|
||||
<Tr key={(row.cells?.[0] as IRowCell).props.value}>
|
||||
<Td
|
||||
className="pf-u-px-lg"
|
||||
select={{
|
||||
rowIndex,
|
||||
onSelect: (event) =>
|
||||
handleRowSelect(
|
||||
event as ChangeEvent<HTMLInputElement>,
|
||||
rowIndex,
|
||||
),
|
||||
isSelected: isRowSelected(
|
||||
(row.cells?.[0] as IRowCell).props.value,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Td className="pf-m-sm pf-u-px-sm" dataLabel={t("key")}>
|
||||
{(row.cells?.[0] as IRowCell).props.value}
|
||||
</Td>
|
||||
<Td
|
||||
className="pf-m-sm pf-u-px-sm"
|
||||
dataLabel={t("value")}
|
||||
key={rowIndex}
|
||||
>
|
||||
<Form
|
||||
isHorizontal
|
||||
className="kc-form-bundleValue"
|
||||
onSubmit={handleSubmit(() => {
|
||||
onSubmit(formValue, rowIndex);
|
||||
})}
|
||||
>
|
||||
<FormGroup
|
||||
fieldId="kc-bundleValue"
|
||||
className="pf-u-display-inline-block"
|
||||
>
|
||||
{editStates[rowIndex] ? (
|
||||
<>
|
||||
<KeycloakTextInput
|
||||
aria-label={t("editUserLabel")}
|
||||
type="text"
|
||||
className="pf-u-w-initial"
|
||||
data-testid={`editUserLabelInput-${rowIndex}`}
|
||||
value={formValue}
|
||||
onChange={(
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
setFormValue(event.target.value);
|
||||
}}
|
||||
key={`edit-input-${rowIndex}`}
|
||||
/>
|
||||
<Button
|
||||
variant="link"
|
||||
className="pf-m-plain"
|
||||
data-testid={`editUserLabelAcceptBtn-${rowIndex}`}
|
||||
type="submit"
|
||||
icon={<CheckIcon />}
|
||||
/>
|
||||
<Button
|
||||
variant="link"
|
||||
className="pf-m-plain"
|
||||
data-testid={`editUserLabelCancelBtn-${rowIndex}`}
|
||||
icon={<TimesIcon />}
|
||||
onClick={() => {
|
||||
setEditStates((prevEditStates) => ({
|
||||
...prevEditStates,
|
||||
[rowIndex]: false,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>
|
||||
{(row.cells?.[1] as IRowCell).props.value}
|
||||
</span>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const currentValue = (
|
||||
tableRows[rowIndex].cells?.[1] as IRowCell
|
||||
).props.value;
|
||||
setFormValue(currentValue);
|
||||
setEditStates((prevState) => ({
|
||||
...prevState,
|
||||
[rowIndex]: true,
|
||||
}));
|
||||
}}
|
||||
key={`edit-button-${rowIndex}`}
|
||||
aria-label={t("editUserLabel")}
|
||||
variant="link"
|
||||
className="pf-m-plain"
|
||||
data-testid={`editUserLabelBtn-${rowIndex}`}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Td>
|
||||
<Td isActionCell>
|
||||
<Dropdown
|
||||
toggle={
|
||||
<KebabToggle
|
||||
className="pf-m-plain"
|
||||
data-testid="realmOverrides-deleteKebabToggle"
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
setSelectedRowKeys([
|
||||
(row.cells?.[0] as IRowCell).props.value,
|
||||
]);
|
||||
toggleDeleteDialog();
|
||||
setKebabOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</PaginatingTableToolbar>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -169,3 +169,13 @@ export const addTrailingSlash = (url: string) =>
|
|||
url.endsWith("/") ? url : url + "/";
|
||||
|
||||
export const generateId = () => Math.floor(Math.random() * 1000);
|
||||
|
||||
export const localeToDisplayName = (locale: string, displayLocale: string) => {
|
||||
try {
|
||||
return new Intl.DisplayNames([displayLocale], { type: "language" }).of(
|
||||
locale,
|
||||
);
|
||||
} catch (error) {
|
||||
return locale;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export default interface EffectiveMessageBundleRepresentation {
|
||||
theme?: string;
|
||||
themeType?: string;
|
||||
locale?: string;
|
||||
source?: boolean;
|
||||
key: string;
|
||||
value: string;
|
||||
source: "THEME" | "REALM";
|
||||
}
|
||||
|
|
|
@ -3,6 +3,14 @@ import type { ServerInfoRepresentation } from "../defs/serverInfoRepesentation.j
|
|||
import type KeycloakAdminClient from "../index.js";
|
||||
import type EffectiveMessageBundleRepresentation from "../defs/effectiveMessageBundleRepresentation.js";
|
||||
|
||||
export interface MessageBundleQuery {
|
||||
realm: string;
|
||||
theme?: string;
|
||||
themeType?: string;
|
||||
locale?: string;
|
||||
source?: boolean;
|
||||
}
|
||||
|
||||
export class ServerInfo extends Resource {
|
||||
constructor(client: KeycloakAdminClient) {
|
||||
super(client, {
|
||||
|
@ -17,18 +25,12 @@ export class ServerInfo extends Resource {
|
|||
});
|
||||
|
||||
public findEffectiveMessageBundles = this.makeRequest<
|
||||
{
|
||||
realm: string;
|
||||
theme?: string;
|
||||
themeType?: string;
|
||||
locale?: string;
|
||||
source?: boolean;
|
||||
},
|
||||
MessageBundleQuery,
|
||||
EffectiveMessageBundleRepresentation[]
|
||||
>({
|
||||
method: "GET",
|
||||
path: "/resources/{realm}/{themeType}/{locale}",
|
||||
urlParamKeys: ["realm", "themeType", "locale"],
|
||||
queryParamKeys: ["theme", "source"],
|
||||
path: "/resources/{realm}/{theme}/{locale}",
|
||||
urlParamKeys: ["realm", "theme", "locale"],
|
||||
queryParamKeys: ["themeType", "source"],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
*/
|
||||
package org.keycloak.services.resources;
|
||||
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.OPTIONS;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
|
@ -30,9 +34,6 @@ import org.keycloak.services.util.CacheControlUtil;
|
|||
import org.keycloak.services.util.LocaleUtil;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
|
@ -102,6 +103,12 @@ public class ThemeResource {
|
|||
}
|
||||
}
|
||||
|
||||
@Path("/{realm}/{themeType}/{locale}")
|
||||
@OPTIONS
|
||||
public Response localizationTextPreflight() {
|
||||
return Cors.add(session.getContext().getHttpRequest(), Response.ok()).auth().preflight().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{realm}/{themeType}/{locale}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
@ -109,6 +116,9 @@ public class ThemeResource {
|
|||
@PathParam("locale") String localeString, @PathParam("themeType") String themeType,
|
||||
@QueryParam("source") boolean showSource) throws IOException {
|
||||
final RealmModel realm = session.realms().getRealmByName(realmName);
|
||||
if (realm == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
session.getContext().setRealm(realm);
|
||||
List<KeySource> result;
|
||||
|
||||
|
|
Loading…
Reference in a new issue