fixing UXD review issue (#529)
* fixing UXD review issue fixing: #509 * added detail expandable sections
This commit is contained in:
parent
b86db32ba8
commit
9e5104b668
7 changed files with 187 additions and 50 deletions
|
@ -1,4 +1,4 @@
|
||||||
import React, { ReactNode, useEffect, useState } from "react";
|
import React, { isValidElement, ReactNode, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useErrorHandler } from "react-error-boundary";
|
import { useErrorHandler } from "react-error-boundary";
|
||||||
import {
|
import {
|
||||||
|
@ -20,21 +20,33 @@ import { asyncStateFetch } from "../../context/auth/AdminClient";
|
||||||
import { ListEmptyState } from "../list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../list-empty-state/ListEmptyState";
|
||||||
import { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon";
|
import { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon";
|
||||||
|
|
||||||
type Row<T> = {
|
type TitleCell = { title: JSX.Element };
|
||||||
|
type Cell<T> = keyof T | JSX.Element | TitleCell;
|
||||||
|
|
||||||
|
type BaseRow<T> = {
|
||||||
data: T;
|
data: T;
|
||||||
|
cells: Cell<T>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Row<T> = BaseRow<T> & {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
|
isOpen: boolean;
|
||||||
disableSelection: boolean;
|
disableSelection: boolean;
|
||||||
disableActions: boolean;
|
disableActions: boolean;
|
||||||
cells: (keyof T | JSX.Element)[];
|
};
|
||||||
|
|
||||||
|
type SubRow<T> = BaseRow<T> & {
|
||||||
|
parent: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DataTableProps<T> = {
|
type DataTableProps<T> = {
|
||||||
ariaLabelKey: string;
|
ariaLabelKey: string;
|
||||||
columns: Field<T>[];
|
columns: Field<T>[];
|
||||||
rows: Row<T>[];
|
rows: (Row<T> | SubRow<T>)[];
|
||||||
actions?: IActions;
|
actions?: IActions;
|
||||||
actionResolver?: IActionsResolver;
|
actionResolver?: IActionsResolver;
|
||||||
onSelect?: (isSelected: boolean, rowIndex: number) => void;
|
onSelect?: (isSelected: boolean, rowIndex: number) => void;
|
||||||
|
onCollapse?: (isOpen: boolean, rowIndex: number) => void;
|
||||||
canSelectAll: boolean;
|
canSelectAll: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,6 +57,7 @@ function DataTable<T>({
|
||||||
actionResolver,
|
actionResolver,
|
||||||
ariaLabelKey,
|
ariaLabelKey,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onCollapse,
|
||||||
canSelectAll,
|
canSelectAll,
|
||||||
...props
|
...props
|
||||||
}: DataTableProps<T>) {
|
}: DataTableProps<T>) {
|
||||||
|
@ -58,6 +71,11 @@ function DataTable<T>({
|
||||||
? (_, isSelected, rowIndex) => onSelect(isSelected, rowIndex)
|
? (_, isSelected, rowIndex) => onSelect(isSelected, rowIndex)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onCollapse={
|
||||||
|
onCollapse
|
||||||
|
? (_, rowIndex, isOpen) => onCollapse(isOpen, rowIndex)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
canSelectAll={canSelectAll}
|
canSelectAll={canSelectAll}
|
||||||
cells={columns.map((column) => {
|
cells={columns.map((column) => {
|
||||||
return { ...column, title: t(column.displayKey || column.name) };
|
return { ...column, title: t(column.displayKey || column.name) };
|
||||||
|
@ -81,6 +99,12 @@ export type Field<T> = {
|
||||||
cellRenderer?: (row: T) => ReactNode;
|
cellRenderer?: (row: T) => ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DetailField<T> = {
|
||||||
|
name: string;
|
||||||
|
enabled?: (row: T) => boolean;
|
||||||
|
cellRenderer?: (row: T) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
export type Action<T> = IAction & {
|
export type Action<T> = IAction & {
|
||||||
onRowClick?: (row: T) => Promise<boolean> | void;
|
onRowClick?: (row: T) => Promise<boolean> | void;
|
||||||
};
|
};
|
||||||
|
@ -89,6 +113,7 @@ export type DataListProps<T> = {
|
||||||
loader: (first?: number, max?: number, search?: string) => Promise<T[]>;
|
loader: (first?: number, max?: number, search?: string) => Promise<T[]>;
|
||||||
onSelect?: (value: T[]) => void;
|
onSelect?: (value: T[]) => void;
|
||||||
canSelectAll?: boolean;
|
canSelectAll?: boolean;
|
||||||
|
detailColumns?: DetailField<T>[];
|
||||||
isRowDisabled?: (value: T) => boolean;
|
isRowDisabled?: (value: T) => boolean;
|
||||||
isPaginated?: boolean;
|
isPaginated?: boolean;
|
||||||
ariaLabelKey: string;
|
ariaLabelKey: string;
|
||||||
|
@ -119,6 +144,7 @@ export type DataListProps<T> = {
|
||||||
* @param {boolean} props.isPaginated - if true the the loader will be called with first, max and search and a pager will be added in the header
|
* @param {boolean} props.isPaginated - if true the the loader will be called with first, max and search and a pager will be added in the header
|
||||||
* @param {(first?: number, max?: number, search?: string) => Promise<T[]>} props.loader - loader function that will fetch the data to display first, max and search are only applicable when isPaginated = true
|
* @param {(first?: number, max?: number, search?: string) => Promise<T[]>} props.loader - loader function that will fetch the data to display first, max and search are only applicable when isPaginated = true
|
||||||
* @param {Field<T>} props.columns - definition of the columns
|
* @param {Field<T>} props.columns - definition of the columns
|
||||||
|
* @param {Field<T>} props.detailColumns - definition of the columns expandable columns
|
||||||
* @param {Action[]} props.actions - the actions that appear on the row
|
* @param {Action[]} props.actions - the actions that appear on the row
|
||||||
* @param {IActionsResolver} props.actionResolver Resolver for the given action
|
* @param {IActionsResolver} props.actionResolver Resolver for the given action
|
||||||
* @param {ReactNode} props.toolbarItem - Toolbar items that appear on the top of the table {@link ToolbarItem}
|
* @param {ReactNode} props.toolbarItem - Toolbar items that appear on the top of the table {@link ToolbarItem}
|
||||||
|
@ -130,6 +156,7 @@ export function KeycloakDataTable<T>({
|
||||||
isPaginated = false,
|
isPaginated = false,
|
||||||
onSelect,
|
onSelect,
|
||||||
canSelectAll = false,
|
canSelectAll = false,
|
||||||
|
detailColumns,
|
||||||
isRowDisabled,
|
isRowDisabled,
|
||||||
loader,
|
loader,
|
||||||
columns,
|
columns,
|
||||||
|
@ -143,9 +170,9 @@ export function KeycloakDataTable<T>({
|
||||||
}: DataListProps<T>) {
|
}: DataListProps<T>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selected, setSelected] = useState<T[]>([]);
|
const [selected, setSelected] = useState<T[]>([]);
|
||||||
const [rows, setRows] = useState<Row<T>[]>();
|
const [rows, setRows] = useState<(Row<T> | SubRow<T>)[]>();
|
||||||
const [unPaginatedData, setUnPaginatedData] = useState<T[]>();
|
const [unPaginatedData, setUnPaginatedData] = useState<T[]>();
|
||||||
const [filteredData, setFilteredData] = useState<Row<T>[]>();
|
const [filteredData, setFilteredData] = useState<(Row<T> | SubRow<T>)[]>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [max, setMax] = useState(10);
|
const [max, setMax] = useState(10);
|
||||||
|
@ -194,7 +221,7 @@ export function KeycloakDataTable<T>({
|
||||||
);
|
);
|
||||||
}, [key, first, max, search]);
|
}, [key, first, max, search]);
|
||||||
|
|
||||||
const getNodeText = (node: keyof T | JSX.Element): string => {
|
const getNodeText = (node: Cell<T>): string => {
|
||||||
if (["string", "number"].includes(typeof node)) {
|
if (["string", "number"].includes(typeof node)) {
|
||||||
return node!.toString();
|
return node!.toString();
|
||||||
}
|
}
|
||||||
|
@ -202,27 +229,55 @@ export function KeycloakDataTable<T>({
|
||||||
return node.map(getNodeText).join("");
|
return node.map(getNodeText).join("");
|
||||||
}
|
}
|
||||||
if (typeof node === "object" && node) {
|
if (typeof node === "object" && node) {
|
||||||
return getNodeText(node.props.children);
|
return getNodeText(
|
||||||
|
isValidElement((node as TitleCell).title)
|
||||||
|
? (node as TitleCell).title.props.children
|
||||||
|
: (node as JSX.Element).props.children
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const convertToColumns = (data: T[]) => {
|
const convertToColumns = (data: T[]): (Row<T> | SubRow<T>)[] => {
|
||||||
return data!.map((value) => {
|
return data!
|
||||||
const disabledRow = isRowDisabled ? isRowDisabled(value) : false;
|
.map((value, index) => {
|
||||||
return {
|
const disabledRow = isRowDisabled ? isRowDisabled(value) : false;
|
||||||
data: value,
|
const row: (Row<T> | SubRow<T>)[] = [
|
||||||
disableSelection: disabledRow,
|
{
|
||||||
disableActions: disabledRow,
|
data: value,
|
||||||
selected: !!selected.find((v) => (v as any).id === (value as any).id),
|
disableSelection: disabledRow,
|
||||||
cells: columns.map((col) => {
|
disableActions: disabledRow,
|
||||||
if (col.cellRenderer) {
|
selected: !!selected.find(
|
||||||
return col.cellRenderer(value);
|
(v) => _.get(v, "id") === _.get(value, "id")
|
||||||
}
|
),
|
||||||
return _.get(value, col.name);
|
isOpen: false,
|
||||||
}),
|
cells: columns.map((col) => {
|
||||||
};
|
if (col.cellRenderer) {
|
||||||
});
|
return { title: col.cellRenderer(value) };
|
||||||
|
}
|
||||||
|
return _.get(value, col.name);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (
|
||||||
|
detailColumns &&
|
||||||
|
detailColumns[0] &&
|
||||||
|
detailColumns[0].enabled &&
|
||||||
|
detailColumns[0].enabled(value)
|
||||||
|
) {
|
||||||
|
row.push({
|
||||||
|
parent: index * 2,
|
||||||
|
cells: detailColumns!.map((col) => {
|
||||||
|
if (col.cellRenderer) {
|
||||||
|
return { title: col.cellRenderer(value) };
|
||||||
|
}
|
||||||
|
return _.get(value, col.name);
|
||||||
|
}),
|
||||||
|
} as SubRow<T>);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = (search: string) => {
|
const filter = (search: string) => {
|
||||||
|
@ -247,6 +302,9 @@ export function KeycloakDataTable<T>({
|
||||||
(filteredData || rows)![rowIndex].data
|
(filteredData || rows)![rowIndex].data
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
if (!isPaginated) {
|
||||||
|
setFilteredData(undefined);
|
||||||
|
}
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -264,12 +322,12 @@ export function KeycloakDataTable<T>({
|
||||||
if (rowIndex === -1) {
|
if (rowIndex === -1) {
|
||||||
setRows(
|
setRows(
|
||||||
data!.map((row) => {
|
data!.map((row) => {
|
||||||
row.selected = isSelected;
|
(row as Row<T>).selected = isSelected;
|
||||||
return row;
|
return row;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data![rowIndex].selected = isSelected;
|
(data![rowIndex] as Row<T>).selected = isSelected;
|
||||||
|
|
||||||
setRows([...rows!]);
|
setRows([...rows!]);
|
||||||
}
|
}
|
||||||
|
@ -284,13 +342,19 @@ export function KeycloakDataTable<T>({
|
||||||
// Selected rows are any rows previously selected from a different page, plus current page selections
|
// Selected rows are any rows previously selected from a different page, plus current page selections
|
||||||
const selectedRows = [
|
const selectedRows = [
|
||||||
...difference,
|
...difference,
|
||||||
...data!.filter((row) => row.selected).map((row) => row.data),
|
...data!.filter((row) => (row as Row<T>).selected).map((row) => row.data),
|
||||||
];
|
];
|
||||||
|
|
||||||
setSelected(selectedRows);
|
setSelected(selectedRows);
|
||||||
onSelect!(selectedRows);
|
onSelect!(selectedRows);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCollapse = (isOpen: boolean, rowIndex: number) => {
|
||||||
|
const data = filteredData || rows;
|
||||||
|
(data![rowIndex] as Row<T>).isOpen = isOpen;
|
||||||
|
setRows([...data!]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{rows && (
|
{rows && (
|
||||||
|
@ -319,6 +383,7 @@ export function KeycloakDataTable<T>({
|
||||||
{...props}
|
{...props}
|
||||||
canSelectAll={canSelectAll}
|
canSelectAll={canSelectAll}
|
||||||
onSelect={onSelect ? _onSelect : undefined}
|
onSelect={onSelect ? _onSelect : undefined}
|
||||||
|
onCollapse={detailColumns ? onCollapse : undefined}
|
||||||
actions={convertAction()}
|
actions={convertAction()}
|
||||||
actionResolver={actionResolver}
|
actionResolver={actionResolver}
|
||||||
rows={filteredData || rows}
|
rows={filteredData || rows}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { ReactElement, useContext, useState } from "react";
|
import React, { ReactElement, ReactNode, useContext, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
PageSection,
|
PageSection,
|
||||||
|
@ -27,7 +27,7 @@ export type ViewHeaderProps = {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
badgeId?: string;
|
badgeId?: string;
|
||||||
badgeIsRead?: boolean;
|
badgeIsRead?: boolean;
|
||||||
subKey: string;
|
subKey: string | ReactNode;
|
||||||
actionsDropdownId?: string;
|
actionsDropdownId?: string;
|
||||||
subKeyLinkProps?: FormattedLinkProps;
|
subKeyLinkProps?: FormattedLinkProps;
|
||||||
dropdownItems?: ReactElement[];
|
dropdownItems?: ReactElement[];
|
||||||
|
@ -133,7 +133,7 @@ export const ViewHeader = ({
|
||||||
{enabled && (
|
{enabled && (
|
||||||
<TextContent id="view-header-subkey">
|
<TextContent id="view-header-subkey">
|
||||||
<Text>
|
<Text>
|
||||||
{t(subKey)}
|
{React.isValidElement(subKey) ? subKey : t(subKey as string)}
|
||||||
{subKeyLinkProps && (
|
{subKeyLinkProps && (
|
||||||
<FormattedLink
|
<FormattedLink
|
||||||
{...subKeyLinkProps}
|
{...subKeyLinkProps}
|
||||||
|
|
|
@ -160,7 +160,7 @@ export const AdminEvents = () => {
|
||||||
{
|
{
|
||||||
name: "time",
|
name: "time",
|
||||||
displayKey: "events:time",
|
displayKey: "events:time",
|
||||||
cellRenderer: (row) => moment(row.time).fromNow(),
|
cellRenderer: (row) => moment(row.time).format("LLL"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "resourcePath",
|
name: "resourcePath",
|
||||||
|
|
|
@ -1,32 +1,40 @@
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { Link } from "react-router-dom";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import moment from "moment";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Label,
|
DescriptionList,
|
||||||
|
DescriptionListDescription,
|
||||||
|
DescriptionListGroup,
|
||||||
|
DescriptionListTerm,
|
||||||
PageSection,
|
PageSection,
|
||||||
Tab,
|
Tab,
|
||||||
TabTitleText,
|
TabTitleText,
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
|
Tooltip,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import moment from "moment";
|
import { cellWidth, expandable } from "@patternfly/react-table";
|
||||||
|
import { CheckCircleIcon, WarningTriangleIcon } from "@patternfly/react-icons";
|
||||||
import EventRepresentation from "keycloak-admin/lib/defs/eventRepresentation";
|
import EventRepresentation from "keycloak-admin/lib/defs/eventRepresentation";
|
||||||
|
|
||||||
import { useAdminClient } from "../context/auth/AdminClient";
|
import { useAdminClient } from "../context/auth/AdminClient";
|
||||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||||
import { RealmContext } from "../context/realm-context/RealmContext";
|
import { RealmContext } from "../context/realm-context/RealmContext";
|
||||||
import { InfoCircleIcon } from "@patternfly/react-icons";
|
|
||||||
import { AdminEvents } from "./AdminEvents";
|
import { AdminEvents } from "./AdminEvents";
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
||||||
|
|
||||||
|
import "./events-section.css";
|
||||||
|
|
||||||
export const EventsSection = () => {
|
export const EventsSection = () => {
|
||||||
const { t } = useTranslation("events");
|
const { t } = useTranslation("events");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { realm } = useContext(RealmContext);
|
const { realm } = useContext(RealmContext);
|
||||||
|
|
||||||
const [key, setKey] = useState("");
|
const [key, setKey] = useState(0);
|
||||||
const refresh = () => setKey(`${new Date().getTime()}`);
|
const refresh = () => setKey(new Date().getTime());
|
||||||
|
|
||||||
const loader = async (first?: number, max?: number, search?: string) => {
|
const loader = async (first?: number, max?: number, search?: string) => {
|
||||||
const params = {
|
const params = {
|
||||||
|
@ -40,21 +48,72 @@ export const EventsSection = () => {
|
||||||
return await adminClient.realms.findEvents({ ...params });
|
return await adminClient.realms.findEvents({ ...params });
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusRow = (event: EventRepresentation) => {
|
const StatusRow = (event: EventRepresentation) => (
|
||||||
return (
|
<>
|
||||||
<>
|
{!event.error && (
|
||||||
<Label color="red" icon={<InfoCircleIcon />}>
|
<span key={`status-${event.time}-${event.type}`}>
|
||||||
|
<CheckCircleIcon
|
||||||
|
color="green"
|
||||||
|
key={`circle-${event.time}-${event.type}`}
|
||||||
|
/>{" "}
|
||||||
{event.type}
|
{event.type}
|
||||||
</Label>
|
</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 UserDetailLink = (event: EventRepresentation) => (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
key={`link-${event.time}-${event.type}`}
|
||||||
|
to={`/${realm}/users/${event.userId}/details`}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
titleKey="events:title"
|
titleKey="events:title"
|
||||||
subKey="events:eventExplain"
|
subKey={
|
||||||
|
<Trans i18nKey="events:eventExplain">
|
||||||
|
If you want to configure user events, Admin events or Event
|
||||||
|
listeners, please enter
|
||||||
|
<Link to={`/${realm}/`}>{t("eventConfig")}</Link>
|
||||||
|
page realm settings to configure.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
divider={false}
|
divider={false}
|
||||||
/>
|
/>
|
||||||
<PageSection variant="light" className="pf-u-p-0">
|
<PageSection variant="light" className="pf-u-p-0">
|
||||||
|
@ -66,6 +125,13 @@ export const EventsSection = () => {
|
||||||
<KeycloakDataTable
|
<KeycloakDataTable
|
||||||
key={key}
|
key={key}
|
||||||
loader={loader}
|
loader={loader}
|
||||||
|
detailColumns={[
|
||||||
|
{
|
||||||
|
name: "details",
|
||||||
|
enabled: (event) => event.details !== undefined,
|
||||||
|
cellRenderer: DetailCell,
|
||||||
|
},
|
||||||
|
]}
|
||||||
isPaginated
|
isPaginated
|
||||||
ariaLabelKey="events:title"
|
ariaLabelKey="events:title"
|
||||||
searchPlaceholderKey="events:searchForEvent"
|
searchPlaceholderKey="events:searchForEvent"
|
||||||
|
@ -80,11 +146,13 @@ export const EventsSection = () => {
|
||||||
{
|
{
|
||||||
name: "time",
|
name: "time",
|
||||||
displayKey: "events:time",
|
displayKey: "events:time",
|
||||||
cellRenderer: (row) => moment(row.time).fromNow(),
|
cellRenderer: (row) => moment(row.time).format("LLL"),
|
||||||
|
cellFormatters: [expandable],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "userId",
|
name: "userId",
|
||||||
displayKey: "events:user",
|
displayKey: "events:user",
|
||||||
|
cellRenderer: UserDetailLink,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "type",
|
name: "type",
|
||||||
|
@ -94,6 +162,7 @@ export const EventsSection = () => {
|
||||||
{
|
{
|
||||||
name: "ipAddress",
|
name: "ipAddress",
|
||||||
displayKey: "events:ipAddress",
|
displayKey: "events:ipAddress",
|
||||||
|
transforms: [cellWidth(10)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "clientId",
|
name: "clientId",
|
||||||
|
|
4
src/events/events-section.css
Normal file
4
src/events/events-section.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
.keycloak_eventsection_details {
|
||||||
|
--pf-c-description-list--m-horizontal__term--width: 15ch;
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"events": {
|
"events": {
|
||||||
"title": "Events",
|
"title": "Events",
|
||||||
"eventExplain": "If you want to configure user events, Admin events or Event listeners, please enter Event configs page realm settings to configure.",
|
"eventExplain": "If you want to configure user events, Admin events or Event listeners, please enter <1>Event configs</1> page realm settings to configure.",
|
||||||
|
"eventConfigs": "Event configs",
|
||||||
"userEvents": "User events",
|
"userEvents": "User events",
|
||||||
"adminEvents": "Admin events",
|
"adminEvents": "Admin events",
|
||||||
"searchForEvent": "Search user event",
|
"searchForEvent": "Search user event",
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { useAdminClient } from "../context/auth/AdminClient";
|
||||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||||
import { formattedLinkTableCell } from "../components/external-link/FormattedLink";
|
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
import { emptyFormatter, boolFormatter } from "../util";
|
import { emptyFormatter, boolFormatter } from "../util";
|
||||||
|
@ -108,7 +107,6 @@ export const RolesList = ({
|
||||||
name: "name",
|
name: "name",
|
||||||
displayKey: "roles:roleName",
|
displayKey: "roles:roleName",
|
||||||
cellRenderer: RoleDetailLink,
|
cellRenderer: RoleDetailLink,
|
||||||
cellFormatters: [formattedLinkTableCell(), emptyFormatter()],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "composite",
|
name: "composite",
|
||||||
|
|
Loading…
Reference in a new issue