Skip to content

Add read-only Kubernetes cluster mirror mode with filtering and 3D layered layout#35

Open
Denis112345 wants to merge 1 commit into
rohitg00:mainfrom
Denis112345:feature/real-k8s-cluster-view
Open

Add read-only Kubernetes cluster mirror mode with filtering and 3D layered layout#35
Denis112345 wants to merge 1 commit into
rohitg00:mainfrom
Denis112345:feature/real-k8s-cluster-view

Conversation

@Denis112345

@Denis112345 Denis112345 commented May 26, 2026

Copy link
Copy Markdown

This PR adds a new Mirror Cluster mode that lets users import a real Kubernetes cluster snapshot into K8s Games and explore it visually without mutating the cluster model.

The import currently supports kubectl ... -o json snapshots, including large JSON dumps and UTF-16/BOM encoded files. Once imported, the mirrored cluster is locked as read-only: create, edit, delete, scale, drain, cordon, rollout, palette actions, context-menu mutations, and simulated kubectl mutation commands are blocked.

What changed

Added a new Mirror Cluster entry in the main menu.
Added a Mirror toolbar action for importing/replacing a snapshot.
Added read-only enforcement at the cluster state, game engine, command bar, context menu, inspector, palette, and simulation layers.
Added support for importing real Kubernetes objects while preserving metadata, spec, status, UID, namespace, labels, annotations, owner references, and finalizers.
Added object filtering by namespace, kind, status, and name.
Added visibility-aware connection rendering.
Improved auto-layout for large clusters with compact 3D layered placement instead of one long flat row.
Expanded camera range and added fit-to-visible-resources behavior.
Added UTF-16/BOM snapshot file handling.
Why
Large real Kubernetes clusters are hard to inspect in the current flat layout: objects spread too far horizontally, and camera limits make navigation difficult. This change makes real cluster snapshots usable for visual inspection while keeping them safe from accidental edits.

Testing

Ran JS syntax checks for modified modules.
Verified the index.html module script parses successfully.
Tested import with a real Kubernetes dump containing 706 objects.
Verified imported mirror mode is read-only and write attempts are blocke

Summary by CodeRabbit

Release Notes

  • New Features

    • Mirror Cluster mode: Import and view Kubernetes JSON configurations as read-only snapshots for safe exploration.
    • Rendered Objects panel: Filter resources by namespace, status, and kind with integrated search and fit-to-view functionality.
  • Improvements

    • Enhanced auto-layout algorithm with namespace-aware resource placement.
    • Improved camera controls with better framing and visibility management for filtered resources.
    • Read-only state prevents accidental modifications (deletions, edits, scaling) when mirroring clusters.

Review Change Stack

@vercel

vercel Bot commented May 26, 2026

Copy link
Copy Markdown

@Denis112345 is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces two major features: a Mirror Cluster read-only mode for importing Kubernetes snapshots and a Rendered Objects filter panel with live search and visibility control. Read-only enforcement spans state management, game engine commands, UI components, and input handlers. The renderer gains visibility control and smart camera framing. Auto-layout logic is refactored to respect namespaces.

Changes

Mirror Cluster Mode and Rendered Objects Filtering

Layer / File(s) Summary
Read-only Mode State and Gating
js/engine/ClusterState.js
ClusterState adds readOnly, readOnlyReason, mirrorInfo fields and public setReadOnly()/isReadOnly() methods. _canWrite() guards all mutations (add/update/remove/clear) and emits cluster:write-blocked when writes are disallowed. _initDefaultNamespaces() accepts options to control re-initialization.
Kubernetes Object Ingestion and Snapshot Import
js/engine/ClusterState.js
addResource() extracts richer Kubernetes metadata (labels, annotations, ownerReferences, finalizers, uid, timestamps, resourceVersion), handles cluster-scoped kinds, deep-copies spec/status, and derives status.phase. New importSnapshot() orchestrates clear, normalize, batch-import as read-only, and emits cluster:imported. Support helpers normalize objects and compute phases across kinds.
Game Engine Command Blocking and Read-only Enforcement
js/engine/GameEngine.js, js/engine/SimulationTick.js
GameEngine adds MIRROR mode, listens for cluster:write-blocked to warn, and blocks mutating commands in _executeCommand() when read-only via _isMutatingCommand() classifier. reset() clears with { force: true } and disables read-only. SimulationTick checks read-only and early-exits with a non-ticking event.
Command Bar and UI Command Blocking
js/ui/CommandBar.js
_dispatch() checks cluster read-only state and blocks mutating commands (scale, delete, apply, create, run, label, drain, cordon/uncordon, with rollout subcommand handling) by emitting cluster:write-blocked and returning Forbidden. Centralized _isReadOnlyBlockedCommand() determines what is disallowed.
Context Menu, Inspector, and CSS Styling for Read-only
js/ui/ContextMenu.js, js/ui/InspectorPanel.js, style.css
ContextMenu defines mutating events (delete, scale, restart, rollback, cordon/drain), filters them when read-only, and shows a "Read-only mirror" label. InspectorPanel hides Edit button and blocks edits when read-only. CSS adds .palette-item.disabled and .mirror-readonly .palette-item styling (not-allowed cursor, reduced opacity, no hover effects).
Renderer Visibility Control and Camera Framing
js/rendering/ClusterRenderer.js
setVisibleResources(visibleIds) and showAllResources() toggle resource visibility and rebuild pickables. Resources track userData.baseY to preserve Y during animations. getVisibleBounds() computes visible resource bounds; frameAllResources(duration) positions/animates camera around those bounds. resetCamera() now frames visible resources. Scene scale increased (grid, far plane); OrbitControls tuned.
Rendered Objects Filter Panel: UI, State, and Application
index.html
New #render-filter-panel with search, namespace/status selectors, kind checkboxes with counts, and All/None/Fit controls. renderFilters state initialized with search/namespace/status/kinds and debounce timer. bindRenderFilters() wires events; toggleRenderFilterPanel() shows/hides; refreshRenderFilterOptions() regenerates checkboxes. applyRenderFilters() and _resourceMatchesFilters() compute visibility; setVisibleResources() applies it. Large helper block manages binding, refresh, option generation, and namespace plane updates.
Mirror Cluster Dialog and Menu Integration
index.html
"Mirror Cluster" menu button and toolbar button added. startMode('mirror') shows the dialog instead of starting the engine. showMirrorDialog() provides paste/file-load UI. importMirrorSnapshot() normalizes JSON, extracts K8s items, imports as read-only via importSnapshot(), and sets read-only UI. _readSnapshotFile() reads with encoding heuristics; helpers normalize and extract objects.
Write Path Guards in Input Handlers
index.html
Delete/backspace, palette selection, drag-start, click-to-create, canvas drop, selector picker, edit dialog, and scale dialog all check read-only and emit write-blocked notifications or early-return instead of executing. placeResource() and ingress picker similarly guarded.
Namespace-aware Auto-layout Algorithm
index.html
autoAlignResources() replaced with layered placement: groups kinds into render layers (Namespace, Service, Pod, Deployment, etc.), arranges within namespace grids, places cluster-scoped resources below, and assigns fallback positions. Post-layout frames resources and updates namespace plane bounds.
Cluster Lifecycle Events and Filter Integration
index.html
cluster:added, cluster:deleted trigger render-filter refresh. New listeners for cluster:readonly-changed (update badge) and cluster:imported (refresh and auto-align). Events ensure newly added/imported resources appear in filter state and visibility.
Connection Rendering with Visibility Filtering
index.html
syncRenderState() checks visibility when creating connections so links are only rendered between visible resources. Service-to-pod and service-to-deployment links skip filtered-out pods/deployments, reducing visual clutter.
Tutorial Mode and Documentation Updates
index.html
Tutorial goal text and name mapping extended for mirror mode. New tutorial rules describe mirror import, read-only behavior, and write-action blocking.
Return-to-Menu Cleanup and Reset
index.html
returnToMenu() clears/nullifies render-filter timer, resets read-only UI, hides filter panel, then tears down game UI. Ensures clean state when exiting mirror mode.
Filter Panel Toolbar Button Wiring
index.html
btn-filter-resources click handler toggles filter panel. Camera reset changed to frameAllResources(500) for eased framing of visible resources.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UILayer as Index.html<br/>(UI Events)
  participant ClusterState
  participant ClusterRenderer
  participant FilterSystem
  
  User->>UILayer: Click "Mirror Cluster"
  UILayer->>UILayer: Show mirror dialog
  User->>UILayer: Paste/upload K8s JSON
  UILayer->>ClusterState: importSnapshot(objects, {mirrorInfo})
  ClusterState->>ClusterState: Normalize objects
  ClusterState->>ClusterState: Clear state (skip defaults)
  ClusterState->>ClusterState: addResource() for each K8s object
  ClusterState->>ClusterState: setReadOnly(true)
  ClusterState->>UILayer: Emit cluster:imported
  UILayer->>FilterSystem: refreshRenderFilterOptions()
  UILayer->>UILayer: autoAlignResources()
  UILayer->>ClusterRenderer: frameAllResources()
  ClusterRenderer->>ClusterRenderer: Compute visible bounds
  ClusterRenderer->>ClusterRenderer: Animate camera to frame
  
  User->>UILayer: Interact with resources (read-only)
  UILayer->>ClusterState: isReadOnly()
  ClusterState-->>UILayer: true
  UILayer->>UILayer: Block action, emit write-blocked
  UILayer->>User: Show notification (action denied)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A mirror reflects what is, not what might be—
Read-only clusters viewed in harmony.
Filter and frame the dance of your pods,
Where snapshots peek through like magical clods!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changes: adding a read-only mirror mode for Kubernetes clusters with filtering and improved 3D layout, which aligns with the core functionality across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
js/rendering/ClusterRenderer.js (1)

781-794: ⚡ Quick win

Prevent overlapping camera frame animations

frameAllResources() starts a new RAF animation each call but does not cancel an in-flight one. Rapid Fit/filter actions can cause competing camera tweens and jitter. Track and cancel the previous frame animation before starting a new one.

Proposed fix
+        if (this._frameCameraRafId) {
+            cancelAnimationFrame(this._frameCameraRafId);
+            this._frameCameraRafId = null;
+        }
         const startTime = performance.now();
         const startCamera = this.camera.position.clone();
         const startTarget = this.controls.target.clone();
         const animate = () => {
             const elapsed = performance.now() - startTime;
@@
             this.camera.position.lerpVectors(startCamera, targetPosition, ease);
             this.controls.target.lerpVectors(startTarget, center, ease);
             this.controls.update();
-            if (t < 1) requestAnimationFrame(animate);
+            if (t < 1) {
+                this._frameCameraRafId = requestAnimationFrame(animate);
+            } else {
+                this._frameCameraRafId = null;
+            }
         };
-        requestAnimationFrame(animate);
+        this._frameCameraRafId = requestAnimationFrame(animate);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/rendering/ClusterRenderer.js` around lines 781 - 794, frameAllResources
starts a new requestAnimationFrame loop each call and doesn't cancel any
previous one, causing overlapping camera tweens and jitter; modify
frameAllResources to store the RAF id (e.g., this._frameAnimationId) before
calling requestAnimationFrame, call cancelAnimationFrame(this._frameAnimationId)
if set to cancel any in-flight animation, assign the returned id when starting
the new animate, and clear this._frameAnimationId (set to null) when the
animation completes (when t >= 1) so subsequent calls won't try to cancel a
finished handle.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/engine/ClusterState.js`:
- Around line 895-899: Imported Job completion uses the string "Succeeded" but
elsewhere in ClusterState.js completed Jobs are marked "Complete"; update the
Job handling branch (the obj.kind === 'Job' block) to return 'Complete' when
obj.status?.succeeded is truthy (leaving the failed, active/Running, and Pending
branches unchanged) so imported snapshots use the same "Complete" status as the
rest of the engine.

In `@js/rendering/ClusterRenderer.js`:
- Line 293: The drag persistence currently records the group's animated Y
(bobbing) by calling onResourceMoved(rid, { x: group.position.x, y:
group.position.y, z: group.position.z }); change it to use the baseline Y stored
in group.userData.baseY (falling back to group.position.y when baseY is missing)
so the call becomes onResourceMoved(rid, { x: group.position.x, y:
(group.userData && group.userData.baseY) ?? group.position.y, z:
group.position.z }); to prevent gradual vertical drift after repeated drags.

In `@js/ui/InspectorPanel.js`:
- Around line 127-128: Header readOnly state is only sampled in _renderHeader(),
so toggling mirror/read-only mode doesn't re-render the header; subscribe to the
cluster change event and trigger a header re-render: in the InspectorPanel class
(e.g., in activateListeners or constructor) add a listener on
window.game?.cluster (use the cluster's event, e.g., 'change' or the specific
mirror/readOnly event if available) that calls this._renderHeader() or
this.render(), and remove that listener on teardown (e.g., in close/destroy) to
avoid leaks; keep the existing editableKinds/kind logic but ensure the header is
re-rendered whenever window.game.cluster.isReadOnly() can change.

---

Nitpick comments:
In `@js/rendering/ClusterRenderer.js`:
- Around line 781-794: frameAllResources starts a new requestAnimationFrame loop
each call and doesn't cancel any previous one, causing overlapping camera tweens
and jitter; modify frameAllResources to store the RAF id (e.g.,
this._frameAnimationId) before calling requestAnimationFrame, call
cancelAnimationFrame(this._frameAnimationId) if set to cancel any in-flight
animation, assign the returned id when starting the new animate, and clear
this._frameAnimationId (set to null) when the animation completes (when t >= 1)
so subsequent calls won't try to cancel a finished handle.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 616e101b-26a7-407c-95d3-201481168958

📥 Commits

Reviewing files that changed from the base of the PR and between af5dbc1 and ae752d7.

📒 Files selected for processing (9)
  • index.html
  • js/engine/ClusterState.js
  • js/engine/GameEngine.js
  • js/engine/SimulationTick.js
  • js/rendering/ClusterRenderer.js
  • js/ui/CommandBar.js
  • js/ui/ContextMenu.js
  • js/ui/InspectorPanel.js
  • style.css

Comment thread js/engine/ClusterState.js
Comment on lines +895 to +899
if (obj.kind === 'Job') {
if (obj.status?.failed) return 'Failed';
if (obj.status?.succeeded) return 'Succeeded';
return obj.status?.active ? 'Running' : 'Pending';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep imported Job completion status aligned with the rest of the engine.

Completed Jobs are marked as Complete elsewhere in this class, but mirror imports derive Succeeded instead. That makes imported snapshots diverge from in-app Job status handling and can misclassify finished Jobs in status-based UI.

Suggested fix
-      if (obj.status?.succeeded) return 'Succeeded';
+      if (obj.status?.succeeded) return 'Complete';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/engine/ClusterState.js` around lines 895 - 899, Imported Job completion
uses the string "Succeeded" but elsewhere in ClusterState.js completed Jobs are
marked "Complete"; update the Job handling branch (the obj.kind === 'Job' block)
to return 'Complete' when obj.status?.succeeded is truthy (leaving the failed,
active/Running, and Pending branches unchanged) so imported snapshots use the
same "Complete" status as the rest of the engine.

const group = this.resourceMeshes.get(rid);
if (group && this.onResourceMoved) {
this.onResourceMoved(rid, { x: group.position.x, y: 0, z: group.position.z });
this.onResourceMoved(rid, { x: group.position.x, y: group.position.y, z: group.position.z });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use baseline Y when persisting drag result

Line 293 should not persist the animated Y offset for bobbing resources. Emit group.userData.baseY (fallback to group.position.y) to avoid gradual vertical drift after repeated drags.

Proposed fix
-                this.onResourceMoved(rid, { x: group.position.x, y: group.position.y, z: group.position.z });
+                this.onResourceMoved(rid, {
+                    x: group.position.x,
+                    y: group.userData.baseY ?? group.position.y,
+                    z: group.position.z
+                });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.onResourceMoved(rid, { x: group.position.x, y: group.position.y, z: group.position.z });
this.onResourceMoved(rid, {
x: group.position.x,
y: group.userData.baseY ?? group.position.y,
z: group.position.z
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/rendering/ClusterRenderer.js` at line 293, The drag persistence currently
records the group's animated Y (bobbing) by calling onResourceMoved(rid, { x:
group.position.x, y: group.position.y, z: group.position.z }); change it to use
the baseline Y stored in group.userData.baseY (falling back to group.position.y
when baseY is missing) so the call becomes onResourceMoved(rid, { x:
group.position.x, y: (group.userData && group.userData.baseY) ??
group.position.y, z: group.position.z }); to prevent gradual vertical drift
after repeated drags.

Comment thread js/ui/InspectorPanel.js
Comment on lines +127 to +128
const readOnly = window.game?.cluster?.isReadOnly?.() || false;
if (editableKinds.includes(kind) && !readOnly) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Refresh the header when read-only mode changes.

readOnly is only sampled during _renderHeader(). If a resource is already selected when mirror mode is entered or exited, the Edit button state stays stale until the selection changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/ui/InspectorPanel.js` around lines 127 - 128, Header readOnly state is
only sampled in _renderHeader(), so toggling mirror/read-only mode doesn't
re-render the header; subscribe to the cluster change event and trigger a header
re-render: in the InspectorPanel class (e.g., in activateListeners or
constructor) add a listener on window.game?.cluster (use the cluster's event,
e.g., 'change' or the specific mirror/readOnly event if available) that calls
this._renderHeader() or this.render(), and remove that listener on teardown
(e.g., in close/destroy) to avoid leaks; keep the existing editableKinds/kind
logic but ensure the header is re-rendered whenever
window.game.cluster.isReadOnly() can change.

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.

1 participant