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,16 @@
import TooltipButton from "@/components/shared/Buttons/TooltipButton";
import { Icon } from "@/components/ui/icon";

export function ComponentLibraryWindowMiniContent() {
return (
<TooltipButton
tooltip="View Component Library"
tooltipSide="right"
variant="outline"
size="icon"
aria-label="Components"
>
<Icon name="LayoutGrid" size="sm" className="text-gray-700" />
</TooltipButton>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cva } from "class-variance-authority";
import { observer } from "mobx-react-lite";
import type { ComponentProps, MouseEvent } from "react";

import { useAwaitAuthorization } from "@/components/shared/Authentication/useAwaitAuthorization";
import TooltipButton from "@/components/shared/Buttons/TooltipButton";
Expand All @@ -12,6 +13,7 @@ import { tracking } from "@/utils/tracking";

const quickRunIconVariants = cva("transition-colors", {
variants: {
variant: { menubar: "", mini: "" },
hasErrors: { true: "", false: "" },
onlyWarnings: { true: "", false: "" },
},
Expand All @@ -32,6 +34,7 @@ const quickRunIconVariants = cva("transition-colors", {
},
],
defaultVariants: {
variant: "menubar",
hasErrors: false,
onlyWarnings: false,
},
Expand All @@ -43,7 +46,19 @@ function tooltipLabel(hasErrors: boolean, onlyWarnings: boolean) {
return "Submit Run";
}

export const QuickRunButton = observer(function QuickRunButton() {
interface QuickRunButtonProps {
variant?: "menubar" | "mini";
renderSubmitter?: boolean;
trackingKey?: string;
}

export const QuickRunButton = observer(function QuickRunButton({
variant = "menubar",
renderSubmitter = true,
trackingKey = "v2.pipeline_editor.quick_run",
...tooltipButtonProps
}: QuickRunButtonProps &
Omit<ComponentProps<typeof TooltipButton>, "tooltip" | "variant" | "size">) {
const { navigation } = useSharedStores();
const { isAuthorized } = useAwaitAuthorization();
const rootSpec = navigation.rootSpec;
Expand All @@ -52,35 +67,52 @@ export const QuickRunButton = observer(function QuickRunButton() {
const hasErrors = errorCount > 0;
const onlyWarnings = allIssues.length > 0 && errorCount === 0;

let legacySpec: ReturnType<typeof serializeComponentSpec> | undefined;
let serializedPipelineSpec:
| ReturnType<typeof serializeComponentSpec>
| undefined;
try {
legacySpec = rootSpec
serializedPipelineSpec = rootSpec
? deepClone(serializeComponentSpec(rootSpec))
: undefined;
} catch {
legacySpec = undefined;
serializedPipelineSpec = undefined;
}

const tooltip = tooltipLabel(hasErrors, onlyWarnings);
const isMini = variant === "mini";

Comment thread
maxy-shpfy marked this conversation as resolved.
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
if (isMini) event.stopPropagation();
triggerSubmitRun();
};

return (
<>
<TooltipButton
{...tooltipButtonProps}
tooltip={tooltip}
className="hover:bg-transparent"
variant={isMini ? "outline" : undefined}
size={isMini ? "icon" : undefined}
className={
isMini
? "relative size-8 shrink-0 rounded-md"
: "hover:bg-transparent"
}
aria-label={isMini ? tooltip : undefined}
disabled={hasErrors}
onClick={triggerSubmitRun}
{...tracking("v2.pipeline_editor.quick_run")}
onClick={handleClick}
{...tracking(trackingKey)}
>
<Icon
name="Play"
className={quickRunIconVariants({ hasErrors, onlyWarnings })}
size={isMini ? "sm" : undefined}
className={quickRunIconVariants({ variant, hasErrors, onlyWarnings })}
/>
</TooltipButton>
{legacySpec && isAuthorized && (
{renderSubmitter && serializedPipelineSpec && isAuthorized && (
<div data-quick-run className="sr-only">
<TangleSubmitter
componentSpec={legacySpec}
componentSpec={serializedPipelineSpec}
isComponentTreeValid={rootSpec?.isValid}
onlyFixableIssues={!hasErrors && allIssues.length > 0}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { observer } from "mobx-react-lite";

import TooltipButton from "@/components/shared/Buttons/TooltipButton";
import { Icon } from "@/components/ui/icon";
import {
countErrors,
countWarnings,
} from "@/routes/v2/pages/Editor/components/ValidationSummary";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

import { IssueBadge } from "./components/IssueBadge";

export const PipelineTreeWindowMiniContent = observer(
function PipelineTreeWindowMiniContent() {
const { navigation } = useSharedStores();
const rootSpec = navigation.rootSpec;
const issues = rootSpec?.allValidationIssues ?? [];
const errorCount = countErrors(issues);
const warningCount = countWarnings(issues);
const hasIssues = errorCount > 0 || warningCount > 0;
const showValidBadge = rootSpec !== undefined && !hasIssues;

return (
<TooltipButton
tooltip="View Pipeline Structure"
tooltipSide="right"
variant="outline"
size="icon"
className="relative"
aria-label="Pipeline Structure"
>
<Icon name="GitBranch" size="sm" className="text-gray-700" />
{showValidBadge && (
<span className="pointer-events-none absolute -bottom-0.5 -right-0.5 flex size-3.5 items-center justify-center rounded-full bg-white shadow-sm ring-1 ring-green-200">
<Icon name="CircleCheck" size="xs" className="text-green-600" />
</span>
)}
{rootSpec && hasIssues && (
<span className="pointer-events-none absolute -top-1 -right-1">
<IssueBadge issues={issues} />
</span>
)}
</TooltipButton>
);
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { observer } from "mobx-react-lite";

import { QuickRunButton } from "@/routes/v2/pages/Editor/components/EditorMenuBar/components/QuickRunButton";

export const RunsAndSubmissionWindowMiniContent = observer(
function RunsAndSubmissionWindowMiniContent() {
return (
<QuickRunButton
variant="mini"
tooltipSide="right"
renderSubmitter={false}
trackingKey="v2.pipeline_editor.quick_run_mini"
onPointerDown={(event) => event.stopPropagation()}
/>
);
},
);
23 changes: 12 additions & 11 deletions src/routes/v2/pages/Editor/hooks/useComponentLibraryWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { useEffect } from "react";

import { ComponentLibraryContent } from "@/routes/v2/pages/Editor/components/ComponentLibraryContent";
import { ComponentLibraryWindowMiniContent } from "@/routes/v2/pages/Editor/components/ComponentLibraryWindowMiniContent";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

const COMPONENT_LIBRARY_WINDOW_ID = "component-library";

export function useComponentLibraryWindow() {
const { windows } = useSharedStores();
useEffect(() => {
if (!windows.getWindowById(COMPONENT_LIBRARY_WINDOW_ID)) {
windows.openWindow(<ComponentLibraryContent />, {
id: COMPONENT_LIBRARY_WINDOW_ID,
title: "Components",
position: { x: 0, y: 100 },
size: { width: 280, height: 350 },
disabledActions: ["close"],
persisted: true,
defaultDockState: "left",
});
}
if (windows.getWindowById(COMPONENT_LIBRARY_WINDOW_ID)) return;
windows.openWindow(<ComponentLibraryContent />, {
id: COMPONENT_LIBRARY_WINDOW_ID,
title: "Components",
position: { x: 0, y: 100 },
size: { width: 280, height: 350 },
disabledActions: ["close"],
persisted: true,
defaultDockState: "left",
miniContent: <ComponentLibraryWindowMiniContent />,
});
}, [windows]);
}
23 changes: 12 additions & 11 deletions src/routes/v2/pages/Editor/hooks/usePipelineTreeWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { useEffect } from "react";

import { PipelineTreeContent } from "@/routes/v2/pages/Editor/components/PipelineTreeContent/PipelineTreeContent";
import { PipelineTreeWindowMiniContent } from "@/routes/v2/pages/Editor/components/PipelineTreeContent/PipelineTreeWindowMiniContent";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

const PIPELINE_TREE_WINDOW_ID = "pipeline-tree";

export function usePipelineTreeWindow() {
const { windows } = useSharedStores();
useEffect(() => {
if (!windows.getWindowById(PIPELINE_TREE_WINDOW_ID)) {
windows.openWindow(<PipelineTreeContent />, {
id: PIPELINE_TREE_WINDOW_ID,
title: "Pipeline Structure",
position: { x: 300, y: 100 },
size: { width: 280, height: 400 },
disabledActions: ["close"],
persisted: true,
defaultDockState: "left",
});
}
if (windows.getWindowById(PIPELINE_TREE_WINDOW_ID)) return;
windows.openWindow(<PipelineTreeContent />, {
id: PIPELINE_TREE_WINDOW_ID,
title: "Pipeline Structure",
position: { x: 300, y: 100 },
size: { width: 280, height: 400 },
disabledActions: ["close"],
persisted: true,
defaultDockState: "left",
miniContent: <PipelineTreeWindowMiniContent />,
});
}, [windows]);
}
31 changes: 16 additions & 15 deletions src/routes/v2/pages/Editor/hooks/useRunsAndSubmissionWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import { useEffect } from "react";

import { RunsAndSubmissionContent } from "@/routes/v2/pages/Editor/components/RunsAndSubmissionContent";
import { RunsAndSubmissionWindowMiniContent } from "@/routes/v2/pages/Editor/components/RunsAndSubmissionWindowMiniContent";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

const RUNS_AND_SUBMISSION_WINDOW_ID = "runs-and-submission";

export function useRunsAndSubmissionWindow() {
const { windows } = useSharedStores();
useEffect(() => {
if (!windows.getWindowById(RUNS_AND_SUBMISSION_WINDOW_ID)) {
windows.openWindow(<RunsAndSubmissionContent />, {
id: RUNS_AND_SUBMISSION_WINDOW_ID,
title: "Runs & Submissions",
position: { x: 100, y: 460 },
size: { width: 280, height: 50 },
minSize: {
width: 280,
height: 50,
},
disabledActions: ["close"],
persisted: true,
defaultDockState: "left",
});
}
if (windows.getWindowById(RUNS_AND_SUBMISSION_WINDOW_ID)) return;
windows.openWindow(<RunsAndSubmissionContent />, {
id: RUNS_AND_SUBMISSION_WINDOW_ID,
title: "Runs & Submissions",
position: { x: 100, y: 460 },
size: { width: 280, height: 50 },
minSize: {
width: 280,
height: 50,
},
disabledActions: ["close"],
persisted: true,
defaultDockState: "left",
miniContent: <RunsAndSubmissionWindowMiniContent />,
});
}, [windows]);
}
79 changes: 79 additions & 0 deletions src/routes/v2/shared/windows/CollapsedDockWindowMini.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { observer } from "mobx-react-lite";
import type { CSSProperties } from "react";

import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Text } from "@/components/ui/typography";
import { cn } from "@/lib/utils";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

import { WindowContextProvider } from "./ContentWindowStateContext";
import { MAX_DOCK_AREA_WIDTH } from "./types";

interface CollapsedDockWindowMiniProps {
windowId: string;
dockSide: "left" | "right";
}

export const CollapsedDockWindowMini = observer(
function CollapsedDockWindowMini({
windowId,
dockSide,
}: CollapsedDockWindowMiniProps) {
const { windows } = useSharedStores();
const model = windows.getWindowById(windowId);
const mini = windows.getWindowMiniContent(windowId);
const content = windows.getWindowContent(windowId);

if (!model || model.state === "hidden" || !mini || !content) {
return null;
}

const popoverSide = dockSide === "left" ? "right" : "left";
const panelWidth = Math.min(model.size.width, MAX_DOCK_AREA_WIDTH);

return (
<Popover>
{/*
Radix asChild merges ref and listeners onto one DOM node. Mini content
is often a MobX observer() component that does not forwardRef to a host
element, so we wrap it in a div that receives the trigger props.
*/}
<PopoverTrigger asChild>
<div className="relative z-20 flex w-full shrink-0 justify-center outline-none">
Comment thread
maxy-shpfy marked this conversation as resolved.
{mini}
</div>
</PopoverTrigger>
<PopoverContent
side={popoverSide}
align="start"
sideOffset={6}
collisionPadding={12}
className={cn(
"p-0 overflow-hidden flex flex-col border shadow-lg z-50",
"max-h-[min(70vh,720px)] w-[min(92vw,var(--dock-mini-popover-w))]",
)}
style={
{
"--dock-mini-popover-w": `${panelWidth}px`,
} as CSSProperties
}
>
<div className="shrink-0 border-b bg-white px-2 py-1.5">
<Text size="xs" weight="semibold" className="truncate">
{model.title}
</Text>
</div>
<div className="min-h-0 flex-1 overflow-auto bg-white">
<WindowContextProvider value={{ model, content }}>
{content}
</WindowContextProvider>
</div>
</PopoverContent>
</Popover>
);
},
);
Loading
Loading