Hassio snapshot: download + design tweaks (#972)

* Hassio snapshot download, upload

* some improvements

* Remove event

* Remove upload stuff

* Feedback

* Make downloads as suggested

* More updates

* Add tooltips
This commit is contained in:
c727 2018-03-09 00:52:52 +01:00 committed by Paulus Schoutsen
parent 08d4e0af18
commit a1dbd18a9a
2 changed files with 160 additions and 120 deletions

View File

@ -1,81 +1,117 @@
<link rel='import' href='../../bower_components/polymer/polymer-element.html'> <link rel='import' href='../../bower_components/polymer/polymer-element.html'>
<link rel='import' href='../../bower_components/paper-dialog/paper-dialog.html'> <link rel='import' href='../../bower_components/paper-dialog/paper-dialog.html'>
<link rel='import' href='../../bower_components/paper-dialog-scrollable/paper-dialog-scrollable.html'>
<link rel='import' href='../../bower_components/app-layout/app-toolbar/app-toolbar.html'>
<link rel='import' href='../../bower_components/paper-icon-button/paper-icon-button.html'>
<link rel='import' href='../../bower_components/paper-checkbox/paper-checkbox.html'> <link rel='import' href='../../bower_components/paper-checkbox/paper-checkbox.html'>
<link rel='import' href='../../bower_components/paper-button/paper-button.html'> <link rel='import' href='../../bower_components/paper-button/paper-button.html'>
<link rel="import" href='../../bower_components/paper-input/paper-input.html'> <link rel="import" href='../../bower_components/paper-input/paper-input.html'>
<link rel='import' href='../../src/resources/ha-style.html'>
<dom-module id='hassio-snapshot'> <dom-module id='hassio-snapshot'>
<template> <template>
<style include='ha-style'> <style include='ha-style-dialog'>
paper-dialog {
min-width: 350px;
font-size: 14px;
border-radius: 2px;
}
app-toolbar {
margin: 0;
padding: 0 16px;
color: var(--primary-text-color);
background-color: var(--secondary-background-color);
}
app-toolbar [main-title] {
margin-left: 16px;
}
paper-dialog-scrollable {
margin: 0;
}
paper-checkbox { paper-checkbox {
display: block; display: block;
margin: 4px; margin: 4px;
} }
.snapshot-details { @media all and (max-width: 450px), all and (max-height: 500px) {
color: var(--secondary-text-color); paper-dialog {
margin-bottom: 16px; max-height: 100%;
height: 100%;
}
app-toolbar {
color: var(--text-primary-color);
background-color: var(--primary-color);
}
} }
.addon-version { .details {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.download {
color: var(--primary-color);
}
.warning, .warning,
.error { .error {
color: var(--google-red-500); color: var(--google-red-500);
} }
@media screen and (max-width: 600px) {
paper-dialog {
position: absolute !important;
bottom: 0; left: 0; right: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
margin: 0;
}
}
</style> </style>
<paper-dialog id='dialog' with-backdrop on-iron-overlay-closed='dialogClosed'> <paper-dialog id='dialog' with-backdrop on-iron-overlay-closed='_dialogClosed'>
<h2>[[computeName(snapshot)]]</h2> <app-toolbar>
<div> <paper-icon-button
<div class='snapshot-details'> icon='mdi:close'
[[computeType(snapshot.type)]] ([[computeSize(snapshot.size)]])<br/> dialog-dismiss
[[formatDatetime(snapshot.date)]] ></paper-icon-button>
</div> <div main-title>[[_computeName(snapshot)]]</div>
<div> </app-toolbar>
Home Assistant: <div class='details'>
<paper-checkbox checked='{{restoreHass}}'> [[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])<br/>
Home Assistant [[snapshot.homeassistant]] [[_formatDatetime(snapshot.date)]]
</div>
<div>Home Assistant:</div>
<paper-checkbox checked='{{restoreHass}}'>
Home Assistant [[snapshot.homeassistant]]
</paper-checkbox>
<template is='dom-if' if='[[snapshot.addons.length]]'>
<div>Folders:</div>
<template is='dom-repeat' items='[[snapshot.folders]]'>
<paper-checkbox checked='{{item.checked}}'>
[[item.name]]
</paper-checkbox> </paper-checkbox>
</div> </template>
<div> </template>
Folders: <template is='dom-if' if='[[snapshot.addons.length]]'>
<template is='dom-repeat' items='[[snapshot.folders]]'> <div>Add-ons:</div>
<paper-dialog-scrollable>
<template is='dom-repeat' items='[[snapshot.addons]]' sort='_sortAddons'>
<paper-checkbox checked='{{item.checked}}'> <paper-checkbox checked='{{item.checked}}'>
[[item.name]] [[item.name]]
<span class='details'>([[item.version]])</span>
</paper-checkbox> </paper-checkbox>
</template> </template>
</div> </paper-dialog-scrollable>
<template is='dom-if' if='[[snapshot.addons.length]]'> </template>
<div> <template is='dom-if' if='[[snapshot.protected]]'>
Add-ons: <paper-input autofocus label='Password' type='password' value='{{snapshotPassword}}'></paper-input>
<template is='dom-repeat' items='[[snapshot.addons]]' sort='sortAddons'> </template>
<paper-checkbox checked='{{item.checked}}'> <template is='dom-if' if='[[error]]'>
[[item.name]] <p class='error'>Error: [[error]]</p>
<span class='addon-version'>([[item.version]])</span> </template>
</paper-checkbox>
</template>
</div>
</template>
<template is='dom-if' if='[[snapshot.protected]]'>
<paper-input autofocus label='Password' type='password' value='{{snapshotPassword}}'></paper-input>
</template>
<template is='dom-if' if='[[error]]'>
<p class='error'>Error: [[error]]</p>
</template>
</div>
<div class='buttons'> <div class='buttons'>
<paper-button class='warning' on-click='deleteTapped'>Delete</paper-button> <paper-icon-button
<paper-button on-click='partialRestoreTapped'>Partial restore</paper-button> icon='mdi:delete'
<template is='dom-if' if='[[isFullSnapshot(snapshot.type)]]'> on-click='_deleteClicked'
<paper-button on-click='fullRestoreTapped'>Full restore</paper-button> class='warning'
title='Delete snapshot'
></paper-icon-button>
<a href='[[_computeDownloadUrl(snapshotSlug)]]' download='[[_computeDownloadName(snapshot)]]'>
<paper-icon-button
icon='mdi:download'
class='download'
title='Download snapshot'
></paper-icon-button>
</a>
<paper-button on-click='_partialRestoreClicked'>Restore selected</paper-button>
<template is='dom-if' if='[[_isFullSnapshot(snapshot.type)]]'>
<paper-button on-click='_fullRestoreClicked'>Wipe &amp; restore</paper-button>
</template> </template>
</div> </div>
</paper-dialog> </paper-dialog>
@ -92,7 +128,7 @@ class HassioSnapshot extends Polymer.Element {
snapshotSlug: { snapshotSlug: {
type: String, type: String,
notify: true, notify: true,
observer: 'snapshotSlugChanged', observer: '_snapshotSlugChanged',
}, },
snapshotDeleted: { snapshotDeleted: {
type: Boolean, type: Boolean,
@ -108,12 +144,12 @@ class HassioSnapshot extends Polymer.Element {
}; };
} }
snapshotSlugChanged(snapshotSlug) { _snapshotSlugChanged(snapshotSlug) {
if (!snapshotSlug || snapshotSlug === 'update') return; if (!snapshotSlug || snapshotSlug === 'update') return;
this.hass.callApi('get', `hassio/snapshots/${snapshotSlug}/info`) this.hass.callApi('get', `hassio/snapshots/${snapshotSlug}/info`)
.then((info) => { .then((info) => {
info.data.folders = this.computeFolders(info.data.folders); info.data.folders = this._computeFolders(info.data.folders);
info.data.addons = this.computeAddons(info.data.addons); info.data.addons = this._computeAddons(info.data.addons);
this.snapshot = info.data; this.snapshot = info.data;
this.$.dialog.open(); this.$.dialog.open();
}, () => { }, () => {
@ -121,7 +157,7 @@ class HassioSnapshot extends Polymer.Element {
}); });
} }
computeFolders(folders) { _computeFolders(folders) {
const list = []; const list = [];
if (folders.includes('homeassistant')) list.push({ slug: 'homeassistant', name: 'Home Assistant configuration', checked: true }); if (folders.includes('homeassistant')) list.push({ slug: 'homeassistant', name: 'Home Assistant configuration', checked: true });
if (folders.includes('ssl')) list.push({ slug: 'ssl', name: 'SSL', checked: true }); if (folders.includes('ssl')) list.push({ slug: 'ssl', name: 'SSL', checked: true });
@ -130,16 +166,16 @@ class HassioSnapshot extends Polymer.Element {
return list; return list;
} }
computeAddons(addons) { _computeAddons(addons) {
return addons.map(addon => ( return addons.map(addon => (
{ slug: addon.slug, name: addon.name, version: addon.version, checked: true })); { slug: addon.slug, name: addon.name, version: addon.version, checked: true }));
} }
isFullSnapshot(type) { _isFullSnapshot(type) {
return type === 'full'; return type === 'full';
} }
partialRestoreTapped() { _partialRestoreClicked() {
if (!confirm('Are you sure you want to restore this snapshot?')) { if (!confirm('Are you sure you want to restore this snapshot?')) {
return; return;
} }
@ -162,7 +198,7 @@ class HassioSnapshot extends Polymer.Element {
}); });
} }
fullRestoreTapped() { _fullRestoreClicked() {
if (!confirm('Are you sure you want to restore this snapshot?')) { if (!confirm('Are you sure you want to restore this snapshot?')) {
return; return;
} }
@ -176,7 +212,7 @@ class HassioSnapshot extends Polymer.Element {
}); });
} }
deleteTapped() { _deleteClicked() {
if (!confirm('Are you sure you want to delete this snapshot?')) { if (!confirm('Are you sure you want to delete this snapshot?')) {
return; return;
} }
@ -189,23 +225,33 @@ class HassioSnapshot extends Polymer.Element {
}); });
} }
computeName(snapshot) { _computeDownloadUrl(snapshotSlug) {
const password = encodeURI(this.hass.connection.options.authToken);
return `/api/hassio/snapshots/${snapshotSlug}/download?api_password=${password}`;
}
_computeDownloadName(snapshot) {
const name = this._computeName(snapshot).replace(/[^a-z0-9]+/gi, '_');
return `Hass_io_${name}.tar`;
}
_computeName(snapshot) {
return snapshot.name || snapshot.slug; return snapshot.name || snapshot.slug;
} }
computeType(type) { _computeType(type) {
return type === 'full' ? 'Full snapshot' : 'Partial snapshot'; return type === 'full' ? 'Full snapshot' : 'Partial snapshot';
} }
computeSize(size) { _computeSize(size) {
return Math.ceil(size) + ' MB'; return (Math.ceil(size * 10) / 10) + ' MB';
} }
sortAddons(a, b) { _sortAddons(a, b) {
return a.name < b.name ? -1 : 1; return a.name < b.name ? -1 : 1;
} }
formatDatetime(datetime) { _formatDatetime(datetime) {
return new Date(datetime).toLocaleDateString(navigator.language, { return new Date(datetime).toLocaleDateString(navigator.language, {
weekday: 'long', weekday: 'long',
year: 'numeric', year: 'numeric',
@ -216,7 +262,7 @@ class HassioSnapshot extends Polymer.Element {
}); });
} }
dialogClosed() { _dialogClosed() {
this.snapshotSlug = null; this.snapshotSlug = null;
} }
} }

View File

@ -13,7 +13,14 @@
<dom-module id="hassio-snapshots"> <dom-module id="hassio-snapshots">
<template> <template>
<style include="ha-style hassio-style"> <style include='ha-style hassio-style'>
paper-radio-group {
display: block;
}
paper-radio-button {
padding: 0 0 2px 2px;
}
paper-radio-button,
paper-checkbox, paper-checkbox,
paper-input[type="password"] { paper-input[type="password"] {
display: block; display: block;
@ -22,14 +29,11 @@
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
.hidden {
display: none;
}
</style> </style>
<div class='content'> <div class='content'>
<div class='card-group'> <div class='card-group'>
<div class='title'> <div class='title'>
New snapshot Create snapshot
<div class='description'> <div class='description'>
Snapshots allow you to easily backup and Snapshots allow you to easily backup and
restore all data of your Hass.io instance. restore all data of your Hass.io instance.
@ -38,8 +42,8 @@
<paper-card> <paper-card>
<div class='card-content'> <div class='card-content'>
<paper-input autofocus label='Name' value='{{snapshotName}}'></paper-input> <paper-input autofocus label='Name' value='{{snapshotName}}'></paper-input>
<label id='lbltype'>Type:</label> Type:
<paper-radio-group selected='{{snapshotType}}' aria-labelledby='lbltype' on-paper-radio-group-changed='typeChanged'> <paper-radio-group selected='{{snapshotType}}'>
<paper-radio-button name='full'> <paper-radio-button name='full'>
Full snapshot Full snapshot
</paper-radio-button> </paper-radio-button>
@ -47,35 +51,31 @@
Partial snapshot Partial snapshot
</paper-radio-button> </paper-radio-button>
</paper-radio-group> </paper-radio-group>
<div id='folders' class='hidden'> <template is='dom-if' if='[[!_fullSelected(snapshotType)]]'>
Folders: Folders:
<template is='dom-repeat' items='[[folderList]]'> <template is='dom-repeat' items='[[folderList]]'>
<paper-checkbox checked='{{item.checked}}'> <paper-checkbox checked='{{item.checked}}'>
[[item.name]] [[item.name]]
</paper-checkbox> </paper-checkbox>
</template> </template>
</div>
<div id='addons' class='hidden'>
Add-ons: Add-ons:
<template is='dom-repeat' items='[[addonList]]' sort='sortAddons'> <template is='dom-repeat' items='[[addonList]]' sort='_sortAddons'>
<paper-checkbox checked='{{item.checked}}'> <paper-checkbox checked='{{item.checked}}'>
[[item.name]] [[item.name]]
</paper-checkbox> </paper-checkbox>
</template> </template>
</div> </template>
<div> Security:
Password: <paper-checkbox checked='{{snapshotHasPassword}}'>Password protection</paper-checkbox>
<paper-checkbox checked='{{snapshotHasPassword}}'>Password protected</paper-checkbox> <template is='dom-if' if='[[snapshotHasPassword]]'>
<template is='dom-if' if='[[snapshotHasPassword]]'> <paper-input label='Password' type='password' value='{{snapshotPassword}}'></paper-input>
<paper-input label='Password' type='password' value='{{snapshotPassword}}'></paper-input> </template>
</template>
</div>
<template is='dom-if' if='[[error]]'> <template is='dom-if' if='[[error]]'>
<p class='error'>[[error]]</p> <p class='error'>[[error]]</p>
</template> </template>
</div> </div>
<div class='card-actions'> <div class='card-actions'>
<paper-button disabled='[[creatingSnapshot]]' on-click='createSnapshot'>Create</paper-button> <paper-button disabled='[[creatingSnapshot]]' on-click='_createSnapshot'>Create</paper-button>
</div> </div>
</paper-card> </paper-card>
</div> </div>
@ -87,14 +87,14 @@
<div class='card-content'>You don't have any snapshots yet.</div> <div class='card-content'>You don't have any snapshots yet.</div>
</paper-card> </paper-card>
</template> </template>
<template is='dom-repeat' items='[[snapshots]]' as='snapshot' sort='sortSnapshots'> <template is='dom-repeat' items='[[snapshots]]' as='snapshot' sort='_sortSnapshots'>
<paper-card class='pointer' on-click='snapshotTapped'> <paper-card class='pointer' on-click='_snapshotClicked'>
<div class='card-content'> <div class='card-content'>
<hassio-card-content <hassio-card-content
title='[[computeName(snapshot)]]' title='[[_computeName(snapshot)]]'
description='[[computeDetails(snapshot)]]' description='[[_computeDetails(snapshot)]]'
datetime='[[snapshot.date]]' datetime='[[snapshot.date]]'
icon='[[computeIcon(snapshot.type)]]' icon='[[_computeIcon(snapshot.type)]]'
icon-class='snapshot' icon-class='snapshot'
></hassio-card-content> ></hassio-card-content>
</div> </div>
@ -131,7 +131,7 @@ class HassioSnapshots extends window.hassMixins.EventsMixin(Polymer.Element) {
}, },
installedAddons: { installedAddons: {
type: Array, type: Array,
observer: 'installedAddonsChanged', observer: '_installedAddonsChanged',
}, },
addonList: Array, addonList: Array,
folderList: { folderList: {
@ -150,7 +150,7 @@ class HassioSnapshots extends window.hassMixins.EventsMixin(Polymer.Element) {
snapshotDeleted: { snapshotDeleted: {
type: Boolean, type: Boolean,
notify: true, notify: true,
observer: 'snapshotDeletedChanged', observer: '_snapshotDeletedChanged',
}, },
creatingSnapshot: Boolean, creatingSnapshot: Boolean,
dialogOpened: Boolean, dialogOpened: Boolean,
@ -160,17 +160,17 @@ class HassioSnapshots extends window.hassMixins.EventsMixin(Polymer.Element) {
ready() { ready() {
super.ready(); super.ready();
this.addEventListener('hass-api-called', ev => this.apiCalled(ev)); this.addEventListener('hass-api-called', ev => this._apiCalled(ev));
this.updateSnapshots(); this._updateSnapshots();
} }
apiCalled(ev) { _apiCalled(ev) {
if (ev.detail.success) { if (ev.detail.success) {
this.updateSnapshots(); this._updateSnapshots();
} }
} }
updateSnapshots() { _updateSnapshots() {
this.hass.callApi('get', 'hassio/snapshots') this.hass.callApi('get', 'hassio/snapshots')
.then((result) => { .then((result) => {
this.snapshots = result.data.snapshots; this.snapshots = result.data.snapshots;
@ -179,7 +179,7 @@ class HassioSnapshots extends window.hassMixins.EventsMixin(Polymer.Element) {
}); });
} }
createSnapshot() { _createSnapshot() {
this.error = ''; this.error = '';
if (this.snapshotHasPassword && !this.snapshotPassword.length) { if (this.snapshotHasPassword && !this.snapshotPassword.length) {
this.error = 'Please enter a password.'; this.error = 'Please enter a password.';
@ -220,48 +220,42 @@ class HassioSnapshots extends window.hassMixins.EventsMixin(Polymer.Element) {
}); });
} }
installedAddonsChanged(addons) { _installedAddonsChanged(addons) {
this.addonList = addons.map(addon => ({ slug: addon.slug, name: addon.name, checked: true })); this.addonList = addons.map(addon => ({ slug: addon.slug, name: addon.name, checked: true }));
} }
sortAddons(a, b) { _sortAddons(a, b) {
return a.name < b.name ? -1 : 1; return a.name < b.name ? -1 : 1;
} }
sortSnapshots(a, b) { _sortSnapshots(a, b) {
return a.date < b.date ? 1 : -1; return a.date < b.date ? 1 : -1;
} }
computeName(snapshot) { _computeName(snapshot) {
return snapshot.name || snapshot.slug; return snapshot.name || snapshot.slug;
} }
computeDetails(snapshot) { _computeDetails(snapshot) {
const type = snapshot.type === 'full' ? 'Full snapshot' : 'Partial snapshot'; const type = snapshot.type === 'full' ? 'Full snapshot' : 'Partial snapshot';
return snapshot.protected ? `${type}, password protected` : type; return snapshot.protected ? `${type}, password protected` : type;
} }
computeIcon(type) { _computeIcon(type) {
return type === 'full' ? 'mdi:package-variant-closed' : 'mdi:package-variant'; return type === 'full' ? 'mdi:package-variant-closed' : 'mdi:package-variant';
} }
snapshotTapped(ev) { _snapshotClicked(ev) {
this.snapshotSlug = ev.model.snapshot.slug; this.snapshotSlug = ev.model.snapshot.slug;
} }
typeChanged() { _fullSelected(type) {
if (this.snapshotType === 'full') { return type === 'full';
this.$.folders.classList.add('hidden');
this.$.addons.classList.add('hidden');
} else {
this.$.folders.classList.remove('hidden');
this.$.addons.classList.remove('hidden');
}
} }
snapshotDeletedChanged(snapshotDeleted) { _snapshotDeletedChanged(snapshotDeleted) {
if (snapshotDeleted) { if (snapshotDeleted) {
this.updateSnapshots(); this._updateSnapshots();
this.snapshotDeleted = false; this.snapshotDeleted = false;
} }
} }
@ -269,7 +263,7 @@ class HassioSnapshots extends window.hassMixins.EventsMixin(Polymer.Element) {
refreshData() { refreshData() {
this.hass.callApi('post', 'hassio/snapshots/reload') this.hass.callApi('post', 'hassio/snapshots/reload')
.then(() => { .then(() => {
this.updateSnapshots(); this._updateSnapshots();
}); });
} }
} }