Merge pull request #2040 from safing/feature/2039-UI-auto-reload-connections
Feature: UI auto reload connections
This commit is contained in:
@@ -268,17 +268,58 @@
|
||||
</div>
|
||||
</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-secondary">of {{totalConnCount}} total connections</span>
|
||||
</span>
|
||||
<span class="flex-grow"></span>
|
||||
<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>
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="flex flex-row">
|
||||
|
||||
<!-- Auto-Reload Interval selector -->
|
||||
<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>
|
||||
|
||||
<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 { 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, merge, of, timer } from "rxjs";
|
||||
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";
|
||||
@@ -62,6 +62,13 @@ const orderByKeys: (keyof Partial<NetqueryConnection>)[] = [
|
||||
'profile',
|
||||
]
|
||||
|
||||
export const reloadIntervalValues: { [key: string]: number } = {
|
||||
"⏸\u00A0\u00A0Don't auto-reload": 0,
|
||||
"↻\u00A0\u00A0Reload every 10 seconds": 10,
|
||||
"↻\u00A0\u00A0Reload every 1 minute": 60,
|
||||
"↻\u00A0\u00A0Reload every 5 minutes": 300,
|
||||
}
|
||||
|
||||
interface LocalQueryResult extends QueryResult {
|
||||
_chart: Observable<ChartResult[]> | null;
|
||||
_group: Observable<DynamicItemsPaginator<NetqueryConnection>> | null;
|
||||
@@ -239,13 +246,64 @@ 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<void>();
|
||||
lastReloadTicker = merge(
|
||||
interval(2000),
|
||||
this.lastReloadTickerForceUpdate$.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
)
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(() => Math.floor((new Date()).getTime() - this.lastReload.getTime()) / 1000),
|
||||
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) {
|
||||
const 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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const intervalSeconds = reloadIntervalValues[this.autoReloadIntervalName] || 0;
|
||||
if (intervalSeconds <= 0) return 0;
|
||||
|
||||
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;
|
||||
|
||||
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 +610,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 = [];
|
||||
@@ -661,6 +719,8 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
this.skipUrlUpdate = false;
|
||||
|
||||
this.lastReload = new Date();
|
||||
this.lastReloadTickerForceUpdate$.next();
|
||||
this.loading = false;
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
@@ -812,6 +872,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<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.
|
||||
// This is used in "group-by" views to lazy-load the content of the group once the user
|
||||
// expands it.
|
||||
@@ -1003,8 +1087,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();
|
||||
|
||||
@@ -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