feat(FN-2864): merge fusion/fn-2864
This commit is contained in:
9
packages/dashboard/app/components/RoutingTab.css
Normal file
9
packages/dashboard/app/components/RoutingTab.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.routing-tab {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.routing-tab {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
45
packages/dashboard/app/components/RoutingTab.tsx
Normal file
45
packages/dashboard/app/components/RoutingTab.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import "./RoutingTab.css";
|
||||
import type { Settings, Task, TaskDetail } from "@fusion/core";
|
||||
|
||||
interface RoutingTabProps {
|
||||
task: Task | TaskDetail;
|
||||
settings?: Settings;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export function RoutingTab({ task, settings, projectId }: RoutingTabProps) {
|
||||
void projectId;
|
||||
|
||||
return (
|
||||
<div className="detail-section routing-tab">
|
||||
<h4>Node Routing</h4>
|
||||
<dl className="detail-source-grid">
|
||||
<div>
|
||||
<dt>Task Override</dt>
|
||||
<dd>{task.nodeId ?? <span className="detail-source-empty">(none)</span>}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Effective Node</dt>
|
||||
<dd>{(task as Task & { effectiveNodeId?: string }).effectiveNodeId ?? "local execution"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Routing Source</dt>
|
||||
<dd>{(task as Task & { effectiveNodeSource?: string }).effectiveNodeSource ?? "local"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Unavailable Node Policy</dt>
|
||||
<dd>{(settings as (Settings & { unavailableNodePolicy?: string }) | undefined)?.unavailableNodePolicy ?? "block"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Blocking Reason</dt>
|
||||
<dd>
|
||||
{((task as Task & { blockedReason?: string; statusReason?: string }).blockedReason ||
|
||||
(task as Task & { statusReason?: string }).statusReason) ?? (
|
||||
<span className="detail-source-empty">(not blocked)</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { TaskChangesTab } from "./TaskChangesTab";
|
||||
import { TaskForm, type PendingImage } from "./TaskForm";
|
||||
import { useNodes } from "../hooks/useNodes";
|
||||
import { WorkflowResultsTab } from "./WorkflowResultsTab";
|
||||
import { RoutingTab } from "./RoutingTab";
|
||||
import { TaskDocumentsTab } from "./TaskDocumentsTab";
|
||||
import { TaskTokenStatsPanel } from "./TaskTokenStatsPanel";
|
||||
import { PluginSlot } from "./PluginSlot";
|
||||
@@ -164,7 +165,7 @@ function formatBytes(bytes: number): string {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
type TabId = "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "documents" | "stats" | `plugin-${string}`;
|
||||
type TabId = "definition" | "logs" | "changes" | "comments" | "model" | "routing" | "workflow" | "documents" | "stats" | `plugin-${string}`;
|
||||
|
||||
interface TaskDetailModalProps {
|
||||
task: Task | TaskDetail;
|
||||
@@ -1481,6 +1482,12 @@ export function TaskDetailModal({
|
||||
>
|
||||
Model
|
||||
</button>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "routing" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("routing")}
|
||||
>
|
||||
Routing
|
||||
</button>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "workflow" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("workflow")}
|
||||
@@ -1524,6 +1531,8 @@ export function TaskDetailModal({
|
||||
<div className="detail-section">
|
||||
<ModelSelectorTab task={task} addToast={addToast} onTaskUpdated={onTaskUpdated} settings={settings} />
|
||||
</div>
|
||||
) : activeTab === "routing" ? (
|
||||
<RoutingTab task={task} settings={settings} projectId={projectId} />
|
||||
) : activeTab === "logs" ? (
|
||||
<div className={`detail-section${logSubview === "agent-log" ? " detail-section--agent-log" : ""}`}>
|
||||
<div className="log-subview-toggle">
|
||||
@@ -1620,31 +1629,6 @@ export function TaskDetailModal({
|
||||
</div>
|
||||
)}
|
||||
<MergeDetails task={task} />
|
||||
<div className="detail-section">
|
||||
<h4>Node Routing</h4>
|
||||
<dl className="detail-source-grid">
|
||||
<div>
|
||||
<dt>Task Override</dt>
|
||||
<dd>{task.nodeId ?? <span className="detail-source-empty">(none)</span>}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Effective Node</dt>
|
||||
<dd>{(task as Task & { effectiveNodeId?: string }).effectiveNodeId ?? "local execution"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Routing Source</dt>
|
||||
<dd>{(task as Task & { effectiveNodeSource?: string }).effectiveNodeSource ?? "local"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Unavailable Node Policy</dt>
|
||||
<dd>{(settings as Settings & { unavailableNodePolicy?: string } | undefined)?.unavailableNodePolicy ?? "block"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Blocking Reason</dt>
|
||||
<dd>{((task as Task & { blockedReason?: string; statusReason?: string }).blockedReason || (task as Task & { statusReason?: string }).statusReason) ?? <span className="detail-source-empty">(not blocked)</span>}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
{task.sourceIssue && (
|
||||
<div className="detail-section detail-source-section">
|
||||
<h4>Source Issue</h4>
|
||||
|
||||
@@ -1660,23 +1660,24 @@ describe("TaskDetailModal", () => {
|
||||
|
||||
// For an in-progress task (no workflow steps, no merge commit), the
|
||||
// top-level tabs are: Definition, Logs, Changes, Comments, Documents,
|
||||
// Model, Workflow, Stats.
|
||||
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Documents", "Model", "Workflow", "Stats"];
|
||||
// Model, Routing, Workflow, Stats.
|
||||
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Documents", "Model", "Routing", "Workflow", "Stats"];
|
||||
const tabs = screen.getAllByRole("button").filter((b) =>
|
||||
tabTexts.includes(b.textContent || "")
|
||||
);
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs.length).toBe(9);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
expect(tabs[6].textContent).toBe("Routing");
|
||||
expect(tabs[7].textContent).toBe("Workflow");
|
||||
expect(tabs[8].textContent).toBe("Stats");
|
||||
|
||||
// Activity and Agent Log are NOT top-level tabs (they are subviews inside Logs)
|
||||
expect(container.querySelectorAll(".detail-tab").length).toBe(8);
|
||||
expect(container.querySelectorAll(".detail-tab").length).toBe(9);
|
||||
// Workflow tab should always appear even when no workflow steps are configured
|
||||
expect(screen.getByText("Workflow")).toBeInTheDocument();
|
||||
// Commits tab should NOT appear for non-done tasks
|
||||
@@ -2468,7 +2469,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(container.querySelector(".modal-actions .modal-actions-spacer")).toBeTruthy();
|
||||
expect(container.querySelector(".detail-body")).toBeTruthy();
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs.length).toBe(9);
|
||||
expect(tabs[0].classList.contains("detail-tab-active")).toBe(true);
|
||||
expect(Array.from(tabs).slice(1).every((t) => !t.classList.contains("detail-tab-active"))).toBe(true);
|
||||
// Responsive CSS controls sizing — no inline padding/fontSize/borderBottom leaks
|
||||
@@ -3166,18 +3167,19 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// In-progress tasks show exactly 8 tabs:
|
||||
// Definition, Logs, Changes, Comments, Documents, Model, Workflow, Stats
|
||||
// In-progress tasks show exactly 9 tabs:
|
||||
// Definition, Logs, Changes, Comments, Documents, Model, Routing, Workflow, Stats
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs.length).toBe(9);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
expect(tabs[6].textContent).toBe("Routing");
|
||||
expect(tabs[7].textContent).toBe("Workflow");
|
||||
expect(tabs[8].textContent).toBe("Stats");
|
||||
// Commits tab should NOT be present for non-done tasks
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -3195,17 +3197,18 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// In-progress task with workflow steps: 8 tabs (Workflow after Model, Stats last)
|
||||
// In-progress task with workflow steps: 9 tabs (Routing between Model and Workflow, Stats last)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs.length).toBe(9);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
expect(tabs[6].textContent).toBe("Routing");
|
||||
expect(tabs[7].textContent).toBe("Workflow");
|
||||
expect(tabs[8].textContent).toBe("Stats");
|
||||
});
|
||||
|
||||
it("does NOT show Commits tab for done task with mergeDetails.commitSha (changes merged into Changes tab)", () => {
|
||||
@@ -3224,22 +3227,23 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Comments, Documents, Model, Workflow, Stats (8 tabs, no Commits)
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Comments, Documents, Model, Routing, Workflow, Stats (9 tabs, no Commits)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs.length).toBe(9);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
expect(tabs[6].textContent).toBe("Routing");
|
||||
expect(tabs[7].textContent).toBe("Workflow");
|
||||
expect(tabs[8].textContent).toBe("Stats");
|
||||
// Commits tab should NOT be present
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows 8 tabs for done task with workflow steps and commit SHA (Commits merged into Changes)", () => {
|
||||
it("shows 9 tabs for done task with workflow steps and commit SHA (Commits merged into Changes)", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
@@ -3256,17 +3260,18 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Done task with workflow steps and commit SHA: 8 tabs (no Commits)
|
||||
// Done task with workflow steps and commit SHA: 9 tabs (no Commits)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs.length).toBe(9);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
expect(tabs[6].textContent).toBe("Routing");
|
||||
expect(tabs[7].textContent).toBe("Workflow");
|
||||
expect(tabs[8].textContent).toBe("Stats");
|
||||
// Commits tab should NOT be present
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -3285,9 +3290,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
|
||||
expect(triageTabs.length).toBe(7); // Definition, Logs, Comments, Documents, Model, Workflow, Stats
|
||||
expect(triageTabs.length).toBe(8); // Definition, Logs, Comments, Documents, Model, Routing, Workflow, Stats
|
||||
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow", "Stats",
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Routing", "Workflow", "Stats",
|
||||
]);
|
||||
|
||||
const { container: todoContainer } = render(
|
||||
@@ -3303,9 +3308,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
|
||||
expect(todoTabs.length).toBe(7); // Definition, Logs, Comments, Documents, Model, Workflow, Stats
|
||||
expect(todoTabs.length).toBe(8); // Definition, Logs, Comments, Documents, Model, Routing, Workflow, Stats
|
||||
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow", "Stats",
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Routing", "Workflow", "Stats",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user