8000 Improved user and client event tabs by agagancarczyk · Pull Request #34698 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion js/apps/admin-ui/cypress/e2e/clients_test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1221,7 +1221,7 @@ describe("Clients test", () => {
.checkTabExists(ClientsDetailsTab.Sessions, true)
.checkTabExists(ClientsDetailsTab.Permissions, true)
.checkTabExists(ClientsDetailsTab.Advanced, true)
.checkTabExists(ClientsDetailsTab.UserEvents, true)
.checkTabExists(ClientsDetailsTab.ClientEvents, true)
.checkNumberOfTabsIsEqual(6);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export enum ClientsDetailsTab {
ServiceAccountsRoles = "Service accounts roles",
Advanced = "Advanced",
Scope = "Scope",
UserEvents = "User events",
ClientEvents = "Client events",
}

export default class ClientDetailsPage extends CommonPage {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3296,3 +3296,8 @@ clearUserCacheHelp=This will clear entries for all realms.
clearKeysCacheHelp=Clears all entries from the cache of external public keys. These are keys of external clients or identity providers. This will clear all entries for all realms.
clearCacheSuccess=Cache cleared successfully
clearCacheError=Could not clear cache\: {{error}}
clientEvents=Client events
searchForClientEvent=Search client event
emptyClientEvents=No client events
emptyClientEventsTabInstructions=There are no client events in this realm. To configure event tracking settings, visit <1>Event Configs</1>.
emptyUserEventsTabInstructions=There are no user events in this realm. To configure event tracking settings, visit <1>Event Configs</1>.
28 changes: 23 additions & 5 deletions js/apps/admin-ui/src/clients/ClientDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ export default function ClientDetails() {
});

const [client, setClient] = useState<ClientRepresentation>();
const [isClientEventsSearchPerformed, setIsClientEventsSearchPerformed] =
useState(true);

const loader = async () => {
const roles = await adminClient.clients.listRoles({ id: clientId });
Expand Down Expand Up @@ -307,13 +309,26 @@ export default function ClientDetails() {
};

useFetch(
() => adminClient.clients.findOne({ id: clientId }),
(fetchedClient) => {
async () => {
const fetchedClient = await adminClient.clients.findOne({ id: clientId });
const events = await adminClient.realms.findEvents({
realm,
client: fetchedClient?.clientId,
});

return { fetchedClient, events };
},
({ fetchedClient, events }) => {
if (!fetchedClient) {
throw new Error(t("notFound"));
}

setClient(cloneDeep(fetchedClient));
setupForm(fetchedClient);

if (events && events.length > 0) {
setIsClientEventsSearchPerformed(false);
}
},
[clientId, key],
);
Expand Down Expand Up @@ -667,11 +682,14 @@ export default function ClientDetails() {
</Tab>
{hasAccess("view-events") && (
<Tab
data-testid="user-events-tab"
title={<TabTitleText>{t("userEvents")}</TabTitleText>}
data-testid="client-events-tab"
title={<TabTitleText>{t("clientEvents")}</TabTitleText>}
{...userEventsTab}
>
<UserEvents client={client.clientId} />
<UserEvents
isClientEventsSearchPerformed={isClientEventsSearchPerformed}
client={client.clientId}
/>
</Tab>
)}
</RoutableTabs>
Expand Down
84 changes: 64 additions & 20 deletions js/apps/admin-ui/src/events/UserEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ import { cellWidth } from "@patternfly/react-table";
import { pickBy } from "lodash-es";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useAdminClient } from "../admin-client";
import DropdownPanel from "../components/dropdown-panel/DropdownPanel";
import { useRealm } from "../context/realm-context/RealmContext";
import { toUser } from "../user/routes/User";
import useFormatDate, { FORMAT_DATE_AND_TIME } from "../utils/useFormatDate";

import { toRealmSettings } from "../realm-settings/routes/RealmSettings";
import "./events.css";

type UserEventSearchForm = {
Expand Down Expand Up @@ -114,9 +114,16 @@ const UserDetailLink = (event: EventRepresentation) => {
type UserEventsProps = {
user?: string;
client?: string;
isClientEventsSearchPerformed?: boolean;
isUserEventsSearchPerformed?: boolean;
};

export const UserEvents = ({ user, client }: UserEventsProps) => {
export const UserEvents = ({
user,
client,
isClientEventsSearchPerformed,
isUserEventsSearchPerformed,
}: UserEventsProps) => {
const { adminClient } = useAdminClient();

const { t } = useTranslation();
Expand Down Expand Up @@ -231,7 +238,9 @@ export const UserEvents = ({ user, client }: UserEventsProps) => {
>
<FlexItem>
<DropdownPanel
buttonText={t("searchForUserEvent")}
buttonText={
client ? t("searchForClientEvent") : t("searchForUserEvent")
}
setSearchDropdownOpen={setSearchDropdownOpen}
searchDropdownOpen={searchDropdownOpen}
marginRight="2.5rem"
Expand All @@ -243,12 +252,13 @@ export const UserEvents = ({ user, client }: UserEventsProps) => {
>
isHorizontal
>
<TextControl
name="user"
label={t("userId")}
data-testid="userId-searchField"
isDisabled={!!user}
/>
{!user && (
<TextControl
name="user"
label={t("userId")}
data-testid="userId-searchField"
/>
)}
<FormGroup
label={t("eventType")}
fieldId="kc-eventType"
Expand Down Expand Up @@ -310,12 +320,13 @@ export const UserEvents = ({ user, client }: UserEventsProps) => {
)}
/>
</FormGroup>
<TextControl
name="client"
label={t("client")}
data-testid="client-searchField"
isDisabled={!!client}
/>
{!client && (
<TextControl
name="client"
label={t("client")}
data-testid="client-searchField"
/>
)}
<FormGroup
label={t("dateFrom")}
fieldId="kc-dateFrom"
Expand Down Expand Up @@ -386,16 +397,15 @@ export const UserEvents = ({ user, client }: UserEventsProps) => {
string | EventType[],
];

const disableClose =
(key === "user" && !!user) ||
(key === "client" && !!client);
if (client && key === "client") return null;
if (user && key === "user") return null;

return (
<ChipGroup
className="pf-v5-u-mt-md pf-v5-u-mr-md"
key={key}
categoryName={filterLabels[key]}
isClosable={!disableClose}
isClosable
=> removeFilter(key)}
>
{typeof value === "string" ? (
Expand All @@ -421,6 +431,40 @@ export const UserEvents = ({ user, client }: UserEventsProps) => {
);
};

const getEventSearchInstructions = (eventSearch: string) => (
<Trans
i18nKey={
eventSearch === "client"
? "emptyClientEventsTabInstructions"
: "emptyUserEventsTabInstructions"
}
>
If you want to configure events, please enter
<Link to={toRealmSettings({ realm, tab: "events" })}>
{t("eventConfig")}
</Link>
page realm settings to configure.
</Trans>
);

if (isClientEventsSearchPerformed || isUserEventsSearchPerformed) {
return (
<ListEmptyState
message={
isClientEventsSearchPerformed
? t("emptyClientEvents")
: t("emptyUserEvents")
}
instructions={
isClientEventsSearchPerformed
? getEventSearchInstructions("client")
: getEventSearchInstructions("user")
}
isSearchVariant
/>
);
}

return (
<div className="keycloak__events_table">
<KeycloakDataTable
Expand Down
26 changes: 19 additions & 7 deletions js/apps/admin-ui/src/user/EditUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export default function EditUser() {
const isFeatureEnabled = useIsFeatureEnabled();
const showOrganizations =
isFeatureEnabled(Feature.Organizations) && realm?.organizationsEnabled;
const [isUserEventsSearchPerformed, setIsUserEventsSearchPerformed] =
useState(true);

const toTab = (tab: UserTab) =>
toUser({
Expand All @@ -118,23 +120,27 @@ export default function EditUser() {
useFetch(
async () =>
Promise.all([
adminClient.users.findOne({
id: id!,
userProfileMetadata: true,
}) as UIUserRepresentation | undefined,
adminClient.users.findOne({ id: id!, userProfileMetadata: true }) as
| UIUserRepresentation
| undefined,
adminClient.attackDetection.findOne({ id: id! }),
adminClient.users.getUnmanagedAttributes({ id: id! }),
adminClient.users.getProfile({ realm: realmName }),
showOrganizations
? adminClient.organizations.find({ first: 0, max: 1 })
: [],
adminClient.realms.findEvents({
realm: realmName,
user: id!,
}),
]),
([
userData,
attackDetection,
unmanagedAttributes,
upConfig,
organizations,
events,
]) => {
if (!userData || !realm || !attackDetection) {
throw new Error(t("notFound"));
Expand All @@ -157,10 +163,13 @@ export default function EditUser() {

const isBruteForceProtected = realm.bruteForceProtected;
const isLocked = isBruteForceProtected && attackDetection.disabled;

setBruteForced({ isBruteForceProtected, isLocked });
setRealmHasOrganizations(organizations.length === 1);

if (events && events.length > 0) {
setIsUserEventsSearchPerformed(false);
}

setRealmHasOrganizations(organizations.length === 1);
form.reset(toUserFormFields(user));
},
[refreshCount],
Expand Down Expand Up @@ -432,7 +441,10 @@ export default function EditUser() {
title={<TabTitleText>{t("userEvents")}</TabTitleText>}
{...userEventsTab}
>
<UserEvents user={user.id} />
<UserEvents
isUserEventsSearchPerformed={isUserEventsSearchPerformed}
user={user.id}
/>
</Tab>
)}
</RoutableTabs>
Expand Down
0