Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
088b879
Refine pipeline detail layout and execution navigation
luxass Mar 21, 2026
8196a25
Refine pipeline detail and inspect page layouts
luxass Mar 21, 2026
7c11fd2
refactor(pipeline-server): remove global overview and redirect home t…
luxass Mar 21, 2026
0213049
feat(pipeline-server): redesign source page with two-column layout
luxass Mar 21, 2026
4369162
refactor(pipeline-server): simplify sidebar, source switcher, and pip…
luxass Mar 21, 2026
d82e5b0
refactor(pipeline-server): improve layout and structure of pipeline f…
luxass Mar 21, 2026
8fb1b87
refactor(pipeline-server): rename home/ components to overview/
luxass Mar 21, 2026
507c7f2
feat(pipeline-server): add pipeline sidebar navigation
luxass Mar 21, 2026
3f7a245
feat(pipeline-server): add inspect routes for pipelines
luxass Mar 21, 2026
dea4d14
feat(pipeline-server): enhance pipeline inspection routes
luxass Mar 21, 2026
9870c61
feat(pipeline-server): add detailed output and transform routes
luxass Mar 21, 2026
8c703b9
Extract inspect sidebar and tighten tab layout
luxass Mar 21, 2026
bf89c79
refactor(pipeline-server): extract sidebar list components into separ…
luxass Mar 21, 2026
b15c305
refactor(pipeline-server): improve output key handling and layout
luxass Mar 21, 2026
fc77587
refactor(pipeline-server): unify graph data structures and consolidat…
luxass Mar 22, 2026
40c839b
refactor(pipeline-server): use single card layout for all inspect det…
luxass Mar 22, 2026
43260a5
refactor(pipeline-server): update button styles in output and transfo…
luxass Mar 22, 2026
f313ae0
refactor(pipeline-server): enforce non-null assertions for execution …
luxass Mar 22, 2026
1956024
test(pipeline-server): rewrite inspect tests and refactor test utilities
luxass Mar 24, 2026
ca9cd49
fix(pipeline-server): fix all lint errors and update remaining browse…
luxass Mar 24, 2026
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,8 +1,13 @@
import { sourcesQueryOptions } from "#queries/sources";
import type { PipelineDetails } from "#queries/pipeline";
import { useExecute } from "#hooks/use-execute";
import { usePipelineVersions } from "#hooks/use-pipeline-versions";
import { pipelineQueryOptions } from "#queries/pipeline";
import { sourceQueryOptions } from "#queries/source";
import { useSuspenseQuery } from "@tanstack/react-query";
import { Link, useParams } from "@tanstack/react-router";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { ThemeToggle, UcdLogo } from "@ucdjs-internal/shared-ui/components";
import { Badge } from "@ucdjs-internal/shared-ui/ui/badge";
import { Button } from "@ucdjs-internal/shared-ui/ui/button";
import {
Sidebar,
SidebarContent,
Expand All @@ -13,10 +18,10 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
} from "@ucdjs-internal/shared-ui/ui/sidebar";
import { BookOpen, ChevronRight, ExternalLink, Hash, Tag } from "lucide-react";
import { ArrowLeft, BookOpen, ClipboardList, ExternalLink, Eye, FileCode2, GitBranch, Hash, Layers, Workflow as PipelineIcon, Play, Tag } from "lucide-react";
import * as React from "react";
import { useCallback } from "react";
import { SourceFileList } from "./source-file-list";
import { SourceSwitcher } from "./source-switcher";

Expand All @@ -25,11 +30,16 @@ export interface PipelineSidebarProps {
version?: string;
}

const PIPELINE_NAV_ITEMS = [
{ id: "overview", label: "Overview", to: "", icon: Eye },
{ id: "inspect", label: "Inspect", to: "/inspect", icon: ClipboardList },
{ id: "executions", label: "Executions", to: "/executions", icon: Layers },
] as const;

export function PipelineSidebar({
workspaceId,
version,
}: PipelineSidebarProps) {
const { data: sourcesData } = useSuspenseQuery(sourcesQueryOptions());
const params = useParams({ strict: false });

const currentSourceId = "sourceId" in params && typeof params.sourceId === "string"
Expand All @@ -48,7 +58,7 @@ export function PipelineSidebar({
setExpanded((prev) => ({ ...prev, [key]: !isOpen }));
}, []);

const sources = sourcesData ?? [];
const showPipelineNav = currentSourceId && currentFileId && currentPipelineId;

return (
<Sidebar data-testid="pipeline-sidebar">
Expand Down Expand Up @@ -97,74 +107,32 @@ export function PipelineSidebar({
</div>
</SidebarHeader>

<div className="px-3 pb-1.5" data-testid="pipeline-sidebar-source-switcher">
<SourceSwitcher />
</div>
{!showPipelineNav && (
<div className="px-3 pb-1.5" data-testid="pipeline-sidebar-source-switcher">
<SourceSwitcher />
</div>
)}

<SidebarContent data-testid="pipeline-sidebar-content">
{currentSourceId
{showPipelineNav
? (
<SidebarGroup className="px-2 py-1" data-testid={`pipeline-sidebar-current-source:${currentSourceId}`}>
<SourceFileList
sourceId={currentSourceId}
currentFileId={currentFileId}
currentPipelineId={currentPipelineId}
expanded={expanded}
toggle={toggle}
/>
</SidebarGroup>
<PipelineNavSection
sourceId={currentSourceId}
fileId={currentFileId}
pipelineId={currentPipelineId}
/>
)
: (
sources.map((source) => {
const sourceKey = `source:${source.id}`;
const isOpen = expanded[sourceKey] ?? false;
const isActive = currentSourceId === source.id;

return (
<SidebarGroup
key={source.id}
className="px-2 py-1"
data-testid={`pipeline-sidebar-source-group:${source.id}`}
>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
className="h-9 gap-2 px-2.5"
data-testid={`pipeline-sidebar-source-toggle:${source.id}`}
onClick={() => toggle(sourceKey, isOpen)}
>
<ChevronRight className={`size-3.5 shrink-0 transition-transform duration-150 ${isOpen ? "rotate-90" : ""}`} />
<SidebarGroupLabel className="h-auto min-w-0 flex-1 px-0 text-[11px] font-semibold uppercase tracking-[0.08em] text-sidebar-foreground/75">
<Link
to="/s/$sourceId"
params={{ sourceId: source.id }}
data-testid={`pipeline-sidebar-source-link:${source.id}`}
className={isActive
? "block truncate text-sidebar-foreground"
: "block truncate hover:text-sidebar-foreground"}
onClick={(e) => e.stopPropagation()}
>
{source.label}
</Link>
</SidebarGroupLabel>
</SidebarMenuButton>
{isOpen && (
<SidebarMenuSub>
<SourceFileList
sourceId={source.id}
currentFileId={currentFileId}
currentPipelineId={currentPipelineId}
expanded={expanded}
toggle={toggle}
/>
</SidebarMenuSub>
)}
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
})
)}
: currentSourceId && (
<SidebarGroup className="px-2 py-1" data-testid={`pipeline-sidebar-current-source:${currentSourceId}`}>
<SourceFileList
sourceId={currentSourceId}
currentFileId={currentFileId}
currentPipelineId={currentPipelineId}
expanded={expanded}
toggle={toggle}
/>
</SidebarGroup>
)}
</SidebarContent>

<SidebarFooter data-testid="pipeline-sidebar-footer">
Expand Down Expand Up @@ -202,3 +170,174 @@ export function PipelineSidebar({
</Sidebar>
);
}

interface PipelineNavSectionProps {
sourceId: string;
fileId: string;
pipelineId: string;
}

function PipelineNavSection({ sourceId, fileId, pipelineId }: PipelineNavSectionProps) {
const { data: pipelineResponse } = useSuspenseQuery(pipelineQueryOptions({ sourceId, fileId, pipelineId }));
const { data: source } = useSuspenseQuery(sourceQueryOptions({ sourceId }));
const pipeline = pipelineResponse.pipeline;
const file = source.files.find((f) => f.id === fileId);

return (
<div className="flex flex-col gap-1 px-2" data-testid="pipeline-sidebar-nav">
<BackToSourceLink sourceId={sourceId} />
<PipelineIdentity pipeline={pipeline} filePath={file?.path} />
<PipelineNavLinks sourceId={sourceId} fileId={fileId} pipelineId={pipelineId} />
<PipelineInfoSection pipeline={pipeline} />
<PipelineExecuteButton sourceId={sourceId} fileId={fileId} pipelineId={pipelineId} pipeline={pipeline} />
</div>
);
}

function BackToSourceLink({ sourceId }: { sourceId: string }) {
return (
<Link
to="/s/$sourceId"
params={{ sourceId }}
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors py-2 px-1"
data-testid="pipeline-sidebar-back-link"
>
<ArrowLeft className="h-3.5 w-3.5" />
<span>Back to source</span>
</Link>
);
}

function PipelineIdentity({ pipeline, filePath }: { pipeline: PipelineDetails; filePath?: string }) {
return (
<div className="px-1 py-2 space-y-1" data-testid="pipeline-sidebar-identity">
<h3 className="font-semibold text-sm truncate">{pipeline.name || pipeline.id}</h3>
{filePath && (
<div className="inline-flex items-center gap-1.5 text-[11px] text-muted-foreground min-w-0">
<FileCode2 className="h-3 w-3 shrink-0" />
<span className="truncate">{filePath}</span>
</div>
)}
{pipeline.description && (
<p className="text-xs text-muted-foreground leading-relaxed">{pipeline.description}</p>
)}
</div>
);
}

function PipelineNavLinks({
sourceId,
fileId,
pipelineId,
}: {
sourceId: string;
fileId: string;
pipelineId: string;
}) {
return (
<SidebarMenu>
{PIPELINE_NAV_ITEMS.map((item) => (
<SidebarMenuItem key={item.id}>
<SidebarMenuButton
render={(
<Link
to={`/s/$sourceId/$sourceFileId/$pipelineId${item.to}`}
params={{ sourceId, sourceFileId: fileId, pipelineId }}
activeProps={{
className: "bg-muted text-foreground font-medium",
}}
activeOptions={{
exact: item.id === "overview",
}}
data-testid={`pipeline-sidebar-nav-${item.id}`}
>
<item.icon className="size-4" />
<span>{item.label}</span>
</Link>
)}
/>
</SidebarMenuItem>
))}
</SidebarMenu>
);
}

function PipelineInfoSection({ pipeline }: { pipeline: PipelineDetails }) {
return (
<div className="px-1 py-3 space-y-2 border-t border-border/60" data-testid="pipeline-sidebar-info">
<SidebarGroupLabel className="px-0 text-[11px]">Pipeline info</SidebarGroupLabel>
<div className="grid gap-1.5 text-xs">
<div className="flex items-center gap-2 text-muted-foreground">
<GitBranch className="h-3.5 w-3.5 shrink-0" />
<span>
{pipeline.versions.length}
{" "}
{pipeline.versions.length === 1 ? "version" : "versions"}
</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<PipelineIcon className="h-3.5 w-3.5 shrink-0" />
<span>
{pipeline.routeCount}
{" "}
{pipeline.routeCount === 1 ? "route" : "routes"}
</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<Layers className="h-3.5 w-3.5 shrink-0" />
<span>
{pipeline.sourceCount}
{" "}
{pipeline.sourceCount === 1 ? "source" : "sources"}
</span>
</div>
</div>
</div>
);
}

function PipelineExecuteButton({
sourceId,
fileId,
pipelineId,
pipeline,
}: {
sourceId: string;
fileId: string;
pipelineId: string;
pipeline: PipelineDetails;
}) {
const { execute, executing } = useExecute();
const navigate = useNavigate();
const versionStorageKey = `${sourceId}:${fileId}:${pipelineId}`;
const { selectedVersions } = usePipelineVersions(versionStorageKey, pipeline.versions);

const handleExecute = useCallback(async () => {
const result = await execute(sourceId, fileId, pipelineId, [...selectedVersions]);
if (result.success && result.executionId) {
navigate({
to: "/s/$sourceId/$sourceFileId/$pipelineId/executions/$executionId",
params: {
sourceId,
sourceFileId: fileId,
pipelineId,
executionId: result.executionId,
},
});
}
}, [execute, navigate, fileId, pipelineId, selectedVersions, sourceId]);

return (
<div className="px-1 pt-2 border-t border-border/60" data-testid="pipeline-sidebar-execute">
<Button
className="w-full"
disabled={executing || selectedVersions.size === 0}
size="sm"
onClick={handleExecute}
>
<Play className="mr-2 h-4 w-4" />
{executing ? "Running..." : "Execute"}
</Button>
</div>
);
}
Loading
Loading