Hass.io improved snapshot view (#872)

* HASSIO Improved snapshot view

* HASSIO Improved snapshot view2

* Update main

* Feedback

* Travis

* Travis 2
This commit is contained in:
c727 2018-02-08 22:16:13 +01:00 committed by GitHub
parent 710c2f1094
commit 6ce72444ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 516 additions and 186 deletions

View File

@ -13,7 +13,7 @@
<link rel="import" href="./addon-view/hassio-addon-view.html">
<link rel="import" href="./addon-store/hassio-addon-store.html">
<link rel="import" href="./supervisor/hassio-supervisor.html">
<link rel="import" href="./snapshot/hassio-snapshot.html">
<link rel="import" href="./snapshots/hassio-snapshots.html">
<link rel="import" href="./hassio-data.html">
<dom-module id="hassio-main">
@ -86,10 +86,11 @@
hass-info='[[hassInfo]]'
></hassio-advanced>
<hassio-snapshot
<hassio-snapshots
page-name='snapshot'
hass='[[hass]]'
></hassio-snapshot>
installed-addons='[[supervisorInfo.addons]]'
></hassio-snapshots>
<hass-error-screen
page-name='not-found'

View File

@ -1,183 +0,0 @@
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/app-layout/app-header-layout/app-header-layout.html">
<link rel="import" href="../../bower_components/app-layout/app-header/app-header.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-card/paper-card.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='../../src/util/hass-mixins.html'>
<link rel="import" href="../../src/components/ha-menu-button.html">
<link rel="import" href="../../src/components/buttons/ha-call-api-button.html">
<dom-module id="hassio-snapshot">
<template>
<style include="iron-flex ha-style">
paper-card {
display: block;
}
paper-card {
margin-bottom: 32px;
}
.content {
padding: 24px 0 32px;
max-width: 600px;
margin: 0 auto;
}
.form paper-input {
max-width: 300px;
}
ha-call-api-button {
color: var(--primary-color);
}
</style>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<paper-icon-button
icon='mdi:arrow-left'
on-tap='_backTapped'
></paper-icon-button>
<div main-title>Snapshots</div>
<paper-icon-button
icon="mdi:refresh"
on-tap="_refreshTapped"
></paper-icon-button>
</app-toolbar>
</app-header>
<div class='content'>
<template is='dom-if' if='[[_error]]'>
<p class='error'>[[_error]]</p>
</template>
<paper-card heading='Make a snapshot'>
<div class='card-content form'>
Snapshots allow you to easily backup and restore all data in your Hass.io instance.
<p>
<paper-input
disabled='[[_creatingSnapshot]]'
label="Snapshot name"
value='{{_snapshotName}}'
></paper-input>
</div>
<div class='card-actions'>
<paper-button
disabled='[[_creatingSnapshot]]'
on-tap='_createSnapshot'
>Create</paper-button>
</div>
</paper-card>
<paper-card heading='Available Snapshots'>
<div class='card-content'>
Snapshots allow you to easily backup and restore all data in your Hass.io instance.
<template is='dom-if' if='[[!data.length]]'>
<p>Looks like you don't have any snapshots yet.</p>
</template>
</div>
<template is='dom-repeat' items='[[_data]]' as='snapshot'>
<paper-item>
<paper-item-body two-line>
<div>[[snapshot.name]]</div>
<div secondary>[[snapshot.date]]</div>
</paper-item-body>
<ha-call-api-button
hass='[[hass]]'
path="[[_computeRestorePath(snapshot)]]"
>Restore</ha-call-api-button>
</paper-item>
</template>
</paper-card>
</div>
</app-header-layout>
</template>
</dom-module>
<script>
class HassioSnapshot extends window.hassMixins.EventsMixin(Polymer.Element) {
static get is() { return 'hassio-snapshot'; }
static get properties() {
return {
hass: Object,
visible: {
type: Boolean,
observer: '_visibleChanged',
},
_snapshotName: String,
_creatingSnapshot: Boolean,
_error: Object,
_data: {
type: Array,
value: [],
},
};
}
ready() {
super.ready();
this.addEventListener('hass-api-called', ev => this.apiCalled(ev));
}
_visibleChanged(visible) {
if (visible) {
this._updateData();
}
}
_apiCalled(ev) {
if (ev.detail.success) {
this._updateData();
}
}
_updateData() {
this.hass.callApi('get', 'hassio/snapshots')
.then(function (result) {
this._data = result.data.snapshots;
}.bind(this), function (error) {
this._error = error.message;
}.bind(this));
}
_createSnapshot() {
this._creatingSnapshot = true;
this.hass.callApi('post', 'hassio/snapshots/new/full', {
name: this._snapshotName,
})
.then(function () {
this._creatingSnapshot = false;
this._snapshotName = '';
// This will trigger a refresh of supervisor info
this.fire('hass-api-called', { success: true });
}.bind(this), function (error) {
this._creatingSnapshot = false;
this._error = error.message;
}.bind(this));
}
_computeRestorePath(snapshot) {
return 'hassio/snapshots/' + snapshot.slug + '/restore/full';
}
_backTapped() {
history.back();
}
_refreshTapped() {
this.hass.callApi('post', 'hassio/snapshots/reload')
.then(function () {
this._updateData();
}.bind(this));
}
}
customElements.define(HassioSnapshot.is, HassioSnapshot);
</script>

View File

@ -0,0 +1,221 @@
<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-checkbox/paper-checkbox.html'>
<link rel='import' href='../../bower_components/paper-button/paper-button.html'>
<dom-module id='hassio-snapshot'>
<template>
<style include='ha-style'>
paper-checkbox {
display: block;
margin: 4px;
}
.snapshot-details {
color: var(--secondary-text-color);
margin-bottom: 16px;
}
.addon-version {
color: var(--secondary-text-color);
}
.warning,
.error {
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>
<paper-dialog id='dialog' with-backdrop on-iron-overlay-closed='dialogClosed'>
<h2>[[computeName(snapshot)]]</h2>
<div>
<div class='snapshot-details'>
[[computeType(snapshot.type)]] ([[computeSize(snapshot.size)]])<br/>
[[formatDatetime(snapshot.date)]]
</div>
<div>
Home Assistant:
<paper-checkbox checked='{{restoreHass}}'>
Home Assistant [[snapshot.homeassistant]]
</paper-checkbox>
</div>
<div>
Folders:
<template is='dom-repeat' items='[[snapshot.folders]]'>
<paper-checkbox checked='{{item.checked}}'>
[[item.name]]
</paper-checkbox>
</template>
</div>
<template is='dom-if' if='[[snapshot.addons.length]]'>
<div>
Add-ons:
<template is='dom-repeat' items='[[snapshot.addons]]' sort='sortAddons'>
<paper-checkbox checked='{{item.checked}}'>
[[item.name]]
<span class='addon-version'>([[item.version]])</span>
</paper-checkbox>
</template>
</div>
</template>
<template is='dom-if' if='[[error]]'>
<p class='error'>Error: [[error]]</p>
</template>
</div>
<div class='buttons'>
<paper-button class='warning' on-tap='deleteTapped'>Delete</paper-button>
<paper-button on-tap='partialRestoreTapped'>Partial restore</paper-button>
<template is='dom-if' if='[[isFullSnapshot(snapshot.type)]]'>
<paper-button on-tap='fullRestoreTapped'>Full restore</paper-button>
</template>
</div>
</paper-dialog>
</template>
</dom-module>
<script>
class HassioSnapshot extends window.hassMixins.EventsMixin(Polymer.Element) {
static get is() { return 'hassio-snapshot'; }
static get properties() {
return {
hass: Object,
snapshotSlug: {
type: String,
observer: 'computeSnapshot',
},
snapshot: Object,
restoreHass: {
type: Boolean,
value: true,
},
opened: {
type: Boolean,
observer: 'openedChanged',
notify: true,
},
updateOverview: {
type: Boolean,
notify: true,
},
error: String,
};
}
computeSnapshot(snapshotSlug) {
this.hass.callApi('get', `hassio/snapshots/${snapshotSlug}/info`)
.then((info) => {
info.data.folders = this.computeFolders(info.data.folders);
info.data.addons = this.computeAddons(info.data.addons);
this.snapshot = info.data;
}, () => {
this.snapshot = null;
});
}
computeFolders(folders) {
const list = [];
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('share')) list.push({ slug: 'share', name: 'Share', checked: true });
if (folders.includes('addons/local')) list.push({ slug: 'addons/local', name: 'Local add-ons', checked: true });
return list;
}
computeAddons(addons) {
return addons.map(addon => (
{ slug: addon.slug, name: addon.name, version: addon.version, checked: true }));
}
isFullSnapshot(type) {
return type === 'full';
}
partialRestoreTapped() {
if (!confirm('Are you sure you want to restore this snapshot?')) {
return;
}
const addons = this.snapshot.addons.filter(addon => addon.checked).map(addon => addon.slug);
const folders =
this.snapshot.folders.filter(folder => folder.checked).map(folder => folder.slug);
this.hass.callApi('post', `hassio/snapshots/${this.snapshotSlug}/restore/partial`, {
homeassistant: this.restoreHass,
addons: addons,
folders: folders
}).then(() => {
alert('Snapshot restored!');
this.$.dialog.close();
}, (error) => {
this.error = error.body.message;
});
}
fullRestoreTapped() {
if (!confirm('Are you sure you want to restore this snapshot?')) {
return;
}
this.hass.callApi('post', `hassio/snapshots/${this.snapshotSlug}/restore/full`)
.then(() => {
alert('Snapshot restored!');
this.$.dialog.close();
}, (error) => {
this.error = error.body.message;
});
}
deleteTapped() {
if (!confirm('Are you sure you want to delete this snapshot?')) {
return;
}
this.hass.callApi('post', `hassio/snapshots/${this.snapshotSlug}/remove`)
.then(() => {
this.$.dialog.close();
this.updateOverview = true;
}, (error) => {
this.error = error.body.message;
});
}
computeName(snapshot) {
return snapshot.name || snapshot.slug;
}
computeType(type) {
return type === 'full' ? 'Full snapshot' : 'Partial snapshot';
}
computeSize(size) {
return Math.ceil(size) + ' MB';
}
sortAddons(a, b) {
return a.name < b.name ? -1 : 1;
}
formatDatetime(datetime) {
return new Date(datetime + '+00:00').toLocaleDateString(navigator.language, {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
}
openedChanged(opened) {
if (opened) this.$.dialog.open();
}
dialogClosed() {
this.opened = false;
}
}
customElements.define(HassioSnapshot.is, HassioSnapshot);
</script>

View File

@ -0,0 +1,291 @@
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/app-layout/app-header-layout/app-header-layout.html">
<link rel="import" href="../../bower_components/app-layout/app-header/app-header.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-card/paper-card.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-radio-group/paper-radio-group.html'>
<link rel='import' href='../../bower_components/paper-radio-button/paper-radio-button.html'>
<link rel='import' href='../../bower_components/paper-checkbox/paper-checkbox.html'>
<link rel='import' href='../../src/util/hass-mixins.html'>
<link rel='import' href='../../src/components/hassio-card-content.html'>
<link rel='import' href='../../src/resources/hassio-style.html'>
<link rel='import' href='./hassio-snapshot.html'>
<dom-module id="hassio-snapshots">
<template>
<style include="ha-style hassio-style">
paper-checkbox {
display: block;
margin: 4px 0 4px 48px;
}
.pointer {
cursor: pointer;
}
.hidden {
display: none;
}
</style>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<paper-icon-button
icon='mdi:arrow-left'
on-tap='_backTapped'
></paper-icon-button>
<div main-title>Snapshots</div>
<paper-icon-button
icon="mdi:refresh"
on-tap="_refreshTapped"
></paper-icon-button>
</app-toolbar>
</app-header>
<div class='content'>
<div class='card-group'>
<div class='title'>
New snapshot
<div class='description'>
Snapshots allow you to easily backup and
restore all data of your Hass.io instance.
</div>
</div>
<paper-card>
<div class='card-content'>
<paper-input autofocus label='Name' value='{{snapshotName}}'>
</paper-input>
<label id='lbltype'>Type:</label>
<paper-radio-group selected='{{snapshotType}}' aria-labelledby='lbltype' on-paper-radio-group-changed='typeChanged'>
<paper-radio-button name='full'>
Full snapshot
</paper-radio-button>
<paper-radio-button name='partial'>
Partial snapshot
</paper-radio-button>
</paper-radio-group>
<div id='folders' class='hidden'>
Folders:
<template is='dom-repeat' items='[[folderList]]'>
<paper-checkbox checked='{{item.checked}}'>
[[item.name]]
</paper-checkbox>
</template>
</div>
<div id='addons' class='hidden'>
Add-ons:
<template is='dom-repeat' items='[[addonList]]' sort='sortAddons'>
<paper-checkbox checked='{{item.checked}}'>
[[item.name]]
</paper-checkbox>
</template>
</div>
</div>
<div class='card-actions'>
<paper-button disabled='[[creatingSnapshot]]' on-tap='createSnapshot'>Create</paper-button>
<template is='dom-if' if='[[error]]'>
<p class='error'>[[error]]</p>
</template>
</div>
</paper-card>
</div>
<div class='card-group'>
<div class='title'>Available snapshots</div>
<template is='dom-if' if='[[!snapshots.length]]'>
<paper-card>
<div class='card-content'>You don't have any snapshots yet.</div>
</paper-card>
</template>
<template is='dom-repeat' items='[[snapshots]]' as='snapshot' sort='sortSnapshots'>
<paper-card class='pointer' on-click='snapshotTapped'>
<div class='card-content'>
<hassio-card-content
title='[[computeName(snapshot)]]'
description='[[computeType(snapshot.type)]]'
datetime='[[snapshot.date]]+00:00'
icon='[[computeIcon(snapshot.type)]]'
icon-class='snapshot'
></hassio-card-content>
</div>
</paper-card>
</template>
</div>
</div>
</app-header-layout>
<hassio-snapshot
hass='[[hass]]'
snapshot-slug='[[selectedSnapshot]]'
opened='{{dialogOpened}}'
update-overview='{{updateOverview}}'
></hassio-snapshot>
</template>
</dom-module>
<script>
class HassioSnapshots extends window.hassMixins.EventsMixin(Polymer.Element) {
static get is() { return 'hassio-snapshots'; }
static get properties() {
return {
hass: Object,
visible: {
type: Boolean,
observer: 'visibleChanged',
},
snapshotName: String,
snapshotType: {
type: String,
value: 'full',
},
snapshots: {
type: Array,
value: [],
},
installedAddons: {
type: Array,
observer: 'installedAddonsChanged',
},
addonList: Array,
folderList: {
type: Array,
value: [
{ slug: 'homeassistant', name: 'Home Assistant configuration', checked: true },
{ slug: 'ssl', name: 'SSL', checked: true },
{ slug: 'share', name: 'Share', checked: true },
{ slug: 'addons/local', name: 'Local add-ons', checked: true },
],
},
selectedSnapshot: String,
creatingSnapshot: Boolean,
dialogOpened: Boolean,
updateOverview: {
type: Boolean,
observer: 'updateOverviewChanged',
},
error: String,
};
}
ready() {
super.ready();
this.addEventListener('hass-api-called', ev => this.apiCalled(ev));
}
visibleChanged(visible) {
if (visible) {
this.updateSnapshots();
}
}
apiCalled(ev) {
if (ev.detail.success) {
this.updateSnapshots();
}
}
updateSnapshots() {
this.hass.callApi('get', 'hassio/snapshots')
.then((result) => {
this.snapshots = result.data.snapshots;
}, (error) => {
this.error = error.message;
});
}
createSnapshot() {
this.creatingSnapshot = true;
let name = this.snapshotName;
if (!name || name.length === 0) {
name = new Date().toLocaleDateString(navigator.language, {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric' });
}
let data;
let path;
if (this.snapshotType === 'full') {
data = { name: name };
path = 'hassio/snapshots/new/full';
} else {
const addons = this.addonList.filter(addon => addon.checked).map(addon => addon.slug);
const folders = this.folderList.filter(folder => folder.checked).map(folder => folder.slug);
data = { name: name, folders: folders, addons: addons };
path = 'hassio/snapshots/new/partial';
}
this.hass.callApi('post', path, data)
.then(() => {
this.creatingSnapshot = false;
this.fire('hass-api-called', { success: true });
}, (error) => {
this.creatingSnapshot = false;
this.error = error.message;
});
}
installedAddonsChanged(addons) {
this.addonList = addons.map(addon => ({ slug: addon.slug, name: addon.name, checked: true }));
}
sortAddons(a, b) {
return a.name < b.name ? -1 : 1;
}
sortSnapshots(a, b) {
return a.date < b.date ? 1 : -1;
}
computeName(snapshot) {
return snapshot.name || snapshot.slug;
}
computeType(type) {
return type === 'full' ? 'Full snapshot' : 'Partial snapshot';
}
computeIcon(type) {
return type === 'full' ? 'mdi:package-variant-closed' : 'mdi:package-variant';
}
snapshotTapped(ev) {
this.selectedSnapshot = ev.model.snapshot.slug;
this.dialogOpened = true;
}
typeChanged() {
if (this.snapshotType === 'full') {
this.$.folders.classList.add('hidden');
this.$.addons.classList.add('hidden');
} else {
this.$.folders.classList.remove('hidden');
this.$.addons.classList.remove('hidden');
}
}
updateOverviewChanged(update) {
if (update) {
this.updateSnapshots();
this.updateOverview = false;
}
}
backTapped() {
history.back();
}
refreshTapped() {
this.hass.callApi('post', 'hassio/snapshots/reload')
.then(() => {
this.updateSnapshots();
});
}
}
customElements.define(HassioSnapshots.is, HassioSnapshots);
</script>