added missing action menu on flow details (#1311)

This commit is contained in:
Erik Jan de Wit 2021-10-14 16:54:07 +02:00 committed by GitHub
parent 7ba966b517
commit 0b4cb21134
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 217 additions and 13 deletions

View file

@ -5,7 +5,7 @@ export default class DuplicateFlowModal {
fill(name?: string, description?: string) { fill(name?: string, description?: string) {
if (name) { if (name) {
cy.findByTestId(this.aliasInput).type(name); cy.findByTestId(this.aliasInput).clear().type(name);
if (description) cy.get(this.descriptionInput).type(description); if (description) cy.get(this.descriptionInput).type(description);
} }

View file

@ -219,7 +219,7 @@ export const AuthenticationSection = () => {
key={key} key={key}
loader={loader} loader={loader}
ariaLabelKey="authentication:title" ariaLabelKey="authentication:title"
searchPlaceholderKey="authentication:searchForEvent" searchPlaceholderKey="authentication:searchForFlow"
toolbarItem={ toolbarItem={
<ToolbarItem> <ToolbarItem>
<Button <Button

View file

@ -37,7 +37,7 @@ export const DuplicateFlowModal = ({
useEffect(() => { useEffect(() => {
setValue("description", description); setValue("description", description);
setValue("name", t("copyOf", { name })); setValue("alias", t("copyOf", { name }));
}, [name, description, setValue]); }, [name, description, setValue]);
const save = async () => { const save = async () => {

View file

@ -0,0 +1,89 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { FormProvider, useForm } from "react-hook-form";
import {
AlertVariant,
Button,
ButtonVariant,
Form,
Modal,
ModalVariant,
} from "@patternfly/react-core";
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts";
import { NameDescription } from "./form/NameDescription";
type EditFlowModalProps = {
flow: AuthenticationFlowRepresentation;
toggleDialog: () => void;
};
type EditForm = {
alias: string;
description: string;
};
export const EditFlowModal = ({ flow, toggleDialog }: EditFlowModalProps) => {
const { t } = useTranslation("authentication");
const form = useForm<EditForm>({
shouldUnregister: false,
});
const { reset, handleSubmit } = form;
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
useEffect(() => {
reset(flow);
}, [flow, reset]);
const save = async (values: EditForm) => {
try {
await adminClient.authenticationManagement.updateFlow(
{ flowId: flow.id! },
{ ...flow, ...values }
);
addAlert(t("updateFlowSuccess"), AlertVariant.success);
} catch (error) {
addError("authentication:updateFlowError", error);
}
toggleDialog();
};
return (
<Modal
title={t("editFlow")}
isOpen={true}
onClose={toggleDialog}
variant={ModalVariant.small}
actions={[
<Button
id="modal-confirm"
key="confirm"
onClick={handleSubmit(save)}
data-testid="confirm"
>
{t("edit")}
</Button>,
<Button
data-testid="cancel"
id="modal-cancel"
key="cancel"
variant={ButtonVariant.link}
onClick={() => {
toggleDialog();
}}
>
{t("common:cancel")}
</Button>,
]}
>
<FormProvider {...form}>
<Form isHorizontal>
<NameDescription />
</Form>
</FormProvider>
</Modal>
);
};

View file

@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { import {
DataList, DataList,
@ -13,13 +13,14 @@ import {
ActionGroup, ActionGroup,
Button, Button,
ButtonVariant, ButtonVariant,
DropdownItem,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { CheckCircleIcon, PlusIcon, TableIcon } from "@patternfly/react-icons"; import { CheckCircleIcon, PlusIcon, TableIcon } from "@patternfly/react-icons";
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation"; import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation"; import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation"; import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
import type { FlowParams } from "./routes/Flow"; import { FlowParams, toFlow } from "./routes/Flow";
import { ViewHeader } from "../components/view-header/ViewHeader"; import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { EmptyExecutionState } from "./EmptyExecutionState"; import { EmptyExecutionState } from "./EmptyExecutionState";
@ -37,6 +38,10 @@ import { useAlerts } from "../components/alert/Alerts";
import { AddStepModal } from "./components/modals/AddStepModal"; import { AddStepModal } from "./components/modals/AddStepModal";
import { AddSubFlowModal, Flow } from "./components/modals/AddSubFlowModal"; import { AddSubFlowModal, Flow } from "./components/modals/AddSubFlowModal";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { DuplicateFlowModal } from "./DuplicateFlowModal";
import { useRealm } from "../context/realm-context/RealmContext";
import { toAuthentication } from "./routes/Authentication";
import { EditFlowModal } from "./EditFlowModal";
export const providerConditionFilter = ( export const providerConditionFilter = (
value: AuthenticationProviderRepresentation value: AuthenticationProviderRepresentation
@ -45,8 +50,10 @@ export const providerConditionFilter = (
export const FlowDetails = () => { export const FlowDetails = () => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { realm } = useRealm();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const { id, usedBy, builtIn } = useParams<FlowParams>(); const { id, usedBy, builtIn } = useParams<FlowParams>();
const history = useHistory();
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime()); const refresh = () => setKey(new Date().getTime());
@ -62,6 +69,8 @@ export const FlowDetails = () => {
const [showAddSubFlowDialog, setShowSubFlowDialog] = useState<boolean>(); const [showAddSubFlowDialog, setShowSubFlowDialog] = useState<boolean>();
const [selectedExecution, setSelectedExecution] = const [selectedExecution, setSelectedExecution] =
useState<ExpandableExecution>(); useState<ExpandableExecution>();
const [open, setOpen] = useState(false);
const [edit, setEdit] = useState(false);
useFetch( useFetch(
async () => { async () => {
@ -168,6 +177,20 @@ export const FlowDetails = () => {
} }
}; };
const setAsDefault = async () => {
try {
const r = await adminClient.realms.findOne({ realm });
await adminClient.realms.update(
{ realm },
{ ...r, browserFlow: flow?.alias }
);
addAlert(t("updateFlowSuccess"), AlertVariant.success);
history.push(toFlow({ id, realm, usedBy: "default", builtIn }));
} catch (error) {
addError("authentication:updateFlowError", error);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "authentication:deleteConfirmExecution", titleKey: "authentication:deleteConfirmExecution",
children: ( children: (
@ -191,10 +214,90 @@ export const FlowDetails = () => {
}, },
}); });
const [toggleDeleteFlow, DeleteFlowConfirm] = useConfirmDialog({
titleKey: "authentication:deleteConfirmFlow",
children: (
<Trans i18nKey="authentication:deleteConfirmFlowMessage">
{" "}
<strong>{{ flow: flow?.alias || "" }}</strong>.
</Trans>
),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.authenticationManagement.deleteFlow({
flowId: flow!.id!,
});
history.push(toAuthentication({ realm }));
addAlert(t("deleteFlowSuccess"), AlertVariant.success);
} catch (error) {
addError("authentication:deleteFlowError", error);
}
},
});
const hasExecutions = executionList?.expandableList.length !== 0; const hasExecutions = executionList?.expandableList.length !== 0;
const dropdownItems = [
...(usedBy !== "default"
? [
<DropdownItem
data-testid="set-as-default"
key="default"
onClick={() => setAsDefault()}
>
{t("setAsDefault")}
</DropdownItem>,
]
: []),
<DropdownItem key="duplicate" onClick={() => setOpen(true)}>
{t("duplicate")}
</DropdownItem>,
...(!builtIn
? [
<DropdownItem
data-testid="edit-flow"
key="edit"
onClick={() => setEdit(true)}
>
{t("editInfo")}
</DropdownItem>,
<DropdownItem
data-testid="delete-flow"
key="delete"
onClick={() => toggleDeleteFlow()}
>
{t("common:delete")}
</DropdownItem>,
]
: []),
];
return ( return (
<> <>
{open && (
<DuplicateFlowModal
name={flow?.alias!}
description={flow?.description!}
toggleDialog={() => setOpen(!open)}
onComplete={() => {
refresh();
setOpen(false);
}}
/>
)}
{edit && (
<EditFlowModal
flow={flow!}
toggleDialog={() => {
setEdit(!edit);
refresh();
}}
/>
)}
<DeleteFlowConfirm />
<ViewHeader <ViewHeader
titleKey={toUpperCase(flow?.alias || "")} titleKey={toUpperCase(flow?.alias || "")}
badges={[ badges={[
@ -213,6 +316,7 @@ export const FlowDetails = () => {
} }
: {}, : {},
]} ]}
dropdownItems={dropdownItems}
/> />
<PageSection variant="light"> <PageSection variant="light">
{hasExecutions && ( {hasExecutions && (
@ -279,6 +383,7 @@ export const FlowDetails = () => {
<> <>
{executionList.expandableList.map((execution) => ( {executionList.expandableList.map((execution) => (
<FlowRow <FlowRow
builtIn={!!builtIn}
key={execution.id} key={execution.id}
execution={execution} execution={execution}
onRowClick={(execution) => { onRowClick={(execution) => {

View file

@ -26,6 +26,7 @@ import { EditFlowDropdown } from "./EditFlowDropdown";
import "./flow-row.css"; import "./flow-row.css";
type FlowRowProps = { type FlowRowProps = {
builtIn: boolean;
execution: ExpandableExecution; execution: ExpandableExecution;
onRowClick: (execution: ExpandableExecution) => void; onRowClick: (execution: ExpandableExecution) => void;
onRowChange: (execution: AuthenticationExecutionInfoRepresentation) => void; onRowChange: (execution: AuthenticationExecutionInfoRepresentation) => void;
@ -38,6 +39,7 @@ type FlowRowProps = {
}; };
export const FlowRow = ({ export const FlowRow = ({
builtIn,
execution, execution,
onRowClick, onRowClick,
onRowChange, onRowChange,
@ -101,13 +103,14 @@ export const FlowRow = ({
{execution.configurable && ( {execution.configurable && (
<ExecutionConfigModal execution={execution} /> <ExecutionConfigModal execution={execution} />
)} )}
{execution.authenticationFlow && ( {execution.authenticationFlow && !builtIn && (
<EditFlowDropdown <EditFlowDropdown
execution={execution} execution={execution}
onAddExecution={onAddExecution} onAddExecution={onAddExecution}
onAddFlow={onAddFlow} onAddFlow={onAddFlow}
/> />
)} )}
{!builtIn && (
<Button <Button
variant="plain" variant="plain"
aria-label={t("common:delete")} aria-label={t("common:delete")}
@ -115,6 +118,7 @@ export const FlowRow = ({
> >
<TrashIcon /> <TrashIcon />
</Button> </Button>
)}
</DataListCell>, </DataListCell>,
]} ]}
/> />
@ -124,6 +128,7 @@ export const FlowRow = ({
hasSubList && hasSubList &&
execution.executionList?.map((execution) => ( execution.executionList?.map((execution) => (
<FlowRow <FlowRow
builtIn={builtIn}
key={execution.id} key={execution.id}
execution={execution} execution={execution}
onRowClick={onRowClick} onRowClick={onRowClick}

View file

@ -3,6 +3,7 @@ export default {
title: "Authentication", title: "Authentication",
flows: "Flows", flows: "Flows",
flowName: "Flow name", flowName: "Flow name",
searchForFlow: "Search for flow",
usedBy: "Used by", usedBy: "Used by",
buildIn: "Built-in", buildIn: "Built-in",
appliedByProviders: "Applied by the following providers", appliedByProviders: "Applied by the following providers",
@ -12,6 +13,10 @@ export default {
default: "Default", default: "Default",
notInUse: "Not in use", notInUse: "Not in use",
duplicate: "Duplicate", duplicate: "Duplicate",
setAsDefault: "Set as default",
editInfo: "Edit info",
editFlow: "Edit flow",
edit: "Edit",
deleteConfirmFlow: "Delete flow?", deleteConfirmFlow: "Delete flow?",
deleteConfirmFlowMessage: deleteConfirmFlowMessage:
'Are you sure you want to permanently delete the flow "<1>{{flow}}</1>".', 'Are you sure you want to permanently delete the flow "<1>{{flow}}</1>".',