UI: Automatic reloading of the connections list
https://github.com/safing/portmaster/issues/2039
This commit is contained in:
@@ -268,17 +268,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<div class="flex flex-row items-center justify-start gap-3 mt-3">
|
<!-- "Total connections & LastReload info "-->
|
||||||
|
<div class="flex flex-row items-center justify-start gap-3">
|
||||||
|
|
||||||
<span class="text-xxs text-primary" *ngIf="!loading">{{ totalResultCount }} Results
|
<span class="text-xxs text-primary" *ngIf="!loading">{{ totalResultCount }} Results
|
||||||
<span class="text-secondary">of {{totalConnCount}} total connections</span>
|
<span class="text-secondary">of {{totalConnCount}} total connections</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="flex-grow"></span>
|
<span class="flex-grow"></span>
|
||||||
<span class="pr-3 text-xxs text-secondary" [ngClass]="{
|
|
||||||
'text-yellow-300': ((lastReloadTicker|async)||0) > 60,
|
<div class="flex flex-row items-center">
|
||||||
'text-red-300': ((lastReloadTicker|async)||0) > 600
|
<div class="flex flex-row">
|
||||||
}">
|
|
||||||
Last Reload: {{ lastReload | timeAgo:(lastReloadTicker|async) }}
|
<!-- Auto-Reload Interval selector -->
|
||||||
</span>
|
<app-menu-trigger [menu]="autoReloadMenu" useContent="true"
|
||||||
|
class="text-secondary hover:text-primary flex !m-0"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<!-- Auto-Reload Interval countdown--->
|
||||||
|
<span *ngIf="!!autoReloadIntervalName" class="pr-3 text-xxs text-secondary" style="opacity: 0.5;" >
|
||||||
|
{{autoReloadIntervalName}}
|
||||||
|
|
||||||
|
<span *ngIf="(autoReloadInterval$ | async) as interval">
|
||||||
|
<span *ngIf="interval > 0 && interval <= 30">
|
||||||
|
(in {{interval}} sec)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Last Reload Time -->
|
||||||
|
<span class="pr-3 text-xxs text-secondary" [ngClass]="{
|
||||||
|
'text-yellow-300': ((lastReloadTicker|async)||0) > 60,
|
||||||
|
'text-red-300': ((lastReloadTicker|async)||0) > 600
|
||||||
|
}">
|
||||||
|
Last Reload: {{ lastReload | timeAgo:(lastReloadTicker|async) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</app-menu-trigger>
|
||||||
|
|
||||||
|
<app-menu #autoReloadMenu>
|
||||||
|
<app-menu-item *ngFor="let value of reloadIntervals" (click)="onAutoRefreshChange(value)">
|
||||||
|
{{value}}
|
||||||
|
</app-menu-item>
|
||||||
|
</app-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<sfng-pagination *ngIf="!loading; else: loadingTemplate" [source]="paginator" class="flex flex-col">
|
<sfng-pagination *ngIf="!loading; else: loadingTemplate" [source]="paginator" class="flex flex-col">
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|||||||
import { ActivatedRoute, Router } from "@angular/router";
|
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 { 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 { Datasource, DynamicItemsPaginator, SelectOption } from "@safing/ui";
|
||||||
import { BehaviorSubject, Observable, Subject, combineLatest, forkJoin, interval, of } from "rxjs";
|
import { BehaviorSubject, Observable, Subject, combineLatest, forkJoin, interval, of, timer } from "rxjs";
|
||||||
import { catchError, debounceTime, filter, map, share, skip, switchMap, take, takeUntil } from "rxjs/operators";
|
import { catchError, debounceTime, filter, map, share, skip, startWith, switchMap, take, takeUntil } from "rxjs/operators";
|
||||||
import { ActionIndicatorService } from "../action-indicator";
|
import { ActionIndicatorService } from "../action-indicator";
|
||||||
import { ExpertiseService } from "../expertise";
|
import { ExpertiseService } from "../expertise";
|
||||||
import { objKeys } from "../utils";
|
import { objKeys } from "../utils";
|
||||||
@@ -62,6 +62,15 @@ const orderByKeys: (keyof Partial<NetqueryConnection>)[] = [
|
|||||||
'profile',
|
'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 {
|
interface LocalQueryResult extends QueryResult {
|
||||||
_chart: Observable<ChartResult[]> | null;
|
_chart: Observable<ChartResult[]> | null;
|
||||||
_group: Observable<DynamicItemsPaginator<NetqueryConnection>> | null;
|
_group: Observable<DynamicItemsPaginator<NetqueryConnection>> | null;
|
||||||
@@ -246,6 +255,48 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
share()
|
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.
|
// whether or not the history database should be queried as well.
|
||||||
get useHistory() {
|
get useHistory() {
|
||||||
return this.dateFilter?.length;
|
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
|
// 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.
|
// with queries while the user is still configuring their filters.
|
||||||
this.search$
|
this.search$
|
||||||
.pipe(
|
.pipe(
|
||||||
debounceTime(1000),
|
this.adaptiveDebounce(1000, () => this.lastReload.getTime()),
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.connectionChartData = [];
|
this.connectionChartData = [];
|
||||||
@@ -659,6 +710,7 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
this.skipUrlUpdate = false;
|
this.skipUrlUpdate = false;
|
||||||
|
|
||||||
|
this.lastReload = new Date();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.cdr.markForCheck();
|
this.cdr.markForCheck();
|
||||||
})
|
})
|
||||||
@@ -810,6 +862,30 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.helper.dispose();
|
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<T>( minDelayMs: number, getLastOperationTime: () => number) {
|
||||||
|
return (source: Observable<T>) => 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.
|
// 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
|
// This is used in "group-by" views to lazy-load the content of the group once the user
|
||||||
// expands it.
|
// expands it.
|
||||||
@@ -1001,8 +1077,7 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
|
|
||||||
/** @private Query the portmaster service for connections matching the current settings */
|
/** @private Query the portmaster service for connections matching the current settings */
|
||||||
performSearch() {
|
performSearch() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.lastReload = new Date();
|
|
||||||
this.paginator.clear()
|
this.paginator.clear()
|
||||||
this.search$.next();
|
this.search$.next();
|
||||||
this.updateTagbarValues();
|
this.updateTagbarValues();
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user