more robust links out from events (#2133)

This commit is contained in:
Erik Jan de Wit 2022-02-23 15:28:59 +01:00 committed by GitHub
parent bda591fb0d
commit 6a4e490928
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 47 deletions

View file

@ -8,12 +8,12 @@ export type ClientScopeTab = "settings" | "mappers" | "scope";
export type ClientScopeParams = { export type ClientScopeParams = {
realm: string; realm: string;
id: string; id: string;
type: string;
tab: ClientScopeTab; tab: ClientScopeTab;
type?: string;
}; };
export const ClientScopeRoute: RouteDef = { export const ClientScopeRoute: RouteDef = {
path: "/:realm/client-scopes/:id/:type/:tab", path: "/:realm/client-scopes/:id/:tab/:type?",
component: lazy(() => import("../form/ClientScopeForm")), component: lazy(() => import("../form/ClientScopeForm")),
breadcrumb: (t) => t("client-scopes:clientScopeDetails"), breadcrumb: (t) => t("client-scopes:clientScopeDetails"),
access: "view-clients", access: "view-clients",

View file

@ -15,7 +15,6 @@ import {
SelectOption, SelectOption,
SelectVariant, SelectVariant,
TextInput, TextInput,
Tooltip,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { import {
cellWidth, cellWidth,
@ -30,7 +29,6 @@ import moment from "moment";
import React, { FunctionComponent, useMemo, useState } from "react"; import React, { FunctionComponent, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { pickBy } from "lodash-es"; import { pickBy } from "lodash-es";
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";
@ -38,6 +36,8 @@ import { useAdminClient } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { prettyPrintJSON } from "../util"; import { prettyPrintJSON } from "../util";
import { CellResourceLinkRenderer } from "./ResourceLinks";
import "./events.css"; import "./events.css";
type DisplayDialogProps = { type DisplayDialogProps = {
@ -87,27 +87,6 @@ const DisplayDialog: FunctionComponent<DisplayDialogProps> = ({
); );
}; };
const MAX_TEXT_LENGTH = 38;
const Truncate = ({
text,
children,
}: {
text?: string;
children: (text: string) => any;
}) => {
const definedText = text || "";
const needsTruncation = definedText.length > MAX_TEXT_LENGTH;
const truncatedText = definedText.substr(0, MAX_TEXT_LENGTH);
return (
<>
{needsTruncation && (
<Tooltip content={text}>{children(truncatedText + "...")}</Tooltip>
)}
{!needsTruncation && <>{children(definedText)}</>}
</>
);
};
export const AdminEvents = () => { export const AdminEvents = () => {
const { t } = useTranslation("events"); const { t } = useTranslation("events");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
@ -204,25 +183,6 @@ export const AdminEvents = () => {
commitFilters(); commitFilters();
} }
const LinkResource = (row: AdminEventRepresentation) => (
<Truncate text={row.resourcePath}>
{(text) => (
<>
{row.resourceType !== "COMPONENT" && (
<Link
to={`/${realm}/${row.resourcePath}${
row.resourceType !== "GROUP" ? "/settings" : ""
}`}
>
{text}
</Link>
)}
{row.resourceType === "COMPONENT" && <span>{text}</span>}
</>
)}
</Truncate>
);
const adminEventSearchFormDisplay = () => { const adminEventSearchFormDisplay = () => {
return ( return (
<Flex <Flex
@ -606,7 +566,7 @@ export const AdminEvents = () => {
{ {
name: "resourcePath", name: "resourcePath",
displayKey: "events:resourcePath", displayKey: "events:resourcePath",
cellRenderer: LinkResource, cellRenderer: CellResourceLinkRenderer,
}, },
{ {
name: "resourceType", name: "resourceType",

View file

@ -0,0 +1,118 @@
import React, { ReactElement } from "react";
import { Link } from "react-router-dom";
import { Tooltip } from "@patternfly/react-core";
import type AdminEventRepresentation from "@keycloak/keycloak-admin-client/lib/defs/adminEventRepresentation";
import { useRealm } from "../context/realm-context/RealmContext";
import { toClient } from "../clients/routes/Client";
import { toGroups } from "../groups/routes/Groups";
import { toClientScope } from "../client-scopes/routes/ClientScope";
import { toUser } from "../user/routes/User";
import { toRealmRole } from "../realm-roles/routes/RealmRole";
import { toFlow } from "../authentication/routes/Flow";
type ResourceLinkProps = {
event: AdminEventRepresentation;
};
const MAX_TEXT_LENGTH = 38;
const Truncate = ({
text,
children,
}: {
text?: string;
children: (text: string) => ReactElement;
}) => {
const definedText = text || "";
const needsTruncation = definedText.length > MAX_TEXT_LENGTH;
const truncatedText = definedText.substring(0, MAX_TEXT_LENGTH);
return needsTruncation ? (
<Tooltip content={text}>{children(truncatedText + "…")}</Tooltip>
) : (
children(definedText)
);
};
const isLinkable = (event: AdminEventRepresentation) => {
if (event.operationType === "DELETE") {
return false;
}
return (
event.resourceType === "USER" ||
event.resourceType === "GROUP_MEMBERSHIP" ||
event.resourceType === "GROUP" ||
event.resourceType === "CLIENT" ||
event.resourceType?.startsWith("AUTHORIZATION_RESOURCE") ||
event.resourceType === "CLIENT_SCOPE" ||
event.resourceType === "AUTH_FLOW" ||
event.resourcePath?.startsWith("roles-by-id")
);
};
const idRegex = new RegExp(
/([0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})/
);
const createLink = (realm: string, event: AdminEventRepresentation) => {
const part = idRegex.exec(event.resourcePath!);
if (!part) {
console.warn("event didn't contain a valid link", event);
return "";
}
const id = part[1];
if (
event.resourceType === "CLIENT" ||
event.resourceType?.startsWith("AUTHORIZATION_RESOURCE")
) {
return toClient({
realm,
clientId: id,
tab: event.resourceType === "CLIENT" ? "settings" : "authorization",
});
}
if (event.resourceType === "GROUP") {
return toGroups({ realm, id });
}
if (event.resourceType === "CLIENT_SCOPE") {
return toClientScope({ realm, id, tab: "settings" });
}
if (
event.resourceType === "USER" ||
event.resourceType === "GROUP_MEMBERSHIP"
) {
return toUser({ realm, id, tab: "settings" });
}
if (event.resourceType === "AUTH_FLOW") {
return toFlow({ realm, id, usedBy: "-" });
}
if (event.resourcePath?.startsWith("roles-by-id")) {
return toRealmRole({ realm, id });
}
return "";
};
export const ResourceLink = ({ event }: ResourceLinkProps) => {
const { realm } = useRealm();
return (
<Truncate text={event.resourcePath}>
{(text) =>
isLinkable(event) ? (
<Link to={createLink(realm, event)}>{text}</Link>
) : (
<span>{text}</span>
)
}
</Truncate>
);
};
export const CellResourceLinkRenderer = (
adminEvent: AdminEventRepresentation
) => <ResourceLink event={adminEvent} />;

View file

@ -3,10 +3,10 @@ import { lazy } from "react";
import { generatePath } from "react-router-dom"; import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config"; import type { RouteDef } from "../../route-config";
export type GroupsParams = { realm: string }; export type GroupsParams = { realm: string; id?: string };
export const GroupsRoute: RouteDef = { export const GroupsRoute: RouteDef = {
path: "/:realm/groups", path: "/:realm/groups/:id?",
component: lazy(() => import("../GroupsSection")), component: lazy(() => import("../GroupsSection")),
access: "query-groups", access: "query-groups",
matchOptions: { matchOptions: {