Skip to content

Edit in sandbox: create or join a sandbox from a live workflow#4901

Open
elias-ba wants to merge 1 commit into
sandbox-devxfrom
4859-edit-in-sandbox
Open

Edit in sandbox: create or join a sandbox from a live workflow#4901
elias-ba wants to merge 1 commit into
sandbox-devxfrom
4859-edit-in-sandbox

Conversation

@elias-ba

@elias-ba elias-ba commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds "Edit in sandbox". From a live workflow, the header gains an "Edit in sandbox" action that opens a picker to either branch a new sandbox from the current live version or join an active one, then lands the user in the sandbox editor with that workflow's trigger live and its own endpoint. The parent project is untouched; changes come back through promotion in a later slice of the epic.

Creating clones the parent project (the existing full-clone provisioning), then promotes the edited workflow to live inside the sandbox. The other cloned workflows stay draft, so a workflow's state stays coherent with its triggers. The sandbox copy remains editable because the live read-only lock only applies outside a sandbox. The picker lists the parent's active sandboxes (last edited first, with collaborators), and join is disabled for a sandbox that does not contain this workflow. A sandbox badge is shown in the editor header. Server-side errors (permission, usage limit) surface their real message in the picker, including the limit upsell, rather than a generic toast.

Closes #4859

Validation steps

  1. On a live workflow, click Edit in sandbox to open the picker.
  2. Create new provisions a full-clone sandbox, lands in the sandbox editor with the workflow editable and a sandbox badge.
  3. Confirm the workflow's trigger is live in the sandbox with its own endpoint, and the parent is unchanged.
  4. Reopen the picker from another live workflow and confirm the active sandbox is listed to join, and that join is disabled for a sandbox that does not contain that workflow.

Additional notes for the reviewer

  1. Two things are deliberately deferred rather than half-built here: refining what the header shows inside a sandbox (suppressing the draft/live controls and adding Promote) belongs with the promote work (Sandbox DevX: Promote request (pending + withdraw) #4861); and hiding the action client-side for users who cannot provision (today the server enforces it and the picker surfaces the reason) needs a permission flag on the session context.
  2. The who-can-edit-in-sandbox permission model and what "join an active sandbox" should mean access-wise are product calls worth a confirm with the feature owners before this leaves the epic branch. This PR reuses the existing :provision_sandbox permission (owner/admin/editor) and the :new_sandbox usage limit.
  3. There is no last-editor field on a project, so the picker sorts by last-edited time and shows project members as collaborators (membership, not live presence).

AI Usage

Please disclose whether you've used AI anywhere in this PR (it's cool, we just
want to know!):

  • I have used Claude Code
  • I have used another model
  • I have not used AI

You can read more details in our
Responsible AI Policy

Pre-submission checklist

  • I have performed an AI review of my code (we recommend using /review
    with Claude Code)
  • I have implemented and tested all related authorization policies.
    (e.g., :owner, :admin, :editor, :viewer)
  • I have updated the changelog. (Deferred: this targets the epic
    integration branch; the changelog is updated when the epic merges to main.)
  • I have ticked a box in "AI usage" in this PR

From a live workflow, "Edit in sandbox" opens a picker to either branch a new
sandbox from the current live version or join an active one, then lands the
user in the sandbox editor with that workflow's trigger live and its own
endpoint. The parent stays untouched until a change is promoted back (a later
slice of the epic).

- New channel events: list a parent's active sandboxes (with collaborators and
  the joinable workflow, sorted by last edited) and create-and-open a sandbox.
- Creating clones the parent project, then promotes the edited workflow to live
  inside the sandbox; the other cloned workflows stay draft so state and
  triggers remain coherent. The sandbox copy stays editable because the
  read-only lock only applies to live workflows outside a sandbox.
- A sandbox badge in the editor header. Server-side errors (permission, usage
  limit) surface their real message in the picker rather than a generic one.
@github-project-automation github-project-automation Bot moved this to New Issues in Core Jun 26, 2026
@github-actions

Copy link
Copy Markdown

Security Review ✅

  • S0 (project scoping): New list_active_sandboxes_for_editing/2 in lib/lightning/projects.ex:1913 filters on parent_id from socket.assigns.project (authorized at channel join, workflow_channel.ex:1352-1416), and the workflow lookup is scoped to those sandbox_ids; promote_cloned_workflow/3 filters by the just-created sandbox.id, so no cross-project read or write is reachable.
  • S1 (authorization): handle_in("edit_in_sandbox", ...) gates on Permissions.can?(:sandboxes, :provision_sandbox, user, parent) (workflow_channel.ex:1194), which lib/lightning/policies/sandboxes.ex:66 resolves to editor/admin/owner on the parent — matching the existing policy; list_sandboxes is a read confined to children of the already-authorized parent project, consistent with list_workspace_projects/2.
  • S2 (audit trail): N/A — the handler reuses the pre-existing Projects.provision_sandbox/3 and Workflows.go_live/2/save_workflow flows; no new config-resource write path is introduced that bypasses an existing audit step.

@elias-ba elias-ba requested review from doc-han and lmac-1 and removed request for doc-han June 29, 2026 08:50
@lmac-1

lmac-1 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

@elias-ba I noticed that you haven't checked the box that you've done an AI self review of the code here. Have you done a Claude review?

@lmac-1 lmac-1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking exciting @elias-ba - it's a big review so strap in 🤩! I've left a few comments in the code that I think are worth addressing within this PR. One final code comment:

assets/js/collaborative-editor/components/EditInSandboxPicker.tsx:1 and test files — multi-paragraph block comments violate CLAUDE.md

CLAUDE.md: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max."

Four blocks violate this:

  • EditInSandboxPicker.tsx lines 1–14 (14-line file header with 3 paragraph breaks)
  • EditInSandboxPicker.tsx lines 36–39 (serverMessage function comment)
  • EditInSandboxPicker.test.tsx lines 1–12
  • Header.sandbox.test.tsx lines 1–12

My feedback from a UX/UI perspective on "Edit in sandbox" experience (not blocking but observational):

  1. It was a bit confusing seeing "Join an active sandbox" and then both sandboxes having disabled "Join" buttons. Perhaps it shouldn't show this section if there is no sandbox to join?
Image
  1. When hovering over the disabled "Join" button in the above scenario, there was no explanatory tooltip message to explain why. Sometimes it appears but after a long pause, not instantly.

  2. The "Join an active sandbox" wording was a bit confusing.. it made me feel like I wasn't a part of that sandbox before. Not sure what would be better wording here.

  3. What exactly are the collaborator circles for? What do they represent?

  4. If entering a sandbox name is optional, our styling in the header should support long workflow/sandbox names more elegantly. Suggestions:
    (1) Make the Save button grow
    (2) Add ellipsis for the sandbox/workflow name when it's a certain length:

Image
  1. I think we should improve the toast message when you try to add a sandbox with a name that already exists rather than the generic toast message. It should tell the user that a sandbox already exists with that name so they know how to fix it.

  2. It is confusing for me when I switch to sandbox and see "Live" -> Switch to draft. I know what it means because of the extra context from meetings. But it's a bit strange to come from somewhere where I can't edit a live workflow and then it is marked as "Live" in the sandbox.


Some general comments outside of the scope of this PR of things I noticed for this epic while I'm here:

  1. Switch to draft modal - should Esc close this modal?

  2. Switch to draft modal: I'm not sure the message is clear enough that it will turn off the production workflow. Not sure of the wording but perhaps needs to warn more explicitly.

  3. At one point I forgot I was reviewing this PR, went to rename my workflow and got confused why I couldn't edit anything. Perhaps this tooltip should be extended to mention why you don't have permission to edit and how to edit? (however, you might not be able to mention sandboxes as the experience is different for free vs paid users)

Image
  1. The box shadows of the new buttons feel very different stylistically compared to the other parts of the application. Are they part of the new design system?
Image
  1. You cannot copy or see the full webhook URL for a live workflow

  2. Go live button is the wrong colour when disabled (should be the same as Save)

Image
  1. I see the old trigger picker.. you should merge from main to avoid any nasty conflicts while you can 🤭

  2. The top bar feels very hectic.. there is a LOT of things going on up there now. I'm sure this will get better as other epics come in but it feels like a lot.

{overflow > 0 && (
<span
className="inline-flex h-6 w-6 items-center justify-center rounded-full
bg-gray-100 text-[10px] font-medium text-gray-500 ring-2 ring-white"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love text-[10px], but this is more for us to tidy up these sorts of sizes and design tokens when we generalise the design system more comprehensively later on. No block to merging.

setShowEditInSandboxPicker(true);
}}
className={cn(
'inline-flex items-center gap-1 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we can just use className as it is one long static string

{:reply, {:ok, %{project_id: sandbox.id, workflow_id: cloned_workflow.id}},
socket}
else
error -> workflow_error_reply(socket, error)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

workflow_error_reply in workflow_channel.ex doesn't have a clause for {:error, :nesting_too_deep}, which is one of the return values from Projects.provision_sandbox (see sandboxes.ex:585). If that error reaches workflow_error_reply, the channel process will crash and drop the user's WebSocket connection. Needs a handler clause added, similar to the existing ones at line 1045-1069.

promote_cloned_workflow(sandbox, workflow.name, user) do
{:reply, {:ok, %{project_id: sandbox.id, workflow_id: cloned_workflow.id}},
socket}
else

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If provision_sandbox succeeds but promote_cloned_workflow fails, the sandbox is already committed to the database and this else branch doesn't do anything to clean it up.

The user gets an error but the sandbox persists and consume quota. Not blocking but could add a compensating delete of the sandbox here, or both steps wrapped in a single transaction to make it a better user experience.

end
end

defp promote_cloned_workflow(sandbox, workflow_name, user) do

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function queries the database directly instead of going through the Workflows context. It would be better to extract a Workflows.get_workflow_by_name(project_id, name) function in lib/lightning/workflows.ex

"#{base}-sandbox"
end

defp random_sandbox_color do

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a duplicate of get_random_color/0 in sandbox_live/form_component_ex.

Since color_palette_hex_colors/0 already lives in LightningWeb.SandboxLive.Components we could extract this into a shared public random_color/0 function there and call it from both places.

}
end

defp collaborator_name(user) do

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. Claude mentioned how this function is duplicating the first/last name logic that already appears in mailer.ex, workflow_json.ex and elsewhere. There isn't a canonical helper yet. But we could extract this out to a shared Accounts.display_name/1 function (not sure if that's the right place) and replace all the copies. This is out of scope of this PR but flagging for follow-up.

Comment thread lib/lightning/projects.ex
joinable_workflow_ids =
from(w in Workflow,
where:
w.project_id in ^sandbox_ids and w.name == ^workflow_name and

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the parent project has no sandboxes, sandbox_ids will be [] and the workflow lookup query still runs against the database unnecessarily, always returning nothing. Suggest adding an early return guard here: if sandbox_ids == [], do: []

@github-project-automation github-project-automation Bot moved this from New Issues to In review in Core Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

2 participants