Improve UXD (#38088)

* Improve UXD

fixes: #34202
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix for flow

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit
2025-03-17 16:53:48 +01:00
committed by GitHub
parent 21c903e562
commit fd32831786
7 changed files with 237 additions and 217 deletions

View File

@@ -6,7 +6,6 @@ import {
AlertVariant,
Button,
ButtonVariant,
DataList,
DragDrop,
DropdownItem,
Droppable,
@@ -19,6 +18,7 @@ import {
ToolbarItem,
} from "@patternfly/react-core";
import { DomainIcon, TableIcon } from "@patternfly/react-icons";
import { Table, Tbody } from "@patternfly/react-table";
import { useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
@@ -32,6 +32,7 @@ import { BuildInLabel } from "./BuildInLabel";
import { DuplicateFlowModal } from "./DuplicateFlowModal";
import { EditFlowModal } from "./EditFlowModal";
import { EmptyExecutionState } from "./EmptyExecutionState";
import { AuthenticationProviderContextProvider } from "./components/AuthenticationProviderContext";
import { FlowDiagram } from "./components/FlowDiagram";
import { FlowHeader } from "./components/FlowHeader";
import { FlowRow } from "./components/FlowRow";
@@ -307,7 +308,7 @@ export default function FlowDetails() {
];
return (
<>
<AuthenticationProviderContextProvider>
{bindFlowOpen && (
<BindFlowDialog
flowAlias={flow?.alias!}
@@ -438,33 +439,34 @@ export default function FlowDetails() {
}}
>
<Droppable hasNoWrapper>
<DataList aria-label={t("flows")}>
<Table aria-label={t("flows")} isTreeTable>
<FlowHeader />
<>
{executionList.expandableList.map((execution) => (
<FlowRow
builtIn={!!builtIn}
key={execution.id}
execution={execution}
onRowClick={(execution) => {
execution.isCollapsed = !execution.isCollapsed;
setExecutionList(executionList.clone());
}}
onRowChange={update}
onAddExecution={(execution, type) =>
addExecution(execution.displayName!, type)
}
onAddFlow={(execution, flow) =>
addFlow(execution.displayName!, flow)
}
onDelete={(execution) => {
setSelectedExecution(execution);
toggleDeleteDialog();
}}
/>
<Tbody draggable key={execution.id}>
<FlowRow
builtIn={!!builtIn}
execution={execution}
onRowClick={(execution) => {
execution.isCollapsed = !execution.isCollapsed;
setExecutionList(executionList.clone());
}}
onRowChange={update}
onAddExecution={(execution, type) =>
addExecution(execution.displayName!, type)
}
onAddFlow={(execution, flow) =>
addFlow(execution.displayName!, flow)
}
onDelete={(execution) => {
setSelectedExecution(execution);
toggleDeleteDialog();
}}
/>
</Tbody>
))}
</>
</DataList>
</Table>
</Droppable>
</DragDrop>
)}
@@ -513,6 +515,6 @@ export default function FlowDetails() {
/>
))}
</PageSection>
</>
</AuthenticationProviderContextProvider>
);
}

View File

@@ -0,0 +1,40 @@
import { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import {
createNamedContext,
useFetch,
useRequiredContext,
} from "@keycloak/keycloak-ui-shared";
import { PropsWithChildren, useState } from "react";
import { useAdminClient } from "../../admin-client";
export const AuthenticationProviderContext = createNamedContext<
{ providers?: AuthenticationProviderRepresentation[] } | undefined
>("AuthenticationProviderContext", undefined);
export const AuthenticationProviderContextProvider = ({
children,
}: PropsWithChildren) => {
const { adminClient } = useAdminClient();
const [providers, setProviders] =
useState<AuthenticationProviderRepresentation[]>();
useFetch(
async () =>
Promise.all([
adminClient.authenticationManagement.getClientAuthenticatorProviders(),
adminClient.authenticationManagement.getFormActionProviders(),
adminClient.authenticationManagement.getAuthenticatorProviders(),
]),
(providers) => setProviders(providers.flat()),
[],
);
return (
<AuthenticationProviderContext.Provider value={{ providers }}>
{children}
</AuthenticationProviderContext.Provider>
);
};
export const useAuthenticationProvider = () =>
useRequiredContext(AuthenticationProviderContext);

View File

@@ -1,34 +1,26 @@
import { DataListDragButton } from "@patternfly/react-core";
import { Th, Tr } from "@patternfly/react-table";
import { useTranslation } from "react-i18next";
import {
DataListItem,
DataListItemRow,
DataListDragButton,
DataListItemCells,
DataListCell,
} from "@patternfly/react-core";
import "./flow-header.css";
export const FlowHeader = () => {
const { t } = useTranslation();
return (
<DataListItem aria-labelledby="headerName" id="header">
<DataListItemRow>
<Tr aria-labelledby="headerName" id="header">
<Th>
<DataListDragButton
className="keycloak__authentication__header-drag-button"
aria-label={t("disabled")}
/>
<DataListItemCells
className="keycloak__authentication__header"
dataListCells={[
<DataListCell key="step" id="headerName">
{t("steps")}
</DataListCell>,
<DataListCell key="requirement">{t("requirement")}</DataListCell>,
<DataListCell key="config"></DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
<Th screenReaderText={t("expandRow")} />
</Th>
<Th>{t("steps")}</Th>
<Th>{t("requirement")}</Th>
<Th screenReaderText={t("config")}></Th>
<Th screenReaderText={t("config")}></Th>
<Th screenReaderText={t("config")}></Th>
<Th screenReaderText={t("config")}></Th>
</Tr>
);
};

View File

@@ -1,19 +1,7 @@
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import {
Button,
DataListCell,
DataListControl,
DataListDragButton,
DataListItem,
DataListItemCells,
DataListItemRow,
DataListToggle,
Draggable,
Text,
TextVariants,
Tooltip,
} from "@patternfly/react-core";
import { Button, Draggable, Tooltip } from "@patternfly/react-core";
import { TrashIcon } from "@patternfly/react-icons";
import { Td, TreeRowWrapper } from "@patternfly/react-table";
import { useTranslation } from "react-i18next";
import type { ExpandableExecution } from "../execution-model";
import { AddFlowDropdown } from "./AddFlowDropdown";
@@ -38,6 +26,21 @@ type FlowRowProps = {
onDelete: (execution: ExpandableExecution) => void;
};
export type FlowType = "flow" | "condition" | "execution" | "step";
const convertToType = (execution: ExpandableExecution): FlowType => {
if (execution.authenticationFlow) {
return "flow";
}
if (execution.displayName!.startsWith("Condition -")) {
return "condition";
}
if (execution.level === 0) {
return "execution";
}
return "step";
};
export const FlowRow = ({
builtIn,
execution,
@@ -50,90 +53,85 @@ export const FlowRow = ({
const { t } = useTranslation();
const hasSubList = !!execution.executionList?.length;
const treeRow = {
onCollapse: () => onRowClick(execution),
props: {
isExpanded: !execution.isCollapsed,
isDetailsExpanded: !execution.isCollapsed,
"aria-level": execution.level! + 1,
"aria-labelledby": execution.id,
"aria-setsize": hasSubList ? execution.executionList!.length : 0,
},
};
return (
<>
<Draggable key={`draggable-${execution.id}`} hasNoWrapper>
<DataListItem
className="keycloak__authentication__flow-item"
id={execution.id}
isExpanded={!execution.isCollapsed}
aria-labelledby={`title-id-${execution.id}`}
<TreeRowWrapper
row={{ props: treeRow.props }}
className="keycloak__authentication__flow-row"
>
<DataListItemRow
className="keycloak__authentication__flow-row"
aria-level={execution.level! + 1}
role="heading"
aria-labelledby={execution.id}
>
<DataListControl>
<DataListDragButton aria-label={t("dragHelp")} />
</DataListControl>
{hasSubList && (
<DataListToggle
onClick={() => onRowClick(execution)}
isExpanded={!execution.isCollapsed}
id={`toggle1-${execution.id}`}
aria-controls={execution.executionList![0].id}
/>
)}
<DataListItemCells
dataListCells={[
<DataListCell key={`${execution.id}-name`}>
{!execution.authenticationFlow && (
<FlowTitle
id={execution.id}
key={execution.id}
alias={execution.alias!}
title={execution.displayName!}
/>
)}
{execution.authenticationFlow && (
<span data-testid={execution.displayName}>
{execution.displayName} <br />{" "}
<Text component={TextVariants.small}>
{execution.alias} {execution.description}
</Text>
</span>
)}
</DataListCell>,
<DataListCell key={`${execution.id}-requirement`}>
<FlowRequirementDropdown
flow={execution}
onChange={onRowChange}
/>
</DataListCell>,
<DataListCell key={`${execution.id}-config`}>
<ExecutionConfigModal execution={execution} />
{execution.authenticationFlow && !builtIn && (
<>
<AddFlowDropdown
execution={execution}
onAddExecution={onAddExecution}
onAddFlow={onAddFlow}
/>
<EditFlow
execution={execution}
onRowChange={onRowChange}
/>
</>
)}
{!builtIn && (
<Tooltip content={t("delete")}>
<Button
variant="plain"
data-testid={`${execution.displayName}-delete`}
aria-label={t("delete")}
onClick={() => onDelete(execution)}
>
<TrashIcon />
</Button>
</Tooltip>
)}
</DataListCell>,
]}
<Td
draggableRow={{
id: execution.id!,
}}
/>
<Td treeRow={treeRow}>
<FlowTitle
id={execution.id}
type={convertToType(execution)}
key={execution.id}
subtitle={
(execution.authenticationFlow
? execution.description
: execution.alias) || ""
}
providerId={execution.providerId!}
title={execution.displayName!}
/>
</DataListItemRow>
</DataListItem>
</Td>
<Td>
<FlowRequirementDropdown flow={execution} onChange={onRowChange} />
</Td>
{(!execution.authenticationFlow || builtIn) && (
<>
<Td isActionCell />
<Td isActionCell />
</>
)}
<Td isActionCell>
<ExecutionConfigModal execution={execution} />
</Td>
{execution.authenticationFlow && !builtIn && (
<>
<Td isActionCell>
<AddFlowDropdown
execution={execution}
onAddExecution={onAddExecution}
onAddFlow={onAddFlow}
/>
</Td>
<Td isActionCell>
<EditFlow execution={execution} onRowChange={onRowChange} />
</Td>
</>
)}
<Td isActionCell>
{!builtIn && (
<Tooltip content={t("delete")}>
<Button
variant="plain"
data-testid={`${execution.displayName}-delete`}
aria-label={t("delete")}
onClick={() => onDelete(execution)}
>
<TrashIcon />
</Button>
</Tooltip>
)}
</Td>
</TreeRowWrapper>
</Draggable>
{!execution.isCollapsed &&
hasSubList &&

View File

@@ -1,24 +1,73 @@
import { Card, CardBody, Text, TextVariants } from "@patternfly/react-core";
import "./flow-title.css";
import { HelpItem } from "@keycloak/keycloak-ui-shared";
import { Label } from "@patternfly/react-core";
import {
CodeBranchIcon,
MapMarkerIcon,
ProcessAutomationIcon,
TaskIcon,
} from "@patternfly/react-icons";
import { useTranslation } from "react-i18next";
import { useAuthenticationProvider } from "./AuthenticationProviderContext";
import { FlowType } from "./FlowRow";
type FlowTitleProps = {
id?: string;
type: FlowType;
title: string;
alias: string;
subtitle: string;
providerId?: string;
};
export const FlowTitle = ({ id, title, alias }: FlowTitleProps) => {
const FlowIcon = ({ type }: { type: FlowType }) => {
switch (type) {
case "condition":
return <TaskIcon />;
case "flow":
return <CodeBranchIcon />;
case "execution":
return <ProcessAutomationIcon />;
case "step":
return <MapMarkerIcon />;
default:
return undefined;
}
};
function mapTypeToColor(type: FlowType) {
switch (type) {
case "condition":
return "purple";
case "flow":
return "green";
case "execution":
return "blue";
case "step":
return "cyan";
default:
return "grey";
}
}
export const FlowTitle = ({
id,
type,
title,
subtitle,
providerId,
}: FlowTitleProps) => {
const { t } = useTranslation();
const { providers } = useAuthenticationProvider();
const helpText =
providers?.find((p) => p.id === providerId)?.description || subtitle;
return (
<Card
data-testid={title}
className="keycloak__authentication__title"
isFlat
>
<CardBody data-id={id} id={`title-id-${id}`}>
{title} <br />
<Text component={TextVariants.small}>{alias}</Text>
</CardBody>
</Card>
<div data-testid={title}>
<span data-id={id} id={`title-id-${id}`}>
<Label icon={<FlowIcon type={type} />} color={mapTypeToColor(type)}>
{t(type)}
</Label>{" "}
{title}{" "}
{helpText && <HelpItem helpText={helpText} fieldLabelId={id!} />}
</span>
</div>
);
};

View File

@@ -1,55 +1,3 @@
.keycloak__authentication__flow-item:before {
width: 0;
}
.keycloak__authentication__flow-row[aria-level="1"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 2
);
}
.keycloak__authentication__flow-row[aria-level="2"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 3
);
}
.keycloak__authentication__flow-row[aria-level="3"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 4
);
}
.keycloak__authentication__flow-row[aria-level="4"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 5
);
}
.keycloak__authentication__flow-row[aria-level="5"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 6
);
}
.keycloak__authentication__flow-row[aria-level="6"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 7
);
}
.keycloak__authentication__flow-row[aria-level="7"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 8
);
}
.keycloak__authentication__flow-row[aria-level="8"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 9
);
}
.keycloak__authentication__flow-row[aria-level="9"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 10
);
}
.keycloak__authentication__flow-row[aria-level="10"] {
padding-left: calc(
var(--pf-v5-global--spacer--lg) * 11
);
.pf-v5-c-table__tr:hover {
background-color: var(--pf-v5-global--BackgroundColor--200);
}

View File

@@ -1,9 +0,0 @@
.keycloak__authentication__title {
width: fit-content;
width: -moz-fit-content;
}
.keycloak__authentication__title .pf-v5-c-card__body {
padding-bottom: var(--pf-v5-global--spacer--sm);
padding-top: var(--pf-v5-global--spacer--sm);
}