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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ and this project adheres to
- Divergence warning when merging sandboxes - displays alert if target branch
was modified after sandbox creation to prevent data loss
[#3747](https://github.com/OpenFn/lightning/issues/3747)
- Sandbox indicator banners in workflow editor (inspector) to help indicate when
working in a sandbox environment
[#3413](https://github.com/OpenFn/lightning/issues/3413)

### Changed

Expand Down
6 changes: 3 additions & 3 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,9 @@
border-radius: 4px;
}
.alert-info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
color: var(--color-primary-800);
background-color: var(--color-primary-100);
border-color: var(--color-primary-200);
}
.alert-warning {
color: #8b5f0d;
Expand Down
75 changes: 63 additions & 12 deletions assets/js/collaborative-editor/CollaborativeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface CollaborativeEditorDataProps {
"data-workflow-name": string;
"data-project-id": string;
"data-project-name"?: string;
"data-project-color"?: string;
"data-project-env"?: string;
"data-root-project-id"?: string;
"data-root-project-name"?: string;
"data-is-new-workflow"?: string;
}

Expand All @@ -39,13 +43,19 @@ interface BreadcrumbContentProps {
workflowName: string;
projectIdFallback?: string;
projectNameFallback?: string;
projectEnvFallback?: string;
rootProjectIdFallback?: string | null | undefined;
rootProjectNameFallback?: string | null | undefined;
}

function BreadcrumbContent({
workflowId,
workflowName,
projectIdFallback,
projectNameFallback,
projectEnvFallback,
rootProjectIdFallback,
rootProjectNameFallback,
}: BreadcrumbContentProps) {
// Get project from store (may be null if not yet loaded)
const projectFromStore = useProject();
Expand All @@ -60,8 +70,14 @@ function BreadcrumbContent({
// 3. Full collaborative mode (uses store)
const projectId = projectFromStore?.id ?? projectIdFallback;
const projectName = projectFromStore?.name ?? projectNameFallback;
const projectEnv = projectFromStore?.env ?? projectEnvFallback;
const currentWorkflowName = workflowFromStore?.name ?? workflowName;

const rootProjectId = rootProjectIdFallback;
const rootProjectName = rootProjectNameFallback;

const isSandbox = !!rootProjectId;

const breadcrumbElements = useMemo(() => {
return [
<BreadcrumbLink href="/" icon="hero-home-mini" key="home">
Expand All @@ -78,21 +94,37 @@ function BreadcrumbContent({
</BreadcrumbLink>,
<div key="workflow" className="flex items-center gap-2">
<BreadcrumbText>{currentWorkflowName}</BreadcrumbText>
<div
id="canvas-workflow-version-container"
className="flex items-middle text-sm font-normal"
>
<span
id="canvas-workflow-version"
className="inline-flex items-center rounded-md px-1.5 py-0.5 text-xs font-medium bg-blue-100 text-blue-800"
title="This is the latest version of this workflow"
<div className="flex items-center gap-1.5">
<div
id="canvas-workflow-version-container"
className="flex items-middle text-sm font-normal"
>
latest
</span>
<span
id="canvas-workflow-version"
className="inline-flex items-center rounded-md px-1.5 py-0.5 text-xs font-medium bg-primary-100 text-primary-800"
title="This is the latest version of this workflow"
>
latest
</span>
</div>
{projectEnv && (
<div
id="canvas-project-env-container"
className="flex items-middle text-sm font-normal"
>
<span
id="canvas-project-env"
className="inline-flex items-center rounded-md px-1.5 py-0.5 text-xs font-medium bg-primary-100 text-primary-800"
title={`Project environment is ${projectEnv}`}
>
{projectEnv}
</span>
</div>
)}
</div>
</div>,
];
}, [projectId, projectName, currentWorkflowName]);
}, [projectId, projectName, projectEnv, currentWorkflowName]);

return (
<Header
Expand All @@ -113,6 +145,9 @@ export const CollaborativeEditor: WithActionProps<
// Migration: Props are now fallbacks, sessionContextStore is primary source
const projectId = props["data-project-id"];
const projectName = props["data-project-name"];
const projectEnv = props["data-project-env"];
const rootProjectId = props["data-root-project-id"] ?? null;
const rootProjectName = props["data-root-project-name"] ?? null;
const isNewWorkflow = props["data-is-new-workflow"] === "true";

return (
Expand All @@ -138,9 +173,25 @@ export const CollaborativeEditor: WithActionProps<
{...(projectName !== undefined && {
projectNameFallback: projectName,
})}
{...(projectEnv !== undefined && {
projectEnvFallback: projectEnv,
})}
{...(rootProjectId !== null && {
rootProjectIdFallback: rootProjectId,
})}
{...(rootProjectName !== null && {
rootProjectNameFallback: rootProjectName,
})}
/>
<div className="flex-1 min-h-0 overflow-hidden">
<WorkflowEditor />
<WorkflowEditor
{...(rootProjectId !== null && {
parentProjectId: rootProjectId,
})}
{...(rootProjectName !== null && {
parentProjectName: rootProjectName,
})}
/>
<CollaborationWidget />
</div>
</StoreProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,24 @@ export function EmailVerificationBanner() {

return (
<div
className="alert-danger"
id="account-confirmation-alert"
className="alert-danger w-full flex items-center gap-x-6 px-6 py-2.5 sm:px-3.5 sm:before:flex-1"
data-testid="email-verification-banner"
role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"
>
<span className="hero-x-circle-solid" />
<p>
You must verify your email by {formattedDeadline} or your account will
be deleted.{" "}
<a href="/users/send-confirmation-email">
Resend confirmation email &rarr;
<p className="text-sm leading-6">
<span className="hero-x-circle-solid h-5 w-5 inline-block align-middle mr-2" />{" "}
Please confirm your account before {formattedDeadline} to continue using
OpenFn.{" "}
<a
href="/users/send-confirmation-email"
className="whitespace-nowrap font-semibold"
>
Resend confirmation email
<span aria-hidden="true"> &rarr;</span>
</a>
</p>
<div className="flex flex-1 justify-end"></div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* # Sandbox Indicator Banner
*
* Displays a warning banner when working in a sandbox environment.
* Shows only when the current project has a parent (is a sandbox).
*
* Matches the Phoenix Common.banner component structure exactly.
* Can be positioned absolutely (canvas overlay) or relatively (inspector panel).
*
* Note: parentProjectId/parentProjectName props actually contain the ROOT project
* (top-most ancestor), not the immediate parent. This is computed via Projects.root_of/1
* in Phoenix to handle arbitrarily deep sandbox hierarchies.
*
* Variants:
* - full: Shows full message with "Switch to root project" link (for canvas)
* - compact: Shows only "sandbox: name" (for inspector panel)
*/

interface SandboxIndicatorBannerProps {
parentProjectId?: string | null | undefined;
parentProjectName?: string | null | undefined;
projectName?: string | null | undefined;
position?: "absolute" | "relative";
variant?: "full" | "compact";
}

export function SandboxIndicatorBanner({
parentProjectId,
parentProjectName,
projectName,
position = "absolute",
variant = "full",
}: SandboxIndicatorBannerProps) {
const isSandbox = !!parentProjectId;
const sandboxName = projectName || "sandbox";

if (!isSandbox) {
return null;
}

const positionClasses = position === "absolute" ? "absolute z-5" : "relative";

return (
<div
id="sandbox-mode-alert"
className={`bg-primary-100 text-primary-800 w-full flex items-center gap-x-6 px-6 py-2.5 sm:px-3.5 sm:before:flex-1 ${positionClasses}`}
data-testid="sandbox-indicator-banner"
>
<p className="text-sm leading-6">
<span className="hero-beaker h-5 w-5 inline-block align-middle mr-2" />{" "}
{variant === "compact" ? (
<>
sandbox: <span className="font-bold">{sandboxName}</span>
</>
) : (
<>
You are currently working in the sandbox{" "}
<span className="font-bold">{sandboxName}</span>
</>
)}
</p>
<div className="flex flex-1 justify-end"></div>
</div>
);
}
19 changes: 16 additions & 3 deletions assets/js/collaborative-editor/components/WorkflowEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ import { ManualRunPanel } from "./ManualRunPanel";

const logger = _logger.ns("WorkflowEditor").seal();

export function WorkflowEditor() {
interface WorkflowEditorProps {
parentProjectId?: string | null;
parentProjectName?: string | null;
}

export function WorkflowEditor({
parentProjectId,
parentProjectName,
}: WorkflowEditorProps = {}) {
const { hash, searchParams, updateSearchParams } = useURLState();
const { currentNode, selectNode } = useNodeSelection();
const workflowStore = useWorkflowStoreContext();
Expand Down Expand Up @@ -164,7 +172,7 @@ export function WorkflowEditor() {
{workflow && (
<div
id="inspector"
className={`absolute top-0 right-0 h-full transition-transform duration-300 ease-in-out ${
className={`absolute top-0 right-0 h-full transition-transform duration-300 ease-in-out z-10 ${
showInspector
? "translate-x-0"
: "translate-x-full pointer-events-none"
Expand Down Expand Up @@ -212,7 +220,12 @@ export function WorkflowEditor() {

{/* Full-Screen IDE - replaces canvas when open */}
{isIDEOpen && selectedJobId && (
<FullScreenIDE jobId={selectedJobId} onClose={handleCloseIDE} />
<FullScreenIDE
jobId={selectedJobId}
onClose={handleCloseIDE}
parentProjectId={parentProjectId}
parentProjectName={parentProjectName}
/>
)}
</div>
);
Expand Down
19 changes: 17 additions & 2 deletions assets/js/collaborative-editor/components/ide/FullScreenIDE.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import {
import { useURLState } from "../../../react/lib/use-url-state";
import _logger from "#/utils/logger";
import { useSession } from "../../hooks/useSession";
import { useProject } from "../../hooks/useSessionContext";
import {
useCanSave,
useCurrentJob,
useWorkflowActions,
} from "../../hooks/useWorkflow";
import { CollaborativeMonaco } from "../CollaborativeMonaco";
import { SandboxIndicatorBanner } from "../SandboxIndicatorBanner";

import { IDEHeader } from "./IDEHeader";

Expand All @@ -24,6 +26,8 @@ const logger = _logger.ns("FullScreenIDE").seal();
interface FullScreenIDEProps {
jobId?: string;
onClose: () => void;
parentProjectId?: string | null | undefined;
parentProjectName?: string | null | undefined;
}

/**
Expand All @@ -38,13 +42,18 @@ interface FullScreenIDEProps {
*
* Panel layout persists to localStorage automatically.
*/
export function FullScreenIDE({ onClose }: FullScreenIDEProps) {
export function FullScreenIDE({
onClose,
parentProjectId,
parentProjectName,
}: FullScreenIDEProps) {
const { searchParams } = useURLState();
const jobIdFromURL = searchParams.get("job");
const { selectJob, saveWorkflow } = useWorkflowActions();
const { job: currentJob, ytext: currentJobYText } = useCurrentJob();
const { awareness } = useSession();
const { canSave, tooltipMessage } = useCanSave();
const project = useProject();

const leftPanelRef = useRef<ImperativePanelHandle>(null);
const centerPanelRef = useRef<ImperativePanelHandle>(null);
Expand Down Expand Up @@ -75,7 +84,7 @@ export function FullScreenIDE({ onClose }: FullScreenIDEProps) {

if (isMonacoFocused) {
// First Escape: blur Monaco editor to remove focus
(activeElement as HTMLElement)?.blur();
(activeElement as HTMLElement).blur();
event.preventDefault();
} else {
// Second Escape: close IDE
Expand Down Expand Up @@ -181,6 +190,12 @@ export function FullScreenIDE({ onClose }: FullScreenIDEProps) {
canSave={canSave}
saveTooltip={tooltipMessage}
/>
<SandboxIndicatorBanner
parentProjectId={parentProjectId}
parentProjectName={parentProjectName}
projectName={project?.name}
position="relative"
/>

{/* 3-panel layout */}
<div className="flex-1 overflow-hidden">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useState } from "react";

import { useURLState } from "#/react/lib/use-url-state";

import { useJobDeleteValidation } from "../../hooks/useJobDeleteValidation";
import { usePermissions } from "../../hooks/useSessionContext";
import { useWorkflowActions } from "../../hooks/useWorkflow";
Expand Down
1 change: 1 addition & 0 deletions assets/js/collaborative-editor/types/sessionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const UserContextSchema = z.object({
export const ProjectContextSchema = z.object({
id: uuidSchema,
name: z.string(),
env: z.string().nullable().optional(),
});

export const AppConfigSchema = z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ describe("EmailVerificationBanner", () => {
await waitFor(() => {
const alert = screen.getByRole("alert");
expect(alert).toBeInTheDocument();
expect(alert).toHaveTextContent(/You must verify your email/i);
expect(alert).toHaveTextContent(/or your account will be deleted/i);
expect(alert).toHaveTextContent(/Please confirm your account before/i);
expect(alert).toHaveTextContent(/to continue using OpenFn/i);
expect(alert).toHaveTextContent(/Wednesday, 15 January @ 10:30 UTC/);
});
});
Expand Down
Loading