mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 19:26:36 +00:00
commit
038f7b43d5
@ -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",
|
||||
|
@ -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: [
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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);
|
112
hassio/src/addon-store/hassio-addon-repository.ts
Normal file
112
hassio/src/addon-store/hassio-addon-repository.ts
Normal 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);
|
@ -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);
|
116
hassio/src/addon-store/hassio-addon-store.ts
Normal file
116
hassio/src/addon-store/hassio-addon-store.ts
Normal 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);
|
@ -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);
|
148
hassio/src/addon-store/hassio-repositories-editor.ts
Normal file
148
hassio/src/addon-store/hassio-repositories-editor.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
97
hassio/src/components/hassio-card-content.ts
Normal file
97
hassio/src/components/hassio-card-content.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
52
hassio/src/dashboard/hassio-dashboard.ts
Normal file
52
hassio/src/dashboard/hassio-dashboard.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
18
hassio/src/dialogs/markdown/show-dialog-hassio-markdown.ts
Normal file
18
hassio/src/dialogs/markdown/show-dialog-hassio-markdown.ts
Normal 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,
|
||||
});
|
||||
};
|
@ -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);
|
18
hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts
Normal file
18
hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts
Normal 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,
|
||||
});
|
||||
};
|
@ -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);
|
||||
|
@ -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);
|
@ -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
152
hassio/src/hassio-main.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
131
hassio/src/hassio-pages-with-tabs.ts
Normal file
131
hassio/src/hassio-pages-with-tabs.ts
Normal 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;
|
||||
}
|
||||
}
|
66
hassio/src/hassio-tabs-router.ts
Normal file
66
hassio/src/hassio-tabs-router.ts
Normal 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;
|
||||
}
|
||||
}
|
100
hassio/src/ingress-view/hassio-ingress-view.ts
Normal file
100
hassio/src/ingress-view/hassio-ingress-view.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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>`;
|
||||
|
@ -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);
|
363
hassio/src/snapshots/hassio-snapshots.ts
Normal file
363
hassio/src/snapshots/hassio-snapshots.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
147
package.json
147
package.json
@ -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": {
|
||||
|
2
setup.py
2
setup.py
@ -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",
|
||||
|
@ -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) {
|
||||
|
@ -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];
|
||||
|
||||
|
@ -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 {
|
28
src/components/ha-paper-dropdown-menu.ts
Normal file
28
src/components/ha-paper-dropdown-menu.ts
Normal 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);
|
@ -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
217
src/data/hassio.ts
Normal 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);
|
@ -16,6 +16,7 @@ export interface ZHADevice {
|
||||
manufacturer_code: number;
|
||||
device_reg_id: string;
|
||||
user_given_name?: string;
|
||||
power_source?: string;
|
||||
area_id?: string;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
57
src/dialogs/make-dialog-manager.ts
Normal file
57
src/dialogs/make-dialog-manager.ts
Normal 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);
|
||||
}
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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 -->
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 */
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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'>
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
35
src/layouts/loading-screen.ts
Normal file
35
src/layouts/loading-screen.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -74,6 +74,8 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
@property() public narrow?: boolean;
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
32
src/mixins/provide-hass-lit-mixin.ts
Normal file
32
src/mixins/provide-hass-lit-mixin.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
`,
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
`,
|
||||
|
@ -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)
|
||||
)}
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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")) {
|
||||
|
@ -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
|
||||
);
|
@ -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;
|
||||
}
|
||||
}
|
8
src/panels/lovelace/components/notifications/types.ts
Normal file
8
src/panels/lovelace/components/notifications/types.ts
Normal 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;
|
||||
};
|
@ -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 {
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user