Change workflow view function to update. Also handle authorization.

Implements #43041
Implements #43450

Signed-off-by: Stan Silvert <ssilvert@redhat.com>
This commit is contained in:
Stan Silvert 2025-10-22 13:08:22 -04:00 committed by Pedro Igor
parent b287543f6c
commit 398cf1afed
6 changed files with 90 additions and 81 deletions

View File

@ -3607,11 +3607,15 @@ workflowDeleteConfirmDialog=This action will permanently delete the workflow. Th
workflowNameRequired=Workflow name is required.
workflowDeletedSuccess=The workflow has been deleted.
workflowDeleteError=Could not delete the workflow\: {{error}}
viewWorkflow=View workflow
updateWorkflow=Update workflow
copyWorkflow=Copy workflow
workflowDetails=Workflow details
viewWorkflowDetails=Workflows can not be edited except to change enabled/disabled. You can copy the workflow and edit the copy.
copyWorkflowDetails=You are about to create a new workflow based on an existing one. You can change the name and edit the JSON of the new workflow.
updateWorkflowDetails=Currently, workflows can not be edited except to change the name and enabled/disabled. You can copy the workflow and edit the copy.
copyWorkflowDetails=You are about to create a new workflow based on an existing one.
createWorkflowDetails=Create a new workflow by providing its JSON representation.
changeStatus=Change Status
changeStatusTooltip=Enable or disable this workflow
workflowEnabled=Workflow enabled
workflowDisabled=Workflow disabled
workflowUpdated=Workflow updated successfully
workflowUpdateError=Could not update the workflow\: {{error}}

View File

@ -65,7 +65,7 @@ const LeftNav = ({ title, path, id }: LeftNavProps) => {
export const PageNav = () => {
const { t } = useTranslation();
const { environment } = useEnvironment<Environment>();
const { hasSomeAccess } = useAccess();
const { hasAccess, hasSomeAccess } = useAccess();
const { componentTypes } = useServerInfo();
const isFeatureEnabled = useIsFeatureEnabled();
const pages =
@ -99,6 +99,9 @@ export const PageNav = () => {
"view-identity-providers",
);
const showWorkflows =
hasAccess("manage-realm") && isFeatureEnabled(Feature.Workflows);
const showManageRealm = environment.masterRealm === environment.realm;
return (
@ -145,9 +148,7 @@ export const PageNav = () => {
)}
<LeftNav title="identityProviders" path="/identity-providers" />
<LeftNav title="userFederation" path="/user-federation" />
{isFeatureEnabled(Feature.Workflows) && (
<LeftNav title="workflows" path="/workflows" />
)}
{showWorkflows && <LeftNav title="workflows" path="/workflows" />}
{isFeatureEnabled(Feature.DeclarativeUI) &&
pages?.map((p) => (
<LeftNav

View File

@ -31,6 +31,7 @@ import {
toWorkflowDetail,
} from "./routes/WorkflowDetail";
import { ViewHeader } from "../components/view-header/ViewHeader";
import WorkflowRepresentation from "libs/keycloak-admin-client/lib/defs/workflowRepresentation";
type AttributeForm = {
workflowJSON?: string;
@ -48,7 +49,6 @@ export default function WorkflowDetailForm() {
const { addAlert, addError } = useAlerts();
const { mode, id } = useParams<WorkflowDetailParams>();
const [workflowJSON, setWorkflowJSON] = useState("");
const [enabled, setEnabled] = useState(true);
useFetch(
async () => {
@ -70,53 +70,33 @@ export default function WorkflowDetailForm() {
}
setWorkflowJSON(JSON.stringify(workflow, null, 2));
setEnabled(workflow?.enabled ?? true);
},
[mode, id],
);
const onSubmit: SubmitHandler<AttributeForm> = async () => {
if (mode === "view") {
navigate(toWorkflowDetail({ realm, mode: "copy", id: id! }));
return;
const validateWorkflowJSON = (): WorkflowRepresentation => {
const json = JSON.parse(workflowJSON);
if (!json.name) {
throw new Error(t("workflowNameRequired"));
}
return json;
};
const onUpdate: SubmitHandler<AttributeForm> = async () => {
try {
const json = JSON.parse(workflowJSON);
if (!json.name) {
throw new Error(t("workflowNameRequired"));
}
const payload = {
realm,
...json,
};
await adminClient.workflows.create(payload);
addAlert(t("workflowCreated"), AlertVariant.success);
navigate(toWorkflows({ realm }));
const json = validateWorkflowJSON();
await adminClient.workflows.update({ id: json.id! }, json);
addAlert(t("workflowUpdated"), AlertVariant.success);
} catch (error) {
addError("workflowCreateError", error);
addError("workflowUpdateError", error);
}
};
const toggleEnabled = async () => {
const json = JSON.parse(workflowJSON);
json.enabled = !enabled;
const onCreate: SubmitHandler<AttributeForm> = async () => {
try {
const payload = {
realm,
...json,
};
await adminClient.workflows.update({ id: json.id }, payload);
setWorkflowJSON(JSON.stringify(json, null, 2));
setEnabled(!enabled);
addAlert(
enabled ? t("workflowDisabled") : t("workflowEnabled"),
AlertVariant.success,
);
await adminClient.workflows.create(validateWorkflowJSON());
addAlert(t("workflowCreated"), AlertVariant.success);
navigate(toWorkflows({ realm }));
} catch (error) {
addError("workflowCreateError", error);
}
@ -125,32 +105,31 @@ export default function WorkflowDetailForm() {
const titlekeyMap: Record<WorkflowDetailParams["mode"], string> = {
copy: "copyWorkflow",
create: "createWorkflow",
view: "viewWorkflow",
update: "updateWorkflow",
};
const subkeyMap: Record<WorkflowDetailParams["mode"], string> = {
copy: "copyWorkflowDetails",
create: "createWorkflowDetails",
view: "viewWorkflowDetails",
update: "updateWorkflowDetails",
};
return (
<>
<ViewHeader
titleKey={titlekeyMap[mode]}
subKey={subkeyMap[mode]}
isEnabled={enabled}
onToggle={mode === "view" ? toggleEnabled : undefined}
/>
<ViewHeader titleKey={titlekeyMap[mode]} subKey={subkeyMap[mode]} />
<FormProvider {...form}>
<PageSection variant="light">
<FormAccess
isHorizontal
onSubmit={handleSubmit(onSubmit)}
onSubmit={
mode === "update"
? handleSubmit(onUpdate)
: handleSubmit(onCreate)
}
role={"manage-realm"}
className="pf-v5-u-mt-lg"
fineGrainedAccess={true} // TODO: Set this properly
fineGrainedAccess={true}
>
<FormGroup
label={t("workflowJSON")}
@ -171,7 +150,6 @@ export default function WorkflowDetailForm() {
<CodeEditor
id="workflowJSON"
data-testid="workflowJSON"
readOnly={mode === "view"}
value={workflowJSON}
onChange={(value) => setWorkflowJSON(value ?? "")}
language="json"
@ -181,25 +159,27 @@ export default function WorkflowDetailForm() {
/>
</FormGroup>
<ActionGroup>
{mode !== "view" && (
<FormSubmitButton
formState={form.formState}
data-testid="save"
allowInvalid
allowNonDirty
>
{t("save")}
</FormSubmitButton>
)}
{mode === "view" && (
<FormSubmitButton
formState={form.formState}
<FormSubmitButton
formState={form.formState}
data-testid="save"
allowInvalid
allowNonDirty
>
{mode === "update" ? t("save") : t("create")}
</FormSubmitButton>
{mode === "update" && (
<Button
data-testid="copy"
allowInvalid
allowNonDirty
variant="link"
component={(props) => (
<Link
{...props}
to={toWorkflowDetail({ realm, mode: "copy", id: id! })}
/>
)}
>
{t("copy")}
</FormSubmitButton>
</Button>
)}
<Button
data-testid="cancel"

View File

@ -16,7 +16,6 @@ import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { useAdminClient } from "../admin-client";
import { ViewHeader } from "../components/view-header/ViewHeader";
//import { useAccess } from "../context/access/Access";
import { useRealm } from "../context/realm-context/RealmContext";
import helpUrls from "../help-urls";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
@ -30,10 +29,6 @@ export default function WorkflowsSection() {
const navigate = useNavigate();
const { addAlert, addError } = useAlerts();
// TODO: handle role-based access
//const { hasAccess } = useAccess();
//const isManager = hasAccess("manage-realm");
const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1);
@ -51,6 +46,25 @@ export default function WorkflowsSection() {
);
};
const toggleEnabled = async (workflowJSON: WorkflowRepresentation) => {
workflowJSON.enabled = !(workflowJSON.enabled ?? true);
try {
await adminClient.workflows.update(
{ id: workflowJSON.id! },
workflowJSON,
);
addAlert(
workflowJSON.enabled ? t("workflowEnabled") : t("workflowDisabled"),
AlertVariant.success,
);
refresh();
} catch (error) {
addError("workflowUpdateError", error);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "workflowDeleteConfirm",
messageKey: t("workflowDeleteConfirmDialog", {
@ -100,7 +114,7 @@ export default function WorkflowsSection() {
displayKey: "name",
cellRenderer: (row: WorkflowRepresentation) => (
<Link
to={toWorkflowDetail({ realm, mode: "view", id: row.id! })}
to={toWorkflowDetail({ realm, mode: "update", id: row.id! })}
>
{row.name}
</Link>
@ -111,8 +125,8 @@ export default function WorkflowsSection() {
displayKey: "id",
},
{
name: "enabled",
displayKey: "enabled",
name: "status",
displayKey: "status",
cellRenderer: (row: WorkflowRepresentation) => {
return (row.enabled ?? true) ? t("enabled") : t("disabled");
},
@ -135,6 +149,16 @@ export default function WorkflowsSection() {
);
},
} as Action<WorkflowRepresentation>,
{
title: t("changeStatus"),
tooltipProps: {
content: t("changeStatusTooltip"),
},
onRowClick: (workflow) => {
setSelectedWorkflow(workflow);
void toggleEnabled(workflow);
},
} as Action<WorkflowRepresentation>,
]}
loader={loader}
ariaLabelKey="workflows"

View File

@ -6,7 +6,7 @@ import type { AppRouteObject } from "../../routes";
export type WorkflowDetailParams = {
realm: string;
id: string;
mode: "view" | "copy" | "create";
mode: "update" | "copy" | "create";
};
const WorkflowDetailForm = lazy(() => import("../WorkflowDetailForm"));
@ -16,7 +16,7 @@ export const WorkflowDetailRoute: AppRouteObject = {
element: <WorkflowDetailForm />,
breadcrumb: (t) => t("workflowDetails"),
handle: {
access: "anyone", // TODO: update access when view permission is added
access: "manage-realm",
},
};

View File

@ -12,7 +12,7 @@ export const WorkflowsRoute: AppRouteObject = {
element: <WorkflowsSection />,
breadcrumb: (t) => t("workflows"),
handle: {
access: "view-realm",
access: "manage-realm",
},
};