Skip to content
Closed
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
@@ -1,6 +1,5 @@
import { beforeEach, describe, expect, it } from "vitest";

import { EDITOR_CONDITIONAL_EXECUTION_ANNOTATION } from "@/utils/annotations";
import { IS_ENABLED_PORT_NAME } from "@/utils/conditionalExecution";

import { IncrementingIdGenerator } from "../../factories/idGenerator";
Expand Down Expand Up @@ -127,11 +126,8 @@ describe("YamlDeserializer", () => {
const consumer = spec.tasks.find((t) => t.name === "Consumer");
const producer = spec.tasks.find((t) => t.name === "Producer");

// Conditional mode: entity value cleared, mode annotation set.
// The binding is the sole record of the reference.
expect(consumer?.isEnabled).toBeUndefined();
expect(
consumer?.annotations.get(EDITOR_CONDITIONAL_EXECUTION_ANNOTATION),
).toBe("true");

// A binding to the reserved port drives the connection.
const binding = spec.bindings.find(
Expand Down Expand Up @@ -160,9 +156,6 @@ describe("YamlDeserializer", () => {
const task = spec.tasks.at(0);

expect(task?.isEnabled).toBe("false");
expect(
task?.annotations.get(EDITOR_CONDITIONAL_EXECUTION_ANNOTATION),
).toBeUndefined();
expect(spec.bindings.length).toBe(0);
});

Expand Down
24 changes: 21 additions & 3 deletions src/models/componentSpec/entities/componentSpec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { computed } from "mobx";
import { idProp, Model, model, modelAction, prop } from "mobx-keystone";

import { IS_ENABLED_PORT_NAME } from "@/utils/conditionalExecution";

import { Annotations } from "../annotations";
import { collectValidationIssues } from "../validation/collectIssues";
import type {
Expand Down Expand Up @@ -105,15 +107,28 @@ export class ComponentSpec extends Model({
this.bindings.push(binding);
}

@modelAction
private clearGateLiteral(binding: Binding | undefined) {
if (!binding || binding.targetPortName !== IS_ENABLED_PORT_NAME) return;
const task = this.tasks.find((t) => t.$id === binding.targetEntityId);
task?.setIsEnabled(undefined);
}

@modelAction
removeBinding(index: number) {
return this.bindings.splice(index, 1)[0];
const removed = this.bindings.splice(index, 1)[0];
this.clearGateLiteral(removed);
return removed;
}

@modelAction
removeBindingBy(predicate: (b: Binding) => boolean): Binding | undefined {
const idx = this.bindings.findIndex(predicate);
if (idx >= 0) return this.bindings.splice(idx, 1)[0];
if (idx >= 0) {
const removed = this.bindings.splice(idx, 1)[0];
this.clearGateLiteral(removed);
return removed;
}
return undefined;
}

Expand All @@ -130,6 +145,9 @@ export class ComponentSpec extends Model({
removed.push(this.bindings.splice(i, 1)[0]);
}
}
for (const binding of removed) {
this.clearGateLiteral(binding);
}
return removed;
}

Expand Down Expand Up @@ -202,7 +220,7 @@ export class ComponentSpec extends Model({
deleteEdgeById(bindingId: string): boolean {
const idx = this.bindings.findIndex((b) => b.$id === bindingId);
if (idx < 0) return false;
this.bindings.splice(idx, 1);
this.clearGateLiteral(this.bindings.splice(idx, 1)[0]);
return true;
}

Expand Down
20 changes: 4 additions & 16 deletions src/models/componentSpec/serialization/yamlDeserializer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
EDITOR_CONDITIONAL_EXECUTION_ANNOTATION,
IS_ENABLED_PORT_NAME,
isConditionalArgument,
} from "@/utils/conditionalExecution";
Expand Down Expand Up @@ -129,21 +128,10 @@ export class YamlDeserializer {
}
}

// A reference-valued `isEnabled` is the "Conditional" mode: it becomes a
// binding to the reserved port (see buildBindings) and the entity keeps
// `isEnabled` empty. Literal values (e.g. "false") stay on the entity.
// A reference-valued `isEnabled` becomes a binding to the reserved port
// (see buildBindings) and the entity keeps `isEnabled` empty. Literal
// values (e.g. "false") stay on the entity.
const conditionalEnabled = isConditionalArgument(taskJson.isEnabled);
if (
conditionalEnabled &&
!annotationItems.some(
(a) => a.key === EDITOR_CONDITIONAL_EXECUTION_ANNOTATION,
)
) {
annotationItems.push({
key: EDITOR_CONDITIONAL_EXECUTION_ANNOTATION,
value: "true",
});
}

const args: Argument[] = [];
if (taskJson.arguments) {
Expand Down Expand Up @@ -209,7 +197,7 @@ export class YamlDeserializer {
tasks,
targetTask.$id,
IS_ENABLED_PORT_NAME,
taskJson.isEnabled as ArgumentType,
taskJson.isEnabled,
);
if (binding) bindings.push(binding);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,24 @@ import {
import { useFlagValue } from "@/components/shared/Settings/useFlags";
import { ColorPicker } from "@/components/ui/color";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Heading, Paragraph } from "@/components/ui/typography";
import type { Task } from "@/models/componentSpec";
import { useAnalytics } from "@/providers/AnalyticsProvider";
import { useSpec } from "@/routes/v2/shared/providers/SpecContext";
import type { AnnotationConfig, Annotations } from "@/types/annotations";
import {
EDITOR_COLLAPSED_ANNOTATION,
EDITOR_CONDITIONAL_EXECUTION_ANNOTATION,
TASK_COLOR_ANNOTATION,
} from "@/utils/annotations";
import { IS_ENABLED_PORT_NAME } from "@/utils/conditionalExecution";
import {
isTaskConditional,
resolveConditionalReference,
} from "@/utils/conditionalExecution";
import { ISO8601_DURATION_ZERO_DAYS } from "@/utils/constants";

import type { EnableTaskMode } from "./taskConfig.actions";
import { useTaskConfigActions } from "./useTaskConfigActions";

interface ConfigurationSectionProps {
Expand All @@ -52,30 +47,30 @@ export const ConfigurationSection = observer(function ConfigurationSection({
setTaskColor,
clearProviderAnnotations,
setCollapsed,
setEnableTaskMode,
setTaskConditional,
setTaskCondition,
} = useTaskConfigActions();
const isSubgraph = task.subgraphSpec !== undefined;

const isConditionalConnected =
spec?.bindings.some(
(b) =>
b.targetEntityId === task.$id &&
b.targetPortName === IS_ENABLED_PORT_NAME,
) ?? false;
const isConditional =
task.annotations.get(EDITOR_CONDITIONAL_EXECUTION_ANNOTATION) === "true" ||
isConditionalConnected;
const enableMode: EnableTaskMode = isConditional
? "conditional"
: task.isEnabled === "false"
? "false"
: "true";
const isConditional = isTaskConditional(task);
const conditionReference = resolveConditionalReference(task, spec);
const conditionValue =
task.isEnabled === "true" || task.isEnabled === "false"
? task.isEnabled
: "";

const handleEnableModeChange = (value: string) => {
const handleConditionalChange = (checked: boolean) => {
if (!spec) return;
const mode = value as EnableTaskMode;
setEnableTaskMode(spec, task, mode);
track("v2.pipeline_editor.task_details.enable_task.change", { mode });
setTaskConditional(spec, task, checked);
track("v2.pipeline_editor.task_details.conditional_task.toggle", {
conditional: checked,
});
};

const handleConditionChange = (value: string) => {
const enabled = value === "true";
setTaskCondition(task, enabled);
track("v2.pipeline_editor.task_details.task_condition.change", { enabled });
};

const cacheDisabled =
Expand Down Expand Up @@ -196,33 +191,42 @@ export const ConfigurationSection = observer(function ConfigurationSection({
<>
<Separator />

<BlockStack gap="1">
<BlockStack gap="3">
<InlineStack align="space-between" gap="2" className="w-full">
<Paragraph size="xs" tone="subdued">
Enable task
Conditional task
</Paragraph>
<Select value={enableMode} onValueChange={handleEnableModeChange}>
<SelectTrigger className="h-6 text-xs px-2 py-0 min-w-25">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true" className="text-xs">
True
</SelectItem>
<SelectItem value="false" className="text-xs">
False
</SelectItem>
<SelectItem value="conditional" className="text-xs">
Conditional
</SelectItem>
</SelectContent>
</Select>
<Switch
checked={isConditional}
onCheckedChange={handleConditionalChange}
/>
</InlineStack>
{isConditional && !isConditionalConnected && (
<Paragraph size="xs" tone="subdued">
Connect a task output or pipeline input to the “Is enabled?”
port on the node.
</Paragraph>

{isConditional && (
<InlineStack align="space-between" gap="2" className="w-full">
<Paragraph size="xs" tone="subdued">
Condition
</Paragraph>
{conditionReference ? (
<code className="text-2xs font-mono bg-muted rounded px-1.5 py-0.5 overflow-x-auto whitespace-nowrap max-w-50">
{JSON.stringify(conditionReference)}
</code>
) : (
<Tabs
value={conditionValue}
onValueChange={handleConditionChange}
>
<TabsList className="h-6">
<TabsTrigger value="true" className="text-xs px-2.5">
True
</TabsTrigger>
<TabsTrigger value="false" className="text-xs px-2.5">
False
</TabsTrigger>
</TabsList>
</Tabs>
)}
</InlineStack>
)}
</BlockStack>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import {
import { IS_ENABLED_PORT_NAME } from "@/utils/conditionalExecution";
import { ISO8601_DURATION_ZERO_DAYS } from "@/utils/constants";

/** The three "Enable task" choices exposed in the Config tab. */
export type EnableTaskMode = "true" | "false" | "conditional";

export function toggleCacheDisable(
undo: UndoGroupable,
task: Task,
Expand Down Expand Up @@ -61,30 +58,35 @@ export function setCollapsed(
});
}

export function setEnableTaskMode(
export function setTaskConditional(
undo: UndoGroupable,
spec: ComponentSpec,
task: Task,
mode: EnableTaskMode,
conditional: boolean,
) {
undo.withGroup("Set enable task", () => {
if (mode === "conditional") {
// The connection is modelled as a binding the user draws to the virtual
// "Is enabled?" port; here we just enter conditional mode so the port
// shows. `isEnabled` is derived from that binding at serialize time.
undo.withGroup("Toggle conditional task", () => {
if (conditional) {
task.annotations.set(EDITOR_CONDITIONAL_EXECUTION_ANNOTATION, "true");
task.setIsEnabled(undefined);
return;
}

// Leaving conditional mode: drop any connection to the reserved port.
spec.removeAllBindingsBy(
(b) =>
b.targetEntityId === task.$id &&
b.targetPortName === IS_ENABLED_PORT_NAME,
);
task.annotations.remove(EDITOR_CONDITIONAL_EXECUTION_ANNOTATION);
task.setIsEnabled(mode === "false" ? "false" : undefined);
task.setIsEnabled(undefined);
});
}

export function setTaskCondition(
undo: UndoGroupable,
task: Task,
enabled: boolean,
) {
undo.withGroup("Set task condition", () => {
task.setIsEnabled(enabled ? "true" : "false");
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import {
clearProviderAnnotations,
saveAnnotation,
setCollapsed,
setEnableTaskMode,
setTaskColor,
setTaskCondition,
setTaskConditional,
toggleCacheDisable,
} from "./taskConfig.actions";

Expand All @@ -17,7 +18,8 @@ export function useTaskConfigActions() {
saveAnnotation: saveAnnotation.bind(null, undo),
setTaskColor: setTaskColor.bind(null, undo),
setCollapsed: setCollapsed.bind(null, undo),
setEnableTaskMode: setEnableTaskMode.bind(null, undo),
setTaskConditional: setTaskConditional.bind(null, undo),
setTaskCondition: setTaskCondition.bind(null, undo),
clearProviderAnnotations: clearProviderAnnotations.bind(null, undo),
};
}
Loading
Loading