feat: pause and resume functionality improvements + UI
https://github.com/safing/portmaster/issues/2050
This commit is contained in:
@@ -153,7 +153,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-lower-list">
|
||||
<div class="relative link" sfngTipUpTrigger="navTools" sfngTipUpPassive tooltip="Version and Tools"
|
||||
<div class="relative link" sfngTipUpTrigger="navTools" sfngTipUpPassive sfng-tooltip="Version and Tools"
|
||||
sfngTooltipDelay="1000" snfgTooltipPosition="right" (click)="settingsMenu.dropdown.toggle(settingsMenuTrigger)"
|
||||
cdkOverlayOrigin #settingsMenuTrigger="cdkOverlayOrigin" [class.active]="settingsMenu.dropdown.isOpen">
|
||||
|
||||
@@ -202,8 +202,46 @@
|
||||
<app-menu-item (click)="cleanupHistory($event)">Cleanup Network History</app-menu-item>
|
||||
</app-menu>
|
||||
|
||||
<!-- Pause Menu -->
|
||||
<div sfngTipUpTrigger="navPause" sfngTipUpPassive sfng-tooltip="Pause and Resume" sfngTooltipDelay="1000"
|
||||
snfgTooltipPosition="right" class="link" (click)="pauseMenu.dropdown.toggle(pauseMenuTrigger)" cdkOverlayOrigin
|
||||
#pauseMenuTrigger="cdkOverlayOrigin" [class.active]="pauseMenu.dropdown.isOpen">
|
||||
|
||||
<svg viewBox="0 0 24 24" class="help" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"></path>
|
||||
<path d="M14 9L14 15"></path>
|
||||
<path d="M10 9L10 15"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
<app-menu #pauseMenu offsetY="0" offsetX="10" overlayClass="rounded-t">
|
||||
<div *ngIf="isPaused">
|
||||
<div class="flex flex-col p-4 text-xxs">
|
||||
<span class="text-secondary">
|
||||
Paused: <span class="text-primary">{{ pauseInfo }} </span>
|
||||
</span>
|
||||
<span class="text-secondary">
|
||||
Auto-resume at: <span class="text-primary">{{ pauseInfoTillTime }} </span>
|
||||
</span>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
|
||||
<app-menu-item (click)="pauseSPN($event, 60*3)" [disabled]="isPausedInterception">Pause SPN for 3 minutes</app-menu-item>
|
||||
<app-menu-item (click)="pauseSPN($event, 60*15)" [disabled]="isPausedInterception">Pause SPN for 15 minutes</app-menu-item>
|
||||
<app-menu-item (click)="pauseSPN($event, 60*60)" [disabled]="isPausedInterception">Pause SPN for 1 hour</app-menu-item>
|
||||
<hr/>
|
||||
<app-menu-item (click)="pause($event, 60*3)">Pause for 3 minutes</app-menu-item>
|
||||
<app-menu-item (click)="pause($event, 60*15)">Pause for 15 minutes</app-menu-item>
|
||||
<app-menu-item (click)="pause($event, 60*60)">Pause for 1 hour</app-menu-item>
|
||||
<hr/>
|
||||
<app-menu-item (click)="resume($event)" [disabled]="!isPaused">Resume</app-menu-item>
|
||||
</app-menu>
|
||||
|
||||
<!-- Power Menu -->
|
||||
<div sfngTipUpTrigger="navPower" sfngTipUpPassive tooltip="Shutdown and Restart" sfngTooltipDelay="1000"
|
||||
<div sfngTipUpTrigger="navPower" sfngTipUpPassive sfng-tooltip="Shutdown and Restart" sfngTooltipDelay="1000"
|
||||
snfgTooltipPosition="right" class="link" (click)="powerMenu.dropdown.toggle(powerMenuTrigger)" cdkOverlayOrigin
|
||||
#powerMenuTrigger="cdkOverlayOrigin" [class.active]="powerMenu.dropdown.isOpen">
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, In
|
||||
import { ConfigService, DebugAPI, PortapiService, SPNService, StringSetting } from '@safing/portmaster-api';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { NotificationType, NotificationsService, StatusService, VersionStatus } from 'src/app/services';
|
||||
import { NotificationType, NotificationsService, StatusService, VersionStatus, GetModuleState, ControlPauseStateData } from 'src/app/services';
|
||||
import { ActionIndicatorService } from 'src/app/shared/action-indicator';
|
||||
import { fadeInAnimation, fadeOutAnimation } from 'src/app/shared/animations';
|
||||
import { ExitService } from 'src/app/shared/exit-screen';
|
||||
@@ -36,6 +36,24 @@ export class NavigationComponent implements OnInit {
|
||||
/** The color to use for the notifcation-available hint (dot) */
|
||||
notificationColor: string = 'text-green-300';
|
||||
|
||||
pauseState: ControlPauseStateData | null = null;
|
||||
get isPaused(): boolean { return this.pauseState?.Interception===true || this.pauseState?.SPN===true; }
|
||||
get isPausedInterception(): boolean { return this.pauseState?.Interception===true; }
|
||||
get pauseInfo(): string {
|
||||
if (this.pauseState?.Interception===true && this.pauseState?.SPN===true)
|
||||
return 'Portmaster and SPN';
|
||||
else if (this.pauseState?.Interception===true)
|
||||
return 'Portmaster';
|
||||
else if (this.pauseState?.SPN===true)
|
||||
return 'SPN';
|
||||
return '';
|
||||
}
|
||||
get pauseInfoTillTime(): string {
|
||||
if (this.isPaused && this.pauseState?.TillTime)
|
||||
return new Date(this.pauseState.TillTime).toLocaleTimeString(undefined, { hour12: false });
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Whether or not we have new, unseen prompts */
|
||||
hasNewPrompts = false;
|
||||
|
||||
@@ -93,6 +111,10 @@ export class NavigationComponent implements OnInit {
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
|
||||
this.statusService.status$.subscribe(status => {
|
||||
this.pauseState = GetModuleState(status, 'Control', 'control:paused')?.Data || null;
|
||||
});
|
||||
|
||||
this.configService.watch<StringSetting>('filter/defaultAction')
|
||||
.subscribe(defaultAction => {
|
||||
this.globalPromptingEnabled = defaultAction === 'ask';
|
||||
@@ -251,6 +273,43 @@ export class NavigationComponent implements OnInit {
|
||||
))
|
||||
}
|
||||
|
||||
pause(event: Event, duration: number) {
|
||||
// prevent default and stop-propagation to avoid
|
||||
// expanding the accordion body.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.portapi.pause(duration, false)
|
||||
.subscribe(this.actionIndicator.httpObserver(
|
||||
'Pausing ...',
|
||||
'Failed to Pause',
|
||||
))
|
||||
}
|
||||
pauseSPN(event: Event, duration: number) {
|
||||
// prevent default and stop-propagation to avoid
|
||||
// expanding the accordion body.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.portapi.pause(duration, true)
|
||||
.subscribe(this.actionIndicator.httpObserver(
|
||||
'Pausing SPN...',
|
||||
'Failed to Pause SPN',
|
||||
))
|
||||
}
|
||||
resume(event: Event) {
|
||||
// prevent default and stop-propagation to avoid
|
||||
// expanding the accordion body.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.portapi.resume()
|
||||
.subscribe(this.actionIndicator.httpObserver(
|
||||
'Resuming ...',
|
||||
'Failed to Resume',
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Opens the data-directory of the portmaster installation.
|
||||
|
||||
@@ -27,7 +27,7 @@ export function getOnlineStatusString(stat: OnlineStatus): string {
|
||||
export interface CoreStatus extends Record {
|
||||
OnlineStatus: OnlineStatus;
|
||||
CaptivePortal: CaptivePortal;
|
||||
// Modules: []ModuleState; // TODO: Do we need all modules?
|
||||
Modules: StateUpdate[]; // TODO: Do we need all modules?
|
||||
WorstState: {
|
||||
Module: string,
|
||||
ID: string,
|
||||
@@ -39,6 +39,20 @@ export interface CoreStatus extends Record {
|
||||
}
|
||||
}
|
||||
|
||||
export interface StateUpdate {
|
||||
Module: string;
|
||||
States: State[];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
ID: string; // Program-unique identifier
|
||||
Name: string; // State name (may serve as notification title)
|
||||
Message?: string; // Detailed message about the state
|
||||
Type?: ModuleStateType; // State type
|
||||
Time?: Date; // Creation time
|
||||
Data?: any; // Additional data for processing
|
||||
}
|
||||
|
||||
export enum ModuleStateType {
|
||||
Undefined = "",
|
||||
Hint = "hint",
|
||||
@@ -110,3 +124,55 @@ export interface VersionStatus extends Record {
|
||||
[key: string]: Resource
|
||||
}
|
||||
}
|
||||
|
||||
function getModuleStates(status: CoreStatus, moduleID: string): State[] {
|
||||
const module = status.Modules?.find(m => m.Module === moduleID);
|
||||
return module?.States || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a specific state from a module within the CoreStatus.
|
||||
* @param status The CoreStatus object containing module states.
|
||||
* @param moduleID The identifier of the module to search within.
|
||||
* @param stateID The identifier of the state to retrieve.
|
||||
* @returns The State object if found; otherwise, null.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const state = GetModuleState(status, 'Control', 'control:paused');
|
||||
* if (state) {
|
||||
* console.log(`State found: ${state.Name}`);
|
||||
* } else {
|
||||
* console.log('State not found');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function GetModuleState(status: CoreStatus, moduleID: string, stateID: string): State | null {
|
||||
const states = getModuleStates(status, moduleID);
|
||||
for (const state of states) {
|
||||
if (state.ID === stateID) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure for the 'control:paused' state from the 'Control' module.
|
||||
*
|
||||
* This interface defines the expected structure of the Data field when Portmaster
|
||||
* or its components are temporarily paused by the user.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const pausedState = GetModuleState(status, 'Control', 'control:paused');
|
||||
* if (pausedState?.Data) {
|
||||
* const pauseData = pausedState.Data as ControlPauseStateData;
|
||||
* console.log(`SPN paused: ${pauseData.SPN}`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface ControlPauseStateData {
|
||||
Interception: boolean; // Whether Portmaster interception is paused
|
||||
SPN: boolean; // Whether SPN is paused
|
||||
TillTime: string; // When the pause will end (JSON date as string, has to be converted to Date)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Input, OnInit, inject } from "@angular/core";
|
||||
import { SecurityLevel } from "@safing/portmaster-api";
|
||||
import { combineLatest } from "rxjs";
|
||||
import { StatusService, ModuleStateType } from "src/app/services";
|
||||
import { StatusService, ModuleStateType, GetModuleState, ControlPauseStateData } from "src/app/services";
|
||||
import { fadeInAnimation, fadeOutAnimation } from "../animations";
|
||||
|
||||
interface SecurityOption {
|
||||
@@ -62,6 +62,17 @@ export class SecurityLockComponent implements OnInit {
|
||||
break;
|
||||
}
|
||||
|
||||
// Checking for Control:Paused state
|
||||
const pausedState = GetModuleState(status, 'Control', 'control:paused');
|
||||
if (pausedState?.Data) {
|
||||
const pauseData = pausedState.Data as ControlPauseStateData;
|
||||
if (pauseData.Interception === true) {
|
||||
this.lockLevel.displayText = 'Insecure: PAUSED';
|
||||
} else if (pauseData.SPN === true) {
|
||||
this.lockLevel.displayText = 'Secure: SPN Paused';
|
||||
}
|
||||
}
|
||||
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,6 +99,14 @@ navTools:
|
||||
title: Version and Tools
|
||||
content: |
|
||||
View the Portmaster's version and use special actions and tools.
|
||||
nextKey: navPause
|
||||
|
||||
navPause:
|
||||
title: Pause and Resume
|
||||
content: |
|
||||
Temporarily disable Portmaster's protection and network monitoring.
|
||||
|
||||
Choose to pause SPN only or disable all protection completely.
|
||||
nextKey: navPower
|
||||
|
||||
navPower:
|
||||
|
||||
Reference in New Issue
Block a user