more robust links out from events (#2133)
This commit is contained in:
parent
bda591fb0d
commit
6a4e490928
4 changed files with 125 additions and 47 deletions
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
118
src/events/ResourceLinks.tsx
Normal file
118
src/events/ResourceLinks.tsx
Normal 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} />;
|
|
@ -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: {
|
||||||
|
|
Loading…
Reference in a new issue