From 153768fb2163671c7243fe214a6aab467d1bec74 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 15 Oct 2025 15:23:11 +0300 Subject: [PATCH 1/5] UI: Automatic reloading of the connections list https://github.com/safing/portmaster/issues/2039 --- .../shared/netquery/netquery.component.html | 55 ++++++++++-- .../app/shared/netquery/netquery.component.ts | 87 +++++++++++++++++-- .../src/app/shared/pipes/time-ago.pipe.ts | 2 +- 3 files changed, 130 insertions(+), 14 deletions(-) diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.html b/desktop/angular/src/app/shared/netquery/netquery.component.html index 6b5dfbf0..dac7211c 100644 --- a/desktop/angular/src/app/shared/netquery/netquery.component.html +++ b/desktop/angular/src/app/shared/netquery/netquery.component.html @@ -268,17 +268,58 @@ -
+ +
+ {{ totalResultCount }} Results of {{totalConnCount}} total connections - - Last Reload: {{ lastReload | timeAgo:(lastReloadTicker|async) }} - + +
+
+ + + +
+ + + {{autoReloadIntervalName}} + + + + (in {{interval}} sec) + + + + + + + Last Reload: {{ lastReload | timeAgo:(lastReloadTicker|async) }} + + + + + +
+
+ + + + {{value}} + + +
+
+
diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.ts b/desktop/angular/src/app/shared/netquery/netquery.component.ts index a09d18eb..a0d4aea8 100644 --- a/desktop/angular/src/app/shared/netquery/netquery.component.ts +++ b/desktop/angular/src/app/shared/netquery/netquery.component.ts @@ -5,8 +5,8 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { BandwidthChartResult, ChartResult, Condition, Database, FeatureID, GreaterOrEqual, IPScope, LessOrEqual, Netquery, NetqueryConnection, OrderBy, Pin, PossilbeValue, Query, QueryResult, SPNService, Select, Verdict } from "@safing/portmaster-api"; import { Datasource, DynamicItemsPaginator, SelectOption } from "@safing/ui"; -import { BehaviorSubject, Observable, Subject, combineLatest, forkJoin, interval, of } from "rxjs"; -import { catchError, debounceTime, filter, map, share, skip, switchMap, take, takeUntil } from "rxjs/operators"; +import { BehaviorSubject, Observable, Subject, combineLatest, forkJoin, interval, of, timer } from "rxjs"; +import { catchError, debounceTime, filter, map, share, skip, startWith, switchMap, take, takeUntil } from "rxjs/operators"; import { ActionIndicatorService } from "../action-indicator"; import { ExpertiseService } from "../expertise"; import { objKeys } from "../utils"; @@ -62,6 +62,15 @@ const orderByKeys: (keyof Partial)[] = [ 'profile', ] +export const reloadIntervalValues: { [key: string]: number } = { + "⏸\u00A0\u00A0Don't auto-reload": 0, + "↻\u00A0\u00A0Reload every 10 seconds": 10, + "↻\u00A0\u00A0Reload every 30 seconds": 30, + "↻\u00A0\u00A0Reload every 1 minute": 60, + "↻\u00A0\u00A0Reload every 5 minutes": 300, + "↻\u00A0\u00A0Reload every 30 minutes": 1800, +} + interface LocalQueryResult extends QueryResult { _chart: Observable | null; _group: Observable> | null; @@ -246,6 +255,48 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { share() ) + + /** Auto-reload: The list of all intervals */ + readonly reloadIntervals = Object.keys(reloadIntervalValues); + /** Auto-reload: The name of the currently selected auto-reload interval */ + autoReloadIntervalName: string = ''; + /** Auto-reload: The timestamp of auto-reload being enabled */ + autoReloadEnabledTimestamp: Date | null = null; + /** Auto-reload: Enable/disable auto-reload and set the interval */ + onAutoRefreshChange(intervalName: string) { + let delaySec = reloadIntervalValues[intervalName] || 0; + if (delaySec <= 0) { + this.autoReloadIntervalName = ''; + return; + } + this.autoReloadEnabledTimestamp = new Date(); + this.autoReloadIntervalName = intervalName; + } + /** Auto-reload: An observable that emits the remaining seconds until the next reload, and triggers reloads*/ + autoReloadInterval$ = interval(900) // use less than 1 second to prevent skipping a second + .pipe( + startWith(0), // Emit immediately when subscribed + takeUntilDestroyed(this.destroyRef), + filter(() => !!this.autoReloadIntervalName), // Only emit when auto-reload is enabled + map(() => { + if (this.loading) return 0; + + const intervalSeconds = reloadIntervalValues[this.autoReloadIntervalName] || 0; + if (intervalSeconds <= 0) return 0; + + let startTime = (this.autoReloadEnabledTimestamp && this.autoReloadEnabledTimestamp > this.lastReload) ? this.autoReloadEnabledTimestamp : this.lastReload; + const elapsedSeconds = Math.floor((new Date().getTime() - startTime.getTime()) / 1000); + const remainingSeconds = intervalSeconds - elapsedSeconds; + + if (remainingSeconds <= 0) { + this.performSearch(); // Trigger reload when time is up + return 0; + } + return remainingSeconds; + }), + share() + ); + // whether or not the history database should be queried as well. get useHistory() { return this.dateFilter?.length; @@ -552,8 +603,8 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { // The actual searching is debounced by second so we don't flood the Portmaster service // with queries while the user is still configuring their filters. this.search$ - .pipe( - debounceTime(1000), + .pipe( + this.adaptiveDebounce(1000, () => this.lastReload.getTime()), switchMap(() => { this.loading = true; this.connectionChartData = []; @@ -659,6 +710,7 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { } this.skipUrlUpdate = false; + this.lastReload = new Date(); this.loading = false; this.cdr.markForCheck(); }) @@ -810,6 +862,30 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { this.helper.dispose(); } +/** + * Delays emissions only when last operation was recent. + * + * @param minDelayMs - Minimum milliseconds between operations + * @param getLastOperationTime - Function returning last operation timestamp + * + * @example + * // Delay search only if last search was < 1 second ago + * this.search$.pipe(adaptiveDebounce(1000, () => this.lastReload.getTime())) + */ + adaptiveDebounce( minDelayMs: number, getLastOperationTime: () => number) { + return (source: Observable) => source.pipe( + switchMap((value) => { + const timeSinceLastOperation = Date.now() - getLastOperationTime(); + if (timeSinceLastOperation >= minDelayMs) { + return of(value); // Execute immediately + } else { + const remainingDelay = minDelayMs - timeSinceLastOperation; + return timer(remainingDelay).pipe(map(() => value)); + } + }) + ); +} + // lazyLoadGroup returns an observable that will emit a DynamicItemsPaginator once subscribed. // This is used in "group-by" views to lazy-load the content of the group once the user // expands it. @@ -1001,8 +1077,7 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { /** @private Query the portmaster service for connections matching the current settings */ performSearch() { - this.loading = true; - this.lastReload = new Date(); + this.loading = true; this.paginator.clear() this.search$.next(); this.updateTagbarValues(); diff --git a/desktop/angular/src/app/shared/pipes/time-ago.pipe.ts b/desktop/angular/src/app/shared/pipes/time-ago.pipe.ts index 25f53ac7..a0c2821e 100644 --- a/desktop/angular/src/app/shared/pipes/time-ago.pipe.ts +++ b/desktop/angular/src/app/shared/pipes/time-ago.pipe.ts @@ -52,5 +52,5 @@ export function timeAgo(value: number | Date | string) { } } - return "< 1 min" + suffix // actually just now (diffInSeconds == 0) + return "< 1 min " + suffix // actually just now (diffInSeconds == 0) } From 54fe389c9d9e3d42a01a6d36ad772ec993288618 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 15 Oct 2025 16:13:55 +0300 Subject: [PATCH 2/5] UI: skip auto-reload for historical data queries https://github.com/safing/portmaster/issues/2039 --- .../angular/src/app/shared/netquery/netquery.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.ts b/desktop/angular/src/app/shared/netquery/netquery.component.ts index a0d4aea8..f2168add 100644 --- a/desktop/angular/src/app/shared/netquery/netquery.component.ts +++ b/desktop/angular/src/app/shared/netquery/netquery.component.ts @@ -281,6 +281,11 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { map(() => { if (this.loading) return 0; + if (this.dateFilter?.length >= 2 && this.dateFilter[1].getTime() <= this.lastReload.getTime()) { + // Skip reload when dateFilter[1] (end date) <= lastReload (no new results expected) + return 0; + } + const intervalSeconds = reloadIntervalValues[this.autoReloadIntervalName] || 0; if (intervalSeconds <= 0) return 0; From 2dc2193587c7706bc343d8191e319a16bdd34a54 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 15 Oct 2025 17:43:02 +0300 Subject: [PATCH 3/5] feat: add manual trigger for lastReloadTicker updates Add Subject-based manual trigger to immediately update "Last reload X ago" display when search completes, instead of waiting up to 2 seconds for next interval tick. --- .../src/app/shared/netquery/netquery.component.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.ts b/desktop/angular/src/app/shared/netquery/netquery.component.ts index f2168add..6e5268ca 100644 --- a/desktop/angular/src/app/shared/netquery/netquery.component.ts +++ b/desktop/angular/src/app/shared/netquery/netquery.component.ts @@ -5,7 +5,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { BandwidthChartResult, ChartResult, Condition, Database, FeatureID, GreaterOrEqual, IPScope, LessOrEqual, Netquery, NetqueryConnection, OrderBy, Pin, PossilbeValue, Query, QueryResult, SPNService, Select, Verdict } from "@safing/portmaster-api"; import { Datasource, DynamicItemsPaginator, SelectOption } from "@safing/ui"; -import { BehaviorSubject, Observable, Subject, combineLatest, forkJoin, interval, of, timer } from "rxjs"; +import { BehaviorSubject, Observable, Subject, combineLatest, forkJoin, interval, merge, of, timer } from "rxjs"; import { catchError, debounceTime, filter, map, share, skip, startWith, switchMap, take, takeUntil } from "rxjs/operators"; import { ActionIndicatorService } from "../action-indicator"; import { ExpertiseService } from "../expertise"; @@ -248,7 +248,11 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { lastReload: Date = new Date(); /** @private Used to refresh the "Last reload xxx ago" message */ - lastReloadTicker = interval(2000) + private lastReloadTickerForceUpdate$ = new Subject(); + lastReloadTicker = merge( + interval(2000), + this.lastReloadTickerForceUpdate$.pipe(takeUntilDestroyed(this.destroyRef)) + ) .pipe( takeUntilDestroyed(this.destroyRef), map(() => Math.floor((new Date()).getTime() - this.lastReload.getTime()) / 1000), @@ -716,6 +720,7 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { this.skipUrlUpdate = false; this.lastReload = new Date(); + this.lastReloadTickerForceUpdate$.next(); this.loading = false; this.cdr.markForCheck(); }) From a8e84d6d28294c1a805952b048c761e1f082a969 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Fri, 17 Oct 2025 22:01:24 +0300 Subject: [PATCH 4/5] refactor(UI): remove unnecessary reload intervals from settings --- desktop/angular/src/app/shared/netquery/netquery.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.ts b/desktop/angular/src/app/shared/netquery/netquery.component.ts index 6e5268ca..e8df932a 100644 --- a/desktop/angular/src/app/shared/netquery/netquery.component.ts +++ b/desktop/angular/src/app/shared/netquery/netquery.component.ts @@ -65,10 +65,8 @@ const orderByKeys: (keyof Partial)[] = [ export const reloadIntervalValues: { [key: string]: number } = { "⏸\u00A0\u00A0Don't auto-reload": 0, "↻\u00A0\u00A0Reload every 10 seconds": 10, - "↻\u00A0\u00A0Reload every 30 seconds": 30, "↻\u00A0\u00A0Reload every 1 minute": 60, "↻\u00A0\u00A0Reload every 5 minutes": 300, - "↻\u00A0\u00A0Reload every 30 minutes": 1800, } interface LocalQueryResult extends QueryResult { From 1ebac535aff5a8ec38ea5d444072f51081bda53c Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Sat, 18 Oct 2025 00:01:01 +0300 Subject: [PATCH 5/5] minor refactoring --- .../angular/src/app/shared/netquery/netquery.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.ts b/desktop/angular/src/app/shared/netquery/netquery.component.ts index e8df932a..62230901 100644 --- a/desktop/angular/src/app/shared/netquery/netquery.component.ts +++ b/desktop/angular/src/app/shared/netquery/netquery.component.ts @@ -6,7 +6,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { BandwidthChartResult, ChartResult, Condition, Database, FeatureID, GreaterOrEqual, IPScope, LessOrEqual, Netquery, NetqueryConnection, OrderBy, Pin, PossilbeValue, Query, QueryResult, SPNService, Select, Verdict } from "@safing/portmaster-api"; import { Datasource, DynamicItemsPaginator, SelectOption } from "@safing/ui"; import { BehaviorSubject, Observable, Subject, combineLatest, forkJoin, interval, merge, of, timer } from "rxjs"; -import { catchError, debounceTime, filter, map, share, skip, startWith, switchMap, take, takeUntil } from "rxjs/operators"; +import { catchError, filter, map, share, skip, startWith, switchMap, take, takeUntil } from "rxjs/operators"; import { ActionIndicatorService } from "../action-indicator"; import { ExpertiseService } from "../expertise"; import { objKeys } from "../utils"; @@ -266,7 +266,7 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { autoReloadEnabledTimestamp: Date | null = null; /** Auto-reload: Enable/disable auto-reload and set the interval */ onAutoRefreshChange(intervalName: string) { - let delaySec = reloadIntervalValues[intervalName] || 0; + const delaySec = reloadIntervalValues[intervalName] || 0; if (delaySec <= 0) { this.autoReloadIntervalName = ''; return; @@ -283,7 +283,7 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { map(() => { if (this.loading) return 0; - if (this.dateFilter?.length >= 2 && this.dateFilter[1].getTime() <= this.lastReload.getTime()) { + if (this.dateFilter?.length >= 2 && this.dateFilter[1] && this.dateFilter[1].getTime() <= this.lastReload.getTime()) { // Skip reload when dateFilter[1] (end date) <= lastReload (no new results expected) return 0; } @@ -291,7 +291,7 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { const intervalSeconds = reloadIntervalValues[this.autoReloadIntervalName] || 0; if (intervalSeconds <= 0) return 0; - let startTime = (this.autoReloadEnabledTimestamp && this.autoReloadEnabledTimestamp > this.lastReload) ? this.autoReloadEnabledTimestamp : this.lastReload; + const startTime = (this.autoReloadEnabledTimestamp && this.autoReloadEnabledTimestamp > this.lastReload) ? this.autoReloadEnabledTimestamp : this.lastReload; const elapsedSeconds = Math.floor((new Date().getTime() - startTime.getTime()) / 1000); const remainingSeconds = intervalSeconds - elapsedSeconds;