Skip to content

Conversation

Abhi-MS
Copy link

@Abhi-MS Abhi-MS commented Sep 23, 2025

What kind of change does this PR introduce?

  • Set up vitest for the frontend and achieved >95% coverage.
  • This PR introduces Connection Charts page
    • Each device has an accordion-style list of its connections.
    • Expanding a connection reveals charts for key metrics such as: Traffic, Unicast packets, Errors, and Discards
      (Backend work on extending the poller to collect these metrics automatically is pending, once that is finished charts will update with live SNMP data. Currently only ifspeed is received by the frontend)
    • Added UI controls for:
      - Pagination → navigate through large connection sets.
      - Expand all / Collapse all → quickly open or close the whole list.
      - Download as CSV → export raw connection data for external analysis.
  • Added other improvements such as
    • Added an all zones option for home page
    • Fixed lldp rendering issue
    • Improved fallback ui

Issue Number:

Fixes #338

Snapshots/Videos:

Connection Charts UI

Connection.Charts.mp4

Connection Charts Responsiveness

SwitchMap.PR.5.2.mp4

Connection Charts Download Button

SwitchMap.PR.5.3.mp4

Frontend Test Coverage
SwitchMap PR 5 1

If relevant, did you update the documentation?

Will update docs in the next PR to avoid lengthy PRs

Summary

Introduces connection charts tab, set up frontend testing and overall ui polish.

Does this PR introduce a breaking change?

No

Checklist

CodeRabbit AI Review

  • I have reviewed and addressed all critical issues flagged by CodeRabbit AI
  • I have implemented or provided justification for each non-critical suggestion
  • I have documented my reasoning in the PR comments where CodeRabbit AI suggestions were not implemented

Test Coverage

  • I have written tests for all new changes/features
  • I have verified that test coverage meets or exceeds 95%
  • I have run the test suite locally and all tests pass

Other information

This feature is part of the ongoing GSOC 2025 project

Have you read the contributing guide?

Yes

Summary by CodeRabbit

  • New Features

    • Connection Charts per‑interface with time ranges and CSV export; Sidebar theme toggle; "All zones" device mode; enhanced topology visualization and richer node/edge tooltips.
  • Improvements

    • Better loading/error/no‑data handling, accessibility labels, stricter date‑range validation, conditional chart rendering, new subtle border theme variable.
  • Documentation

    • Trimmed/updated frontend docs, removed stale entries, adjusted source references.
  • Tests

    • Extensive unit tests and test setup added.
  • Chores

    • Frontend CI test job (80% coverage gate), Vitest/Vite tooling, package and ignore/readme updates.

Copy link

coderabbitai bot commented Sep 23, 2025

Walkthrough

Adds frontend testing (Vitest) and CI enforcement, implements ConnectionCharts and wiring into device page, rewrites TopologyChart, extends frontend types and backend GraphQL resolvers, introduces many component/page tests, and updates build/test/config files and docs.

Changes

Cohort / File(s) Summary
CI Workflow
\.github/workflows/pull-request.yml
Adds "Test Frontend" job that sets up Node 22, caches frontend node_modules, runs Vitest with coverage, uploads frontend/coverage/lcov.info to Codecov, and enforces a minimum frontend coverage of 80%.
Backend GraphQL Resolvers
switchmap/server/db/schemas.py
Adds hostname arg to devices field; implements resolve_devices and resolve_deviceMetrics with optional hostname and time-range filtering, deterministic ordering, and NULL timestamp exclusion.
Frontend build & test config
frontend/package.json, frontend/vite.config.ts, frontend/vitest.setup.ts, frontend/tsconfig.json, frontend/postcss.config.mjs, frontend/.gitignore, frontend/README.md
Enable ESM ("type":"module"), add Vitest scripts/devDeps, introduce Vite config and Vitest setup (jsdom, v8 coverage), update tsconfig to include test types/files, change PostCSS export shape, update .gitignore, and simplify README.
New Feature: Connection Charts
frontend/src/app/components/ConnectionCharts.tsx, frontend/src/app/devices/[id]/page.tsx
Adds ConnectionCharts component (tabs, pagination, CSV export, date validation, abortable GraphQL fetch) and mounts it into the device page tab with loading/error handling.
Topology visualization rewrite
frontend/src/app/components/TopologyChart.tsx
Reworks topology mapping to use LLDP/CDP fields, stable edge IDs, HTML-escaped rich tooltips, extra-node support, improved lifecycle/cleanup, theming-aware styling; introduces TopologyChartProps interface.
Device details & charts
frontend/src/app/components/DeviceDetails.tsx, frontend/src/app/components/HistoricalChart.tsx, frontend/src/app/components/DeviceDetails.module.css
Conditional chart rendering with no-data placeholders; HistoricalChart now has a named export (default retained); CSS reformatting only.
Zone dropdown enhancements
frontend/src/app/components/ZoneDropdown.tsx
Fetches tsCreated, adds "All" selection handling, cancellation-safe updates, selection fallback behavior, testing attributes, and displays tsCreated.
Devices overview rendering
frontend/src/app/components/DevicesOverview.tsx
Removes the explicit "No data" row branch so tbody renders rows unconditionally (empty tbody when no rows).
Home & History pages
frontend/src/app/page.tsx, frontend/src/app/history/page.tsx
Home: adds all-zones fetch path (GetAllZonesDevices), dedupes devices by hostname, expanded interface fields, and unified error handling. History: stores all hostnames for suggestions, adds accessibility/test attributes and minor layout tweaks.
Types expansion & docs
frontend/src/app/types/graphql/GetDeviceInterfaces.ts, docs/docs/frontend/types/graphql/GetZoneDevices.md, docs/docs/backend/Server.md, docs/docs/frontend/**/*.md
InterfaceNode gains numeric counters and tsIdle changes to number; lastPolled removed from GetZoneDevices docs; many documentation link/line-reference edits.
Styling & theme vars
frontend/src/app/globals.css
Adds --border-subtle variable and maps --color-border-subtle into theme blocks.
Component refactors (no API change)
frontend/src/app/components/ConnectionDetails.tsx, frontend/src/app/components/Sidebar.tsx
ConnectionDetails layout/styling refactor preserving signature; Sidebar adds ThemeToggle and data-testid="slide-in-sidebar".
Mocks & test data
frontend/src/app/components/__mocks__/*
Adds deviceMocks and chartMocks fixtures used by tests (devices, interfaces, metrics, chart data).
Tests added
frontend/src/app/**/*.spec.tsx
Large suite of Vitest + React Testing Library tests for components, pages, and utilities (ConnectionCharts, DeviceDetails, TopologyChart, ZoneDropdown, History, Home, Sidebar, utils, etc.).
Vite / project test tooling
frontend/vite.config.ts, frontend/vitest.setup.ts
New Vite config and test setup registering globals, jsdom env, coverage provider (v8), CSS mocks, and Next.js hook mocks.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant DevicePage as Device Page
  participant ConnectionCharts as ConnectionCharts
  participant API as GraphQL API

  User->>DevicePage: Open "Connection Charts" tab
  DevicePage->>ConnectionCharts: Render with device prop
  ConnectionCharts->>API: POST GraphQL QUERY(hostname, range)
  API-->>ConnectionCharts: Metrics + interfaces
  alt Valid data
    ConnectionCharts->>ConnectionCharts: Aggregate, paginate, render charts
    User->>ConnectionCharts: Switch tab / range / expand panels
    ConnectionCharts->>ConnectionCharts: Recompute series / validate dates
    User->>ConnectionCharts: Download CSV
    ConnectionCharts->>User: Trigger file download
  else Error
    ConnectionCharts->>User: Show inline error alert
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Home as Home Page
  participant API as GraphQL API

  User->>Home: Load with zoneId
  alt zoneId == "all"
    Home->>API: GetAllZonesDevices
  else Specific zone
    Home->>API: GetZoneDevices(zoneId)
  end
  API-->>Home: Devices (may include duplicates)
  Home->>Home: Deduplicate by hostname
  Home-->>User: Render Topology + DevicesOverview
  alt Fetch/error
    Home->>Home: Build error message
    Home-->>User: Show error UI
  end
Loading
sequenceDiagram
  autonumber
  actor Dev
  participant CI as PR Workflow
  participant Node as Node 22
  participant Codecov as Codecov

  Dev->>CI: Open/Update PR
  CI->>Node: Setup + cache deps
  CI->>Node: Run vitest with coverage
  Node-->>CI: lcov.info
  CI->>Codecov: Upload lcov.info
  CI->>CI: Enforce >=80% frontend coverage
  alt <80%
    CI-->>Dev: Fail job
  else >=80%
    CI-->>Dev: Pass job
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

I nibble keys and hop with cheer,
Charts and tests now gather near. 🐰
LLDP links and CSV delights,
CI hums through many testy nights.
Thump—coverage leaps into flight!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The PR contains several sizable changes that are not strictly required by the linked issue and should be justified or split: large documentation rewrites/removals, a broad TopologyChart rewrite, addition of "type": "module" to frontend/package.json (a potentially breaking change to module resolution), and backend schema/resolver additions; these are beyond the core scope of adding the charts, tests, and CI updates and may increase review surface or introduce unrelated risk. Please either split unrelated large refactors and documentation edits into separate PRs or add explicit justification and testing evidence in this PR for each non-core change (notably package.json module type, TopologyChart rewrite, and backend resolver/schema edits), and ensure CI/e2e checks cover any runtime impacts before merging.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly and concisely summarizes the primary work in this changeset: adding a Connection Charts page and a large frontend testing effort that achieved >95% coverage; the GSOC tag is contextual but does not obscure the main change and the phrasing is specific and relevant to reviewers scanning history.
Linked Issues Check ✅ Passed The changes implement the linked issue's coding objectives: a ConnectionCharts component/page and tests were added and integrated (ConnectionCharts.tsx and spec), Vitest and test setup/config (vite.config.ts, frontend/vitest.setup.ts, package.json devDependencies) are present, many component/page/util test suites were added, and CI was updated with a frontend test job that runs coverage and enforces a minimum threshold; GraphQL/type additions and supporting fixtures were also introduced to supply the data surface the charts require.
Description Check ✅ Passed The PR description follows the repository template and includes the change summary, issue reference (Fixes #338), high-level motivation, snapshots/coverage evidence, and checklist entries; it also documents that docs updates will follow in a subsequent PR and declares no breaking changes, so the description is largely complete and actionable for reviewers.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

Our Pull Request Approval Process

We have these basic policies to make the approval process smoother for our volunteer team.

Testing Your Code

Please make sure your code passes all tests. Our test code coverage system will fail if either of these two conditions occur:

  1. The overall code coverage drops below the target threshold of the repository
  2. Any file in the pull request has code coverage levels below the repository threshold

The process helps maintain the overall reliability of the code base and is a prerequisite for getting your PR approved. Assigned reviewers regularly review the PR queue and tend to focus on PRs that are passing.

Reviewers

When your PR has been assigned reviewers contact them to get your code reviewed and approved via:

  1. comments in this PR or
  2. our slack channel

Reviewing Your Code

Your reviewer(s) will have the following roles:

  1. arbitrators of future discussions with other contributors about the validity of your changes
  2. point of contact for evaluating the validity of your work
  3. person who verifies matching issues by others that should be closed.
  4. person who gives general guidance in fixing your tests

🎯 Contacting the person who assigned the mentors is not advised unless they make a request. Do not @ the person who did the assignment otherwise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (65)
docs/docs/frontend/utils/time.md (2)

15-15: Avoid line‑anchored links; they drift on edits.

Linking to #L2 is brittle. Prefer a permalink (commit SHA) or drop the line anchor to the file path to prevent doc rot.

-Defined in: [utils/time.ts:2](https://github.com/PalisadoesFoundation/switchmap-ng/blob/develop/frontend/src/app/utils/time.ts#L2)
+Defined in: [utils/time.ts](https://github.com/PalisadoesFoundation/switchmap-ng/blob/develop/frontend/src/app/utils/time.ts)

11-26: Add a one‑line purpose, units, and behavior note.

A brief description improves self‑containment: state input units (hundredths of a second) and flooring behavior. Consider an example.

 ### formatUptime()
 
 > **formatUptime**(`hundredths`): `string`
 
+Converts device uptime from hundredths of a second to a human‑readable "Xd Xh Xm Xs" string. Values are floored to whole seconds.
+
+Example: `formatUptime(9006100) -> "1d 1h 1m 1s"`
+
 #### Parameters
@@
 `string`
frontend/src/app/utils/time.spec.tsx (2)

21-24: Add edge/boundary tests to lock flooring semantics and minute/hour rollovers.

These help prevent regressions if rounding logic changes later.

   it("formats days correctly", () => {
     expect(formatUptime(9006100)).toBe("1d 1h 1m 1s"); // 9006100 hundredths = 1d 1h 1m 1s
   });
+
+  it("floors fractional seconds (hundredths) as implemented", () => {
+    expect(formatUptime(150)).toBe("0d 0h 0m 1s"); // 1.5s -> floors to 1s
+    expect(formatUptime(199)).toBe("0d 0h 0m 1s"); // 1.99s -> floors to 1s
+  });
+
+  it("handles 59s -> 1m transition", () => {
+    expect(formatUptime(5900)).toBe("0d 0h 0m 59s");
+    expect(formatUptime(6000)).toBe("0d 0h 1m 0s");
+  });
+
+  it("handles 3599s -> 1h transition", () => {
+    expect(formatUptime(359900)).toBe("0d 0h 59m 59s");
+    expect(formatUptime(360000)).toBe("0d 1h 0m 0s");
+  });
 });

1-1: Use .spec.ts for non‑JSX tests (consistency).

This file doesn’t use JSX; renaming to .spec.ts aligns with common conventions and can speed up TSX transforms slightly.

You can rename frontend/src/app/utils/time.spec.tsxfrontend/src/app/utils/time.spec.ts.

frontend/.gitignore (8)

7-11: Add Yarn Berry SDKs exception.

Include .yarn/sdks to avoid ignoring editor SDKs when using Yarn PnP.

 .yarn/*
 !.yarn/patches
 !.yarn/plugins
 !.yarn/releases
 !.yarn/versions
+!.yarn/sdks

16-22: Remove duplicated Next.js ignores (consolidate later block).

This block duplicates the more complete Next.js section below. Keep one canonical block to reduce noise.

-# next.js
-/.next/
-/out/
-/.next/
-/out/
-.next/cache

37-40: Avoid blanket .env* ignore; keep example files tracked.

.env* also ignores .env.example; prefer explicit patterns to keep example files versioned.

-# env files (can opt-in for committing if needed)
-.env*
-.env.*.local
+# env files (explicit; keep .env.example tracked)
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local

79-81: Remove Svelte-specific ignores if not applicable.

If the frontend is Next.js-only, drop .svelte-kit/ to avoid confusion.

-# Svelte
-.svelte-kit/

95-104: Deduplicate IDE entries.

.vscode/ and .idea/ already appear above. Keep one occurrence.

-# JetBrains IDEs
-*.iml
-*.ipr
-*.iws
-.idea/
-
-# VS Code settings
-.vscode/
-.history/
+# JetBrains IDEs
+*.iml
+*.ipr
+*.iws

105-134: Collapse redundant Next.js ignores.

Since .next/ and out/ are ignored, the nested files/dirs under .next are redundant. Keep the minimal set.

-# Next.js build output
-.next/
-out/
-.next/cache/
-
-# Next.js static export
-out/
-
-# Next.js serverless functions output
-.next/server/
-
-# Next.js telemetry
-.next/telemetry/
-
-# Next.js build manifests
-.next/static/
-
-# Next.js cache
-.next/cache/
-
-# Next.js build traces
-.next/trace
-
-# Next.js build logs
-.next/build-manifest.json
-.next/prerender-manifest.json
-.next/routes-manifest.json
-.next/export-marker.json
-.next/BUILD_ID
+# Next.js build output
+.next/
+out/

138-143: Env files duplicated below or above.

After adopting the explicit env list (see Lines 37-40), keep only one env section to avoid drift.


144-149: Deduplicate npm/yarn/pnpm log ignores.

Already covered in the “# debug” section (Lines 32-35). Keep one copy.

-# npm logs
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
frontend/src/app/history/page.tsx (3)

199-201: Sort hostnames for stable, deterministic suggestions (and tests).

Add a localeCompare sort to prevent flicker and make snapshots stable.

-          setAllDeviceHostnames(
-            Array.from(new Set(devicesWithZones.map((d) => d.hostname)))
-          ); // all devices, unfiltered
+          setAllDeviceHostnames(
+            Array.from(new Set(devicesWithZones.map((d) => d.hostname))).sort(
+              (a, b) => a.localeCompare(b)
+            )
+          ); // all devices, unfiltered & sorted

319-319: Avoid nested scrollbars (double overflow).

Both the outer container (this line) and the inner content (Line 321) have overflow-y-auto, which can cause double scrollbars. Drop it on the outer container.

-      <div className="flex h-screen overflow-y-auto">
+      <div className="flex h-screen">

333-335: Use role="search" and add an accessible name for the landmark.

Form already implies role=form; search is a better landmark.

-              role="form"
-              data-testid="search-form"
+              role="search"
+              aria-label="Device search"
+              data-testid="search-form"
docs/docs/frontend/utils/stringUtils.md (1)

17-38: Document defaults and clarify ellipsis behavior.

Please note in the docs:

  • Defaults: lines=2, maxLength=50
  • If str is falsy => "N/A"
  • If str.length <= maxLength => returns original string (no ellipsis)
  • Ellipsis "..." is appended when truncated and is not counted toward maxLength
frontend/src/app/utils/stringUtils.spec.tsx (2)

29-34: Add edge-case tests for invalid options (lines<=0, maxLength<=0).

Current implementation can produce odd output (e.g., empty lines then "...") when lines > maxLength or lines<=0. Recommend guarding in the util and adding tests like below.

Apply this diff to extend tests (insert before the closing brace):

@@
   it("respects the maxLength option", () => {
     const str = "abcdefghijklmnopqrstuvwxyz";
     const result = truncateLines(str, { maxLength: 12, lines: 2 });
     expect(result).toBe("abcdef\nghijkl...");
   });
+
+  it("handles lines=0 by falling back to simple truncation", () => {
+    const str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    const result = truncateLines(str, { lines: 0 });
+    expect(result.endsWith("...")).toBe(true);
+    expect(result.includes("\n")).toBe(false);
+  });
+
+  it("handles maxLength=0 by returning only an ellipsis", () => {
+    const result = truncateLines("abc", { maxLength: 0 });
+    expect(result).toBe("...");
+  });

And update the util accordingly:

-  const { lines = 2, maxLength = 50 } = options ?? {};
+  const { lines = 2, maxLength = 50 } = options ?? {};
+  const safeLines = Math.floor(lines);
+  const safeMaxLen = Math.floor(maxLength);
 
   if (!str) return "N/A";
-  if (str.length <= maxLength) return str;
+  if (safeLines <= 0 || safeMaxLen <= 0) return str.slice(0, Math.max(0, safeMaxLen)) + "...";
+  if (str.length <= safeMaxLen) return str;
 
-  const segmentLength = Math.floor(maxLength / lines);
+  const segmentLength = Math.floor(safeMaxLen / safeLines);
+  if (segmentLength <= 0) return str.slice(0, safeMaxLen) + "...";
 
   let result = "";
-  for (let i = 0; i < lines; i++) {
-    const start = i * segmentLength;
+  for (let i = 0; i < safeLines; i++) {
+    const start = i * segmentLength;
     const end = start + segmentLength;
     result += str.slice(start, end);
-    if (i < lines - 1) result += "\n";
+    if (i < safeLines - 1) result += "\n";
   }

1-1: Consider .spec.ts instead of .spec.tsx.

The file doesn’t use JSX; renaming to .spec.ts can speed up tooling marginally and align with conventions.

frontend/src/app/components/DevicesOverview.tsx (3)

63-70: Guard against undefined edges to avoid runtime crashes

If l1interfaces or edges is absent, this will throw. Add nullish guards with a safe fallback.

-      const interfaces = device.l1interfaces.edges.map(
-        (e: InterfaceEdge) => e.node
-      );
+      const interfaces =
+        device.l1interfaces?.edges?.map((e: InterfaceEdge) => e.node) ?? [];

42-48: Align row type with data shape and use stable row IDs

You return id in data but it’s not declared on DeviceRow. Declare it and wire getRowId for stable row keys.

 interface DeviceRow {
+  id: string;
   name: string;
   hostname: string;
   ports: string;
   uptime: string;
   link: string;
 }
   const table = useReactTable({
     data,
     columns,
     state: {
       sorting,
       globalFilter,
     },
     onSortingChange: setSorting,
     onGlobalFilterChange: setGlobalFilter,
     getCoreRowModel: getCoreRowModel(),
     getSortedRowModel: getSortedRowModel(),
     getFilteredRowModel: getFilteredRowModel(),
+    getRowId: (row) => row.id,
   });

Also applies to: 110-123


134-139: Add accessible name to the search input

Provide an aria-label so screen readers can identify the control.

       <input
         value={globalFilter}
         onChange={(e) => setGlobalFilter(e.target.value)}
         placeholder="Search..."
+        aria-label="Search devices"
         className="mb-4 p-2 border rounded w-full max-w-sm"
       />
frontend/vitest.setup.ts (1)

6-26: Harden ResizeObserver polyfill typings and assignment

Match DOM signatures to reduce TS friction and assign via globalThis for broader compatibility.

-class ResizeObserver {
-  callback: ResizeObserverCallback;
-  constructor(callback: ResizeObserverCallback) {
-    this.callback = callback;
-  }
-  observe() {
-    // no-op
-  }
-  unobserve() {
-    // no-op
-  }
-  disconnect() {
-    // no-op
-  }
-}
-
-(global as any).ResizeObserver = ResizeObserver;
+class ResizeObserver {
+  private callback: ResizeObserverCallback;
+  constructor(callback: ResizeObserverCallback) {
+    this.callback = callback;
+  }
+  observe(_target: Element, _options?: ResizeObserverOptions) {
+    // no-op
+  }
+  unobserve(_target: Element) {
+    // no-op
+  }
+  disconnect() {
+    // no-op
+  }
+}
+
+// Assign in a cross-environment safe way
+// @ts-ignore
+globalThis.ResizeObserver = ResizeObserver;
+// @ts-ignore
+if (typeof window !== 'undefined') window.ResizeObserver = ResizeObserver as any;
frontend/tsconfig.json (1)

19-22: Consider isolating Vitest globals to a test tsconfig

Adding vitest/globals to the main tsconfig leaks test globals into app code. Prefer a separate tsconfig (e.g., tsconfig.vitest.json) that extends this file and adds the Vitest types, referenced by your Vitest config.

Would you like a follow-up diff to introduce tsconfig.vitest.json and update vite/vitest config accordingly?

switchmap/server/db/schemas.py (1)

210-231: Consider addressing the unused kwargs parameter.

The new resolve_devices resolver correctly implements hostname-based device filtering with proper encoding. The implementation follows GraphQL best practices and matches the existing resolve_deviceMetrics pattern.

However, the static analysis tool identified that the kwargs parameter is unused. While this may be intentional for future extensibility, consider one of these approaches:

Apply this fix to address the unused parameter warning:

-    def resolve_devices(root, info, hostname=None, **kwargs):
+    def resolve_devices(root, info, hostname=None, **_kwargs):

Alternatively, if the parameter serves no purpose, it could be removed entirely:

-    def resolve_devices(root, info, hostname=None, **kwargs):
+    def resolve_devices(root, info, hostname=None):
frontend/src/app/components/ConnectionCharts.tsx (1)

41-62: Consider parameterizing the GraphQL query and endpoint.

The hardcoded GraphQL query and endpoint configuration could be improved for maintainability and testability:

  1. Extract the GraphQL query: Consider moving the query to a separate file or constant:
// queries/deviceQueries.ts
export const GET_DEVICE_INTERFACES_QUERY = `
{
  devices(hostname: $hostname) {
    // ... query fields
  }
}
`;
  1. Consider using a GraphQL client: For better error handling and caching, consider using a proper GraphQL client like Apollo Client or urql instead of raw fetch calls.

  2. Extract endpoint configuration: The endpoint fallback could be centralized:

const GRAPHQL_ENDPOINT = process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || "http://localhost:7000/switchmap/api/graphql";
frontend/package.json (1)

11-13: Align coverage script with CI needs (emit lcov).

Your CI expects lcov.info. Ensure Vitest outputs lcov via script to avoid Codecov failures.

Apply:

-    "test": "vitest run",
+    "test": "vitest run",
     "test:watch": "vitest --ui",
-    "test:coverage": "vitest run --coverage"
+    "test:coverage": "vitest run --coverage --coverage.reporter=lcov --coverage.reportsDirectory=coverage"
.github/workflows/pull-request.yml (4)

262-269: Use the dedicated coverage script to guarantee lcov output.

CI currently runs npm run test -- --coverage, but later steps assume lcov at frontend/coverage/lcov.info. Call the script that enforces lcov.

-      - name: Run frontend tests with coverage
-        working-directory: frontend
-        run: npm run test -- --coverage
+      - name: Run frontend tests with coverage (lcov)
+        working-directory: frontend
+        run: npm run test:coverage

If you prefer flags over scripts, configure Vitest coverage reporter to include "lcov" in vitest.config.


272-278: Codecov token handling for forked PRs.

Tokens aren’t available from forks; this may fail PRs. Prefer omitting token on public repos or conditionally gating the step.

Option A (remove token for public repo):

-          token: ${{ secrets.CODECOV_TOKEN }}
           files: frontend/coverage/lcov.info
           fail_ci_if_error: true
           verbose: true

Option B (skip on forks):

-      - name: Upload coverage to Codecov
+      - name: Upload coverage to Codecov
+        if: ${{ github.event.pull_request.head.repo.fork == false }}
         uses: codecov/codecov-action@v5

263-265: Prefer reproducible installs.

Use npm ci for deterministic, faster CI installs.

-        run: npm install
+        run: npm ci

317-323: Target branch gate will fail this PR.

This job hard-fails unless base is develop. Current PR targets develop-abhi. Adjust list or add a bypass label.

Suggestion:

  • Allowlist both branches:
-        if: ${{ github.event.pull_request.base.ref != 'develop' }}
+        if: ${{ github.event.pull_request.base.ref != 'develop' && github.event.pull_request.base.ref != 'develop-abhi' }}

Or gate behind a label (similar to your sensitive-files bypass).

frontend/src/app/history/page.spec.tsx (4)

143-164: Duplicate test for 180‑day custom range.

Both cases assert the same rule; keep one to reduce noise.

Remove one of the two “custom range cannot exceed 180 days” tests or consolidate into a table-driven case.

Also applies to: 275-293


7-41: Tighten fetch mocking and teardown to avoid cross‑test leaks.

Resetting mocks doesn’t restore globals. Capture and restore original fetch.

+const originalFetch = global.fetch;
 beforeEach(() => {
   global.fetch = vi.fn().mockResolvedValue({
@@
 });
 
 afterEach(() => {
   vi.resetAllMocks();
+  global.fetch = originalFetch as any;
 });

236-240: Restore window.location after stubbing reload.

Define/restore to prevent bleed into other tests.

+const originalLocation = window.location;
@@
   Object.defineProperty(window, "location", {
     configurable: true,
     value: { ...window.location, reload: reloadMock },
   });
@@
   expect(reloadMock).toHaveBeenCalled();
+  Object.defineProperty(window, "location", {
+    configurable: true,
+    value: originalLocation,
+  });

Alternatively, use vi.spyOn(window.location, "reload") if your test environment supports it.

Also applies to: 249-250


18-22: Consider awaiting UI that appears after effects.

Theme/components that render after useEffect can be flaky with getBy*. Prefer findBy* in those cases.

No action required if stable locally/CI; otherwise switch to findBy* where appropriate.

Also applies to: 24-29

frontend/src/app/components/ZoneDropdown.tsx (2)

83-89: Type safety: avoid relying on GetZoneDevices types for a different query shape.

events → zones edges may not align with GetZoneDevices.ZoneEdge. Define a local type for this query.

-        const rawZones =
-          json?.data?.events?.edges?.[0]?.node?.zones?.edges?.map(
-            (edge: ZoneEdge) => edge.node
-          ) ?? [];
+        type ZonesEdge = { node: { id: string; idxZone: string; name: string; tsCreated: string } };
+        const rawZones =
+          (json?.data?.events?.edges?.[0]?.node?.zones?.edges as ZonesEdge[] | undefined)?.map(
+            (edge) => edge.node
+          ) ?? [];

121-124: Minor UX: format tsCreated for display.

Raw timestamps can be unreadable. Format or hide when empty.

-        {selected?.tsCreated || ""}
+        {selected?.tsCreated ? new Date(selected.tsCreated).toLocaleString() : ""}

Also applies to: 167-169

frontend/src/app/theme-toggle.spec.tsx (1)

18-22: Stabilize tests by waiting for post‑effect render.

ThemeToggle renders after useEffect. Use findByRole and clear mocks between tests.

-    const button = screen.getByRole("button", { name: /toggle theme/i });
+    const button = await screen.findByRole("button", { name: /toggle theme/i });
@@
-    const button = screen.getByRole("button", { name: /toggle theme/i });
+    const button = await screen.findByRole("button", { name: /toggle theme/i });

Also:

+import { afterEach } from "vitest";
+afterEach(() => setThemeMock.mockClear());

Also applies to: 24-29

frontend/src/app/components/DeviceOverview.spec.tsx (2)

47-59: Strengthen sorting tests with an actual order assertion.

Right now we only fire keys and assert the header exists. Add a second device and assert row order toggles on Enter/Space to actually verify the sorting handler.

Apply this diff to introduce a second device and assert order:

   it("calls sorting handler on Enter and Space key press", () => {
-    render(
-      <DevicesOverview devices={[mockDevice]} loading={false} error={null} />
-    );
+    const another = {
+      ...mockDevice,
+      id: "2",
+      idxDevice: 2,
+      sysName: "A-Device",
+      hostname: "host2",
+    };
+    render(
+      <DevicesOverview devices={[mockDevice, another]} loading={false} error={null} />
+    );
 
     const headerCell = screen.getByRole("button", {
       name: /sort by device name/i,
     });
 
     fireEvent.keyDown(headerCell, { key: "Enter" });
     fireEvent.keyDown(headerCell, { key: " " });
-    expect(headerCell).toBeInTheDocument();
+    const rows = screen.getAllByRole("row").slice(1); // skip header
+    expect(rows[0]).toHaveTextContent("A-Device"); // asc then desc
+    fireEvent.keyDown(headerCell, { key: "Enter" });
+    const rowsDesc = screen.getAllByRole("row").slice(1);
+    expect(rowsDesc[0]).toHaveTextContent("Device 1");
   });

61-72: Also verify click sorting changes order.

Same as keyboard: verify the table order changes after click.

   it("calls sorting handler on mouse click", () => {
-    render(
-      <DevicesOverview devices={[mockDevice]} loading={false} error={null} />
-    );
+    const another = { ...mockDevice, id: "2", idxDevice: 2, sysName: "A-Device", hostname: "host2" };
+    render(
+      <DevicesOverview devices={[mockDevice, another]} loading={false} error={null} />
+    );
 
     const headerCell = screen.getByRole("button", {
       name: /sort by device name/i,
     });
 
     fireEvent.click(headerCell);
-    expect(headerCell).toBeInTheDocument();
+    const rows = screen.getAllByRole("row").slice(1);
+    expect(rows[0]).toHaveTextContent("A-Device");
   });
frontend/src/app/components/Sidebar.tsx (2)

108-114: Add ARIA state to the hamburger for accessibility.

Expose expanded state and control relationship.

       <button
         className="p-3 text-2xl lg:hidden fixed top-4 left-4 z-50 bg-bg border border-border rounded"
         onClick={() => setOpen(true)}
-        aria-label="Open sidebar"
+        aria-label="Open sidebar"
+        aria-expanded={open}
+        aria-controls="slide-in-sidebar"
       >

126-129: Make slide‑in focusable for better keyboard UX.

Auto-focus the panel when opened; enables ESC/Tab handling later if desired.

-          <aside
-            data-testid="slide-in-sidebar"
-            ref={sidebarRef}
+          <aside
+            id="slide-in-sidebar"
+            data-testid="slide-in-sidebar"
+            ref={sidebarRef}
+            tabIndex={-1}
             className="fixed top-0 left-0 w-60 h-full bg-bg border-r border-border z-50 p-4 shadow-md transition-transform transform lg:hidden"
           >

Optionally focus on open:

 useEffect(() => {
   const handleClickOutside = (e: MouseEvent): void => {
@@
   if (open) {
     document.addEventListener("mousedown", handleClickOutside);
+    // focus slide-in when opened
+    setTimeout(() => sidebarRef.current?.focus(), 0);
   }
frontend/src/app/components/HistoricalChart.spec.tsx (1)

2-2: Remove unused import.

fireEvent isn’t used in this file.

-import { fireEvent, render, screen } from "@testing-library/react";
+import { render, screen } from "@testing-library/react";
frontend/src/app/components/TopologyChart.tsx (3)

340-345: Fix edge reset logic: updates target undefined ids.

initialGraph edges don’t have ids; updates no‑op. Reset using DataSet’s actual edge ids.

-      // Reset edges color
-      initialGraph.current.edges.forEach((originalEdge) => {
-        edgesData.current!.update({
-          id: originalEdge.id,
-          color: originalEdge.color || (isDark ? "#444" : "#BBBBBB"), // fallback if color missing
-        });
-      });
+      // Reset edges color using current DataSet ids
+      edgesData.current.get().forEach((edge) => {
+        edgesData.current!.update({
+          id: edge.id as string | number,
+          color: (edge as any).color || (isDark ? "#444" : "#BBBBBB"),
+        });
+      });
@@
-    initialGraph.current.edges.forEach((originalEdge) => {
-      edgesData.current!.update({
-        id: originalEdge.id,
-        color: originalEdge.color || (isDark ? "#444" : "#BBBBBB"), // fallback if color missing
-        arrows: { to: false }, // reset arrow visibility
-      });
-    });
+    edgesData.current.get().forEach((edge) => {
+      edgesData.current!.update({
+        id: edge.id as string | number,
+        color: (edge as any).color || (isDark ? "#444" : "#BBBBBB"),
+        arrows: { to: false },
+      });
+    });

Also applies to: 382-389


77-115: Include clickToUse/zoomView in options dependencies.

Props changes won’t reflect without updating the memo.

-  const options: Options = useMemo(
+  const options: Options = useMemo(
     () => ({
       clickToUse: clickToUse ?? true,
@@
-    }),
-    [isDark]
+    }),
+    [isDark, clickToUse, zoomView]
   );

229-230: Avoid 8‑digit hex; use RGB/RGBA for compatibility.

vis-network may not parse #RRGGBBAA consistently.

-        color: "#383e44ff",
+        color: "rgba(56, 62, 68, 1)",
frontend/src/app/components/Sidebar.spec.tsx (3)

28-38: Scope assertions to the slide‑in sidebar to avoid false positives

getAllByText may match the static (lg) sidebar; assert within data-testid="slide-in-sidebar" after opening.

   fireEvent.click(button);

-  const networkLink = screen.getAllByText(/Network Topology/i)[0];
-  const devicesLink = screen.getAllByText(/Devices Overview/i)[0];
+  const slideIn = screen.getByTestId("slide-in-sidebar");
+  const networkLink = within(slideIn).getByText(/Network Topology/i);
+  const devicesLink = within(slideIn).getByText(/Devices Overview/i);

   expect(networkLink).toBeInTheDocument();
   expect(devicesLink).toBeInTheDocument();

40-48: Fix selector: the slide‑in has data‑test id, not an aria‑label

queryByLabelText("slide-in sidebar") will always be null. Use queryByTestId to actually verify closure.

- expect(screen.queryByLabelText("slide-in sidebar")).toBeNull();
+ expect(screen.queryByTestId("slide-in-sidebar")).toBeNull();

50-60: Deduplicate the “closes sidebar when clicking outside” test

Two tests assert the same behavior. Keep the version using queryByTestId and remove or rename the other to reduce noise.

docs/docs/frontend/devices/[id]/page.md (1)

13-20: Docs mismatch: default() can return null in code

DevicePage returns null when sidebarOpen === null. Update the return type to Element | null.

-> **default**(): `Element`
+> **default**(): `Element` | `null`
@@
-`Element`
+`Element` | `null`
frontend/src/app/components/TopologyChart.spec.tsx (2)

167-204: This test overrides Network.on and no longer exercises component logic

By replacing mockInstance.on with your own impl, you’re testing the test, not the component’s registered handler. Prefer invoking the actual callback captured from the component’s on calls.

Example refactor:

const selectNodeHandler =
  mockInstance.on.mock.calls.find(([e]: [string, Function]) => e === "selectNode")?.[1];
expect(selectNodeHandler).toBeDefined();
selectNodeHandler({ nodes: ["1"] });
expect(mockUpdate).toHaveBeenCalledWith(expect.objectContaining({ id: "1", opacity: 1 }));

24-57: Consider making DataSet.get(id) honor the id for closer parity

Your DataSet mock’s get() ignores the id and returns the full array, which can mask issues in id-based lookups (e.g., doubleClick handler). Optional: make get(id) return the matching item.

docs/docs/frontend/components/ConnectionDetails.md (1)

13-28: Docs don’t match the component signature

Docs show ConnectionDetails({ deviceId?: string }) while code exports ConnectionDetails({ device }: { device: DeviceNode }). Update param docs to device: DeviceNode.

-**ConnectionDetails**(`__namedParameters`): `null` | `Element`
+**ConnectionDetails**(`__namedParameters`): `null` | `Element`
@@
-###### deviceId?
-
-`string`
+###### device
+
+`DeviceNode`
frontend/src/app/components/ZoneDropdown.spec.tsx (2)

15-23: Unnecessary vi.restoreAllMocks() here

You’re manually assigning global.fetch and restoring it in afterEach. restoreAllMocks isn’t needed and can surprise by resetting spies elsewhere.

 beforeEach(() => {
   originalFetch = global.fetch;
   vi.clearAllMocks();
-  vi.restoreAllMocks();
 });

79-94: Resolve the pending fetch using a promise-returning json for consistency

Keep json returning a Promise to mirror real fetch.json() and reduce timing flakiness.

-act(() =>
-  resolveFetch({ ok: true, json: () => ({ data: { events: [] } }) })
-);
+act(() =>
+  resolveFetch({
+    ok: true,
+    json: () => Promise.resolve({ data: { events: [] } }),
+  })
+);
frontend/src/app/components/ConnectionDetails.spec.tsx (1)

84-102: Tighten CDP/LLDP “missing” test to actually clear both identifiers

Currently only lldpremsysname is cleared; CDP id may still render, and other “-” cells can cause false positives. Clear both IDs to validate the intended columns.

   const deviceWithMissingCDP = {
     ...mockDevice,
     l1interfaces: {
       edges: [
         {
           node: {
             ...mockDevice.l1interfaces.edges[0].node,
-            cdpcachedeviceport: "",
-            lldpremsysname: "",
+            cdpcachedeviceid: "",
+            cdpcachedeviceport: "",
+            lldpremsysname: "",
+            lldpremportdesc: "",
           },
         },
       ],
     },
   };

   render(<ConnectionDetails device={deviceWithMissingCDP} />);
-  expect(screen.getByText("-")).toBeInTheDocument();
+  // Expect at least two “-” entries for CDP and LLDP columns
+  expect(screen.getAllByText("-", { exact: true }).length).toBeGreaterThanOrEqual(2);
frontend/src/app/devices/[id]/page.spec.tsx (2)

88-100: Add an assertion for the “Connection Charts” tab

Since the page now includes a Connection Charts tab, add a quick switch-and-render assertion to keep coverage aligned with the new feature.

Example:

fireEvent.click(screen.getByText("Connection Charts"));
await waitFor(() => {
  // e.g., assert placeholder text rendered by ConnectionCharts mock or component
  expect(screen.getByText(/No device data\./i)).not.toBeInTheDocument();
});

12-16: Optionally assert router.replace on tab change

handleTabChange updates the URL’s tab param via router.replace. Spy and assert called to harden behavior.

frontend/src/app/components/DeviceDetails.tsx (2)

122-126: Potential uptime unit mismatch (×100)

formatUptime expects SNMP hundredths. Backend stores sysUpTime in hundredths (see ingest/update/device.py). Multiplying deviceMetrics.uptime by 100 likely overstates uptime. Use the raw value.

-              value={formatUptime(
-                device.sysUptime ?? (deviceMetrics?.uptime ?? 0) * 100
-              )}
+              value={formatUptime(device.sysUptime ?? (deviceMetrics?.uptime ?? 0))}

201-233: Minor DRY: factor timestamp mapping into a helper

lastPolled → ISO conversion is repeated thrice. Extract a small helper to reduce duplication. Optional.

frontend/src/app/components/ConnectionCharts.spec.tsx (2)

6-6: Restore stubbed globals after each test to prevent leakage.

vi.stubGlobal("fetch", ...) and the URL stub should be restored per test to avoid cross-test interference.

Apply this diff:

-import { describe, it, expect, vi, beforeEach, Mock } from "vitest";
+import { describe, it, expect, vi, beforeEach, afterEach, Mock } from "vitest";
@@
 vi.stubGlobal("fetch", vi.fn());
 
 // ---------- Helpers ----------
@@
 describe("ConnectionCharts", () => {
   beforeEach(() => {
@@
   });
+
+  afterEach(() => {
+    vi.restoreAllMocks();
+  });

134-149: Make dropdown querying resilient to label changes.

The test caches the dropdown button initially labeled “Past 1 day” and reuses the same node after label changes. Prefer re-querying by role each time to avoid brittle coupling to text content.

frontend/src/app/page.tsx (4)

98-98: Remove unused parameter retryCount.

retryCount is not used. Drop it to reduce noise.

Apply this diff:

-    const fetchDevices = async (retryCount = 0) => {
+    const fetchDevices = async () => {

146-154: Unify environment variable for GraphQL endpoint across the app.

This file uses NEXT_PUBLIC_API_URL while ConnectionCharts.tsx uses NEXT_PUBLIC_GRAPHQL_ENDPOINT. Divergent env names lead to misconfig in deployments.

Apply this diff (or update the other component to match; pick one name project‑wide):

-        const res = await fetch(
-          process.env.NEXT_PUBLIC_API_URL ||
+        const res = await fetch(
+          process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT ||
           "http://localhost:7000/switchmap/api/graphql",

104-143: Include LLDP fields in the “all zones” query for consistency.

Zone-specific query requests lldpremportdesc, lldpremsysname, lldpremsysdesc, lldpremsyscapenabled, but the “all” query omits them. TopologyChart and downstream features may rely on these fields.

Apply this diff to the all‑zones selection set:

             hostname
             l1interfaces {
               edges {
                 node {
                   ifoperstatus
                   cdpcachedeviceid
                   cdpcachedeviceport
+                  lldpremportdesc
+                  lldpremsysname
+                  lldpremsysdesc
+                  lldpremsyscapenabled 
                 }
               }
             }

170-186: Deduplication logic is sound; consider extracting to a helper.

Same hostname-dedup appears in both branches. Extracting a small helper reduces duplication and risk of drift.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00a6221 and 3cd99b5.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (52)
  • .github/workflows/pull-request.yml (1 hunks)
  • docs/docs/backend/Server.md (1 hunks)
  • docs/docs/frontend/components/ConnectionDetails.md (1 hunks)
  • docs/docs/frontend/components/TopologyChart.md (1 hunks)
  • docs/docs/frontend/components/ZoneDropdown.md (1 hunks)
  • docs/docs/frontend/devices/[id]/page.md (1 hunks)
  • docs/docs/frontend/modules.md (0 hunks)
  • docs/docs/frontend/types/graphql/GetZoneDevices.md (5 hunks)
  • docs/docs/frontend/utils/stringUtils.md (1 hunks)
  • docs/docs/frontend/utils/time.md (1 hunks)
  • frontend/.gitignore (1 hunks)
  • frontend/README.md (1 hunks)
  • frontend/package.json (2 hunks)
  • frontend/postcss.config.mjs (1 hunks)
  • frontend/src/app/components/ConnectionCharts.spec.tsx (1 hunks)
  • frontend/src/app/components/ConnectionCharts.tsx (1 hunks)
  • frontend/src/app/components/ConnectionDetails.spec.tsx (1 hunks)
  • frontend/src/app/components/ConnectionDetails.tsx (1 hunks)
  • frontend/src/app/components/DeviceDetails.module.css (1 hunks)
  • frontend/src/app/components/DeviceDetails.spec.tsx (1 hunks)
  • frontend/src/app/components/DeviceDetails.tsx (1 hunks)
  • frontend/src/app/components/DeviceOverview.spec.tsx (1 hunks)
  • frontend/src/app/components/DevicesOverview.tsx (1 hunks)
  • frontend/src/app/components/HistoricalChart.spec.tsx (1 hunks)
  • frontend/src/app/components/HistoricalChart.tsx (1 hunks)
  • frontend/src/app/components/LineChartWrapper.spec.tsx (1 hunks)
  • frontend/src/app/components/Sidebar.spec.tsx (1 hunks)
  • frontend/src/app/components/Sidebar.tsx (1 hunks)
  • frontend/src/app/components/TopologyChart.spec.tsx (1 hunks)
  • frontend/src/app/components/TopologyChart.tsx (1 hunks)
  • frontend/src/app/components/ZoneDropdown.spec.tsx (1 hunks)
  • frontend/src/app/components/ZoneDropdown.tsx (1 hunks)
  • frontend/src/app/components/__mocks__/chartMocks.ts (1 hunks)
  • frontend/src/app/components/__mocks__/deviceMocks.ts (1 hunks)
  • frontend/src/app/devices/[id]/page.spec.tsx (1 hunks)
  • frontend/src/app/devices/[id]/page.tsx (2 hunks)
  • frontend/src/app/globals.css (2 hunks)
  • frontend/src/app/history/page.spec.tsx (1 hunks)
  • frontend/src/app/history/page.tsx (8 hunks)
  • frontend/src/app/layout.spec.tsx (1 hunks)
  • frontend/src/app/page.spec.tsx (1 hunks)
  • frontend/src/app/page.tsx (2 hunks)
  • frontend/src/app/theme-toggle.spec.tsx (1 hunks)
  • frontend/src/app/types/graphql/GetDeviceInterfaces.ts (1 hunks)
  • frontend/src/app/utils/stringUtils.spec.tsx (1 hunks)
  • frontend/src/app/utils/time.spec.tsx (1 hunks)
  • frontend/src/app/utils/timeStamp.spec.tsx (1 hunks)
  • frontend/tsconfig.json (2 hunks)
  • frontend/vite.config.ts (1 hunks)
  • frontend/vitest.setup.ts (1 hunks)
  • switchmap/server/db/schemas.py (1 hunks)
  • temp-frontend (1 hunks)
💤 Files with no reviewable changes (1)
  • docs/docs/frontend/modules.md
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-06T17:08:44.440Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#337
File: frontend/src/app/components/HistoricalChart.tsx:62-67
Timestamp: 2025-09-06T17:08:44.440Z
Learning: In HistoricalChart.tsx, using XAxis with type="number" and custom tickFormatter can cause empty charts if the lastPolled data contains null values or mixed timestamp formats (seconds vs milliseconds). The default categorical behavior is more robust for handling inconsistent timestamp data.

Applied to files:

  • frontend/src/app/components/HistoricalChart.tsx
  • frontend/src/app/components/DeviceDetails.tsx
📚 Learning: 2025-09-04T20:04:37.212Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#337
File: docs/docs/frontend/types/graphql/GetZoneDevices.md:59-64
Timestamp: 2025-09-04T20:04:37.212Z
Learning: Documentation files in docs/docs/frontend/ are autogenerated by TypeDoc from TypeScript source code and should not be manually edited. Changes should be made to the source TypeScript files instead.

Applied to files:

  • docs/docs/frontend/components/ZoneDropdown.md
  • docs/docs/frontend/components/TopologyChart.md
  • frontend/README.md
📚 Learning: 2025-07-30T15:34:06.789Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#333
File: frontend/src/app/components/TopologyChart.tsx:174-178
Timestamp: 2025-07-30T15:34:06.789Z
Learning: In the TopologyChart component, manual cleanup is implemented when devices change to address potential memory leaks from DOM element creation in the htmlTitle function.

Applied to files:

  • frontend/src/app/components/TopologyChart.tsx
  • frontend/src/app/components/TopologyChart.spec.tsx
  • docs/docs/frontend/components/TopologyChart.md
  • frontend/src/app/components/DeviceDetails.module.css
📚 Learning: 2025-09-04T19:53:01.998Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#337
File: frontend/src/app/history/page.tsx:45-51
Timestamp: 2025-09-04T19:53:01.998Z
Learning: GraphQL lastPolled field returns an integer timestamp (seconds since epoch), not a string. The backend uses graphene.Int for this field, and frontend components like DeviceDetails multiply by 1000 to convert to milliseconds for Date constructor.

Applied to files:

  • docs/docs/frontend/types/graphql/GetZoneDevices.md
📚 Learning: 2025-09-05T23:17:28.772Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#337
File: frontend/src/app/history/page.tsx:20-41
Timestamp: 2025-09-05T23:17:28.772Z
Learning: In the switchmap-ng project, each poll creates a new device/event record, so the zones->devices GraphQL query effectively returns all historical points for devices, not just current snapshots. This enables time-series visualization without needing a separate historical data source.

Applied to files:

  • docs/docs/frontend/types/graphql/GetZoneDevices.md
  • frontend/src/app/page.tsx
🧬 Code graph analysis (30)
switchmap/server/db/schemas.py (1)
switchmap/server/db/models.py (1)
  • Device (168-211)
frontend/src/app/components/ConnectionCharts.tsx (3)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/types/graphql/GetDeviceInterfaces.ts (1)
  • InterfaceNode (44-73)
frontend/src/app/components/HistoricalChart.tsx (1)
  • HistoricalChart (48-79)
frontend/src/app/components/__mocks__/deviceMocks.ts (2)
frontend/src/app/types/graphql/GetDeviceInterfaces.ts (1)
  • InterfaceNode (44-73)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/components/ConnectionDetails.spec.tsx (2)
frontend/src/app/components/ConnectionDetails.tsx (1)
  • ConnectionDetails (29-162)
frontend/src/app/components/__mocks__/deviceMocks.ts (1)
  • mockDevice (47-59)
frontend/src/app/page.spec.tsx (1)
frontend/src/app/page.tsx (1)
  • Home (34-232)
frontend/vite.config.ts (1)
frontend/eslint.config.mjs (1)
  • __dirname (6-6)
frontend/src/app/theme-toggle.spec.tsx (1)
frontend/src/app/theme-toggle.tsx (1)
  • ThemeToggle (23-43)
frontend/src/app/components/TopologyChart.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
frontend/src/app/layout.spec.tsx (1)
frontend/src/app/layout.tsx (1)
  • RootLayout (43-57)
frontend/src/app/components/ZoneDropdown.spec.tsx (1)
frontend/src/app/components/ZoneDropdown.tsx (1)
  • ZoneDropdown (35-211)
frontend/src/app/components/TopologyChart.spec.tsx (2)
frontend/src/app/components/TopologyChart.tsx (1)
  • TopologyChart (34-579)
frontend/src/app/components/__mocks__/deviceMocks.ts (2)
  • mockDeviceLLDP (98-103)
  • mockDevice (47-59)
frontend/src/app/history/page.spec.tsx (1)
frontend/src/app/history/page.tsx (1)
  • DeviceHistoryChart (85-580)
frontend/src/app/components/DeviceOverview.spec.tsx (2)
frontend/src/app/components/DevicesOverview.tsx (1)
  • DevicesOverview (50-246)
frontend/src/app/components/__mocks__/deviceMocks.ts (1)
  • mockDevice (47-59)
frontend/src/app/components/HistoricalChart.spec.tsx (2)
frontend/src/app/components/HistoricalChart.tsx (1)
  • HistoricalChart (48-79)
frontend/src/app/components/__mocks__/chartMocks.ts (1)
  • mockData (1-4)
frontend/src/app/components/LineChartWrapper.spec.tsx (2)
frontend/src/app/components/LineChartWrapper.tsx (1)
  • LineChartWrapper (66-117)
frontend/src/app/components/__mocks__/chartMocks.ts (1)
  • mockLineData (6-9)
frontend/src/app/components/DeviceDetails.spec.tsx (2)
frontend/src/app/components/__mocks__/deviceMocks.ts (2)
  • mockMetricsForHost (61-86)
  • mockDevice (47-59)
frontend/src/app/components/DeviceDetails.tsx (1)
  • DeviceDetails (65-444)
frontend/src/app/components/DevicesOverview.tsx (3)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/types/graphql/GetDeviceInterfaces.ts (1)
  • InterfaceNode (44-73)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
frontend/src/app/devices/[id]/page.tsx (1)
frontend/src/app/components/ConnectionCharts.tsx (1)
  • ConnectionCharts (64-496)
frontend/src/app/utils/time.spec.tsx (1)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
frontend/src/app/utils/stringUtils.spec.tsx (1)
frontend/src/app/utils/stringUtils.ts (1)
  • truncateLines (8-28)
frontend/src/app/components/ConnectionCharts.spec.tsx (2)
frontend/src/app/components/ConnectionCharts.tsx (1)
  • ConnectionCharts (64-496)
frontend/src/app/components/__mocks__/deviceMocks.ts (1)
  • mockDevice (47-59)
frontend/src/app/components/ConnectionDetails.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (2)
  • DeviceNode (38-48)
  • InterfaceEdge (30-32)
frontend/src/app/types/graphql/GetDeviceInterfaces.ts (3)
  • MacPort (40-42)
  • InterfaceEdge (75-77)
  • InterfaceNode (44-73)
frontend/src/app/devices/[id]/page.spec.tsx (1)
frontend/src/app/devices/[id]/page.tsx (1)
  • DevicePage (79-271)
frontend/src/app/components/Sidebar.tsx (1)
frontend/src/app/theme-toggle.tsx (1)
  • ThemeToggle (23-43)
frontend/src/app/page.tsx (1)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/components/DeviceDetails.tsx (7)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/components/TopologyChart.tsx (1)
  • TopologyChart (34-579)
switchmap/server/db/ingest/update/device.py (1)
  • device (53-140)
frontend/src/app/utils/stringUtils.ts (1)
  • truncateLines (8-28)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
frontend/src/app/utils/timeStamp.ts (1)
  • formatUnixTimestamp (7-16)
frontend/src/app/components/HistoricalChart.tsx (1)
  • HistoricalChart (48-79)
frontend/src/app/utils/timeStamp.spec.tsx (1)
frontend/src/app/utils/timeStamp.ts (1)
  • formatUnixTimestamp (7-16)
frontend/src/app/components/Sidebar.spec.tsx (1)
frontend/src/app/components/Sidebar.tsx (1)
  • Sidebar (29-136)
frontend/src/app/components/ZoneDropdown.tsx (3)
switchmap/server/db/schemas.py (1)
  • Zone (143-150)
frontend/src/app/types/graphql/GetZoneDevices.ts (2)
  • Zone (69-71)
  • ZoneEdge (57-59)
switchmap/server/db/models.py (1)
  • Zone (129-165)
frontend/src/app/history/page.tsx (1)
frontend/src/app/components/Sidebar.tsx (1)
  • Sidebar (29-136)
🪛 Ruff (0.13.1)
switchmap/server/db/schemas.py

214-214: Unused method argument: kwargs

(ARG002)

🪛 markdownlint-cli2 (0.18.1)
docs/docs/backend/Server.md

2035-2035: Heading levels should only increment by one level at a time
Expected: h3; Actual: h4

(MD001, heading-increment)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Application

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 27

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
frontend/.gitignore (1)

1-149: Revert frontend/.gitignore — CI rejects modifications

Pipeline error: "frontend/.gitignore is unauthorized to change/delete." Restore this file to the target branch state; use .git/info/exclude locally or move ignore rules to an approved path. Confirm repo policy on protected files.

docs/docs/frontend/devices/[id]/page.md (1)

13-20: Do not edit auto-generated docs directly

Per team guidance, files under docs/docs/frontend are generated by TypeDoc. Revert manual edits and update the corresponding TS source (frontend/src/app/devices/[id]/page.tsx), then regenerate docs.

.github/workflows/pull-request.yml (1)

117-124: Fix sensitive-files pattern matching (false positives on nested package.json)

Current regex match flags frontend/package.json as unauthorized. Use glob matching so root-only names don’t match nested paths.

Apply:

-          # Check if any file in the diff matches our sensitive patterns
-          for PATTERN in "${SENSITIVE_FILES[@]}"; do
-            for FILE in $CHANGED_FILES $DELETED_FILES; do
-              if [[ "$FILE" == "$PATTERN" || "$FILE" =~ $PATTERN ]]; then
-                ALL_CHANGED_FILES+="$FILE "
-                SENSITIVE_CHANGED=true
-              fi
-            done
-          done
+          # Check if any file in the diff matches our sensitive patterns
+          # Use shell globs so 'package.json' doesn't match 'frontend/package.json',
+          # and 'docs/sidebar*.js' works as intended.
+          for PATTERN in "${SENSITIVE_FILES[@]}"; do
+            for FILE in $CHANGED_FILES $DELETED_FILES; do
+              case "$FILE" in
+                $PATTERN)
+                  ALL_CHANGED_FILES+="$FILE "
+                  SENSITIVE_CHANGED=true
+                  ;;
+              esac
+            done
+          done
frontend/package.json (2)

15-23: Restore 'recharts' to frontend/package.json or remove its imports

frontend/src/app/components/HistoricalChart.tsx:11 and frontend/src/app/components/LineChartWrapper.tsx:11 import "recharts" — either re-add the dependency or refactor those files to remove/replace recharts usage.


1-49: CI blocks nested package.json edits — workflow matcher is too broad

The Check-Sensitive-Files job in .github/workflows/pull-request.yml includes "package.json" in SENSITIVE_FILES and uses if [[ "$FILE" == "$PATTERN" || "$FILE" =~ $PATTERN ]], so any path containing "package.json" (e.g., frontend/package.json) is flagged and the PR is blocked.
Fix options: anchor the pattern (e.g. ^package\.json$) to match only the repo root, use explicit paths (e.g. docs/package.json), or change the matcher to compare basename; alternatively apply the "ignore-sensitive-files-pr" label to bypass.
Location: .github/workflows/pull-request.yml — SENSITIVE_FILES array + matcher loop (~lines 77–125).

frontend/src/app/page.tsx (1)

63-87: Missing lastPolled in single-zone query; align shape with types and dependents.

DeviceNode includes lastPolled. Add it to the selection to prevent undefineds downstream.

           node {
             idxDevice
             sysObjectid
             sysUptime
             sysDescription
             sysName
             hostname
+            lastPolled
             l1interfaces {
🧹 Nitpick comments (47)
frontend/src/app/utils/time.spec.tsx (1)

4-24: Refactor to table-driven tests for brevity and scalability.

Reduce repetition and make it easier to add new cases.

Apply this diff:

 describe("formatUptime", () => {
-  it("formats zero hundredths correctly", () => {
-    expect(formatUptime(0)).toBe("0d 0h 0m 0s");
-  });
-
-  it("formats seconds correctly", () => {
-    expect(formatUptime(100)).toBe("0d 0h 0m 1s"); // 100 hundredths = 1 second
-  });
-
-  it("formats minutes correctly", () => {
-    expect(formatUptime(6100)).toBe("0d 0h 1m 1s"); // 6100 hundredths = 61 seconds
-  });
-
-  it("formats hours correctly", () => {
-    expect(formatUptime(366000)).toBe("0d 1h 1m 0s"); // 366000 hundredths = 1h 1m
-  });
-
-  it("formats days correctly", () => {
-    expect(formatUptime(9006100)).toBe("1d 1h 1m 1s"); // 9006100 hundredths = 1d 1h 1m 1s
-  });
+  it.each([
+    [0, "0d 0h 0m 0s"],
+    [100, "0d 0h 0m 1s"],       // 1s
+    [6100, "0d 0h 1m 1s"],     // 61s
+    [366000, "0d 1h 1m 0s"],   // 1h1m
+    [9006100, "1d 1h 1m 1s"],  // 1d1h1m1s
+  ])("formats %d hundredths correctly", (input, expected) => {
+    expect(formatUptime(input)).toBe(expected);
+  });
 });
frontend/src/app/utils/stringUtils.spec.tsx (1)

28-33: Add a test for uneven maxLength/lines split.

Validates floor-based segment sizing and truncation of leftovers.

Append this test:

   it("respects the maxLength option", () => {
     const str = "abcdefghijklmnopqrstuvwxyz";
     const result = truncateLines(str, { maxLength: 12, lines: 2 });
     expect(result).toBe("abcdef\nghijkl...");
   });
+
+  it("handles maxLength not divisible by lines (drops remainder)", () => {
+    const str = "abcdefghijklmnopqrstuvwxyz";
+    const result = truncateLines(str, { maxLength: 10, lines: 3 });
+    const lines = result.replace(/\.\.\.$/, "").split("\n");
+    expect(lines).toHaveLength(3);
+    expect(lines[0]).toHaveLength(3);
+    expect(lines[1]).toHaveLength(3);
+    expect(lines[2]).toHaveLength(3);
+    expect(result.endsWith("...")).toBe(true);
+  });
frontend/src/app/utils/timeStamp.spec.tsx (1)

17-27: Consider additional inputs and clarify formatting requirements.

  • If a locale-agnostic format is desired in UX (e.g., ISO), reliance on toLocaleString may be insufficient; confirm product intent.
  • Add a test for numeric strings with whitespace to mirror real-world inputs.

Proposed test addition:

   it("formats valid string timestamps correctly", () => {
     const tsStr = "1693574400";
     const formatted = formatUnixTimestamp(tsStr);
     expect(new Date(Number(tsStr) * 1000).toLocaleString()).toBe(formatted);
   });
+
+  it("trims and parses numeric string timestamps with spaces", () => {
+    const tsStr = " 1693574400 ";
+    const formatted = formatUnixTimestamp(tsStr);
+    expect(new Date(Number(tsStr) * 1000).toLocaleString()).toBe(formatted);
+  });

If we need locale-agnostic formatting, I can provide a variant using date-fns/Intl.DateTimeFormat with explicit locale/timeZone.

docs/docs/frontend/utils/time.md (1)

15-15: Avoid hardcoded line anchors in source links.

Line numbers drift; prefer linking to the file (or a permalinks/typedoc anchor) without #L2.

Apply this diff:

-Defined in: [utils/time.ts:2](https://github.com/PalisadoesFoundation/switchmap-ng/blob/develop/frontend/src/app/utils/time.ts#L2)
+Defined in: [utils/time.ts](https://github.com/PalisadoesFoundation/switchmap-ng/blob/develop/frontend/src/app/utils/time.ts)
docs/docs/frontend/utils/stringUtils.md (1)

15-15: Prefer stable source links over line-specific anchors.

Prevents docs rot as code shifts.

Apply this diff:

-Defined in: [utils/stringUtils.ts:5](https://github.com/PalisadoesFoundation/switchmap-ng/blob/develop/frontend/src/app/utils/stringUtils.ts#L5)
+Defined in: [utils/stringUtils.ts](https://github.com/PalisadoesFoundation/switchmap-ng/blob/develop/frontend/src/app/utils/stringUtils.ts)
frontend/README.md (1)

32-36: “Deploy on Vercel” may be misleading for this project.

If deployment is self-hosted/on-prem (Docker/K8s/Ansible), linking to Vercel could misdirect users. Align deployment docs with project’s supported ops paths, or gate this note behind a “for local previews only” qualifier.

Would you like a short “Deployment” section pointing to the project’s actual deployment methods and CI?

frontend/src/app/globals.css (1)

36-36: Consider theme-specific values for subtle borders.

Currently light and dark both use #343B4F, which may appear heavy on light backgrounds. Suggest a lighter alpha for light mode.

Apply in light theme:

-  --border-subtle: #343B4F;
+  --border-subtle: rgba(8, 16, 40, 0.10);

And keep a stronger value in .dark (already present) to ensure visibility on dark surfaces.

frontend/.gitignore (6)

16-22: Deduplicate Next.js ignores; keep a single canonical block.

You have repeated .next/, out/, .next/cache[/], manifests, telemetry, etc. Consolidate to one block and standardize trailing slashes.

If/when edits to this file are allowed, collapse duplicates to a single section to reduce maintenance risk.

Also applies to: 105-134, 107-112, 123-127


37-40: Unify env file patterns.

.env*, .env..local, and the later .env..local entries overlap. Keep a single, minimal set (e.g., .env*, .env.local, .env.development.local, .env.test.local, .env.production.local).

Also applies to: 138-143


29-29: Consolidate log patterns.

With *.log already present, npm/yarn/pnpm debug log patterns are redundant. Prefer a single pattern like .log or keep specific debug logs and drop the wildcard — not both.

Also applies to: 144-149, 31-36


79-81: Remove framework-specific ignores not used (Svelte).

.svelte-kit/ appears unrelated to this Next.js app. Drop it to reduce noise unless you actually use Svelte in this workspace.

Is Svelte used anywhere under frontend/? If not, we can remove this line.


76-78: Verify Storybook usage.

If storybook-static/ isn’t produced in this project, remove the entry; otherwise keep it.

Do we have Storybook configured for frontend? If no, we can prune this.


48-53: Editor/IDE ignores duplicated.

.vscode/ and .idea/ are declared twice. Keep them once.

Also applies to: 95-104

docs/docs/frontend/types/graphql/GetZoneDevices.md (1)

172-179: Docs: add a note on zones→devices returning historical snapshots.

Per project behavior, each poll yields a new record; consumers may assume “current state.” Add a short remark to clarify this.

Apply:

@@
 #### Remarks
@@
 It is designed to provide a type-safe interface for working with GraphQL data in TypeScript.
 The types defined here are used throughout the application to ensure that the data structures
 are consistent and correctly typed, reducing the risk of runtime errors
 and improving developer experience.
+
+Note: In Switchmap‑NG, each poll creates a new device/event record. As a result, the
+zones → devices query may return multiple historical snapshots for a device, not only a
+single “current” snapshot. Callers should account for time‑series semantics when aggregating
+or rendering device data.
.github/workflows/pull-request.yml (3)

249-269: Use npm cache via setup-node + npm ci; call test:coverage script

Faster, reproducible installs and simpler command.

Apply:

       - name: Set up Node.js
         uses: actions/setup-node@v4
         with:
           node-version: 22
+          cache: npm
+          cache-dependency-path: frontend/package-lock.json

-      - name: Cache node modules
-        uses: actions/cache@v4
-        with:
-          path: frontend/node_modules
-          key: ${{ runner.os }}-node-${{ hashFiles('frontend/package-lock.json') }}
-          restore-keys: |
-            ${{ runner.os }}-node-
-
       - name: Install frontend dependencies
         working-directory: frontend
-        run: npm install
+        run: npm ci

       - name: Run frontend tests with coverage
         working-directory: frontend
-        run: npm run test -- --coverage
+        run: npm run test:coverage

316-323: Workflow will fail if PR target branch ≠ develop

This PR targets develop-abhi; the job enforces base ref 'develop'. Retarget the PR or relax this check.


324-343: Gate CodeRabbit validation on frontend tests as well

Optional: add Test-Frontend to needs to avoid validating before FE tests finish.

Apply:

-    needs: [Code-Quality-Checks, Test-Application]
+    needs: [Code-Quality-Checks, Test-Application, Test-Frontend]
frontend/vitest.setup.ts (1)

24-26: Clear/reset mocks after each test

Prevents cross-test leakage; cheap safety.

Apply:

-import { vi } from 'vitest';
+import { vi, afterEach } from 'vitest';
@@
 (global as any).ResizeObserver = ResizeObserver;
@@
 vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }) }));
 vi.mock('next-themes', () => ({ useTheme: () => ({ theme: 'light' }) }));
+
+afterEach(() => {
+  vi.clearAllMocks();
+  vi.resetAllMocks();
+});
frontend/package.json (1)

2-14: Optional: declare Node engine

Helps CI/local consistency.

Apply:

 {
   "name": "frontend",
   "version": "0.1.0",
   "type": "module",
   "private": true,
+  "engines": {
+    "node": ">=20 <=22"
+  },
   "scripts": {
frontend/tsconfig.json (1)

19-23: Vitest globals in base tsconfig

Works, but consider isolating test-only types in a tsconfig.vitest.json to avoid bleeding into app type space.

frontend/src/app/components/__mocks__/chartMocks.ts (1)

1-9: Add explicit types (optional).

Annotating the mock arrays prevents drift with component expectations.

Apply this diff:

-export const mockData = [
+type MockDailyPoint = { lastPolled: string; value: number };
+export const mockData: MockDailyPoint[] = [
   { lastPolled: "2025-08-01", value: 1 },
   { lastPolled: "2025-08-02", value: 0 },
 ];
 
-export const mockLineData = [
+type MockLinePoint = { time: string; value: number };
+export const mockLineData: MockLinePoint[] = [
   { time: "2025-08-01T00:00:00Z", value: 10 },
   { time: "2025-08-02T00:00:00Z", value: 20 },
 ];
frontend/src/app/components/DevicesOverview.tsx (1)

152-165: Expose sorting state to assistive tech.

Add aria-sort (and scope) to sortable headers for better a11y.

Apply this diff:

                   <th
                     key={header.id}
                     onClick={header.column.getToggleSortingHandler()}
                     onKeyDown={(e) => {
                       if (e.key === "Enter" || e.key === " ") {
                         e.preventDefault();
                         header.column.getToggleSortingHandler()?.(e);
                       }
                     }}
                     className="cursor-pointer px-4 py-2"
                     tabIndex={0}
                     role="button"
                     aria-label={`Sort by ${header.column.columnDef.header}`}
+                    aria-sort={
+                      header.column.getIsSorted() === "asc"
+                        ? "ascending"
+                        : header.column.getIsSorted() === "desc"
+                        ? "descending"
+                        : "none"
+                    }
+                    scope="col"
                   >
frontend/src/app/components/ConnectionDetails.tsx (2)

2-2: Remove unused useParams import/variable.

They are not used; keep surface aligned with prop-driven component.

Apply this diff:

-import { useParams } from "next/navigation";
@@
-  const params = useParams();

Also applies to: 30-30


115-119: Use strict equality for status checks.

Prevents accidental coercion.

Apply this diff:

-                  {iface.ifoperstatus == 1
+                  {iface.ifoperstatus === 1
                     ? "Active"
-                    : iface.ifoperstatus == 2
+                    : iface.ifoperstatus === 2
                     ? "Disabled"
                     : "N/A"}
frontend/src/app/components/DeviceDetails.module.css (1)

25-28: Remove redundant border reset

border-width: 0 is redundant when border: none is already set.

 .tableCustom *{
-  border: none ;
-  border-width: 0 ;
+  border: none;
 }
docs/docs/frontend/components/TopologyChart.md (1)

15-15: Avoid manual edits to autogenerated frontend docs

Per prior learnings, docs under docs/docs/frontend are autogenerated by TypeDoc; update the TypeScript source and regenerate instead of editing this file directly.

frontend/src/app/components/Sidebar.tsx (1)

108-114: Improve a11y: reflect expanded state and associate controller/region

Expose open/closed state and control relationship; add an id on the slide‑in aside.

       <button
         className="p-3 text-2xl lg:hidden fixed top-4 left-4 z-50 bg-bg border border-border rounded"
         onClick={() => setOpen(true)}
         aria-label="Open sidebar"
+        aria-expanded={open}
+        aria-controls="mobile-sidebar"
       >
       {open && (
         <>
           <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" />
           <aside
+            id="mobile-sidebar"
             data-testid="slide-in-sidebar"
             ref={sidebarRef}
             className="fixed top-0 left-0 w-60 h-full bg-bg border-r border-border z-50 p-4 shadow-md transition-transform transform lg:hidden"
           >

Optional: add Escape key handling to close the sidebar when open.

Also applies to: 125-131

docs/docs/frontend/components/ZoneDropdown.md (1)

15-15: Frontend docs appear autogenerated — prefer regenerating

ZoneDropdown docs under docs/docs/frontend should be generated via TypeDoc; make changes in the TS source and regenerate.

frontend/src/app/devices/[id]/page.spec.tsx (1)

4-4: Restore globals after tests to prevent cross-test pollution

matchMedia and fetch are globally stubbed; add afterEach to restore/un-stub.

-import { describe, it, expect, vi, beforeEach } from "vitest";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
@@
   });
 });
 
+afterEach(() => {
+  vi.restoreAllMocks();
+  vi.unstubAllGlobals();
+});

Also applies to: 71-71

frontend/src/app/components/TopologyChart.tsx (2)

77-115: Options should react to prop changes (clickToUse/zoomView)

useMemo depends only on theme; clickToUse and zoomView changes won’t update options.

-  const options: Options = useMemo(
+  const options: Options = useMemo(
     () => ({
@@
-    }),
-    [isDark]
+    }),
+    [isDark, clickToUse, zoomView]
   );

503-522: Remove duplicate suggestion filtering; keep logic in one place

You filter suggestions both in onChange and in the useEffect for inputTerm. Keep only the effect.

-              onChange={(e) => {
-                const value = e.target.value;
-                setInputTerm(value);
-                if (value.trim() === "") {
-                  setSuggestions([]);
-                  return;
-                }
-                const filtered = allNodeLabels
-                  .filter((label) =>
-                    label.toLowerCase().includes(value.toLowerCase())
-                  )
-                  .slice(0, 5);
-                setSuggestions(filtered);
-              }}
+              onChange={(e) => setInputTerm(e.target.value)}
             />

Also applies to: 399-410

frontend/src/app/components/Sidebar.spec.tsx (1)

40-60: Consolidate duplicate outside-click test and assert open/close state via test id

The first outside-click test uses a non-existent aria-label and duplicates the second. Keep one and verify the element exists before/after.

-  it("closes sidebar when clicking outside", () => {
-    render(<Sidebar />);
-    const button = screen.getByLabelText(/open sidebar/i);
-    fireEvent.click(button);
-
-    fireEvent.mouseDown(document.body);
-
-    expect(screen.queryByLabelText("slide-in sidebar")).toBeNull();
-  });
-
-  it("closes sidebar when clicking outside", () => {
+  it("closes sidebar when clicking outside", () => {
     render(<Sidebar />);
     const button = screen.getByLabelText(/open sidebar/i);
     fireEvent.click(button);
 
+    // Sidebar is open
+    expect(screen.getByTestId("slide-in-sidebar")).toBeInTheDocument();
     // Click outside
     fireEvent.mouseDown(document.body);
 
     // Slide-in sidebar should be removed
     expect(screen.queryByTestId("slide-in-sidebar")).toBeNull();
   });
frontend/src/app/history/page.tsx (1)

87-87: Suggestion: minor cleanups and a11y/test hooks look good

  • New allDeviceHostnames-based suggestions: good.
  • Consider removing now-unused uniqueHostnames memo to reduce dead code.
  • Added role/testids/aria-labels improve testability and a11y. LGTM.

Also applies to: 226-230, 319-335

frontend/src/app/components/TopologyChart.spec.tsx (3)

25-57: Improve vis-network DataSet mock to mimic real API (fix id-based get).

Your DataSetMock.get always returns the full data array. The component likely calls get(id) to retrieve a single node. Aligning the mock avoids brittle handler logic and future regressions.

Apply:

 vi.mock("vis-network/standalone/esm/vis-network", () => {
-  const updateMock = vi.fn();
+  const updateMock = vi.fn();
   const removeMock = vi.fn();
   const focusMock = vi.fn();
   const addMock = vi.fn();
   const forEachMock = vi.fn((callback: any) =>
     [{ id: "1" }, { id: "2" }].forEach(callback)
   );

-  const DataSetMock = vi.fn((data) => ({
-    get: vi.fn(() => data),
+  const DataSetMock = vi.fn((data) => ({
+    get: vi.fn((id?: string) =>
+      id ? (Array.isArray(data) ? data.find((n: any) => n?.id === id) : undefined) : data
+    ),
     add: vi.fn(),
     clear: vi.fn(),
     update: updateMock,
     current: data || [],
-    forEach: vi.fn(),
+    forEach: vi.fn((cb?: (v: any) => void) => {
+      if (!cb || !Array.isArray(data)) return;
+      data.forEach(cb);
+    }),
   }));

   const NetworkMock = vi.fn(() => ({
     fit: vi.fn(),
     moveTo: vi.fn(),
     on: vi.fn(),
     focus: focusMock,
     unselectAll: vi.fn(),
     nodes: [],
     edges: [],
     nodesData: { add: addMock, update: updateMock, forEach: forEachMock },
     edgesData: { update: updateMock, forEach: forEachMock },
     getSelectedEdges: vi.fn(() => []),
   }));

-  return { Network: NetworkMock, DataSet: DataSetMock, removeMock };
+  return { Network: NetworkMock, DataSet: DataSetMock, removeMock };
 });

167-204: Don’t override on; use the component’s registered handler in tests.

This test replaces mockInstance.on and executes your own callback, so it no longer verifies the component’s real selectNode logic.

Refactor to invoke the handler the component registered:

-  const callbacks: Record<string, (params: any) => void> = {};
-  mockInstance.on = vi.fn((event, cb) => {
-    callbacks[event] = cb;
-  });
-
-  mockInstance.on("selectNode", ({ nodes }: { nodes: string[] }) => {
-    const selected = nodes[0];
-    mockInstance.nodesData.current.forEach((node: any) => {
-      const isSelected = node.id === selected;
-      mockInstance.nodesData.update({
-        id: node.id,
-        opacity: isSelected ? 1 : 0.6,
-        color: { border: "#555" },
-        font: { color: isSelected ? "black" : "#A9A9A9" },
-      });
-    });
-  });
-
-  callbacks["selectNode"]({ nodes: ["1"] });
-  expect(mockUpdate).toHaveBeenCalledWith(
+  const selectNodeHandler = mockInstance.on.mock.calls.find(
+    ([e]: [string, Function]) => e === "selectNode"
+  )?.[1];
+  // Ensure nodesData.get(id) returns a node; DataSetMock.get was fixed above.
+  selectNodeHandler?.({ nodes: ["1"] });
+  // Assert that updates were issued (from the component’s handler)
+  expect(mockInstance.nodesData.update).toHaveBeenCalledWith(
     expect.objectContaining({ id: "1", opacity: 1 })
   );
-  expect(mockUpdate).toHaveBeenCalledWith(
+  expect(mockInstance.nodesData.update).toHaveBeenCalledWith(
     expect.objectContaining({ id: "2", opacity: 0.6 })
   );

206-213: Assert updates via the component’s handler, not test-owned objects.

Asserting mockInstance.nodesData.update/edgesData.update is okay only if the component writes there. Ensure this matches the handler’s behavior; otherwise, observe the DataSetMock.update calls instead.

If the above assertion is flaky, prefer asserting call count on the DataSetMock.update you defined for vis mocks.

frontend/src/app/components/ConnectionCharts.spec.tsx (1)

91-104: Hostname is asserted correctly; consider tighter assertion on GraphQL body.

You can parse the body to assert query and variables shape to catch regressions.

Example:

const [, init] = (fetch as Mock).mock.calls[0];
const body = JSON.parse(init.body as string);
expect(body.query).toMatch(/query/i);
expect(body.query).toMatch(/devices|interfaces/i);
expect(body).toEqual(expect.objectContaining({ query: expect.any(String) }));
frontend/src/app/components/ZoneDropdown.spec.tsx (1)

79-94: Resolve pending promise with act; consider asserting end-of-loading UI.

You already resolve the pending fetch; optionally assert loading indicator disappears to strengthen the test.

await waitFor(() => expect(screen.queryByText(/\(Loading...\)/)).not.toBeInTheDocument());
frontend/src/app/page.tsx (3)

98-104: Remove dead parameter or implement retry/backoff.

retryCount is unused. Remove it or implement exponential backoff.

-    const fetchDevices = async (retryCount = 0) => {
+    const fetchDevices = async () => {

90-97: Avoid setting error when no zone is selected (noise).

When no zone is selected, you set an error string that isn’t shown. Consider leaving error as null.

-      setError("Waiting for zone selection to load devices.");
+      setError(null);

50-56: Derived state: zoneSelected can be computed.

zoneSelected duplicates !!zoneId. Compute inline to reduce state.

-  const [zoneSelected, setZoneSelected] = useState<boolean>(false);
+  const zoneSelected = !!zoneId;
...
-  useEffect(() => {
-    if (zoneId) {
-      localStorage.setItem("zoneId", zoneId);
-      setZoneSelected(!!zoneId);
-    }
-  }, [zoneId]);
+  useEffect(() => {
+    if (zoneId) localStorage.setItem("zoneId", zoneId);
+  }, [zoneId]);
frontend/src/app/components/ZoneDropdown.tsx (2)

42-76: Add AbortController for fetch cancellation on unmount.

The active flag prevents setState after unmount but still consumes network. Abort to free resources.

  useEffect(() => {
-    let active = true;
+    let active = true;
+    const ac = new AbortController();

     const fetchZones = async () => {
       setLoading(true);
       setError(null);
       try {
         const res = await fetch(
           process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT ||
             "http://localhost:7000/switchmap/api/graphql",
           {
             method: "POST",
             headers: { "Content-Type": "application/json" },
             body: JSON.stringify({
               query: `{
                 events(last: 1) {
                   edges {
                     node {
                       zones {
                         edges {
                           node {
                             tsCreated
                             id
                             idxZone
                             name
                           }
                         }
                       }
                     }
                   }
                 }
               }`,
             }),
+            signal: ac.signal,
           }
         );

         if (!res.ok) throw new Error(`Network error: ${res.status}`);
-    return () => {
-      active = false;
-    };
+    return () => {
+      active = false;
+      ac.abort();
+    };

141-169: Minor a11y: add ARIA attributes.

Expose expanded state and menu semantics.

-      <button
+      <button
         type="button"
         onClick={() => setOpen(!open)}
         className="inline-flex justify-between items-center bg-bg w-48 px-4 py-2 border border-gray-300 rounded-md shadow-sm outline"
+        aria-haspopup="menu"
+        aria-expanded={open}
       >
...
-      <p className="text-sm text-gray-700 pt-2 text-right">
+      <p className="text-sm text-gray-700 pt-2 text-right" aria-live="polite">
         {selected?.tsCreated || ""}
       </p>

And for the menu:

-          <div
+          <div
             data-testid="zone-dropdown-menu"
-            className="absolute bg-bg mt-1 w-48 rounded-md shadow-lg border border-color z-10 max-h-60 overflow-auto"
+            className="absolute bg-bg mt-1 w-48 rounded-md shadow-lg border border-color z-10 max-h-60 overflow-auto"
+            role="menu"
           >
...
-              <button
+              <button
                 data-testid={`zone-button-${zone.id}`}
                 key={zone.id}
                 onClick={() => {
                   onChange(zone.id);
                   setOpen(false);
                 }}
-                className="block w-full text-left px-4 py-2 hover:bg-hover-bg focus:outline-none"
+                className="block w-full text-left px-4 py-2 hover:bg-hover-bg focus:outline-none"
+                role="menuitem"
               >
frontend/src/app/components/DeviceDetails.tsx (2)

269-274: Add accessible alert semantics to error banner

Add role and aria-live so screen readers announce errors.

-        <div className="fixed inset-0 flex mt-2 items-start justify-center z-50 pointer-events-none">
-          <div className="bg-gray-300 text-gray-900 px-6 py-3 rounded shadow-lg animate-fade-in pointer-events-auto">
+        <div className="fixed inset-0 flex mt-2 items-start justify-center z-50 pointer-events-none">
+          <div
+            role="alert"
+            aria-live="assertive"
+            className="bg-gray-300 text-gray-900 px-6 py-3 rounded shadow-lg animate-fade-in pointer-events-auto"
+          >
             {errorMsg}
           </div>
         </div>

395-440: Avoid recomputing filters on every render

filterByRange is called 3 times per render. Memoize filtered datasets to reduce work.

Example:

const filteredUptime = useMemo(() => filterByRange(uptimeData), [uptimeData, selectedRange, customRange]);
const filteredCpu = useMemo(() => filterByRange(cpuUsageData), [cpuUsageData, selectedRange, customRange]);
const filteredMem = useMemo(() => filterByRange(memoryUsageData), [memoryUsageData, selectedRange, customRange]);
frontend/src/app/components/ConnectionCharts.tsx (2)

194-195: Sort series by timestamp before rendering

GraphQL edge order isn’t guaranteed. Sort each per-interface series by lastPolled for stable charts.

-        setData(newData);
+        Object.values(newData).forEach(seriesByTab => {
+          Object.values(seriesByTab).forEach(series => {
+            series.sort((a, b) => new Date(a.lastPolled).getTime() - new Date(b.lastPolled).getTime());
+          });
+        });
+        setData(newData);

285-357: Add accessible alert semantics to error banner and inputs

Mirror DeviceDetails: role="alert" with aria-live; also add aria-invalid on date inputs when showing an error.

Example:

-      {error && (
+      {error && (
         <div className="fixed inset-0 flex mt-2 items-start justify-center z-50 pointer-events-none">
-          <div className="bg-gray-300 text-gray-900 px-6 py-3 rounded shadow-lg animate-fade-in pointer-events-auto">
+          <div role="alert" aria-live="assertive" className="bg-gray-300 text-gray-900 px-6 py-3 rounded shadow-lg animate-fade-in pointer-events-auto">
             {error}
           </div>
         </div>
       )}

And when error is set, add aria-invalid on the offending input.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00a6221 and 3cd99b5.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (52)
  • .github/workflows/pull-request.yml (1 hunks)
  • docs/docs/backend/Server.md (1 hunks)
  • docs/docs/frontend/components/ConnectionDetails.md (1 hunks)
  • docs/docs/frontend/components/TopologyChart.md (1 hunks)
  • docs/docs/frontend/components/ZoneDropdown.md (1 hunks)
  • docs/docs/frontend/devices/[id]/page.md (1 hunks)
  • docs/docs/frontend/modules.md (0 hunks)
  • docs/docs/frontend/types/graphql/GetZoneDevices.md (5 hunks)
  • docs/docs/frontend/utils/stringUtils.md (1 hunks)
  • docs/docs/frontend/utils/time.md (1 hunks)
  • frontend/.gitignore (1 hunks)
  • frontend/README.md (1 hunks)
  • frontend/package.json (2 hunks)
  • frontend/postcss.config.mjs (1 hunks)
  • frontend/src/app/components/ConnectionCharts.spec.tsx (1 hunks)
  • frontend/src/app/components/ConnectionCharts.tsx (1 hunks)
  • frontend/src/app/components/ConnectionDetails.spec.tsx (1 hunks)
  • frontend/src/app/components/ConnectionDetails.tsx (1 hunks)
  • frontend/src/app/components/DeviceDetails.module.css (1 hunks)
  • frontend/src/app/components/DeviceDetails.spec.tsx (1 hunks)
  • frontend/src/app/components/DeviceDetails.tsx (1 hunks)
  • frontend/src/app/components/DeviceOverview.spec.tsx (1 hunks)
  • frontend/src/app/components/DevicesOverview.tsx (1 hunks)
  • frontend/src/app/components/HistoricalChart.spec.tsx (1 hunks)
  • frontend/src/app/components/HistoricalChart.tsx (1 hunks)
  • frontend/src/app/components/LineChartWrapper.spec.tsx (1 hunks)
  • frontend/src/app/components/Sidebar.spec.tsx (1 hunks)
  • frontend/src/app/components/Sidebar.tsx (1 hunks)
  • frontend/src/app/components/TopologyChart.spec.tsx (1 hunks)
  • frontend/src/app/components/TopologyChart.tsx (1 hunks)
  • frontend/src/app/components/ZoneDropdown.spec.tsx (1 hunks)
  • frontend/src/app/components/ZoneDropdown.tsx (1 hunks)
  • frontend/src/app/components/__mocks__/chartMocks.ts (1 hunks)
  • frontend/src/app/components/__mocks__/deviceMocks.ts (1 hunks)
  • frontend/src/app/devices/[id]/page.spec.tsx (1 hunks)
  • frontend/src/app/devices/[id]/page.tsx (2 hunks)
  • frontend/src/app/globals.css (2 hunks)
  • frontend/src/app/history/page.spec.tsx (1 hunks)
  • frontend/src/app/history/page.tsx (8 hunks)
  • frontend/src/app/layout.spec.tsx (1 hunks)
  • frontend/src/app/page.spec.tsx (1 hunks)
  • frontend/src/app/page.tsx (2 hunks)
  • frontend/src/app/theme-toggle.spec.tsx (1 hunks)
  • frontend/src/app/types/graphql/GetDeviceInterfaces.ts (1 hunks)
  • frontend/src/app/utils/stringUtils.spec.tsx (1 hunks)
  • frontend/src/app/utils/time.spec.tsx (1 hunks)
  • frontend/src/app/utils/timeStamp.spec.tsx (1 hunks)
  • frontend/tsconfig.json (2 hunks)
  • frontend/vite.config.ts (1 hunks)
  • frontend/vitest.setup.ts (1 hunks)
  • switchmap/server/db/schemas.py (1 hunks)
  • temp-frontend (1 hunks)
💤 Files with no reviewable changes (1)
  • docs/docs/frontend/modules.md
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-07-30T15:34:06.789Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#333
File: frontend/src/app/components/TopologyChart.tsx:174-178
Timestamp: 2025-07-30T15:34:06.789Z
Learning: In the TopologyChart component, manual cleanup is implemented when devices change to address potential memory leaks from DOM element creation in the htmlTitle function.

Applied to files:

  • docs/docs/frontend/components/TopologyChart.md
  • frontend/src/app/components/TopologyChart.tsx
  • frontend/src/app/components/DeviceDetails.module.css
📚 Learning: 2025-09-04T20:04:37.212Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#337
File: docs/docs/frontend/types/graphql/GetZoneDevices.md:59-64
Timestamp: 2025-09-04T20:04:37.212Z
Learning: Documentation files in docs/docs/frontend/ are autogenerated by TypeDoc from TypeScript source code and should not be manually edited. Changes should be made to the source TypeScript files instead.

Applied to files:

  • docs/docs/frontend/components/TopologyChart.md
  • frontend/README.md
  • docs/docs/frontend/components/ZoneDropdown.md
  • docs/docs/frontend/devices/[id]/page.md
📚 Learning: 2025-09-04T19:53:01.998Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#337
File: frontend/src/app/history/page.tsx:45-51
Timestamp: 2025-09-04T19:53:01.998Z
Learning: GraphQL lastPolled field returns an integer timestamp (seconds since epoch), not a string. The backend uses graphene.Int for this field, and frontend components like DeviceDetails multiply by 1000 to convert to milliseconds for Date constructor.

Applied to files:

  • docs/docs/frontend/types/graphql/GetZoneDevices.md
📚 Learning: 2025-09-05T23:17:28.772Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#337
File: frontend/src/app/history/page.tsx:20-41
Timestamp: 2025-09-05T23:17:28.772Z
Learning: In the switchmap-ng project, each poll creates a new device/event record, so the zones->devices GraphQL query effectively returns all historical points for devices, not just current snapshots. This enables time-series visualization without needing a separate historical data source.

Applied to files:

  • docs/docs/frontend/types/graphql/GetZoneDevices.md
  • frontend/src/app/page.tsx
📚 Learning: 2025-09-06T17:08:44.440Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#337
File: frontend/src/app/components/HistoricalChart.tsx:62-67
Timestamp: 2025-09-06T17:08:44.440Z
Learning: In HistoricalChart.tsx, using XAxis with type="number" and custom tickFormatter can cause empty charts if the lastPolled data contains null values or mixed timestamp formats (seconds vs milliseconds). The default categorical behavior is more robust for handling inconsistent timestamp data.

Applied to files:

  • frontend/src/app/components/HistoricalChart.tsx
  • frontend/src/app/components/HistoricalChart.spec.tsx
🧬 Code graph analysis (30)
frontend/src/app/components/LineChartWrapper.spec.tsx (2)
frontend/src/app/components/LineChartWrapper.tsx (1)
  • LineChartWrapper (66-117)
frontend/src/app/components/__mocks__/chartMocks.ts (1)
  • mockLineData (6-9)
frontend/src/app/components/DeviceOverview.spec.tsx (2)
frontend/src/app/components/DevicesOverview.tsx (1)
  • DevicesOverview (50-246)
frontend/src/app/components/__mocks__/deviceMocks.ts (1)
  • mockDevice (47-59)
frontend/vite.config.ts (1)
frontend/eslint.config.mjs (1)
  • __dirname (6-6)
frontend/src/app/components/__mocks__/deviceMocks.ts (2)
frontend/src/app/types/graphql/GetDeviceInterfaces.ts (1)
  • InterfaceNode (44-73)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/devices/[id]/page.tsx (1)
frontend/src/app/components/ConnectionCharts.tsx (1)
  • ConnectionCharts (64-496)
frontend/src/app/components/DeviceDetails.spec.tsx (2)
frontend/src/app/components/__mocks__/deviceMocks.ts (2)
  • mockMetricsForHost (61-86)
  • mockDevice (47-59)
frontend/src/app/components/DeviceDetails.tsx (1)
  • DeviceDetails (65-444)
frontend/src/app/components/ConnectionCharts.spec.tsx (2)
frontend/src/app/components/ConnectionCharts.tsx (1)
  • ConnectionCharts (64-496)
frontend/src/app/components/__mocks__/deviceMocks.ts (1)
  • mockDevice (47-59)
frontend/src/app/components/ZoneDropdown.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (2)
  • Zone (69-71)
  • ZoneEdge (57-59)
switchmap/server/db/models.py (1)
  • Zone (129-165)
frontend/src/app/layout.spec.tsx (1)
frontend/src/app/layout.tsx (1)
  • RootLayout (43-57)
frontend/src/app/utils/timeStamp.spec.tsx (1)
frontend/src/app/utils/timeStamp.ts (1)
  • formatUnixTimestamp (7-16)
frontend/src/app/theme-toggle.spec.tsx (1)
frontend/src/app/theme-toggle.tsx (1)
  • ThemeToggle (23-43)
frontend/src/app/components/TopologyChart.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
frontend/src/app/components/Sidebar.spec.tsx (1)
frontend/src/app/components/Sidebar.tsx (1)
  • Sidebar (29-136)
frontend/src/app/devices/[id]/page.spec.tsx (2)
frontend/src/app/components/__mocks__/deviceMocks.ts (1)
  • mockDevice (47-59)
frontend/src/app/devices/[id]/page.tsx (1)
  • DevicePage (79-271)
frontend/src/app/utils/time.spec.tsx (1)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
frontend/src/app/components/ConnectionCharts.tsx (3)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/types/graphql/GetDeviceInterfaces.ts (1)
  • InterfaceNode (44-73)
frontend/src/app/components/HistoricalChart.tsx (1)
  • HistoricalChart (48-79)
frontend/src/app/components/ConnectionDetails.spec.tsx (2)
frontend/src/app/components/ConnectionDetails.tsx (1)
  • ConnectionDetails (29-162)
frontend/src/app/components/__mocks__/deviceMocks.ts (1)
  • mockDevice (47-59)
frontend/src/app/components/ZoneDropdown.spec.tsx (1)
frontend/src/app/components/ZoneDropdown.tsx (1)
  • ZoneDropdown (35-211)
switchmap/server/db/schemas.py (1)
switchmap/server/db/models.py (1)
  • Device (168-211)
frontend/src/app/components/Sidebar.tsx (1)
frontend/src/app/theme-toggle.tsx (1)
  • ThemeToggle (23-43)
frontend/src/app/components/HistoricalChart.spec.tsx (2)
frontend/src/app/components/HistoricalChart.tsx (1)
  • HistoricalChart (48-79)
frontend/src/app/components/__mocks__/chartMocks.ts (1)
  • mockData (1-4)
frontend/src/app/components/DeviceDetails.tsx (6)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/components/TopologyChart.tsx (1)
  • TopologyChart (34-579)
frontend/src/app/utils/stringUtils.ts (1)
  • truncateLines (8-28)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
frontend/src/app/utils/timeStamp.ts (1)
  • formatUnixTimestamp (7-16)
frontend/src/app/components/HistoricalChart.tsx (1)
  • HistoricalChart (48-79)
frontend/src/app/components/ConnectionDetails.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (2)
  • DeviceNode (38-48)
  • InterfaceEdge (30-32)
frontend/src/app/types/graphql/GetDeviceInterfaces.ts (3)
  • MacPort (40-42)
  • InterfaceEdge (75-77)
  • InterfaceNode (44-73)
frontend/src/app/page.spec.tsx (1)
frontend/src/app/page.tsx (1)
  • Home (34-232)
frontend/src/app/components/TopologyChart.spec.tsx (2)
frontend/src/app/components/TopologyChart.tsx (1)
  • TopologyChart (34-579)
frontend/src/app/components/__mocks__/deviceMocks.ts (2)
  • mockDeviceLLDP (98-103)
  • mockDevice (47-59)
frontend/src/app/history/page.tsx (1)
frontend/src/app/components/Sidebar.tsx (1)
  • Sidebar (29-136)
frontend/src/app/utils/stringUtils.spec.tsx (1)
frontend/src/app/utils/stringUtils.ts (1)
  • truncateLines (8-28)
frontend/src/app/history/page.spec.tsx (1)
frontend/src/app/history/page.tsx (1)
  • DeviceHistoryChart (85-580)
frontend/src/app/components/DevicesOverview.tsx (3)
frontend/src/app/types/graphql/GetZoneDevices.ts (2)
  • DeviceNode (38-48)
  • InterfaceEdge (30-32)
frontend/src/app/types/graphql/GetDeviceInterfaces.ts (2)
  • InterfaceEdge (75-77)
  • InterfaceNode (44-73)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
frontend/src/app/page.tsx (1)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
🪛 GitHub Actions: PR Workflow
frontend/README.md

[error] 1-1: frontend/README.md is unauthorized to change/delete.

frontend/src/app/components/ConnectionCharts.spec.tsx

[error] 1-1: Test suite failed to run due to module resolution error for recharts in LineChartWrapper/HistoricalChart components.

frontend/src/app/devices/[id]/page.spec.tsx

[error] 11-11: Command: npm run test -- --coverage. Failed to resolve import "recharts" from "src/app/components/HistoricalChart.tsx". Does the file exist?

frontend/src/app/components/HistoricalChart.tsx

[error] 11-11: Command: npm run test -- --coverage. Failed to resolve import "recharts" from "src/app/components/HistoricalChart.tsx". Does the file exist?

frontend/package.json

[error] 1-1: frontend/package.json is unauthorized to change/delete.

frontend/src/app/history/page.spec.tsx

[error] 1-1: Test suite could not be executed due to previous import-resolution failures.

frontend/.gitignore

[error] 1-1: frontend/.gitignore is unauthorized to change/delete.

🪛 Ruff (0.13.1)
switchmap/server/db/schemas.py

214-214: Unused method argument: kwargs

(ARG002)

🔇 Additional comments (33)
frontend/src/app/utils/time.spec.tsx (1)

4-24: Solid coverage of key uptime cases.

Covers boundaries from 0s through multi-day; assertions align with implementation.

frontend/src/app/utils/stringUtils.spec.tsx (1)

4-34: Good coverage across empty, passthrough, and truncation scenarios.

Assertions match current truncateLines behavior.

frontend/src/app/utils/timeStamp.spec.tsx (1)

4-28: Robust and environment-stable assertions.

Comparing to Date(...).toLocaleString() within the same runtime avoids locale flakiness.

frontend/src/app/globals.css (1)

16-16: Confirm --color-border-subtle is consumed (or remove it)

Mapping --color-border-subtle → var(--border-subtle) matches the token pattern; automated sandbox search returned "No files were searched" so please verify that components/utilities or Tailwind config actually reference --color-border-subtle (frontend/src/app/globals.css line 16) — remove it if unused.

frontend/.gitignore (1)

63-67: Don't ignore lockfiles; commit the one your package manager uses.

frontend/.gitignore (lines 63–67) currently ignores package-lock.json, yarn.lock, pnpm-lock.yaml. frontend/package.json has no "packageManager" field and there are no lockfiles in the repo — pick a package manager, remove its lockfile from .gitignore, and commit exactly that lockfile (pnpm-lock.yaml for pnpm).

docs/docs/frontend/types/graphql/GetZoneDevices.md (3)

182-190: Anchor verified — InterfaceNode exists

docs/docs/frontend/types/graphql/GetDeviceInterfaces.md contains "### InterfaceNode" at lines 76–78; cross-reference resolves.


27-33: Removal of lastPolled is incorrect — it’s still used in frontend and backend.

  • Frontend (types/usage): frontend/src/app/types/graphql/GetZoneDevices.ts, frontend/src/app/history/page.tsx, frontend/src/app/devices/[id]/page.tsx, frontend/src/app/components/DeviceDetails.tsx, frontend/src/app/components/ConnectionCharts.tsx, frontend/src/app/components/HistoricalChart.tsx, mocks/specs under frontend/src/app/components/mocks and tests.
  • Backend/tests: switchmap/dashboard/net/routes/api/api.py, switchmap/dashboard/data/system.py, tests/switchmap_/server/api/routes/test_graphql.py, and testdata JSON files (e.g., tests/testdata_/device-01.json, device-03.json).

Revert the docs change or update the backend schema and all frontend callers/mocks/tests together.

Likely an incorrect or invalid review comment.


15-15: Verify 'Defined in' anchors and lastPolled in GetZoneDevices

Exports exist in frontend/src/app/types/graphql/GetZoneDevices.ts (DeviceNode starts at line 38, DeviceEdge at 50, GetZoneDevicesVars at 81) but several docs "Defined in" anchors point to other line numbers (e.g., docs/docs/frontend/types/graphql/GetZoneDevices.md:15 links to types/graphql/GetZoneDevices.ts:49) — update anchors to match the TS file. Also, lastPolled is present in the TS file; confirm whether it should be documented or removed.

frontend/src/app/layout.spec.tsx (2)

17-26: LGTM: layout renders children as expected

Test is minimal and correct.


6-15: Vitest globals enabled — no import required

vite.config.ts sets test.globals: true, so vi is available globally and using vi.mock is fine.

frontend/postcss.config.mjs (1)

1-5: LGTM: Tailwind v4 plugin map format

Config is valid for Tailwind/PostCSS v4.

frontend/src/app/components/DeviceOverview.spec.tsx (1)

10-73: LGTM: solid coverage of states and interactions

Mocks and assertions align with component behavior.

frontend/src/app/components/__mocks__/chartMocks.ts (1)

1-9: LGTM.

Mock shapes look fine for chart tests.

frontend/src/app/components/ConnectionDetails.tsx (1)

121-123: Confirm units for Days Inactive and Speed.

tsIdle type changed to number; ensure values are in expected units for "Days Inactive" label, and consider formatting ifspeed for readability (e.g., Mbps/Gbps).

frontend/src/app/page.spec.tsx (4)

72-77: LGTM.

Good smoke test for critical layout elements.


79-122: LGTM.

Fetch mocking and dedup assertions look sound; localStorage persistence verified.


160-183: LGTM.

Error handling paths are well-covered.


240-255: LGTM.

Scroll behavior test is precise and cleans up state.

frontend/src/app/types/graphql/GetDeviceInterfaces.ts (1)

50-62: Type extensions look correct.

New counters as number|null and tsIdle as number align with charting needs.

Confirm backend resolvers/queries populate these fields (or that UI paths guard nulls) to avoid unexpected undefined at runtime.

frontend/src/app/devices/[id]/page.tsx (1)

169-178: Connection Charts tab wiring LGTM

Loading/error states and rendering ConnectionCharts are correctly integrated.

Confirm NEXT_PUBLIC_GRAPHQL_ENDPOINT is set in CI, or that fetch is mocked in page tests to avoid external calls.

docs/docs/backend/Server.md (1)

2033-2054: Docs for resolve_devices match backend changes

New resolver docs align with schema updates and are clear.

frontend/src/app/history/page.spec.tsx (1)

185-190: LGTM: error handling path is covered

Good coverage for fetch rejection and error UI.

frontend/src/app/components/ConnectionDetails.spec.tsx (1)

18-25: LGTM: table and headers assertions are solid

Validates key columns and section header correctly.

frontend/src/app/components/HistoricalChart.spec.tsx (1)

10-39: Tests are fine; will pass once "recharts" is installed or mocked

Nothing to change in this file; dependency fix required upstream.

Run the verification script in the HistoricalChart.tsx comment to confirm recharts presence and Vitest jsdom config.

frontend/src/app/components/TopologyChart.spec.tsx (1)

216-241: Edge hover/blur interaction tests look good.

frontend/src/app/components/ZoneDropdown.spec.tsx (2)

68-77: Auto-select test is valid and helpful.


159-170: Good coverage for network error path (non-ok response).

frontend/src/app/components/ZoneDropdown.tsx (1)

119-135: Selection sync looks correct.

Auto-fallback to first zone when an invalid id is provided is solid.

If zones can be empty, ensure parent handles onChange not being called (current code is safe).

frontend/src/app/components/DeviceDetails.spec.tsx (3)

38-51: Solid happy-path coverage

Good isolation via TopologyChart mock and assertions for all three charts plus device status.


79-159: Custom range validations are thorough

Edge cases are well covered (duration cap, ordering, in-range filtering). Nice.


161-225: Robust error handling tests

Network failure, HTTP error, GraphQL error, and malformed responses are all asserted. Good use of spies.

frontend/src/app/components/ConnectionCharts.tsx (2)

4-4: HistoricalChart exported both named and default — both import styles are valid

frontend/src/app/components/HistoricalChart.tsx contains both export function HistoricalChart(...) and export default HistoricalChart;, so the default import in ConnectionCharts and the named import in DeviceDetails both work; no change required. Consider standardizing imports for consistency.

Likely an incorrect or invalid review comment.


186-190: Confirm ifspeed units; convert to Mbps if backend provides bps

SNMP ifSpeed is usually in bits/sec — this component labels speed as Mbps but uses iface.ifspeed raw; mocks set ifspeed = 1000 (suggesting Mbps). Confirm whether the GraphQL API returns Mbps or bps. If it returns bps, convert before use (chart + details). Example change:

-              value: iface.ifspeed,
+              value: Math.round((iface.ifspeed ?? 0) / 1_000_000),

Files to check/update: frontend/src/app/components/ConnectionCharts.tsx:186-190 and frontend/src/app/components/ConnectionDetails.tsx:122.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
frontend/src/app/components/TopologyChart.tsx (4)

365-375: Use consistent arrows API shape to disable arrows

Align with the enabled/disabled object form to avoid type mismatches.

Apply:

   networkRef.current.on("blurEdge", (params) => {
@@
-      if (!selectedEdges.includes(params.edge)) {
-        edgesData.current.update({
-          id: params.edge,
-          arrows: { to: false },
-        });
-      }
+      if (!selectedEdges.includes(params.edge)) {
+        edgesData.current.update({
+          id: params.edge,
+          arrows: { to: { enabled: false } },
+        });
+      }
   });
@@
   networkRef.current.on("deselectEdge", (params) => {
@@
-      initialGraph.current.edges.forEach((originalEdge) => {
-        edgesData.current!.update({
-          id: originalEdge.id,
-          color: originalEdge.color || (isDark ? "#444" : "#BBBBBB"),
-          arrows: { to: false },
-        });
-      });
+      initialGraph.current.edges.forEach((originalEdge) => {
+        edgesData.current!.update({
+          id: originalEdge.id,
+          color: originalEdge.color || (isDark ? "#444" : "#BBBBBB"),
+          arrows: { to: { enabled: false } },
+        });
+      });
   });

Also applies to: 386-396


468-490: Simplify reset to avoid redundant dataset churn and race with re-init

setGraph(initialGraph.current) already triggers network re-creation; clearing/adding datasets immediately after can race with cleanup.

Apply:

   const handleReset = () => {
     setInputTerm("");
     setSearchResult("");
     setGraph(initialGraph.current);
-
-    if (!networkRef.current || !nodesData.current || !edgesData.current) return;
-
-    // Clear selection
-    networkRef.current.unselectAll();
-
-    // Instead of clear + add, do update
-    const originalNodes = initialGraph.current.nodes;
-    const originalEdges = initialGraph.current.edges;
-
-    nodesData.current.clear();
-    edgesData.current.clear();
-
-    nodesData.current.add(originalNodes);
-    edgesData.current.add(originalEdges);
-
-    // Reset view
-    networkRef.current.fit();
   };

433-466: Update search highlight on theme change

Theme colors in this effect won’t update until searchTerm changes. Add isDark.

Apply:

-  }, [searchTerm]);
+  }, [searchTerm, isDark]);

155-161: Type the interface node instead of any

Use a typed shape for iface to reduce runtime surprises and improve DX.

For example:

type IfaceNode = {
  cdpcachedeviceid?: string | null;
  cdpcachedeviceport?: string | null;
  lldpremsysname?: string | null;
  lldpremportdesc?: string | null;
};
(device.l1interfaces?.edges ?? []).forEach(({ node: iface }: { node: IfaceNode }) => { ... });
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 674776b and 267c960.

📒 Files selected for processing (6)
  • docs/docs/frontend/components/ConnectionDetails.md (1 hunks)
  • frontend/src/app/components/DeviceDetails.tsx (1 hunks)
  • frontend/src/app/components/LineChartWrapper.spec.tsx (1 hunks)
  • frontend/src/app/components/TopologyChart.spec.tsx (1 hunks)
  • frontend/src/app/components/TopologyChart.tsx (1 hunks)
  • frontend/src/app/components/__mocks__/deviceMocks.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src/app/components/TopologyChart.spec.tsx
  • frontend/src/app/components/LineChartWrapper.spec.tsx
  • frontend/src/app/components/DeviceDetails.tsx
  • docs/docs/frontend/components/ConnectionDetails.md
  • frontend/src/app/components/mocks/deviceMocks.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-30T15:34:06.789Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#333
File: frontend/src/app/components/TopologyChart.tsx:174-178
Timestamp: 2025-07-30T15:34:06.789Z
Learning: In the TopologyChart component, manual cleanup is implemented when devices change to address potential memory leaks from DOM element creation in the htmlTitle function.

Applied to files:

  • frontend/src/app/components/TopologyChart.tsx
🧬 Code graph analysis (1)
frontend/src/app/components/TopologyChart.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Check Docstring Compliance
  • GitHub Check: Test Frontend and Coverage
  • GitHub Check: Checks if number of files changed is acceptable
🔇 Additional comments (2)
frontend/src/app/components/TopologyChart.tsx (2)

162-191: Good fix: stable edge IDs added

Edge IDs are now assigned; reset/update handlers can safely reference them.


405-414: Good: Network cleanup prevents leaks/dupe handlers

Destroying the instance and nulling refs on cleanup is correct.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (5)
frontend/src/app/components/TopologyChart.tsx (5)

277-289: Handle empty graph: destroy instance and clear suggestions.

Prevents stale canvas/listeners and leftover search suggestions.

Apply this diff:

-  // If no graph data is available, do not render the network
-  if (!containerRef.current || graph.nodes.length === 0) return;
+  // If no graph data is available, do not render the network
+  if (!containerRef.current) return;
+  if (graph.nodes.length === 0) {
+    networkRef.current?.destroy();
+    networkRef.current = null;
+    nodesData.current = null;
+    edgesData.current = null;
+    setAllNodeLabels([]);
+    return;
+  }

243-249: Let theme control extra-node color.

Avoid hard-coded color so theme changes apply consistently.

Apply this diff:

       nodesArray.push({
         id: sysName,
         label: sysName,
-        color: "#383e44",
       });

450-454: Use theme-aware colors for search highlight.

Improves contrast in dark mode.

Apply this diff:

-          color: {
-            background: n.id === searchTerm ? "#FF6347" : "#D3D3D3",
-            border: "#555",
-          },
+          color: {
+            background:
+              n.id === searchTerm ? "#FF6347" : (isDark ? "#444" : "#D3D3D3"),
+            border: isDark ? "#999" : "#555",
+          },

469-491: Simplify reset; avoid double work.

Setting graph and also clearing/adding DataSets duplicates effort. Prefer one path.

Consider relying solely on setGraph(initialGraph.current) and let the [graph, options] effect recreate the instance, or skip setGraph and only restore via DataSet operations—don’t do both.


196-208: Optional: build titles via DOM, not innerHTML.

You escape dynamic fields, which is good. For defense-in-depth, construct the tooltip via createElement/textContent.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 267c960 and d1278ba.

📒 Files selected for processing (1)
  • frontend/src/app/components/TopologyChart.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-30T15:34:06.789Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#333
File: frontend/src/app/components/TopologyChart.tsx:174-178
Timestamp: 2025-07-30T15:34:06.789Z
Learning: In the TopologyChart component, manual cleanup is implemented when devices change to address potential memory leaks from DOM element creation in the htmlTitle function.

Applied to files:

  • frontend/src/app/components/TopologyChart.tsx
🧬 Code graph analysis (1)
frontend/src/app/components/TopologyChart.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Application
🔇 Additional comments (1)
frontend/src/app/components/TopologyChart.tsx (1)

77-115: Options reactivity: good fix.

Including isDark, clickToUse, and zoomView in memo deps and using options in effect deps looks correct.

coderabbitai[bot]
coderabbitai bot previously approved these changes Sep 23, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
frontend/src/app/components/TopologyChart.tsx (4)

349-355: Unify edge reset color with options to avoid theme inconsistency

Resets use "#444"/"#BBBBBB" while options use "#888"/"#BBB". Align them.

Apply this diff:

-        edgesData.current!.update({
-          id: originalEdge.id!,
-          color: isDark ? "#444" : "#BBBBBB",
-        });
+        edgesData.current!.update({
+          id: originalEdge.id!,
+          color: isDark ? "#888" : "#BBB",
+        });
-        edgesData.current!.update({
-          id: originalEdge.id!,
-          color: isDark ? "#444" : "#BBBBBB",
-          arrows: { to: false },
-        });
+        edgesData.current!.update({
+          id: originalEdge.id!,
+          color: isDark ? "#888" : "#BBB",
+          arrows: { to: false },
+        });

Also applies to: 392-397


439-469: Make search robust to label vs id mismatch

Currently nodesData.get(searchTerm) assumes label===id. Add a label fallback.

Apply this diff:

-    const node = nodesData.current.get(searchTerm);
-    if (!node) {
-      setSearchResult("No results found for: " + searchTerm);
-      return;
-    } else {
-      // Highlight the node and focus the network
-      setSearchResult("Showing results for: " + searchTerm);
-
-      networkRef.current.focus(searchTerm, { scale: 1.5, animation: true });
+    const byId = nodesData.current.get(searchTerm as any);
+    const node = Array.isArray(byId) ? byId[0] : byId;
+    let nodeId = searchTerm;
+    if (!node) {
+      const matches = nodesData.current.get({
+        filter: (n: any) =>
+          String(n.label ?? "").toLowerCase() === searchTerm.toLowerCase(),
+      }) as any[];
+      if (!matches || matches.length === 0) {
+        setSearchResult("No results found for: " + searchTerm);
+        return;
+      }
+      nodeId = String(matches[0].id);
+    }
+    // Highlight the node and focus the network
+    setSearchResult("Showing results for: " + nodeId);
+
+    networkRef.current.focus(nodeId, { scale: 1.5, animation: true });
@@
-      nodesData.current.get().forEach((n) => {
+      nodesData.current.get().forEach((n) => {
         nodesData.current!.update({
           id: n.id,
           color: {
-            background: n.id === searchTerm ? "#FF6347" : "#D3D3D3",
+            background: n.id === nodeId ? "#FF6347" : "#D3D3D3",
             border: "#555",
           },
           font: {
             color:
-              n.id === searchTerm
+              n.id === nodeId
                 ? isDark
                   ? "#fff"
                   : "black"
                 : isDark
                 ? "#aaa"
                 : "#A9A9A9",
           },
         });
       });
-    }
+    

246-250: Theme-aware color for extra nodes (optional)

Consider adjusting extra node color per theme for visibility.

Apply this diff:

-      nodesArray.push({
-        id: sysName,
-        label: sysName,
-        color: "#383e44",
-      });
+      nodesArray.push({
+        id: sysName,
+        label: sysName,
+        color: isDark ? "#383e44" : "#E0E0E0",
+      });

155-193: Deduplicate edges to reduce visual noise (optional)

If CDP/LLDP produce repeats, add a Set of keys like ${from}__${to}__${title} to skip duplicates when pushing edges.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1278ba and 4ede812.

📒 Files selected for processing (1)
  • frontend/src/app/components/TopologyChart.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-30T15:34:06.789Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#333
File: frontend/src/app/components/TopologyChart.tsx:174-178
Timestamp: 2025-07-30T15:34:06.789Z
Learning: In the TopologyChart component, manual cleanup is implemented when devices change to address potential memory leaks from DOM element creation in the htmlTitle function.

Applied to files:

  • frontend/src/app/components/TopologyChart.tsx
🧬 Code graph analysis (1)
frontend/src/app/components/TopologyChart.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
🔇 Additional comments (4)
frontend/src/app/components/TopologyChart.tsx (4)

77-115: Options react correctly to prop/theme changes

Including clickToUse/zoomView/isDark in deps is correct; options now re-compute and re-init as expected.


163-176: Stable edge IDs implemented

Assigning id on edges resolves prior reset/update issues that referenced originalEdge.id.

Also applies to: 177-189


194-206: XSS hardening looks good

Escaping dynamic fields and wrapping via htmlTitle prevents injection in tooltips.

Also applies to: 216-222, 174-175, 188-189


408-417: Network cleanup on dependency changes is correct

Destroying the previous instance and nulling refs prevents leaks and duplicate handlers.

coderabbitai[bot]
coderabbitai bot previously approved these changes Sep 23, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
frontend/src/app/components/TopologyChart.tsx (5)

284-291: Fit view after creating the Network.

Ensures the initial layout is fully visible; also aligns with the comment about moving fit() to this effect.

   networkRef.current = new Network(
     containerRef.current,
     {
       nodes: nodesData.current,
       edges: edgesData.current,
     },
     options
   );
+  networkRef.current.fit();

349-355: Align edge reset colors with options for consistency.

Use the same theme colors as in options (dark: #888, light: #BBB) instead of #444/#BBBBBB.

-        edgesData.current!.update({
-          id: originalEdge.id!,
-          color: isDark ? "#444" : "#BBBBBB",
-        });
+        edgesData.current!.update({
+          id: originalEdge.id!,
+          color: isDark ? "#888" : "#BBB",
+        });
-        edgesData.current!.update({
-          id: originalEdge.id!,
-          color: isDark ? "#444" : "#BBBBBB",
-          arrows: { to: false },
-        });
+        edgesData.current!.update({
+          id: originalEdge.id!,
+          color: isDark ? "#888" : "#BBB",
+          arrows: { to: false },
+        });

Also applies to: 392-397


155-161: Avoid any for interface nodes; add a minimal shape.

Improves type safety and readability.

-      (device.l1interfaces?.edges ?? []).forEach(
-        ({ node: iface }: { node: any }) => {
+      (device.l1interfaces?.edges ?? []).forEach(
+        ({
+          node: iface,
+        }: {
+          node: {
+            cdpcachedeviceid?: string;
+            cdpcachedeviceport?: string;
+            lldpremsysname?: string;
+            lldpremportdesc?: string;
+          };
+        }) => {

63-63: Use resolvedTheme for accurate dark/light detection.

Handles "system" theme correctly.

-  const { theme } = useTheme();
+  const { resolvedTheme } = useTheme();
@@
-  const isDark = theme === "dark";
+  const isDark = resolvedTheme === "dark";

Also applies to: 76-76


437-469: Include theme in search highlight deps.

Ensures highlighted colors update when theme toggles.

-  }, [searchTerm]);
+  }, [searchTerm, isDark]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ede812 and c610857.

📒 Files selected for processing (1)
  • frontend/src/app/components/TopologyChart.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-30T15:34:06.789Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#333
File: frontend/src/app/components/TopologyChart.tsx:174-178
Timestamp: 2025-07-30T15:34:06.789Z
Learning: In the TopologyChart component, manual cleanup is implemented when devices change to address potential memory leaks from DOM element creation in the htmlTitle function.

Applied to files:

  • frontend/src/app/components/TopologyChart.tsx
🧬 Code graph analysis (1)
frontend/src/app/components/TopologyChart.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Application
🔇 Additional comments (4)
frontend/src/app/components/TopologyChart.tsx (4)

279-283: Handle empty graphs by destroying any existing Network instance.

Prevents stale canvases if graph becomes empty.

-  // If no graph data is available, do not render the network
-  if (!containerRef.current || graph.nodes.length === 0) return;
+  // If no graph data is available, do not render the network
+  if (!containerRef.current) return;
+  if (graph.nodes.length === 0) {
+    networkRef.current?.destroy();
+    networkRef.current = null;
+    nodesData.current = null;
+    edgesData.current = null;
+    setAllNodeLabels([]);
+    return;
+  }

163-176: Good: stable edge IDs and sanitized titles.

Edge IDs are now assigned and titles are escaped/HTMLElement-wrapped. This unblocks reset logic and removes XSS risk.

Also applies to: 177-190


77-115: Good: options are memoized with reactive deps.

Including isDark, clickToUse, and zoomView ensures runtime toggles take effect.


408-417: Good: Network cleanup on effect teardown.

Destroying the instance and clearing DataSets prevents leaks and duplicate handlers.

coderabbitai[bot]
coderabbitai bot previously approved these changes Sep 23, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
frontend/src/app/components/TopologyChart.tsx (5)

286-288: Avoid stale suggestions when graph becomes empty

When graph.nodes.length === 0 you early return without clearing allNodeLabels, leaving stale search suggestions.

Apply this diff:

-    // If no graph data is available, do not render the network
-    if (!containerRef.current || graph.nodes.length === 0) return;
+    // If no graph data is available, do not render the network
+    if (!containerRef.current) return;
+    if (graph.nodes.length === 0) {
+      setAllNodeLabels([]);
+      return;
+    }

299-299: Reset edge colors consistently using Options; avoid hardcoded theme color drift

Use the current options.edges.color for resets so colors match the active theme config.

Apply these diffs:

Introduce a base color right after creating the Network:

     );
 
+    // Resolve default edge color from current options for consistent resets
+    const baseEdgeColor =
+      typeof options.edges?.color === "string"
+        ? (options.edges!.color as string)
+        : isDark
+        ? "#888"
+        : "#BBB";

Use it in deselectNode:

-      initialGraph.current.edges.forEach((originalEdge) => {
-        edgesData.current!.update({
-          id: originalEdge.id!,
-          color: isDark ? "#444" : "#BBBBBB",
-        });
-      });
+      initialGraph.current.edges.forEach((originalEdge) => {
+        edgesData.current!.update({
+          id: originalEdge.id!,
+          color: baseEdgeColor,
+        });
+      });

Use it in deselectEdge:

-      initialGraph.current.edges.forEach((originalEdge) => {
-        edgesData.current!.update({
-          id: originalEdge.id!,
-          color: isDark ? "#444" : "#BBBBBB",
-          arrows: { to: false },
-        });
-      });
+      initialGraph.current.edges.forEach((originalEdge) => {
+        edgesData.current!.update({
+          id: originalEdge.id!,
+          color: baseEdgeColor,
+          arrows: { to: false },
+        });
+      });

Also applies to: 356-361, 398-405


303-311: Use VisNode typing in doubleClick handler instead of any

Removes unnecessary array check and any-casts; improves type safety.

Apply this diff:

-        const nodeData = nodesData.current?.get(nodeId);
-        const node = Array.isArray(nodeData) ? nodeData[0] : nodeData;
-
-        // Only navigate if idxDevice exists
-        if ((node as any)?.idxDevice) {
-          const idxDevice = (node as any).idxDevice;
-          const sysName = (node as any)?.label ?? "";
+        const node = nodesData.current?.get(nodeId) as VisNode | undefined;
+        if (node?.idxDevice) {
+          const idxDevice = node.idxDevice;
+          const sysName = String(node.label ?? "");

443-475: Re-apply highlight after network re-create/theme change

When options/graph changes (network re-created), the searchTerm highlight is lost; include graph/options in deps to re-apply.

Apply this diff:

-  }, [searchTerm]);
+  }, [searchTerm, graph, options]);

487-499: Simplify reset: rely on setGraph + effect to re-create network

Clearing/re-adding DataSets is redundant since setGraph triggers the effect to rebuild the Network; consider removing mid-flight mutations to avoid flicker.

Suggested simplification:

  • Keep setGraph(initialGraph.current) and unselectAll().
  • Drop nodesData.current.clear()/edgesData.current.clear()/add(...) and just call fit() after the effect runs (or omit fit() and let initial mount behavior handle it).
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c610857 and d62ef42.

📒 Files selected for processing (1)
  • frontend/src/app/components/TopologyChart.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-30T15:34:06.789Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#333
File: frontend/src/app/components/TopologyChart.tsx:174-178
Timestamp: 2025-07-30T15:34:06.789Z
Learning: In the TopologyChart component, manual cleanup is implemented when devices change to address potential memory leaks from DOM element creation in the htmlTitle function.

Applied to files:

  • frontend/src/app/components/TopologyChart.tsx
📚 Learning: 2025-09-23T19:27:36.395Z
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#341
File: frontend/src/app/components/TopologyChart.tsx:231-231
Timestamp: 2025-09-23T19:27:36.395Z
Learning: In TopologyChart component, vis-network tooltips use their own styling context separate from the application's theme system, so hardcoded white text color works correctly against the tooltip's background in both light and dark themes.

Applied to files:

  • frontend/src/app/components/TopologyChart.tsx
🧬 Code graph analysis (1)
frontend/src/app/components/TopologyChart.tsx (2)
frontend/src/app/types/graphql/GetZoneDevices.ts (1)
  • DeviceNode (38-48)
frontend/src/app/utils/time.ts (1)
  • formatUptime (6-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Application
🔇 Additional comments (3)
frontend/src/app/components/TopologyChart.tsx (3)

165-193: Good: Stable edge IDs and sanitized tooltips

  • Stable edge IDs resolve update/reset correctness.
  • Tooltips are properly escaped via escapeHtml and rendered via HTMLElement.

Also applies to: 197-209, 213-247


414-424: Good: Proper Network cleanup to prevent leaks

Cleanup destroys the Network and nulls DataSets, avoiding duplicate handlers and memory leaks.


288-289: Fix TS generic mismatch: use DataSet

nodesData is typed as DataSet, but you’re instantiating DataSet. This will fail type-checking and weakens type safety for idxDevice.

Apply this diff:

-    nodesData.current = new DataSet<Node>(graph.nodes);
+    nodesData.current = new DataSet<VisNode>(graph.nodes);

@Abhi-MS
Copy link
Author

Abhi-MS commented Sep 23, 2025

@palisadoes @aashimawadhwa @DMills27 Code rabbit has approved this PR, please review.

@aashimawadhwa aashimawadhwa merged commit eee9db6 into PalisadoesFoundation:develop-abhi Sep 25, 2025
9 of 11 checks passed
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.

2 participants