From 23f21c8232642f0533efb7583df34b69d5df2a01 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Thu, 9 Oct 2025 10:48:44 -0400 Subject: [PATCH] Implement workflow view/copy/enable/disable. Closes #43041 Signed-off-by: Stan Silvert --- .../admin/messages/messages_en.properties | 31 ++- js/apps/admin-ui/src/help-urls.ts | 2 +- .../admin-ui/src/workflows/CreateWorkflow.tsx | 121 ---------- .../src/workflows/WorkflowDetailForm.tsx | 226 ++++++++++++++++++ .../src/workflows/WorkflowsSection.tsx | 27 ++- js/apps/admin-ui/src/workflows/routes.ts | 4 +- .../src/workflows/routes/AddWorkflow.tsx | 21 -- .../src/workflows/routes/WorkflowDetail.tsx | 29 +++ .../src/resources/workflows.ts | 20 ++ 9 files changed, 322 insertions(+), 159 deletions(-) delete mode 100644 js/apps/admin-ui/src/workflows/CreateWorkflow.tsx create mode 100644 js/apps/admin-ui/src/workflows/WorkflowDetailForm.tsx delete mode 100644 js/apps/admin-ui/src/workflows/routes/AddWorkflow.tsx create mode 100644 js/apps/admin-ui/src/workflows/routes/WorkflowDetail.tsx 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 2b418305149..b9940befe66 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 @@ -3580,6 +3580,18 @@ oid4vciNonceLifetimeHelp=The lifetime of the OID4VCI nonce in seconds. preAuthorizedCodeLifespan=Pre-Authorized Code Lifespan preAuthorizedCodeLifespanHelp=The lifespan of the pre-authorized code in seconds. oid4vciFormValidationError=Please ensure the OID4VCI attribute fields are filled with values 30 seconds or greater. +# OID4VCI Credential Configuration +credentialConfigurationId=Credential Configuration ID +credentialConfigurationIdHelp=The unique identifier for this credential configuration. This ID is used in the credential issuer metadata and credential requests. +credentialIdentifier=Credential Identifier +credentialIdentifierHelp=A specific identifier for this credential type. This can be used to distinguish between different variants of the same credential type. +issuerDid=Issuer DID +issuerDidHelp=The Decentralized Identifier (DID) of the credential issuer. This identifies who is issuing the verifiable credentials. +credentialLifetime=Credential Lifetime (seconds) +credentialLifetimeHelp=The lifetime of the credential in seconds. After this time, the credential will expire and become invalid. +supportedFormats=Supported Formats +supportedFormatsHelp=The format of the verifiable credential. Currently supported formats: SD-JWT VC (dc+sd-jwt), JWT VC (jwt_vc_json). +# Workflows workflows=Workflows titleWorkflows=Workflows workflowsExplain=Workflows empower administrators to automate the management of realm resources through time-based or event-based policies. @@ -3595,14 +3607,11 @@ 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}} -# OID4VCI Credential Configuration -credentialConfigurationId=Credential Configuration ID -credentialConfigurationIdHelp=The unique identifier for this credential configuration. This ID is used in the credential issuer metadata and credential requests. -credentialIdentifier=Credential Identifier -credentialIdentifierHelp=A specific identifier for this credential type. This can be used to distinguish between different variants of the same credential type. -issuerDid=Issuer DID -issuerDidHelp=The Decentralized Identifier (DID) of the credential issuer. This identifies who is issuing the verifiable credentials. -credentialLifetime=Credential Lifetime (seconds) -credentialLifetimeHelp=The lifetime of the credential in seconds. After this time, the credential will expire and become invalid. -supportedFormats=Supported Formats -supportedFormatsHelp=The format of the verifiable credential. Currently supported formats: SD-JWT VC (dc+sd-jwt), JWT VC (jwt_vc_json). +viewWorkflow=View 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. +createWorkflowDetails=Create a new workflow by providing its JSON representation. +workflowEnabled=Workflow enabled +workflowDisabled=Workflow disabled diff --git a/js/apps/admin-ui/src/help-urls.ts b/js/apps/admin-ui/src/help-urls.ts index a9247c049c5..161f881fd09 100644 --- a/js/apps/admin-ui/src/help-urls.ts +++ b/js/apps/admin-ui/src/help-urls.ts @@ -20,5 +20,5 @@ export default { guides: `${keycloakHomepageURL}/guides`, community: `${keycloakHomepageURL}/community`, blog: `${keycloakHomepageURL}/blog`, - workflowsUrl: `https://github.com/keycloak/keycloak/issues/39888`, + workflowsUrl: `https://www.keycloak.org/2025/10/workflows-experimental-26-4`, }; diff --git a/js/apps/admin-ui/src/workflows/CreateWorkflow.tsx b/js/apps/admin-ui/src/workflows/CreateWorkflow.tsx deleted file mode 100644 index 06374807256..00000000000 --- a/js/apps/admin-ui/src/workflows/CreateWorkflow.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { - ActionGroup, - Button, - FormGroup, - PageSection, -} from "@patternfly/react-core"; -import { AlertVariant } from "@patternfly/react-core"; -import { useState } from "react"; -import { - Controller, - FormProvider, - SubmitHandler, - useForm, -} from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { Link, useNavigate } from "react-router-dom"; -import { useAdminClient } from "../admin-client"; -import { - HelpItem, - FormSubmitButton, - useAlerts, -} from "@keycloak/keycloak-ui-shared"; -import { useRealm } from "../context/realm-context/RealmContext"; -import { FormAccess } from "../components/form/FormAccess"; -import { toWorkflows } from "./routes/Workflows"; -import CodeEditor from "../components/form/CodeEditor"; - -type AttributeForm = { - workflowJSON?: string; - [key: string]: any; -}; - -export default function CreateWorkflow() { - const { adminClient } = useAdminClient(); - - const { t } = useTranslation(); - const form = useForm({ mode: "onChange" }); - const { control, handleSubmit } = form; - const navigate = useNavigate(); - const { realm } = useRealm(); - const { addAlert, addError } = useAlerts(); - const [workflowJSON, setWorkflowJSON] = useState(""); - - const onSubmit: SubmitHandler = 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 })); - } catch (error) { - addError("workflowCreateError", error); - } - }; - - return ( - - - - - } - fieldId="code" - isRequired - > - ( - setWorkflowJSON(value ?? "")} - language="json" - height={600} - /> - )} - /> - - - - {t("save")} - - - - - - - ); -} diff --git a/js/apps/admin-ui/src/workflows/WorkflowDetailForm.tsx b/js/apps/admin-ui/src/workflows/WorkflowDetailForm.tsx new file mode 100644 index 00000000000..5bd4e136c8f --- /dev/null +++ b/js/apps/admin-ui/src/workflows/WorkflowDetailForm.tsx @@ -0,0 +1,226 @@ +import { + ActionGroup, + Button, + FormGroup, + PageSection, +} from "@patternfly/react-core"; +import { AlertVariant } from "@patternfly/react-core"; +import { useState } from "react"; +import { + Controller, + FormProvider, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { Link, useNavigate } from "react-router-dom"; +import { useAdminClient } from "../admin-client"; +import { + HelpItem, + FormSubmitButton, + useAlerts, + useFetch, +} from "@keycloak/keycloak-ui-shared"; +import { useRealm } from "../context/realm-context/RealmContext"; +import { FormAccess } from "../components/form/FormAccess"; +import { toWorkflows } from "./routes/Workflows"; +import CodeEditor from "../components/form/CodeEditor"; +import { useParams } from "../utils/useParams"; +import { + WorkflowDetailParams, + toWorkflowDetail, +} from "./routes/WorkflowDetail"; +import { ViewHeader } from "../components/view-header/ViewHeader"; + +type AttributeForm = { + workflowJSON?: string; + [key: string]: any; +}; + +export default function WorkflowDetailForm() { + const { adminClient } = useAdminClient(); + + const { t } = useTranslation(); + const form = useForm({ mode: "onChange" }); + const { control, handleSubmit } = form; + const navigate = useNavigate(); + const { realm } = useRealm(); + const { addAlert, addError } = useAlerts(); + const { mode, id } = useParams(); + const [workflowJSON, setWorkflowJSON] = useState(""); + const [enabled, setEnabled] = useState(true); + + useFetch( + async () => { + if (mode === "create") { + return undefined; + } + return adminClient.workflows.findOne({ + id: id!, + }); + }, + (workflow) => { + if (!workflow) { + return; + } + + if (mode === "copy") { + delete workflow.id; + workflow.name = `${workflow.name} -- ${t("copy")}`; + } + + setWorkflowJSON(JSON.stringify(workflow, null, 2)); + setEnabled(workflow?.enabled ?? true); + + form.reset({ + workflowJSON: JSON.stringify(workflow, null, 2), + }); + }, + [mode, id], + ); + + const onSubmit: SubmitHandler = async () => { + if (mode === "view") { + navigate(toWorkflowDetail({ realm, mode: "copy", id: id! })); + return; + } + + 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 })); + } catch (error) { + addError("workflowCreateError", error); + } + }; + + const toggleEnabled = async () => { + const json = JSON.parse(workflowJSON); + json.enabled = !enabled; + + try { + const payload = { + realm, + ...json, + }; + await adminClient.workflows.update({ id: json.id }, payload); + + setWorkflowJSON(JSON.stringify(json, null, 2)); + setEnabled(!enabled); + form.reset({ + workflowJSON: JSON.stringify(json, null, 2), + }); + addAlert( + enabled ? t("workflowDisabled") : t("workflowEnabled"), + AlertVariant.success, + ); + } catch (error) { + addError("workflowCreateError", error); + } + }; + + const titlekeyMap: Record = { + copy: "copyWorkflow", + create: "createWorkflow", + view: "viewWorkflow", + }; + + const subkeyMap: Record = { + copy: "copyWorkflowDetails", + create: "createWorkflowDetails", + view: "viewWorkflowDetails", + }; + + return ( + <> + + + + + + + } + fieldId="code" + isRequired + > + ( + setWorkflowJSON(value ?? "")} + language="json" + height={600} + /> + )} + /> + + + {mode !== "view" && ( + + {t("save")} + + )} + {mode === "view" && ( + + {t("copy")} + + )} + + + + + + + ); +} diff --git a/js/apps/admin-ui/src/workflows/WorkflowsSection.tsx b/js/apps/admin-ui/src/workflows/WorkflowsSection.tsx index 074a6d43eba..ec623740585 100644 --- a/js/apps/admin-ui/src/workflows/WorkflowsSection.tsx +++ b/js/apps/admin-ui/src/workflows/WorkflowsSection.tsx @@ -19,8 +19,8 @@ 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 { toAddWorkflow } from "./routes/AddWorkflow"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; +import { toWorkflowDetail } from "./routes/WorkflowDetail"; export default function WorkflowsSection() { const { adminClient } = useAdminClient(); @@ -85,7 +85,10 @@ export default function WorkflowsSection() {