feat(resources): import multi-resource YAML from a file or drop target - #1742
Conversation
/api/resources/apply read its body with an unbounded io.ReadAll, letting a single request decide how much memory Radar spent on it. Both routes now cap before reading rather than after: - 6 MiB request body on apply and preview, returning 413. The cap fires before anything parses the body, so a 400 would blame content nobody has read yet and send the caller to debug syntax that may be fine. - 100 YAML documents on apply, the limit preview already enforced. A byte cap alone still admits a small body holding tens of thousands of documents, and the two routes disagreeing about the same bundle is what made that reachable. An oversize preview body previously answered 400 "invalid preview request: http: request body too large" — a size problem reported as a malformed request, and the one users actually hit, since preview always runs first. Splitting the documents now happens before the cluster client is fetched, so the request is fully validated before Radar reaches for a client.
Applying a rendered manifest bundle meant pasting it into the editor by hand. The dialog now takes a file: an "Import YAML file" button, and the editor surface itself as the drop target so the interaction is discoverable without adding another panel. The file becomes editor text and nothing else. Client validation, schema loading, dry-run review, conflict handling and apply are untouched, so a multi-document file behaves exactly as a pasted one already did. Limits and rejections: - one .yaml or .yml file, up to 6 MiB — the same limit the server enforces, refused at pick time instead of after an upload. - the extension decides, not the MIME type. Browsers report nothing dependable for YAML: macOS Chrome commonly sends "", Windows sends assorted octet-stream variants. - empty, unreadable and multi-file selections report through the error banner the dialog already uses for apply failures, and clear on the next successful import. The drag overlay promises only what a drag can know. A dragover exposes an item's kind and the item count but withholds file names, so it highlights for any file drag and calls out a multi-file drag, leaving the extension check for the drop. Replacing the editor asks first only when there is work to lose. The dialog opens prefilled in both of its entry points — a kind skeleton from ResourcesView, duplicate YAML from WorkloadView — so a non-empty editor is the normal state, not a signal the user has typed anything. The confirmation is gated on the content differing from what the dialog itself put there: dropping onto an untouched skeleton, or into an empty editor, loads straight away with no prompt. Cancelling leaves the editor exactly as it was.
Two problems with the import, both about what the user can see. The drag overlay never appeared in Safari. Detection read dataTransfer.items, which Safari keeps empty for the whole drag — the dragged items stay protected until the drop. Every browser advertises "Files" in dataTransfer.types, so that is the signal for whether files are coming; items now only supplies the count, where the browser is willing to give one. Loading a file then handed the content straight to the editor, which parses the whole buffer and blocks the main thread while it does — measured in Chrome at about 90ms for 128 KiB, 460ms for 1 MiB and 2.4s for 5.5 MiB. Nothing said so, and a pause that long reads as a hang. The import now names the file it is loading. Two details that sound like implementation but are the whole point: - the status is committed and painted before the content reaches the editor, across two animation frames. One frame only commits it; the paint needs the second, and a status that arrives after the block has started is no status. - it is retired when a frame arrives on time again, not when the content was handed over. Clearing it alongside the value batches both into the single render that blocks, which retires the message while the editor is still unusable — exactly the state it exists to describe. No spinner. The thread it would animate on is the one that is blocked, so it would freeze mid-turn and read as more broken than plain text.
PR Summary by QodoImport multi-resource YAML files with bounded preview and apply
AI Description
Diagram
High-Level Assessment
Files changed (7)
|
Code Review by Qodo
1.
|
An import spans an await and several animation frames, and nothing tied the result back to the editor it was meant for. Two ways that went wrong. Reading a file captured the buffer as it stood when the read began, so an edit made while a large file was being read was judged against the old value and could be replaced without the confirmation ever appearing; two overlapping imports could also finish out of order and let the older one land last. Separately, closing the dialog neither cancelled the queued frames nor cleared the pending confirmation, so closing and reopening quickly let a frame from the previous session overwrite the buffer the open effect had just reset. Every import now takes a generation on entry. The resume after the read and each deferred frame check it before touching state, and closing, reopening or unmounting bumps the generation and cancels whatever frames are still outstanding. The replacement check reads the buffer as it stands now through a ref rather than through the render-time closure. Confirming a replacement deliberately takes a fresh generation rather than the one its read started under: starting another import while the prompt is up would otherwise retire it, and Replace would silently do nothing. Confirming is a decision about the editor as it stands when the button is clicked.
Apply and the importer accept 6 MiB of YAML. Preview applied the same 6 MiB to its JSON body, which carries that YAML escaped inside an envelope and is always the larger of the two — so a manifest at the advertised limit was refused before review, and the point it started failing moved with however much escaping the file happened to need. Preview runs before apply, so this was the limit users actually met. The two are now separate concerns. maxYAMLContentBytes is the limit Radar advertises, checked on the decoded yaml field, which preview never bounded at all before — its envelope cap only refused large documents as a side effect. Apply keeps stating it directly, because there the body is the content. maxYAMLPreviewRequestBytes drops to being about transport and gets room for the escaping, so the envelope stops deciding how much YAML fits. Nothing a caller may submit grew: every route still refuses YAML over 6 MiB, and preview now refuses it for that reason rather than incidentally. A document made almost entirely of quotes could still exceed the envelope and is answered with the same size error. Boundary tests cover both directions — a document at exactly the limit whose encoded envelope is provably larger survives review, and one over the limit inside an envelope that fits is refused.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 21d08d2. Configure here.
…other Taking a generation retires the frames the previous import queued, including the one that would have cleared its loading status. When the new import then returned early — a rejected file, or one waiting on replace confirmation — it never reached the editor, so nothing cleared the status and the previous import's overlay stayed over the editor with no frame left alive to retire it. The overlay does not take pointer events, so starting a second import while one is loading is easy to do. An import now clears the status as soon as it takes ownership, before branching on the result, so every path out leaves it consistent: handing off to the editor sets it again in the same tick, and the paths that return early leave it clear.
The envelope held 8 MiB against a 6 MiB content limit, which covers the escaping a manifest typically incurs and not the escaping one is allowed to. A ConfigMap wrapping minified JSON is close to half quotes, and every quote doubles when the document is encoded — so a 6 MiB bundle of that shape encoded past 8 MiB and preview refused it with 413 while apply would have taken it. Preview runs first, so the file simply could not be applied. Twice the content limit is not a margin chosen for comfort, it is the ceiling the grammar allows. JSON escaping doubles `"` and `\`, and YAML 1.2 admits no raw C0 control character in content except tab and newline, which also escape to two bytes; every other byte survives encoding unchanged. No valid document within the content limit can fail to fit. The slack on top is the envelope's own bytes. Doubling the content alone is short by the field names and braces around it: a document of nothing but quotes encodes to exactly twice its size, and the wrapper then pushes it over. That is how the tests found it. The limit callers meet is unchanged — 6 MiB of YAML on every route, still checked against the decoded field. Only the transport allowance moved.
|
Thanks, merging as-is. Keeping 6 MiB. The parse cost is worth its own PR rather than a smaller cap. It's the two or three parses per change you pointed at, so it's more than moving a number, and a lower import cap would only hide it on one path. |




Radar already applied multi-document YAML, but only from text pasted into the editor. This adds a file picker and a drop target that feed that same editor, plus the request bound the issue asked for.
What changed
An Import YAML file button beside a short hint, and the editor surface itself as the drop target — no extra panel to discover.
The file becomes editor text and nothing else. Client validation, schema loading, dry-run review, conflict handling and apply are untouched, so a multi-document file behaves exactly as a pasted one already did. There is no second apply path.
.yaml/.yml, up to 6 MiBThe extension, not the MIME type. Browsers report nothing dependable for YAML: macOS Chrome commonly sends
"", Windows sends assortedoctet-streamvariants. A.yamlreported asapplication/octet-streamis accepted; a.txtclaimingtext/yamlis refused.The drag overlay promises only what a drag can know.
dragoverexposes an item'skindand the item count but withholds file names, so it highlights for any file drag and calls out a multi-file one, leaving the extension check for the drop. Detection readsdataTransfer.typesrather thanitems, because Safari keepsitemsempty for the whole drag — the overlay never appeared there otherwise.Replacing the editor asks first only when there is work to lose. The dialog opens prefilled in both entry points — a kind skeleton from
ResourcesView, duplicate YAML fromWorkloadView— so a non-empty editor is the normal state, not a sign the user typed anything. The confirmation is gated on the content differing from what the dialog itself put there: dropping onto an untouched skeleton, or into an empty editor, loads with no prompt.Server bounds
handleApplyResourceread its body with an unboundedio.ReadAll. The limit is now one number — 6 MiB of YAML — enforced on every route, and stated on each in the terms that route actually carries:yamlfield. It never bounded the document at all before; its envelope cap only refused large files as a side effect,so the point it began failing moved with however much escaping a file happened to need.
"and\, and YAML 1.2 admits no raw C0 control character in content except tab and newline, which also double. A ConfigMap holding minified JSON is close to half quotes, so a smaller envelope strands real files.Oversize returns 413, not 400: the cap fires before anything parses the body, so a 400 would blame content nobody has read yet. The convention is recorded alongside the existing status codes.
Correcting myself on the document cap
I said on the issue that capping documents on apply "would reject content that pastes fine today." That was wrong.
YAMLReview: trueis hardcoded, so preview always runs first and already caps at 100 — nothing pasted in the UI is affected, only direct API callers. That is why it is included here rather than left open.Verification
Frontend: 3,668 tests, 23 covering the import module — extension over MIME, empty, oversize, unreadable, multi-file, multi-document passthrough, drag classification including Safari's shape, and confirm gating. Backend: handler tests for the 413 and the document cap, plus tests pinning apply's limits equal to preview's.
Against a live kind cluster: a dropped two-document file moves the editor status bar from "1 cluster schema active" to "2", and Review returns "2 accepted · 0 rejected · 0 not previewable" with admission-injected fields diffed — the existing pipeline, unchanged. A 7 MiB file is refused in 3 ms with the editor untouched.
Known limitation
Loading a large file blocks the main thread while the editor parses the buffer — in Chrome roughly 90 ms at 128 KiB, 460 ms at 1 MiB, 2.4 s at 5.5 MiB. The import covers that by naming the file it is loading, painted before the editor starts and retired only once frames arrive on time again, so the status describes when the editor is usable rather than when the content was handed over.
Safari is materially slower, and at multi-megabyte sizes the pause is long enough to be a real problem. The cost is
YamlEditorparsing the whole buffer two to three times per change (parseFallbackYamlDiagnostics→parseYamlDocumentIdentities, plusparseAllDocuments), which affects paste and the resource YAML editor equally — it is not introduced here. Happy to lower the import cap to whatever keeps the worst case tolerable, or open a separate issue for the parse cost, whichever you prefer.There is also a ceiling above Radar's. The Kubernetes API server refuses request bodies over 3 MiB, and etcd's practical per-object limit is lower still — and preview's dry-run goes through the apiserver like any other write. So 6 MiB describes a file, not an object: a multi-document bundle reaches it fine, while a single object approaching it is refused by the cluster whatever Radar allows. That error surfaces unchanged rather than being masked.
Type of change
How has this been tested?
Related issues
Closes #1580.
Note
Medium Risk
Changes cluster YAML apply/preview HTTP handlers (body limits, status codes, document cap) that direct API callers may hit; UI import only affects the create dialog but loads full file content into the editor on the main thread.
Overview
Adds Import YAML file and drag-and-drop on the create-resource dialog so multi-document manifests land in the same editor and flow through the existing preview/apply pipeline—no new apply path. Client checks enforce one
.yaml/.ymlup to 6 MiB (extension-based), with a replace confirmation only when the buffer differs from the dialog’s initial skeleton, plus loading UI while the editor ingests large text.On the server, apply and preview now cap request bodies before read (
readBoundedTextBody/ bounded JSON decode) and return 413 for oversize payloads instead of mislabeling size as malformed 400. Shared constants align 6 MiB YAML content, a preview envelope sized for worst-case JSON escaping, and a 100-document cap on apply (matching preview). Apply validates document count andreviewedVersionsindices before opening a cluster client. CLAUDE.md documents the 413 convention.Reviewed by Cursor Bugbot for commit 197e57a. Bugbot is set up for automated code reviews on this repo. Configure here.