Hass.io snapshots -> Lit (#3078)

* Hass.io snapshots

* Fix rootnav
This commit is contained in:
Paulus Schoutsen 2019-04-09 13:05:56 -07:00 committed by GitHub
parent 5b861bb4c6
commit 4d2390daf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 414 additions and 330 deletions

View File

@ -26,21 +26,15 @@ class HassioIngressView extends LitElement {
protected render(): TemplateResult | void {
if (!this._addon) {
return html`
<hass-loading-screen rootnav></hass-loading-screen>
<hass-loading-screen></hass-loading-screen>
`;
}
const iframe = html`
<iframe src=${this._addon.ingress_url}></iframe>
return html`
<hass-subpage .header=${this._addon.name} hassio>
<iframe src=${this._addon.ingress_url}></iframe>
</hass-subpage>
`;
return location.search === "?kiosk"
? iframe
: html`
<hass-subpage .header=${this._addon.name} hassio root>
${iframe}
</hass-subpage>
`;
}
protected updated(changedProps: PropertyValues) {

View File

@ -1,316 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hassio-card-content";
import "../resources/hassio-style";
import EventsMixin from "../../../src/mixins/events-mixin";
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
class HassioSnapshots extends EventsMixin(PolymerElement) {
static get template() {
return html`
<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-input[type="password"] {
display: block;
margin: 4px 0 4px 48px;
}
.pointer {
cursor: pointer;
}
</style>
<div class="content">
<div class="card-group">
<div class="title">
Create 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>
Type:
<paper-radio-group selected="{{snapshotType}}">
<paper-radio-button name="full">
Full snapshot
</paper-radio-button>
<paper-radio-button name="partial">
Partial snapshot
</paper-radio-button>
</paper-radio-group>
<template is="dom-if" if="[[!_fullSelected(snapshotType)]]">
Folders:
<template is="dom-repeat" items="[[folderList]]">
<paper-checkbox checked="{{item.checked}}">
[[item.name]]
</paper-checkbox>
</template>
Add-ons:
<template
is="dom-repeat"
items="[[addonList]]"
sort="_sortAddons"
>
<paper-checkbox checked="{{item.checked}}">
[[item.name]]
</paper-checkbox>
</template>
</template>
Security:
<paper-checkbox checked="{{snapshotHasPassword}}"
>Password protection</paper-checkbox
>
<template is="dom-if" if="[[snapshotHasPassword]]">
<paper-input
label="Password"
type="password"
value="{{snapshotPassword}}"
></paper-input>
</template>
<template is="dom-if" if="[[error]]">
<p class="error">[[error]]</p>
</template>
</div>
<div class="card-actions">
<mwc-button
disabled="[[creatingSnapshot]]"
on-click="_createSnapshot"
>Create</mwc-button
>
</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="_snapshotClicked">
<div class="card-content">
<hassio-card-content
hass="[[hass]]"
title="[[_computeName(snapshot)]]"
description="[[_computeDetails(snapshot)]]"
datetime="[[snapshot.date]]"
icon="[[_computeIcon(snapshot.type)]]"
icon-class="snapshot"
></hassio-card-content>
</div>
</paper-card>
</template>
</div>
</div>
`;
}
static get properties() {
return {
hass: Object,
snapshotName: {
type: String,
value: "",
},
snapshotPassword: {
type: String,
value: "",
},
snapshotHasPassword: Boolean,
snapshotType: {
type: String,
value: "full",
},
snapshots: {
type: Array,
value: [],
},
supervisorInfo: Object,
installedAddons: {
type: Array,
computed: "_computeAddons(supervisorInfo)",
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 },
],
},
snapshotSlug: {
type: String,
notify: true,
},
snapshotDeleted: {
type: Boolean,
notify: true,
observer: "_snapshotDeletedChanged",
},
creatingSnapshot: Boolean,
dialogOpened: Boolean,
error: String,
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
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.error = "";
if (this.snapshotHasPassword && !this.snapshotPassword.length) {
this.error = "Please enter a password.";
return;
}
this.creatingSnapshot = true;
let name = this.snapshotName;
if (!name.length) {
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";
}
if (this.snapshotHasPassword) {
data.password = this.snapshotPassword;
}
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;
}
_computeDetails(snapshot) {
const type =
snapshot.type === "full" ? "Full snapshot" : "Partial snapshot";
return snapshot.protected ? `${type}, password protected` : type;
}
_computeIcon(type) {
return type === "full"
? "hassio:package-variant-closed"
: "hassio:package-variant";
}
_snapshotClicked(ev) {
showHassioSnapshotDialog(this, {
slug: ev.model.snapshot.slug,
onDelete: () => this._updateSnapshots(),
});
this.snapshotSlug = ev.model.snapshot.slug;
}
_fullSelected(type) {
return type === "full";
}
refreshData() {
this.hass.callApi("post", "hassio/snapshots/reload").then(() => {
this._updateSnapshots();
});
}
_computeAddons(supervisorInfo) {
return supervisorInfo.addons;
}
}
customElements.define("hassio-snapshots", HassioSnapshots);

View File

@ -0,0 +1,363 @@
import {
LitElement,
TemplateResult,
html,
CSSResultArray,
css,
property,
PropertyValues,
customElement,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
import { HomeAssistant } from "../../../src/types";
import {
HassioSnapshot,
HassioSupervisorInfo,
fetchHassioSnapshots,
reloadHassioSnapshots,
HassioFullSnapshotCreateParams,
HassioPartialSnapshotCreateParams,
createHassioFullSnapshot,
createHassioPartialSnapshot,
} from "../../../src/data/hassio";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { fireEvent } from "../../../src/common/dom/fire_event";
// Not duplicate, used for typing
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
// tslint:disable-next-line
import { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group";
// tslint:disable-next-line
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
interface CheckboxItem {
slug: string;
name: string;
checked: boolean;
}
@customElement("hassio-snapshots")
class HassioSnapshots extends LitElement {
@property() public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() private _snapshotName = "";
@property() private _snapshotPassword = "";
@property() private _snapshotHasPassword = false;
@property() private _snapshotType: HassioSnapshot["type"] = "full";
@property() private _snapshots?: HassioSnapshot[] = [];
@property() private _addonList: CheckboxItem[] = [];
@property() private _folderList: CheckboxItem[] = [
{
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 },
];
@property() private _creatingSnapshot = false;
@property() private _error = "";
public async refreshData() {
await reloadHassioSnapshots(this.hass);
await this._updateSnapshots();
}
protected render(): TemplateResult | void {
return html`
<div class="content">
<div class="card-group">
<div class="title">
Create 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"
name="snapshotName"
.value=${this._snapshotName}
@value-changed=${this._handleTextValueChanged}
></paper-input>
Type:
<paper-radio-group
name="snapshotType"
.selected=${this._snapshotType}
@selected-changed=${this._handleRadioValueChanged}
>
<paper-radio-button name="full">
Full snapshot
</paper-radio-button>
<paper-radio-button name="partial">
Partial snapshot
</paper-radio-button>
</paper-radio-group>
${this._snapshotType === "full"
? undefined
: html`
Folders:
${this._folderList.map(
(folder, idx) => html`
<paper-checkbox
.idx=${idx}
.checked=${folder.checked}
@checked-changed=${this._folderChecked}
>
${folder.name}
</paper-checkbox>
`
)}
Add-ons:
${this._addonList.map(
(addon, idx) => html`
<paper-checkbox
.idx=${idx}
.checked="{{item.checked}}"
@checked-changed=${this._addonChecked}
>
${addon.name}
</paper-checkbox>
`
)}
`}
Security:
<paper-checkbox
name="snapshotHasPassword"
.checked=${this._snapshotHasPassword}
@checked-changed=${this._handleCheckboxValueChanged}
>
Password protection
</paper-checkbox>
${this._snapshotHasPassword
? html`
<paper-input
label="Password"
type="password"
name="snapshotPassword"
.value=${this._snapshotPassword}
@value-changed=${this._handleTextValueChanged}
></paper-input>
`
: undefined}
${this._error !== ""
? html`
<p class="error">${this._error}</p>
`
: undefined}
</div>
<div class="card-actions">
<mwc-button
.disabled=${this._creatingSnapshot}
@click=${this._createSnapshot}
>
Create
</mwc-button>
</div>
</paper-card>
</div>
<div class="card-group">
<div class="title">Available snapshots</div>
${this._snapshots === undefined
? undefined
: this._snapshots.length === 0
? html`
<paper-card>
<div class="card-content">
You don't have any snapshots yet.
</div>
</paper-card>
`
: this._snapshots.map(
(snapshot) => html`
<paper-card
class="pointer"
.snapshot=${snapshot}
@click=${this._snapshotClicked}
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${snapshot.name || snapshot.slug}
.description=${this._computeDetails(snapshot)}
.datetime=${snapshot.date}
.icon=${snapshot.type === "full"
? "hassio:package-variant-closed"
: "hassio:package-variant"}
.
.icon-class="snapshot"
></hassio-card-content>
</div>
</paper-card>
`
)}
</div>
</div>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._updateSnapshots();
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("supervisorInfo")) {
this._addonList = this.supervisorInfo.addons
.map((addon) => ({
slug: addon.slug,
name: addon.name,
checked: true,
}))
.sort((a, b) => (a.name < b.name ? -1 : 1));
}
}
private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
const input = ev.currentTarget as PaperInputElement;
this[`_${input.name}`] = ev.detail.value;
}
private _handleCheckboxValueChanged(ev) {
const input = ev.currentTarget as PaperCheckboxElement;
this[`_${input.name}`] = input.checked;
}
private _handleRadioValueChanged(ev: PolymerChangedEvent<string>) {
const input = ev.currentTarget as PaperRadioGroupElement;
this[`_${input.getAttribute("name")}`] = ev.detail.value;
}
private _folderChecked(ev) {
const { idx, checked } = ev.currentTarget!;
this._folderList = this._folderList.map((folder, curIdx) =>
curIdx === idx ? { ...folder, checked } : folder
);
}
private _addonChecked(ev) {
const { idx, checked } = ev.currentTarget!;
this._addonList = this._addonList.map((addon, curIdx) =>
curIdx === idx ? { ...addon, checked } : addon
);
}
private async _updateSnapshots() {
try {
this._snapshots = await fetchHassioSnapshots(this.hass);
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
} catch (err) {
this._error = err.message;
}
}
private async _createSnapshot() {
this._error = "";
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
this._error = "Please enter a password.";
return;
}
this._creatingSnapshot = true;
await this.updateComplete;
const name =
this._snapshotName ||
new Date().toLocaleDateString(navigator.language, {
weekday: "long",
year: "numeric",
month: "short",
day: "numeric",
});
try {
if (this._snapshotType === "full") {
const data: HassioFullSnapshotCreateParams = { name };
if (this._snapshotHasPassword) {
data.password = this._snapshotPassword;
}
await createHassioFullSnapshot(this.hass, data);
} 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);
const data: HassioPartialSnapshotCreateParams = {
name,
folders,
addons,
};
if (this._snapshotHasPassword) {
data.password = this._snapshotPassword;
}
await createHassioPartialSnapshot(this.hass, data);
}
this._updateSnapshots();
fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) {
this._error = err.message;
} finally {
this._creatingSnapshot = false;
}
}
private _computeDetails(snapshot: HassioSnapshot) {
const type =
snapshot.type === "full" ? "Full snapshot" : "Partial snapshot";
return snapshot.protected ? `${type}, password protected` : type;
}
private _snapshotClicked(ev) {
showHassioSnapshotDialog(this, {
slug: ev.currentTarget!.snapshot.slug,
onDelete: () => this._updateSnapshots(),
});
}
static get styles(): CSSResultArray {
return [
hassioStyle,
css`
paper-radio-group {
display: block;
}
paper-radio-button {
padding: 0 0 2px 2px;
}
paper-radio-button,
paper-checkbox,
paper-input[type="password"] {
display: block;
margin: 4px 0 4px 48px;
}
.pointer {
cursor: pointer;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-snapshots": HassioSnapshots;
}
}

View File

@ -33,6 +33,7 @@ module.exports = {
},
optimization: {
...webpackBase.optimization(latestBuild),
// Try #4323432 to get hass.io to wrk on es5
concatenateModules: false,
},
plugins: [

View File

@ -102,6 +102,25 @@ export type HassioHomeAssistantInfo = any;
export type HassioSupervisorInfo = any;
export type HassioHostInfo = any;
export interface HassioSnapshot {
slug: string;
date: string;
name: string;
type: "full" | "partial";
protected: boolean;
}
export interface HassioFullSnapshotCreateParams {
name: string;
password?: string;
}
export interface HassioPartialSnapshotCreateParams {
name: string;
folders: string[];
addons: string[];
password?: string;
}
const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
response.data;
@ -116,9 +135,7 @@ export const createHassioSession = async (hass: HomeAssistant) => {
};
export const reloadHassioAddons = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<unknown>>("POST", `hassio/addons/reload`)
.then(hassioApiResultExtractor);
hass.callApi<unknown>("POST", `hassio/addons/reload`);
export const fetchHassioAddonsInfo = (hass: HomeAssistant) =>
hass
@ -153,3 +170,24 @@ export const fetchHassioHomeAssistantInfo = (hass: HomeAssistant) =>
"hassio/homeassistant/info"
)
.then(hassioApiResultExtractor);
export const fetchHassioSnapshots = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<{ snapshots: HassioSnapshot[] }>>(
"GET",
"hassio/snapshots"
)
.then((resp) => resp.data.snapshots);
export const reloadHassioSnapshots = (hass: HomeAssistant) =>
hass.callApi<unknown>("POST", `hassio/snapshots/reload`);
export const createHassioFullSnapshot = (
hass: HomeAssistant,
data: HassioFullSnapshotCreateParams
) => hass.callApi<unknown>("POST", "hassio/snapshots/new/full", data);
export const createHassioPartialSnapshot = (
hass: HomeAssistant,
data: HassioPartialSnapshotCreateParams
) => hass.callApi<unknown>("POST", "hassio/snapshots/new/partial", data);

View File

@ -28,5 +28,9 @@ declare global {
"hass-notification": {
message: string;
};
"hass-api-called": {
success: boolean;
response: unknown;
};
}
}