Make saving the configuration more robust (#594)

* feat: Make saving the configuration more robust

* parse backup user config before return

* fix some bug

* Add write-userconfig.js and invalid-config.bin

* throw exception if failed user config parsing

* Merge branch 'master' into feat-467-make-save-more-robust

* hide keymaps and macros if agent in restore mode

* fix Device name settings
This commit is contained in:
Róbert Kiss
2018-04-09 10:11:26 +02:00
committed by László Monda
parent 00c5b69129
commit 13ec617d58
38 changed files with 1087 additions and 1903 deletions

View File

@@ -4,6 +4,7 @@ import { DeviceConfigurationComponent } from './configuration/device-configurati
import { DeviceFirmwareComponent } from './firmware/device-firmware.component';
import { MouseSpeedComponent } from './mouse-speed/mouse-speed.component';
import { LEDBrightnessComponent } from './led-brightness/led-brightness.component';
import { RestoreConfigurationComponent } from './restore-configuration/restore-configuration.component';
export const deviceRoutes: Routes = [
{
@@ -29,6 +30,10 @@ export const deviceRoutes: Routes = [
{
path: 'firmware',
component: DeviceFirmwareComponent
},
{
path: 'restore-user-configuration',
component: RestoreConfigurationComponent
}
]
}

View File

@@ -2,4 +2,5 @@ export * from './configuration/device-configuration.component';
export * from './firmware/device-firmware.component';
export * from './mouse-speed/mouse-speed.component';
export * from './led-brightness/led-brightness.component';
export * from './restore-configuration/restore-configuration.component';
export * from './device.routes';

View File

@@ -0,0 +1,19 @@
<h1>
<i class="fa fa-exclamation-circle"></i>
<span>Fix configuration</span>
</h1>
<p>
Your on-board device configuration is invalid.
</p>
<button class="btn btn-primary"
*ngIf="state.hasBackupUserConfiguration"
[disabled]="state.restoringUserConfiguration"
(click)="restoreUserConfiguration()"> Restore the last valid device configuration
</button>
<button class="btn btn-danger"
*ngIf="!state.hasBackupUserConfiguration"
[disabled]="state.restoringUserConfiguration"
(click)="resetUserConfiguration()">Reset device configuration
</button>

View File

@@ -0,0 +1,10 @@
:host {
overflow-y: auto;
display: block;
height: 100%;
width: 100%;
p {
margin: 1.5rem 0;
}
}

View File

@@ -0,0 +1,48 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs/Subscription';
import { AppState, getBackupUserConfigurationState } from '../../../store';
import { ResetUserConfigurationAction, RestoreUserConfigurationFromBackupAction } from '../../../store/actions/device';
import { RestoreConfigurationState } from '../../../models/restore-configuration-state';
@Component({
selector: 'restore-configuration',
templateUrl: './restore-configuration.component.html',
styleUrls: ['./restore-configuration.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class RestoreConfigurationComponent implements OnInit, OnDestroy {
state: RestoreConfigurationState;
private stateSubscription: Subscription;
constructor(private store: Store<AppState>,
private cdRef: ChangeDetectorRef) {
}
ngOnDestroy(): void {
if (this.stateSubscription) {
this.stateSubscription.unsubscribe();
}
}
ngOnInit(): void {
this.stateSubscription = this.store
.select(getBackupUserConfigurationState)
.subscribe(data => {
this.state = data;
this.cdRef.markForCheck();
});
}
resetUserConfiguration() {
this.store.dispatch(new ResetUserConfigurationAction());
}
restoreUserConfiguration(): void {
this.store.dispatch(new RestoreUserConfigurationFromBackupAction());
}
}

View File

@@ -5,6 +5,7 @@
<input #deviceName cancelable
class="pane-title__name"
type="text"
[readonly]="state.restoreUserConfiguration"
(change)="editDeviceName($event.target.value)"
(keyup.enter)="deviceName.blur()"
(keyup)="calculateHeaderTextWidth($event.target.value)">
@@ -17,33 +18,43 @@
<i class="fa fa-chevron-up pull-right" (click)="toggleHide($event, 'configuration')"></i>
</div>
<ul [@toggler]="animation['configuration']">
<li class="sidebar__level-2--item">
<li class="sidebar__level-2--item"
*ngIf="!state.restoreUserConfiguration">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/device/mouse-speed']"
[class.disabled]="updatingFirmware$ | async">Mouse speed</a>
[class.disabled]="state.updatingFirmware">Mouse speed</a>
</div>
</li>
<li class="sidebar__level-2--item">
<li class="sidebar__level-2--item"
*ngIf="!state.restoreUserConfiguration">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/device/led-brightness']"
[class.disabled]="updatingFirmware$ | async">LED brightness</a>
[class.disabled]="state.updatingFirmware">LED brightness</a>
</div>
</li>
<li class="sidebar__level-2--item">
<li class="sidebar__level-2--item"
*ngIf="!state.restoreUserConfiguration">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/device/configuration']"
[class.disabled]="updatingFirmware$ | async">Configuration</a>
[class.disabled]="state.updatingFirmware">Configuration</a>
</div>
</li>
<li class="sidebar__level-2--item"
*ngIf="state.restoreUserConfiguration">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/device/restore-user-configuration']">Fix configuration</a>
</div>
</li>
<li class="sidebar__level-2--item">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/device/firmware']"
[class.disabled]="updatingFirmware$ | async">Firmware</a>
[class.disabled]="state.updatingFirmware">Firmware</a>
</div>
</li>
</ul>
</li>
<li class="sidebar__level-1--item">
<li class="sidebar__level-1--item"
*ngIf="!state.restoreUserConfiguration">
<div class="sidebar__level-1">
<i class="fa fa-keyboard-o"></i> Keymaps
<!--a [routerLink]="['/keymap/add']"
@@ -55,10 +66,10 @@
(click)="toggleHide($event, 'keymap')"></i>
</div>
<ul [@toggler]="animation['keymap']">
<li *ngFor="let keymap of keymaps$ | async" class="sidebar__level-2--item">
<li *ngFor="let keymap of state.keymaps" class="sidebar__level-2--item">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/keymap', keymap.abbreviation]"
[class.disabled]="updatingFirmware$ | async">{{keymap.name}}</a>
[class.disabled]="state.updatingFirmware">{{keymap.name}}</a>
<i *ngIf="keymap.isDefault" class="fa fa-star sidebar__fav"
title="This is the default keymap which gets activated when powering the keyboard."
data-toggle="tooltip" data-placement="bottom"></i>
@@ -66,26 +77,27 @@
</li>
</ul>
</li>
<li class="sidebar__level-1--item">
<li class="sidebar__level-1--item"
*ngIf="!state.restoreUserConfiguration">
<div class="sidebar__level-1">
<i class="fa fa-play"></i> Macros
<a (click)="addMacro()"
class="btn btn-default pull-right btn-sm"
[class.disabled]="updatingFirmware$ | async">
[class.disabled]="state.updatingFirmware">
<i class="fa fa-plus"></i>
</a>
<i class="fa fa-chevron-up pull-right" (click)="toggleHide($event, 'macro')"></i>
</div>
<ul [@toggler]="animation['macro']">
<li *ngFor="let macro of macros$ | async" class="sidebar__level-2--item">
<li *ngFor="let macro of state.macros" class="sidebar__level-2--item">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/macro', macro.id]"
[class.disabled]="updatingFirmware$ | async">{{macro.name}}</a>
[class.disabled]="state.updatingFirmware">{{macro.name}}</a>
</div>
</li>
</ul>
</li>
<li class="sidebar__level-1--item" *ngIf="showAddonMenu$ | async">
<li class="sidebar__level-1--item" *ngIf="state.showAddonMenu">
<div class="sidebar__level-1">
<i class="fa fa-puzzle-piece"></i> Add-on modules
<i class="fa fa-chevron-up pull-right" (click)="toggleHide($event, 'addon')"></i>
@@ -94,25 +106,25 @@
<li class="sidebar__level-2--item" data-name="Key cluster" data-abbrev="">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/add-on', 'Key cluster']"
[class.disabled]="updatingFirmware$ | async">Key cluster</a>
[class.disabled]="state.updatingFirmware">Key cluster</a>
</div>
</li>
<li class="sidebar__level-2--item" data-name="Trackball" data-abbrev="">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/add-on', 'Trackball']"
[class.disabled]="updatingFirmware$ | async">Trackball</a>
[class.disabled]="state.updatingFirmware">Trackball</a>
</div>
</li>
<li class="sidebar__level-2--item" data-name="Toucpad" data-abbrev="">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/add-on', 'Touchpad']"
[class.disabled]="updatingFirmware$ | async">Touchpad</a>
[class.disabled]="state.updatingFirmware">Touchpad</a>
</div>
</li>
<li class="sidebar__level-2--item" data-name="Trackpoint" data-abbrev="">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/add-on', 'Trackpoint']"
[class.disabled]="updatingFirmware$ | async">Trackpoint</a>
[class.disabled]="state.updatingFirmware">Trackpoint</a>
</div>
</li>
</ul>
@@ -123,19 +135,19 @@
<div class="sidebar__level-0">
<i class="uhk-icon uhk-icon-agent-icon"></i> Agent
<i class="fa fa-chevron-up pull-right"
(click)="toggleHide($event, 'agent')"></i>
(click)="toggleHide($event, 'agent')"></i>
</div>
<ul [@toggler]="animation['agent']">
<li class="sidebar__level-2--item">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/settings']"
[class.disabled]="updatingFirmware$ | async">Settings</a>
[class.disabled]="state.updatingFirmware">Settings</a>
</div>
</li>
<li class="sidebar__level-2--item">
<div class="sidebar__level-2" [routerLinkActive]="['active']">
<a [routerLink]="['/about']"
[class.disabled]="updatingFirmware$ | async">About</a>
[class.disabled]="state.updatingFirmware">About</a>
</div>
</li>
</ul>

View File

@@ -1,20 +1,27 @@
import { AfterContentInit, Component, ElementRef, OnDestroy, Renderer2, ViewChild } from '@angular/core';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy, OnInit,
Renderer2,
ViewChild
} from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Keymap, Macro } from 'uhk-common';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/let';
import { AppState, getDeviceName, runningInElectron, showAddonMenu, updatingFirmware } from '../../store';
import { AppState, getSideMenuPageState } from '../../store';
import { MacroActions } from '../../store/actions';
import { getKeymaps, getMacros } from '../../store/reducers/user-configuration';
import * as util from '../../util';
import { RenameUserConfigurationAction } from '../../store/actions/user-config';
import { SideMenuPageState } from '../../models/side-menu-page-state';
@Component({
animations: [
@@ -30,24 +37,19 @@ import { RenameUserConfigurationAction } from '../../store/actions/user-config';
],
selector: 'side-menu',
templateUrl: './side-menu.component.html',
styleUrls: ['./side-menu.component.scss']
styleUrls: ['./side-menu.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SideMenuComponent implements AfterContentInit, OnDestroy {
showAddonMenu$: Observable<boolean>;
runInElectron$: Observable<boolean>;
updatingFirmware$: Observable<boolean>;
deviceName$: Observable<string>;
deviceNameSubscription: Subscription;
keymaps$: Observable<Keymap[]>;
macros$: Observable<Macro[]>;
export class SideMenuComponent implements AfterContentInit, OnInit, OnDestroy {
state: SideMenuPageState;
animation: { [key: string]: 'active' | 'inactive' };
deviceNameValue: string;
updatingFirmware = false;
updatingFirmwareSubscription: Subscription;
@ViewChild('deviceName') deviceName: ElementRef;
constructor(private store: Store<AppState>, private renderer: Renderer2) {
private stateSubscription: Subscription;
constructor(private store: Store<AppState>,
private renderer: Renderer2,
private cdRef: ChangeDetectorRef) {
this.animation = {
device: 'active',
configuration: 'active',
@@ -55,20 +57,13 @@ export class SideMenuComponent implements AfterContentInit, OnDestroy {
macro: 'active',
addon: 'active'
};
}
this.keymaps$ = store.let(getKeymaps());
this.macros$ = store.let(getMacros());
this.showAddonMenu$ = this.store.select(showAddonMenu);
this.runInElectron$ = this.store.select(runningInElectron);
this.deviceName$ = store.select(getDeviceName);
this.deviceNameSubscription = this.deviceName$.subscribe(name => {
this.deviceNameValue = name;
ngOnInit(): void {
this.stateSubscription = this.store.select(getSideMenuPageState).subscribe(data => {
this.state = data;
this.setDeviceName();
});
this.updatingFirmware$ = store.select(updatingFirmware);
this.updatingFirmwareSubscription = this.updatingFirmware$.subscribe(updating => {
this.updatingFirmware = updating;
this.cdRef.markForCheck();
});
}
@@ -77,12 +72,13 @@ export class SideMenuComponent implements AfterContentInit, OnDestroy {
}
ngOnDestroy(): void {
this.deviceNameSubscription.unsubscribe();
this.updatingFirmwareSubscription.unsubscribe();
if (this.stateSubscription) {
this.stateSubscription.unsubscribe();
}
}
toggleHide(event: Event, type: string) {
if (this.updatingFirmware) {
if (this.state.updatingFirmware) {
return;
}
@@ -110,7 +106,7 @@ export class SideMenuComponent implements AfterContentInit, OnDestroy {
}
editDeviceName(name: string): void {
if (!util.isValidName(name) || name.trim() === this.deviceNameValue) {
if (!util.isValidName(name) || name.trim() === this.state.deviceName) {
this.setDeviceName();
return;
}
@@ -126,7 +122,7 @@ export class SideMenuComponent implements AfterContentInit, OnDestroy {
private setDeviceName(): void {
if (this.deviceName) {
this.renderer.setProperty(this.deviceName.nativeElement, 'value', this.deviceNameValue);
this.renderer.setProperty(this.deviceName.nativeElement, 'value', this.state.deviceName);
this.calculateHeaderTextWidth(this.deviceName.nativeElement.value);
}
}