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
4 changes: 2 additions & 2 deletions src/routes/remotePipelineEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ describe("remote pipeline editor entry", () => {
});
renderEditor(<EditorV2 />);
expect(screen.getByRole("alert")).toHaveTextContent(message);
expect(screen.queryByText("View only")).not.toBeInTheDocument();
expect(screen.queryByText("Read only")).not.toBeInTheDocument();
expect(Boolean(screen.queryByRole("button", { name: "Retry" }))).toBe(
retryable,
);
Expand Down Expand Up @@ -654,7 +654,7 @@ describe("remote pipeline editor entry", () => {
});
renderEditor(<EditorV2 />);
expect(screen.getByRole("alert")).toHaveTextContent(message);
expect(screen.queryByText("View only")).not.toBeInTheDocument();
expect(screen.queryByText("Read only")).not.toBeInTheDocument();
expect(screen.getByRole("link", { name: "Home" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Sign in" }),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Panel } from "@xyflow/react";
import type { PropsWithChildren } from "react";

import { InlineStack } from "@/components/ui/layout";
import { cn } from "@/lib/utils";
import { CANVAS_MENU_SURFACE_CLASS } from "@/routes/v2/shared/components/CanvasMenuBar";

import { ReadOnlyBadge } from "./ReadOnlyBadge";

export function EmbeddedPipelineViewControls({ children }: PropsWithChildren) {
return (
<Panel
position="top-right"
role="group"
aria-label="Pipeline view controls"
>
<InlineStack
gap="2"
blockAlign="center"
wrap="nowrap"
className={cn(
"rounded-md border border-white/10 px-2 py-1 shadow-md",
CANVAS_MENU_SURFACE_CLASS,
)}
>
<ReadOnlyBadge />
<InlineStack wrap="nowrap" blockAlign="center">
{children}
</InlineStack>
</InlineStack>
</Panel>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Badge } from "@/components/ui/badge";

export function ReadOnlyBadge() {
return (
<Badge size="sm" className="shrink-0">
Read only
</Badge>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { PropsWithChildren, ReactNode } from "react";

import { Badge } from "@/components/ui/badge";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Text } from "@/components/ui/typography";
import { AppMenuActions } from "@/routes/v2/shared/components/AppMenuActions";
Expand All @@ -9,6 +8,21 @@ import {
CanvasMenuBar,
} from "@/routes/v2/shared/components/CanvasMenuBar";

import { ReadOnlyBadge } from "./ReadOnlyBadge";

export function RemotePipelineFrame({ children }: PropsWithChildren) {
return (
<BlockStack
fill
align="stretch"
inlineAlign="start"
className="bg-slate-100 dark:bg-background select-none"
>
{children}
</BlockStack>
);
}

export function RemotePipelineShell({
name = "Pipeline",
readOnly = false,
Expand All @@ -20,12 +34,7 @@ export function RemotePipelineShell({
menus?: ReactNode;
}>) {
return (
<BlockStack
fill
align="stretch"
inlineAlign="start"
className="bg-slate-100 dark:bg-background select-none"
>
<RemotePipelineFrame>
<CanvasMenuBar>
<InlineStack
gap="3"
Expand All @@ -48,11 +57,7 @@ export function RemotePipelineShell({
>
{name}
</Text>
{readOnly && (
<Badge size="sm" className="shrink-0">
View only
</Badge>
)}
{readOnly && <ReadOnlyBadge />}
</InlineStack>
{menus && (
<InlineStack wrap="nowrap" blockAlign="center">
Expand All @@ -66,6 +71,6 @@ export function RemotePipelineShell({
</InlineStack>
</CanvasMenuBar>
{children}
</BlockStack>
</RemotePipelineFrame>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,11 @@ it.each([false, true])("inspects pipelines (embed=%s)", async (embedded) => {
screen.getByRole("menuitemcheckbox", { name: "Sidekick" }),
).toBeInTheDocument();
await user.keyboard("{Escape}");
const viewControls = embedded
? screen.getByRole("group", { name: "Pipeline view controls" })
: screen.getByRole("banner");
expect(
within(screen.getByRole("banner")).queryByRole("button", {
within(viewControls).queryByRole("button", {
name: /^(Details|Clone pipeline|Submit Run)$/,
}),
).not.toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ReactFlowProps } from "@xyflow/react";
Expand Down Expand Up @@ -179,7 +180,7 @@ it("shows a read-only graph and navigates subgraphs without changing the spec",
{ wrapper: RouterWrapper },
);
await screen.findByText("First task");
expect(screen.getByText("View only")).toBeInTheDocument();
expect(screen.getByText("Read only")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Fit view" })).toBeInTheDocument();
expect(canvas).toMatchObject({
nodesConnectable: false,
Expand Down Expand Up @@ -212,7 +213,7 @@ it("starts embedded views compact with Sidekick disabled and keeps the minimap a
const { container } = render(<RemotePipelineView spec={spec} embedded />, {
wrapper: RouterWrapper,
});
await screen.findByText("View only");
await screen.findByText("Read only");
expect(container.querySelector('[data-dock-area="left"]')).toBeNull();
expect(container.querySelector('[data-dock-area="right"]')).toHaveStyle({
width: "36px",
Expand All @@ -226,6 +227,27 @@ it("starts embedded views compact with Sidekick disabled and keeps the minimap a
expect(await screen.findByRole("button", { name: "Fit view" })).toBeVisible();
});

it("floats the view controls on an embedded canvas instead of a menu bar", async () => {
const spec = new YamlDeserializer(new IncrementingIdGenerator()).deserialize({
name: "Shared pipeline",
implementation: { graph: { tasks: {} } },
});
render(<RemotePipelineView spec={spec} embedded />, {
wrapper: RouterWrapper,
});
const controls = await screen.findByRole("group", {
name: "Pipeline view controls",
});
expect(within(controls).getByText("Read only")).toBeInTheDocument();
expect(within(controls).getAllByRole("button")).toHaveLength(2);
for (const name of ["View", "Windows"]) {
expect(within(controls).getByRole("button", { name })).toBeInTheDocument();
}
expect(screen.queryByRole("banner")).toBeNull();
expect(screen.queryByRole("link", { name: "Home" })).toBeNull();
expect(screen.queryByText("Shared pipeline")).toBeNull();
});

it("copies the selected task only while the view is active", async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
const originalClipboard = Object.getOwnPropertyDescriptor(
Expand Down Expand Up @@ -406,7 +428,7 @@ it.each<RemotePipelineNodeType>(["task", "input", "output"])(
await waitFor(() => expect(editor.selectedNodeId).toBe(node.$id));
expect(editor.selectedNodeType).toBe(nodeType);
expect(canvas.nodes?.some((item) => item.id === node.$id)).toBe(true);
expect(screen.getByText("View only")).toBeInTheDocument();
expect(screen.getByText("Read only")).toBeInTheDocument();
await waitFor(() =>
expect(router.state.location.search.nodeId).toBe("shared"),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ import { useWindowPersistence } from "@/routes/v2/shared/windows/windowPersisten
import type { PipelineFile } from "@/services/pipelineStorage/PipelineFile";
import { getEditorPipelineIdentity } from "@/services/pipelineStorage/types";

import { EmbeddedPipelineViewControls } from "./EmbeddedPipelineViewControls";
import { RemotePipelineActions } from "./RemotePipelineActions";
import { RemotePipelineShell } from "./RemotePipelineShell";
import {
RemotePipelineFrame,
RemotePipelineShell,
} from "./RemotePipelineShell";
import { remotePipelineViewRegistry } from "./remotePipelineViewRegistry";

const VIEW_PRESETS: ViewPreset[] = [
Expand Down Expand Up @@ -136,20 +140,18 @@ const RemotePipelineContent = observer(function RemotePipelineContent({

const activeSpec = navigation.activeSpec;

return (
<RemotePipelineShell
readOnly
name={spec.name || "Untitled pipeline"}
menus={
<>
<ViewMenu />
<WindowManagementMenu
presets={embedded ? [embeddedPreset] : VIEW_PRESETS}
trackingPrefix="v2.pipeline_view.windows_menu"
/>
</>
}
>
const menus = (
<>
<ViewMenu />
<WindowManagementMenu
presets={embedded ? [embeddedPreset] : VIEW_PRESETS}
trackingPrefix="v2.pipeline_view.windows_menu"
/>
</>
);

const content = (
<>
{file?.editingError && (
<div role="alert" className="w-full border-b p-3">
<InlineStack gap="2" blockAlign="center">
Expand Down Expand Up @@ -189,13 +191,33 @@ const RemotePipelineContent = observer(function RemotePipelineContent({
spec={activeSpec}
nodesDraggable={false}
className="h-full"
/>
>
{embedded && (
<EmbeddedPipelineViewControls>
{menus}
</EmbeddedPipelineViewControls>
)}
</ReadOnlyFlowCanvas>
<WindowContainer />
</div>
<DockArea side="right" />
</InlineStack>
</SpecProvider>
)}
</>
);

if (embedded) {
return <RemotePipelineFrame>{content}</RemotePipelineFrame>;
}

return (
<RemotePipelineShell
readOnly
name={spec.name || "Untitled pipeline"}
menus={menus}
>
{content}
</RemotePipelineShell>
);
});
Expand Down
8 changes: 7 additions & 1 deletion src/routes/v2/shared/components/CanvasMenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import type { PropsWithChildren } from "react";

import logo from "/Tangle_Icon_White.png";
import { InlineStack } from "@/components/ui/layout";
import { cn } from "@/lib/utils";
import { APP_ROUTES } from "@/routes/appRoutes";
import { TOP_NAV_HEIGHT } from "@/utils/constants";
import { tracking } from "@/utils/tracking";

export const CANVAS_MENU_SURFACE_CLASS = "bg-stone-900";

export function CanvasMenuBar({ children }: PropsWithChildren) {
return (
<header
className="relative w-full shrink-0 bg-stone-900 px-3 py-1 md:px-4"
className={cn(
"relative w-full shrink-0 px-3 py-1 md:px-4",
CANVAS_MENU_SURFACE_CLASS,
)}
style={{ height: TOP_NAV_HEIGHT }}
>
<InlineStack
Expand Down
Loading