From 33b479fa3b69d25aa02a0bdd6a65fb8c1f46e38e Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Wed, 12 Nov 2025 08:13:00 -0500 Subject: [PATCH] Workflows now use YAML instead of JSON. Closes #43665 Signed-off-by: Stan Silvert --- .../admin/messages/messages_en.properties | 4 +-- js/apps/admin-ui/package.json | 3 +- .../src/workflows/WorkflowDetailForm.tsx | 33 ++++++++++-------- .../src/workflows/WorkflowsSection.tsx | 34 ++++++++----------- .../src/resources/workflows.ts | 11 ++++++ js/pnpm-lock.yaml | 4 +++ 6 files changed, 52 insertions(+), 37 deletions(-) diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 8821c035e8c..11b1314b942 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3593,8 +3593,8 @@ workflows=Workflows titleWorkflows=Workflows workflowsExplain=Workflows empower administrators to automate the management of realm resources through time-based or event-based policies. createWorkflow=Create workflow -workflowJSON=Workflow JSON -workflowJsonHelp=The JSON representation of the workflow. +workflowYAML=Workflow YAML +workflowYAMLHelp=The YAML representation of the workflow. emptyWorkflows=No workflows emptyWorkflowsInstructions=There are no workflows in this realm. Please create a workflow to get started. workflowCreated=The workflow has been created. diff --git a/js/apps/admin-ui/package.json b/js/apps/admin-ui/package.json index 5b36b029f62..d8747c2f5b3 100644 --- a/js/apps/admin-ui/package.json +++ b/js/apps/admin-ui/package.json @@ -109,7 +109,8 @@ "react-i18next": "^16.0.1", "react-router-dom": "^6.30.1", "reactflow": "^11.11.4", - "use-react-router-breadcrumbs": "^4.0.1" + "use-react-router-breadcrumbs": "^4.0.1", + "yaml": "^2.8.1" }, "devDependencies": { "@axe-core/playwright": "^4.10.2", diff --git a/js/apps/admin-ui/src/workflows/WorkflowDetailForm.tsx b/js/apps/admin-ui/src/workflows/WorkflowDetailForm.tsx index df3cda8eb94..e2395898671 100644 --- a/js/apps/admin-ui/src/workflows/WorkflowDetailForm.tsx +++ b/js/apps/admin-ui/src/workflows/WorkflowDetailForm.tsx @@ -13,6 +13,7 @@ import { } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; +import yaml from "yaml"; import { useAdminClient } from "../admin-client"; import { HelpItem, @@ -33,7 +34,7 @@ import { ViewHeader } from "../components/view-header/ViewHeader"; import WorkflowRepresentation from "libs/keycloak-admin-client/lib/defs/workflowRepresentation"; type AttributeForm = { - workflowJSON: string; + workflowYAML: string; }; export default function WorkflowDetailForm() { @@ -47,7 +48,7 @@ export default function WorkflowDetailForm() { const form = useForm({ mode: "onChange", defaultValues: { - workflowJSON: "", + workflowYAML: "", }, }); const { control, handleSubmit, setValue } = form; @@ -72,13 +73,13 @@ export default function WorkflowDetailForm() { workflowToSet.name = `${workflow.name} -- ${t("copy")}`; } - setValue("workflowJSON", JSON.stringify(workflowToSet, null, 2)); + setValue("workflowYAML", yaml.stringify(workflowToSet)); }, [mode, id, setValue, t], ); - const validateWorkflowJSON = (jsonStr: string): WorkflowRepresentation => { - const json = JSON.parse(jsonStr); + const validateworkflowYAML = (yamlStr: string): WorkflowRepresentation => { + const json: WorkflowRepresentation = yaml.parse(yamlStr); if (!json.name) { throw new Error(t("workflowNameRequired")); } @@ -87,8 +88,8 @@ export default function WorkflowDetailForm() { const onUpdate: SubmitHandler = async (data) => { try { - const json = validateWorkflowJSON(data.workflowJSON); - await adminClient.workflows.update({ id: json.id! }, json); + const json = validateworkflowYAML(data.workflowYAML); + await adminClient.workflows.update({ id }, json); addAlert(t("workflowUpdated"), AlertVariant.success); } catch (error) { addError("workflowUpdateError", error); @@ -97,8 +98,10 @@ export default function WorkflowDetailForm() { const onCreate: SubmitHandler = async (data) => { try { - const json = validateWorkflowJSON(data.workflowJSON); - await adminClient.workflows.create(json); + await adminClient.workflows.createAsYaml({ + realm, + yaml: data.workflowYAML, + }); addAlert(t("workflowCreated"), AlertVariant.success); navigate(toWorkflows({ realm })); } catch (error) { @@ -136,10 +139,10 @@ export default function WorkflowDetailForm() { fineGrainedAccess={true} > } @@ -147,15 +150,15 @@ export default function WorkflowDetailForm() { isRequired > ( )} diff --git a/js/apps/admin-ui/src/workflows/WorkflowsSection.tsx b/js/apps/admin-ui/src/workflows/WorkflowsSection.tsx index 1a44614f462..d6f84499005 100644 --- a/js/apps/admin-ui/src/workflows/WorkflowsSection.tsx +++ b/js/apps/admin-ui/src/workflows/WorkflowsSection.tsx @@ -3,6 +3,7 @@ import { Button, ButtonVariant, PageSection, + Switch, } from "@patternfly/react-core"; import { Action, @@ -46,17 +47,17 @@ export default function WorkflowsSection() { ); }; - const toggleEnabled = async (workflowJSON: WorkflowRepresentation) => { - workflowJSON.enabled = !(workflowJSON.enabled ?? true); - + const toggleEnabled = async (workflow: WorkflowRepresentation) => { + const enabled = !(workflow.enabled ?? true); + const workflowToUpdate = { ...workflow, enabled }; try { await adminClient.workflows.update( - { id: workflowJSON.id! }, - workflowJSON, + { id: workflow.id! }, + workflowToUpdate, ); addAlert( - workflowJSON.enabled ? t("workflowEnabled") : t("workflowDisabled"), + workflowToUpdate.enabled ? t("workflowEnabled") : t("workflowDisabled"), AlertVariant.success, ); refresh(); @@ -127,9 +128,14 @@ export default function WorkflowsSection() { { name: "status", displayKey: "status", - cellRenderer: (row: WorkflowRepresentation) => { - return (row.enabled ?? true) ? t("enabled") : t("disabled"); - }, + cellRenderer: (workflow: WorkflowRepresentation) => ( + toggleEnabled(workflow)} + /> + ), }, ]} actions={[ @@ -149,16 +155,6 @@ export default function WorkflowsSection() { ); }, } as Action, - { - title: t("changeStatus"), - tooltipProps: { - content: t("changeStatusTooltip"), - }, - onRowClick: (workflow) => { - setSelectedWorkflow(workflow); - void toggleEnabled(workflow); - }, - } as Action, ]} loader={loader} ariaLabelKey="workflows" diff --git a/js/libs/keycloak-admin-client/src/resources/workflows.ts b/js/libs/keycloak-admin-client/src/resources/workflows.ts index 4179569b14d..ee3c6cbe528 100644 --- a/js/libs/keycloak-admin-client/src/resources/workflows.ts +++ b/js/libs/keycloak-admin-client/src/resources/workflows.ts @@ -40,9 +40,20 @@ export class Workflows extends Resource<{ realm?: string }> { public create = this.makeRequest({ method: "POST", + headers: { "Content-Type": "application/json" }, returnResourceIdInLocationHeader: { field: "id" }, }); + public createAsYaml = this.makeRequest< + { realm: string; yaml: string }, + { id: string } + >({ + method: "POST", + headers: { "Content-Type": "application/yaml", Accept: "application/yaml" }, + returnResourceIdInLocationHeader: { field: "id" }, + payloadKey: "yaml", + }); + public delById = this.makeRequest<{ id: string }, void>({ method: "DELETE", path: "/{id}", diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index b4b968568d8..2f7086f7f61 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -222,6 +222,9 @@ importers: use-react-router-breadcrumbs: specifier: ^4.0.1 version: 4.0.1(react-router-dom@6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + yaml: + specifier: ^2.8.1 + version: 2.8.1 devDependencies: '@axe-core/playwright': specifier: ^4.10.2 @@ -5141,6 +5144,7 @@ snapshots: react-router-dom: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) reactflow: 11.11.4(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) use-react-router-breadcrumbs: 4.0.1(react-router-dom@6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + yaml: 2.8.1 transitivePeerDependencies: - '@babel/runtime' - '@types/react'