Workflows now use YAML instead of JSON.

Closes #43665

Signed-off-by: Stan Silvert <ssilvert@redhat.com>
This commit is contained in:
Stan Silvert
2025-11-12 08:13:00 -05:00
committed by Pedro Igor
parent b2317dabdc
commit 33b479fa3b
6 changed files with 52 additions and 37 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -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<AttributeForm>({
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<AttributeForm> = 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<AttributeForm> = 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}
>
<FormGroup
label={t("workflowJSON")}
label={t("workflowYAML")}
labelIcon={
<HelpItem
helpText={t("workflowJsonHelp")}
helpText={t("workflowYAMLHelp")}
fieldLabelId="code"
/>
}
@@ -147,15 +150,15 @@ export default function WorkflowDetailForm() {
isRequired
>
<Controller
name="workflowJSON"
name="workflowYAML"
control={control}
render={({ field }) => (
<CodeEditor
id="workflowJSON"
data-testid="workflowJSON"
id="workflowYAML"
data-testid="workflowYAML"
value={field.value}
onChange={field.onChange}
language="json"
language="yaml"
height={600}
/>
)}

View File

@@ -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) => (
<Switch
label={t("enabled")}
labelOff={t("disabled")}
isChecked={workflow.enabled ?? true}
onChange={() => toggleEnabled(workflow)}
/>
),
},
]}
actions={[
@@ -149,16 +155,6 @@ 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

@@ -40,9 +40,20 @@ export class Workflows extends Resource<{ realm?: string }> {
public create = this.makeRequest<WorkflowRepresentation, { id: string }>({
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}",

4
js/pnpm-lock.yaml generated
View File

@@ -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'