Service Worker Updates

Introdcues a new update service.
Listens to service-worker updates and shows a snack-bar to inform about updates.
Provides a function to manually check for updates.

The service worker tries to be consistent in it's own version and
updates in the background.
Some manuall trigger will be required to update, which is either a
reload or the execution of the provded check function

with help from
@FinnStutzenstein
This commit is contained in:
Sean Engelhardt 2019-05-03 17:39:48 +02:00
parent d7c6583b7e
commit e4d3e119d3
7 changed files with 162 additions and 40 deletions

View File

@ -5,21 +5,15 @@
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.png",
"/index.html",
"/*.css",
"/*.js"
]
"files": ["/favicon.png", "/index.html", "/*.css", "/*.js"]
}
}, {
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**"
]
"files": ["/assets/**"]
}
}
]

View File

@ -12,6 +12,7 @@ import { OperatorService } from './core/core-services/operator.service';
import { ServertimeService } from './core/core-services/servertime.service';
import { ThemeService } from './core/ui-services/theme.service';
import { DataStoreUpgradeService } from './core/core-services/data-store-upgrade.service';
import { UpdateService } from './core/ui-services/update.service';
/**
* Angular's global App Component
@ -38,6 +39,8 @@ export class AppComponent {
* @param countUsersService to call the constructor of the CountUserService
* @param configService to call the constructor of the ConfigService
* @param loadFontService to call the constructor of the LoadFontService
* @param dataStoreUpgradeService
* @param update Service Worker Updates
*/
public constructor(
translate: TranslateService,
@ -50,7 +53,8 @@ export class AppComponent {
countUsersService: CountUsersService, // Needed to register itself.
configService: ConfigService,
loadFontService: LoadFontService,
dataStoreUpgradeService: DataStoreUpgradeService // to start it.
dataStoreUpgradeService: DataStoreUpgradeService, // to start it.
update: UpdateService
) {
// manually add the supported languages
translate.addLangs(['en', 'de', 'cs']);

View File

@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { UpdateService } from './update.service';
import { E2EImportsModule } from 'e2e-imports.module';
describe('UpdateService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [UpdateService]
})
);
it('should be created', () => {
const service: UpdateService = TestBed.get(UpdateService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,61 @@
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { MatSnackBar } from '@angular/material';
import { NotifyService } from '../core-services/notify.service';
/**
* Handle Service Worker updates using the SwUpdate service form angular.
*/
@Injectable({
providedIn: 'root'
})
export class UpdateService {
private static NOTIFY_NAME = 'swCheckForUpdate';
/**
* Constructor.
* Listens to available updates
*
* @param swUpdate Service Worker update service
* @param matSnackBar Currently to show that an update is available
*/
public constructor(private swUpdate: SwUpdate, matSnackBar: MatSnackBar, private notify: NotifyService) {
swUpdate.available.subscribe(() => {
// TODO: Find a better solution OR make an update-bar like for history mode
const ref = matSnackBar.open('A new update is available!', 'Refresh', {
duration: 0
});
// Enforces an update
ref.onAction().subscribe(() => {
this.swUpdate.activateUpdate().then(() => {
document.location.reload();
});
});
});
// Listen on requests from other users to check for updates.
this.notify.getMessageObservable(UpdateService.NOTIFY_NAME).subscribe(() => {
this.checkForUpdate();
});
}
/**
* Trigger that to manually check for updates
*/
public checkForUpdate(): void {
if (this.swUpdate.isEnabled) {
this.swUpdate.checkForUpdate();
}
}
/**
* Emits a message to all clients initiating to check for updates. This method
* can only be called by users with 'users.can_manage'. This will be checked by
* the server.
*/
public initiateUpdateCheckForAllClients(): void {
this.notify.sendToAllUsers(UpdateService.NOTIFY_NAME, {});
}
}

View File

@ -1,4 +1,4 @@
<os-head-bar [nav]=false [goBack]=true>
<os-head-bar [nav]="false" [goBack]="true">
<div class="title-slot">
<h2 translate>Legal notice</h2>
</div>
@ -7,9 +7,29 @@
<os-legal-notice-content></os-legal-notice-content>
<mat-card class="os-card">
<div>
<button type="button" mat-button (click)="resetCache()">
<span translate>Reset cache</span>
</button>
</div>
<div>
<button type="button" mat-button (click)="checkForUpdate()">
<span translate>Check for updates</span>
</button>
</div>
<div *osPerms="'users.can_manage'">
<button
type="button"
mat-button
(click)="initiateUpdateCheckForAllClients()"
matTooltip="{{ 'This will send an update notification to all active clients' | translate }}"
>
<span translate>Initiate update check for all clients</span>
<mat-icon class="warning spacer-left-10">warning</mat-icon>
</button>
</div>
</mat-card>
<os-count-users></os-count-users>

View File

@ -1,14 +1,23 @@
import { Component } from '@angular/core';
import { OpenSlidesService } from 'app/core/core-services/openslides.service';
import { UpdateService } from 'app/core/ui-services/update.service';
@Component({
selector: 'os-legal-notice',
templateUrl: './legal-notice.component.html'
})
export class LegalNoticeComponent {
public constructor(private openSlidesService: OpenSlidesService) {}
public constructor(private openSlidesService: OpenSlidesService, private update: UpdateService) {}
public resetCache(): void {
this.openSlidesService.reset();
}
public checkForUpdate(): void {
this.update.checkForUpdate();
}
public initiateUpdateCheckForAllClients(): void {
this.update.initiateUpdateCheckForAllClients();
}
}

View File

@ -1,5 +1,6 @@
from typing import Any
from typing import Any, Dict
from ..utils.auth import async_has_perm
from ..utils.constants import get_constants
from ..utils.projector import get_projector_data
from ..utils.websocket import (
@ -44,10 +45,25 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
},
"required": ["name", "content"],
}
# Define a required permission for a notify message here. If the emitting user does not
# have this permission, he will get an error message in response.
notify_permissions: Dict[str, str] = {"swCheckForUpdate": "users.can_manage"}
async def receive_content(
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
) -> None:
# Check if the user is allowed to send this notify message
perm = self.notify_permissions.get(content["name"])
if perm is not None and not await async_has_perm(
consumer.scope["user"]["id"], perm
):
await consumer.send_json(
type="error",
content=f"You need '{perm}' to send this message.",
in_response=id,
)
else:
# Forward to all other active site consumers to handle the notify message.
await consumer.channel_layer.group_send(
"site",
{