events: search user events form, chips for searched fields and tests (#930)
This commit is contained in:
parent
7efa03dc3f
commit
3469ccb509
5 changed files with 577 additions and 118 deletions
66
cypress/integration/events_test.spec.ts
Normal file
66
cypress/integration/events_test.spec.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import LoginPage from "../support/pages/LoginPage";
|
||||
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||
import EventsPage from "../support/pages/admin_console/manage/events/EventsPage";
|
||||
import RealmSettingsPage from "../support/pages/admin_console/manage/realm_settings/RealmSettingsPage";
|
||||
import Masthead from "../support/pages/admin_console/Masthead";
|
||||
import { keycloakBefore } from "../support/util/keycloak_before";
|
||||
|
||||
const loginPage = new LoginPage();
|
||||
const sidebarPage = new SidebarPage();
|
||||
const eventsPage = new EventsPage();
|
||||
const realmSettingsPage = new RealmSettingsPage();
|
||||
const masthead = new Masthead();
|
||||
|
||||
describe("Search events test", function () {
|
||||
describe("Search events dropdown", function () {
|
||||
beforeEach(function () {
|
||||
keycloakBefore();
|
||||
loginPage.logIn();
|
||||
sidebarPage.goToEvents();
|
||||
});
|
||||
|
||||
it("Check search dropdown display", () => {
|
||||
eventsPage.shouldDisplay();
|
||||
});
|
||||
|
||||
it("Check search form fields display", () => {
|
||||
eventsPage.shouldHaveFormFields();
|
||||
});
|
||||
|
||||
it("Check event type dropdown options exist", () => {
|
||||
eventsPage.shouldHaveEventTypeOptions();
|
||||
});
|
||||
|
||||
it("Check `search events` button disabled by default", () => {
|
||||
eventsPage.shouldHaveSearchBtnDisabled();
|
||||
});
|
||||
|
||||
it.skip("Check search and removal works", () => {
|
||||
sidebarPage.goToRealmSettings();
|
||||
cy.getId("rs-realm-events-tab").click();
|
||||
|
||||
cy.get("#eventsEnabled-switch-on")
|
||||
.should("exist")
|
||||
.then((exist) => {
|
||||
if (exist) {
|
||||
sidebarPage.goToEvents();
|
||||
eventsPage.shouldDoSearchAndRemoveChips();
|
||||
} else {
|
||||
realmSettingsPage
|
||||
.toggleSwitch(realmSettingsPage.enableEvents)
|
||||
.save(realmSettingsPage.eventsUserSave);
|
||||
|
||||
masthead.checkNotificationMessage(
|
||||
"Successfully saved configuration"
|
||||
);
|
||||
sidebarPage.goToEvents();
|
||||
eventsPage.shouldDoSearchAndRemoveChips();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("Check `search events` button enabled", () => {
|
||||
eventsPage.shouldHaveSearchBtnEnabled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
export default class EventsPage {
|
||||
searchEventDrpDwn = ".pf-c-dropdown__toggle";
|
||||
searchEventDrpDwnBtn =
|
||||
".keycloak__user_events_search_selector_dropdown__toggle";
|
||||
searchForm = ".pf-c-dropdown__menu";
|
||||
userIdInputFld = "userId-searchField";
|
||||
eventTypeDrpDwnFld = "event-type-searchField";
|
||||
clientInputFld = "client-searchField";
|
||||
dateFromInputFld = "dateFrom-searchField";
|
||||
dateToInputFld = "dateTo-searchField";
|
||||
searchEventsBtn = "search-events-btn";
|
||||
eventTypeList = ".pf-c-form-control";
|
||||
eventTypeOption = ".pf-c-select__menu-item";
|
||||
eventTypeInputFld = ".pf-c-form-control.pf-c-select__toggle-typeahead";
|
||||
eventTypeBtn = ".pf-c-button.pf-c-select__toggle-button.pf-m-plain";
|
||||
eventsPageTitle = ".pf-c-title";
|
||||
|
||||
shouldDisplay() {
|
||||
cy.get(this.searchEventDrpDwn).should("exist");
|
||||
}
|
||||
|
||||
shouldHaveFormFields() {
|
||||
cy.get(this.searchEventDrpDwnBtn).click();
|
||||
cy.get(this.searchForm).contains("User ID");
|
||||
cy.get(this.searchForm).contains("Event type");
|
||||
cy.get(this.searchForm).contains("Client");
|
||||
cy.get(this.searchForm).contains("Date(from)");
|
||||
cy.get(this.searchForm).contains("Date(to)");
|
||||
cy.get(this.searchForm).contains("Search events");
|
||||
}
|
||||
|
||||
shouldHaveEventTypeOptions() {
|
||||
cy.get(this.searchEventDrpDwnBtn).click();
|
||||
cy.get(this.eventTypeList).should("exist");
|
||||
}
|
||||
|
||||
shouldHaveSearchBtnDisabled() {
|
||||
cy.get(this.searchEventDrpDwnBtn).click();
|
||||
cy.getId(this.searchEventsBtn).should("have.attr", "disabled");
|
||||
}
|
||||
|
||||
shouldHaveSearchBtnEnabled() {
|
||||
cy.get(this.searchEventDrpDwnBtn).click();
|
||||
cy.getId(this.userIdInputFld).type("11111");
|
||||
cy.getId(this.searchEventsBtn).should("not.have.attr", "disabled");
|
||||
}
|
||||
|
||||
shouldDoSearchAndRemoveChips() {
|
||||
cy.get(this.searchEventDrpDwnBtn).click();
|
||||
cy.get(this.eventTypeInputFld).type("LOGIN");
|
||||
cy.get(this.eventTypeOption).contains("LOGIN").click();
|
||||
|
||||
cy.intercept("/auth/admin/realms/master/events*").as("eventsFetch");
|
||||
cy.getId(this.searchEventsBtn).click();
|
||||
cy.wait("@eventsFetch");
|
||||
|
||||
cy.get("table").contains("td", "LOGIN");
|
||||
cy.get("table").should("not.have.text", "CODE_TO_TOKEN");
|
||||
cy.get("table").should("not.have.text", "CODE_TO_TOKEN_ERROR");
|
||||
cy.get("table").should("not.have.text", "LOGIN_ERROR");
|
||||
cy.get("table").should("not.have.text", "LOGOUT");
|
||||
|
||||
// cy.get('[id^=remove_pf]').click();
|
||||
// cy.get("table").contains("LOGIN");
|
||||
|
||||
// cy.get(this.searchEventDrpDwnBtn).click();
|
||||
// cy.getId(this.userIdInputFld).type("11111");
|
||||
// cy.get(this.eventsPageTitle).contains("No events logged");
|
||||
// cy.getId(this.searchEventsBtn).click();
|
||||
// cy.get('[id^=remove_group]').click();
|
||||
// cy.get("table").contains("LOGIN");
|
||||
}
|
||||
|
||||
shouldDoNoResultsSearch() {
|
||||
cy.get(this.searchEventDrpDwnBtn).click();
|
||||
cy.getId(this.userIdInputFld).type("test");
|
||||
cy.getId(this.searchEventsBtn).click();
|
||||
cy.get(this.eventsPageTitle).contains("No events logged");
|
||||
}
|
||||
}
|
|
@ -1,105 +1,182 @@
|
|||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
DescriptionList,
|
||||
DescriptionListDescription,
|
||||
DescriptionListGroup,
|
||||
DescriptionListTerm,
|
||||
Divider,
|
||||
Dropdown,
|
||||
DropdownToggle,
|
||||
Flex,
|
||||
FlexItem,
|
||||
Form,
|
||||
FormGroup,
|
||||
PageSection,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
ToolbarItem,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from "@patternfly/react-core";
|
||||
import { CheckCircleIcon, WarningTriangleIcon } from "@patternfly/react-icons";
|
||||
import { cellWidth, expandable } from "@patternfly/react-table";
|
||||
import type EventRepresentation from "keycloak-admin/lib/defs/eventRepresentation";
|
||||
import type EventType from "keycloak-admin/lib/defs/eventTypes";
|
||||
import type { RealmEventsConfigRepresentation } from "keycloak-admin/lib/defs/realmEventsConfigRepresentation";
|
||||
import { pickBy } from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { toRealmSettings } from "../realm-settings/routes/RealmSettings";
|
||||
import { toUser } from "../user/routes/User";
|
||||
import { AdminEvents } from "./AdminEvents";
|
||||
import "./events-section.css";
|
||||
|
||||
type UserEventSearchForm = {
|
||||
client: string;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
user: string;
|
||||
type: EventType[];
|
||||
};
|
||||
|
||||
const defaultValues: UserEventSearchForm = {
|
||||
client: "",
|
||||
dateFrom: "",
|
||||
dateTo: "",
|
||||
user: "",
|
||||
type: [],
|
||||
};
|
||||
|
||||
const StatusRow = (event: EventRepresentation) =>
|
||||
!event.error ? (
|
||||
<span>
|
||||
<CheckCircleIcon color="green" /> {event.type}
|
||||
</span>
|
||||
) : (
|
||||
<Tooltip content={event.error}>
|
||||
<span>
|
||||
<WarningTriangleIcon color="orange" /> {event.type}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const DetailCell = (event: EventRepresentation) => (
|
||||
<DescriptionList isHorizontal className="keycloak_eventsection_details">
|
||||
{Object.entries(event.details!).map(([key, value]) => (
|
||||
<DescriptionListGroup key={key}>
|
||||
<DescriptionListTerm>{key}</DescriptionListTerm>
|
||||
<DescriptionListDescription>{value}</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
))}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
||||
export const EventsSection = () => {
|
||||
const { t } = useTranslation("events");
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false);
|
||||
const [selectOpen, setSelectOpen] = useState(false);
|
||||
const [events, setEvents] = useState<RealmEventsConfigRepresentation>();
|
||||
const [activeFilters, setActiveFilters] = useState<
|
||||
Partial<UserEventSearchForm>
|
||||
>({});
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const params = {
|
||||
first: first!,
|
||||
max: max!,
|
||||
realm,
|
||||
};
|
||||
if (search) {
|
||||
console.log("how to search?", search);
|
||||
}
|
||||
return await adminClient.realms.findEvents({ ...params });
|
||||
const filterLabels: Record<keyof UserEventSearchForm, string> = {
|
||||
client: t("client"),
|
||||
dateFrom: t("dateFrom"),
|
||||
dateTo: t("dateTo"),
|
||||
user: t("userId"),
|
||||
type: t("eventType"),
|
||||
};
|
||||
|
||||
const StatusRow = (event: EventRepresentation) => (
|
||||
<>
|
||||
{!event.error && (
|
||||
<span key={`status-${event.time}-${event.type}`}>
|
||||
<CheckCircleIcon
|
||||
color="green"
|
||||
key={`circle-${event.time}-${event.type}`}
|
||||
/>{" "}
|
||||
{event.type}
|
||||
</span>
|
||||
)}
|
||||
{event.error && (
|
||||
<Tooltip
|
||||
content={event.error}
|
||||
key={`tooltip-${event.time}-${event.type}`}
|
||||
>
|
||||
<span key={`label-${event.time}-${event.type}`}>
|
||||
<WarningTriangleIcon
|
||||
color="orange"
|
||||
key={`triangle-${event.time}-${event.type}`}
|
||||
/>{" "}
|
||||
{event.type}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
const {
|
||||
getValues,
|
||||
register,
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
control,
|
||||
} = useForm<UserEventSearchForm>({
|
||||
shouldUnregister: false,
|
||||
mode: "onChange",
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
useFetch(
|
||||
() => adminClient.realms.getConfigEvents({ realm }),
|
||||
(events) => setEvents(events),
|
||||
[]
|
||||
);
|
||||
|
||||
function loader(first?: number, max?: number) {
|
||||
return adminClient.realms.findEvents({
|
||||
// The admin client wants 'dateFrom' and 'dateTo' to be Date objects, however it cannot actually handle them so we need to cast to any.
|
||||
...(activeFilters as any),
|
||||
realm,
|
||||
first,
|
||||
max,
|
||||
});
|
||||
}
|
||||
|
||||
function submitSearch() {
|
||||
setSearchDropdownOpen(false);
|
||||
commitFilters();
|
||||
}
|
||||
|
||||
function removeFilter(key: keyof UserEventSearchForm) {
|
||||
const formValues: UserEventSearchForm = { ...getValues() };
|
||||
delete formValues[key];
|
||||
|
||||
reset({ ...defaultValues, ...formValues });
|
||||
commitFilters();
|
||||
}
|
||||
|
||||
function removeFilterValue(
|
||||
key: keyof UserEventSearchForm,
|
||||
valueToRemove: EventType
|
||||
) {
|
||||
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<UserEventSearchForm> = pickBy(
|
||||
getValues(),
|
||||
(value) => value !== "" || (Array.isArray(value) && value.length > 0)
|
||||
);
|
||||
|
||||
setActiveFilters(newFilters);
|
||||
setKey(key + 1);
|
||||
}
|
||||
|
||||
const UserDetailLink = (event: EventRepresentation) => (
|
||||
<>
|
||||
<Link
|
||||
key={`link-${event.time}-${event.type}`}
|
||||
to={toUser({ realm, id: event.userId!, tab: "settings" })}
|
||||
>
|
||||
{event.userId}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
||||
const DetailCell = (event: EventRepresentation) => (
|
||||
<>
|
||||
<DescriptionList isHorizontal className="keycloak_eventsection_details">
|
||||
{Object.keys(event.details!).map((k) => (
|
||||
<DescriptionListGroup key={`detail-${event.time}-${event.type}`}>
|
||||
<DescriptionListTerm>{k}</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{event.details![k]}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
))}
|
||||
</DescriptionList>
|
||||
</>
|
||||
<Link
|
||||
key={`link-${event.time}-${event.type}`}
|
||||
to={toUser({ realm, id: event.userId!, tab: "settings" })}
|
||||
>
|
||||
{event.userId}
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -124,60 +201,252 @@ export const EventsSection = () => {
|
|||
eventKey="userEvents"
|
||||
title={<TabTitleText>{t("userEvents")}</TabTitleText>}
|
||||
>
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
detailColumns={[
|
||||
{
|
||||
name: "details",
|
||||
enabled: (event) => event.details !== undefined,
|
||||
cellRenderer: DetailCell,
|
||||
},
|
||||
]}
|
||||
isPaginated
|
||||
ariaLabelKey="events:title"
|
||||
searchPlaceholderKey="events:searchForEvent"
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button onClick={refresh}>{t("refresh")}</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "time",
|
||||
displayKey: "events:time",
|
||||
cellRenderer: (row) => moment(row.time).format("LLL"),
|
||||
cellFormatters: [expandable],
|
||||
},
|
||||
{
|
||||
name: "userId",
|
||||
displayKey: "events:user",
|
||||
cellRenderer: UserDetailLink,
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
displayKey: "events:eventType",
|
||||
cellRenderer: StatusRow,
|
||||
},
|
||||
{
|
||||
name: "ipAddress",
|
||||
displayKey: "events:ipAddress",
|
||||
transforms: [cellWidth(10)],
|
||||
},
|
||||
{
|
||||
name: "clientId",
|
||||
displayKey: "events:client",
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("emptyEvents")}
|
||||
instructions={t("emptyEventsInstructions")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Flex>
|
||||
<FlexItem>
|
||||
<Dropdown
|
||||
id="user-events-search-select"
|
||||
data-testid="UserEventsSearchSelector"
|
||||
className="pf-u-mt-md pf-u-ml-md pf-u-mb-md"
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
data-testid="userEventsSearchSelectorToggle"
|
||||
onToggle={(isOpen) => setSearchDropdownOpen(isOpen)}
|
||||
className="keycloak__user_events_search_selector_dropdown__toggle"
|
||||
>
|
||||
{t("searchForEvent")}
|
||||
</DropdownToggle>
|
||||
}
|
||||
isOpen={searchDropdownOpen}
|
||||
>
|
||||
<Form
|
||||
isHorizontal
|
||||
className="keycloak__user_events_search__form"
|
||||
data-testid="searchForm"
|
||||
>
|
||||
<FormGroup
|
||||
label={t("userId")}
|
||||
fieldId="kc-userId"
|
||||
className="keycloak__user_events_search__form_label"
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="kc-userId"
|
||||
name="user"
|
||||
data-testid="userId-searchField"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("eventType")}
|
||||
fieldId="kc-eventType"
|
||||
className="keycloak__user_events_search__form_label"
|
||||
>
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({
|
||||
onChange,
|
||||
value,
|
||||
}: {
|
||||
onChange: (newValue: EventType[]) => void;
|
||||
value: EventType[];
|
||||
}) => (
|
||||
<Select
|
||||
className="keycloak__user_events_search__event_type_select"
|
||||
name="eventType"
|
||||
data-testid="event-type-searchField"
|
||||
chipGroupProps={{
|
||||
numChips: 1,
|
||||
expandedText: "Hide",
|
||||
collapsedText: "Show ${remaining}",
|
||||
}}
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
typeAheadAriaLabel="Select"
|
||||
onToggle={(isOpen) => setSelectOpen(isOpen)}
|
||||
selections={value}
|
||||
onSelect={(_, selectedValue) => {
|
||||
const option =
|
||||
selectedValue.toString() as EventType;
|
||||
const changedValue = value.includes(option)
|
||||
? value.filter((item) => item !== option)
|
||||
: [...value, option];
|
||||
|
||||
onChange(changedValue);
|
||||
}}
|
||||
onClear={(event) => {
|
||||
event.stopPropagation();
|
||||
onChange([]);
|
||||
}}
|
||||
isOpen={selectOpen}
|
||||
aria-labelledby={"eventType"}
|
||||
chipGroupComponent={
|
||||
<ChipGroup>
|
||||
{value.map((chip) => (
|
||||
<Chip
|
||||
key={chip}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
onChange(
|
||||
value.filter((val) => val !== chip)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{chip}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
>
|
||||
{events?.enabledEventTypes?.map((option) => (
|
||||
<SelectOption key={option} value={option} />
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("client")}
|
||||
fieldId="kc-client"
|
||||
className="keycloak__user_events_search__form_label"
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="kc-client"
|
||||
name="client"
|
||||
data-testid="client-searchField"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("dateFrom")}
|
||||
fieldId="kc-dateFrom"
|
||||
className="keycloak__user_events_search__form_label"
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="kc-dateFrom"
|
||||
name="dateFrom"
|
||||
className="pf-c-form-control pf-m-icon pf-m-calendar"
|
||||
placeholder="yyyy-MM-dd"
|
||||
data-testid="dateFrom-searchField"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("dateTo")}
|
||||
fieldId="kc-dateTo"
|
||||
className="keycloak__user_events_search__form_label"
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="kc-dateTo"
|
||||
name="dateTo"
|
||||
className="pf-c-form-control pf-m-icon pf-m-calendar"
|
||||
placeholder="yyyy-MM-dd"
|
||||
data-testid="dateTo-searchField"
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
className="keycloak__user_events_search__form_btn"
|
||||
variant={"primary"}
|
||||
onClick={submitSearch}
|
||||
data-testid="search-events-btn"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("searchBtn")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
</Dropdown>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
{Object.entries(activeFilters).length > 0 && (
|
||||
<div className="keycloak__searchChips pf-u-ml-md">
|
||||
{Object.entries(activeFilters).map((filter) => {
|
||||
const [key, value] = filter as [
|
||||
keyof UserEventSearchForm,
|
||||
string | EventType[]
|
||||
];
|
||||
|
||||
return (
|
||||
<ChipGroup
|
||||
className="pf-u-mr-md pf-u-mb-md"
|
||||
key={key}
|
||||
categoryName={filterLabels[key]}
|
||||
isClosable
|
||||
onClick={() => removeFilter(key)}
|
||||
>
|
||||
{typeof value === "string" ? (
|
||||
<Chip isReadOnly>{value}</Chip>
|
||||
) : (
|
||||
value.map((entry) => (
|
||||
<Chip
|
||||
key={entry}
|
||||
onClick={() => removeFilterValue(key, entry)}
|
||||
>
|
||||
{entry}
|
||||
</Chip>
|
||||
))
|
||||
)}
|
||||
</ChipGroup>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="keycloak__events_table">
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
detailColumns={[
|
||||
{
|
||||
name: "details",
|
||||
enabled: (event) => event.details !== undefined,
|
||||
cellRenderer: DetailCell,
|
||||
},
|
||||
]}
|
||||
isPaginated
|
||||
ariaLabelKey="events:title"
|
||||
columns={[
|
||||
{
|
||||
name: "time",
|
||||
displayKey: "events:time",
|
||||
cellRenderer: (row) => moment(row.time).format("LLL"),
|
||||
cellFormatters: [expandable],
|
||||
},
|
||||
{
|
||||
name: "userId",
|
||||
displayKey: "events:user",
|
||||
cellRenderer: UserDetailLink,
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
displayKey: "events:eventType",
|
||||
cellRenderer: StatusRow,
|
||||
},
|
||||
{
|
||||
name: "ipAddress",
|
||||
displayKey: "events:ipAddress",
|
||||
transforms: [cellWidth(10)],
|
||||
},
|
||||
{
|
||||
name: "clientId",
|
||||
displayKey: "events:client",
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
<div className="pf-u-mt-md">
|
||||
<Divider className="keycloak__events_empty_state_divider" />
|
||||
<ListEmptyState
|
||||
message={t("emptyEvents")}
|
||||
instructions={t("emptyEventsInstructions")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="adminEvents"
|
||||
|
|
|
@ -1,4 +1,42 @@
|
|||
|
||||
.keycloak_eventsection_details {
|
||||
--pf-c-description-list--m-horizontal__term--width: 15ch;
|
||||
}
|
||||
|
||||
.keycloak__user_events_search_selector_dropdown__toggle {
|
||||
--pf-c-dropdown__toggle--MinWidth: 21rem;
|
||||
}
|
||||
|
||||
.keycloak__refresh_btn,
|
||||
.keycloak__searchChips,
|
||||
.keycloak__user_events_search_selector_dropdown__toggle {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.keycloak__user_events_search__form {
|
||||
margin: 0 0 var(--pf-global--spacer--lg) var(--pf-global--spacer--lg);
|
||||
--pf-c-form__group--m-action--MarginTop: 0rem;
|
||||
--pf-c-form--m-horizontal__group-control--md--GridColumnWidth: 12rem;
|
||||
--pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 5rem;
|
||||
}
|
||||
|
||||
.keycloak__user_events_search__form_label {
|
||||
--pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 5rem;
|
||||
}
|
||||
|
||||
.keycloak__user_events_search__event_type_select .pf-c-select__menu {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pf-c-toolbar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.keycloak__events_table {
|
||||
margin-top: -4.25rem;
|
||||
}
|
||||
|
||||
.keycloak__events_empty_state_divider {
|
||||
margin-top: 4.25rem;
|
||||
}
|
|
@ -12,9 +12,15 @@ export default {
|
|||
emptyEventsInstructions: "Configure event logging in the realm settings",
|
||||
time: "Time",
|
||||
user: "User",
|
||||
userId: "User ID",
|
||||
username: "User name",
|
||||
email: "Email",
|
||||
eventType: "Event type",
|
||||
ipAddress: "IP address",
|
||||
client: "Client",
|
||||
dateFrom: "Date(from)",
|
||||
dateTo: "Date(to)",
|
||||
searchBtn: "Search events",
|
||||
resourcePath: "Resource path",
|
||||
resourceType: "Resource type",
|
||||
operationType: "Operation type",
|
||||
|
|
Loading…
Reference in a new issue