Introduces new Realm Events tab (#618)
* initial version events tab * added save * fix for admin events * added clear and reset * add type add dialog * add remove and add actions * fix ids * add cofirm disable button * added key * added tests * fixed messages * add disabled on save on not dirty * fixed review comment * fixed test * merged tests
This commit is contained in:
parent
4b5193dcef
commit
544dcd31db
15 changed files with 1097 additions and 94 deletions
|
@ -1,12 +1,16 @@
|
|||
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||
import LoginPage from "../support/pages/LoginPage";
|
||||
import RealmSettingsPage from "../support/pages/admin_console/manage/realm_settings/RealmSettingsPage";
|
||||
import Masthead from "../support/pages/admin_console/Masthead";
|
||||
import ModalUtils from "../support/util/ModalUtils";
|
||||
import { keycloakBefore } from "../support/util/keycloak_before";
|
||||
import AdminClient from "../support/util/AdminClient";
|
||||
import ListingPage from "../support/pages/admin_console/ListingPage";
|
||||
|
||||
// describe("Realm settings test", () => {
|
||||
const loginPage = new LoginPage();
|
||||
const sidebarPage = new SidebarPage();
|
||||
const masthead = new Masthead();
|
||||
const modalUtils = new ModalUtils();
|
||||
const realmSettingsPage = new RealmSettingsPage();
|
||||
|
||||
describe("Realm settings", () => {
|
||||
|
@ -22,11 +26,11 @@ describe("Realm settings", () => {
|
|||
await new AdminClient().createRealm(realmName);
|
||||
});
|
||||
|
||||
// after(async () => {
|
||||
// await new AdminClient().deleteRealm(realmName);
|
||||
// });
|
||||
after(async () => {
|
||||
await new AdminClient().deleteRealm(realmName);
|
||||
});
|
||||
|
||||
it("Go to general tab", function () {
|
||||
it("Go to general tab", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
realmSettingsPage.toggleSwitch(realmSettingsPage.managedAccessSwitch);
|
||||
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
|
||||
|
@ -34,7 +38,7 @@ describe("Realm settings", () => {
|
|||
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
|
||||
});
|
||||
|
||||
it("Go to login tab", function () {
|
||||
it("Go to login tab", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.getId("rs-login-tab").click();
|
||||
realmSettingsPage.toggleSwitch(realmSettingsPage.userRegSwitch);
|
||||
|
@ -43,7 +47,7 @@ describe("Realm settings", () => {
|
|||
realmSettingsPage.toggleSwitch(realmSettingsPage.verifyEmailSwitch);
|
||||
});
|
||||
|
||||
it("Go to email tab", function () {
|
||||
it("Go to email tab", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.getId("rs-email-tab").click();
|
||||
|
||||
|
@ -57,7 +61,7 @@ describe("Realm settings", () => {
|
|||
realmSettingsPage.save(realmSettingsPage.emailSaveBtn);
|
||||
});
|
||||
|
||||
it("Go to themes tab", function () {
|
||||
it("Go to themes tab", () => {
|
||||
cy.wait(5000);
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.getId("rs-themes-tab").click();
|
||||
|
@ -69,7 +73,46 @@ describe("Realm settings", () => {
|
|||
realmSettingsPage.saveThemes();
|
||||
});
|
||||
|
||||
it("Go to keys tab", function () {
|
||||
describe("Events tab", () => {
|
||||
const listingPage = new ListingPage();
|
||||
|
||||
it("Enable user events", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.getId("rs-realm-events-tab").click();
|
||||
|
||||
cy.wait(5000);
|
||||
realmSettingsPage
|
||||
.toggleSwitch(realmSettingsPage.enableEvents)
|
||||
.save(realmSettingsPage.eventsUserSave);
|
||||
masthead.checkNotificationMessage("Successfully saved configuration");
|
||||
|
||||
realmSettingsPage.clearEvents("user");
|
||||
|
||||
modalUtils
|
||||
.checkModalMessage(
|
||||
"If you clear all events of this realm, all records will be permanently cleared in the database"
|
||||
)
|
||||
.confirmModal();
|
||||
|
||||
masthead.checkNotificationMessage("The user events have been cleared");
|
||||
|
||||
const events = ["Client info", "Client info error"];
|
||||
|
||||
cy.intercept("GET", `/auth/admin/realms/${realmName}/events/config`).as(
|
||||
"fetchConfig"
|
||||
);
|
||||
realmSettingsPage.addUserEvents(events).clickAdd();
|
||||
masthead.checkNotificationMessage("Successfully saved configuration");
|
||||
cy.wait(["@fetchConfig"]);
|
||||
cy.get(".pf-c-spinner__tail-ball").should("not.exist");
|
||||
|
||||
for (const event of events) {
|
||||
listingPage.searchItem(event, false).itemExist(event);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("Go to keys tab", () => {
|
||||
cy.wait(5000);
|
||||
|
||||
sidebarPage.goToRealmSettings();
|
||||
|
@ -77,7 +120,7 @@ describe("Realm settings", () => {
|
|||
cy.getId("rs-keys-tab").click();
|
||||
});
|
||||
|
||||
it("add Providers", function () {
|
||||
it("add Providers", () => {
|
||||
cy.wait(5000);
|
||||
sidebarPage.goToRealmSettings();
|
||||
|
||||
|
@ -115,4 +158,3 @@ describe("Realm settings", () => {
|
|||
realmSettingsPage.addProvider();
|
||||
});
|
||||
});
|
||||
// });
|
||||
|
|
|
@ -25,5 +25,5 @@
|
|||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
Cypress.Commands.add("getId", (selector, ...args) => {
|
||||
return cy.get(`[data-testid=${selector}]`, ...args);
|
||||
return cy.get(`[data-testid="${selector}"]`, ...args);
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ export default class ListingPage {
|
|||
const searchUrl = `/auth/admin/realms/master/*${searchValue}*`;
|
||||
cy.intercept(searchUrl).as("search");
|
||||
}
|
||||
cy.get(this.searchInput).type(searchValue);
|
||||
cy.get(this.searchInput).clear().type(searchValue);
|
||||
cy.get(this.searchBtn).click();
|
||||
if (wait) {
|
||||
cy.wait(["@search"]);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const expect = chai.expect;
|
||||
export default class RealmSettingsPage {
|
||||
generalSaveBtn = "general-tab-save";
|
||||
themesSaveBtn = "themes-tab-save";
|
||||
|
@ -28,6 +29,9 @@ export default class RealmSettingsPage {
|
|||
addProviderDropdown = "addProviderDropdown";
|
||||
addProviderButton = "add-provider-button";
|
||||
displayName = "display-name-input";
|
||||
enableEvents = "eventsEnabled";
|
||||
eventsUserSave = "save-user";
|
||||
eventTypeColumn = 'tbody > tr > [data-label="Event saved type"]';
|
||||
|
||||
selectLoginThemeType(themeType: string) {
|
||||
const themesUrl = "/auth/admin/realms/master/themes";
|
||||
|
@ -86,7 +90,7 @@ export default class RealmSettingsPage {
|
|||
}
|
||||
|
||||
toggleSwitch(switchName: string) {
|
||||
cy.getId(switchName).next().click();
|
||||
cy.getId(switchName).click({ force: true });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -120,4 +124,36 @@ export default class RealmSettingsPage {
|
|||
|
||||
return this;
|
||||
}
|
||||
|
||||
clearEvents(type: "admin" | "user") {
|
||||
cy.getId(`clear-${type}-events`).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addUserEvents(events: string[]) {
|
||||
cy.getId("addTypes").click();
|
||||
for (const event of events) {
|
||||
cy.get(this.eventTypeColumn)
|
||||
.contains(event)
|
||||
.parent()
|
||||
.find("input")
|
||||
.click();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
checkUserEvents(events: string[]) {
|
||||
cy.get(this.eventTypeColumn).should((event) => {
|
||||
for (const user of events) {
|
||||
expect(event).to.contain(user);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
clickAdd() {
|
||||
cy.getId("addEventTypeConfirm").click();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,9 @@ const AppContexts = ({ children }: { children: ReactNode }) => (
|
|||
const RealmPathSelector = ({ children }: { children: ReactNode }) => {
|
||||
const { setRealm } = useRealm();
|
||||
const { realm } = useParams<{ realm: string }>();
|
||||
useEffect(() => setRealm(realm), []);
|
||||
useEffect(() => {
|
||||
if (realm) setRealm(realm);
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import React, { isValidElement, ReactNode, useEffect, useState } from "react";
|
||||
import React, {
|
||||
isValidElement,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
IAction,
|
||||
|
@ -175,7 +181,6 @@ export function KeycloakDataTable<T>({
|
|||
const [selected, setSelected] = useState<T[]>([]);
|
||||
const [rows, setRows] = useState<(Row<T> | SubRow<T>)[]>();
|
||||
const [unPaginatedData, setUnPaginatedData] = useState<T[]>();
|
||||
const [filteredData, setFilteredData] = useState<(Row<T> | SubRow<T>)[]>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [max, setMax] = useState(10);
|
||||
|
@ -185,60 +190,8 @@ export function KeycloakDataTable<T>({
|
|||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
useEffect(() => {
|
||||
if (canSelectAll) {
|
||||
const checkboxes = document
|
||||
.getElementsByClassName("pf-c-table__check")
|
||||
.item(0);
|
||||
if (checkboxes) {
|
||||
const checkAllCheckbox = checkboxes.children!.item(
|
||||
0
|
||||
)! as HTMLInputElement;
|
||||
checkAllCheckbox.indeterminate =
|
||||
selected.length > 0 &&
|
||||
selected.length < (filteredData || rows)!.length;
|
||||
}
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
setLoading(true);
|
||||
return unPaginatedData || (await loader(first, max, search));
|
||||
},
|
||||
(data) => {
|
||||
if (!isPaginated) {
|
||||
setUnPaginatedData(data);
|
||||
data = data.slice(first, first + max);
|
||||
}
|
||||
|
||||
const result = convertToColumns(data);
|
||||
setRows(result);
|
||||
setFilteredData(result);
|
||||
setLoading(false);
|
||||
},
|
||||
[key, first, max, search]
|
||||
);
|
||||
|
||||
const getNodeText = (node: Cell<T>): string => {
|
||||
if (["string", "number"].includes(typeof node)) {
|
||||
return node!.toString();
|
||||
}
|
||||
if (node instanceof Array) {
|
||||
return node.map(getNodeText).join("");
|
||||
}
|
||||
if (typeof node === "object" && node) {
|
||||
return getNodeText(
|
||||
isValidElement((node as TitleCell).title)
|
||||
? (node as TitleCell).title.props.children
|
||||
: (node as JSX.Element).props.children
|
||||
);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const convertToColumns = (data: T[]): (Row<T> | SubRow<T>)[] => {
|
||||
return data!
|
||||
return data
|
||||
.map((value, index) => {
|
||||
const disabledRow = isRowDisabled ? isRowDisabled(value) : false;
|
||||
const row: (Row<T> | SubRow<T>)[] = [
|
||||
|
@ -279,18 +232,73 @@ export function KeycloakDataTable<T>({
|
|||
.flat();
|
||||
};
|
||||
|
||||
const filter = (search: string) => {
|
||||
setFilteredData(
|
||||
convertToColumns(unPaginatedData!).filter((row) =>
|
||||
row.cells.some(
|
||||
(cell) =>
|
||||
cell &&
|
||||
getNodeText(cell).toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
)
|
||||
);
|
||||
const getNodeText = (node: Cell<T>): string => {
|
||||
if (["string", "number"].includes(typeof node)) {
|
||||
return node!.toString();
|
||||
}
|
||||
if (node instanceof Array) {
|
||||
return node.map(getNodeText).join("");
|
||||
}
|
||||
if (typeof node === "object" && node) {
|
||||
return getNodeText(
|
||||
isValidElement((node as TitleCell).title)
|
||||
? (node as TitleCell).title.props.children
|
||||
: (node as TitleCell).title
|
||||
? (node as TitleCell).title
|
||||
: (node as JSX.Element).props.children
|
||||
);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const filteredData = useMemo<(Row<T> | SubRow<T>)[] | undefined>(
|
||||
() =>
|
||||
search === "" || isPaginated
|
||||
? undefined
|
||||
: convertToColumns(unPaginatedData || []).filter((row) =>
|
||||
row.cells.some(
|
||||
(cell) =>
|
||||
cell &&
|
||||
getNodeText(cell).toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
),
|
||||
[search]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (canSelectAll) {
|
||||
const checkboxes = document
|
||||
.getElementsByClassName("pf-c-table__check")
|
||||
.item(0);
|
||||
if (checkboxes) {
|
||||
const checkAllCheckbox = checkboxes.children!.item(
|
||||
0
|
||||
)! as HTMLInputElement;
|
||||
checkAllCheckbox.indeterminate =
|
||||
selected.length > 0 &&
|
||||
selected.length < (filteredData || rows)!.length;
|
||||
}
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
setLoading(true);
|
||||
return unPaginatedData || (await loader(first, max, search));
|
||||
},
|
||||
(data) => {
|
||||
if (!isPaginated) {
|
||||
setUnPaginatedData(data);
|
||||
data = data.slice(first, first + max);
|
||||
}
|
||||
|
||||
const result = convertToColumns(data);
|
||||
setRows(result);
|
||||
setLoading(false);
|
||||
},
|
||||
[key, first, max, search]
|
||||
);
|
||||
|
||||
const convertAction = () =>
|
||||
actions &&
|
||||
_.cloneDeep(actions).map((action: Action<T>, index: number) => {
|
||||
|
@ -301,7 +309,7 @@ export function KeycloakDataTable<T>({
|
|||
);
|
||||
if (result) {
|
||||
if (!isPaginated) {
|
||||
setFilteredData(undefined);
|
||||
setSearch("");
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
@ -370,9 +378,7 @@ export function KeycloakDataTable<T>({
|
|||
inputGroupName={
|
||||
searchPlaceholderKey ? `${ariaLabelKey}input` : undefined
|
||||
}
|
||||
inputGroupOnEnter={
|
||||
isPaginated ? setSearch : (search) => filter(search)
|
||||
}
|
||||
inputGroupOnEnter={setSearch}
|
||||
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
||||
searchTypeComponent={searchTypeComponent}
|
||||
toolbarItem={toolbarItem}
|
||||
|
@ -392,7 +398,7 @@ export function KeycloakDataTable<T>({
|
|||
/>
|
||||
)}
|
||||
{!loading &&
|
||||
rows.length === 0 &&
|
||||
(filteredData || rows).length === 0 &&
|
||||
search !== "" &&
|
||||
searchPlaceholderKey && (
|
||||
<ListEmptyState
|
||||
|
|
|
@ -64,7 +64,7 @@ export const WhoAmIContextProvider = ({ children }: WhoAmIProviderProps) => {
|
|||
const [key, setKey] = useState(0);
|
||||
|
||||
useFetch(
|
||||
() => adminClient.whoAmI.find({ realm: "master" }),
|
||||
() => adminClient.whoAmI.find(),
|
||||
(me) => {
|
||||
const whoAmI = new WhoAmI(adminClient.keycloak?.realm, me);
|
||||
setWhoAmI(whoAmI);
|
||||
|
|
|
@ -110,7 +110,9 @@ export const EventsSection = () => {
|
|||
<Trans i18nKey="events:eventExplain">
|
||||
If you want to configure user events, Admin events or Event
|
||||
listeners, please enter
|
||||
<Link to={`/${realm}/`}>{t("eventConfig")}</Link>
|
||||
<Link to={`/${realm}/realm-settings/events`}>
|
||||
{t("eventConfig")}
|
||||
</Link>
|
||||
page realm settings to configure.
|
||||
</Trans>
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import { PartialImportDialog } from "./PartialImport";
|
|||
import { RealmSettingsThemesTab } from "./ThemesTab";
|
||||
import { RealmSettingsEmailTab } from "./EmailTab";
|
||||
import { KeysListTab } from "./KeysListTab";
|
||||
import { EventsTab } from "./event-config/EventsTab";
|
||||
import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation";
|
||||
import { KeysProviderTab } from "./KeysProvidersTab";
|
||||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||
|
@ -233,7 +234,7 @@ export const RealmSettingsSection = () => {
|
|||
<KeycloakTabs isBox>
|
||||
<Tab
|
||||
eventKey="general"
|
||||
title={<TabTitleText>{t("realm-settings:general")}</TabTitleText>}
|
||||
title={<TabTitleText>{t("general")}</TabTitleText>}
|
||||
data-testid="rs-general-tab"
|
||||
>
|
||||
<RealmSettingsGeneralTab
|
||||
|
@ -243,21 +244,21 @@ export const RealmSettingsSection = () => {
|
|||
</Tab>
|
||||
<Tab
|
||||
eventKey="login"
|
||||
title={<TabTitleText>{t("realm-settings:login")}</TabTitleText>}
|
||||
title={<TabTitleText>{t("login")}</TabTitleText>}
|
||||
data-testid="rs-login-tab"
|
||||
>
|
||||
<RealmSettingsLoginTab save={save} realm={realm!} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="email"
|
||||
title={<TabTitleText>{t("realm-settings:email")}</TabTitleText>}
|
||||
title={<TabTitleText>{t("email")}</TabTitleText>}
|
||||
data-testid="rs-email-tab"
|
||||
>
|
||||
{realm && <RealmSettingsEmailTab realm={realm} />}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="themes"
|
||||
title={<TabTitleText>{t("realm-settings:themes")}</TabTitleText>}
|
||||
title={<TabTitleText>{t("themes")}</TabTitleText>}
|
||||
data-testid="rs-themes-tab"
|
||||
>
|
||||
<RealmSettingsThemesTab
|
||||
|
@ -297,6 +298,13 @@ export const RealmSettingsSection = () => {
|
|||
</Tabs>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="events"
|
||||
title={<TabTitleText>{t("events")}</TabTitleText>}
|
||||
data-testid="rs-realm-events-tab"
|
||||
>
|
||||
<EventsTab />
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
|
|
62
src/realm-settings/event-config/AddEventTypesDialog.tsx
Normal file
62
src/realm-settings/event-config/AddEventTypesDialog.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal, ModalVariant } from "@patternfly/react-core";
|
||||
|
||||
import { EventsTypeTable, EventType } from "./EventsTypeTable";
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
|
||||
type AddEventTypesDialogProps = {
|
||||
onConfirm: (selected: EventType[]) => void;
|
||||
onClose: () => void;
|
||||
configured: string[];
|
||||
};
|
||||
|
||||
export const AddEventTypesDialog = ({
|
||||
onConfirm,
|
||||
onClose,
|
||||
configured,
|
||||
}: AddEventTypesDialogProps) => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const { enums } = useServerInfo();
|
||||
|
||||
const [selectedTypes, setSelectedTypes] = useState<EventType[]>([]);
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.medium}
|
||||
title={t("addTypes")}
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="addEventTypeConfirm"
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
onClick={() => onConfirm(selectedTypes)}
|
||||
>
|
||||
{t("common:add")}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="moveCancel"
|
||||
key="cancel"
|
||||
variant="link"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<EventsTypeTable
|
||||
onSelect={(selected) => setSelectedTypes(selected)}
|
||||
loader={() =>
|
||||
Promise.resolve(
|
||||
enums!["eventType"]
|
||||
.filter((type) => !configured.includes(type))
|
||||
.map((id) => {
|
||||
return { id };
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
183
src/realm-settings/event-config/EventConfigForm.tsx
Normal file
183
src/realm-settings/event-config/EventConfigForm.tsx
Normal file
|
@ -0,0 +1,183 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, UseFormMethods } from "react-hook-form";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
Divider,
|
||||
FormGroup,
|
||||
Switch,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { TimeSelector } from "../../components/time-selector/TimeSelector";
|
||||
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||
|
||||
export type EventsType = "admin" | "user";
|
||||
|
||||
type EventConfigFormProps = {
|
||||
type: EventsType;
|
||||
form: UseFormMethods;
|
||||
reset: () => void;
|
||||
clear: () => void;
|
||||
};
|
||||
|
||||
export const EventConfigForm = ({
|
||||
type,
|
||||
form,
|
||||
reset,
|
||||
clear,
|
||||
}: EventConfigFormProps) => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isDirty },
|
||||
} = form;
|
||||
|
||||
const eventKey = type === "admin" ? "adminEventsEnabled" : "eventsEnabled";
|
||||
const eventsEnabled: boolean = watch(eventKey);
|
||||
|
||||
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
||||
titleKey: "realm-settings:events-disable-title",
|
||||
messageKey: "realm-settings:events-disable-confirm",
|
||||
continueButtonLabel: "realm-settings:confirm",
|
||||
onConfirm: () => setValue(eventKey, false),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DisableConfirm />
|
||||
<FormGroup
|
||||
hasNoPaddingTop
|
||||
label={t("saveEvents")}
|
||||
fieldId={eventKey}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={`realm-settings-help:save-${type}-events`}
|
||||
forLabel={t("saveEvents")}
|
||||
forID={eventKey}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name={eventKey}
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
data-testid={eventKey}
|
||||
id={eventKey}
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value}
|
||||
onChange={(value) => {
|
||||
if (!value) {
|
||||
toggleDisableDialog();
|
||||
} else {
|
||||
onChange(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{eventsEnabled && (
|
||||
<>
|
||||
{type === "admin" && (
|
||||
<FormGroup
|
||||
hasNoPaddingTop
|
||||
label={t("includeRepresentation")}
|
||||
fieldId="includeRepresentation"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="realm-settings-help:includeRepresentation"
|
||||
forLabel={t("includeRepresentation")}
|
||||
forID="includeRepresentation"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="adminEventsDetailsEnabled"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
data-testid="includeRepresentation"
|
||||
id="includeRepresentation"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{type === "user" && (
|
||||
<FormGroup
|
||||
label={t("expiration")}
|
||||
fieldId="expiration"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="realm-settings-help:expiration"
|
||||
forLabel={t("expiration")}
|
||||
forID="expiration"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="eventsExpiration"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
id={`save-${type}`}
|
||||
data-testid={`save-${type}`}
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={reset}>
|
||||
{t("common:revert")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
<Divider />
|
||||
<FormGroup
|
||||
label={t("clearEvents")}
|
||||
fieldId={`clear-${type}-events`}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={`realm-settings-help:${type}-clearEvents`}
|
||||
forLabel={t("clearEvents")}
|
||||
forID={`clear-${type}-events`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="danger"
|
||||
id={`clear-${type}-events`}
|
||||
data-testid={`clear-${type}-events`}
|
||||
onClick={() => clear()}
|
||||
>
|
||||
{t("clearEvents")}
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</>
|
||||
);
|
||||
};
|
210
src/realm-settings/event-config/EventsTab.tsx
Normal file
210
src/realm-settings/event-config/EventsTab.tsx
Normal file
|
@ -0,0 +1,210 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
AlertVariant,
|
||||
ButtonVariant,
|
||||
PageSection,
|
||||
Tab,
|
||||
Tabs,
|
||||
TabTitleText,
|
||||
Title,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type { RealmEventsConfigRepresentation } from "keycloak-admin/lib/defs/realmEventsConfigRepresentation";
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { useFetch, useAdminClient } from "../../context/auth/AdminClient";
|
||||
import { EventConfigForm, EventsType } from "./EventConfigForm";
|
||||
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||
import { EventsTypeTable, EventType } from "./EventsTypeTable";
|
||||
import { AddEventTypesDialog } from "./AddEventTypesDialog";
|
||||
|
||||
export const EventsTab = () => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const form = useForm<RealmEventsConfigRepresentation>();
|
||||
const { setValue, handleSubmit, watch, reset } = form;
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
const [tableKey, setTableKey] = useState(0);
|
||||
const reload = () => setTableKey(new Date().getTime());
|
||||
|
||||
const [activeTab, setActiveTab] = useState("user");
|
||||
const [events, setEvents] = useState<RealmEventsConfigRepresentation>();
|
||||
const [type, setType] = useState<EventsType>();
|
||||
const [addEventType, setAddEventType] = useState(false);
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const setupForm = (eventConfig?: RealmEventsConfigRepresentation) => {
|
||||
reset(eventConfig);
|
||||
setEvents(eventConfig);
|
||||
Object.entries(eventConfig || {}).forEach((entry) =>
|
||||
setValue(entry[0], entry[1])
|
||||
);
|
||||
};
|
||||
|
||||
const clear = async (type: EventsType) => {
|
||||
setType(type);
|
||||
toggleDeleteDialog();
|
||||
};
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "realm-settings:deleteEvents",
|
||||
messageKey: "realm-settings:deleteEventsConfirm",
|
||||
continueButtonLabel: "common:clear",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
switch (type) {
|
||||
case "admin":
|
||||
await adminClient.realms.clearAdminEvents({ realm });
|
||||
break;
|
||||
case "user":
|
||||
await adminClient.realms.clearEvents({ realm });
|
||||
break;
|
||||
}
|
||||
addAlert(t(`${type}-events-cleared`), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t(`${type}-events-cleared-error`, {
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useFetch(
|
||||
() => adminClient.realms.getConfigEvents({ realm }),
|
||||
(eventConfig) => {
|
||||
setupForm(eventConfig);
|
||||
reload();
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
const save = async (eventConfig: RealmEventsConfigRepresentation) => {
|
||||
try {
|
||||
await adminClient.realms.updateConfigEvents({ realm }, eventConfig);
|
||||
setupForm({ ...events, ...eventConfig });
|
||||
addAlert(t("eventConfigSuccessfully"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("eventConfigError", {
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const addEventTypes = async (eventTypes: EventType[]) => {
|
||||
const eventsTypes = eventTypes.map((type) => type.id);
|
||||
const enabledEvents = events!.enabledEventTypes?.concat(eventsTypes);
|
||||
await addEvents(enabledEvents);
|
||||
};
|
||||
|
||||
const addEvents = async (events: string[] = []) => {
|
||||
const eventConfig = { ...form.getValues(), enabledEventTypes: events };
|
||||
await save(eventConfig);
|
||||
setAddEventType(false);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const eventsEnabled: boolean = watch("eventsEnabled") || false;
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
{addEventType && (
|
||||
<AddEventTypesDialog
|
||||
onConfirm={(eventTypes) => addEventTypes(eventTypes)}
|
||||
configured={events?.enabledEventTypes || []}
|
||||
onClose={() => setAddEventType(false)}
|
||||
/>
|
||||
)}
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onSelect={(_, key) => setActiveTab(key as string)}
|
||||
>
|
||||
<Tab
|
||||
eventKey="user"
|
||||
title={<TabTitleText>{t("userEventsSettings")}</TabTitleText>}
|
||||
data-testid="rs-events-tab"
|
||||
>
|
||||
<PageSection>
|
||||
<Title headingLevel="h4" size="xl">
|
||||
{t("userEventsConfig")}
|
||||
</Title>
|
||||
</PageSection>
|
||||
<PageSection>
|
||||
<FormAccess
|
||||
role="manage-events"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<EventConfigForm
|
||||
type="user"
|
||||
form={form}
|
||||
reset={() => setupForm(events)}
|
||||
clear={() => clear("user")}
|
||||
/>
|
||||
</FormAccess>
|
||||
</PageSection>
|
||||
{eventsEnabled && (
|
||||
<PageSection>
|
||||
<EventsTypeTable
|
||||
key={tableKey}
|
||||
addTypes={() => setAddEventType(true)}
|
||||
loader={() =>
|
||||
Promise.resolve(
|
||||
events?.enabledEventTypes?.map((id) => {
|
||||
return { id };
|
||||
}) || []
|
||||
)
|
||||
}
|
||||
onDelete={(value) => {
|
||||
const enabledEventTypes = events?.enabledEventTypes?.filter(
|
||||
(e) => e !== value.id
|
||||
);
|
||||
addEvents(enabledEventTypes);
|
||||
setEvents({ ...events, enabledEventTypes });
|
||||
}}
|
||||
/>
|
||||
</PageSection>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="admin"
|
||||
title={<TabTitleText>{t("adminEventsSettings")}</TabTitleText>}
|
||||
data-testid="rs-events-tab"
|
||||
>
|
||||
<PageSection>
|
||||
<Title headingLevel="h4" size="xl">
|
||||
{t("adminEventsConfig")}
|
||||
</Title>
|
||||
</PageSection>
|
||||
<PageSection>
|
||||
<FormAccess
|
||||
role="manage-events"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<EventConfigForm
|
||||
type="admin"
|
||||
form={form}
|
||||
reset={() => setupForm(events)}
|
||||
clear={() => clear("admin")}
|
||||
/>
|
||||
</FormAccess>
|
||||
</PageSection>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
};
|
84
src/realm-settings/event-config/EventsTypeTable.tsx
Normal file
84
src/realm-settings/event-config/EventsTypeTable.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import React, { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, ToolbarItem } from "@patternfly/react-core";
|
||||
import type { IFormatterValueType } from "@patternfly/react-table";
|
||||
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||
|
||||
export type EventType = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type EventsTypeTableProps = {
|
||||
loader: () => Promise<EventType[]>;
|
||||
addTypes?: () => void;
|
||||
onSelect?: (value: EventType[]) => void;
|
||||
onDelete?: (value: EventType) => void;
|
||||
};
|
||||
|
||||
export function EventsTypeTable({
|
||||
loader,
|
||||
addTypes,
|
||||
onSelect,
|
||||
onDelete,
|
||||
}: EventsTypeTableProps) {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
|
||||
const DescriptionCell = (event: EventType) => (
|
||||
<Fragment key={event.id}>
|
||||
{t(`eventTypes.${event.id}.description`)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<KeycloakDataTable
|
||||
ariaLabelKey="userEventsRegistered"
|
||||
searchPlaceholderKey="realm-settings:searchEventType"
|
||||
loader={loader}
|
||||
onSelect={onSelect ? onSelect : undefined}
|
||||
canSelectAll={!!onSelect}
|
||||
toolbarItem={
|
||||
<>
|
||||
{addTypes && (
|
||||
<ToolbarItem>
|
||||
<Button id="addTypes" onClick={addTypes} data-testid="addTypes">
|
||||
{t("addSavedTypes")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
!onDelete
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onRowClick: onDelete,
|
||||
},
|
||||
]
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "id",
|
||||
displayKey: "realm-settings:eventType",
|
||||
cellFormatters: [
|
||||
(data?: IFormatterValueType) => t(`eventTypes.${data}.name`),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
displayKey: "description",
|
||||
cellRenderer: DescriptionCell,
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("emptyEvents")}
|
||||
instructions={t("emptyEventsInstructions")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -17,6 +17,12 @@
|
|||
"enabled": "Set if the keys are enabled",
|
||||
"active": "Set if the keys can be used for signing",
|
||||
"AESKeySize": "Size in bytes for the generated AES key. Size 16 is for AES-128, Size 24 for AES-192, and Size 32 for AES-256. WARN: Bigger keys than 128 are not allowed on some JDK implementations.",
|
||||
"save-user-events": "If enabled, login events are saved to the database, which makes events available to the admin and account management consoles.",
|
||||
"save-admin-events": "If enabled, admin events are saved to the database, which makes events available to the admin console.",
|
||||
"expiration": "Sets the expiration for events. Expired events are periodically deleted from the database.",
|
||||
"admin-clearEvents": "Deletes all admin events in the database.",
|
||||
"includeRepresentation": "Include JSON representation for create and update requests.",
|
||||
"user-clearEvents": "Deletes all user events in the database.",
|
||||
"ellipticCurve": "Elliptic curve used in ECDSA",
|
||||
"secretSize": "Size in bytes for the generated secret",
|
||||
"algorithm": "Intended algorithm for the key",
|
||||
|
|
|
@ -21,6 +21,15 @@
|
|||
"general": "General",
|
||||
"login": "Login",
|
||||
"themes": "Themes",
|
||||
"events": "Events",
|
||||
"userEventsConfig": "User events configuration",
|
||||
"userEventsSettings": "User events settings",
|
||||
"adminEventsConfig": "Admin events config",
|
||||
"adminEventsSettings": "Admin events settings",
|
||||
"saveEvents": "Save events",
|
||||
"expiration": "Expiration",
|
||||
"clearEvents": "Clear user events",
|
||||
"includeRepresentation": "Include representation",
|
||||
"email": "Email",
|
||||
"template": "Template",
|
||||
"connectionAndAuthentication": "Connection & Authentication",
|
||||
|
@ -39,7 +48,7 @@
|
|||
"password": "Password",
|
||||
"keys": "Keys",
|
||||
"keysList": "Keys list",
|
||||
"searchKey":"Search key",
|
||||
"searchKey": "Search key",
|
||||
"keystore": "Keystore",
|
||||
"keystorePassword": "Keystore password",
|
||||
"keyAlias": "Key alias",
|
||||
|
@ -124,7 +133,360 @@
|
|||
"emailTheme": "Email theme",
|
||||
"internationalization": "Internationalization",
|
||||
"supportedLocales": "Supported locales",
|
||||
"defaultLocale": "Default locale"
|
||||
"defaultLocale": "Default locale",
|
||||
|
||||
"eventType": "Event saved type",
|
||||
"searchEventType": "Search saved event type",
|
||||
"addSavedTypes": "Add saved types",
|
||||
"addTypes": "Add types",
|
||||
"eventTypes": {
|
||||
"SEND_RESET_PASSWORD": {
|
||||
"name": "Send reset password",
|
||||
"description": "Send reset password"
|
||||
},
|
||||
"UPDATE_CONSENT_ERROR": {
|
||||
"name": "Update consent error",
|
||||
"description": "Update consent error"
|
||||
},
|
||||
"GRANT_CONSENT": {
|
||||
"name": "Grant consent",
|
||||
"description": "Grant consent"
|
||||
},
|
||||
"REMOVE_TOTP": { "name": "Remove totp", "description": "Remove totp" },
|
||||
"REVOKE_GRANT": { "name": "Revoke grant", "description": "Revoke grant" },
|
||||
"UPDATE_TOTP": { "name": "Update totp", "description": "Update totp" },
|
||||
"LOGIN_ERROR": { "name": "Login error", "description": "Login error" },
|
||||
"CLIENT_LOGIN": { "name": "Client login", "description": "Client login" },
|
||||
"RESET_PASSWORD_ERROR": {
|
||||
"name": "Reset password error",
|
||||
"description": "Reset password error"
|
||||
},
|
||||
"IMPERSONATE_ERROR": {
|
||||
"name": "Impersonate error",
|
||||
"description": "Impersonate error"
|
||||
},
|
||||
"CODE_TO_TOKEN_ERROR": {
|
||||
"name": "Code to token error",
|
||||
"description": "Code to token error"
|
||||
},
|
||||
"CUSTOM_REQUIRED_ACTION": {
|
||||
"name": "Custom required action",
|
||||
"description": "Custom required action"
|
||||
},
|
||||
"RESTART_AUTHENTICATION": {
|
||||
"name": "Restart authentication",
|
||||
"description": "Restart authentication"
|
||||
},
|
||||
"IMPERSONATE": { "name": "Impersonate", "description": "Impersonate" },
|
||||
"UPDATE_PROFILE_ERROR": {
|
||||
"name": "Update profile error",
|
||||
"description": "Update profile error"
|
||||
},
|
||||
"LOGIN": { "name": "Login", "description": "Login" },
|
||||
"UPDATE_PASSWORD_ERROR": {
|
||||
"name": "Update password error",
|
||||
"description": "Update password error"
|
||||
},
|
||||
"CLIENT_INITIATED_ACCOUNT_LINKING": {
|
||||
"name": "Client initiated account linking",
|
||||
"description": "Client initiated account linking"
|
||||
},
|
||||
"TOKEN_EXCHANGE": {
|
||||
"name": "Token exchange",
|
||||
"description": "Token exchange"
|
||||
},
|
||||
"LOGOUT": { "name": "Logout", "description": "Logout" },
|
||||
"REGISTER": { "name": "Register", "description": "Register" },
|
||||
"DELETE_ACCOUNT_ERROR": {
|
||||
"name": "Delete account error",
|
||||
"description": "Delete account error"
|
||||
},
|
||||
"CLIENT_REGISTER": {
|
||||
"name": "Client register",
|
||||
"description": "Client register"
|
||||
},
|
||||
"IDENTITY_PROVIDER_LINK_ACCOUNT": {
|
||||
"name": "Identity provider link account",
|
||||
"description": "Identity provider link account"
|
||||
},
|
||||
"DELETE_ACCOUNT": {
|
||||
"name": "Delete account",
|
||||
"description": "Delete account"
|
||||
},
|
||||
"UPDATE_PASSWORD": {
|
||||
"name": "Update password",
|
||||
"description": "Update password"
|
||||
},
|
||||
"CLIENT_DELETE": {
|
||||
"name": "Client delete",
|
||||
"description": "Client delete"
|
||||
},
|
||||
"FEDERATED_IDENTITY_LINK_ERROR": {
|
||||
"name": "Federated identity link error",
|
||||
"description": "Federated identity link error"
|
||||
},
|
||||
"IDENTITY_PROVIDER_FIRST_LOGIN": {
|
||||
"name": "Identity provider first login",
|
||||
"description": "Identity provider first login"
|
||||
},
|
||||
"CLIENT_DELETE_ERROR": {
|
||||
"name": "Client delete error",
|
||||
"description": "Client delete error"
|
||||
},
|
||||
"VERIFY_EMAIL": { "name": "Verify email", "description": "Verify email" },
|
||||
"CLIENT_LOGIN_ERROR": {
|
||||
"name": "Client login error",
|
||||
"description": "Client login error"
|
||||
},
|
||||
"RESTART_AUTHENTICATION_ERROR": {
|
||||
"name": "Restart authentication error",
|
||||
"description": "Restart authentication error"
|
||||
},
|
||||
"EXECUTE_ACTIONS": {
|
||||
"name": "Execute actions",
|
||||
"description": "Execute actions"
|
||||
},
|
||||
"REMOVE_FEDERATED_IDENTITY_ERROR": {
|
||||
"name": "Remove federated identity error",
|
||||
"description": "Remove federated identity error"
|
||||
},
|
||||
"TOKEN_EXCHANGE_ERROR": {
|
||||
"name": "Token exchange error",
|
||||
"description": "Token exchange error"
|
||||
},
|
||||
"PERMISSION_TOKEN": {
|
||||
"name": "Permission token",
|
||||
"description": "Permission token"
|
||||
},
|
||||
"SEND_IDENTITY_PROVIDER_LINK_ERROR": {
|
||||
"name": "Send identity provider link error",
|
||||
"description": "Send identity provider link error"
|
||||
},
|
||||
"EXECUTE_ACTION_TOKEN_ERROR": {
|
||||
"name": "Execute action token error",
|
||||
"description": "Execute action token error"
|
||||
},
|
||||
"SEND_VERIFY_EMAIL": {
|
||||
"name": "Send verify email",
|
||||
"description": "Send verify email"
|
||||
},
|
||||
"EXECUTE_ACTIONS_ERROR": {
|
||||
"name": "Execute actions error",
|
||||
"description": "Execute actions error"
|
||||
},
|
||||
"REMOVE_FEDERATED_IDENTITY": {
|
||||
"name": "Remove federated identity",
|
||||
"description": "Remove federated identity"
|
||||
},
|
||||
"IDENTITY_PROVIDER_POST_LOGIN": {
|
||||
"name": "Identity provider post login",
|
||||
"description": "Identity provider post login"
|
||||
},
|
||||
"IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR": {
|
||||
"name": "Identity provider link account error",
|
||||
"description": "Identity provider link account error"
|
||||
},
|
||||
"UPDATE_EMAIL": { "name": "Update email", "description": "Update email" },
|
||||
"REGISTER_ERROR": {
|
||||
"name": "Register error",
|
||||
"description": "Register error"
|
||||
},
|
||||
"REVOKE_GRANT_ERROR": {
|
||||
"name": "Revoke grant error",
|
||||
"description": "Revoke grant error"
|
||||
},
|
||||
"EXECUTE_ACTION_TOKEN": {
|
||||
"name": "Execute action token",
|
||||
"description": "Execute action token"
|
||||
},
|
||||
"LOGOUT_ERROR": { "name": "Logout error", "description": "Logout error" },
|
||||
"UPDATE_EMAIL_ERROR": {
|
||||
"name": "Update email error",
|
||||
"description": "Update email error"
|
||||
},
|
||||
"CLIENT_UPDATE_ERROR": {
|
||||
"name": "Client update error",
|
||||
"description": "Client update error"
|
||||
},
|
||||
"UPDATE_PROFILE": {
|
||||
"name": "Update profile",
|
||||
"description": "Update profile"
|
||||
},
|
||||
"CLIENT_REGISTER_ERROR": {
|
||||
"name": "Client register error",
|
||||
"description": "Client register error"
|
||||
},
|
||||
"FEDERATED_IDENTITY_LINK": {
|
||||
"name": "Federated identity link",
|
||||
"description": "Federated identity link"
|
||||
},
|
||||
"SEND_IDENTITY_PROVIDER_LINK": {
|
||||
"name": "Send identity provider link",
|
||||
"description": "Send identity provider link"
|
||||
},
|
||||
"SEND_VERIFY_EMAIL_ERROR": {
|
||||
"name": "Send verify email error",
|
||||
"description": "Send verify email error"
|
||||
},
|
||||
"RESET_PASSWORD": {
|
||||
"name": "Reset password",
|
||||
"description": "Reset password"
|
||||
},
|
||||
"CLIENT_INITIATED_ACCOUNT_LINKING_ERROR": {
|
||||
"name": "Client initiated account linking error",
|
||||
"description": "Client initiated account linking error"
|
||||
},
|
||||
"UPDATE_CONSENT": {
|
||||
"name": "Update consent",
|
||||
"description": "Update consent"
|
||||
},
|
||||
"REMOVE_TOTP_ERROR": {
|
||||
"name": "Remove totp error",
|
||||
"description": "Remove totp error"
|
||||
},
|
||||
"VERIFY_EMAIL_ERROR": {
|
||||
"name": "Verify email error",
|
||||
"description": "Verify email error"
|
||||
},
|
||||
"SEND_RESET_PASSWORD_ERROR": {
|
||||
"name": "Send reset password error",
|
||||
"description": "Send reset password error"
|
||||
},
|
||||
"CLIENT_UPDATE": {
|
||||
"name": "Client update",
|
||||
"description": "Client update"
|
||||
},
|
||||
"CUSTOM_REQUIRED_ACTION_ERROR": {
|
||||
"name": "Custom required action error",
|
||||
"description": "Custom required action error"
|
||||
},
|
||||
"IDENTITY_PROVIDER_POST_LOGIN_ERROR": {
|
||||
"name": "Identity provider post login error",
|
||||
"description": "Identity provider post login error"
|
||||
},
|
||||
"UPDATE_TOTP_ERROR": {
|
||||
"name": "Update totp error",
|
||||
"description": "Update totp error"
|
||||
},
|
||||
"CODE_TO_TOKEN": {
|
||||
"name": "Code to token",
|
||||
"description": "Code to token"
|
||||
},
|
||||
"GRANT_CONSENT_ERROR": {
|
||||
"name": "Grant consent error",
|
||||
"description": "Grant consent error"
|
||||
},
|
||||
"IDENTITY_PROVIDER_FIRST_LOGIN_ERROR": {
|
||||
"name": "Identity provider first login error",
|
||||
"description": "Identity provider first login error"
|
||||
},
|
||||
"REGISTER_NODE_ERROR": {
|
||||
"name": "Register node error",
|
||||
"description": "Register node error"
|
||||
},
|
||||
"PERMISSION_TOKEN_ERROR": {
|
||||
"name": "Permission token error",
|
||||
"description": "Permission token error"
|
||||
},
|
||||
"IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR": {
|
||||
"name": "Identity provider retrieve token error",
|
||||
"description": "Identity provider retrieve token error"
|
||||
},
|
||||
"CLIENT_INFO": {
|
||||
"name": "Client info",
|
||||
"description": "Client info"
|
||||
},
|
||||
"VALIDATE_ACCESS_TOKEN": {
|
||||
"name": "Validate access token",
|
||||
"description": "Validate access token"
|
||||
},
|
||||
"IDENTITY_PROVIDER_LOGIN": {
|
||||
"name": "Identity provider login",
|
||||
"description": "Identity provider login"
|
||||
},
|
||||
"CLIENT_INFO_ERROR": {
|
||||
"name": "Client info error",
|
||||
"description": "Client info error"
|
||||
},
|
||||
"INTROSPECT_TOKEN_ERROR": {
|
||||
"name": "Introspect token error",
|
||||
"description": "Introspect token error"
|
||||
},
|
||||
"INTROSPECT_TOKEN": {
|
||||
"name": "Introspect token",
|
||||
"description": "Introspect token"
|
||||
},
|
||||
"UNREGISTER_NODE": {
|
||||
"name": "Unregister node",
|
||||
"description": "Unregister node"
|
||||
},
|
||||
"REGISTER_NODE": {
|
||||
"name": "Register node",
|
||||
"description": "Register node"
|
||||
},
|
||||
"INVALID_SIGNATURE": {
|
||||
"name": "Invalid signature",
|
||||
"description": "Invalid signature"
|
||||
},
|
||||
"USER_INFO_REQUEST_ERROR": {
|
||||
"name": "User info request error",
|
||||
"description": "User info request error"
|
||||
},
|
||||
"REFRESH_TOKEN": {
|
||||
"name": "Refresh token",
|
||||
"description": "Refresh token"
|
||||
},
|
||||
"IDENTITY_PROVIDER_RESPONSE": {
|
||||
"name": "Identity provider response",
|
||||
"description": "Identity provider response"
|
||||
},
|
||||
"IDENTITY_PROVIDER_RETRIEVE_TOKEN": {
|
||||
"name": "Identity provider retrieve token",
|
||||
"description": "Identity provider retrieve token"
|
||||
},
|
||||
"UNREGISTER_NODE_ERROR": {
|
||||
"name": "Unregister node error",
|
||||
"description": "Unregister node error"
|
||||
},
|
||||
"VALIDATE_ACCESS_TOKEN_ERROR": {
|
||||
"name": "Validate access token error",
|
||||
"description": "Validate access token error"
|
||||
},
|
||||
"INVALID_SIGNATURE_ERROR": {
|
||||
"name": "Invalid signature error",
|
||||
"description": "Invalid signature error"
|
||||
},
|
||||
"USER_INFO_REQUEST": {
|
||||
"name": "User info request",
|
||||
"description": "User info request"
|
||||
},
|
||||
"IDENTITY_PROVIDER_RESPONSE_ERROR": {
|
||||
"name": "Identity provider response error",
|
||||
"description": "Identity provider response error"
|
||||
},
|
||||
"IDENTITY_PROVIDER_LOGIN_ERROR": {
|
||||
"name": "Identity provider login error",
|
||||
"description": "Identity provider login error"
|
||||
},
|
||||
"REFRESH_TOKEN_ERROR": {
|
||||
"name": "Refresh token error",
|
||||
"description": "Refresh token error"
|
||||
}
|
||||
|
||||
},
|
||||
"emptyEvents": "Nothing to add",
|
||||
"emptyEventsInstructions": "There are no more events types left to add",
|
||||
"eventConfigSuccessfully": "Successfully saved configuration",
|
||||
"eventConfigError": "Could not save event configuration {{error}}",
|
||||
"deleteEvents": "Clear events",
|
||||
"deleteEventsConfirm": "If you clear all events of this realm, all records will be permanently cleared in the database",
|
||||
"admin-events-cleared": "The admin events have been cleared",
|
||||
"admin-events-cleared-error": "Could not clear the admin events {{error}}",
|
||||
"user-events-cleared": "The user events have been cleared",
|
||||
"user-events-cleared-error": "Could not clear the user events {{error}}",
|
||||
"events-disable-title": "Unsave events?",
|
||||
"events-disable-confirm": "If \"Save events\" is disabled, subsequent events will not be displayed in the \"Events\" menu",
|
||||
"confirm": "Confirm"
|
||||
},
|
||||
"partial-import": {
|
||||
"partialImportHeaderText": "Partial import allows you to import users, clients, and resources from a previously exported json file.",
|
||||
|
|
Loading…
Reference in a new issue