Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@alexandrabain
Copy link

@alexandrabain alexandrabain commented Nov 5, 2025

Changes:
[Note dependent on https://github.com/truenas/middleware/pull/17514]

  1. Section to display Non-TrueNAS deployed app i.e., apps deployed through tools such as Dockge or through Docker CLI
  • Includes display only, no lifecycle management of these Apps
  • Does Include Resources
  1. Added header row for the TrueNAS managed Apps
  2. Added collapse ability on the section headers
  3. Added overall Apps Utilization Row.
image image

Testing:

Tested new and upgraded deployments, with a variety of apps in both categories.
Tested existing controls for TrueNAS Managed apps remain functional
Tested sorting of application names worked in each group
Tested filtering works across each group.

Downstream

Affects Reasoning
Documentation Revised UI with a new section for Other Apps and with a collapsible section
Testing Need to expand to include having apps deployed means other than just the TrueNAS Managed option to test the table - recommend Auto tests.

alexandrabain and others added 15 commits October 27, 2025 14:45
- Add 'source' field to App TypeScript interface
- Source can be 'truenas' or 'external'
- Aligns with middleware API schema changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add isExternalApp computed property to app-row and app-info-card
- Disable start/stop/restart buttons for external apps
- Disable edit/update/convert/rollback/delete buttons for external apps
- Show "Manage by deployment tool" tooltip for disabled buttons
- Roll Back button now hidden entirely for external apps

External apps (deployed via Portainer, Docker CLI, etc.) can only be
monitored in TrueNAS. Management operations must be performed through
the original deployment tool.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Hide Workloads, Notes, and Metadata tiles for external apps since
they are not managed by TrueNAS and these details are not relevant.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…f version info

For external apps in Application Info tile:
- Replace App Version and Version fields with "Deployment: Deployed via External Tool"
- Hide Source and Train fields (not relevant for external apps)
- Keep Name field visible

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Hide checkbox in app-row for external apps
- Filter external apps from checkedApps, activeCheckedApps, and stoppedCheckedApps
- Update allAppsChecked to only consider TrueNAS apps
- Update toggleAppsChecked to only select TrueNAS apps
- Prevents bulk actions (start/stop/update/delete) on external apps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@claude
Copy link

claude bot commented Nov 5, 2025

Claude finished @alexandrabain's task —— View job


Code Review Complete! 🎉

Great work on adding external app visualization and statistics! The implementation is solid with comprehensive tests and good architectural decisions. Here's my feedback:

What's Working Well

The PR demonstrates strong adherence to Angular best practices, excellent test coverage for external app filtering logic, and thoughtful UX with collapsible sections and keyboard navigation. Nice work on the shared utility functions!


🔍 Key Findings

1. Memory Leak in totalUtilization$ Observable (Critical)

Location: installed-apps-list.component.ts:534-579

The totalUtilization$ observable creates subscriptions inside combineLatest without proper cleanup:

readonly totalUtilization$ = this.installedAppsStore.installedApps$.pipe(
  switchMap((apps) => {
    return combineLatest(
      apps.map((app) => this.getAppStats(app.name)),
    ).pipe(
      takeUntilDestroyed(this.destroyRef), // ⚠️ Only cleans inner observable
      map((statsArray) => { /* ... */ }),
    );
  }),
  shareReplay({ bufferSize: 1, refCount: true }),
);

Problem: When installedApps$ emits a new array, the switchMap creates a new combineLatest, but the takeUntilDestroyed inside only cleans up that inner subscription. The outer stream isn't cleaned up.

Fix: Move takeUntilDestroyed to the outer pipe:

readonly totalUtilization$ = this.installedAppsStore.installedApps$.pipe(
  switchMap((apps) => {
    if (!apps?.length || !apps.some((app) => !!app)) {
      return of({ cpu: 0, memory: 0, blkioRead: 0, blkioWrite: 0, networkRx: 0, networkTx: 0 });
    }
    return combineLatest(
      apps.map((app) => this.getAppStats(app.name)),
    ).pipe(
      map((statsArray) => { /* ... */ }),
    );
  }),
  takeUntilDestroyed(this.destroyRef), // ✅ Clean up outer stream
  shareReplay({ bufferSize: 1, refCount: true }),
);

2. Duplicate External App Detection Logic

Locations:

  • app-row.component.ts:71-73 - isExternalApp computed
  • app-info-card.component.ts:91 - Inline check this.app()?.source === 'external'
  • app-details-panel.component.ts:30-32 - Inline check this.app()?.source === 'external'

Issue: You created excellent utility functions (isExternalApp, isTruenasApp) in app-type.utils.ts, but they're not consistently used. Some components use the utility, others duplicate the logic.

Recommendation: Use isExternalApp() from the utility everywhere for consistency:

// app-info-card.component.ts:91
protected readonly isExternalApp = computed<boolean>(() => isExternalApp(this.app()));

// app-details-panel.component.ts:30
protected readonly isExternalApp = computed(() => isExternalApp(this.app()));

3. Keyboard Event Handler Order Issue

Location: installed-apps-list.component.html:179

(keydown.enter)="externalAppsExpanded.set(!externalAppsExpanded())"
(keydown.space)="externalAppsExpanded.set(!externalAppsExpanded()); $event.preventDefault()"

Issue: The keydown.space handler has $event.preventDefault() to prevent page scrolling, but the keydown.enter handler doesn't. This could cause unexpected navigation behavior if the section header is focused.

Recommendation: While Enter typically doesn't cause page scroll issues, consider whether you want to prevent default behavior consistently for both keys. If the header is acting as a pure button, both should prevent default:

(keydown.enter)="externalAppsExpanded.set(!externalAppsExpanded()); $event.preventDefault()"
(keydown.space)="externalAppsExpanded.set(!externalAppsExpanded()); $event.preventDefault()"

Or keep it as-is if you specifically want Enter to maintain default behavior (which is usually fine for role="button" elements).


4. Accessibility Enhancement Opportunity

Location: installed-apps-list.component.html:132-143 and 171-183

The collapsible section headers have good ARIA attributes but could benefit from an aria-label for screen readers:

<h3
  class="section-header"
  role="button"
  tabindex="0"
  [attr.aria-expanded]="truenasAppsExpanded()"
  [attr.aria-controls]="'truenas-apps-section'"
  [attr.aria-label]="('Toggle TrueNAS Apps section' | translate)"
  (click)="truenasAppsExpanded.set(!truenasAppsExpanded())"
  ...
>

This makes the purpose clearer for assistive technology users.


5. Test Coverage Gap

Location: installed-apps-list.component.spec.ts

The tests thoroughly cover external app filtering and bulk operations, but there's no test for:

  • Collapsible section toggling (expanding/collapsing TrueNAS Apps and Other Apps)
  • The total utilization row calculation with mixed TrueNAS/external apps
  • Keyboard navigation on section headers

Recommendation: Add tests like:

it('toggles TrueNAS apps section visibility', () => {
  const component = spectator.component;
  
  expect(component.truenasAppsExpanded()).toBe(true);
  component.truenasAppsExpanded.set(false);
  spectator.detectChanges();
  
  expect(component.truenasAppsExpanded()).toBe(false);
  expect(spectator.query('#truenas-apps-section')).not.toExist();
});

6. Minor: Typo in Utility Comment

Location: app-type.utils.ts:5

// External apps are deployed outside TrueNAS (e.g., via Docker CLI, Portainer, Dockage).

"Dockage" should be "Dockge" (the actual tool name).


7. Consider i18n for "Other Apps"

Location: Multiple files use the term "Other Apps" for external apps

While "Other Apps" is translated ({{ 'Other Apps' | translate }}), consider if this is the clearest label for users. Alternatives might be:

  • "External Apps" (more explicit about source)
  • "Third-Party Apps"
  • "Custom Deployed Apps"

This is mostly a UX/product decision, but worth discussing with the team.


📋 Overall Assessment

This is a well-structured PR with excellent separation of concerns and comprehensive tests for the new external app filtering logic. The main concern is the memory leak in the statistics aggregation observable. Once that's addressed, this will be production-ready!


@codecov
Copy link

codecov bot commented Nov 5, 2025

⚠️ JUnit XML file not found

The CLI was unable to find any JUnit XML files to upload.
For more help, visit our troubleshooting guide.

alexandrabain and others added 12 commits November 6, 2025 10:26
- Add null safety checks (app.source ?? 'truenas') for backward compatibility
- Replace manual Observable with of() in totalUtilizationAnalysis getter
- Ensures external apps filtering works with existing apps that may not have source field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add source field to mock app data
- Mock AppsStatsService.getStatsForApp() to return valid observable
- Prevents "undefined where stream was expected" error in combineLatest

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add optional chaining for app.source in isExternalApp computed
- Add source field to app-row test mock data
- Ensures backward compatibility with apps that don't have source field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add source field to mock app data
- Mock AppsStatsService.getStatsForApp() to return valid observable
- Prevents combineLatest error in tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add null safety checks to incomingTrafficBits and outgoingTrafficBits computed signals
- Add keyboard navigation (Enter/Space) to collapsible section headers
- Add tabindex="0" for keyboard accessibility on section headers
- Prevents runtime errors when stats are null/undefined (e.g., stopped apps)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Fix stats aggregation performance: Convert getter to cached observable with shareReplay
- Add ARIA attributes (role, aria-expanded, aria-controls) for screen reader support
- Add helper methods (isExternalApp, isTruenasApp) for consistent app type checking
- Add defensive null checks to network stats (rx_bytes, tx_bytes)
- Improves maintainability and eliminates duplicate null-handling logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add external app (external-nginx) to test data
- Test filtering of TrueNAS vs external apps
- Test bulk selection excludes external apps
- Test active/stopped checked apps exclude external apps
- Update bulk update test to verify only TrueNAS apps included
- Improves test coverage for the main feature of this PR

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…ation

- Add optional chaining (stats?.cpu_usage, stats?.memory) for top-level properties
- Add optional chaining (net?.rx_bytes, net?.tx_bytes) for network stats
- Prevents potential runtime errors with null/undefined stats from external apps
- All stats properties now gracefully handle missing data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…leanup

- Replace @UntilDestroy() decorator with DestroyRef inject pattern
- Replace untilDestroyed(this) with takeUntilDestroyed(this.destroyRef)
- Update installed-apps-list.component.ts
- Update app-info-card.component.ts
- Follows modern Angular standards per CLAUDE.md guidelines

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…ggregation

- Add safeAdd() helper to prevent NaN from null/undefined/non-number values
- Add safeNetworkSum() helper for safe network stats accumulation
- Improve empty check: !apps?.length || !apps.some((app) => !!app)
- Add typeof stats !== 'object' check to validate stats shape
- All math operations now guaranteed to return valid numbers, never NaN
- Handles all edge cases: null apps, undefined stats, missing properties, empty arrays

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Move $event.preventDefault() before signal update
- Ensures preventDefault() is called even if signal update throws error
- Prevents unwanted page scroll on space key press
- Applied to both TrueNAS Apps and Other Apps section headers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…nal app detection utility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
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