Skip to content

SY-4112: Add Schematic Symbol to Embed Media and iFrames#2260

Open
sy-nico wants to merge 12 commits intorcfrom
sy-4112-create-embedcontainer-schematic-symbol
Open

SY-4112: Add Schematic Symbol to Embed Media and iFrames#2260
sy-nico wants to merge 12 commits intorcfrom
sy-4112-create-embedcontainer-schematic-symbol

Conversation

@sy-nico
Copy link
Copy Markdown
Contributor

@sy-nico sy-nico commented Apr 24, 2026

Issue Pull Request

Linear Issue

SY-4112

Description

  • Add support for "Embed" symbols in the schematic:
    • MediaEmbed supports mjpeg, svg, png, and gifs
    • IframeEmbed supports iframes (e.g. Grafana dashboards) with a sandbox
      attribute for security. A "Block Cookies" toggle controls whether the
      embedded site can use cookies (allow-same-origin).
    • Stubs for PageEmbed to later support embedding Synnax logs, plots, and
      tables. Proof of concept was shown for page embeds, but that feature will
      require a larger refactor and is outside the scope of this feature.
  • Edge resizing for symbol components via drag handles on edges and corners.
  • useStaticData search improvements: Fuse.js keys are now collected from all
    items (not just the first), and ignoreLocation enables substring matching
    anywhere in a field.
image

.

image

Basic Readiness

  • I have performed a self-review of my code.
  • I have added relevant, automated tests to cover the changes.

Greptile Summary

This PR adds MediaEmbed (mjpeg/svg/png/gif) and IframeEmbed (iframe with sandbox) schematic symbols, edge/corner resize handles via NodeResizeControl in the Grid layer, and improves useStaticData Fuse.js search to collect keys from all items and enable ignoreLocation. The Tauri window creation is refactored to use an invoke-based command so the on_new_window handler can intercept and redirect navigations for new windows.

  • The previously flagged P1 in console/src/log/services/link.ts (re-introduced data key in spread and dropped anyStateZ.parse() validation) remains unaddressed.
  • The expect() panics in the Tauri setup closure (main.rs) flagged in the prior round are also still present.

Confidence Score: 3/5

Not safe to merge until the outstanding P1 in link.ts (data key re-introduced, schema validation removed) is resolved.

Score reduced from the P1 ceiling of 4 because the link.ts defect is a pre-existing flagged issue that is still unaddressed, and the main.rs expect() panics remain. The new embed/resize/search code itself is clean and well-tested.

console/src/log/services/link.ts (P1 data-key / schema-validation regression), console/src-tauri/src/main.rs (expect() panics in setup closure)

Important Files Changed

Filename Overview
console/src/log/services/link.ts Re-introduces data key into spread and removes anyStateZ.parse() validation — P1 issue flagged in prior review, not yet addressed.
console/src-tauri/src/main.rs Adds create_webview_window Tauri command with on_new_window handler; setup closure uses expect() (flagged in prior review) and manually creates the main window with "create": false in config.
drift/src/tauri/index.ts Extracts buildWindowOptions helper and replaces WebviewWindow instantiation with invoke("create_webview_window"), correctly preserving position-visibility clamping before the call.
pluto/src/schematic/symbol/Symbols.tsx Adds MediaEmbed, IframeEmbed, PageEmbed symbols; moves resize handling from primitive-level onResize props to Grid-level NodeResizeControl; MediaEmbedBase useEffect dependency on onChange is fragile.
pluto/src/schematic/symbol/Grid.tsx Adds optional onResize prop and renders NodeResizeControl handles (edges + corners) when editable; exports roundResizeDims helper.
pluto/src/schematic/symbol/registry.ts Registers mediaEmbed and iframeEmbed variants with searchTerms, defaultProps, and a new "containers" group; PageEmbed intentionally omitted (stub).
pluto/src/list/useStaticData.ts Collects Fuse.js keys from all items (not just the first) via flatMap, and enables ignoreLocation for substring matching anywhere in a field.
pluto/src/schematic/Schematic.css Broadens resize-control visibility rule from .pluto-symbol-primitive-scoped to any selected node — intentional to support new embed symbols that lack the primitive class.

Comments Outside Diff (3)

  1. pluto/src/schematic/symbol/Symbols.tsx, line 901-908 (link)

    P2 security allow-scripts + allow-same-origin sandbox escape

    When both allow-scripts and allow-same-origin are set together on an iframe sandbox, a script running inside the iframe — if it is same-origin with the Synnax app — can call window.frameElement.removeAttribute("sandbox") and fully escape the sandbox. MDN explicitly warns against this combination for untrusted content. For cross-origin embeds (e.g. Grafana on a different host) window.frameElement returns null, so the risk doesn't materialise in the common case, but it's worth adding a code comment here to document the known limitation alongside the existing postMessage warning.

  2. console/src/log/services/link.ts, line 92-93 (link)

    P1 data key leaked into spread and schema validation removed

    The old code destructured data out of the retrieve result so that log did not contain a data key, then validated it with anyStateZ.parse(data). The new code spreads ...log directly, which re-introduces data: log.data into the object passed to Log.create. This means Log.create now receives both the individual flattened fields from log.data and a top-level data key pointing at the same nested object — a different shape than before. The silent removal of anyStateZ.parse() also drops the schema validation that previously guaranteed the data conformed to the expected shape before layout creation.

  3. console/src/log/services/link.ts, line 3-5 (link)

    P1 data key re-introduced and schema validation removed

    { ...log.data, ...log } spreads log.data's fields first, then overwrites with log's top-level fields — which include a data key pointing at the same nested object. Log.create now receives an unexpected data property that the old code explicitly stripped out via destructuring. The old code also validated through anyStateZ.parse(data) before use; that guard is gone, so a malformed server response will propagate silently instead of throwing at the boundary.

Reviews (8): Last reviewed commit: "Merge remote-tracking branch 'origin/rc'..." | Re-trigger Greptile

@sy-nico sy-nico requested a review from pjdotson April 24, 2026 17:30
Comment thread pluto/src/schematic/symbol/EmbedRender.spec.tsx Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

❌ Patch coverage is 78.09524% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.35%. Comparing base (1d26a30) to head (2a56428).

Files with missing lines Patch % Lines
pluto/src/schematic/symbol/Symbols.tsx 79.06% 9 Missing ⚠️
drift/src/tauri/index.ts 57.14% 5 Missing and 1 partial ⚠️
pluto/src/schematic/symbol/Grid.tsx 37.50% 5 Missing ⚠️
console/src/log/services/link.ts 0.00% 2 Missing ⚠️
pluto/src/schematic/symbol/Forms.tsx 94.44% 1 Missing ⚠️

❌ Your patch check has failed because the patch coverage (78.09%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##               rc    #2260      +/-   ##
==========================================
+ Coverage   64.22%   64.35%   +0.13%     
==========================================
  Files        2157     2157              
  Lines      108983   109068      +85     
  Branches     8297     8310      +13     
==========================================
+ Hits        69989    70186     +197     
+ Misses      32983    32873     -110     
+ Partials     6011     6009       -2     
Flag Coverage Δ
console 20.35% <0.00%> (ø)
drift 42.66% <57.14%> (+3.61%) ⬆️
pluto 56.41% <83.14%> (+0.70%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sy-nico sy-nico force-pushed the sy-4112-create-embedcontainer-schematic-symbol branch from 9fd1302 to 3c91cce Compare April 24, 2026 19:32
Comment on lines 120 to +135
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_process::init())
.setup(|app| {
// Create the main window manually so we can attach on_new_window handler
let main_window_config = app.config().app.windows.first().cloned()
.expect("No window config found");
let app_handle_for_main = app.handle().clone();
WebviewWindowBuilder::from_config(app.handle(), &main_window_config)
.expect("Failed to create main window builder")
.on_new_window(move |url, _features| {
#[allow(deprecated)]
let _ = tauri_plugin_shell::ShellExt::shell(&app_handle_for_main)
.open(url.to_string(), None::<tauri_plugin_shell::open::Program>);
NewWindowResponse::Deny
})
.build()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 expect() panics in setup closure instead of propagating errors

The three expect() calls inside the setup closure will panic (hard crash) if any of them fail, rather than returning an error. The closure signature is |app| -> Result<(), Box<dyn Error>>, so ? is the correct idiom here and is already used on the very next statement (app.handle().plugin(...)?). A failure to build the main window should surface as a clean error exit, not a panic.

Copy link
Copy Markdown
Contributor

@pjdotson pjdotson left a comment

Choose a reason for hiding this comment

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

Two things:

  • I really don't think we should allow a general iframe embed. iframes could have potential security risks from users embedding untrusted content, all they are is just embedding a mini browser where it is likely that the user would just prefer the actual browser. It feels disconnected from our product, and I would really not want to add it unless we see a lot of evidence of user demand for it. Also, there is going to be a split between use cases for Synnax where it may primarily be used via a desktop app (like having an operator on-console during a test) and where it may primarily be used via browser (a factory setting where a lot of things are constantly running). I would be a little worried about users getting frustrated if a link that is supposed to open in a new tab doesn't work in an iframe for instance. I don't completely know what the right answer here is, but am curious to hear your thoughts. Personally, I think what might make more sense is an "image" and "video" element where you can link to an image or a video, and then we wrap that in the proper tag (we already have a Video element in Pluto).
  • there is a lot of edited code in both the Drift and Rust code that I am not sure is necessary and also isn't tested / can cause odd issues. We should talk through this together.

const { data, ...log } = await client.logs.retrieve({ key });
placeLayout(Log.create({ ...anyStateZ.parse(data), ...log }));
const log = await client.logs.retrieve({ key });
placeLayout(Log.create({ ...log.data, ...log }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why aren't we parsing the log data?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The other handlers don't parse either, and the spread of .data already extracts the state. I was just aiming for consistency with the other link.ts files. Should I change this?

NewWindowResponse::Deny
})
.build()
.expect("Failed to build main window");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

let's talk in person about why we have to do all of this stuff. I am not really
convinced it's necessary, especially since the documentation iframe already embeds fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Without this, right-clicking a link inside an iframe and selecting "Open in New Window/Tab" silently fails (no URL opens, regardless of protocol). Tauri's default webview denies new window requests when no on_new_window handler is set. The change attaches a handler that opens the link in the native browser, which then let's us open synnax:// urls that are embedded in the iframe page content.

What I tried:

  • allow-top-navigation-by-user-activation in the iframe sandbox. Doesn't fire for custom protocols like synnax://
  • Tauri's on_navigation handler. Fires for http/https but not custom protocols from sandboxed iframes
  • registerProtocolHandler Browsers only support web+ prefix

Another thing I tried to do was to "intercept" clicks on a synnax:// hyperlink to allow users to seamlessly open the page links. Since they are not valid links, when clicked from within an iframe, no navigation occurs. But I couldn't get this to work.

keys: Object.keys(filteredData[0]),
keys: [...keys],
threshold: 0.3,
ignoreLocation: true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what does ignoreLocation do?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It tells Fuse.js to score matches equally regardless of where they appear in the string. For MediaEmbed, I wanted various searches to work:
"Media Embed, Picture Embed, Video Embed.... PNG Embed...."

The problem was that PNG Embed was so deep into the string, that searching for "PNG Embed" returned 0 results instead of finding the MediaEmbed symbol.

Comment thread pluto/src/schematic/symbol/Symbols.tsx Outdated
}: SymbolProps<MediaEmbedProps>): ReactElement => (
<Primitives.Embed dimensions={dims} color={colorVal} placeholder="Enter a URL">
{url != null && url.length > 0 ? (
<img src={url} style={{ width: "100%", height: "100%", objectFit: "contain" }} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should this always be wrapped in a img tag? why not use a video tag for videos and
live streams?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Using a video tag prevents us from rendering PNGs, Gifs, SVGs, or other image formats. video is needed for HLS/DASH/MP4, which I don't see a use case for now. Maybe in the future when we have proper video stream handling.

@sy-nico
Copy link
Copy Markdown
Contributor Author

sy-nico commented Apr 27, 2026

@pjdotson ,

  • The <img> tag is more encompassing, and the pluto video tag you mentioned only handles MP4. It seems less invasive to use the img tag and not have to touch the pluto code, no?

  • On security, the iframe is sandboxed by default with sandbox="allow-scripts", which blocks cookies, form submission, popups, same-origin access, and top navigation. This is more restrictive than a regular browser tab. We also provide a "Block Cookies" toggle (default: on).

  • The use case is embedding live dashboards directly into schematics so operators don't have to context-switch to a browser. It's the same reason we have MediaEmbed for camera feeds. Inline visibility matters for control workflows and ops management. Of the folks I have reached out to, they were all excited at the possibility of the new feature. Supporting iFrames is a low-cost way to enable support for ideas that we could never consider, but would resolve real day-to-day pains for an operator. If Synnax can be used for anything, it would follow that it should be easy to integrate anything into Synnax.

@sy-nico sy-nico force-pushed the sy-4112-create-embedcontainer-schematic-symbol branch from 35f3551 to 2a56428 Compare April 30, 2026 00:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants