Skip to content

Commit 16d41c2

Browse files
feat(orchestrator): Run as Event for Kafka-configured workflows (#2749)
* feat(orchestrator): add Run as Event for Kafka-configured workflows Show Run as Event when orchestrator.kafka is present; submit execute with isEvent in input data; on kafkaEvent response navigate to workflow runs with an informational alert. Extend form-react with optional second submit action; document KafkaJS-style kafka config in orchestrator-common. Made-with: Cursor * chore(orchestrator-form-api): update API report warning line reference Made-with: Cursor
1 parent 0979dc1 commit 16d41c2

22 files changed

Lines changed: 381 additions & 102 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator': patch
3+
'@red-hat-developer-hub/backstage-plugin-orchestrator-common': patch
4+
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-api': patch
5+
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-react': patch
6+
---
7+
8+
Add Run as Event when `orchestrator.kafka` is configured: send `isEvent` with execute input, redirect to workflow runs with a notice when the response id is `kafkaEvent`.

workspaces/orchestrator/plugins/orchestrator-common/config.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,30 @@ export interface Config {
7878
*/
7979
url: string;
8080
};
81+
/**
82+
* Kafka configuration for event-triggered workflows (KafkaJS-style).
83+
* When present, the UI can show actions such as "Run as Event".
84+
* @visibility frontend
85+
*/
86+
kafka?: {
87+
/**
88+
* Logical identifier of the Kafka client application.
89+
* @see https://kafka.js.org/docs/configuration#client-id
90+
* @visibility frontend
91+
*/
92+
clientId: string;
93+
/**
94+
* Kafka broker addresses (`host:port`).
95+
* @visibility frontend
96+
*/
97+
brokers: string[];
98+
/**
99+
* Log level for orchestrator Kafka services. Defaults to INFO (4); e.g. use 5 for DEBUG.
100+
* @see https://kafka.js.org/docs/configuration#logging
101+
* @visibility frontend
102+
*/
103+
logLevel?: number;
104+
};
81105
/**
82106
* Configuration for the workflow log provider.
83107
* If configured, the "View Logs" button will be shown in the workflow instance results.

workspaces/orchestrator/plugins/orchestrator-form-api/report.api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export type ReviewComponentProps = {
7373
data: JsonObject;
7474
handleBack: () => void;
7575
handleExecute: () => void;
76+
executeLabel?: string;
77+
handleExecuteAsEvent?: () => void;
78+
executeAsEventLabel?: string;
7679
};
7780

7881
// @public
@@ -87,7 +90,7 @@ export const useOrchestratorFormApiOrDefault: () => OrchestratorFormApi;
8790

8891
// Warnings were encountered during analysis:
8992
//
90-
// src/api.d.ts:125:22 - (ae-undocumented) Missing documentation for "useOrchestratorFormApiOrDefault".
93+
// src/api.d.ts:131:22 - (ae-undocumented) Missing documentation for "useOrchestratorFormApiOrDefault".
9194

9295
// (No @packageDocumentation comment for this package)
9396
```

workspaces/orchestrator/plugins/orchestrator-form-api/src/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ export type ReviewComponentProps = {
133133
handleBack: () => void;
134134
/** Callback to execute the workflow */
135135
handleExecute: () => void;
136+
/** Label for the primary execute button (default review UI falls back to translated "Run") */
137+
executeLabel?: string;
138+
/** Optional second action to execute as an event-style (e.g. Kafka) workflow */
139+
handleExecuteAsEvent?: () => void;
140+
/** Label for the event execute control when `handleExecuteAsEvent` is set */
141+
executeAsEventLabel?: string;
136142
};
137143

138144
/**

workspaces/orchestrator/plugins/orchestrator-form-react/report.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ export type OrchestratorFormProps = {
5858
setAuthTokenDescriptors: OrchestratorFormContextProps['setAuthTokenDescriptors'];
5959
isExecuting: boolean;
6060
handleExecute: (parameters: JsonObject) => Promise<void>;
61+
handleExecuteAsEvent?: (parameters: JsonObject) => Promise<void>;
6162
initialFormData: JsonObject;
6263
t: TranslationFunction;
64+
executeLabel?: string;
65+
executeAsEventLabel?: string;
6366
};
6467

6568
// @public

workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorForm.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,11 @@ export type OrchestratorFormProps = {
5959
setAuthTokenDescriptors: OrchestratorFormContextProps['setAuthTokenDescriptors'];
6060
isExecuting: boolean;
6161
handleExecute: (parameters: JsonObject) => Promise<void>;
62+
handleExecuteAsEvent?: (parameters: JsonObject) => Promise<void>;
6263
initialFormData: JsonObject;
6364
t: TranslationFunction;
65+
executeLabel?: string;
66+
executeAsEventLabel?: string;
6467
};
6568

6669
/**
@@ -111,6 +114,9 @@ type ReviewStepHostProps = {
111114
schema: JSONSchema7;
112115
data: JsonObject;
113116
handleExecute: () => void;
117+
executeLabel?: string;
118+
handleExecuteAsEvent?: () => void;
119+
executeAsEventLabel?: string;
114120
};
115121

116122
/** Supplies `handleBack` from stepper context to the default or custom review component. */
@@ -120,6 +126,9 @@ const ReviewStepHost = ({
120126
schema,
121127
data,
122128
handleExecute,
129+
executeLabel,
130+
handleExecuteAsEvent,
131+
executeAsEventLabel,
123132
}: ReviewStepHostProps) => {
124133
const { handleBack } = useStepperContext();
125134
return (
@@ -129,6 +138,9 @@ const ReviewStepHost = ({
129138
data={data}
130139
handleBack={handleBack}
131140
handleExecute={handleExecute}
141+
executeLabel={executeLabel}
142+
handleExecuteAsEvent={handleExecuteAsEvent}
143+
executeAsEventLabel={executeAsEventLabel}
132144
/>
133145
);
134146
};
@@ -141,10 +153,13 @@ const OrchestratorForm = ({
141153
schema: rawSchema,
142154
updateSchema,
143155
handleExecute,
156+
handleExecuteAsEvent,
144157
isExecuting,
145158
initialFormData,
146159
setAuthTokenDescriptors,
147160
t,
161+
executeLabel,
162+
executeAsEventLabel,
148163
}: OrchestratorFormProps) => {
149164
// Extract static defaults from fetch:response:default in schema and merge with initialFormData
150165
// This ensures defaults are available before widgets render
@@ -192,6 +207,11 @@ const OrchestratorForm = ({
192207
// Use pruned data for execution to avoid submitting stale properties
193208
handleExecute(workflowInputData);
194209
}, [workflowInputData, handleExecute]);
210+
const _handleExecuteAsEvent = useCallback(() => {
211+
if (handleExecuteAsEvent) {
212+
handleExecuteAsEvent(workflowInputData);
213+
}
214+
}, [workflowInputData, handleExecuteAsEvent]);
195215

196216
const onSubmit = useCallback(
197217
(_formData: JsonObject) => {
@@ -219,6 +239,11 @@ const OrchestratorForm = ({
219239
schema={schema}
220240
busy={isExecuting}
221241
handleExecute={_handleExecute}
242+
executeLabel={executeLabel}
243+
handleExecuteAsEvent={
244+
handleExecuteAsEvent ? _handleExecuteAsEvent : undefined
245+
}
246+
executeAsEventLabel={executeAsEventLabel}
222247
/>
223248
);
224249
}, [
@@ -227,6 +252,10 @@ const OrchestratorForm = ({
227252
schema,
228253
isExecuting,
229254
_handleExecute,
255+
executeLabel,
256+
handleExecuteAsEvent,
257+
_handleExecuteAsEvent,
258+
executeAsEventLabel,
230259
]);
231260

232261
return (

workspaces/orchestrator/plugins/orchestrator-form-react/src/components/ReviewStep.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ const useStyles = makeStyles()(theme => ({
3737
backButton: {
3838
marginRight: theme.spacing(1),
3939
},
40+
executeAsEventButton: {
41+
marginLeft: theme.spacing(2),
42+
},
4043
footer: {
4144
display: 'flex',
4245
flexDirection: 'row',
@@ -65,6 +68,9 @@ const ReviewStep = ({
6568
data,
6669
handleBack,
6770
handleExecute,
71+
executeLabel,
72+
handleExecuteAsEvent,
73+
executeAsEventLabel,
6874
}: ReviewComponentProps) => {
6975
const { t } = useTranslation();
7076

@@ -104,8 +110,21 @@ const ReviewStep = ({
104110
submitting={busy}
105111
focusOnMount
106112
>
107-
{t('common.run')}
113+
{executeLabel ?? t('common.run')}
108114
</SubmitButton>
115+
{handleExecuteAsEvent && executeAsEventLabel && (
116+
<Button
117+
variant="contained"
118+
color="primary"
119+
type="button"
120+
onClick={handleExecuteAsEvent}
121+
disabled={busy}
122+
className={classes.executeAsEventButton}
123+
disableRipple
124+
>
125+
{executeAsEventLabel}
126+
</Button>
127+
)}
109128
</div>
110129
</Paper>
111130
</Content>

workspaces/orchestrator/plugins/orchestrator/report-alpha.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export const orchestratorTranslationRef: TranslationRef<
256256
readonly 'table.headers.lastRunStatus': string;
257257
readonly 'table.headers.workflowName': string;
258258
readonly 'table.actions.run': string;
259+
readonly 'table.actions.runAsEvent': string;
259260
readonly 'table.actions.viewRuns': string;
260261
readonly 'table.actions.viewInputSchema': string;
261262
readonly 'table.filters.status': string;
@@ -313,6 +314,7 @@ export const orchestratorTranslationRef: TranslationRef<
313314
readonly 'run.status.workflowIsRunning': string;
314315
readonly 'run.status.noAdditionalInfo': string;
315316
readonly 'run.status.resultsWillBeDisplayedHereOnceTheRunIsComplete': string;
317+
readonly 'run.messages.eventTriggered': string;
316318
readonly 'run.pageTitle': string;
317319
readonly 'run.variables': string;
318320
readonly 'run.inputs': string;
@@ -349,6 +351,7 @@ export const orchestratorTranslationRef: TranslationRef<
349351
readonly 'workflow.messages.userNotAuthorizedExecute': string;
350352
readonly 'workflow.messages.workflowDown': string;
351353
readonly 'workflow.buttons.run': string;
354+
readonly 'workflow.buttons.runAsEvent': string;
352355
readonly 'workflow.buttons.running': string;
353356
readonly 'workflow.buttons.runWorkflow': string;
354357
readonly 'workflow.buttons.runAgain': string;

workspaces/orchestrator/plugins/orchestrator/report.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const orchestratorTranslationRef: TranslationRef<
5151
readonly 'table.headers.lastRunStatus': string;
5252
readonly 'table.headers.workflowName': string;
5353
readonly 'table.actions.run': string;
54+
readonly 'table.actions.runAsEvent': string;
5455
readonly 'table.actions.viewRuns': string;
5556
readonly 'table.actions.viewInputSchema': string;
5657
readonly 'table.filters.status': string;
@@ -108,6 +109,7 @@ export const orchestratorTranslationRef: TranslationRef<
108109
readonly 'run.status.workflowIsRunning': string;
109110
readonly 'run.status.noAdditionalInfo': string;
110111
readonly 'run.status.resultsWillBeDisplayedHereOnceTheRunIsComplete': string;
112+
readonly 'run.messages.eventTriggered': string;
111113
readonly 'run.pageTitle': string;
112114
readonly 'run.variables': string;
113115
readonly 'run.inputs': string;
@@ -144,6 +146,7 @@ export const orchestratorTranslationRef: TranslationRef<
144146
readonly 'workflow.messages.userNotAuthorizedExecute': string;
145147
readonly 'workflow.messages.workflowDown': string;
146148
readonly 'workflow.buttons.run': string;
149+
readonly 'workflow.buttons.runAsEvent': string;
147150
readonly 'workflow.buttons.running': string;
148151
readonly 'workflow.buttons.runWorkflow': string;
149152
readonly 'workflow.buttons.runAgain': string;

workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.tsx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,18 @@ import {
4545
} from '@red-hat-developer-hub/backstage-plugin-orchestrator-form-react';
4646

4747
import { orchestratorApiRef } from '../../api';
48+
import { useKafkaEnabled } from '../../hooks/useKafkaEnabled';
4849
import { useOrchestratorAuth } from '../../hooks/useOrchestratorAuth';
4950
import { useTranslation } from '../../hooks/useTranslation';
5051
import {
5152
entityInstanceRouteRef,
53+
entityWorkflowRouteRef,
5254
executeWorkflowRouteRef,
5355
workflowInstanceRouteRef,
56+
workflowRunsRouteRef,
5457
} from '../../routes';
5558
import { getErrorObject } from '../../utils/ErrorUtils';
59+
import { buildUrl } from '../../utils/UrlUtils';
5660
import { BaseOrchestratorPage } from '../ui/BaseOrchestratorPage';
5761
import MissingSchemaNotice from './MissingSchemaNotice';
5862
import { mergeQueryParamsIntoFormData } from './queryParamsToFormData';
@@ -68,10 +72,13 @@ export const ExecuteWorkflowPage = () => {
6872
const [isExecuting, setIsExecuting] = useState(false);
6973
const [updateError, setUpdateError] = useState<Error>();
7074
const [instanceId] = useQueryParamState<string>(QUERY_PARAM_INSTANCE_ID);
75+
const kafkaEnabled = useKafkaEnabled();
7176

7277
const navigate = useNavigate();
7378
const instanceLink = useRouteRef(workflowInstanceRouteRef);
7479
const entityInstanceLink = useRouteRef(entityInstanceRouteRef);
80+
const entityWorkflowLink = useRouteRef(entityWorkflowRouteRef);
81+
const workflowRunsLink = useRouteRef(workflowRunsRouteRef);
7582
const {
7683
value,
7784
loading,
@@ -116,18 +123,28 @@ export const ExecuteWorkflowPage = () => {
116123

117124
const [kind, namespace, name] = targetEntity?.split(/[:\/]/) || [];
118125

119-
const handleExecute = useCallback(
120-
async (parameters: JsonObject) => {
126+
const executeWorkflow = useCallback(
127+
async (parameters: JsonObject, isEvent: boolean) => {
121128
setUpdateError(undefined);
122129
try {
123130
setIsExecuting(true);
124131
const authTokens = await authenticate(authTokenDescriptors);
132+
const executeParameters = isEvent
133+
? { ...parameters, isEvent: true }
134+
: parameters;
125135
const response = await orchestratorApi.executeWorkflow({
126136
workflowId,
127-
parameters,
137+
parameters: executeParameters,
128138
authTokens,
129139
targetEntity: targetEntity ?? undefined,
130140
});
141+
if (response.data.id === 'kafkaEvent') {
142+
const redirectUrl = targetEntity
143+
? entityWorkflowLink({ namespace, kind, name, workflowId })
144+
: workflowRunsLink({ workflowId });
145+
navigate(buildUrl(redirectUrl, { eventTriggered: 'true' }));
146+
return;
147+
}
131148
const url = targetEntity
132149
? entityInstanceLink({
133150
namespace,
@@ -153,11 +170,25 @@ export const ExecuteWorkflowPage = () => {
153170
authenticate,
154171
targetEntity,
155172
entityInstanceLink,
173+
entityWorkflowLink,
174+
workflowRunsLink,
156175
kind,
157176
namespace,
158177
name,
159178
],
160179
);
180+
const handleExecute = useCallback(
181+
async (parameters: JsonObject) => {
182+
await executeWorkflow(parameters, false);
183+
},
184+
[executeWorkflow],
185+
);
186+
const handleExecuteAsEvent = useCallback(
187+
async (parameters: JsonObject) => {
188+
await executeWorkflow(parameters, true);
189+
},
190+
[executeWorkflow],
191+
);
161192

162193
const error = responseError || workflowNameError;
163194
let pageContent;
@@ -185,14 +216,27 @@ export const ExecuteWorkflowPage = () => {
185216
schema={schema}
186217
updateSchema={updateSchema}
187218
handleExecute={handleExecute}
219+
handleExecuteAsEvent={
220+
kafkaEnabled ? handleExecuteAsEvent : undefined
221+
}
188222
isExecuting={isExecuting}
189223
initialFormData={initialFormData}
190224
setAuthTokenDescriptors={setAuthTokenDescriptors}
191225
t={t as unknown as TranslationFunction}
226+
executeLabel={t('common.run')}
227+
executeAsEventLabel={
228+
kafkaEnabled ? t('workflow.buttons.runAsEvent') : undefined
229+
}
192230
/>
193231
) : (
194232
<MissingSchemaNotice
195233
handleExecute={handleExecute}
234+
handleExecuteAsEvent={
235+
kafkaEnabled ? handleExecuteAsEvent : undefined
236+
}
237+
executeAsEventLabel={
238+
kafkaEnabled ? t('workflow.buttons.runAsEvent') : undefined
239+
}
196240
isExecuting={isExecuting}
197241
/>
198242
)}

0 commit comments

Comments
 (0)