's
* in-browser mock, or for a server that honours Prefer: code= (rapidoc mock). Open when the
* in-browser mock is on; otherwise folded, since a real API ignores it.
*/
private renderStatusCodes(graph) {
const mocked = this.findRapiDoc()?.mockServer === "true";
const set = (key, value) => (this.statusCodes = { ...this.statusCodes, [key]: value });
const anyList = Object.values(this.statusCodes || {}).some((c) => String(c).includes(","));
const steps = graph.steps.filter((s) => s.operation);
return html` 0}>
Mock Response Status
${mocked ? "the in-browser mock answers with" : "sent as Prefer: code= (rapidoc mock)"}
${this.field(
"Every step",
"empty: success response",
html` set("*", e.target.value)}
/>`,
{ mono: false },
)}
${steps.map((s) =>
this.field(
s.stepId,
`${s.operation.method.toUpperCase()} ${s.operation.path}`,
html` set(s.stepId, e.target.value)}
/>`,
),
)}
${
anyList
? html`
Several codes are used
(this.mockStrategy = e.target.value)}>
in turn, per step
at random
`
: nothing
}
`;
}
private renderInputsForm(graph) {
const set = (name, value) => (this.runInputs = { ...this.runInputs, [name]: value });
const running = this.runState === "running";
return html``;
}
private renderTraceEntry(e, n) {
const pretty = (v) => {
const text = typeof v === "string" ? v : JSON.stringify(v, null, 2);
return text && text.length > 4000 ? `${text.slice(0, 4000)}…` : text;
};
return html`
${n + 1}. ${e.stepId}
${e.attempt > 1 ? html`attempt ${e.attempt} ` : nothing}
${e.request?.method} ${e.request?.url}
${e.response ? `HTTP ${e.response.status}` : "no response"}${e.mocked ? html` mocked ` : nothing}
${e.outcome === "success" ? "✓" : "✗"}
${e.failedCriterion ? html`${e.failedCriterion}` : nothing}
${e.action ? html`→ ${e.action.type}${e.action.stepId ? ` ${e.action.stepId}` : ""} ` : nothing}
${e.request?.body !== undefined ? html`Request body ${pretty(e.request.body)} ` : nothing}
${e.response ? html`Response ${pretty(e.response.body)} ` : nothing}
${e.outputs && Object.keys(e.outputs).length ? html`Outputs ${pretty(e.outputs)} ` : nothing}
`;
}
/** The run panel under the diagram: the inputs form and button, then the log and the result. */
private renderRun(graph) {
if (this.allowRun === "false") return nothing;
return html`
Run
${this.renderInputsForm(graph)}
${
this.trace.length
? html`Log
${this.trace.map((e, n) => this.renderTraceEntry(e, n))} `
: nothing
}
${this.runProblem ? html`Cannot run this workflow: ${this.runProblem}
` : nothing}
${
this.runResult
? this.runResult.isError
? html`${this.runResult.content[0].text} `
: html`Outputs ${this.runResult.content[0].text} `
: nothing
}
`;
}
private renderViewToggle() {
if (this.showViewToggle === "false") return nothing;
const current = this.view === "compact" ? "compact" : "expanded";
const button = (view, label) =>
html` this.setView(view)}>${label} `;
return html`
${button("compact", "Compact")}${button("expanded", "Expanded")}
`;
}
/** Lists only the kinds of lines the diagram drew. */
private renderLegend(kinds: string[]) {
if (!kinds.length) return nothing;
const line = (cls, head, dashed = false) =>
html` `;
const entries = {
seq: html`${line("rf-seq", "seq")}next step`,
data: html`${line("rf-data", "data", true)}value passed to a later step`,
output: html`${line("rf-data rf-to-output", "output", true)}value passed to the outputs`,
success: html`${line("rf-goto rf-success", "success")}goto on success`,
failure: html`${line("rf-goto rf-failure", "failure")}goto on failure`,
};
return html`
${kinds.map((k) => html`${entries[k]} `)}
`;
}
render() {
if (this.loadError) {
return html`Could not load the workflows: ${this.loadError}
`;
}
if (!this.doc) {
return this.loading
? html`Loading workflows…
`
: nothing;
}
const graph = this.graph;
const ids = this.workflowIds;
const view = this.view === "compact" ? "compact" : "expanded";
const diagram = renderFlowDiagram(graph, {
view,
idPrefix: this.idPrefix,
interactive: true,
});
let svg = diagram.svg;
// The selected step and, during a run, the step whose request is in flight, as box classes
const extraClasses = new Map();
if (this.selectedStepId) extraClasses.set(this.selectedStepId, ["rf-selected"]);
if (this.runningStepId) {
extraClasses.set(this.runningStepId, [
...(extraClasses.get(this.runningStepId) || []),
"rf-running",
]);
}
for (const [id, classes] of extraClasses) {
const stepId = escapeXml(id).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
svg = svg.replace(
new RegExp(`class="rf-node rf-step( rf-clickable)?" data-step-id="${stepId}"`),
(_, clickable) =>
`class="rf-node rf-step${clickable || ""} ${classes.join(" ")}" data-step-id="${escapeXml(id)}"`,
);
}
// Only problems a reader can't miss; the graph's authoring warnings are left to tooling
const shown = this.problems.filter(
(p) => p.level === "error" || p.path.startsWith("sourceDescriptions"),
);
return html`
${
graph
? html`
${
graph.description
? html`${graph.description}
`
: nothing
}
${unsafeSVG(svg)}
${this.renderLegend(diagram.legend)}
${this.renderRun(graph)}
`
: html`No workflow to show.
`
}
${
shown.length
? html`
${shown.map(
(p) =>
html`${p.path ? html`${p.path} ` : nothing}${p.message} `,
)}
`
: nothing
}
`;
}
}