Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { workflowLogPersistenceMode } from "../components/log-persistence";

describe("workflow log persistence mode", () => {
it("reads the strategy, the legacy mode, and an expression", () => {
expect(workflowLogPersistenceMode({
logPersistenceConfig: { default: { evaluationMode: "Strategy", strategyType: "Elsa.Workflows.LogPersistence.Strategies.Exclude, Elsa.Workflows.Core" } },
})).toEqual({ kind: "mode", text: "Exclude" });
expect(workflowLogPersistenceMode({
logPersistenceConfig: { default: { evaluationMode: 0, strategyType: "Elsa.Workflows.LogPersistence.Strategies.Inherit, Elsa.Workflows.Core" }, internalState: { strategyType: "Include" } },
})).toEqual({ kind: "mode", text: "Inherit" });
expect(workflowLogPersistenceMode({ logPersistenceMode: { default: "Include" } })).toEqual({ kind: "mode", text: "Include" });
expect(workflowLogPersistenceMode({ logPersistenceMode: { default: 2 } })).toEqual({ kind: "mode", text: "Exclude" });
expect(workflowLogPersistenceMode({
logPersistenceConfig: { default: { evaluationMode: "Strategy", strategyType: "Elsa.Workflows.LogPersistence.Strategies.Include, Elsa.Workflows.Core" } },
logPersistenceMode: { default: "Exclude" },
})).toEqual({ kind: "mode", text: "Include" });
expect(workflowLogPersistenceMode({
logPersistenceConfig: { default: null },
logPersistenceMode: { default: "Exclude" },
})).toEqual({ kind: "mode", text: "Exclude" });
expect(workflowLogPersistenceMode({
logPersistenceConfig: { default: { evaluationMode: "Expression", expression: { type: "JavaScript", value: "LogPersistenceMode.Exclude" } } },
})).toEqual({ kind: "expression", text: "LogPersistenceMode.Exclude" });
expect(workflowLogPersistenceMode({
logPersistenceConfig: { default: { evaluationMode: "Expression", expression: { type: "JavaScript", value: "Include" } } },
})).toEqual({ kind: "expression", text: "Include" });
});

it("treats a blank expression as Inherit instead of the stale strategy", () => {
expect(workflowLogPersistenceMode({
logPersistenceConfig: {
default: {
evaluationMode: "Expression",
strategyType: "Elsa.Workflows.LogPersistence.Strategies.Include, Elsa.Workflows.Core",
expression: { type: "JavaScript", value: " " },
},
},
})).toEqual({ kind: "mode", text: "Inherit" });
});

it("inherits when the workflow does not set a mode and ignores a broken payload", () => {
expect(workflowLogPersistenceMode(undefined)).toEqual({ kind: "mode", text: "Inherit" });
expect(workflowLogPersistenceMode({})).toEqual({ kind: "mode", text: "Inherit" });
expect(workflowLogPersistenceMode({
logPersistenceConfig: { default: "not-an-object" },
logPersistenceMode: { default: "Exclude" },
})).toEqual({ kind: "mode", text: "Inherit" });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CopyButton } from "@elsa-workflows/react-studio-ui/common/copy-button";
import type { WorkflowInstance } from "@elsa-workflows/react-studio-api-client/types/workflow-instances";
import { href } from "@elsa-workflows/react-studio-sdk/routing";
import { WorkflowDefinitionRoutes } from "@elsa-workflows/react-studio-sdk/routes";
import { workflowLogPersistenceMode, type LogPersistenceDisplay } from "./log-persistence";
import { WorkflowInstanceRoutes } from "../routes";

function Mono({ value }: { value: string | undefined }) {
Expand All @@ -23,8 +24,10 @@ function Mono({ value }: { value: string | undefined }) {
);
}

export function InstanceDetails({ instance, definitionName }: { instance: WorkflowInstance; definitionName?: string }) {
export function InstanceDetails({ instance, definitionName, customProperties }: { instance: WorkflowInstance; definitionName?: string; customProperties?: Record<string, unknown> }) {
const _t = useTranslation();
const logPersistence = workflowLogPersistenceMode(customProperties);
const logPersistenceText = localizedLogPersistence(logPersistence, _t);
return (
<KeyValueTable
className="p-3"
Expand Down Expand Up @@ -52,6 +55,7 @@ export function InstanceDetails({ instance, definitionName }: { instance: Workfl
),
},
{ label: _t("Incidents"), value: <span className="tabular-nums">{instance.incidentCount}</span> },
{ label: _t("Log persistence strategy"), value: <span className={logPersistence.kind === "expression" ? "whitespace-pre-wrap" : undefined}>{logPersistenceText}</span> },
{ label: _t("Created"), value: <Timestamp value={instance.createdAt} /> },
{ label: _t("Updated"), value: <Timestamp value={instance.updatedAt} /> },
{ label: _t("Finished"), value: <Timestamp value={instance.finishedAt} /> },
Expand All @@ -67,3 +71,13 @@ export function InstanceDetails({ instance, definitionName }: { instance: Workfl
/>
);
}

function localizedLogPersistence(display: LogPersistenceDisplay, translate: (key: string) => string) {
if (display.kind === "expression") return display.text;
switch (display.text) {
case "Include": return translate("Include");
case "Exclude": return translate("Exclude");
case "Inherit": return translate("Inherit");
default: return display.text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/** Workflow-level log persistence stored on a definition's custom properties. */
export type LogPersistenceDisplay = {
kind: "mode" | "expression";
text: string;
};

export function workflowLogPersistenceMode(customProperties: Record<string, unknown> | undefined): LogPersistenceDisplay {
const configured = readConfiguration(customProperties?.logPersistenceConfig);
if (configured) return configured;
return readLegacy(customProperties?.logPersistenceMode) ?? mode("Inherit");
}

function readConfiguration(value: unknown): LogPersistenceDisplay | undefined {
if (!hasProperty(value, "default")) return undefined;
const rule = property(value, "default");
// A null default falls through to the legacy mode, matching the runtime.
if (rule == null) return undefined;
if (typeof rule !== "object") return mode("Inherit");

const record = rule as Record<string, unknown>;
const evaluation = record.evaluationMode;
if (evaluation === "Expression" || evaluation === 1) {
// Expression mode ignores strategyType. A blank expression inherits.
const text = expressionText(record.expression);
return text ? expression(text) : mode("Inherit");
}
return mode(shortName(record.strategyType) ?? "Inherit");
}

function readLegacy(value: unknown): LogPersistenceDisplay | undefined {
const legacy = property(value, "default");
if (typeof legacy === "string" && legacy.trim()) {
const trimmed = legacy.trim();
return mode(trimmed.includes(".") ? shortName(trimmed) ?? trimmed : trimmed);
}
if (typeof legacy === "number" && Number.isInteger(legacy)) {
const name = ["Inherit", "Include", "Exclude"][legacy];
return name ? mode(name) : undefined;
}
return undefined;
}

function expressionText(expression: unknown): string | undefined {
if (!expression || typeof expression !== "object") return undefined;
const value = (expression as { value?: unknown }).value;
if (typeof value === "string") {
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
if (typeof value === "number" || typeof value === "boolean") return String(value);
return undefined;
}

function shortName(typeName: unknown): string | undefined {
if (typeof typeName !== "string" || !typeName.trim()) return undefined;
const type = typeName.split(",")[0]?.trim() ?? "";
const parts = type.split(".").filter(Boolean);
return parts.at(-1);
}

function hasProperty(value: unknown, name: string): boolean {
return !!value && typeof value === "object" && name in value;
}

function property(value: unknown, name: string): unknown {
if (!hasProperty(value, name)) return undefined;
return (value as Record<string, unknown>)[name];
}

function mode(text: string): LogPersistenceDisplay {
return { kind: "mode", text };
}

function expression(text: string): LogPersistenceDisplay {
return { kind: "expression", text };
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export function WorkflowInstanceViewerPage({ params }: { params: RouteParams })
root={root}
store={store}
definitionName={definition?.name}
customProperties={definition?.customProperties}
journal={journal}
executions={executions}
variables={variables}
Expand Down Expand Up @@ -211,6 +212,7 @@ function ViewerBody({
root,
store,
definitionName,
customProperties,
journal,
executions,
variables,
Expand All @@ -219,6 +221,7 @@ function ViewerBody({
root: Activity;
store: DesignerStore;
definitionName?: string;
customProperties?: Record<string, unknown>;
journal: ReturnType<typeof useInstanceJournal>;
executions: ReturnType<typeof useActivityExecutions>;
variables: ReturnType<typeof useInstanceVariables>;
Expand Down Expand Up @@ -300,7 +303,7 @@ function ViewerBody({
{_t("Input/Output")}</TabsTrigger>
</TabsList>
<TabsContent value="details" className="min-h-0 flex-1 overflow-y-auto">
<InstanceDetails instance={instance} definitionName={definitionName} />
<InstanceDetails instance={instance} definitionName={definitionName} customProperties={customProperties} />
</TabsContent>
<TabsContent value="incidents" className="min-h-0 flex-1 overflow-y-auto">
<IncidentsPanel incidents={incidents} onSelectActivity={selectActivity} />
Expand Down
2 changes: 2 additions & 0 deletions studio/react/packages/sdk/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Everything is connected. The server runs deeper validation when you save or publish.": "Everything is connected. The server runs deeper validation when you save or publish.",
"Everything on the canvas can be done with the keyboard or the mouse.": "Everything on the canvas can be done with the keyboard or the mouse.",
"Exception": "Exception",
"Exclude": "Exclude",
"Executing": "Executing",
"Execution Timeline": "Execution Timeline",
"Executions": "Executions",
Expand Down Expand Up @@ -438,6 +439,7 @@
"Incident strategy": "Incident strategy",
"Incidents": "Incidents",
"Incidents: any": "Incidents: any",
"Include": "Include",
"Info": "Info",
"Inherit": "Inherit",
"Inherit the workflow's strategy or override when this activity commits workflow state.": "Inherit the workflow's strategy or override when this activity commits workflow state.",
Expand Down
2 changes: 2 additions & 0 deletions studio/react/packages/sdk/src/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Everything is connected. The server runs deeper validation when you save or publish.": "Alles is verbonden. De server voert uitgebreidere validatie uit wanneer u opslaat of publiceert.",
"Everything on the canvas can be done with the keyboard or the mouse.": "Alles op het canvas kan met het toetsenbord of de muis worden gedaan.",
"Exception": "Uitzondering",
"Exclude": "Uitsluiten",
"Executing": "Wordt uitgevoerd",
"Execution Timeline": "Uitvoeringstijdlijn",
"Executions": "Executies",
Expand Down Expand Up @@ -438,6 +439,7 @@
"Incident strategy": "Incidentstrategie",
"Incidents": "Incidenten",
"Incidents: any": "Incidenten: alle",
"Include": "Opnemen",
"Info": "Info",
"Inherit": "Overnemen",
"Inherit the workflow's strategy or override when this activity commits workflow state.": "Neem de werkstroomstrategie over of overschrijf deze wanneer deze activiteit de werkstroomtoestand vastlegt.",
Expand Down
Loading
Loading