Frontend update checking settings

This commit is contained in:
Michael Shamoon 2022-09-30 12:30:23 -07:00
parent 436f9e891e
commit 9e2430da46
10 changed files with 200 additions and 74 deletions

View File

@ -200,14 +200,25 @@
<li class="nav-item mt-2"> <li class="nav-item mt-2">
<div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap"> <div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap">
<div class="me-3">{{ versionString }}</div> <div class="me-3">{{ versionString }}</div>
<div *ngIf="appRemoteVersion" class="version-check"> <div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" class="version-check">
<ng-template #updateAvailablePopContent> <ng-template #updateAvailablePopContent>
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span> <span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span>
</ng-template> </ng-template>
<ng-template #updateCheckingNotEnabledPopContent> <ng-template #updateCheckingNotEnabledPopContent>
<span class="small"><ng-container i18n>Checking for updates is disabled.</ng-container><br/><ng-container i18n>Click for more information.</ng-container></span> <p class="small mb-2">
<ng-container i18n>Paperless-ngx can automatically check for updates</ng-container>
</p>
<div class="btn-group btn-group-xs flex-fill w-100">
<button class="btn btn-outline-primary" (click)="setUpdateChecking(true)">Enable</button>
<button class="btn btn-outline-secondary" (click)="setUpdateChecking(false)">Disable</button>
</div>
<p class="small mb-0 mt-2">
<a class="small text-decoration-none fst-italic" routerLink="/settings" fragment="update-checking" i18n>
How does this work?
</a>
</p>
</ng-template> </ng-template>
<ng-container *ngIf="appRemoteVersion.feature_is_set; else updateCheckNotSet"> <ng-container *ngIf="settingsService.updateCheckingIsSet; else updateCheckNotSet">
<a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases" <a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
@ -217,8 +228,8 @@
</a> </a>
</ng-container> </ng-container>
<ng-template #updateCheckNotSet> <ng-template #updateCheckNotSet>
<a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://paperless-ngx.readthedocs.io/en/latest/configuration.html#update-checking" <a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" /> <use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg> </svg>

View File

@ -24,6 +24,8 @@ import {
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { TasksService } from 'src/app/services/tasks.service' import { TasksService } from 'src/app/services/tasks.service'
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard' import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { ToastService } from 'src/app/services/toast.service'
@Component({ @Component({
selector: 'app-app-frame', selector: 'app-app-frame',
@ -40,13 +42,12 @@ export class AppFrameComponent implements ComponentCanDeactivate {
private remoteVersionService: RemoteVersionService, private remoteVersionService: RemoteVersionService,
private list: DocumentListViewService, private list: DocumentListViewService,
public settingsService: SettingsService, public settingsService: SettingsService,
public tasksService: TasksService public tasksService: TasksService,
private readonly toastService: ToastService
) { ) {
this.remoteVersionService if (settingsService.updateCheckingEnabled) {
.checkForUpdates() this.checkForUpdates()
.subscribe((appRemoteVersion: AppRemoteVersion) => { }
this.appRemoteVersion = appRemoteVersion
})
tasksService.reload() tasksService.reload()
} }
@ -150,4 +151,30 @@ export class AppFrameComponent implements ComponentCanDeactivate {
} }
}) })
} }
private checkForUpdates() {
this.remoteVersionService
.checkForUpdates()
.subscribe((appRemoteVersion: AppRemoteVersion) => {
this.appRemoteVersion = appRemoteVersion
})
}
setUpdateChecking(enable: boolean) {
this.settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, enable)
this.settingsService
.storeSettings()
.pipe(first())
.subscribe({
error: (error) => {
this.toastService.showError(
$localize`An error occurred while saving update checking settings.`
)
console.log(error)
},
})
if (enable) {
this.checkForUpdates()
}
}
} }

View File

@ -17,25 +17,6 @@
} }
} }
.btn-group-xs {
> .btn {
padding: 0.2rem 0.25rem;
font-size: 0.675rem;
line-height: 1.2;
border-radius: 0.15rem;
}
> .btn:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
> .btn:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.btn-group > label.disabled { .btn-group > label.disabled {
filter: brightness(0.5); filter: brightness(0.5);

View File

@ -116,6 +116,21 @@
</div> </div>
</div> </div>
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<p i18n>
Update checking works by pinging the the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">Github API</a> for the latest release to determine whether a new version is available.<br/>
Actual updating of the app must still be performed manually.
</p>
<p i18n>
<em>No tracking data is collected by the app in any way.</em>
</p>
<app-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled" i18n-hint hint="Note that for users of thirdy-party containers e.g. linuxserver.io this notification may be 'ahead' of the current third-party release."></app-input-check>
</div>
</div>
<h4 class="mt-4" i18n>Bulk editing</h4> <h4 class="mt-4" i18n>Bulk editing</h4>
<div class="row mb-3"> <div class="row mb-3">

View File

@ -4,7 +4,7 @@ import {
LOCALE_ID, LOCALE_ID,
OnInit, OnInit,
OnDestroy, OnDestroy,
Renderer2, AfterViewInit,
} from '@angular/core' } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms' import { FormControl, FormGroup } from '@angular/forms'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
@ -18,13 +18,17 @@ import { Toast, ToastService } from 'src/app/services/toast.service'
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
import { Observable, Subscription, BehaviorSubject, first } from 'rxjs' import { Observable, Subscription, BehaviorSubject, first } from 'rxjs'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { ActivatedRoute } from '@angular/router'
import { ViewportScroller } from '@angular/common'
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'], styleUrls: ['./settings.component.scss'],
}) })
export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { export class SettingsComponent
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
{
savedViewGroup = new FormGroup({}) savedViewGroup = new FormGroup({})
settingsForm = new FormGroup({ settingsForm = new FormGroup({
@ -45,6 +49,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
notificationsConsumerFailed: new FormControl(null), notificationsConsumerFailed: new FormControl(null),
notificationsConsumerSuppressOnDashboard: new FormControl(null), notificationsConsumerSuppressOnDashboard: new FormControl(null),
commentsEnabled: new FormControl(null), commentsEnabled: new FormControl(null),
updateCheckingEnabled: new FormControl(null),
}) })
savedViews: PaperlessSavedView[] savedViews: PaperlessSavedView[]
@ -74,9 +79,19 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
private documentListViewService: DocumentListViewService, private documentListViewService: DocumentListViewService,
private toastService: ToastService, private toastService: ToastService,
private settings: SettingsService, private settings: SettingsService,
@Inject(LOCALE_ID) public currentLocale: string @Inject(LOCALE_ID) public currentLocale: string,
private viewportScroller: ViewportScroller,
private activatedRoute: ActivatedRoute
) {} ) {}
ngAfterViewInit(): void {
if (this.activatedRoute.snapshot.fragment) {
this.viewportScroller.scrollToAnchor(
this.activatedRoute.snapshot.fragment
)
}
}
ngOnInit() { ngOnInit() {
this.savedViewService.listAll().subscribe((r) => { this.savedViewService.listAll().subscribe((r) => {
this.savedViews = r.results this.savedViews = r.results
@ -118,6 +133,9 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD
), ),
commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED), commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED),
updateCheckingEnabled: this.settings.get(
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED
),
} }
for (let view of this.savedViews) { for (let view of this.savedViews) {
@ -240,6 +258,10 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
SETTINGS_KEYS.COMMENTS_ENABLED, SETTINGS_KEYS.COMMENTS_ENABLED,
this.settingsForm.value.commentsEnabled this.settingsForm.value.commentsEnabled
) )
this.settings.set(
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
this.settingsForm.value.updateCheckingEnabled
)
this.settings.setLanguage(this.settingsForm.value.displayLanguage) this.settings.setLanguage(this.settingsForm.value.displayLanguage)
this.settings this.settings
.storeSettings() .storeSettings()

View File

@ -37,6 +37,9 @@ export const SETTINGS_KEYS = {
NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD: NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD:
'general-settings:notifications:consumer-suppress-on-dashboard', 'general-settings:notifications:consumer-suppress-on-dashboard',
COMMENTS_ENABLED: 'general-settings:comments-enabled', COMMENTS_ENABLED: 'general-settings:comments-enabled',
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
UPDATE_CHECKING_BACKEND_SETTING:
'general-settings:update-checking:backend-setting',
} }
export const SETTINGS: PaperlessUiSetting[] = [ export const SETTINGS: PaperlessUiSetting[] = [
@ -120,4 +123,14 @@ export const SETTINGS: PaperlessUiSetting[] = [
type: 'boolean', type: 'boolean',
default: true, default: true,
}, },
{
key: SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
type: 'boolean',
default: false,
},
{
key: SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING,
type: 'string',
default: '',
},
] ]

View File

@ -6,7 +6,6 @@ import { environment } from 'src/environments/environment'
export interface AppRemoteVersion { export interface AppRemoteVersion {
version: string version: string
update_available: boolean update_available: boolean
feature_is_set: boolean
} }
@Injectable({ @Injectable({

View File

@ -313,13 +313,7 @@ export class SettingsService {
) )
} }
get(key: string): any { private getSettingRawValue(key: string): any {
let setting = SETTINGS.find((s) => s.key == key)
if (!setting) {
return null
}
let value = null let value = null
// parse key:key:key into nested object // parse key:key:key into nested object
const keys = key.replace('general-settings:', '').split(':') const keys = key.replace('general-settings:', '').split(':')
@ -330,6 +324,17 @@ export class SettingsService {
if (index == keys.length - 1) value = settingObj[keyPart] if (index == keys.length - 1) value = settingObj[keyPart]
else settingObj = settingObj[keyPart] else settingObj = settingObj[keyPart]
}) })
return value
}
get(key: string): any {
let setting = SETTINGS.find((s) => s.key == key)
if (!setting) {
return null
}
let value = this.getSettingRawValue(key)
if (value != null) { if (value != null) {
switch (setting.type) { switch (setting.type) {
@ -359,6 +364,11 @@ export class SettingsService {
}) })
} }
private settingIsSet(key: string): boolean {
let value = this.getSettingRawValue(key)
return value != null
}
storeSettings(): Observable<any> { storeSettings(): Observable<any> {
return this.http.post(this.baseUrl, { settings: this.settings }) return this.http.post(this.baseUrl, { settings: this.settings })
} }
@ -401,4 +411,29 @@ export class SettingsService {
}) })
} }
} }
get updateCheckingEnabled(): boolean {
const backendSetting = this.get(
SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING
)
if (
!this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) &&
backendSetting != 'default'
) {
this.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, backendSetting === 'true')
}
return (
this.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) ||
(!this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) &&
backendSetting == 'true')
)
}
get updateCheckingIsSet(): boolean {
return (
this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) ||
this.get(SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING) != 'default'
)
}
} }

View File

@ -526,6 +526,25 @@ a.badge {
border-color: var(--bs-primary); border-color: var(--bs-primary);
} }
.btn-group-xs {
> .btn {
padding: 0.2rem 0.25rem;
font-size: 0.675rem;
line-height: 1.2;
border-radius: 0.15rem;
}
> .btn:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
> .btn:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
code { code {
color: var(--pngx-body-color-accent) color: var(--pngx-body-color-accent)
} }

View File

@ -780,42 +780,38 @@ class RemoteVersionView(GenericAPIView):
remote_version = "0.0.0" remote_version = "0.0.0"
is_greater_than_current = False is_greater_than_current = False
current_version = packaging_version.parse(version.__full_version_str__) current_version = packaging_version.parse(version.__full_version_str__)
# TODO: this can likely be removed when frontend settings are saved to DB try:
feature_is_set = settings.ENABLE_UPDATE_CHECK != "default" req = urllib.request.Request(
if feature_is_set and settings.ENABLE_UPDATE_CHECK: "https://api.github.com/repos/paperless-ngx/"
try: "paperless-ngx/releases/latest",
req = urllib.request.Request(
"https://api.github.com/repos/paperless-ngx/"
"paperless-ngx/releases/latest",
)
# Ensure a JSON response
req.add_header("Accept", "application/json")
with urllib.request.urlopen(req) as response:
remote = response.read().decode("utf-8")
try:
remote_json = json.loads(remote)
remote_version = remote_json["tag_name"]
# Basically PEP 616 but that only went in 3.9
if remote_version.startswith("ngx-"):
remote_version = remote_version[len("ngx-") :]
except ValueError:
logger.debug("An error occurred parsing remote version json")
except urllib.error.URLError:
logger.debug("An error occurred checking for available updates")
is_greater_than_current = (
packaging_version.parse(
remote_version,
)
> current_version
) )
# Ensure a JSON response
req.add_header("Accept", "application/json")
with urllib.request.urlopen(req) as response:
remote = response.read().decode("utf-8")
try:
remote_json = json.loads(remote)
remote_version = remote_json["tag_name"]
# Basically PEP 616 but that only went in 3.9
if remote_version.startswith("ngx-"):
remote_version = remote_version[len("ngx-") :]
except ValueError:
logger.debug("An error occurred parsing remote version json")
except urllib.error.URLError:
logger.debug("An error occurred checking for available updates")
is_greater_than_current = (
packaging_version.parse(
remote_version,
)
> current_version
)
return Response( return Response(
{ {
"version": remote_version, "version": remote_version,
"update_available": is_greater_than_current, "update_available": is_greater_than_current,
"feature_is_set": feature_is_set,
}, },
) )
@ -848,15 +844,23 @@ class UiSettingsView(GenericAPIView):
displayname = user.username displayname = user.username
if user.first_name or user.last_name: if user.first_name or user.last_name:
displayname = " ".join([user.first_name, user.last_name]) displayname = " ".join([user.first_name, user.last_name])
settings = {} ui_settings = {}
if hasattr(user, "ui_settings"): if hasattr(user, "ui_settings"):
settings = user.ui_settings.settings ui_settings = user.ui_settings.settings
if ui_settings["update_checking"]:
ui_settings["update_checking"][
"backend_setting"
] = settings.ENABLE_UPDATE_CHECK
else:
ui_settings["update_checking"] = {
"backend_setting": settings.ENABLE_UPDATE_CHECK,
}
return Response( return Response(
{ {
"user_id": user.id, "user_id": user.id,
"username": user.username, "username": user.username,
"display_name": displayname, "display_name": displayname,
"settings": settings, "settings": ui_settings,
}, },
) )