Merge pull request #3080 from home-assistant/dev

20190410.0
This commit is contained in:
Paulus Schoutsen 2019-04-10 15:00:36 -07:00 committed by GitHub
commit 038f7b43d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
145 changed files with 6782 additions and 4883 deletions

View File

@ -980,12 +980,12 @@ export const demoEntitiesArsaboo: () => Entity[] = () =>
entity_id: "climate.upstairs",
state: "auto",
attributes: {
current_temperature: 66,
min_temp: 45,
max_temp: 95,
current_temperature: 22,
min_temp: 15,
max_temp: 30,
temperature: null,
target_temp_high: 79,
target_temp_low: 66,
target_temp_high: 24,
target_temp_low: 20,
fan_mode: "auto",
fan_list: ["auto", "on"],
operation_mode: "auto",

View File

@ -19,7 +19,7 @@ module.exports = {
devtool: isProd ? "cheap-source-map" : "inline-source-map",
entry: {
main: "./src/entrypoint.ts",
compatibility: "../src/entrypoints/compatibility.js",
compatibility: "../src/entrypoints/compatibility.ts",
},
module: {
rules: [

View File

@ -44,7 +44,7 @@ function transformXMLtoPolymer(name, xml) {
// Given an iconset name and icon names, generate a polymer iconset
function generateIconset(name, iconNames) {
const iconDefs = iconNames
const iconDefs = Array.from(iconNames)
.map((name) => {
const iconDef = loadIcon(name);
if (!iconDef) {
@ -95,18 +95,27 @@ function findIcons(path, iconsetName) {
}
mapFiles(path, ".js", processFile);
mapFiles(path, ".ts", processFile);
return Array.from(icons);
return icons;
}
function genHassIcons() {
const iconNames = findIcons("./src", "hass").concat(BUILT_IN_PANEL_ICONS);
fs.existsSync(OUTPUT_DIR) || fs.mkdirSync(OUTPUT_DIR);
const iconNames = findIcons("./src", "hass");
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
}
gulp.task("gen-icons-mdi", () => genMDIIcons());
gulp.task("gen-icons-hass", () => genHassIcons());
gulp.task("gen-icons", ["gen-icons-hass", "gen-icons-mdi"], () => {});
gulp.task("gen-icons-mdi", (done) => {
genMDIIcons();
done();
});
gulp.task("gen-icons-hass", (done) => {
genHassIcons();
done();
});
gulp.task("gen-icons", gulp.series("gen-icons-hass", "gen-icons-mdi"));
module.exports = {
findIcons,

View File

@ -118,198 +118,219 @@ tasks.push(taskName);
* the Lokalise update to translations/en.json will not happen immediately.
*/
taskName = "build-master-translation";
gulp.task(taskName, ["clean-translations"], function() {
return gulp
.src("src/translations/en.json")
.pipe(
transform(function(data, file) {
return lokalise_transform(data, data);
})
)
.pipe(rename("translationMaster.json"))
.pipe(gulp.dest(workDir));
});
gulp.task(
taskName,
gulp.series("clean-translations", function() {
return gulp
.src("src/translations/en.json")
.pipe(
transform(function(data, file) {
return lokalise_transform(data, data);
})
)
.pipe(rename("translationMaster.json"))
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
taskName = "build-merged-translations";
gulp.task(taskName, ["build-master-translation"], function() {
return gulp.src(inDir + "/*.json").pipe(
foreach(function(stream, file) {
// For each language generate a merged json file. It begins with the master
// translation as a failsafe for untranslated strings, and merges all parent
// tags into one file for each specific subtag
//
// TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated
// than a base translation + region.
const tr = path.basename(file.history[0], ".json");
const subtags = tr.split("-");
const src = [workDir + "/translationMaster.json"];
for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-");
src.push(inDir + "/" + lang + ".json");
}
return gulp
.src(src)
.pipe(transform((data) => emptyFilter(data)))
.pipe(
merge({
fileName: tr + ".json",
})
)
.pipe(gulp.dest(fullDir));
})
);
});
gulp.task(
taskName,
gulp.series("build-master-translation", function() {
return gulp.src(inDir + "/*.json").pipe(
foreach(function(stream, file) {
// For each language generate a merged json file. It begins with the master
// translation as a failsafe for untranslated strings, and merges all parent
// tags into one file for each specific subtag
//
// TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated
// than a base translation + region.
const tr = path.basename(file.history[0], ".json");
const subtags = tr.split("-");
const src = [workDir + "/translationMaster.json"];
for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-");
src.push(inDir + "/" + lang + ".json");
}
return gulp
.src(src, { allowEmpty: true })
.pipe(transform((data) => emptyFilter(data)))
.pipe(
merge({
fileName: tr + ".json",
})
)
.pipe(gulp.dest(fullDir));
})
);
})
);
tasks.push(taskName);
const splitTasks = [];
TRANSLATION_FRAGMENTS.forEach((fragment) => {
taskName = "build-translation-fragment-" + fragment;
gulp.task(taskName, ["build-merged-translations"], function() {
// Return only the translations for this fragment.
return gulp
.src(fullDir + "/*.json")
.pipe(
transform((data) => ({
ui: {
panel: {
[fragment]: data.ui.panel[fragment],
gulp.task(
taskName,
gulp.series("build-merged-translations", function() {
// Return only the translations for this fragment.
return gulp
.src(fullDir + "/*.json")
.pipe(
transform((data) => ({
ui: {
panel: {
[fragment]: data.ui.panel[fragment],
},
},
},
}))
)
.pipe(gulp.dest(workDir + "/" + fragment));
});
}))
)
.pipe(gulp.dest(workDir + "/" + fragment));
})
);
tasks.push(taskName);
splitTasks.push(taskName);
});
taskName = "build-translation-core";
gulp.task(taskName, ["build-merged-translations"], function() {
// Remove the fragment translations from the core translation.
return gulp
.src(fullDir + "/*.json")
.pipe(
transform((data) => {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
return data;
})
)
.pipe(gulp.dest(coreDir));
});
gulp.task(
taskName,
gulp.series("build-merged-translations", function() {
// Remove the fragment translations from the core translation.
return gulp
.src(fullDir + "/*.json")
.pipe(
transform((data) => {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
return data;
})
)
.pipe(gulp.dest(coreDir));
})
);
tasks.push(taskName);
splitTasks.push(taskName);
taskName = "build-flattened-translations";
gulp.task(taskName, splitTasks, function() {
// Flatten the split versions of our translations, and move them into outDir
return gulp
.src(
TRANSLATION_FRAGMENTS.map(
(fragment) => workDir + "/" + fragment + "/*.json"
).concat(coreDir + "/*.json"),
{ base: workDir }
)
.pipe(
transform(function(data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
})
)
.pipe(minify())
.pipe(hashFilename())
.pipe(
rename((filePath) => {
if (filePath.dirname === "core") {
filePath.dirname = "";
}
})
)
.pipe(gulp.dest(outDir));
});
gulp.task(
taskName,
gulp.series(...splitTasks, function() {
// Flatten the split versions of our translations, and move them into outDir
return gulp
.src(
TRANSLATION_FRAGMENTS.map(
(fragment) => workDir + "/" + fragment + "/*.json"
).concat(coreDir + "/*.json"),
{ base: workDir }
)
.pipe(
transform(function(data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
})
)
.pipe(minify())
.pipe(hashFilename())
.pipe(
rename((filePath) => {
if (filePath.dirname === "core") {
filePath.dirname = "";
}
})
)
.pipe(gulp.dest(outDir));
})
);
tasks.push(taskName);
taskName = "build-translation-fingerprints";
gulp.task(taskName, ["build-flattened-translations"], function() {
return gulp
.src(outDir + "/**/*.json")
.pipe(
rename({
extname: "",
})
)
.pipe(
hash({
algorithm: "md5",
hashLength: 32,
template: "<%= name %>.json",
})
)
.pipe(hash.manifest("translationFingerprints.json"))
.pipe(
transform(function(data) {
// After generating fingerprints of our translation files, consolidate
// all translation fragment fingerprints under the translation name key
const newData = {};
Object.entries(data).forEach(([key, value]) => {
const [path, _md5] = key.rsplit("-", 1);
// let translation = key;
let translation = path;
const parts = translation.split("/");
if (parts.length === 2) {
translation = parts[1];
}
if (!(translation in newData)) {
newData[translation] = {
fingerprints: {},
};
}
newData[translation].fingerprints[path] = value;
});
return newData;
})
)
.pipe(gulp.dest(workDir));
});
gulp.task(
taskName,
gulp.series("build-flattened-translations", function() {
return gulp
.src(outDir + "/**/*.json")
.pipe(
rename({
extname: "",
})
)
.pipe(
hash({
algorithm: "md5",
hashLength: 32,
template: "<%= name %>.json",
})
)
.pipe(hash.manifest("translationFingerprints.json"))
.pipe(
transform(function(data) {
// After generating fingerprints of our translation files, consolidate
// all translation fragment fingerprints under the translation name key
const newData = {};
Object.entries(data).forEach(([key, value]) => {
const [path, _md5] = key.rsplit("-", 1);
// let translation = key;
let translation = path;
const parts = translation.split("/");
if (parts.length === 2) {
translation = parts[1];
}
if (!(translation in newData)) {
newData[translation] = {
fingerprints: {},
};
}
newData[translation].fingerprints[path] = value;
});
return newData;
})
)
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
taskName = "build-translations";
gulp.task(taskName, ["build-translation-fingerprints"], function() {
return gulp
.src([
"src/translations/translationMetadata.json",
workDir + "/translationFingerprints.json",
])
.pipe(merge({}))
.pipe(
transform(function(data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
});
gulp.task(
taskName,
gulp.series("build-translation-fingerprints", function() {
return gulp
.src([
"src/translations/translationMetadata.json",
workDir + "/translationFingerprints.json",
])
.pipe(merge({}))
.pipe(
transform(function(data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
module.exports = tasks;

View File

@ -6,10 +6,13 @@ const {
genMDIIcons,
} = require("../../gulp/tasks/gen-icons.js");
const MENU_BUTTON_ICON = "menu";
function genHassioIcons() {
const iconNames = findIcons("./src", "hassio").concat(MENU_BUTTON_ICON);
const iconNames = findIcons("./src", "hassio");
for (const item of findIcons("../src", "hassio")) {
iconNames.add(item);
}
fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames));
}

View File

@ -1,103 +0,0 @@
import "@polymer/paper-card/paper-card";
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 NavigateMixin from "../../../src/mixins/navigate-mixin";
class HassioAddonRepository extends NavigateMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style hassio-style">
paper-card {
cursor: pointer;
}
.not_available {
opacity: 0.6;
}
a.repo {
display: block;
color: var(--primary-text-color);
}
</style>
<template is="dom-if" if="[[addons.length]]">
<div class="card-group">
<div class="title">
[[repo.name]]
<div class="description">
Maintained by [[repo.maintainer]]
<a class="repo" href="[[repo.url]]" target="_blank"
>[[repo.url]]</a
>
</div>
</div>
<template
is="dom-repeat"
items="[[addons]]"
as="addon"
sort="sortAddons"
>
<paper-card class$="[[computeClass(addon)]]" on-click="addonTapped">
<div class="card-content">
<hassio-card-content
hass="[[hass]]"
title="[[addon.name]]"
description="[[addon.description]]"
available="[[addon.available]]"
icon="[[computeIcon(addon)]]"
icon-title="[[computeIconTitle(addon)]]"
icon-class="[[computeIconClass(addon)]]"
></hassio-card-content>
</div>
</paper-card>
</template>
</div>
</template>
`;
}
static get properties() {
return {
hass: Object,
repo: Object,
addons: Array,
};
}
sortAddons(a, b) {
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
}
computeIcon(addon) {
return addon.installed && addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle";
}
computeIconTitle(addon) {
if (addon.installed)
return addon.installed !== addon.version
? "New version available"
: "Add-on is installed";
return addon.available
? "Add-on is not installed"
: "Add-on is not available on your system";
}
computeIconClass(addon) {
if (addon.installed)
return addon.installed !== addon.version ? "update" : "installed";
return !addon.available ? "not_available" : "";
}
computeClass(addon) {
return !addon.available ? "not_available" : "";
}
addonTapped(ev) {
this.navigate(`/hassio/addon/${ev.model.addon.slug}`);
}
}
customElements.define("hassio-addon-repository", HassioAddonRepository);

View File

@ -0,0 +1,112 @@
import {
css,
TemplateResult,
html,
LitElement,
property,
CSSResultArray,
} from "lit-element";
import "@polymer/paper-card/paper-card";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonInfo,
HassioAddonRepository,
} from "../../../src/data/hassio";
import { navigate } from "../../../src/common/navigate";
class HassioAddonRepositoryEl extends LitElement {
@property() public hass!: HomeAssistant;
@property() public repo!: HassioAddonRepository;
@property() public addons!: HassioAddonInfo[];
protected render(): TemplateResult | void {
const repo = this.repo;
return html`
<div class="card-group">
<div class="title">
${repo.name}
<div class="description">
Maintained by ${repo.maintainer}<br />
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
</div>
</div>
${this.addons
.sort((a, b) =>
a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
)
.map(
(addon) => html`
<paper-card
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this.addonTapped}
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
.available=${addon.available}
.icon=${this.computeIcon(addon)}
.iconTitle=${this.computeIconTitle(addon)}
.iconClass=${this.computeIconClass(addon)}
></hassio-card-content>
</div>
</paper-card>
`
)}
</div>
`;
}
private computeIcon(addon) {
return addon.installed && addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle";
}
private computeIconTitle(addon) {
if (addon.installed) {
return addon.installed !== addon.version
? "New version available"
: "Add-on is installed";
}
return addon.available
? "Add-on is not installed"
: "Add-on is not available on your system";
}
private computeIconClass(addon) {
if (addon.installed) {
return addon.installed !== addon.version ? "update" : "installed";
}
return !addon.available ? "not_available" : "";
}
private addonTapped(ev) {
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
}
static get styles(): CSSResultArray {
return [
hassioStyle,
css`
paper-card {
cursor: pointer;
}
.not_available {
opacity: 0.6;
}
a.repo {
color: var(--primary-text-color);
}
`,
];
}
}
customElements.define("hassio-addon-repository", HassioAddonRepositoryEl);

View File

@ -1,92 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hassio-addon-repository";
import "./hassio-repositories-editor";
class HassioAddonStore extends PolymerElement {
static get template() {
return html`
<style include="iron-flex ha-style">
hassio-addon-repository {
margin-top: 24px;
}
</style>
<hassio-repositories-editor
hass="[[hass]]"
repos="[[repos]]"
></hassio-repositories-editor>
<template is="dom-repeat" items="[[repos]]" as="repo" sort="sortRepos">
<hassio-addon-repository
hass="[[hass]]"
repo="[[repo]]"
addons="[[computeAddons(repo.slug)]]"
></hassio-addon-repository>
</template>
`;
}
static get properties() {
return {
hass: Object,
addons: Array,
repos: Array,
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this.loadData();
}
apiCalled(ev) {
if (ev.detail.success) {
this.loadData();
}
}
sortRepos(a, b) {
if (a.slug === "local") {
return -1;
}
if (b.slug === "local") {
return 1;
}
if (a.slug === "core") {
return -1;
}
if (b.slug === "core") {
return 1;
}
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
}
computeAddons(repo) {
return this.addons.filter(function(addon) {
return addon.repository === repo;
});
}
loadData() {
this.hass.callApi("get", "hassio/addons").then(
(info) => {
this.addons = info.data.addons;
this.repos = info.data.repositories;
},
() => {
this.addons = [];
this.repos = [];
}
);
}
refreshData() {
this.hass.callApi("post", "hassio/addons/reload").then(() => {
this.loadData();
});
}
}
customElements.define("hassio-addon-store", HassioAddonStore);

View File

@ -0,0 +1,116 @@
import "./hassio-addon-repository";
import "./hassio-repositories-editor";
import { TemplateResult, html } from "lit-html";
import {
LitElement,
CSSResult,
css,
property,
PropertyValues,
} from "lit-element";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonRepository,
HassioAddonInfo,
fetchHassioAddonsInfo,
reloadHassioAddons,
} from "../../../src/data/hassio";
import "../../../src/layouts/loading-screen";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") {
return -1;
}
if (b.slug === "local") {
return 1;
}
if (a.slug === "core") {
return -1;
}
if (b.slug === "core") {
return 1;
}
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
};
class HassioAddonStore extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _addons?: HassioAddonInfo[];
@property() private _repos?: HassioAddonRepository[];
public async refreshData() {
this._repos = undefined;
this._addons = undefined;
await reloadHassioAddons(this.hass);
await this._loadData();
}
protected render(): TemplateResult | void {
if (!this._addons || !this._repos) {
return html`
<loading-screen></loading-screen>
`;
}
const repos: TemplateResult[] = [];
for (const repo of this._repos) {
const addons = this._addons!.filter(
(addon) => addon.repository === repo.slug
);
if (addons.length === 0) {
continue;
}
repos.push(html`
<hassio-addon-repository
.hass=${this.hass}
.repo=${repo}
.addons=${addons}
></hassio-addon-repository>
`);
}
return html`
<hassio-repositories-editor
.hass=${this.hass}
.repos=${this._repos}
></hassio-repositories-editor>
${repos}
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this._loadData();
}
private apiCalled(ev) {
if (ev.detail.success) {
this._loadData();
}
}
private async _loadData() {
try {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
this._repos.sort(sortRepos);
this._addons = addonsInfo.addons;
} catch (err) {
alert("Failed to fetch add-on info");
}
}
static get styles(): CSSResult {
return css`
hassio-addon-repository {
margin-top: 24px;
}
`;
}
}
customElements.define("hassio-addon-store", HassioAddonStore);

View File

@ -1,120 +0,0 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import "../components/hassio-card-content";
import "../resources/hassio-style";
class HassioRepositoriesEditor extends PolymerElement {
static get template() {
return html`
<style include="ha-style hassio-style">
.add {
padding: 12px 16px;
}
iron-icon {
color: var(--secondary-text-color);
margin-right: 16px;
display: inline-block;
}
paper-input {
width: calc(100% - 49px);
display: inline-block;
}
</style>
<div class="card-group">
<div class="title">
Repositories
<div class="description">
Configure which add-on repositories to fetch data from:
</div>
</div>
<template
id="list"
is="dom-repeat"
items="[[repoList]]"
as="repo"
sort="sortRepos"
>
<paper-card>
<div class="card-content">
<hassio-card-content
hass="[[hass]]"
title="[[repo.name]]"
description="[[repo.url]]"
icon="hassio:github-circle"
></hassio-card-content>
</div>
<div class="card-actions">
<ha-call-api-button
hass="[[hass]]"
path="hassio/supervisor/options"
data="[[computeRemoveRepoData(repoList, repo.url)]]"
class="warning"
>Remove</ha-call-api-button
>
</div>
</paper-card>
</template>
<paper-card>
<div class="card-content add">
<iron-icon icon="hassio:github-circle"></iron-icon>
<paper-input
label="Add new repository by URL"
value="{{repoUrl}}"
></paper-input>
</div>
<div class="card-actions">
<ha-call-api-button
hass="[[hass]]"
path="hassio/supervisor/options"
data="[[computeAddRepoData(repoList, repoUrl)]]"
>Add</ha-call-api-button
>
</div>
</paper-card>
</div>
`;
}
static get properties() {
return {
hass: Object,
repos: {
type: Array,
observer: "reposChanged",
},
repoList: Array,
repoUrl: String,
};
}
reposChanged(repos) {
this.repoList = repos.filter(
(repo) => repo.slug !== "core" && repo.slug !== "local"
);
this.repoUrl = "";
}
sortRepos(a, b) {
return a.name < b.name ? -1 : 1;
}
computeRemoveRepoData(repoList, url) {
const list = repoList
.filter((repo) => repo.url !== url)
.map((repo) => repo.source);
return { addons_repositories: list };
}
computeAddRepoData(repoList, url) {
const list = repoList ? repoList.map((repo) => repo.source) : [];
list.push(url);
return { addons_repositories: list };
}
}
customElements.define("hassio-repositories-editor", HassioRepositoriesEditor);

View File

@ -0,0 +1,148 @@
import {
LitElement,
html,
CSSResultArray,
css,
property,
TemplateResult,
customElement,
PropertyValues,
} from "lit-element";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input";
import memoizeOne from "memoize-one";
import "../../../src/components/buttons/ha-call-api-button";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import { HomeAssistant } from "../../../src/types";
import { HassioAddonRepository } from "../../../src/data/hassio";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { repeat } from "lit-html/directives/repeat";
@customElement("hassio-repositories-editor")
class HassioRepositoriesEditor extends LitElement {
@property() public hass!: HomeAssistant;
@property() public repos!: HassioAddonRepository[];
@property() private _repoUrl = "";
private _sortedRepos = memoizeOne((repos: HassioAddonRepository[]) =>
repos
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
protected render(): TemplateResult | void {
const repos = this._sortedRepos(this.repos);
return html`
<div class="card-group">
<div class="title">
Repositories
<div class="description">
Configure which add-on repositories to fetch data from:
</div>
</div>
${// Use repeat so that the fade-out from call-service-api-button
// stays with the correct repo after we add/delete one.
repeat(
repos,
(repo) => repo.slug,
(repo) => html`
<paper-card>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${repo.name}
.description=${repo.url}
icon="hassio:github-circle"
></hassio-card-content>
</div>
<div class="card-actions">
<ha-call-api-button
path="hassio/supervisor/options"
.hass=${this.hass}
.data=${this.computeRemoveRepoData(repos, repo.url)}
class="warning"
>
Remove
</ha-call-api-button>
</div>
</paper-card>
`
)}
<paper-card>
<div class="card-content add">
<iron-icon icon="hassio:github-circle"></iron-icon>
<paper-input
label="Add new repository by URL"
.value=${this._repoUrl}
@value-changed=${this._urlChanged}
></paper-input>
</div>
<div class="card-actions">
<ha-call-api-button
path="hassio/supervisor/options"
.hass=${this.hass}
.data=${this.computeAddRepoData(repos, this._repoUrl)}
>
Add
</ha-call-api-button>
</div>
</paper-card>
</div>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("repos")) {
this._repoUrl = "";
}
}
private _urlChanged(ev: PolymerChangedEvent<string>) {
this._repoUrl = ev.detail.value;
}
private computeRemoveRepoData(repoList, url) {
const list = repoList
.filter((repo) => repo.url !== url)
.map((repo) => repo.source);
return { addons_repositories: list };
}
private computeAddRepoData(repoList, url) {
const list = repoList ? repoList.map((repo) => repo.source) : [];
list.push(url);
return { addons_repositories: list };
}
static get styles(): CSSResultArray {
return [
hassioStyle,
css`
.add {
padding: 12px 16px;
}
iron-icon {
color: var(--secondary-text-color);
margin-right: 16px;
display: inline-block;
}
paper-input {
width: calc(100% - 49px);
display: inline-block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-repositories-editor": HassioRepositoriesEditor;
}
}

View File

@ -10,7 +10,9 @@ import "../../../src/components/ha-markdown";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin";
import { navigate } from "../../../src/common/navigate";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import "../components/hassio-card-content";
const PERMIS_DESC = {
@ -59,6 +61,11 @@ const PERMIS_DESC = {
description:
"An addon can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
description:
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
},
};
class HassioAddonInfo extends EventsMixin(PolymerElement) {
@ -161,12 +168,18 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
icon="hassio:arrow-up-bold-circle"
icon-class="update"
></hassio-card-content>
<template is="dom-if" if="[[!addon.available]]">
<p>This update is no longer compatible with your system.</p>
</template>
</div>
<div class="card-actions">
<ha-call-api-button
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/update"
>Update</ha-call-api-button
disabled="[[!addon.available]]"
>
Update
</ha-call-api-button
>
<template is="dom-if" if="[[addon.changelog]]">
<mwc-button on-click="openChangelog">Changelog</mwc-button>
@ -310,6 +323,15 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.ingress]]">
<ha-label-badge
on-click="showMoreInfo"
id="ingress"
icon="hassio:cursor-default-click-outline"
label="ingress"
description=""
></ha-label-badge>
</template>
</div>
<template is="dom-if" if="[[addon.version]]">
<div class="state">
@ -371,7 +393,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</template>
<template
is="dom-if"
if="[[computeShowWebUI(addon.webui, isRunning)]]"
if="[[computeShowWebUI(addon.ingress, addon.webui, isRunning)]]"
>
<a
href="[[pathWebui(addon.webui)]]"
@ -381,6 +403,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
><mwc-button>Open web UI</mwc-button></a
>
</template>
<template
is="dom-if"
if="[[computeShowIngressUI(addon.ingress, isRunning)]]"
>
<mwc-button
tabindex="-1"
class="right"
on-click="openIngress"
>Open web UI</mwc-button>
</template>
</template>
<template is="dom-if" if="[[!addon.version]]">
<template is="dom-if" if="[[!addon.available]]">
@ -448,8 +480,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
return webui && webui.replace("[HOST]", document.location.hostname);
}
computeShowWebUI(webui, isRunning) {
return webui && isRunning;
computeShowWebUI(ingress, webui, isRunning) {
return !ingress && webui && isRunning;
}
openIngress() {
navigate(this, `/hassio/ingress/${this.addon.slug}`);
}
computeShowIngressUI(ingress, isRunning) {
return ingress && isRunning;
}
computeStartOnBoot(state) {
@ -484,7 +524,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
showMoreInfo(e) {
const id = e.target.getAttribute("id");
this.fire("hassio-markdown-dialog", {
showHassioMarkdownDialog(this, {
title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description,
});
@ -495,7 +535,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
.then((resp) => resp, () => "Error getting changelog")
.then((content) => {
this.fire("hassio-markdown-dialog", {
showHassioMarkdownDialog(this, {
title: "Changelog",
content: content,
});

View File

@ -37,6 +37,7 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
<tr>
<th>Container</th>
<th>Host</th>
<th>Description</th>
</tr>
<template is="dom-repeat" items="[[config]]">
<tr>
@ -47,6 +48,7 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
no-label-float=""
></paper-input>
</td>
<td>[[item.description]]</td>
</tr>
</template>
</tbody>
@ -89,9 +91,11 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
if (!addon) return;
const network = addon.network || {};
const description = addon.network_description || {};
const items = Object.keys(network).map((key) => ({
container: key,
host: network[key],
description: description[key],
}));
this.config = items.sort(function(el1, el2) {
return el1.host - el2.host;

View File

@ -1,14 +1,10 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/app-route/app-route";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-menu-button";
import "../../../src/resources/ha-style";
import "../hassio-markdown-dialog";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-info";
@ -18,7 +14,7 @@ import "./hassio-addon-network";
class HassioAddonView extends PolymerElement {
static get template() {
return html`
<style include="iron-flex ha-style">
<style>
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
@ -51,35 +47,19 @@ class HassioAddonView extends PolymerElement {
}
}
</style>
<app-route
route="[[route]]"
pattern="/addon/:slug"
data="{{routeData}}"
active="{{routeMatches}}"
></app-route>
<app-header-layout has-scrolling-region="">
<app-header fixed="" slot="header">
<app-toolbar>
<ha-menu-button hassio></ha-menu-button>
<paper-icon-button
icon="hassio:arrow-left"
on-click="backTapped"
></paper-icon-button>
<div main-title="">Hass.io: add-on details</div>
</app-toolbar>
</app-header>
<hass-subpage header="Hass.io: add-on details" hassio>
<div class="content">
<hassio-addon-info
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[routeData.slug]]"
addon-slug="[[addonSlug]]"
></hassio-addon-info>
<template is="dom-if" if="[[addon.version]]">
<hassio-addon-config
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[routeData.slug]]"
addon-slug="[[addonSlug]]"
></hassio-addon-config>
<template is="dom-if" if="[[addon.audio]]">
@ -93,50 +73,38 @@ class HassioAddonView extends PolymerElement {
<hassio-addon-network
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[routeData.slug]]"
addon-slug="[[addonSlug]]"
></hassio-addon-network>
</template>
<hassio-addon-logs
hass="[[hass]]"
addon-slug="[[routeData.slug]]"
addon-slug="[[addonSlug]]"
></hassio-addon-logs>
</template>
</div>
</app-header-layout>
<hassio-markdown-dialog
title="[[markdownTitle]]"
content="[[markdownContent]]"
></hassio-markdown-dialog>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
route: Object,
routeData: {
route: {
type: Object,
observer: "routeDataChanged",
},
routeMatches: Boolean,
addon: Object,
markdownTitle: String,
markdownContent: {
addonSlug: {
type: String,
value: "",
computed: "_computeSlug(route)",
},
addon: Object,
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this.addEventListener("hassio-markdown-dialog", (ev) =>
this.openMarkdown(ev)
);
}
apiCalled(ev) {
@ -145,15 +113,15 @@ class HassioAddonView extends PolymerElement {
if (!path) return;
if (path.substr(path.lastIndexOf("/") + 1) === "uninstall") {
this.backTapped();
history.back();
} else {
this.routeDataChanged(this.routeData);
this.routeDataChanged(this.route);
}
}
routeDataChanged(routeData) {
if (!this.routeMatches || !routeData || !routeData.slug) return;
this.hass.callApi("get", `hassio/addons/${routeData.slug}/info`).then(
const addon = routeData.path.substr(1);
this.hass.callApi("get", `hassio/addons/${addon}/info`).then(
(info) => {
this.addon = info.data;
},
@ -163,16 +131,8 @@ class HassioAddonView extends PolymerElement {
);
}
backTapped() {
history.back();
}
openMarkdown(ev) {
this.setProperties({
markdownTitle: ev.detail.title,
markdownContent: ev.detail.content,
});
this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog();
_computeSlug(route) {
return route.path.substr(1);
}
}

View File

@ -1,90 +0,0 @@
import "@polymer/iron-icon/iron-icon";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-relative-time";
class HassioCardContent extends PolymerElement {
static get template() {
return html`
<style>
iron-icon {
margin-right: 16px;
margin-top: 16px;
float: left;
color: var(--secondary-text-color);
}
iron-icon.update {
color: var(--paper-orange-400);
}
iron-icon.running,
iron-icon.installed {
color: var(--paper-green-400);
}
iron-icon.hassupdate,
iron-icon.snapshot {
color: var(--paper-item-icon-color);
}
iron-icon.not_available {
color: var(--google-red-500);
}
.title {
color: var(--primary-text-color);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.addition {
color: var(--secondary-text-color);
overflow: hidden;
position: relative;
height: 2.4em;
line-height: 1.2em;
}
ha-relative-time {
display: block;
}
</style>
<iron-icon
icon="[[icon]]"
class\$="[[iconClass]]"
title="[[iconTitle]]"
></iron-icon>
<div>
<div class="title">[[title]]</div>
<div class="addition">
<template is="dom-if" if="[[description]]">
[[description]]
</template>
<template is="dom-if" if="[[!available]]">
(Not available)
</template>
<template is="dom-if" if="[[datetime]]">
<ha-relative-time
hass="[[hass]]"
class="addition"
datetime="[[datetime]]"
></ha-relative-time>
</template>
</div>
</div>
`;
}
static get properties() {
return {
hass: Object,
title: String,
description: String,
available: Boolean,
datetime: String,
icon: {
type: String,
value: "hass:help-circle",
},
iconTitle: String,
iconClass: String,
};
}
}
customElements.define("hassio-card-content", HassioCardContent);

View File

@ -0,0 +1,97 @@
import {
LitElement,
TemplateResult,
html,
CSSResult,
css,
property,
customElement,
} from "lit-element";
import "@polymer/iron-icon/iron-icon";
import "../../../src/components/ha-relative-time";
import { HomeAssistant } from "../../../src/types";
@customElement("hassio-card-content")
class HassioCardContent extends LitElement {
@property() public hass!: HomeAssistant;
@property() public title!: string;
@property() public description?: string;
@property({ type: Boolean }) public available: boolean = true;
@property() public datetime?: string;
@property() public iconTitle?: string;
@property() public iconClass?: string;
@property() public icon = "hass:help-circle";
protected render(): TemplateResult | void {
return html`
<iron-icon
class=${this.iconClass}
.icon=${this.icon}
.title=${this.iconTitle}
></iron-icon>
<div>
<div class="title">${this.title}</div>
<div class="addition">
${this.description} ${this.available ? undefined : " (Not available"}
${this.datetime
? html`
<ha-relative-time
.hass=${this.hass}
class="addition"
.datetime=${this.datetime}
></ha-relative-time>
`
: undefined}
</div>
</div>
`;
}
static get styles(): CSSResult {
return css`
iron-icon {
margin-right: 16px;
margin-top: 16px;
float: left;
color: var(--secondary-text-color);
}
iron-icon.update {
color: var(--paper-orange-400);
}
iron-icon.running,
iron-icon.installed {
color: var(--paper-green-400);
}
iron-icon.hassupdate,
iron-icon.snapshot {
color: var(--paper-item-icon-color);
}
iron-icon.not_available {
color: var(--google-red-500);
}
.title {
color: var(--primary-text-color);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.addition {
color: var(--secondary-text-color);
overflow: hidden;
position: relative;
height: 2.4em;
line-height: 1.2em;
}
ha-relative-time {
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-card-content": HassioCardContent;
}
}

View File

@ -1,38 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hassio-addons";
import "./hassio-hass-update";
import EventsMixin from "../../../src/mixins/events-mixin";
class HassioDashboard extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
margin: 0 auto;
}
</style>
<div class="content">
<hassio-hass-update
hass="[[hass]]"
hass-info="[[hassInfo]]"
></hassio-hass-update>
<hassio-addons
hass="[[hass]]"
addons="[[supervisorInfo.addons]]"
></hassio-addons>
</div>
`;
}
static get properties() {
return {
hass: Object,
supervisorInfo: Object,
hassInfo: Object,
};
}
}
customElements.define("hassio-dashboard", HassioDashboard);

View File

@ -0,0 +1,52 @@
import {
LitElement,
TemplateResult,
html,
CSSResult,
css,
property,
customElement,
} from "lit-element";
import "./hassio-addons";
import "./hassio-hass-update";
import { HomeAssistant } from "../../../src/types";
import {
HassioSupervisorInfo,
HassioHomeAssistantInfo,
} from "../../../src/data/hassio";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() public hassInfo!: HassioHomeAssistantInfo;
protected render(): TemplateResult | void {
return html`
<div class="content">
<hassio-hass-update
.hass=${this.hass}
.hassInfo=${this.hassInfo}
></hassio-hass-update>
<hassio-addons
.hass=${this.hass}
.addons=${this.supervisorInfo.addons}
></hassio-addons>
</div>
`;
}
static get styles(): CSSResult {
return css`
.content {
margin: 0 auto;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-dashboard": HassioDashboard;
}
}

View File

@ -1,18 +1,21 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/components/ha-markdown";
import "../../src/resources/ha-style";
import "../../../../src/components/ha-markdown";
import "../../../../src/resources/ha-style";
import "../../../../src/components/dialog/ha-paper-dialog";
import { customElement } from "lit-element";
import { PaperDialogElement } from "@polymer/paper-dialog";
@customElement("dialog-hassio-markdown")
class HassioMarkdownDialog extends PolymerElement {
static get template() {
return html`
<style include="ha-style-dialog">
paper-dialog {
ha-paper-dialog {
min-width: 350px;
font-size: 14px;
border-radius: 2px;
@ -31,10 +34,10 @@ class HassioMarkdownDialog extends PolymerElement {
margin: 4px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
paper-dialog {
ha-paper-dialog {
max-height: 100%;
}
paper-dialog::before {
ha-paper-dialog::before {
content: "";
position: fixed;
z-index: -1;
@ -50,7 +53,7 @@ class HassioMarkdownDialog extends PolymerElement {
}
}
</style>
<paper-dialog id="dialog" with-backdrop="">
<ha-paper-dialog id="dialog" with-backdrop="">
<app-toolbar>
<paper-icon-button
icon="hassio:close"
@ -61,7 +64,7 @@ class HassioMarkdownDialog extends PolymerElement {
<paper-dialog-scrollable>
<ha-markdown content="[[content]]"></ha-markdown>
</paper-dialog-scrollable>
</paper-dialog>
</ha-paper-dialog>
`;
}
@ -72,8 +75,14 @@ class HassioMarkdownDialog extends PolymerElement {
};
}
openDialog() {
this.$.dialog.open();
public showDialog(params) {
this.setProperties(params);
(this.$.dialog as PaperDialogElement).open();
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-markdown": HassioMarkdownDialog;
}
}
customElements.define("hassio-markdown-dialog", HassioMarkdownDialog);

View File

@ -0,0 +1,18 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
export interface HassioMarkdownDialogParams {
title: string;
content: string;
}
export const showHassioMarkdownDialog = (
element: HTMLElement,
dialogParams: HassioMarkdownDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-markdown",
dialogImport: () =>
import(/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"),
dialogParams,
});
};

View File

@ -2,20 +2,65 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { getSignedPath } from "../../../src/auth/data";
import { getSignedPath } from "../../../../src/auth/data";
import "../../../src/resources/ha-style";
import "../../../../src/resources/ha-style";
import "../../../../src/components/dialog/ha-paper-dialog";
import { customElement } from "lit-element";
import { PaperDialogElement } from "@polymer/paper-dialog";
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
import { fetchHassioSnapshotInfo } from "../../../../src/data/hassio";
const _computeFolders = (folders) => {
const list: Array<{ slug: string; name: string; checked: boolean }> = [];
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;
};
const _computeAddons = (addons) => {
return addons.map((addon) => ({
slug: addon.slug,
name: addon.name,
version: addon.version,
checked: true,
}));
};
@customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog extends PolymerElement {
// Commented out because it breaks Polymer! Kept around for when we migrate
// to Lit. Now just putting ts-ignore everywhere because we need this out.
// Sorry future developer.
// public hass!: HomeAssistant;
// protected error?: string;
// private snapshot?: any;
// private dialogParams?: HassioSnapshotDialogParams;
// private restoreHass!: boolean;
// private snapshotPassword!: string;
class HassioSnapshot extends PolymerElement {
static get template() {
return html`
<style include="ha-style-dialog">
paper-dialog {
ha-paper-dialog {
min-width: 350px;
font-size: 14px;
border-radius: 2px;
@ -29,7 +74,7 @@ class HassioSnapshot extends PolymerElement {
app-toolbar [main-title] {
margin-left: 16px;
}
paper-dialog-scrollable {
ha-paper-dialog-scrollable {
margin: 0;
}
paper-checkbox {
@ -37,7 +82,7 @@ class HassioSnapshot extends PolymerElement {
margin: 4px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
paper-dialog {
ha-paper-dialog {
max-height: 100%;
height: 100%;
}
@ -57,7 +102,7 @@ class HassioSnapshot extends PolymerElement {
color: var(--google-red-500);
}
</style>
<paper-dialog
<ha-paper-dialog
id="dialog"
with-backdrop=""
on-iron-overlay-closed="_dialogClosed"
@ -77,22 +122,18 @@ class HassioSnapshot extends PolymerElement {
<paper-checkbox checked="{{restoreHass}}">
Home Assistant [[snapshot.homeassistant]]
</paper-checkbox>
<template is="dom-if" if="[[snapshot.addons.length]]">
<template is="dom-if" if="[[_folders.length]]">
<div>Folders:</div>
<template is="dom-repeat" items="[[snapshot.folders]]">
<template is="dom-repeat" items="[[_folders]]">
<paper-checkbox checked="{{item.checked}}">
[[item.name]]
</paper-checkbox>
</template>
</template>
<template is="dom-if" if="[[snapshot.addons.length]]">
<template is="dom-if" if="[[_addons.length]]">
<div>Add-ons:</div>
<paper-dialog-scrollable>
<template
is="dom-repeat"
items="[[snapshot.addons]]"
sort="_sortAddons"
>
<template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
<paper-checkbox checked="{{item.checked}}">
[[item.name]] <span class="details">([[item.version]])</span>
</paper-checkbox>
@ -132,23 +173,17 @@ class HassioSnapshot extends PolymerElement {
>
</template>
</div>
</paper-dialog>
</ha-paper-dialog>
`;
}
static get properties() {
return {
hass: Object,
snapshotSlug: {
type: String,
notify: true,
observer: "_snapshotSlugChanged",
},
snapshotDeleted: {
type: Boolean,
notify: true,
},
dialogParams: Object,
snapshot: Object,
_folders: Object,
_addons: Object,
restoreHass: {
type: Boolean,
value: true,
@ -158,140 +193,136 @@ class HassioSnapshot extends PolymerElement {
};
}
_snapshotSlugChanged(snapshotSlug) {
if (!snapshotSlug || snapshotSlug === "update") return;
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.$.dialog.open();
},
() => {
this.snapshot = null;
}
);
public async showDialog(params: HassioSnapshotDialogParams) {
// @ts-ignore
const snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
this.setProperties({
dialogParams: params,
snapshot,
_folders: _computeFolders(snapshot.folders),
_addons: _computeAddons(snapshot.addons),
});
(this.$.dialog as PaperDialogElement).open();
}
_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) {
protected _isFullSnapshot(type) {
return type === "full";
}
_partialRestoreClicked() {
protected _partialRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
const addons = this.snapshot.addons
// @ts-ignore
const addons = this._addons
.filter((addon) => addon.checked)
.map((addon) => addon.slug);
const folders = this.snapshot.folders
// @ts-ignore
const folders = this._folders
.filter((folder) => folder.checked)
.map((folder) => folder.slug);
const data = {
// @ts-ignore
homeassistant: this.restoreHass,
addons: addons,
folders: folders,
addons,
folders,
};
if (this.snapshot.protected) data.password = this.snapshotPassword;
// @ts-ignore
if (this.snapshot.protected) {
// @ts-ignore
data.password = this.snapshotPassword;
}
// @ts-ignore
this.hass
.callApi(
"post",
`hassio/snapshots/${this.snapshotSlug}/restore/partial`,
"POST",
// @ts-ignore
`hassio/snapshots/${this.dialogParams!.slug}/restore/partial`,
data
)
.then(
() => {
alert("Snapshot restored!");
this.$.dialog.close();
(this.$.dialog as PaperDialogElement).close();
},
(error) => {
// @ts-ignore
this.error = error.body.message;
}
);
}
_fullRestoreClicked() {
protected _fullRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
// @ts-ignore
const data = this.snapshot.protected
? { password: this.snapshotPassword }
: null;
? {
password:
// @ts-ignore
this.snapshotPassword,
}
: undefined;
// @ts-ignore
this.hass
.callApi(
"post",
`hassio/snapshots/${this.snapshotSlug}/restore/full`,
"POST",
// @ts-ignore
`hassio/snapshots/${this.dialogParams!.slug}/restore/full`,
data
)
.then(
() => {
alert("Snapshot restored!");
this.$.dialog.close();
(this.$.dialog as PaperDialogElement).close();
},
(error) => {
// @ts-ignore
this.error = error.body.message;
}
);
}
_deleteClicked() {
protected _deleteClicked() {
if (!confirm("Are you sure you want to delete this snapshot?")) {
return;
}
// @ts-ignore
this.hass
.callApi("post", `hassio/snapshots/${this.snapshotSlug}/remove`)
// @ts-ignore
.callApi("POST", `hassio/snapshots/${this.dialogParams!.slug}/remove`)
.then(
() => {
this.$.dialog.close();
this.snapshotDeleted = true;
(this.$.dialog as PaperDialogElement).close();
// @ts-ignore
this.dialogParams!.onDelete();
},
(error) => {
// @ts-ignore
this.error = error.body.message;
}
);
}
async _downloadClicked() {
protected async _downloadClicked() {
let signedPath;
try {
signedPath = await getSignedPath(
// @ts-ignore
this.hass,
`/api/hassio/snapshots/${this.snapshotSlug}/download`
// @ts-ignore
`/api/hassio/snapshots/${this.dialogParams!.slug}/download`
);
} catch (err) {
alert(`Error: ${err.message}`);
return;
}
// @ts-ignore
const name = this._computeName(this.snapshot).replace(/[^a-z0-9]+/gi, "_");
const a = document.createElement("A");
const a = document.createElement("a");
a.href = signedPath.path;
a.download = `Hass_io_${name}.tar`;
this.$.dialog.appendChild(a);
@ -299,23 +330,23 @@ class HassioSnapshot extends PolymerElement {
this.$.dialog.removeChild(a);
}
_computeName(snapshot) {
return snapshot.name || snapshot.slug;
protected _computeName(snapshot) {
return snapshot ? snapshot.name || snapshot.slug : "Unnamed snapshot";
}
_computeType(type) {
protected _computeType(type) {
return type === "full" ? "Full snapshot" : "Partial snapshot";
}
_computeSize(size) {
protected _computeSize(size) {
return Math.ceil(size * 10) / 10 + " MB";
}
_sortAddons(a, b) {
protected _sortAddons(a, b) {
return a.name < b.name ? -1 : 1;
}
_formatDatetime(datetime) {
protected _formatDatetime(datetime) {
return new Date(datetime).toLocaleDateString(navigator.language, {
weekday: "long",
year: "numeric",
@ -326,8 +357,18 @@ class HassioSnapshot extends PolymerElement {
});
}
_dialogClosed() {
this.snapshotSlug = null;
protected _dialogClosed() {
this.setProperties({
dialogParams: undefined,
snapshot: undefined,
_addons: [],
_folders: [],
});
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-snapshot": HassioSnapshotDialog;
}
}
customElements.define("hassio-snapshot", HassioSnapshot);

View File

@ -0,0 +1,18 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
export interface HassioSnapshotDialogParams {
slug: string;
onDelete: () => void;
}
export const showHassioSnapshotDialog = (
element: HTMLElement,
dialogParams: HassioSnapshotDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-snapshot",
dialogImport: () =>
import(/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"),
dialogParams,
});
};

View File

@ -1,5 +1,17 @@
window.loadES5Adapter().then(() => {
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons.js");
import(/* webpackChunkName: "hassio-main" */ "./hassio-main.js");
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons");
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
});
document.body.style.height = "100%";
const styleEl = document.createElement("style");
styleEl.innerHTML = `
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
}
`;
document.head.appendChild(styleEl);

View File

@ -1,59 +0,0 @@
import { PolymerElement } from "@polymer/polymer/polymer-element";
class HassioData extends PolymerElement {
static get properties() {
return {
hass: Object,
supervisor: {
type: Object,
notify: true,
},
host: {
type: Object,
notify: true,
},
homeassistant: {
type: Object,
notify: true,
},
};
}
connectedCallback() {
super.connectedCallback();
this.refresh();
}
refresh() {
return Promise.all([
this.fetchSupervisorInfo(),
this.fetchHostInfo(),
this.fetchHassInfo(),
]);
}
fetchSupervisorInfo() {
return this.hass.callApi("get", "hassio/supervisor/info").then((info) => {
this.supervisor = info.data;
});
}
fetchHostInfo() {
return this.hass.callApi("get", "hassio/host/info").then((info) => {
this.host = info.data;
});
}
fetchHassInfo() {
return this.hass
.callApi("get", "hassio/homeassistant/info")
.then((info) => {
this.homeassistant = info.data;
});
}
}
customElements.define("hassio-data", HassioData);

View File

@ -1,142 +0,0 @@
import "@polymer/app-route/app-route";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/layouts/hass-loading-screen";
import "./addon-view/hassio-addon-view";
import "./hassio-data";
import "./hassio-pages-with-tabs";
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
import EventsMixin from "../../src/mixins/events-mixin";
import NavigateMixin from "../../src/mixins/navigate-mixin";
import { fireEvent } from "../../src/common/dom/fire_event";
class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
static get template() {
return html`
<app-route
route="[[route]]"
pattern="/:page"
data="{{routeData}}"
></app-route>
<hassio-data
id="data"
hass="[[hass]]"
supervisor="{{supervisorInfo}}"
homeassistant="{{hassInfo}}"
host="{{hostInfo}}"
></hassio-data>
<template is="dom-if" if="[[!loaded]]">
<hass-loading-screen></hass-loading-screen>
</template>
<template is="dom-if" if="[[loaded]]">
<template is="dom-if" if="[[!equalsAddon(routeData.page)]]">
<hassio-pages-with-tabs
hass="[[hass]]"
page="[[routeData.page]]"
supervisor-info="[[supervisorInfo]]"
hass-info="[[hassInfo]]"
host-info="[[hostInfo]]"
></hassio-pages-with-tabs>
</template>
<template is="dom-if" if="[[equalsAddon(routeData.page)]]">
<hassio-addon-view
hass="[[hass]]"
route="[[route]]"
></hassio-addon-view>
</template>
</template>
`;
}
static get properties() {
return {
hass: Object,
route: {
type: Object,
// Fake route object
value: {
prefix: "/hassio",
path: "/dashboard",
__queryParams: {},
},
observer: "routeChanged",
},
routeData: Object,
supervisorInfo: Object,
hostInfo: Object,
hassInfo: Object,
loaded: {
type: Boolean,
computed: "computeIsLoaded(supervisorInfo, hostInfo, hassInfo)",
},
};
}
ready() {
super.ready();
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
// Paulus - March 17, 2019
// We went to a single hass-toggle-menu event in HA 0.90. However, the
// supervisor UI can also run under older versions of Home Assistant.
// So here we are going to translate toggle events into the appropriate
// open and close events. These events are a no-op in newer versions of
// Home Assistant.
this.addEventListener("hass-toggle-menu", () => {
fireEvent(
window.parent.customPanel,
this.hass.dockedSidebar ? "hass-close-menu" : "hass-open-menu"
);
});
// Paulus - March 19, 2019
// We changed the navigate event to fire directly on the window, as that's
// where we are listening for it. However, the older panel_custom will
// listen on this element for navigation events, so we need to forward them.
window.addEventListener("location-changed", (ev) =>
fireEvent(this, ev.type, ev.detail, {
bubbles: false,
})
);
}
connectedCallback() {
super.connectedCallback();
this.routeChanged(this.route);
}
apiCalled(ev) {
if (ev.detail.success) {
let tries = 1;
const tryUpdate = () => {
this.$.data.refresh().catch(function() {
tries += 1;
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
});
};
tryUpdate();
}
}
computeIsLoaded(supervisorInfo, hostInfo, hassInfo) {
return supervisorInfo !== null && hostInfo !== null && hassInfo !== null;
}
routeChanged(route) {
if (route.path === "" && route.prefix === "/hassio") {
this.navigate("/hassio/dashboard", true);
}
fireEvent(this, "iron-resize");
}
equalsAddon(page) {
return page && page === "addon";
}
}
customElements.define("hassio-main", HassioMain);

152
hassio/src/hassio-main.ts Normal file
View File

@ -0,0 +1,152 @@
import { customElement, PropertyValues, property } from "lit-element";
import { PolymerElement } from "@polymer/polymer";
import "@polymer/paper-icon-button";
import "../../src/resources/ha-style";
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import {
HassRouterPage,
RouterOptions,
} from "../../src/layouts/hass-router-page";
import { HomeAssistant } from "../../src/types";
import {
fetchHassioSupervisorInfo,
fetchHassioHostInfo,
fetchHassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
} from "../../src/data/hassio";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-pages-with-tabs";
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
// causing an exception when added to DOM. When transpiled to ES5, this will
// break the build.
customElements.get("paper-icon-button").prototype._keyBindings = {};
@customElement("hassio-main")
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
@property() public hass!: HomeAssistant;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._fetchData(),
showLoading: true,
routes: {
dashboard: {
tag: "hassio-pages-with-tabs",
cache: true,
},
snapshots: "dashboard",
store: "dashboard",
system: "dashboard",
addon: {
tag: "hassio-addon-view",
load: () => import("./addon-view/hassio-addon-view"),
},
ingress: {
tag: "hassio-ingress-view",
load: () => import("./ingress-view/hassio-ingress-view"),
},
},
};
@property() private _supervisorInfo: HassioSupervisorInfo;
@property() private _hostInfo: HassioHostInfo;
@property() private _hassInfo: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
// Paulus - March 17, 2019
// We went to a single hass-toggle-menu event in HA 0.90. However, the
// supervisor UI can also run under older versions of Home Assistant.
// So here we are going to translate toggle events into the appropriate
// open and close events. These events are a no-op in newer versions of
// Home Assistant.
this.addEventListener("hass-toggle-menu", () => {
fireEvent(
(window.parent as any).customPanel,
// @ts-ignore
this.hass.dockedSidebar ? "hass-close-menu" : "hass-open-menu"
);
});
// Paulus - March 19, 2019
// We changed the navigate event to fire directly on the window, as that's
// where we are listening for it. However, the older panel_custom will
// listen on this element for navigation events, so we need to forward them.
window.addEventListener("location-changed", (ev) =>
// @ts-ignore
fireEvent(this, ev.type, ev.detail, {
bubbles: false,
})
);
makeDialogManager(this, document.body);
}
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const route =
el.nodeName === "HASSIO-PAGES-WITH-TABS" ? this.route : this.routeTail;
if ("setProperties" in el) {
// As long as we have Polymer pages
(el as PolymerElement).setProperties({
hass: this.hass,
supervisorInfo: this._supervisorInfo,
hostInfo: this._hostInfo,
hassInfo: this._hassInfo,
route,
});
} else {
el.hass = this.hass;
el.supervisorInfo = this._supervisorInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;
el.route = route;
}
}
private async _fetchData() {
const [supervisorInfo, hostInfo, hassInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
]);
this._supervisorInfo = supervisorInfo;
this._hostInfo = hostInfo;
this._hassInfo = hassInfo;
}
private _apiCalled(ev) {
if (!ev.detail.success) {
return;
}
let tries = 1;
const tryUpdate = () => {
this._fetchData().catch(() => {
tries += 1;
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
});
};
tryUpdate();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-main": HassioMain;
}
}

View File

@ -1,161 +0,0 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/components/ha-menu-button";
import "../../src/resources/ha-style";
import "./addon-store/hassio-addon-store";
import "./dashboard/hassio-dashboard";
import "./hassio-markdown-dialog";
import "./snapshots/hassio-snapshot";
import "./snapshots/hassio-snapshots";
import "./system/hassio-system";
import scrollToTarget from "../../src/common/dom/scroll-to-target";
import NavigateMixin from "../../src/mixins/navigate-mixin";
class HassioPagesWithTabs extends NavigateMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex iron-positioning ha-style">
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
paper-tabs {
margin-left: 12px;
--paper-tabs-selection-bar-color: #fff;
text-transform: uppercase;
}
</style>
<app-header-layout id="layout" has-scrolling-region>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button hassio></ha-menu-button>
<div main-title>Hass.io</div>
<template is="dom-if" if="[[showRefreshButton(page)]]">
<paper-icon-button
icon="hassio:refresh"
on-click="refreshClicked"
></paper-icon-button>
</template>
</app-toolbar>
<paper-tabs
scrollable=""
selected="[[page]]"
attr-for-selected="page-name"
on-iron-activate="handlePageSelected"
>
<paper-tab page-name="dashboard">Dashboard</paper-tab>
<paper-tab page-name="snapshots">Snapshots</paper-tab>
<paper-tab page-name="store">Add-on store</paper-tab>
<paper-tab page-name="system">System</paper-tab>
</paper-tabs>
</app-header>
<template is="dom-if" if='[[equals(page, "dashboard")]]'>
<hassio-dashboard
hass="[[hass]]"
supervisor-info="[[supervisorInfo]]"
hass-info="[[hassInfo]]"
></hassio-dashboard>
</template>
<template is="dom-if" if='[[equals(page, "snapshots")]]'>
<hassio-snapshots
hass="[[hass]]"
installed-addons="[[supervisorInfo.addons]]"
snapshot-slug="{{snapshotSlug}}"
snapshot-deleted="{{snapshotDeleted}}"
></hassio-snapshots>
</template>
<template is="dom-if" if='[[equals(page, "store")]]'>
<hassio-addon-store hass="[[hass]]"></hassio-addon-store>
</template>
<template is="dom-if" if='[[equals(page, "system")]]'>
<hassio-system
hass="[[hass]]"
supervisor-info="[[supervisorInfo]]"
host-info="[[hostInfo]]"
></hassio-system>
</template>
</app-header-layout>
<hassio-markdown-dialog
title="[[markdownTitle]]"
content="[[markdownContent]]"
></hassio-markdown-dialog>
<template is="dom-if" if='[[equals(page, "snapshots")]]'>
<hassio-snapshot
hass="[[hass]]"
snapshot-slug="{{snapshotSlug}}"
snapshot-deleted="{{snapshotDeleted}}"
></hassio-snapshot>
</template>
`;
}
static get properties() {
return {
hass: Object,
page: String,
supervisorInfo: Object,
hostInfo: Object,
hassInfo: Object,
snapshotSlug: String,
snapshotDeleted: Boolean,
markdownTitle: String,
markdownContent: {
type: String,
value: "",
},
};
}
ready() {
super.ready();
this.addEventListener("hassio-markdown-dialog", (ev) =>
this.openMarkdown(ev)
);
}
handlePageSelected(ev) {
const newPage = ev.detail.item.getAttribute("page-name");
if (newPage !== this.page) {
this.navigate(`/hassio/${newPage}`);
}
scrollToTarget(this, this.$.layout.header.scrollTarget);
}
equals(a, b) {
return a === b;
}
showRefreshButton(page) {
return page === "store" || page === "snapshots";
}
refreshClicked() {
if (this.page === "snapshots") {
this.shadowRoot.querySelector("hassio-snapshots").refreshData();
} else {
this.shadowRoot.querySelector("hassio-addon-store").refreshData();
}
}
openMarkdown(ev) {
this.setProperties({
markdownTitle: ev.detail.title,
markdownContent: ev.detail.content,
});
this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog();
}
}
customElements.define("hassio-pages-with-tabs", HassioPagesWithTabs);

View File

@ -0,0 +1,131 @@
import {
LitElement,
TemplateResult,
html,
CSSResultArray,
css,
customElement,
property,
} from "lit-element";
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import "../../src/components/ha-menu-button";
import "../../src/resources/ha-style";
import "./hassio-tabs-router";
import scrollToTarget from "../../src/common/dom/scroll-to-target";
import { haStyle } from "../../src/resources/styles";
import { HomeAssistant, Route } from "../../src/types";
import { navigate } from "../../src/common/navigate";
import {
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
} from "../../src/data/hassio";
const HAS_REFRESH_BUTTON = ["store", "snapshots"];
@customElement("hassio-pages-with-tabs")
class HassioPagesWithTabs extends LitElement {
@property() public hass!: HomeAssistant;
@property() public route!: Route;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() public hostInfo!: HassioHostInfo;
@property() public hassInfo!: HassioHomeAssistantInfo;
protected render(): TemplateResult | void {
const page = this._page;
return html`
<app-header-layout has-scrolling-region>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button hassio></ha-menu-button>
<div main-title>Hass.io</div>
${HAS_REFRESH_BUTTON.includes(page)
? html`
<paper-icon-button
icon="hassio:refresh"
@click=${this.refreshClicked}
></paper-icon-button>
`
: undefined}
</app-toolbar>
<paper-tabs
scrollable
attr-for-selected="page-name"
.selected=${page}
@iron-activate=${this.handlePageSelected}
>
<paper-tab page-name="dashboard">Dashboard</paper-tab>
<paper-tab page-name="snapshots">Snapshots</paper-tab>
<paper-tab page-name="store">Add-on store</paper-tab>
<paper-tab page-name="system">System</paper-tab>
</paper-tabs>
</app-header>
<hassio-tabs-router
.route=${this.route}
.hass=${this.hass}
.supervisorInfo=${this.supervisorInfo}
.hostInfo=${this.hostInfo}
.hassInfo=${this.hassInfo}
></hassio-tabs-router>
</app-header-layout>
`;
}
private handlePageSelected(ev) {
const newPage = ev.detail.item.getAttribute("page-name");
if (newPage !== this._page) {
navigate(this, `/hassio/${newPage}`);
}
scrollToTarget(
this,
// @ts-ignore
this.shadowRoot!.querySelector("app-header-layout").header.scrollTarget
);
}
private refreshClicked() {
if (this._page === "snapshots") {
// @ts-ignore
this.shadowRoot.querySelector("hassio-snapshots").refreshData();
} else {
// @ts-ignore
this.shadowRoot.querySelector("hassio-addon-store").refreshData();
}
}
private get _page() {
return this.route.path.substr(1);
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
paper-tabs {
margin-left: 12px;
--paper-tabs-selection-bar-color: #fff;
text-transform: uppercase;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-pages-with-tabs": HassioPagesWithTabs;
}
}

View File

@ -0,0 +1,66 @@
import {
HassRouterPage,
RouterOptions,
} from "../../src/layouts/hass-router-page";
import { customElement, property } from "lit-element";
import { PolymerElement } from "@polymer/polymer";
import { HomeAssistant } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./dashboard/hassio-dashboard";
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
import "./snapshots/hassio-snapshots";
import "./addon-store/hassio-addon-store";
import "./system/hassio-system";
import {
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
} from "../../src/data/hassio";
@customElement("hassio-tabs-router")
class HassioTabsRouter extends HassRouterPage {
@property() public hass!: HomeAssistant;
@property() public supervisorInfo: HassioSupervisorInfo;
@property() public hostInfo: HassioHostInfo;
@property() public hassInfo: HassioHomeAssistantInfo;
protected routerOptions: RouterOptions = {
routes: {
dashboard: {
tag: "hassio-dashboard",
},
snapshots: {
tag: "hassio-snapshots",
},
store: {
tag: "hassio-addon-store",
},
system: {
tag: "hassio-system",
},
},
};
protected updatePageEl(el) {
if ("setProperties" in el) {
// As long as we have Polymer pages
(el as PolymerElement).setProperties({
hass: this.hass,
supervisorInfo: this.supervisorInfo,
hostInfo: this.hostInfo,
hassInfo: this.hassInfo,
});
} else {
el.hass = this.hass;
el.supervisorInfo = this.supervisorInfo;
el.hostInfo = this.hostInfo;
el.hassInfo = this.hassInfo;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-tabs-router": HassioTabsRouter;
}
}

View File

@ -0,0 +1,100 @@
import {
LitElement,
customElement,
property,
TemplateResult,
html,
PropertyValues,
CSSResult,
css,
} from "lit-element";
import { HomeAssistant, Route } from "../../../src/types";
import {
createHassioSession,
HassioAddonDetails,
fetchHassioAddonInfo,
} from "../../../src/data/hassio";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
@customElement("hassio-ingress-view")
class HassioIngressView extends LitElement {
@property() public hass!: HomeAssistant;
@property() public route!: Route;
@property() private _addon?: HassioAddonDetails;
protected render(): TemplateResult | void {
if (!this._addon) {
return html`
<hass-loading-screen></hass-loading-screen>
`;
}
return html`
<hass-subpage .header=${this._addon.name} hassio>
<iframe src=${this._addon.ingress_url}></iframe>
</hass-subpage>
`;
}
protected updated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (!changedProps.has("route")) {
return;
}
const addon = this.route.path.substr(1);
const oldRoute = changedProps.get("route") as this["route"] | undefined;
const oldAddon = oldRoute ? oldRoute.path.substr(1) : undefined;
if (addon && addon !== oldAddon) {
this._fetchData(addon);
}
}
private async _fetchData(addonSlug: string) {
try {
const [addon] = await Promise.all([
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
throw new Error("Failed to fetch add-on info");
}),
createHassioSession(this.hass).catch(() => {
throw new Error("Failed to create an ingress session");
}),
]);
if (!addon.ingress) {
throw new Error("This add-on does not support ingress");
}
this._addon = addon;
} catch (err) {
// tslint:disable-next-line
console.error(err);
alert(err.message || "Unknown error starting ingress.");
history.back();
}
}
static get styles(): CSSResult {
return css`
iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
}
paper-icon-button {
color: var(--text-primary-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-ingress-view": HassioIngressView;
}
}

View File

@ -1,56 +1,64 @@
import { css } from "lit-element";
const documentContainer = document.createElement("template");
documentContainer.setAttribute("style", "display: none;");
export const hassioStyle = css`
.card-group {
margin-top: 24px;
}
.card-group .title {
color: var(--primary-text-color);
font-size: 2em;
padding-left: 8px;
margin-bottom: 8px;
}
.card-group .description {
font-size: 0.5em;
font-weight: 500;
margin-top: 4px;
}
.card-group paper-card {
--card-group-columns: 4;
width: calc(
(100% - 12px * var(--card-group-columns)) / var(--card-group-columns)
);
margin: 4px;
vertical-align: top;
}
@media screen and (max-width: 1200px) and (min-width: 901px) {
.card-group paper-card {
--card-group-columns: 3;
}
}
@media screen and (max-width: 900px) and (min-width: 601px) {
.card-group paper-card {
--card-group-columns: 2;
}
}
@media screen and (max-width: 600px) and (min-width: 0) {
.card-group paper-card {
width: 100%;
margin: 4px 0;
}
.content {
padding: 0;
}
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.error {
color: var(--google-red-500);
margin-top: 16px;
}
`;
documentContainer.innerHTML = `<dom-module id="hassio-style">
<template>
<style>
.card-group {
margin-top: 24px;
}
.card-group .title {
color: var(--primary-text-color);
font-size: 2em;
padding-left: 8px;
margin-bottom: 8px;
}
.card-group .description {
font-size: 0.5em;
font-weight: 500;
margin-top: 4px;
}
.card-group paper-card {
--card-group-columns: 4;
width: calc((100% - 12px * var(--card-group-columns)) / var(--card-group-columns));
margin: 4px;
vertical-align: top;
}
@media screen and (max-width: 1200px) and (min-width: 901px) {
.card-group paper-card {
--card-group-columns: 3;
}
}
@media screen and (max-width: 900px) and (min-width: 601px) {
.card-group paper-card {
--card-group-columns: 2;
}
}
@media screen and (max-width: 600px) and (min-width: 0) {
.card-group paper-card {
width: 100%;
margin: 4px 0;
}
.content {
padding: 0;
}
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.error {
color: var(--google-red-500);
margin-top: 16px;
}
${hassioStyle.toString()}
</style>
</template>
</dom-module>`;

View File

@ -1,311 +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";
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: [],
},
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 },
],
},
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) {
this.snapshotSlug = ev.model.snapshot.slug;
}
_fullSelected(type) {
return type === "full";
}
_snapshotDeletedChanged(snapshotDeleted) {
if (snapshotDeleted) {
this._updateSnapshots();
this.snapshotDeleted = false;
}
}
refreshData() {
this.hass.callApi("post", "hassio/snapshots/reload").then(() => {
this._updateSnapshots();
});
}
}
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

@ -6,10 +6,12 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import EventsMixin from "../../../src/mixins/events-mixin";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
class HassioHostInfo extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
<style>
paper-card {
display: inline-block;
width: 400px;
@ -173,7 +175,7 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
() => "Error getting hardware info"
)
.then((content) => {
this.fire("hassio-markdown-dialog", {
showHassioMarkdownDialog(this, {
title: "Hardware",
content: content,
});

View File

@ -9,7 +9,7 @@ import EventsMixin from "../../../src/mixins/events-mixin";
class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
<style>
paper-card {
display: inline-block;
width: 400px;

View File

@ -1,4 +1,3 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -9,7 +8,7 @@ import "./hassio-supervisor-log";
class HassioSystem extends PolymerElement {
static get template() {
return html`
<style include="iron-flex ha-style">
<style>
.content {
margin: 4px;
color: var(--primary-text-color);

View File

@ -20,7 +20,7 @@
"@material/mwc-button": "^0.5.0",
"@material/mwc-ripple": "^0.5.0",
"@mdi/svg": "3.5.95",
"@polymer/app-layout": "^3.0.1",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-localize-behavior": "^3.0.1",
"@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2",
@ -39,14 +39,14 @@
"@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/neon-animation": "^3.0.1",
"@polymer/paper-card": "^3.0.1",
"@polymer/paper-checkbox": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0",
"@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1",
"@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-drawer-panel": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.0.1",
"@polymer/paper-fab": "^3.0.1",
"@polymer/paper-icon-button": "^3.0.1",
"@polymer/paper-icon-button": "^3.0.2",
"@polymer/paper-input": "^3.0.1",
"@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1",
@ -57,119 +57,114 @@
"@polymer/paper-ripple": "^3.0.1",
"@polymer/paper-scroll-header-panel": "^3.0.1",
"@polymer/paper-slider": "^3.0.1",
"@polymer/paper-spinner": "^3.0.1",
"@polymer/paper-spinner": "^3.0.2",
"@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.0.1",
"@polymer/paper-toast": "^3.0.1",
"@polymer/paper-toggle-button": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "^3.0.5",
"@vaadin/vaadin-combo-box": "^4.2.0",
"@vaadin/vaadin-date-picker": "^3.3.1",
"@polymer/polymer": "^3.2.0",
"@vaadin/vaadin-combo-box": "^4.2.8",
"@vaadin/vaadin-date-picker": "^3.3.3",
"@webcomponents/shadycss": "^1.9.0",
"@webcomponents/webcomponentsjs": "^2.2.6",
"chart.js": "~2.7.2",
"chartjs-chart-timeline": "^0.2.1",
"codemirror": "^5.43.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.45.0",
"deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0",
"fecha": "^3.0.0",
"hls.js": "^0.12.3",
"fecha": "^3.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^3.4.0",
"intl-messageformat": "^2.2.0",
"jquery": "^3.3.1",
"js-yaml": "^3.12.0",
"leaflet": "^1.3.4",
"js-yaml": "^3.13.0",
"leaflet": "^1.4.0",
"lit-element": "^2.1.0",
"lit-html": "^1.0.0",
"marked": "^0.6.0",
"mdn-polyfills": "^5.12.0",
"memoize-one": "^5.0.0",
"moment": "^2.22.2",
"preact": "^8.3.1",
"marked": "^0.6.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"moment": "^2.24.0",
"preact": "^8.4.2",
"preact-compat": "^3.18.4",
"react-big-calendar": "^0.19.2",
"regenerator-runtime": "^0.12.1",
"round-slider": "^1.3.2",
"superstruct": "^0.6.0",
"unfetch": "^4.0.1",
"react-big-calendar": "^0.20.4",
"regenerator-runtime": "^0.13.2",
"round-slider": "^1.3.3",
"superstruct": "^0.6.1",
"unfetch": "^4.1.0",
"web-animations-js": "^2.3.1",
"xss": "^1.0.3"
"xss": "^1.0.6"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/plugin-external-helpers": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.3.0",
"@babel/plugin-proposal-decorators": "^7.3.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-react-jsx": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-typescript": "^7.1.0",
"@gfx/zopfli": "^1.0.9",
"@babel/core": "^7.4.0",
"@babel/plugin-external-helpers": "^7.2.0",
"@babel/plugin-proposal-class-properties": "^7.4.0",
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-react-jsx": "^7.3.0",
"@babel/preset-env": "^7.4.2",
"@babel/preset-typescript": "^7.3.3",
"@gfx/zopfli": "^1.0.11",
"@types/chai": "^4.1.7",
"@types/codemirror": "^0.0.71",
"@types/hls.js": "^0.12.2",
"@types/hls.js": "^0.12.3",
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "^4.1.0",
"@types/mocha": "^5.2.5",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"babel-eslint": "^10",
"babel-loader": "^8.0.4",
"babel-loader": "^8.0.5",
"chai": "^4.2.0",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.5.2",
"del": "^3.0.0",
"eslint": "^5.6.0",
"copy-webpack-plugin": "^5.0.2",
"del": "^4.0.0",
"eslint": "^5.15.3",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^4.0.0",
"eslint-import-resolver-webpack": "^0.10.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-prettier": "^3.0.0",
"eslint-plugin-react": "^7.11.1",
"gulp": "^3.9.1",
"eslint-config-prettier": "^4.1.0",
"eslint-import-resolver-webpack": "^0.11.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4",
"gulp": "^4.0.0",
"gulp-foreach": "^0.1.0",
"gulp-hash": "^4.2.2",
"gulp-hash-filename": "^2.0.1",
"gulp-insert": "^0.5.0",
"gulp-json-transform": "^0.4.5",
"gulp-json-transform": "^0.4.6",
"gulp-jsonminify": "^1.1.0",
"gulp-merge-json": "^1.3.1",
"gulp-rename": "^1.4.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.1.0",
"lint-staged": "^8.0.2",
"husky": "^1.3.1",
"lint-staged": "^8.1.5",
"merge-stream": "^1.0.1",
"mocha": "^5.2.0",
"mocha": "^6.0.2",
"parse5": "^5.1.0",
"polymer-cli": "^1.8.0",
"prettier": "^1.14.3",
"raw-loader": "^0.5.1",
"polymer-cli": "^1.9.7",
"prettier": "^1.16.4",
"raw-loader": "^2.0.0",
"reify": "^0.18.1",
"require-dir": "^1.0.0",
"sinon": "^7.1.1",
"require-dir": "^1.2.0",
"sinon": "^7.3.1",
"terser-webpack-plugin": "^1.2.3",
"ts-mocha": "^2.0.0",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",
"ts-mocha": "^6.0.0",
"tslint": "^5.14.0",
"tslint-config-prettier": "^1.18.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.1.4",
"uglifyjs-webpack-plugin": "^2.1.1",
"wct-browser-legacy": "^1.0.1",
"web-component-tester": "^6.8.0",
"webpack": "^4.19.1",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.8",
"workbox-webpack-plugin": "^3.5.0"
"typescript": "^3.4.1",
"uglifyjs-webpack-plugin": "^2.1.2",
"wct-browser-legacy": "^1.0.2",
"web-component-tester": "^6.9.2",
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.2.1",
"workbox-webpack-plugin": "^4.1.1"
},
"resolutions": {
"@polymer/polymer": "3.1.0",
"@webcomponents/webcomponentsjs": "^2.2.6",
"@webcomponents/shadycss": "^1.9.0",
"@vaadin/vaadin-overlay": "3.2.2",
"@vaadin/vaadin-lumo-styles": "1.3.0",
"@polymer/iron-overlay-behavior": "^3.0.2"
"@webcomponents/webcomponentsjs": "^2.2.7",
"@vaadin/vaadin-lumo-styles": "^1.4.2"
},
"main": "src/home-assistant.js",
"husky": {

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20190331.0",
version="20190410.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@ -8,6 +8,8 @@ const fixedDeviceClassIcons = {
illuminance: "hass:brightness-5",
temperature: "hass:thermometer",
pressure: "hass:gauge",
power: "hass:flash",
signal_strength: "hass:wifi",
};
export default function sensorIcon(state: HassEntity) {

View File

@ -10,11 +10,11 @@ export const timeCachePromiseFunc = async <T>(
func: (
hass: HomeAssistant,
entityId: string,
...args: Array<unknown>
...args: unknown[]
) => Promise<T>,
hass: HomeAssistant,
entityId: string,
...args: Array<unknown>
...args: unknown[]
): Promise<T> => {
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];

View File

@ -1,6 +1,8 @@
import "@polymer/paper-dialog/paper-dialog";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import { HaIronFocusablesHelper } from "./ha-iron-focusables-helper.js";
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
const paperDialogClass = customElements.get("paper-dialog");
@ -13,10 +15,10 @@ const haTabFixBehaviorImpl = {
// paper-dialog that uses the haTabFixBehaviorImpl behvaior
// export class HaPaperDialog extends paperDialogClass {}
export class HaPaperDialog extends mixinBehaviors(
[haTabFixBehaviorImpl],
paperDialogClass
) {}
// @ts-ignore
export class HaPaperDialog
extends mixinBehaviors([haTabFixBehaviorImpl], paperDialogClass)
implements PaperDialogElement {}
declare global {
interface HTMLElementTagNameMap {

View File

@ -0,0 +1,28 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import { Constructor } from "lit-element";
import { PolymerElement } from "@polymer/polymer";
const paperDropdownClass = customElements.get(
"paper-dropdown-menu"
) as Constructor<PolymerElement>;
// patches paper drop down to properly support RTL - https://github.com/PolymerElements/paper-dropdown-menu/issues/183
export class HaPaperDropdownClass extends paperDropdownClass {
public ready() {
super.ready();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
if (window.getComputedStyle(this).direction === "rtl") {
this.style.textAlign = "right";
}
}, 100);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-paper-dropdown-menu": HaPaperDropdownClass;
}
}
customElements.define("ha-paper-dropdown-menu", HaPaperDropdownClass);

View File

@ -9,10 +9,16 @@ const paperIconButtonClass = customElements.get(
) as Constructor<PaperIconButtonElement>;
export class HaPaperIconButtonArrowPrev extends paperIconButtonClass {
public hassio?: boolean;
public connectedCallback() {
this.icon =
window.getComputedStyle(this).direction === "ltr"
? "hass:arrow-left"
? this.hassio
? "hassio:arrow-left"
: "hass:arrow-left"
: this.hassio
? "hassio:arrow-right"
: "hass:arrow-right";
// calling super after setting icon to have it consistently show the icon (otherwise not always shown)

217
src/data/hassio.ts Normal file
View File

@ -0,0 +1,217 @@
import { HomeAssistant } from "../types";
interface HassioResponse<T> {
data: T;
result: "ok";
}
interface CreateSessionResponse {
session: string;
}
export interface HassioAddonInfo {
name: string;
slug: string;
description: string;
repository: "core" | "local" | string;
version: string;
installed: string | undefined;
detached: boolean;
available: boolean;
build: boolean;
url: string | null;
icon: boolean;
logo: boolean;
}
export interface HassioAddonDetails {
name: string;
slug: string;
description: string;
long_description: null | string;
auto_update: boolean;
url: null | string;
detached: boolean;
available: boolean;
arch: "armhf" | "aarch64" | "i386" | "amd64";
machine: any;
homeassistant: string;
repository: null | string;
version: null | string;
last_version: string;
state: "none" | "started" | "stopped";
boot: "auto" | "manual";
build: boolean;
options: object;
network: null | object;
host_network: boolean;
host_pid: boolean;
host_ipc: boolean;
host_dbus: boolean;
privileged: any;
apparmor: "disable" | "default" | "profile";
devices: string[];
auto_uart: boolean;
icon: boolean;
logo: boolean;
changelog: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
homeassistant_api: boolean;
auth_api: boolean;
full_access: boolean;
protected: boolean;
rating: "1-6";
stdin: boolean;
webui: null | string;
gpio: boolean;
kernel_modules: boolean;
devicetree: boolean;
docker_api: boolean;
audio: boolean;
audio_input: null | string;
audio_output: null | string;
services_role: string[];
discovery: string[];
ip_address: string;
ingress: boolean;
ingress_entry: null | string;
ingress_url: null | string;
}
export interface HassioAddonRepository {
slug: string;
name: string;
source: string;
url: string;
maintainer: string;
}
export interface HassioAddonsInfo {
addons: HassioAddonInfo[];
repositories: HassioAddonRepository[];
}
export interface HassioHassOSInfo {
version: string;
version_cli: string;
version_latest: string;
version_cli_latest: string;
board: "ova" | "rpi";
}
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 HassioSnapshotDetail extends HassioSnapshot {
size: string;
homeassistant: string;
addons: Array<{
slug: "ADDON_SLUG";
name: "NAME";
version: "INSTALLED_VERSION";
size: "SIZE_IN_MB";
}>;
repositories: string[];
folders: string[];
}
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;
export const createHassioSession = async (hass: HomeAssistant) => {
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${
response.data.session
};path=/api/hassio_ingress/`;
};
export const reloadHassioAddons = (hass: HomeAssistant) =>
hass.callApi<unknown>("POST", `hassio/addons/reload`);
export const fetchHassioAddonsInfo = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<HassioAddonsInfo>>("GET", `hassio/addons`)
.then(hassioApiResultExtractor);
export const fetchHassioAddonInfo = (hass: HomeAssistant, addon: string) =>
hass
.callApi<HassioResponse<HassioAddonDetails>>(
"GET",
`hassio/addons/${addon}/info`
)
.then(hassioApiResultExtractor);
export const fetchHassioSupervisorInfo = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<HassioSupervisorInfo>>(
"GET",
"hassio/supervisor/info"
)
.then(hassioApiResultExtractor);
export const fetchHassioHostInfo = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<HassioHostInfo>>("GET", "hassio/host/info")
.then(hassioApiResultExtractor);
export const fetchHassioHomeAssistantInfo = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<HassioHomeAssistantInfo>>(
"GET",
"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);
export const fetchHassioSnapshotInfo = (
hass: HomeAssistant,
snapshot: string
) =>
hass
.callApi<HassioResponse<HassioSnapshotDetail>>(
"GET",
`hassio/snapshots/${snapshot}/info`
)
.then(hassioApiResultExtractor);

View File

@ -16,6 +16,7 @@ export interface ZHADevice {
manufacturer_code: number;
device_reg_id: string;
user_given_name?: string;
power_source?: string;
area_id?: string;
}

View File

@ -12,14 +12,14 @@ import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-dialog/paper-dialog";
// Not duplicate, is for typing
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import "../../components/ha-form";
import "../../components/ha-markdown";
import "../../resources/ha-style";
import "../../components/dialog/ha-paper-dialog";
// Not duplicate, is for typing
// tslint:disable-next-line
import { HaPaperDialog } from "../../components/dialog/ha-paper-dialog";
import { haStyleDialog } from "../../resources/styles";
import {
fetchConfigFlow,
@ -108,7 +108,11 @@ class ConfigFlowDialog extends LitElement {
}
return html`
<paper-dialog with-backdrop opened @opened-changed=${this._openedChanged}>
<ha-paper-dialog
with-backdrop
opened
@opened-changed=${this._openedChanged}
>
${this._loading
? html`
<step-flow-loading></step-flow-loading>
@ -144,7 +148,7 @@ class ConfigFlowDialog extends LitElement {
.areas=${this._areas}
></step-flow-create-entry>
`}
</paper-dialog>
</ha-paper-dialog>
`;
}
@ -166,8 +170,8 @@ class ConfigFlowDialog extends LitElement {
}
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private async _fetchDevices(configEntryId) {
@ -226,10 +230,10 @@ class ConfigFlowDialog extends LitElement {
return [
haStyleDialog,
css`
paper-dialog {
ha-paper-dialog {
max-width: 500px;
}
paper-dialog > * {
ha-paper-dialog > * {
margin: 0;
display: block;
padding: 0;

View File

@ -33,20 +33,17 @@ class StepFlowAbort extends LitElement {
);
return html`
<h2>Aborted</h2>
<div class="content">
${
description
? html`
<ha-markdown .content=${description} allow-svg></ha-markdown>
`
: ""
}
</div>
<div class="buttons">
<mwc-button @click="${this._flowDone}">Close</mwc-button>
</div>
</paper-dialog>
<h2>Aborted</h2>
<div class="content">
${description
? html`
<ha-markdown .content=${description} allow-svg></ha-markdown>
`
: ""}
</div>
<div class="buttons">
<mwc-button @click="${this._flowDone}">Close</mwc-button>
</div>
`;
}

View File

@ -52,71 +52,61 @@ class StepFlowCreateEntry extends LitElement {
);
return html`
<h2>Success!</h2>
<div class="content">
${
description
? html`
<ha-markdown .content=${description} allow-svg></ha-markdown>
`
: ""
}
<p>Created config for ${step.title}.</p>
${
this.devices.length === 0
? ""
: html`
<p>We found the following devices:</p>
<div class="devices">
${this.devices.map(
(device) =>
html`
<div class="device">
<b>${device.name}</b><br />
${device.model} (${device.manufacturer})
<h2>Success!</h2>
<div class="content">
${description
? html`
<ha-markdown .content=${description} allow-svg></ha-markdown>
`
: ""}
<p>Created config for ${step.title}.</p>
${this.devices.length === 0
? ""
: html`
<p>We found the following devices:</p>
<div class="devices">
${this.devices.map(
(device) =>
html`
<div class="device">
<b>${device.name}</b><br />
${device.model} (${device.manufacturer})
<paper-dropdown-menu-light
label="Area"
.device=${device.id}
@selected-item-changed=${this._handleAreaChanged}
>
<paper-listbox
slot="dropdown-content"
selected="0"
>
<paper-item>
${localize(
"ui.panel.config.integrations.config_entry.no_area"
)}
<paper-dropdown-menu-light
label="Area"
.device=${device.id}
@selected-item-changed=${this._handleAreaChanged}
>
<paper-listbox slot="dropdown-content" selected="0">
<paper-item>
${localize(
"ui.panel.config.integrations.config_entry.no_area"
)}
</paper-item>
${this.areas.map(
(area) => html`
<paper-item .area=${area.area_id}>
${area.name}
</paper-item>
${this.areas.map(
(area) => html`
<paper-item .area=${area.area_id}>
${area.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
</div>
`
)}
</div>
`
}
</div>
<div class="buttons">
${
this.devices.length > 0
? html`
<mwc-button @click="${this._addArea}">Add Area</mwc-button>
`
: ""
}
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
</div>
`
)}
</div>
`}
</div>
<div class="buttons">
${this.devices.length > 0
? html`
<mwc-button @click="${this._addArea}">Add Area</mwc-button>
`
: ""}
<mwc-button @click="${this._flowDone}">Finish</mwc-button>
</div>
</paper-dialog>
<mwc-button @click="${this._flowDone}">Finish</mwc-button>
</div>
`;
}

View File

@ -170,7 +170,7 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
);
if (
!isComponentLoaded(this.hass, "config.entity_registry") ||
!isComponentLoaded(this.hass, "config") ||
(oldVal && oldVal.entity_id === newVal.entity_id)
) {
return;

View File

@ -0,0 +1,57 @@
import { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
import { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
declare global {
// for fire event
interface HASSDomEvents {
"show-dialog": ShowDialogParams<unknown>;
}
// for add event listener
interface HTMLElementEventMap {
"show-dialog": HASSDomEvent<ShowDialogParams<unknown>>;
}
}
interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]> extends HTMLElement {
showDialog(params: T);
}
interface ShowDialogParams<T> {
dialogTag: keyof HTMLElementTagNameMap;
dialogImport: () => Promise<unknown>;
dialogParams: T;
}
const LOADED = {};
export const showDialog = async (
element: HTMLElement & ProvideHassElement,
root: ShadowRoot | HTMLElement,
dialogImport: () => Promise<unknown>,
dialogTag: string,
dialogParams: unknown
) => {
if (!(dialogTag in LOADED)) {
LOADED[dialogTag] = dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag) as HassDialog;
element.provideHass(dialogEl);
root.appendChild(dialogEl);
return dialogEl;
});
}
const dialogElement = await LOADED[dialogTag];
dialogElement.showDialog(dialogParams);
};
export const makeDialogManager = (
element: HTMLElement & ProvideHassElement,
root: ShadowRoot | HTMLElement
) => {
element.addEventListener(
"show-dialog",
async (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
const { dialogTag, dialogImport, dialogParams } = e.detail;
showDialog(element, root, dialogImport, dialogTag, dialogParams);
}
);
};

View File

@ -1,5 +1,4 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
@ -10,6 +9,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-climate-control";
import "../../../components/ha-paper-slider";
import "../../../components/ha-paper-dropdown-menu";
import attributeClassNames from "../../../common/entity/attribute_class_names";
import featureClassNames from "../../../common/entity/feature_class_names";
@ -64,7 +64,7 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
margin: 22px 16px 0 0;
}
paper-dropdown-menu {
ha-paper-dropdown-menu {
width: 100%;
}
@ -193,7 +193,7 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
<template is="dom-if" if="[[supportsOperationMode(stateObj)]]">
<div class="container-operation_list">
<div class="controls">
<paper-dropdown-menu
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="[[localize('ui.card.climate.operation')]]"
@ -212,14 +212,14 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
>
</template>
</paper-listbox>
</paper-dropdown-menu>
</ha-paper-dropdown-menu>
</div>
</div>
</template>
<template is="dom-if" if="[[supportsFanMode(stateObj)]]">
<div class="container-fan_list">
<paper-dropdown-menu
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="[[localize('ui.card.climate.fan_mode')]]"
@ -233,13 +233,13 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
<paper-item>[[_localizeFanMode(localize, item)]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</ha-paper-dropdown-menu>
</div>
</template>
<template is="dom-if" if="[[supportsSwingMode(stateObj)]]">
<div class="container-swing_list">
<paper-dropdown-menu
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="[[localize('ui.card.climate.swing_mode')]]"
@ -253,7 +253,7 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
<paper-item>[[item]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</ha-paper-dropdown-menu>
</div>
</template>

View File

@ -1,5 +1,4 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@ -8,6 +7,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-attributes";
import "../../../components/ha-paper-dropdown-menu";
import attributeClassNames from "../../../common/entity/attribute_class_names";
import EventsMixin from "../../../mixins/events-mixin";
@ -33,7 +33,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
display: block;
}
paper-dropdown-menu {
ha-paper-dropdown-menu {
width: 100%;
}
@ -44,7 +44,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class$="[[computeClassNames(stateObj)]]">
<div class="container-speed_list">
<paper-dropdown-menu
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="[[localize('ui.card.fan.speed')]]"
@ -57,7 +57,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
<paper-item>[[item]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</ha-paper-dropdown-menu>
</div>
<div class="container-oscillating">

View File

@ -2,7 +2,6 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/polymer/polymer-legacy";
import "@vaadin/vaadin-date-picker/vaadin-date-picker";
import "../../../components/ha-relative-time";

View File

@ -1,5 +1,4 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
@ -8,6 +7,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-attributes";
import "../../../components/ha-color-picker";
import "../../../components/ha-labeled-slider";
import "../../../components/ha-paper-dropdown-menu";
import featureClassNames from "../../../common/entity/feature_class_names";
import EventsMixin from "../../../mixins/events-mixin";
@ -177,7 +177,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
</div>
<div class="control effect_list">
<paper-dropdown-menu
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="[[localize('ui.card.light.effect')]]"
@ -190,7 +190,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
<paper-item>[[item]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</ha-paper-dropdown-menu>
</div>
<ha-attributes

View File

@ -1,6 +1,5 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@ -8,6 +7,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-paper-slider";
import "../../../components/ha-paper-dropdown-menu";
import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
import attributeClassNames from "../../../common/entity/attribute_class_names";
@ -50,7 +50,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
margin-top: 15px;
}
paper-dropdown-menu.source-input {
ha-paper-dropdown-menu.source-input {
margin-left: 10px;
}
@ -148,7 +148,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
hidden$="[[computeHideSelectSource(playerObj)]]"
>
<iron-icon class="source-input" icon="hass:login-variant"></iron-icon>
<paper-dropdown-menu
<ha-paper-dropdown-menu
class="flex source-input"
dynamic-align=""
label-float=""
@ -159,13 +159,13 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
<paper-item>[[item]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</ha-paper-dropdown-menu>
</div>
<!-- SOUND MODE PICKER -->
<template is="dom-if" if="[[!computeHideSelectSoundMode(playerObj)]]">
<div class="controls layout horizontal justified">
<iron-icon class="source-input" icon="hass:music-note"></iron-icon>
<paper-dropdown-menu
<ha-paper-dropdown-menu
class="flex source-input"
dynamic-align
label-float
@ -180,7 +180,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
<paper-item item-name$="[[item]]">[[item]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</ha-paper-dropdown-menu>
</div>
</template>
<!-- TTS -->

View File

@ -1,6 +1,5 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@ -8,6 +7,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-attributes";
import "../../../components/ha-paper-dropdown-menu";
import { supportsFeature } from "../../../common/entity/supports-feature";
class MoreInfoVacuum extends PolymerElement {
@ -104,7 +104,7 @@ class MoreInfoVacuum extends PolymerElement {
<div hidden$="[[!supportsFanSpeed(stateObj)]]">
<div class="horizontal justified layout">
<paper-dropdown-menu
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="Fan speed"
@ -117,7 +117,7 @@ class MoreInfoVacuum extends PolymerElement {
<paper-item>[[item]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</ha-paper-dropdown-menu>
<div
style="justify-content: center; align-self: center; padding-top: 1.3em"
>

View File

@ -1,5 +1,4 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
@ -10,6 +9,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-water_heater-control";
import "../../../components/ha-paper-slider";
import "../../../components/ha-paper-dropdown-menu";
import featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature";
@ -40,7 +40,7 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
.container-operation_list iron-icon,
paper-dropdown-menu {
ha-paper-dropdown-menu {
width: 100%;
}
@ -93,7 +93,7 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
<template is="dom-if" if="[[supportsOperationMode(stateObj)]]">
<div class="container-operation_list">
<div class="controls">
<paper-dropdown-menu
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="[[localize('ui.card.water_heater.operation')]]"
@ -112,7 +112,7 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
>
</template>
</paper-listbox>
</paper-dropdown-menu>
</ha-paper-dropdown-menu>
</div>
</div>
</template>

View File

@ -10,7 +10,6 @@ import LocalizeMixin from "../../mixins/localize-mixin";
import computeStateName from "../../common/entity/compute_state_name";
import computeDomain from "../../common/entity/compute_domain";
import isComponentLoaded from "../../common/config/is_component_loaded";
import { updateEntityRegistryEntry } from "../../data/entity_registry";
import "../../components/ha-paper-icon-button-arrow-prev";
@ -74,11 +73,6 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
hass: Object,
stateObj: Object,
_componentLoaded: {
type: Boolean,
computed: "_computeComponentLoaded(hass)",
},
registryInfo: {
type: Object,
observer: "_registryInfoChanged",
@ -95,10 +89,6 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
return computeStateName(stateObj);
}
_computeComponentLoaded(hass) {
return isComponentLoaded(hass, "config.entity_registry");
}
_computeInvalid(entityId) {
return computeDomain(this.stateObj.entity_id) !== computeDomain(entityId);
}

View File

@ -6,14 +6,12 @@ import objAssign from "es6-object-assign";
objAssign.polyfill();
if (Object.values === undefined) {
Object.values = function(target) {
return Object.keys(target).map(function(key) {
return target[key];
});
Object.values = (target) => {
return Object.keys(target).map((key) => target[key]);
};
}
/* eslint-disable */
/* tslint:disable */
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
if (!String.prototype.padStart) {
@ -31,4 +29,4 @@ if (!String.prototype.padStart) {
}
};
}
/* eslint-enable */
/* tslint:enable */

View File

@ -6,13 +6,13 @@ function initRouting() {
// Cache static content (including translations) on first access.
workbox.routing.registerRoute(
new RegExp(`${location.host}/(static|frontend_latest|frontend_es5)/.+`),
workbox.strategies.cacheFirst()
new workbox.strategies.CacheFirst()
);
// Get api from network.
workbox.routing.registerRoute(
new RegExp(`${location.host}/api/.*`),
workbox.strategies.networkOnly()
new workbox.strategies.NetworkOnly()
);
// Get manifest and service worker from network.
@ -20,7 +20,7 @@ function initRouting() {
new RegExp(
`${location.host}/(service_worker.js|service_worker_es5.js|manifest.json)`
),
workbox.strategies.networkOnly()
new workbox.strategies.NetworkOnly()
);
// For rest of the files (on Home Assistant domain only) try both cache and network.
@ -29,7 +29,7 @@ function initRouting() {
// file.
workbox.routing.registerRoute(
new RegExp(`${location.host}/.*`),
workbox.strategies.staleWhileRevalidate()
new workbox.strategies.StaleWhileRevalidate()
);
}
@ -147,6 +147,12 @@ function initPushNotifications() {
});
}
self.addEventListener("install", (event) => {
// Delete all runtime caching, so that index.html has to be refetched.
const cacheName = workbox.core.cacheNames.runtime;
event.waitUntil(caches.delete(cacheName));
});
self.addEventListener("message", (message) => {
if (message.data.type === "skipWaiting") {
self.skipWaiting();

View File

@ -4,7 +4,7 @@
<title>Home Assistant</title>
<link rel='preload' href='/static/fonts/roboto/Roboto-Light.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<%= require('raw-loader!./_header.html.template') %>
<%= require('raw-loader!./_header.html.template').default %>
<style>
.content {
padding: 20px 16px;

View File

@ -4,7 +4,7 @@
<link rel='preload' href='<%= coreJS %>' as='script'/>
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Medium.ttf' as='font' crossorigin />
<%= require('raw-loader!./_header.html.template') %>
<%= require('raw-loader!./_header.html.template').default %>
<title>Home Assistant</title>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/icons/favicon-apple-180x180.png'>

View File

@ -4,7 +4,7 @@
<title>Home Assistant</title>
<link rel='preload' href='/static/fonts/roboto/Roboto-Light.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<%= require('raw-loader!./_header.html.template') %>
<%= require('raw-loader!./_header.html.template').default %>
<style>
.content {
padding: 20px 16px;

View File

@ -1,6 +1,10 @@
import { Constructor, LitElement } from "lit-element";
import { HASSDomEvent, ValidHassDomEvent } from "../../common/dom/fire_event";
import { HASSDomEvent } from "../../common/dom/fire_event";
import { HassBaseEl } from "./hass-base-mixin";
import {
makeDialogManager,
showDialog,
} from "../../dialogs/make-dialog-manager";
interface RegisterDialogParams {
dialogShowEvent: keyof HASSDomEvents;
@ -8,31 +12,17 @@ interface RegisterDialogParams {
dialogImport: () => Promise<unknown>;
}
interface ShowDialogParams<T> {
dialogTag: keyof HTMLElementTagNameMap;
dialogImport: () => Promise<unknown>;
dialogParams: T;
}
interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]> extends HTMLElement {
showDialog(params: T);
}
declare global {
// for fire event
interface HASSDomEvents {
"register-dialog": RegisterDialogParams;
"show-dialog": ShowDialogParams<unknown>;
}
// for add event listener
interface HTMLElementEventMap {
"register-dialog": HASSDomEvent<RegisterDialogParams>;
"show-dialog": HASSDomEvent<ShowDialogParams<unknown>>;
}
}
const LOADED = {};
export const dialogManagerMixin = (
superClass: Constructor<LitElement & HassBaseEl>
) =>
@ -43,13 +33,7 @@ export const dialogManagerMixin = (
this.addEventListener("register-dialog", (e) =>
this.registerDialog(e.detail)
);
this.addEventListener(
"show-dialog",
async (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
const { dialogTag, dialogImport, dialogParams } = e.detail;
this._showDialog(dialogImport, dialogTag, dialogParams);
}
);
makeDialogManager(this, this.shadowRoot!);
}
private registerDialog({
@ -58,28 +42,13 @@ export const dialogManagerMixin = (
dialogImport,
}: RegisterDialogParams) {
this.addEventListener(dialogShowEvent, (showEv) => {
this._showDialog(
showDialog(
this,
this.shadowRoot!,
dialogImport,
dialogTag,
(showEv as HASSDomEvent<unknown>).detail
);
});
}
private async _showDialog(
dialogImport: () => Promise<unknown>,
dialogTag: string,
dialogParams: unknown
) {
if (!(dialogTag in LOADED)) {
LOADED[dialogTag] = dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag) as HassDialog;
this.provideHass(dialogEl);
this.shadowRoot!.appendChild(dialogEl);
return dialogEl;
});
}
const element = await LOADED[dialogTag];
element.showDialog(dialogParams);
}
};

View File

@ -14,7 +14,7 @@ export class HassBaseEl {
protected hassDisconnected() {}
protected hassChanged(_hass: HomeAssistant, _oldHass?: HomeAssistant) {}
protected panelUrlChanged(_newPanelUrl: string) {}
protected provideHass(_el: HTMLElement) {}
public provideHass(_el: HTMLElement) {}
protected _updateHass(_obj: Partial<HomeAssistant>) {}
}
@ -48,7 +48,7 @@ export default <T>(superClass: Constructor<T>): Constructor<T & HassBaseEl> =>
});
}
protected provideHass(el) {
public provideHass(el) {
this.__provideHass.push(el);
el.hass = this.hass;
}

View File

@ -1,5 +1,6 @@
import { UpdatingElement, property, PropertyValues } from "lit-element";
import "./hass-error-screen";
import "./hass-loading-screen";
import { Route } from "../types";
import { navigate } from "../common/navigate";
import memoizeOne from "memoize-one";
@ -15,18 +16,27 @@ const extractPage = (path: string, defaultPage: string) => {
};
export interface RouteOptions {
// HTML tag of the route page.
tag: string;
load: () => Promise<unknown>;
// Function to load the page.
load?: () => Promise<unknown>;
cache?: boolean;
}
export interface RouterOptions {
// The default route to show if path does not define a page.
defaultPage?: string;
// If all routes should be preloaded
preloadAll?: boolean;
// If a route has been shown, should we keep the element in memory
cacheAll?: boolean;
// Should we show a loading spinner while we load the element for the route
showLoading?: boolean;
// Promise that resolves when the initial data is loaded which is needed to show any route.
initialLoad?: () => Promise<unknown>;
routes: {
[route: string]: RouteOptions;
// If it's a string, it is another route whose options should be adopted.
[route: string]: RouteOptions | string;
};
}
@ -38,16 +48,10 @@ export class HassRouterPage extends UpdatingElement {
protected routerOptions!: RouterOptions;
/**
* Optional variable to define extra routes dynamically.
* It is preferred to use static routes.
*/
protected extraRoutes?: {
[route: string]: RouteOptions;
};
private _currentPage = "";
private _currentLoadProm?: Promise<void>;
private _cache = {};
private _initialLoadDone = false;
private _computeTail = memoizeOne((route: Route) => {
const dividerPos = route.path.indexOf("/", 1);
return dividerPos === -1
@ -64,6 +68,12 @@ export class HassRouterPage extends UpdatingElement {
protected update(changedProps: PropertyValues) {
super.update(changedProps);
const routerOptions = this.routerOptions || { routes: {} };
if (routerOptions && routerOptions.initialLoad && !this._initialLoadDone) {
return;
}
if (!changedProps.has("route")) {
// Do not update if we have a currentLoadProm, because that means
// that there is still an old panel shown and we're moving to a new one.
@ -74,14 +84,22 @@ export class HassRouterPage extends UpdatingElement {
}
const route = this.route;
const routerOptions = this.routerOptions || { routes: {} };
const defaultPage = routerOptions.defaultPage || "";
const defaultPage = routerOptions.defaultPage;
if (route && route.path === "") {
if (route && route.path === "" && defaultPage !== undefined) {
navigate(this, `${route.prefix}/${defaultPage}`, true);
}
const newPage = route ? extractPage(route.path, defaultPage) : "not_found";
let newPage = route
? extractPage(route.path, defaultPage || "")
: "not_found";
let routeOptions = routerOptions.routes[newPage];
// Handle redirects
while (typeof routeOptions === "string") {
newPage = routeOptions;
routeOptions = routerOptions.routes[newPage];
}
if (this._currentPage === newPage) {
if (this.lastChild) {
@ -90,8 +108,6 @@ export class HassRouterPage extends UpdatingElement {
return;
}
const routeOptions = routerOptions.routes[newPage];
if (!routeOptions) {
this._currentPage = "";
if (this.lastChild) {
@ -101,10 +117,15 @@ export class HassRouterPage extends UpdatingElement {
}
this._currentPage = newPage;
const loadProm = routeOptions.load();
const loadProm = routeOptions.load
? routeOptions.load()
: Promise.resolve();
// Check when loading the page source failed.
loadProm.catch(() => {
loadProm.catch((err) => {
// tslint:disable-next-line
console.error("Error loading page", newPage, err);
// Verify that we're still trying to show the same page.
if (this._currentPage !== newPage) {
return;
@ -151,7 +172,12 @@ export class HassRouterPage extends UpdatingElement {
}
created = true;
this._createPanel(routerOptions, newPage, routeOptions);
this._createPanel(
routerOptions,
newPage,
// @ts-ignore TS forgot this is not a string.
routeOptions
);
},
() => {
this._currentLoadProm = undefined;
@ -164,10 +190,28 @@ export class HassRouterPage extends UpdatingElement {
const options = this.routerOptions;
if (options && options.preloadAll) {
Object.values(options.routes).forEach((route) => route.load());
if (!options) {
return;
}
if (options.preloadAll) {
Object.values(options.routes).forEach(
(route) => typeof route === "object" && route.load && route.load()
);
}
if (options.initialLoad) {
setTimeout(() => {
if (!this._initialLoadDone) {
this.appendChild(this.createLoadingScreen());
}
}, LOADING_SCREEN_THRESHOLD);
options.initialLoad().then(() => {
this._initialLoadDone = true;
this.requestUpdate("route");
});
}
}
protected createLoadingScreen() {

View File

@ -1,37 +1,44 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import {
LitElement,
property,
TemplateResult,
html,
customElement,
css,
CSSResult,
} from "lit-element";
import { haStyle } from "../resources/styles";
import "../components/ha-menu-button";
import "../components/ha-paper-icon-button-arrow-prev";
@customElement("hass-subpage")
class HassSubpage extends LitElement {
@property()
public header?: string;
@property({ type: Boolean })
public root = false;
@property({ type: Boolean })
public hassio = false;
protected render(): TemplateResult | void {
return html`
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-paper-icon-button-arrow-prev
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
<div main-title>${this.header}</div>
<slot name="toolbar-icon"></slot>
</app-toolbar>
</app-header>
<div class="toolbar">
${this.root
? html`
<ha-menu-button .hassio=${this.hassio}></ha-menu-button>
`
: html`
<ha-paper-icon-button-arrow-prev
.hassio=${this.hassio}
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
`}
<slot></slot>
</app-header-layout>
<div main-title>${this.header}</div>
<slot name="toolbar-icon"></slot>
</div>
<div class="content"><slot></slot></div>
`;
}
@ -40,7 +47,46 @@ class HassSubpage extends LitElement {
}
static get styles(): CSSResult {
return haStyle;
return css`
:host {
display: block;
height: 100%;
background-color: var(--primary-background-color);
}
.toolbar {
display: flex;
align-items: center;
font-size: 20px;
height: 64px;
padding: 0 16px;
pointer-events: none;
background-color: var(--primary-color);
font-weight: 400;
color: var(--text-primary-color, white);
}
ha-menu-button,
ha-paper-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
pointer-events: auto;
}
[main-title] {
margin: 0 0 0 24px;
line-height: 20px;
flex-grow: 1;
}
.content {
position: relative;
width: 100%;
height: calc(100% - 64px);
overflow-y: auto;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
`;
}
}

View File

@ -12,7 +12,6 @@ import "@polymer/app-layout/app-drawer/app-drawer";
// Not a duplicate, it's for typing
// tslint:disable-next-line
import { AppDrawerElement } from "@polymer/app-layout/app-drawer/app-drawer";
import "@polymer/app-route/app-route";
import "@polymer/iron-media-query/iron-media-query";
import "./partial-panel-resolver";

View File

@ -0,0 +1,35 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
LitElement,
TemplateResult,
html,
css,
customElement,
CSSResult,
} from "lit-element";
@customElement("loading-screen")
class LoadingScreen extends LitElement {
protected render(): TemplateResult | void {
return html`
<paper-spinner-lite active></paper-spinner-lite>
`;
}
static get styles(): CSSResult {
return css`
:host {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"loading-screen": LoadingScreen;
}
}

View File

@ -74,6 +74,8 @@ class PartialPanelResolver extends HassRouterPage {
@property() public narrow?: boolean;
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!changedProps.has("hass")) {
return;
}

View File

@ -0,0 +1,32 @@
import { UpdatingElement, Constructor, PropertyValues } from "lit-element";
import { HomeAssistant } from "../types";
export interface ProvideHassElement {
provideHass(element: HTMLElement);
}
/* tslint:disable */
export const ProvideHassLitMixin = <T extends UpdatingElement>(
superClass: Constructor<T>
): Constructor<T & ProvideHassElement> =>
// @ts-ignore
class extends superClass {
protected hass!: HomeAssistant;
private __provideHass: HTMLElement[] = [];
public provideHass(el) {
this.__provideHass.push(el);
el.hass = this.hass;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("hass")) {
this.__provideHass.forEach((el) => {
(el as any).hass = this.hass;
});
}
}
};

View File

@ -6,10 +6,10 @@ import {
CSSResult,
TemplateResult,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import "../../../components/dialog/ha-paper-dialog";
import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
@ -47,7 +47,7 @@ class DialogAreaDetail extends LitElement {
const entry = this._params.entry;
const nameInvalid = this._name.trim() === "";
return html`
<paper-dialog
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
@ -108,7 +108,7 @@ class DialogAreaDetail extends LitElement {
)}
</mwc-button>
</div>
</paper-dialog>
</ha-paper-dialog>
`;
}
@ -157,7 +157,7 @@ class DialogAreaDetail extends LitElement {
return [
haStyleDialog,
css`
paper-dialog {
ha-paper-dialog {
min-width: 400px;
}
.form {

View File

@ -9,10 +9,10 @@ import {
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dialog/paper-dialog";
import "../../../components/dialog/ha-paper-dialog";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { HaPaperDialog } from "../../../components/dialog/ha-paper-dialog";
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
@ -49,7 +49,7 @@ export class CloudWebhookManageDialog extends LitElement {
? "https://www.home-assistant.io/docs/automation/trigger/#webhook-trigger"
: `https://www.home-assistant.io/components/${webhook.domain}/`;
return html`
<paper-dialog with-backdrop>
<ha-paper-dialog with-backdrop>
<h2>Webhook for ${webhook.name}</h2>
<div>
<p>The webhook is available at the following url:</p>
@ -80,12 +80,12 @@ export class CloudWebhookManageDialog extends LitElement {
>
<mwc-button @click="${this._closeDialog}">CLOSE</mwc-button>
</div>
</paper-dialog>
</ha-paper-dialog>
`;
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private get _paperInput(): PaperInputElement {
@ -127,7 +127,7 @@ export class CloudWebhookManageDialog extends LitElement {
return [
haStyle,
css`
paper-dialog {
ha-paper-dialog {
width: 650px;
}
paper-input {

View File

@ -8,10 +8,10 @@ import {
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-dialog/paper-dialog";
import "../../../components/dialog/ha-paper-dialog";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { HaPaperDialog } from "../../../components/dialog/ha-paper-dialog";
import { HomeAssistant } from "../../../types";
import { haStyle } from "../../../resources/styles";
@ -39,7 +39,7 @@ class DialogCloudCertificate extends LitElement {
const { certificateInfo } = this._params;
return html`
<paper-dialog with-backdrop>
<ha-paper-dialog with-backdrop>
<h2>Certificate Information</h2>
<div>
<p>
@ -58,12 +58,12 @@ class DialogCloudCertificate extends LitElement {
<div class="paper-dialog-buttons">
<mwc-button @click="${this._closeDialog}">CLOSE</mwc-button>
</div>
</paper-dialog>
</ha-paper-dialog>
`;
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private _closeDialog() {
@ -74,7 +74,7 @@ class DialogCloudCertificate extends LitElement {
return [
haStyle,
css`
paper-dialog {
ha-paper-dialog {
width: 535px;
}
`,

View File

@ -63,7 +63,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
</template>
<template is="dom-if" if="[[!cloudStatus.logged_in]]">
<div secondary="">
[[localize('ui.panel.config.cloud.description_not_login')]]
[[localize('ui.panel.config.cloud.description_features')]]
</div>
</template>
</paper-item-body>

View File

@ -6,10 +6,11 @@ import {
CSSResult,
TemplateResult,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import "../../../components/dialog/ha-paper-dialog";
import { EntityRegistryDetailDialogParams } from "./show-dialog-entity-registry-detail";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
@ -56,7 +57,7 @@ class DialogEntityRegistryDetail extends LitElement {
computeDomain(this._params.entry.entity_id);
return html`
<paper-dialog
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
@ -116,7 +117,7 @@ class DialogEntityRegistryDetail extends LitElement {
)}
</mwc-button>
</div>
</paper-dialog>
</ha-paper-dialog>
`;
}
@ -166,7 +167,7 @@ class DialogEntityRegistryDetail extends LitElement {
return [
haStyleDialog,
css`
paper-dialog {
ha-paper-dialog {
min-width: 400px;
}
.form {

View File

@ -6,11 +6,12 @@ import {
TemplateResult,
property,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import "@material/mwc-button";
import "../../../components/dialog/ha-paper-dialog";
import "../../../components/entity/ha-entities-picker";
import "../../../components/user/ha-user-picker";
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
@ -49,7 +50,7 @@ class DialogPersonDetail extends LitElement {
}
const nameInvalid = this._name.trim() === "";
return html`
<paper-dialog
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
@ -114,7 +115,7 @@ class DialogPersonDetail extends LitElement {
${this._params.entry ? "UPDATE" : "CREATE"}
</mwc-button>
</div>
</paper-dialog>
</ha-paper-dialog>
`;
}
@ -175,7 +176,7 @@ class DialogPersonDetail extends LitElement {
return [
haStyleDialog,
css`
paper-dialog {
ha-paper-dialog {
min-width: 400px;
}
.form {

View File

@ -1,9 +1,9 @@
import "@material/mwc-button";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-spinner/paper-spinner";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/dialog/ha-paper-dialog";
import "../../../resources/ha-style";
import LocalizeMixin from "../../../mixins/localize-mixin";
@ -18,14 +18,14 @@ class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
.error {
color: red;
}
paper-dialog {
ha-paper-dialog {
max-width: 500px;
}
.username {
margin-top: -8px;
}
</style>
<paper-dialog
<ha-paper-dialog
id="dialog"
with-backdrop
opened="{{_opened}}"
@ -76,7 +76,7 @@ class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
>
</template>
</div>
</paper-dialog>
</ha-paper-dialog>
`;
}

View File

@ -1,3 +1,5 @@
import { ZHADevice } from "../../../data/zha";
export const formatAsPaddedHex = (value: string | number): string => {
let hex = value;
if (typeof value === "string") {
@ -5,3 +7,9 @@ export const formatAsPaddedHex = (value: string | number): string => {
}
return "0x" + hex.toString(16).padStart(4, "0");
};
export const sortZHADevices = (a: ZHADevice, b: ZHADevice): number => {
const nameA = a.user_given_name ? a.user_given_name : a.name;
const nameb = b.user_given_name ? b.user_given_name : b.name;
return nameA.localeCompare(nameb);
};

View File

@ -20,6 +20,7 @@ import { HASSDomEvent } from "../../../common/dom/fire_event";
import { Cluster, fetchBindableDevices, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { sortZHADevices } from "./functions";
import { ZHAClusterSelectedParams, ZHADeviceSelectedParams } from "./types";
export class HaConfigZha extends LitElement {
@ -99,9 +100,7 @@ export class HaConfigZha extends LitElement {
this._bindableDevices = (await fetchBindableDevices(
this.hass,
this._selectedDevice!.ieee
)).sort((a, b) => {
return a.name.localeCompare(b.name);
});
)).sort(sortZHADevices);
}
}

View File

@ -18,23 +18,28 @@ import {
import { ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { HomeAssistant, Route } from "../../../types";
@customElement("zha-add-devices-page")
class ZHAAddDevicesPage extends LitElement {
@property() public hass!: HomeAssistant;
@property() public isWide?: boolean;
@property() public route?: Route;
@property() private _error?: string;
@property() private _discoveredDevices: ZHADevice[] = [];
@property() private _formattedEvents: string = "";
@property() private _active: boolean = false;
@property() private _showHelp: boolean = false;
private _ieeeAddress?: string;
private _addDevicesTimeoutHandle: any = undefined;
private _subscribed?: Promise<() => Promise<void>>;
public connectedCallback(): void {
super.connectedCallback();
this._subscribe();
if (this.route && this.route.path && this.route.path !== "") {
this._ieeeAddress = this.route.path.substring(1);
}
this._subscribe(this._ieeeAddress);
}
public disconnectedCallback(): void {
@ -151,15 +156,19 @@ class ZHAAddDevicesPage extends LitElement {
}
}
private _subscribe(): void {
private _subscribe(ieeeAddress: string | undefined): void {
const data: any = { type: "zha/devices/permit" };
if (ieeeAddress) {
data.ieee = ieeeAddress;
}
this._subscribed = this.hass!.connection.subscribeMessage(
(message) => this._handleMessage(message),
{ type: "zha/devices/permit" }
data
);
this._active = true;
this._addDevicesTimeoutHandle = setTimeout(
() => this._unsubscribe(),
60000
75000
);
}

View File

@ -22,6 +22,7 @@ import { bindDevices, unbindDevices, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { ItemSelectedEvent } from "./types";
import "@polymer/paper-item/paper-item";
@customElement("zha-binding-control")
export class ZHABindingControl extends LitElement {
@ -64,7 +65,11 @@ export class ZHABindingControl extends LitElement {
>
${this.bindableDevices.map(
(device) => html`
<paper-item>${device.name}</paper-item>
<paper-item
>${device.user_given_name
? device.user_given_name
: device.name}</paper-item
>
`
)}
</paper-listbox>

View File

@ -1,4 +1,5 @@
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../../../components/entity/state-badge";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
@ -34,6 +35,7 @@ import { reconfigureNode, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { ItemSelectedEvent, NodeServiceData } from "./types";
import { navigate } from "../../../common/navigate";
declare global {
// for fire event
@ -222,6 +224,23 @@ class ZHADeviceCard extends LitElement {
</div>
`
: ""}
${this.device!.power_source === "Mains"
? html`
<mwc-button @click=${this._onAddDevicesClick}>
Add Devices
</mwc-button>
${this.showHelp
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="permit"
class="help-text2"
/>
`
: ""}
`
: ""}
</div>
`
: ""
@ -281,6 +300,10 @@ class ZHADeviceCard extends LitElement {
this.device!.area_id = newAreaId;
}
private _onAddDevicesClick() {
navigate(this, "add/" + this.device!.ieee);
}
static get styles(): CSSResult[] {
return [
haStyle,

View File

@ -24,6 +24,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { fetchDevices, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { sortZHADevices } from "./functions";
import { ItemSelectedEvent, ZHADeviceRemovedEvent } from "./types";
declare global {
@ -141,9 +142,7 @@ export class ZHANode extends LitElement {
}
private async _fetchDevices() {
this._nodes = (await fetchDevices(this.hass!)).sort((a, b) => {
return a.name.localeCompare(b.name);
});
this._nodes = (await fetchDevices(this.hass!)).sort(sortZHADevices);
}
private _onDeviceRemoved(event: ZHADeviceRemovedEvent): void {

View File

@ -1,8 +1,8 @@
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dialog/paper-dialog";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/dialog/ha-paper-dialog";
import "../../../resources/ha-style";
import EventsMixin from "../../../mixins/events-mixin";
@ -12,12 +12,12 @@ class ZwaveLogDialog extends EventsMixin(PolymerElement) {
return html`
<style include="ha-style-dialog">
</style>
<paper-dialog id="pwaDialog" with-backdrop="" opened="{{_opened}}">
<ha-paper-dialog id="pwaDialog" with-backdrop="" opened="{{_opened}}">
<h2>OpenZwave internal logfile</h2>
<paper-dialog-scrollable>
<pre>[[_ozwLog]]</pre>
<paper-dialog-scrollable>
</paper-dialog>
</ha-paper-dialog>
`;
}

View File

@ -2,8 +2,6 @@ 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-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../../../mixins/events-mixin";

View File

@ -6,9 +6,10 @@ import {
CSSResult,
TemplateResult,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../../components/dialog/ha-paper-dialog";
import { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
@ -34,7 +35,7 @@ class DialogSystemLogDetail extends LitElement {
const item = this._params.item;
return html`
<paper-dialog
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
@ -53,7 +54,7 @@ class DialogSystemLogDetail extends LitElement {
`
: html``}
</paper-dialog-scrollable>
</paper-dialog>
</ha-paper-dialog>
`;
}
@ -67,7 +68,7 @@ class DialogSystemLogDetail extends LitElement {
return [
haStyleDialog,
css`
paper-dialog {
ha-paper-dialog {
direction: ltr;
}
`,

View File

@ -106,7 +106,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
return html`
<ha-card .header="${title}">
<div class="entities ${classMap({ "no-header": !title })}">
<div class="${classMap({ entities: true, "no-header": !title })}">
${this._configEntities!.map((entityConf) =>
this.renderEntity(entityConf)
)}

View File

@ -21,7 +21,7 @@ declare global {
export class HuiYamlEditor extends HTMLElement {
public _hass?: HomeAssistant;
public codemirror: CodeMirror;
public codemirror!: any;
private _value: string;
@ -89,22 +89,25 @@ export class HuiYamlEditor extends HTMLElement {
public connectedCallback(): void {
if (!this.codemirror) {
this.codemirror = CodeMirror(this.shadowRoot, {
value: this._value,
lineNumbers: true,
mode: "yaml",
tabSize: 2,
autofocus: true,
viewportMargin: Infinity,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters:
this._hass && computeRTL(this._hass!)
? ["rtl-gutter", "CodeMirror-linenumbers"]
: [],
});
this.codemirror = CodeMirror(
(this.shadowRoot as unknown) as HTMLElement,
{
value: this._value,
lineNumbers: true,
mode: "yaml",
tabSize: 2,
autofocus: true,
viewportMargin: Infinity,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters:
this._hass && computeRTL(this._hass!)
? ["rtl-gutter", "CodeMirror-linenumbers"]
: [],
}
);
this.setScrollBarDirection();
this.codemirror.on("changes", () => this._onChange());
} else {

View File

@ -10,14 +10,14 @@ import "@material/mwc-button";
import "./hui-notification-item-template";
import { HomeAssistant } from "../../../../types";
import { HassEntity } from "home-assistant-js-websocket";
import { fireEvent } from "../../../../common/dom/fire_event";
import { HassNotification } from "./types";
@customElement("hui-configurator-notification-item")
export class HuiConfiguratorNotificationItem extends LitElement {
@property() public hass?: HomeAssistant;
@property() public notification?: HassEntity;
@property() public notification?: HassNotification;
protected render(): TemplateResult | void {
if (!this.hass || !this.notification) {

View File

@ -10,14 +10,14 @@ import {
import "./hui-configurator-notification-item";
import "./hui-persistent-notification-item";
import { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../../../../types";
import { HassNotification } from "./types";
@customElement("hui-notification-item")
export class HuiNotificationItem extends LitElement {
@property() public hass?: HomeAssistant;
@property() public notification?: HassEntity;
@property() public notification?: HassNotification;
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (!this.hass || !this.notification || changedProps.has("notification")) {

View File

@ -1,92 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-tooltip/paper-tooltip";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../components/ha-relative-time";
import "../../../../components/ha-markdown";
import "./hui-notification-item-template";
import LocalizeMixin from "../../../../mixins/localize-mixin";
/*
* @appliesMixin LocalizeMixin
*/
export class HuiPersistentNotificationItem extends LocalizeMixin(
PolymerElement
) {
static get template() {
return html`
<style>
.time {
display: flex;
justify-content: flex-end;
margin-top: 6px;
}
ha-relative-time {
color: var(--secondary-text-color);
}
a {
color: var(--primary-color);
}
</style>
<hui-notification-item-template>
<span slot="header">[[_computeTitle(notification)]]</span>
<ha-markdown content="[[notification.message]]"></ha-markdown>
<div class="time">
<span>
<ha-relative-time
hass="[[hass]]"
datetime="[[notification.created_at]]"
></ha-relative-time>
<paper-tooltip
>[[_computeTooltip(hass, notification)]]</paper-tooltip
>
</span>
</div>
<mwc-button slot="actions" on-click="_handleDismiss"
>[[localize('ui.card.persistent_notification.dismiss')]]</mwc-button
>
</hui-notification-item-template>
`;
}
static get properties() {
return {
hass: Object,
notification: Object,
};
}
_handleDismiss() {
this.hass.callService("persistent_notification", "dismiss", {
notification_id: this.notification.notification_id,
});
}
_computeTitle(notification) {
return notification.title || notification.notification_id;
}
_computeTooltip(hass, notification) {
if (!hass || !notification) return null;
const d = new Date(notification.created_at);
return d.toLocaleDateString(hass.language, {
year: "numeric",
month: "short",
day: "numeric",
minute: "numeric",
hour: "numeric",
});
}
}
customElements.define(
"hui-persistent-notification-item",
HuiPersistentNotificationItem
);

View File

@ -0,0 +1,110 @@
import {
html,
LitElement,
TemplateResult,
property,
customElement,
css,
CSSResult,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-tooltip/paper-tooltip";
import "../../../../components/ha-relative-time";
import "../../../../components/ha-markdown";
import "./hui-notification-item-template";
import { HomeAssistant } from "../../../../types";
import { HassNotification } from "./types";
@customElement("hui-persistent-notification-item")
export class HuiPersistentNotificationItem extends LitElement {
@property() public hass?: HomeAssistant;
@property() public notification?: HassNotification;
protected render(): TemplateResult | void {
if (!this.hass || !this.notification) {
return html``;
}
return html`
<hui-notification-item-template>
<span slot="header">${this._computeTitle(this.notification)}</span>
<ha-markdown content="${this.notification.message}"></ha-markdown>
<div class="time">
<span>
<ha-relative-time
.hass="${this.hass}"
.datetime="${this.notification.created_at}"
></ha-relative-time>
<paper-tooltip
>${this._computeTooltip(
this.hass,
this.notification
)}</paper-tooltip
>
</span>
</div>
<mwc-button slot="actions" @click="${this._handleDismiss}"
>${this.hass.localize(
"ui.card.persistent_notification.dismiss"
)}</mwc-button
>
</hui-notification-item-template>
`;
}
static get styles(): CSSResult {
return css`
.time {
display: flex;
justify-content: flex-end;
margin-top: 6px;
}
ha-relative-time {
color: var(--secondary-text-color);
}
a {
color: var(--primary-color);
}
`;
}
private _handleDismiss(): void {
this.hass!.callService("persistent_notification", "dismiss", {
notification_id: this.notification!.notification_id,
});
}
private _computeTitle(notification: HassNotification): string | undefined {
return notification.title || notification.notification_id;
}
private _computeTooltip(
hass: HomeAssistant,
notification: HassNotification
): string | undefined {
if (!hass || !notification) {
return undefined;
}
const d = new Date(notification.created_at!);
return d.toLocaleDateString(hass.language, {
year: "numeric",
month: "short",
day: "numeric",
minute: "numeric",
hour: "numeric",
});
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-persistent-notification-item": HuiPersistentNotificationItem;
}
}

View File

@ -0,0 +1,8 @@
import { HassEntity } from "home-assistant-js-websocket";
export declare type HassNotification = HassEntity & {
notification_id?: string;
created_at?: string;
title?: string;
message?: string;
};

View File

@ -7,10 +7,10 @@ import {
css,
CSSResult,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-item/paper-item";
import "../../../../components/dialog/ha-paper-dialog";
// tslint:disable-next-line:no-duplicate-imports
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog";
import { moveCard } from "../config-util";
import { MoveCardViewDialogParams } from "./show-move-card-view-dialog";
@ -30,7 +30,7 @@ export class HuiDialogMoveCardView extends LitElement {
return html``;
}
return html`
<paper-dialog
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
@ -46,7 +46,7 @@ export class HuiDialogMoveCardView extends LitElement {
>
`;
})}
</paper-dialog>
</ha-paper-dialog>
`;
}
@ -76,8 +76,8 @@ export class HuiDialogMoveCardView extends LitElement {
`;
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private _moveCard(e: Event): void {

View File

@ -6,9 +6,10 @@ import {
CSSResult,
customElement,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../../../../components/dialog/ha-paper-dialog";
import { haStyleDialog } from "../../../../resources/styles";
import "./hui-card-picker";
@ -23,7 +24,7 @@ export class HuiDialogPickCard extends LitElement {
protected render(): TemplateResult | void {
return html`
<paper-dialog
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
@ -40,7 +41,7 @@ export class HuiDialogPickCard extends LitElement {
<div class="paper-dialog-buttons">
<mwc-button @click="${this._skipPick}">MANUAL CARD</mwc-button>
</div>
</paper-dialog>
</ha-paper-dialog>
`;
}
@ -60,19 +61,19 @@ export class HuiDialogPickCard extends LitElement {
css`
@media all and (max-width: 450px), all and (max-height: 500px) {
/* overrule the ha-style-dialog max-height on small screens */
paper-dialog {
ha-paper-dialog {
max-height: 100%;
height: 100%;
}
}
@media all and (min-width: 660px) {
paper-dialog {
ha-paper-dialog {
width: 650px;
}
}
paper-dialog {
ha-paper-dialog {
max-width: 650px;
}
`,

Some files were not shown because too many files have changed in this diff Show More