mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-25 16:42:34 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user