Merge pull request #5757 from home-assistant/dev

This commit is contained in:
Bram Kragten 2020-05-05 17:38:14 +02:00 committed by GitHub
commit ae6243b7bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
244 changed files with 3879 additions and 2475 deletions

View File

@ -62,6 +62,18 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
- Browser and browser version:
- Operating system:
## State of relevant entities
<!--
If your issue is about how an entity is shown in the UI, please add the state
and attributes for all situations with a screenshot of the UI.
You can find this information at `/developer-tools/state`
-->
```yaml
```
## Problem-relevant configuration
<!--

View File

@ -35,7 +35,7 @@ jobs:
env:
CI: true
- name: Build icons
run: ./node_modules/.bin/gulp gen-icons-hassio gen-icons-mdi gen-icons-app
run: ./node_modules/.bin/gulp gen-icons-json
- name: Build translations
run: ./node_modules/.bin/gulp build-translations
- name: Run eslint

View File

@ -5,7 +5,7 @@ const envVars = require("../env");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gen-icons-json.js");
require("./gather-static.js");
require("./compress.js");
require("./webpack.js");
@ -21,7 +21,7 @@ gulp.task(
"clean",
gulp.parallel(
"gen-service-worker-dev",
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
"gen-icons-json",
"gen-pages-dev",
"gen-index-app-dev",
"build-translations"
@ -38,7 +38,7 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean",
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static",
"webpack-prod-app",
...// Don't compress running tests

View File

@ -2,7 +2,6 @@ const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
@ -17,12 +16,7 @@ gulp.task(
},
"clean-cast",
"translations-enable-merge-backend",
gulp.parallel(
"gen-icons-app",
"gen-icons-mdi",
"gen-index-cast-dev",
"build-translations"
),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-cast",
"webpack-dev-server-cast"
)
@ -36,7 +30,7 @@ gulp.task(
},
"clean-cast",
"translations-enable-merge-backend",
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-cast",
"webpack-prod-cast",
"gen-index-cast-prod"

View File

@ -3,7 +3,7 @@ const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gen-icons-json.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
@ -17,13 +17,7 @@ gulp.task(
},
"clean-demo",
"translations-enable-merge-backend",
gulp.parallel(
"gen-icons-app",
"gen-icons-mdi",
"gen-icons-demo",
"gen-index-demo-dev",
"build-translations"
),
gulp.parallel("gen-icons-json", "gen-index-demo-dev", "build-translations"),
"copy-static-demo",
"webpack-dev-server-demo"
)
@ -38,12 +32,7 @@ gulp.task(
"clean-demo",
// Cast needs to be backwards compatible and older HA has no translations
"translations-enable-merge-backend",
gulp.parallel(
"gen-icons-app",
"gen-icons-mdi",
"gen-icons-demo",
"build-translations"
),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-demo",
"webpack-prod-demo",
"gen-index-demo-prod"

View File

@ -47,11 +47,9 @@ gulp.task("gen-pages-dev", (done) => {
for (const page of PAGES) {
const content = renderTemplate(page, {
latestPageJS: `/frontend_latest/${page}.js`,
latestHassIconsJS: "/frontend_latest/hass-icons.js",
es5Compatibility: "/frontend_es5/compatibility.js",
es5PageJS: `/frontend_es5/${page}.js`,
es5HassIconsJS: "/frontend_es5/hass-icons.js",
});
fs.outputFileSync(path.resolve(config.root, `${page}.html`), content);
@ -66,11 +64,9 @@ gulp.task("gen-pages-prod", (done) => {
for (const page of PAGES) {
const content = renderTemplate(page, {
latestPageJS: latestManifest[`${page}.js`],
latestHassIconsJS: latestManifest["hass-icons.js"],
es5Compatibility: es5Manifest["compatibility.js"],
es5PageJS: es5Manifest[`${page}.js`],
es5HassIconsJS: es5Manifest["hass-icons.js"],
});
fs.outputFileSync(
@ -88,13 +84,11 @@ gulp.task("gen-index-app-dev", (done) => {
latestAppJS: "/frontend_latest/app.js",
latestCoreJS: "/frontend_latest/core.js",
latestCustomPanelJS: "/frontend_latest/custom-panel.js",
latestHassIconsJS: "/frontend_latest/hass-icons.js",
es5Compatibility: "/frontend_es5/compatibility.js",
es5AppJS: "/frontend_es5/app.js",
es5CoreJS: "/frontend_es5/core.js",
es5CustomPanelJS: "/frontend_es5/custom-panel.js",
es5HassIconsJS: "/frontend_es5/hass-icons.js",
}).replace(/#THEMEC/g, "{{ theme_color }}");
fs.outputFileSync(path.resolve(config.root, "index.html"), content);
@ -108,13 +102,11 @@ gulp.task("gen-index-app-prod", (done) => {
latestAppJS: latestManifest["app.js"],
latestCoreJS: latestManifest["core.js"],
latestCustomPanelJS: latestManifest["custom-panel.js"],
latestHassIconsJS: latestManifest["hass-icons.js"],
es5Compatibility: es5Manifest["compatibility.js"],
es5AppJS: es5Manifest["app.js"],
es5CoreJS: es5Manifest["core.js"],
es5CustomPanelJS: es5Manifest["custom-panel.js"],
es5HassIconsJS: es5Manifest["hass-icons.js"],
});
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");

View File

@ -3,7 +3,7 @@ const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gen-icons-json.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
@ -17,7 +17,7 @@ gulp.task(
},
"clean-gallery",
"translations-enable-merge-backend",
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-gallery",
"gen-index-gallery-dev",
"webpack-dev-server-gallery"
@ -32,7 +32,7 @@ gulp.task(
},
"clean-gallery",
"translations-enable-merge-backend",
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-gallery",
"webpack-prod-gallery",
"gen-index-gallery-prod"

View File

@ -26,6 +26,13 @@ function copyTranslations(staticDir) {
);
}
function copyMdiIcons(staticDir) {
const staticPath = genStaticPath(staticDir);
// MDI icons output
fs.copySync(polyPath("build/mdi"), staticPath("mdi"));
}
function copyPolyfills(staticDir) {
const staticPath = genStaticPath(staticDir);
@ -80,6 +87,7 @@ gulp.task("copy-static", (done) => {
copyPolyfills(staticDir);
copyFonts(staticDir);
copyTranslations(staticDir);
copyMdiIcons(staticDir);
// Panel assets
copyFileDir(
@ -103,6 +111,7 @@ gulp.task("copy-static-demo", (done) => {
copyMapPanel(paths.demo_static);
copyFonts(paths.demo_static);
copyTranslations(paths.demo_static);
copyMdiIcons(paths.demo_static);
done();
});
@ -115,6 +124,7 @@ gulp.task("copy-static-cast", (done) => {
copyMapPanel(paths.cast_static);
copyFonts(paths.cast_static);
copyTranslations(paths.cast_static);
copyMdiIcons(paths.cast_static);
done();
});
@ -127,5 +137,6 @@ gulp.task("copy-static-gallery", (done) => {
copyMapPanel(paths.gallery_static);
copyFonts(paths.gallery_static);
copyTranslations(paths.gallery_static);
copyMdiIcons(paths.gallery_static);
done();
});

View File

@ -0,0 +1,109 @@
const gulp = require("gulp");
const path = require("path");
const fs = require("fs");
const hash = require("object-hash");
const ICON_PACKAGE_PATH = path.resolve(
__dirname,
"../../node_modules/@mdi/svg/"
);
const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
const ICON_PATH = path.resolve(ICON_PACKAGE_PATH, "svg");
const OUTPUT_DIR = path.resolve(__dirname, "../../build/mdi");
const encoding = "utf8";
const getMeta = () => {
const file = fs.readFileSync(META_PATH, { encoding });
const meta = JSON.parse(file);
return meta.map((icon) => {
const svg = fs.readFileSync(`${ICON_PATH}/${icon.name}.svg`, {
encoding,
});
return { path: svg.match(/ d="([^"]+)"/)[1], name: icon.name };
});
};
const splitBySize = (meta) => {
const chunks = [];
const CHUNK_SIZE = 100000;
let curSize = 0;
let startKey;
let icons = [];
Object.values(meta).forEach((icon) => {
if (startKey === undefined) {
startKey = icon.name;
}
curSize += icon.path.length;
icons.push(icon);
if (curSize > CHUNK_SIZE) {
chunks.push({
startKey,
endKey: icon.name,
icons,
});
curSize = 0;
startKey = undefined;
icons = [];
}
});
chunks.push({
startKey,
icons,
});
return chunks;
};
const findDifferentiator = (curString, prevString) => {
for (let i = 0; i < curString.length; i++) {
if (curString[i] !== prevString[i]) {
return curString.substring(0, i + 1);
}
}
console.error("Cannot find differentiator", curString, prevString);
return undefined;
};
gulp.task("gen-icons-json", (done) => {
const meta = getMeta();
const split = splitBySize(meta);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
const manifest = [];
let lastEnd;
split.forEach((chunk) => {
let startKey;
if (lastEnd === undefined) {
chunk.startKey = undefined;
startKey = undefined;
} else {
startKey = findDifferentiator(chunk.startKey, lastEnd);
}
lastEnd = chunk.endKey;
const output = {};
chunk.icons.forEach((icon) => {
output[icon.name] = icon.path;
});
const filename = hash(output);
manifest.push({ start: startKey, file: filename });
fs.writeFileSync(
path.resolve(OUTPUT_DIR, `${filename}.json`),
JSON.stringify(output)
);
});
fs.writeFileSync(
path.resolve(OUTPUT_DIR, "iconMetadata.json"),
JSON.stringify(manifest)
);
done();
});

View File

@ -1,127 +0,0 @@
const gulp = require("gulp");
const path = require("path");
const fs = require("fs");
const paths = require("../paths");
const { mapFiles } = require("../util");
const ICON_PACKAGE_PATH = path.resolve(
__dirname,
"../../node_modules/@mdi/svg/"
);
const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
const ICON_PATH = path.resolve(ICON_PACKAGE_PATH, "svg");
const OUTPUT_DIR = path.resolve(__dirname, "../../build");
const MDI_OUTPUT_PATH = path.resolve(OUTPUT_DIR, "mdi.html");
const HASS_OUTPUT_PATH = path.resolve(OUTPUT_DIR, "hass-icons.html");
const BUILT_IN_PANEL_ICONS = [
"calendar", // Calendar
"settings", // Config
"home-assistant", // Hass.io
"poll-box", // History panel
"format-list-bulleted-type", // Logbook
"mailbox", // Mailbox
"tooltip-account", // Map
"cart", // Shopping List
"hammer", // developer-tools
];
// Given an icon name, load the SVG file
function loadIcon(name) {
const iconPath = path.resolve(ICON_PATH, `${name}.svg`);
try {
return fs.readFileSync(iconPath, "utf-8");
} catch (err) {
return null;
}
}
// Given an SVG file, convert it to an iron-iconset-svg definition
function transformXMLtoPolymer(name, xml) {
const start = xml.indexOf("><path") + 1;
const end = xml.length - start - 6;
const pth = xml.substr(start, end);
return `<g id="${name}">${pth}</g>`;
}
// Given an iconset name and icon names, generate a polymer iconset
function generateIconset(iconsetName, iconNames) {
const iconDefs = Array.from(iconNames)
.map((name) => {
const iconDef = loadIcon(name);
if (!iconDef) {
throw new Error(`Unknown icon referenced: ${name}`);
}
return transformXMLtoPolymer(name, iconDef);
})
.join("");
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
}
// Find all icons used by the project.
function findIcons(searchPath, iconsetName) {
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
const icons = new Set();
function processFile(filename) {
const content = fs.readFileSync(filename);
let match;
// eslint-disable-next-line
while ((match = iconRegex.exec(content))) {
// strip off "hass:" and add to set
icons.add(match[0].substr(iconsetName.length + 1));
}
}
mapFiles(searchPath, ".js", processFile);
mapFiles(searchPath, ".ts", processFile);
return icons;
}
gulp.task("gen-icons-mdi", (done) => {
const meta = JSON.parse(
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
);
const iconNames = meta.map((iconInfo) => iconInfo.name);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
done();
});
gulp.task("gen-icons-app", (done) => {
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));
done();
});
gulp.task("gen-icons-demo", (done) => {
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
fs.writeFileSync(
path.resolve(paths.demo_dir, "hademo-icons.html"),
generateIconset("hademo", iconNames)
);
done();
});
gulp.task("gen-icons-hassio", (done) => {
const iconNames = findIcons(
path.resolve(paths.hassio_dir, "./src"),
"hassio"
);
// Find hassio icons inside HA main repo.
for (const item of findIcons(
path.resolve(paths.polymer_dir, "./src"),
"hassio"
)) {
iconNames.add(item);
}
fs.writeFileSync(
path.resolve(paths.hassio_dir, "hassio-icons.html"),
generateIconset("hassio", iconNames)
);
done();
});

View File

@ -3,7 +3,7 @@ const gulp = require("gulp");
const envVars = require("../env");
require("./clean.js");
require("./gen-icons.js");
require("./gen-icons-json.js");
require("./webpack.js");
require("./compress.js");
@ -14,7 +14,6 @@ gulp.task(
process.env.NODE_ENV = "development";
},
"clean-hassio",
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
"webpack-watch-hassio"
)
);
@ -26,7 +25,6 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean-hassio",
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
"webpack-prod-hassio",
...// Don't compress running tests
(envVars.isTravis() ? [] : ["compress-hassio"])

View File

@ -28,7 +28,7 @@ const runDevServer = ({
open: true,
watchContentBase: true,
contentBase,
}).listen(port, listenHost, function(err) {
}).listen(port, listenHost, function (err) {
if (err) {
throw err;
}

View File

@ -49,6 +49,9 @@ const createWebpackConfig = ({
},
],
},
externals: {
esprima: "esprima",
},
optimization: {
minimizer: [
new TerserPlugin({
@ -137,7 +140,6 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.ts",
"custom-panel": "./src/entrypoints/custom-panel.ts",
"hass-icons": "./src/entrypoints/hass-icons.ts",
},
outputRoot: paths.root,
isProdBuild,

View File

@ -1,5 +1,3 @@
import "../../../src/components/ha-iconset-svg";
import "../../../src/resources/ha-style";
import "../../../src/resources/hass-icons";
import "../../../src/resources/roboto";
import "./layout/hc-connect";

View File

@ -1,4 +1,3 @@
import "@polymer/iron-icon";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox";
import { Auth, Connection } from "home-assistant-js-websocket";
@ -83,7 +82,7 @@ class HcCast extends LitElement {
? html`
<p class="center-item">
<mwc-button raised @click=${this._handleLaunch}>
<iron-icon icon="hass:cast"></iron-icon>
<ha-icon icon="hass:cast"></ha-icon>
Start Casting
</mwc-button>
</p>
@ -121,7 +120,7 @@ class HcCast extends LitElement {
${this.castManager.status
? html`
<mwc-button @click=${this._handleLaunch}>
<iron-icon icon="hass:cast-connected"></iron-icon>
<ha-icon icon="hass:cast-connected"></ha-icon>
Manage
</mwc-button>
`
@ -243,7 +242,7 @@ class HcCast extends LitElement {
color: var(--secondary-text-color);
}
mwc-button iron-icon {
mwc-button ha-icon {
margin-right: 8px;
height: 18px;
}

View File

@ -1,5 +1,4 @@
import "@material/mwc-button";
import "@polymer/iron-icon";
import "@polymer/paper-input/paper-input";
import {
Auth,
@ -27,6 +26,7 @@ import {
loadTokens,
saveTokens,
} from "../../../../src/common/auth/token_storage";
import "../../../../src/components/ha-icon";
import "../../../../src/layouts/loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout";
@ -136,11 +136,11 @@ export class HcConnect extends LitElement {
<div class="card-actions">
<mwc-button @click=${this._handleDemo}>
Show Demo
<iron-icon
<ha-icon
.icon=${this.castManager.castState === "CONNECTED"
? "hass:cast-connected"
: "hass:cast"}
></iron-icon>
></ha-icon>
</mwc-button>
<div class="spacer"></div>
<mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
@ -316,7 +316,7 @@ export class HcConnect extends LitElement {
color: darkred;
}
mwc-button iron-icon {
mwc-button ha-icon {
margin-left: 8px;
}

View File

@ -1,5 +1,3 @@
import "web-animations-js/web-animations-next-lite.min";
import "../../../src/components/ha-iconset-svg";
import "../../../src/resources/hass-icons";
import "../../../src/resources/roboto";
import "./layout/hc-lovelace";

View File

@ -63,8 +63,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
elements: [
{
style: {
"--iron-icon-width": "100px",
"--iron-icon-height": "100px",
"--mdc-icon-size": "100px",
top: "50%",
left: "50%",
},

View File

@ -1,12 +1,9 @@
import "@polymer/paper-styles/typography";
import "@polymer/polymer/lib/elements/dom-if";
import "@polymer/polymer/lib/elements/dom-repeat";
import "../../src/components/ha-iconset-svg";
import "../../src/resources/ha-style";
import "../../src/resources/hass-icons";
import "../../src/resources/roboto";
import "./ha-demo";
import "./resources/hademo-icons";
/* polyfill for paper-dropdown */
setTimeout(() => {

View File

@ -1,7 +0,0 @@
import "../../../src/components/ha-iconset-svg";
import iconSetContent from "../../hademo-icons.html";
const documentContainer = document.createElement("template");
documentContainer.setAttribute("style", "display: none;");
documentContainer.innerHTML = iconSetContent;
document.head.appendChild(documentContainer.content);

View File

@ -50,10 +50,9 @@ const CONFIGS = [
top: 12%
left: 6%
transform: rotate(-60deg) scaleX(-1)
--iron-icon-height: 30px
--iron-icon-width: 30px
--iron-icon-stroke-color: black
--iron-icon-fill-color: rgba(50, 50, 50, .75)
--mdc-icon-size: 30px
--mdc-icon-stroke-color: black
--mdc-icon-fill-color: rgba(50, 50, 50, .75)
- type: image
entity: light.bed_light
tap_action:
@ -99,10 +98,9 @@ const CONFIGS = [
top: 12%
left: 6%
transform: rotate(-60deg) scaleX(-1)
--iron-icon-height: 30px
--iron-icon-width: 30px
--iron-icon-stroke-color: black
--iron-icon-fill-color: rgba(50, 50, 50, .75)
--mdc-icon-size: 30px
--mdc-icon-stroke-color: black
--mdc-icon-fill-color: rgba(50, 50, 50, .75)
- type: image
entity: light.bed_light
tap_action:

View File

@ -1,9 +1,7 @@
import "@polymer/paper-styles/typography";
import "@polymer/polymer/lib/elements/dom-if";
import "@polymer/polymer/lib/elements/dom-repeat";
import "../../src/components/ha-iconset-svg";
import "../../src/resources/ha-style";
import "../../src/resources/hass-icons";
import "../../src/resources/roboto";
import "./ha-gallery";

View File

@ -1,8 +1,8 @@
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/iron-icon/iron-icon";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../src/components/ha-icon-button";
import "../../src/components/ha-icon";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { html } from "@polymer/polymer/lib/utils/html-tag";
@ -28,7 +28,7 @@ class HaGallery extends PolymerElement {
app-header-layout {
min-height: 100vh;
}
paper-icon-button.invisible {
ha-icon-button.invisible {
visibility: hidden;
}
@ -67,11 +67,11 @@ class HaGallery extends PolymerElement {
<app-header-layout>
<app-header slot="header" fixed>
<app-toolbar>
<paper-icon-button
<ha-icon-button
icon="hass:arrow-left"
on-click="_backTapped"
class$='[[_computeHeaderButtonClass(_demo)]]'
></paper-icon-button>
></ha-icon-button>
<div main-title>[[_withDefault(_demo, "Home Assistant Gallery")]]</div>
</app-toolbar>
</app-header>
@ -98,7 +98,7 @@ class HaGallery extends PolymerElement {
<a href='#[[item]]'>
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon>
<ha-icon icon="hass:chevron-right"></ha-icon>
</paper-item>
</a>
</template>
@ -114,7 +114,7 @@ class HaGallery extends PolymerElement {
<a href='#[[item]]'>
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon>
<ha-icon icon="hass:chevron-right"></ha-icon>
</paper-item>
</a>
</template>
@ -130,7 +130,7 @@ class HaGallery extends PolymerElement {
<a href='#[[item]]'>
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon>
<ha-icon icon="hass:chevron-right"></ha-icon>
</paper-item>
</a>
</template>

1
hassio/.gitignore vendored
View File

@ -1 +0,0 @@
hassio-icons.html

View File

@ -13,11 +13,14 @@ import {
reloadHassioAddons,
} from "../../../src/data/hassio/addon";
import "../../../src/layouts/loading-screen";
import { HomeAssistant } from "../../../src/types";
import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types";
import "../components/hassio-search-input";
import "./hassio-addon-repository";
import "./hassio-repositories-editor";
import { supervisorTabs } from "../hassio-panel";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") {
return -1;
@ -35,11 +38,15 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
};
class HassioAddonStore extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() private _addons?: HassioAddonInfo[];
@property({ type: Boolean }) public narrow!: boolean;
@property() private _repos?: HassioAddonRepository[];
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) private _addons?: HassioAddonInfo[];
@property({ attribute: false }) private _repos?: HassioAddonRepository[];
@property() private _filter?: string;
@ -52,42 +59,62 @@ class HassioAddonStore extends LitElement {
}
protected render(): TemplateResult {
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 (this._repos) {
for (const repo of this._repos) {
const addons = this._addons!.filter(
(addon) => addon.repository === repo.slug
);
if (addons.length === 0) {
continue;
if (addons.length === 0) {
continue;
}
repos.push(html`
<hassio-addon-repository
.hass=${this.hass}
.repo=${repo}
.addons=${addons}
.filter=${this._filter}
></hassio-addon-repository>
`);
}
repos.push(html`
<hassio-addon-repository
.hass=${this.hass}
.repo=${repo}
.addons=${addons}
.filter=${this._filter}
></hassio-addon-repository>
`);
}
return html`
<hassio-repositories-editor
<hass-tabs-subpage
.hass=${this.hass}
.repos=${this._repos}
></hassio-repositories-editor>
.narrow=${this.narrow}
.route=${this.route}
hassio
main-page
.tabs=${supervisorTabs}
>
<span slot="header">Add-on store</span>
<ha-icon-button
icon="hassio:reload"
slot="toolbar-icon"
aria-label="Reload add-ons"
@click=${this.refreshData}
></ha-icon-button>
<hassio-search-input
.filter=${this._filter}
@value-changed=${this._filterChanged}
></hassio-search-input>
${repos.length === 0
? html`<loading-screen></loading-screen>`
: html`
<hassio-repositories-editor
.hass=${this.hass}
.repos=${this._repos!}
></hassio-repositories-editor>
${repos}
<hassio-search-input
.filter=${this._filter}
@value-changed=${this._filterChanged}
></hassio-search-input>
${repos}
`}
</hass-tabs-subpage>
`;
}

View File

@ -1,4 +1,3 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input";
import {
@ -19,6 +18,7 @@ import { PolymerChangedEvent } from "../../../src/polymer-types";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import "../../../src/components/ha-icon";
@customElement("hassio-repositories-editor")
class HassioRepositoriesEditor extends LitElement {
@ -76,7 +76,7 @@ class HassioRepositoriesEditor extends LitElement {
<paper-card>
<div class="card-content add">
<iron-icon icon="hassio:github-circle"></iron-icon>
<ha-icon icon="hassio:github-circle"></ha-icon>
<paper-input
label="Add new repository by URL"
.value=${this._repoUrl}
@ -130,7 +130,7 @@ class HassioRepositoriesEditor extends LitElement {
.add {
padding: 12px 16px;
}
iron-icon {
ha-icon {
color: var(--secondary-text-color);
margin-right: 16px;
display: inline-block;

View File

@ -18,20 +18,21 @@ import {
HassioAddonDetails,
HassioAddonSetOptionParams,
setHassioAddonOption,
} from "../../../src/data/hassio/addon";
} from "../../../../src/data/hassio/addon";
import {
fetchHassioHardwareAudio,
HassioHardwareAudioDevice,
} from "../../../src/data/hassio/hardware";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
} from "../../../../src/data/hassio/hardware";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("hassio-addon-audio")
class HassioAddonAudio extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property() private _error?: string;
@ -56,7 +57,7 @@ class HassioAddonAudio extends LitElement {
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedInput}
.selected=${this._selectedInput!}
>
${this._inputDevices &&
this._inputDevices.map((item) => {
@ -75,7 +76,7 @@ class HassioAddonAudio extends LitElement {
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedOutput}
.selected=${this._selectedOutput!}
>
${this._outputDevices &&
this._outputDevices.map((item) => {
@ -183,6 +184,9 @@ class HassioAddonAudio extends LitElement {
} catch {
this._error = "Failed to set addon audio device";
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
}
}

View File

@ -0,0 +1,81 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../../src/types";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { hassioStyle } from "../../resources/hassio-style";
import { haStyle } from "../../../../src/resources/styles";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-network";
@customElement("hassio-addon-config-tab")
class HassioAddonConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
if (!this.addon) {
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">
<hassio-addon-config
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-config>
${this.addon.network
? html`
<hassio-addon-network
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-network>
`
: ""}
${this.addon.audio
? html`
<hassio-addon-audio
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-audio>
`
: ""}
</div>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
.content {
margin: auto;
padding: 8px;
max-width: 1024px;
}
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-config-tab": HassioAddonConfigDashboard;
}
}

View File

@ -12,24 +12,26 @@ import {
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../src/components/ha-yaml-editor";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
setHassioAddonOption,
} from "../../../src/data/hassio/addon";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
} from "../../../../src/data/hassio/addon";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property() private _error?: string;
@ -43,7 +45,8 @@ class HassioAddonConfig extends LitElement {
const valid = editor ? editor.isValid : true;
return html`
<paper-card heading="Config">
<h1>${this.addon.name}</h1>
<paper-card heading="Configuration">
<div class="card-content">
<ha-yaml-editor
@value-changed=${this._configChanged}
@ -113,6 +116,7 @@ class HassioAddonConfig extends LitElement {
title: this.addon.name,
text: "Are you sure you want to reset all your options?",
confirmText: "reset options",
dismissText: "no",
});
if (!confirmed) {
@ -164,6 +168,9 @@ class HassioAddonConfig extends LitElement {
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
}
}

View File

@ -10,15 +10,17 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
setHassioAddonOption,
} from "../../../src/data/hassio/addon";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
} from "../../../../src/data/hassio/addon";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
interface NetworkItem {
description: string;
@ -32,9 +34,9 @@ interface NetworkItemInput extends PaperInputElement {
@customElement("hassio-addon-network")
class HassioAddonNetwork extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property() private _error?: string;
@ -70,7 +72,7 @@ class HassioAddonNetwork extends LitElement {
<paper-input
@value-changed=${this._configChanged}
placeholder="disabled"
.value=${item.host}
.value=${String(item.host)}
.container=${item.container}
no-label-float
></paper-input>
@ -165,6 +167,9 @@ class HassioAddonNetwork extends LitElement {
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
}
private async _saveTapped(): Promise<void> {
@ -191,6 +196,9 @@ class HassioAddonNetwork extends LitElement {
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
}
}

View File

@ -0,0 +1,92 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../../src/types";
import {
HassioAddonDetails,
fetchHassioAddonDocumentation,
} from "../../../../src/data/hassio/addon";
import "../../../../src/components/ha-markdown";
import "../../../../src/layouts/loading-screen";
import { hassioStyle } from "../../resources/hassio-style";
import { haStyle } from "../../../../src/resources/styles";
@customElement("hassio-addon-documentation-tab")
class HassioAddonDocumentationDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@property() private _error?: string;
@property() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
protected render(): TemplateResult {
if (!this.addon) {
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">
<paper-card>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
<div class="card-content">
${this._content
? html`<ha-markdown .content=${this._content}></ha-markdown>`
: html`<loading-screen></loading-screen>`}
</div>
</paper-card>
</div>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
paper-card {
display: block;
}
.content {
margin: auto;
padding: 8px;
max-width: 1024px;
}
`,
];
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioAddonDocumentation(
this.hass,
this.addon!.slug
);
} catch (err) {
this._error = `Failed to get addon documentation, ${
err.body?.message || err
}`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-documentation-tab": HassioAddonDocumentationDashboard;
}
}

View File

@ -0,0 +1,185 @@
import "../../../src/components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import {
fetchHassioAddonInfo,
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import "./config/hassio-addon-audio";
import "./config/hassio-addon-config";
import "./info/hassio-addon-info";
import "./log/hassio-addon-logs";
import "./config/hassio-addon-network";
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import "./hassio-addon-router";
@customElement("hassio-addon-dashboard")
class HassioAddonDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@property({ type: Boolean }) public narrow!: boolean;
private _computeTail = memoizeOne((route: Route) => {
const dividerPos = route.path.indexOf("/", 1);
return dividerPos === -1
? {
prefix: route.prefix + route.path,
path: "",
}
: {
prefix: route.prefix + route.path.substr(0, dividerPos),
path: route.path.substr(dividerPos),
};
});
protected render(): TemplateResult {
if (!this.addon) {
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
const addonTabs: PageNavigation[] = [
{
name: "Info",
path: `/hassio/addon/${this.addon.slug}/info`,
icon: "hassio:information-variant",
},
];
if (this.addon.documentation) {
addonTabs.push({
name: "Documentation",
path: `/hassio/addon/${this.addon.slug}/documentation`,
icon: "hassio:file-document",
});
}
if (this.addon.version) {
addonTabs.push(
{
name: "Configuration",
path: `/hassio/addon/${this.addon.slug}/config`,
icon: "hassio:cogs",
},
{
name: "Log",
path: `/hassio/addon/${this.addon.slug}/logs`,
icon: "hassio:math-log",
}
);
}
const route = this._computeTail(this.route);
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"}
.route=${route}
hassio
.tabs=${addonTabs}
>
<span slot="header">${this.addon.name}</span>
<hassio-addon-router
.route=${route}
.narrow=${this.narrow}
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-router>
</hass-tabs-subpage>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
.content {
padding: 24px 0 32px;
display: flex;
flex-direction: column;
align-items: center;
}
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
width: 600px;
}
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config,
hassio-addon-logs {
max-width: 100%;
min-width: 100%;
}
}
`,
];
}
protected async firstUpdated(): Promise<void> {
await this._routeDataChanged(this.route);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private async _apiCalled(ev): Promise<void> {
const path: string = ev.detail.path;
if (!path) {
return;
}
if (path === "uninstall") {
history.back();
} else {
await this._routeDataChanged(this.route);
}
}
private async _routeDataChanged(routeData: Route): Promise<void> {
const addon = routeData.path.split("/")[1];
try {
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
this.addon = addoninfo;
} catch {
this.addon = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-dashboard": HassioAddonDashboard;
}
}

View File

@ -0,0 +1,53 @@
import {
HassRouterPage,
RouterOptions,
} from "../../../src/layouts/hass-router-page";
import { customElement, property } from "lit-element";
import { HomeAssistant } from "../../../src/types";
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
import "./info/hassio-addon-info-tab";
import "./config/hassio-addon-config-tab";
import "./log/hassio-addon-log-tab";
import "./documentation/hassio-addon-documentation-tab";
import { HassioAddonDetails } from "../../../src/data/hassio/addon";
@customElement("hassio-addon-router")
class HassioAddonRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon!: HassioAddonDetails;
protected routerOptions: RouterOptions = {
defaultPage: "info",
showLoading: true,
routes: {
info: {
tag: "hassio-addon-info-tab",
},
documentation: {
tag: "hassio-addon-documentation-tab",
},
config: {
tag: "hassio-addon-config-tab",
},
logs: {
tag: "hassio-addon-log-tab",
},
},
};
protected updatePageEl(el) {
el.route = this.routeTail;
el.hass = this.hass;
el.addon = this.addon;
el.narrow = this.narrow;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-router": HassioAddonRouter;
}
}

View File

@ -1,158 +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-spinner/paper-spinner-lite";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import {
fetchHassioAddonInfo,
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-info";
import "./hassio-addon-logs";
import "./hassio-addon-network";
import "../../../src/layouts/hass-subpage";
@customElement("hassio-addon-view")
class HassioAddonView extends LitElement {
@property() public hass!: HomeAssistant;
@property() public route!: Route;
@property() public addon?: HassioAddonDetails;
protected render(): TemplateResult {
if (!this.addon) {
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<hass-subpage header="Hass.io: add-on details" hassio>
<div class="content">
<hassio-addon-info
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-info>
${this.addon && this.addon.version
? html`
<hassio-addon-config
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-config>
${this.addon.audio
? html`
<hassio-addon-audio
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-audio>
`
: ""}
${this.addon.network
? html`
<hassio-addon-network
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-network>
`
: ""}
<hassio-addon-logs
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-logs>
`
: ""}
</div>
</hass-subpage>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
.content {
padding: 24px 0 32px;
display: flex;
flex-direction: column;
align-items: center;
}
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
width: 600px;
}
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config,
hassio-addon-logs {
max-width: 100%;
min-width: 100%;
}
}
`,
];
}
protected async firstUpdated(): Promise<void> {
await this._routeDataChanged(this.route);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private async _apiCalled(ev): Promise<void> {
const path: string = ev.detail.path;
if (!path) {
return;
}
if (path === "uninstall") {
history.back();
} else {
await this._routeDataChanged(this.route);
}
}
private async _routeDataChanged(routeData: Route): Promise<void> {
const addon = routeData.path.substr(1);
try {
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
this.addon = addoninfo;
} catch {
this.addon = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-view": HassioAddonView;
}
}

View File

@ -0,0 +1,62 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../../src/types";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { hassioStyle } from "../../resources/hassio-style";
import { haStyle } from "../../../../src/resources/styles";
import "./hassio-addon-info";
@customElement("hassio-addon-info-tab")
class HassioAddonInfoDashboard extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
if (!this.addon) {
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">
<hassio-addon-info
.narrow=${this.narrow}
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-info>
</div>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
.content {
margin: auto;
padding: 8px;
max-width: 1024px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-info-tab": HassioAddonInfoDashboard;
}
}

View File

@ -1,5 +1,4 @@
import "@material/mwc-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-tooltip/paper-tooltip";
import {
@ -12,14 +11,15 @@ import {
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-switch";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { navigate } from "../../../../src/common/navigate";
import "../../../../src/components/buttons/ha-call-api-button";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-label-badge";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-icon";
import {
fetchHassioAddonChangelog,
HassioAddonDetails,
@ -29,18 +29,29 @@ import {
setHassioAddonOption,
setHassioAddonSecurity,
uninstallHassioAddon,
} from "../../../src/data/hassio/addon";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../resources/hassio-style";
} from "../../../../src/data/hassio/addon";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-card-content";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../../resources/hassio-style";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
const STAGE_ICON = {
stable: "mdi:check-circle",
experimental: "mdi:flask",
deprecated: "mdi:exclamation-thick",
};
const PERMIS_DESC = {
stage: {
title: "Add-on Stage",
description: `Add-ons can have one of three stages:\n\n<ha-icon icon='${STAGE_ICON.stable}'></ha-icon>**Stable**: These are add-ons ready to be used in production.\n<ha-icon icon='${STAGE_ICON.experimental}'></ha-icon>**Experimental**: These may contain bugs, and may be unfinished.\n<ha-icon icon='${STAGE_ICON.deprecated}'></ha-icon>**Deprecated**: These add-ons will no longer receive any updates.`,
},
rating: {
title: "Add-on Security Rating",
description:
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
"Home Assistant provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
@ -58,19 +69,19 @@ const PERMIS_DESC = {
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
hassio_api: {
title: "Hass.io API Access",
title: "Supervisor API Access",
description:
"The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
"The add-on was given access to the Supervisor API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Home Assistant system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
apparmor: {
title: "AppArmor",
@ -91,9 +102,11 @@ const PERMIS_DESC = {
@customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property() public addon!: HassioAddonDetails;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property() private _error?: string;
@ -158,25 +171,25 @@ class HassioAddonInfo extends LitElement {
<paper-card>
<div class="card-content">
<div class="addon-header">
${this.addon.name}
${!this.narrow ? this.addon.name : ""}
<div class="addon-version light-color">
${this.addon.version
? html`
${this.addon.version}
${this._computeIsRunning
? html`
<iron-icon
<ha-icon
title="Add-on is running"
class="running"
icon="hassio:circle"
></iron-icon>
></ha-icon>
`
: html`
<iron-icon
<ha-icon
title="Add-on is stopped"
class="stopped"
icon="hassio:circle"
></iron-icon>
></ha-icon>
`}
`
: html` ${this.addon.version_latest} `}
@ -185,7 +198,7 @@ class HassioAddonInfo extends LitElement {
<div class="description light-color">
${this.addon.description}.<br />
Visit
<a href="${this.addon.url}" target="_blank" rel="noreferrer">
<a href="${this.addon.url!}" target="_blank" rel="noreferrer">
${this.addon.name} page</a
>
for details.
@ -193,7 +206,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.logo
? html`
<a
href="${this.addon.url}"
href="${this.addon.url!}"
target="_blank"
class="logo"
rel="noreferrer"
@ -203,6 +216,18 @@ class HassioAddonInfo extends LitElement {
`
: ""}
<div class="security">
<ha-label-badge
class=${classMap({
green: this.addon.stage === "stable",
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
.icon=${STAGE_ICON[this.addon.stage]}
label="stage"
description=""
></ha-label-badge>
<ha-label-badge
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
@ -362,7 +387,7 @@ class HassioAddonInfo extends LitElement {
<div>
Protection mode
<span>
<iron-icon icon="hassio:information"></iron-icon>
<ha-icon icon="hassio:information"></ha-icon>
<paper-tooltip>
Grant the add-on elevated system access.
</paper-tooltip>
@ -383,29 +408,8 @@ class HassioAddonInfo extends LitElement {
<div class="card-actions">
${this.addon.version
? html`
<mwc-button class="warning" @click=${this._uninstallClicked}>
Uninstall
</mwc-button>
${this.addon.build
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
Rebuild
</ha-call-api-button>
`
: ""}
${this._computeIsRunning
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/restart"
>
Restart
</ha-call-api-button>
<ha-call-api-button
class="warning"
.hass=${this.hass}
@ -413,6 +417,13 @@ class HassioAddonInfo extends LitElement {
>
Stop
</ha-call-api-button>
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/restart"
>
Restart
</ha-call-api-button>
`
: html`
<ha-call-api-button
@ -425,7 +436,7 @@ class HassioAddonInfo extends LitElement {
${this._computeShowWebUI
? html`
<a
.href=${this._pathWebui}
href=${this._pathWebui!}
tabindex="-1"
target="_blank"
class="right"
@ -444,6 +455,23 @@ class HassioAddonInfo extends LitElement {
</mwc-button>
`
: ""}
<mwc-button
class=" right warning"
@click=${this._uninstallClicked}
>
Uninstall
</mwc-button>
${this.addon.build
? html`
<ha-call-api-button
class="warning right"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
Rebuild
</ha-call-api-button>
`
: ""}
`
: html`
${!this.addon.available
@ -534,7 +562,7 @@ class HassioAddonInfo extends LitElement {
width: 180px;
display: inline-block;
}
.state iron-icon {
.state ha-icon {
width: 16px;
height: 16px;
color: var(--secondary-text-color);
@ -542,10 +570,10 @@ class HassioAddonInfo extends LitElement {
ha-switch {
display: flex;
}
iron-icon.running {
ha-icon.running {
color: var(--paper-green-400);
}
iron-icon.stopped {
ha-icon.stopped {
color: var(--google-red-300);
}
ha-call-api-button {
@ -590,7 +618,8 @@ class HassioAddonInfo extends LitElement {
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--iron-icon-height: 45px;
--mdc-icon-size: 45px;
--ha-label-badge-padding: 8px 0 0 0;
}
`,
];
@ -776,9 +805,17 @@ class HassioAddonInfo extends LitElement {
}
private async _uninstallClicked(): Promise<void> {
if (!confirm("Are you sure you want to uninstall this add-on?")) {
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to uninstall this add-on?",
confirmText: "uninstall add-on",
dismissText: "no",
});
if (!confirmed) {
return;
}
this._error = undefined;
try {
await uninstallHassioAddon(this.hass, this.addon.slug);

View File

@ -0,0 +1,59 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../../src/types";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { hassioStyle } from "../../resources/hassio-style";
import { haStyle } from "../../../../src/resources/styles";
import "./hassio-addon-logs";
@customElement("hassio-addon-log-tab")
class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
if (!this.addon) {
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">
<hassio-addon-logs
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-logs>
</div>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
.content {
margin: auto;
padding: 8px;
max-width: 1024px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-log-tab": HassioAddonLogDashboard;
}
}

View File

@ -7,27 +7,26 @@ import {
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import {
fetchHassioAddonLogs,
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
} from "../../../../src/data/hassio/addon";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-ansi-to-html";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("hassio-addon-logs")
class HassioAddonLogs extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property() private _error?: string;
@query("#content") private _logContent!: any;
@property() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
@ -36,9 +35,16 @@ class HassioAddonLogs extends LitElement {
protected render(): TemplateResult {
return html`
<paper-card heading="Log">
<h1>${this.addon.name}</h1>
<paper-card>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
<div class="card-content" id="content"></div>
<div class="card-content">
${this._content
? html`<hassio-ansi-to-html
.content=${this._content}
></hassio-ansi-to-html>`
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
</div>
@ -50,17 +56,11 @@ class HassioAddonLogs extends LitElement {
return [
haStyle,
hassioStyle,
ANSI_HTML_STYLE,
css`
:host,
paper-card {
display: block;
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
@ -72,11 +72,7 @@ class HassioAddonLogs extends LitElement {
private async _loadData(): Promise<void> {
this._error = undefined;
try {
const content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
while (this._logContent.lastChild) {
this._logContent.removeChild(this._logContent.lastChild as Node);
}
this._logContent.appendChild(parseTextToColoredPre(content));
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err) {
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
}

View File

@ -1,223 +0,0 @@
import { css } from "lit-element";
interface State {
bold: boolean;
italic: boolean;
underline: boolean;
strikethrough: boolean;
foregroundColor: null | string;
backgroundColor: null | string;
}
export const ANSI_HTML_STYLE = css`
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.strikethrough {
text-decoration: line-through;
}
.underline.strikethrough {
text-decoration: underline line-through;
}
.fg-red {
color: rgb(222, 56, 43);
}
.fg-green {
color: rgb(57, 181, 74);
}
.fg-yellow {
color: rgb(255, 199, 6);
}
.fg-blue {
color: rgb(0, 111, 184);
}
.fg-magenta {
color: rgb(118, 38, 113);
}
.fg-cyan {
color: rgb(44, 181, 233);
}
.fg-white {
color: rgb(204, 204, 204);
}
.bg-black {
background-color: rgb(0, 0, 0);
}
.bg-red {
background-color: rgb(222, 56, 43);
}
.bg-green {
background-color: rgb(57, 181, 74);
}
.bg-yellow {
background-color: rgb(255, 199, 6);
}
.bg-blue {
background-color: rgb(0, 111, 184);
}
.bg-magenta {
background-color: rgb(118, 38, 113);
}
.bg-cyan {
background-color: rgb(44, 181, 233);
}
.bg-white {
background-color: rgb(204, 204, 204);
}
`;
export function parseTextToColoredPre(text) {
const pre = document.createElement("pre");
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
let i = 0;
const state: State = {
bold: false,
italic: false,
underline: false,
strikethrough: false,
foregroundColor: null,
backgroundColor: null,
};
const addSpan = (content) => {
const span = document.createElement("span");
if (state.bold) {
span.classList.add("bold");
}
if (state.italic) {
span.classList.add("italic");
}
if (state.underline) {
span.classList.add("underline");
}
if (state.strikethrough) {
span.classList.add("strikethrough");
}
if (state.foregroundColor !== null) {
span.classList.add(`fg-${state.foregroundColor}`);
}
if (state.backgroundColor !== null) {
span.classList.add(`bg-${state.backgroundColor}`);
}
span.appendChild(document.createTextNode(content));
pre.appendChild(span);
};
/* eslint-disable no-cond-assign */
let match;
// eslint-disable-next-line
while ((match = re.exec(text)) !== null) {
const j = match!.index;
addSpan(text.substring(i, j));
i = j + match[0].length;
if (match[1] === undefined) {
continue;
}
match[1].split(";").forEach((colorCode: string) => {
switch (parseInt(colorCode, 10)) {
case 0:
// reset
state.bold = false;
state.italic = false;
state.underline = false;
state.strikethrough = false;
state.foregroundColor = null;
state.backgroundColor = null;
break;
case 1:
state.bold = true;
break;
case 3:
state.italic = true;
break;
case 4:
state.underline = true;
break;
case 9:
state.strikethrough = true;
break;
case 22:
state.bold = false;
break;
case 23:
state.italic = false;
break;
case 24:
state.underline = false;
break;
case 29:
state.strikethrough = false;
break;
case 30:
// foreground black
state.foregroundColor = null;
break;
case 31:
state.foregroundColor = "red";
break;
case 32:
state.foregroundColor = "green";
break;
case 33:
state.foregroundColor = "yellow";
break;
case 34:
state.foregroundColor = "blue";
break;
case 35:
state.foregroundColor = "magenta";
break;
case 36:
state.foregroundColor = "cyan";
break;
case 37:
state.foregroundColor = "white";
break;
case 39:
// foreground reset
state.foregroundColor = null;
break;
case 40:
state.backgroundColor = "black";
break;
case 41:
state.backgroundColor = "red";
break;
case 42:
state.backgroundColor = "green";
break;
case 43:
state.backgroundColor = "yellow";
break;
case 44:
state.backgroundColor = "blue";
break;
case 45:
state.backgroundColor = "magenta";
break;
case 46:
state.backgroundColor = "cyan";
break;
case 47:
state.backgroundColor = "white";
break;
case 49:
// background reset
state.backgroundColor = null;
break;
}
});
}
addSpan(text.substring(i));
return pre;
}

View File

@ -0,0 +1,253 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
interface State {
bold: boolean;
italic: boolean;
underline: boolean;
strikethrough: boolean;
foregroundColor: null | string;
backgroundColor: null | string;
}
@customElement("hassio-ansi-to-html")
class HassioAnsiToHtml extends LitElement {
@property() public content!: string;
public render(): TemplateResult | void {
return html`${this._parseTextToColoredPre(this.content)}`;
}
static get styles(): CSSResult {
return css`
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.strikethrough {
text-decoration: line-through;
}
.underline.strikethrough {
text-decoration: underline line-through;
}
.fg-red {
color: rgb(222, 56, 43);
}
.fg-green {
color: rgb(57, 181, 74);
}
.fg-yellow {
color: rgb(255, 199, 6);
}
.fg-blue {
color: rgb(0, 111, 184);
}
.fg-magenta {
color: rgb(118, 38, 113);
}
.fg-cyan {
color: rgb(44, 181, 233);
}
.fg-white {
color: rgb(204, 204, 204);
}
.bg-black {
background-color: rgb(0, 0, 0);
}
.bg-red {
background-color: rgb(222, 56, 43);
}
.bg-green {
background-color: rgb(57, 181, 74);
}
.bg-yellow {
background-color: rgb(255, 199, 6);
}
.bg-blue {
background-color: rgb(0, 111, 184);
}
.bg-magenta {
background-color: rgb(118, 38, 113);
}
.bg-cyan {
background-color: rgb(44, 181, 233);
}
.bg-white {
background-color: rgb(204, 204, 204);
}
`;
}
private _parseTextToColoredPre(text) {
const pre = document.createElement("pre");
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
let i = 0;
const state: State = {
bold: false,
italic: false,
underline: false,
strikethrough: false,
foregroundColor: null,
backgroundColor: null,
};
const addSpan = (content) => {
const span = document.createElement("span");
if (state.bold) {
span.classList.add("bold");
}
if (state.italic) {
span.classList.add("italic");
}
if (state.underline) {
span.classList.add("underline");
}
if (state.strikethrough) {
span.classList.add("strikethrough");
}
if (state.foregroundColor !== null) {
span.classList.add(`fg-${state.foregroundColor}`);
}
if (state.backgroundColor !== null) {
span.classList.add(`bg-${state.backgroundColor}`);
}
span.appendChild(document.createTextNode(content));
pre.appendChild(span);
};
/* eslint-disable no-cond-assign */
let match;
// eslint-disable-next-line
while ((match = re.exec(text)) !== null) {
const j = match!.index;
addSpan(text.substring(i, j));
i = j + match[0].length;
if (match[1] === undefined) {
continue;
}
match[1].split(";").forEach((colorCode: string) => {
switch (parseInt(colorCode, 10)) {
case 0:
// reset
state.bold = false;
state.italic = false;
state.underline = false;
state.strikethrough = false;
state.foregroundColor = null;
state.backgroundColor = null;
break;
case 1:
state.bold = true;
break;
case 3:
state.italic = true;
break;
case 4:
state.underline = true;
break;
case 9:
state.strikethrough = true;
break;
case 22:
state.bold = false;
break;
case 23:
state.italic = false;
break;
case 24:
state.underline = false;
break;
case 29:
state.strikethrough = false;
break;
case 30:
// foreground black
state.foregroundColor = null;
break;
case 31:
state.foregroundColor = "red";
break;
case 32:
state.foregroundColor = "green";
break;
case 33:
state.foregroundColor = "yellow";
break;
case 34:
state.foregroundColor = "blue";
break;
case 35:
state.foregroundColor = "magenta";
break;
case 36:
state.foregroundColor = "cyan";
break;
case 37:
state.foregroundColor = "white";
break;
case 39:
// foreground reset
state.foregroundColor = null;
break;
case 40:
state.backgroundColor = "black";
break;
case 41:
state.backgroundColor = "red";
break;
case 42:
state.backgroundColor = "green";
break;
case 43:
state.backgroundColor = "yellow";
break;
case 44:
state.backgroundColor = "blue";
break;
case 45:
state.backgroundColor = "magenta";
break;
case 46:
state.backgroundColor = "cyan";
break;
case 47:
state.backgroundColor = "white";
break;
case 49:
// background reset
state.backgroundColor = null;
break;
}
});
}
addSpan(text.substring(i));
return pre;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-ansi-to-html": HassioAnsiToHtml;
}
}

View File

@ -1,4 +1,3 @@
import "@polymer/iron-icon/iron-icon";
import {
css,
CSSResult,
@ -9,6 +8,7 @@ import {
TemplateResult,
} from "lit-element";
import "../../../src/components/ha-relative-time";
import "../../../src/components/ha-icon";
import { HomeAssistant } from "../../../src/types";
@customElement("hassio-card-content")
@ -48,11 +48,11 @@ class HassioCardContent extends LitElement {
</div>
`
: html`
<iron-icon
<ha-icon
class=${this.iconClass}
.icon=${this.icon}
.title=${this.iconTitle}
></iron-icon>
></ha-icon>
`}
<div>
<div class="title">
@ -78,25 +78,25 @@ class HassioCardContent extends LitElement {
static get styles(): CSSResult {
return css`
iron-icon {
ha-icon {
margin-right: 24px;
margin-left: 8px;
margin-top: 12px;
float: left;
color: var(--secondary-text-color);
}
iron-icon.update {
ha-icon.update {
color: var(--paper-orange-400);
}
iron-icon.running,
iron-icon.installed {
ha-icon.running,
ha-icon.installed {
color: var(--paper-green-400);
}
iron-icon.hassupdate,
iron-icon.snapshot {
ha-icon.hassupdate,
ha-icon.snapshot {
color: var(--paper-item-icon-color);
}
iron-icon.not_available {
ha-icon.not_available {
color: var(--google-red-500);
}
.title {

View File

@ -1,6 +1,6 @@
import "@material/mwc-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-icon";
import "@polymer/paper-input/paper-input";
import {
css,
@ -24,21 +24,17 @@ class HassioSearchInput extends LitElement {
.value=${this.filter}
@value-changed=${this._filterInputChanged}
>
<iron-icon
icon="hassio:magnify"
slot="prefix"
class="prefix"
></iron-icon>
<ha-icon icon="hassio:magnify" slot="prefix" class="prefix"></ha-icon>
${this.filter &&
html`
<paper-icon-button
<ha-icon-button
slot="suffix"
class="suffix"
@click=${this._clearSearch}
icon="hassio:close"
alt="Clear"
title="Clear"
></paper-icon-button>
></ha-icon-button>
`}
</paper-input>
</div>
@ -71,6 +67,9 @@ class HassioSearchInput extends LitElement {
.prefix {
margin: 8px;
}
ha-icon {
color: var(--primary-text-color);
}
`;
}
}

View File

@ -96,7 +96,7 @@ class HassioAddons extends LitElement {
}
private _addonTapped(ev: any): void {
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}/info`);
}
private _openStore(): void {

View File

@ -13,34 +13,51 @@ import {
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { HomeAssistant, Route } from "../../../src/types";
import "../../../src/layouts/hass-tabs-subpage";
import "./hassio-addons";
import "./hassio-update";
import { supervisorTabs } from "../hassio-panel";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property({ type: Boolean }) public narrow!: boolean;
@property() public hassInfo!: HassioHomeAssistantInfo;
@property({ attribute: false }) public route!: Route;
@property() public hassOsInfo!: HassioHassOSInfo;
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult {
return html`
<div class="content">
<hassio-update
.hass=${this.hass}
.hassInfo=${this.hassInfo}
.supervisorInfo=${this.supervisorInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-update>
<hassio-addons
.hass=${this.hass}
.addons=${this.supervisorInfo.addons}
></hassio-addons>
</div>
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
>
<span slot="header">Dashboard</span>
<div class="content">
<hassio-update
.hass=${this.hass}
.hassInfo=${this.hassInfo}
.supervisorInfo=${this.supervisorInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-update>
<hassio-addons
.hass=${this.hass}
.addons=${this.supervisorInfo.addons}
></hassio-addons>
</div>
</hass-tabs-subpage>
`;
}

View File

@ -1,5 +1,4 @@
import "@material/mwc-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import {
css,
@ -17,6 +16,7 @@ import {
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import { haStyle } from "../../../src/resources/styles";
import "../../../src/components/ha-icon";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
@ -112,7 +112,7 @@ export class HassioUpdate extends LitElement {
${icon
? html`
<div class="icon">
<iron-icon .icon=${icon}></iron-icon>
<ha-icon .icon=${icon}></ha-icon>
</div>
`
: ""}
@ -158,15 +158,16 @@ export class HassioUpdate extends LitElement {
hassioStyle,
css`
.icon {
--iron-icon-height: 48px;
--iron-icon-width: 48px;
--mdc-icon-size: 48px;
float: right;
margin: 0 0 2px 10px;
color: var(--primary-text-color);
}
.update-heading {
font-size: var(--paper-font-subhead_-_font-size);
font-weight: 500;
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
.warning {
color: var(--secondary-text-color);

View File

@ -1,7 +1,7 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { PaperDialogElement } from "@polymer/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../../src/components/ha-icon-button";
import {
css,
CSSResult,
@ -36,10 +36,10 @@ class HassioMarkdownDialog extends LitElement {
return html`
<ha-paper-dialog id="dialog" with-backdrop="">
<app-toolbar>
<paper-icon-button
<ha-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
></ha-icon-button>
<div main-title="">${this.title}</div>
</app-toolbar>
<paper-dialog-scrollable>

View File

@ -1,10 +1,10 @@
import "@material/mwc-button";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/iron-icon/iron-icon";
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import { PaperDialogElement } from "@polymer/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-icon";
import "@polymer/paper-input/paper-input";
import {
css,
@ -119,10 +119,10 @@ class HassioSnapshotDialog extends LitElement {
.on-iron-overlay-closed=${this._dialogClosed}
>
<app-toolbar>
<paper-icon-button
<ha-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
></ha-icon-button>
<div main-title="">${this._computeName}</div>
</app-toolbar>
<div class="details">
@ -200,13 +200,13 @@ class HassioSnapshotDialog extends LitElement {
<ul class="buttons">
<li>
<mwc-button @click=${this._downloadClicked}>
<iron-icon icon="hassio:download" class="icon"></iron-icon>
<ha-icon icon="hassio:download" class="icon"></ha-icon>
Download Snapshot
</mwc-button>
</li>
<li>
<mwc-button @click=${this._partialRestoreClicked}>
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
<ha-icon icon="hassio:history" class="icon"> </ha-icon>
Restore Selected
</mwc-button>
</li>
@ -214,7 +214,7 @@ class HassioSnapshotDialog extends LitElement {
? html`
<li>
<mwc-button @click=${this._fullRestoreClicked}>
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
<ha-icon icon="hassio:history" class="icon"> </ha-icon>
Wipe &amp; restore
</mwc-button>
</li>
@ -222,7 +222,7 @@ class HassioSnapshotDialog extends LitElement {
: ""}
<li>
<mwc-button @click=${this._deleteClicked}>
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
<ha-icon icon="hassio:delete" class="icon warning"> </ha-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
</li>

View File

@ -0,0 +1,33 @@
import type { LitElement } from "lit-element";
import {
HassioAddonDetails,
restartHassioAddon,
} from "../../../src/data/hassio/addon";
import { HomeAssistant } from "../../../src/types";
import {
showConfirmationDialog,
showAlertDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
export const suggestAddonRestart = async (
element: LitElement,
hass: HomeAssistant,
addon: HassioAddonDetails
): Promise<void> => {
const confirmed = await showConfirmationDialog(element, {
title: addon.name,
text: "Do you want to restart the add-on with your changes?",
confirmText: "restart add-on",
dismissText: "no",
});
if (confirmed) {
try {
await restartHassioAddon(hass, addon.slug);
} catch (err) {
showAlertDialog(element, {
title: "Failed to restart",
text: err.body.message,
});
}
}
};

View File

@ -2,8 +2,6 @@ window.loadES5Adapter().then(() => {
// eslint-disable-next-line
import(/* webpackChunkName: "roboto" */ "../../src/resources/roboto");
// eslint-disable-next-line
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons");
// eslint-disable-next-line
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
});

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button";
import "../../src/components/ha-icon-button";
import { PolymerElement } from "@polymer/polymer";
import { customElement, property, PropertyValues } from "lit-element";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
@ -32,13 +32,13 @@ import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import "../../src/resources/ha-style";
import { HomeAssistant } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-pages-with-tabs";
import "./hassio-panel";
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
// The register callback of the IronA11yKeysBehavior inside ha-icon-button
// is not called, causing _keyBindings to be uninitiliazed for ha-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 = {};
customElements.get("ha-icon-button").prototype._keyBindings = {};
@customElement("hassio-main")
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
@ -55,17 +55,17 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
showLoading: true,
routes: {
dashboard: {
tag: "hassio-pages-with-tabs",
tag: "hassio-panel",
cache: true,
},
snapshots: "dashboard",
store: "dashboard",
system: "dashboard",
addon: {
tag: "hassio-addon-view",
tag: "hassio-addon-dashboard",
load: () =>
import(
/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
),
},
ingress: {
@ -132,8 +132,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
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;
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
if ("setProperties" in el) {
// As long as we have Polymer pages
@ -205,7 +204,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
await awaitAlert(
{
text: "Unable to fetch add-on info to start Ingress",
title: "Hass.io",
title: "Supervisor",
},
() => history.back()
);
@ -231,7 +230,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
text: "Add-on is not running. Please start it first",
title: addon.name,
},
() => navigate(this, `/hassio/addon/${addon.slug}`, true)
() => navigate(this, `/hassio/addon/${addon.slug}/info`, true)
);
return;

View File

@ -1,141 +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 {
css,
CSSResultArray,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import scrollToTarget from "../../src/common/dom/scroll-to-target";
import { navigate } from "../../src/common/navigate";
import "../../src/components/ha-menu-button";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import "../../src/resources/ha-style";
import { haStyle } from "../../src/resources/styles";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-tabs-router";
const HAS_REFRESH_BUTTON = ["store", "snapshots"];
@customElement("hassio-pages-with-tabs")
class HassioPagesWithTabs extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public route!: Route;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() public hostInfo!: HassioHostInfo;
@property() public hassInfo!: HassioHomeAssistantInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult {
const page = this._page;
return html`
<app-header-layout has-scrolling-region>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
hassio
></ha-menu-button>
<div main-title>Supervisor</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}
.hassOsInfo=${this.hassOsInfo}
></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: var(--text-primary-color, #fff);
text-transform: uppercase;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-pages-with-tabs": HassioPagesWithTabs;
}
}

View File

@ -1,4 +1,3 @@
import { PolymerElement } from "@polymer/polymer";
import { customElement, property } from "lit-element";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
@ -9,7 +8,7 @@ import {
HassRouterPage,
RouterOptions,
} from "../../src/layouts/hass-router-page";
import { HomeAssistant } from "../../src/types";
import { HomeAssistant, Route } from "../../src/types";
import "./addon-store/hassio-addon-store";
// Don't codesplit it, that way the dashboard always loads fast.
import "./dashboard/hassio-dashboard";
@ -17,29 +16,33 @@ import "./dashboard/hassio-dashboard";
import "./snapshots/hassio-snapshots";
import "./system/hassio-system";
@customElement("hassio-tabs-router")
class HassioTabsRouter extends HassRouterPage {
@property() public hass!: HomeAssistant;
@customElement("hassio-panel-router")
class HassioPanelRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public supervisorInfo: HassioSupervisorInfo;
@property({ attribute: false }) public route!: Route;
@property() public hostInfo: HassioHostInfo;
@property({ type: Boolean }) public narrow!: boolean;
@property() public hassInfo: HassioHomeAssistantInfo;
@property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
@property({ attribute: false }) public hostInfo: HassioHostInfo;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected routerOptions: RouterOptions = {
routes: {
dashboard: {
tag: "hassio-dashboard",
},
snapshots: {
tag: "hassio-snapshots",
},
store: {
tag: "hassio-addon-store",
},
snapshots: {
tag: "hassio-snapshots",
},
system: {
tag: "hassio-system",
},
@ -47,27 +50,18 @@ class HassioTabsRouter extends HassRouterPage {
};
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,
hassOsInfo: this.hassOsInfo,
});
} else {
el.hass = this.hass;
el.supervisorInfo = this.supervisorInfo;
el.hostInfo = this.hostInfo;
el.hassInfo = this.hassInfo;
el.hassOsInfo = this.hassOsInfo;
}
el.hass = this.hass;
el.route = this.route;
el.narrow = this.narrow;
el.supervisorInfo = this.supervisorInfo;
el.hostInfo = this.hostInfo;
el.hassInfo = this.hassInfo;
el.hassOsInfo = this.hassOsInfo;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-tabs-router": HassioTabsRouter;
"hassio-panel-router": HassioPanelRouter;
}
}

View File

@ -0,0 +1,77 @@
import {
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import "../../src/resources/ha-style";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
export const supervisorTabs: PageNavigation[] = [
{
name: "Dashboard",
path: `/hassio/dashboard`,
icon: "hassio:view-dashboard",
},
{
name: "Add-on store",
path: `/hassio/store`,
icon: "hassio:store",
},
{
name: "Snapshots",
path: `/hassio/snapshots`,
icon: "hassio:backup-restore",
},
{
name: "System",
path: `/hassio/system`,
icon: "hassio:cogs",
},
];
@customElement("hassio-panel")
class HassioPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hostInfo!: HassioHostInfo;
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult {
return html`
<hassio-panel-router
.route=${this.route}
.hass=${this.hass}
.narrow=${this.narrow}
.supervisorInfo=${this.supervisorInfo}
.hostInfo=${this.hostInfo}
.hassInfo=${this.hassInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-panel-router>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-panel": HassioPanel;
}
}

View File

@ -86,7 +86,7 @@ class HassioIngressView extends LitElement {
height: 100%;
border: 0;
}
paper-icon-button {
ha-icon-button {
color: var(--text-primary-color);
}
`;

View File

@ -1,7 +0,0 @@
import "../../../src/components/ha-iconset-svg";
import iconSetContent from "../../hassio-icons.html";
const documentContainer = document.createElement("template");
documentContainer.setAttribute("style", "display: none;");
documentContainer.innerHTML = iconSetContent;
document.head.appendChild(documentContainer.content);

View File

@ -4,8 +4,12 @@ export const hassioStyle = css`
.content {
margin: 8px;
}
h1 {
h1,
.description,
.card-content {
color: var(--primary-text-color);
}
h1 {
font-size: 2em;
margin-bottom: 8px;
font-family: var(--paper-font-headline_-_font-family);

View File

@ -30,11 +30,14 @@ import {
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { HomeAssistant, Route } from "../../../src/types";
import "../../../src/layouts/hass-tabs-subpage";
import "../components/hassio-card-content";
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
import { hassioStyle } from "../resources/hassio-style";
import { supervisorTabs } from "../hassio-panel";
interface CheckboxItem {
slug: string;
name: string;
@ -43,9 +46,13 @@ interface CheckboxItem {
@customElement("hassio-snapshots")
class HassioSnapshots extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@property() private _snapshotName = "";
@ -81,135 +88,153 @@ class HassioSnapshots extends LitElement {
protected render(): TemplateResult {
return html`
<div class="content">
<h1>
Create snapshot
</h1>
<p class="description">
Snapshots allow you to easily backup and restore all data of your Home
Assistant instance.
</p>
<div class="card-group">
<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=${addon.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>
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
>
<span slot="header">Snapshots</span>
<h1>Available snapshots</h1>
<div class="card-group">
${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}
>
<ha-icon-button
icon="hassio:reload"
slot="toolbar-icon"
aria-label="Reload snapshots"
@click=${this.refreshData}
></ha-icon-button>
<div class="content">
<h1>
Create snapshot
</h1>
<p class="description">
Snapshots allow you to easily backup and restore all data of your
Home Assistant instance.
</p>
<div class="card-group">
<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=${addon.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>
<h1>Available snapshots</h1>
<div class="card-group">
${this._snapshots === undefined
? undefined
: this._snapshots.length === 0
? html`
<paper-card>
<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>
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>
</div>
</hass-tabs-subpage>
`;
}

View File

@ -12,13 +12,23 @@ import {
import "../../../src/components/buttons/ha-call-api-button";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo as HassioHostInfoType,
rebootHost,
shutdownHost,
updateOS,
changeHostOptions,
} from "../../../src/data/hassio/host";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../resources/hassio-style";
import {
showConfirmationDialog,
showAlertDialog,
showPromptDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
@customElement("hassio-host-info")
class HassioHostInfo extends LitElement {
@ -76,21 +86,15 @@ class HassioHostInfo extends LitElement {
<div class="card-actions">
${this.hostInfo.features.includes("reboot")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/host/reboot"
>Reboot</ha-call-api-button
<mwc-button class="warning" @click=${this._rebootHost}
>Reboot</mwc-button
>
`
: ""}
${this.hostInfo.features.includes("shutdown")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/host/shutdown"
>Shutdown</ha-call-api-button
<mwc-button class="warning" @click=${this._shutdownHost}
>Shutdown</mwc-button
>
`
: ""}
@ -106,11 +110,7 @@ class HassioHostInfo extends LitElement {
`
: ""}
${this.hostInfo.version !== this.hostInfo.version_latest
? html`
<ha-call-api-button .hass=${this.hass} path="hassio/os/update"
>Update</ha-call-api-button
>
`
? html` <mwc-button @click=${this._updateOS}>Update</mwc-button> `
: ""}
</div>
</paper-card>
@ -189,6 +189,72 @@ class HassioHostInfo extends LitElement {
}
}
private async _rebootHost(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Reboot",
text: "Are you sure you want to reboot the host?",
confirmText: "reboot host",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
await rebootHost(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reboot",
text: err.body.message,
});
}
}
private async _shutdownHost(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
confirmText: "shutdown host",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
await shutdownHost(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to shutdown",
text: err.body.message,
});
}
}
private async _updateOS(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Update",
text: "Are you sure you want to update the OS?",
confirmText: "update os",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
await updateOS(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update",
text: err.body.message,
});
}
}
private _objectToMarkdown(obj, indent = ""): string {
let data = "";
Object.keys(obj).forEach((key) => {
@ -210,11 +276,25 @@ class HassioHostInfo extends LitElement {
return data;
}
private _changeHostnameClicked(): void {
const curHostname = this.hostInfo.hostname;
const hostname = prompt("Please enter a new hostname:", curHostname);
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.hostInfo.hostname;
const hostname = await showPromptDialog(this, {
title: "Change hostname",
inputLabel: "Please enter a new hostname:",
inputType: "string",
defaultValue: curHostname,
});
if (hostname && hostname !== curHostname) {
this.hass.callApi("POST", "hassio/host/options", { hostname });
try {
await changeHostOptions(this.hass, { hostname });
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
text: err.body.message,
});
}
}
}
}

View File

@ -19,6 +19,7 @@ import {
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@ -142,17 +143,30 @@ class HassioSupervisorInfo extends LitElement {
}
private async _joinBeta() {
if (
!confirm(`WARNING:
Beta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
This includes beta releases for:
- Home Assistant (Release Candidates)
- Hass.io supervisor
- Host system`)
) {
if (!confirmed) {
return;
}
try {
const data: SupervisorOptions = { channel: "beta" };
await setSupervisorOption(this.hass, data);

View File

@ -1,4 +1,7 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-card/paper-card";
import {
css,
@ -7,22 +10,58 @@ import {
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { fetchSupervisorLogs } from "../../../src/data/hassio/supervisor";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import "../components/hassio-ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
import "../../../src/layouts/loading-screen";
interface LogProvider {
key: string;
name: string;
}
const logProviders: LogProvider[] = [
{
key: "supervisor",
name: "Supervisor",
},
{
key: "core",
name: "Core",
},
{
key: "host",
name: "Host",
},
{
key: "dns",
name: "DNS",
},
{
key: "audio",
name: "Audio",
},
{
key: "multicast",
name: "Multicast",
},
];
@customElement("hassio-supervisor-log")
class HassioSupervisorLog extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() private _error?: string;
@query("#content") private _logContent!: HTMLDivElement;
@property() private _selectedLogProvider = "supervisor";
@property() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
@ -33,7 +72,36 @@ class HassioSupervisorLog extends LitElement {
return html`
<paper-card>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
<div class="card-content" id="content"></div>
${this.hass.userData?.showAdvanced
? html`
<paper-dropdown-menu
label="Log provider"
@iron-select=${this._setLogProvider}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="provider"
.selected=${this._selectedLogProvider}
>
${logProviders.map((provider) => {
return html`
<paper-item provider=${provider.key}
>${provider.name}</paper-item
>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
`
: ""}
<div class="card-content" id="content">
${this._content
? html`<hassio-ansi-to-html
.content=${this._content}
></hassio-ansi-to-html>`
: html`<loading-screen></loading-screen>`}
</div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
</div>
@ -45,7 +113,6 @@ class HassioSupervisorLog extends LitElement {
return [
haStyle,
hassioStyle,
ANSI_HTML_STYLE,
css`
paper-card {
width: 100%;
@ -53,22 +120,36 @@ class HassioSupervisorLog extends LitElement {
pre {
white-space: pre-wrap;
}
paper-dropdown-menu {
padding: 0 2%;
width: 96%;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
.card-content {
padding-top: 0px;
}
`,
];
}
private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider");
this._selectedLogProvider = provider;
await this._loadData();
}
private async _loadData(): Promise<void> {
this._error = undefined;
this._content = undefined;
try {
const content = await fetchSupervisorLogs(this.hass);
while (this._logContent.lastChild) {
this._logContent.removeChild(this._logContent.lastChild as Node);
}
this._logContent.appendChild(parseTextToColoredPre(content));
this._content = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${
err.body?.message || err

View File

@ -14,15 +14,22 @@ import {
} from "../../../src/data/hassio/host";
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import "./hassio-host-info";
import "./hassio-supervisor-info";
import "./hassio-supervisor-log";
import { supervisorTabs } from "../hassio-panel";
@customElement("hassio-system")
class HassioSystem extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property() public supervisorInfo!: HassioSupervisorInfo;
@ -32,22 +39,32 @@ class HassioSystem extends LitElement {
public render(): TemplateResult | void {
return html`
<div class="content">
<h1>Information</h1>
<div class="card-group">
<hassio-supervisor-info
.hass=${this.hass}
.supervisorInfo=${this.supervisorInfo}
></hassio-supervisor-info>
<hassio-host-info
.hass=${this.hass}
.hostInfo=${this.hostInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info>
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
>
<span slot="header">System</span>
<div class="content">
<h1>Information</h1>
<div class="card-group">
<hassio-supervisor-info
.hass=${this.hass}
.supervisorInfo=${this.supervisorInfo}
></hassio-supervisor-info>
<hassio-host-info
.hass=${this.hass}
.hostInfo=${this.hostInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info>
</div>
<h1>System log</h1>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
</div>
<h1>System log</h1>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
</div>
</hass-tabs-subpage>
`;
}

View File

@ -29,18 +29,18 @@
"@material/mwc-checkbox": "^0.13.0",
"@material/mwc-dialog": "^0.13.0",
"@material/mwc-fab": "^0.13.0",
"@material/mwc-icon-button": "^0.13.0",
"@material/mwc-ripple": "^0.13.0",
"@material/mwc-switch": "^0.13.0",
"@mdi/js": "4.9.95",
"@mdi/svg": "4.9.95",
"@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",
"@polymer/font-roboto": "^3.0.2",
"@polymer/iron-autogrow-textarea": "^3.0.1",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
"@polymer/iron-iconset-svg": "^3.0.1",
"@polymer/iron-image": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
"@polymer/iron-label": "^3.0.1",
@ -56,7 +56,6 @@
"@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-drawer-panel": "^3.0.1",
"@polymer/paper-dropdown-menu": "^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",
@ -74,7 +73,6 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.3.7",
"@types/resize-observer-browser": "^0.1.3",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@webcomponents/shadycss": "^1.9.0",
@ -91,7 +89,8 @@
"google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "5.0.0",
"intl-messageformat": "^2.2.0",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1",
"leaflet": "^1.4.0",
"leaflet-draw": "^1.0.4",
@ -136,6 +135,7 @@
"@types/leaflet-draw": "^1.0.1",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"@types/resize-observer-browser": "^0.1.3",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^2.28.0",
"@typescript-eslint/parser": "^2.28.0",
@ -171,6 +171,7 @@
"map-stream": "^0.0.7",
"merge-stream": "^1.0.1",
"mocha": "^6.0.2",
"object-hash": "^2.0.3",
"parse5": "^5.1.0",
"prettier": "^2.0.4",
"raw-loader": "^2.0.0",
@ -199,12 +200,15 @@
"lit-html": "^1.1.2",
"@material/button": "^5.0.0",
"@material/checkbox": "^5.0.0",
"@material/density": "^5.0.0",
"@material/dialog": "^5.0.0",
"@material/fab": "^5.0.0",
"@material/feature-targeting": "^5.0.0",
"@material/switch": "^5.0.0",
"@material/ripple": "^5.0.0",
"@material/dom": "^5.0.0",
"@material/touch-target": "^5.0.0"
"@material/touch-target": "^5.0.0",
"@material/theme": "^5.0.0"
},
"main": "src/home-assistant.js",
"husky": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 B

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 B

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 B

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 818 B

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 B

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 774 B

View File

@ -6,6 +6,6 @@ set -e
cd "$(dirname "$0")/.."
STATS=1 NODE_ENV=production webpack --profile --json > compilation-stats.json
STATS=1 NODE_ENV=production ./node_modules/.bin/webpack --profile --json > compilation-stats.json
npx webpack-bundle-analyzer compilation-stats.json hass_frontend/frontend_latest
rm compilation-stats.json

View File

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

View File

@ -103,6 +103,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
${this.localize("ui.panel.page-authorize.abort_intro")}:
<ha-markdown
allowsvg
breaks
.content=${this.localize(
`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${step.reason}`
)}
@ -113,6 +114,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
${this._computeStepDescription(step)
? html`
<ha-markdown
breaks
.content=${this._computeStepDescription(step)}
></ha-markdown>
`

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
import {
css,
@ -50,14 +50,14 @@ class SearchInput extends LitElement {
<ha-icon icon="hass:magnify" slot="prefix" class="prefix"></ha-icon>
${this.filter &&
html`
<paper-icon-button
<ha-icon-button
slot="suffix"
class="suffix"
@click=${this._clearSearch}
icon="hass:close"
alt="Clear"
title="Clear"
></paper-icon-button>
></ha-icon-button>
`}
</paper-input>
`;

View File

@ -1,4 +1,4 @@
import IntlMessageFormat from "intl-messageformat/src/main";
import IntlMessageFormat from "intl-messageformat";
import { Resources } from "../../types";
export type LocalizeFunc = (key: string, ...args: any[]) => string;
@ -59,7 +59,7 @@ export const computeLocalize = (
let translatedMessage = cache._localizationCache[messageKey];
if (!translatedMessage) {
translatedMessage = new (IntlMessageFormat as any)(
translatedMessage = new IntlMessageFormat(
translatedValue,
language,
formats

View File

@ -1,5 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "../ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@ -61,7 +61,7 @@ const rowRenderer = (
margin: -10px 0;
padding: 0;
}
paper-icon-button {
ha-icon-button {
float: right;
}
.devices {
@ -325,7 +325,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
>
${this.value
? html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
@ -336,12 +336,12 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
no-ripple
>
Clear
</paper-icon-button>
</ha-icon-button>
`
: ""}
${areas.length > 0
? html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
@ -350,7 +350,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
</ha-icon-button>
`
: ""}
</paper-input>
@ -408,7 +408,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
paper-input > ha-icon-button {
width: 24px;
height: 24px;
padding: 2px;

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@ -247,7 +247,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
>
${this.value
? html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
@ -258,12 +258,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
no-ripple
>
Clear
</paper-icon-button>
</ha-icon-button>
`
: ""}
${devices.length > 0
? html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
@ -272,7 +272,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
</ha-icon-button>
`
: ""}
</paper-input>
@ -311,9 +311,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
width: 24px;
height: 24px;
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}

View File

@ -1,4 +1,3 @@
import "@polymer/paper-icon-button/paper-icon-button-light";
import {
customElement,
html,

View File

@ -1,6 +1,6 @@
/* eslint-plugin-disable lit */
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
import "@polymer/paper-icon-button/paper-icon-button";
import "../ha-icon-button";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
@ -107,7 +107,7 @@ class HaChartBase extends mixinBehaviors(
margin-right: inherit;
margin-left: 4px;
}
paper-icon-button {
ha-icon-button {
color: var(--secondary-text-color);
}
</style>

View File

@ -1,4 +1,3 @@
import "@polymer/paper-icon-button/paper-icon-button-light";
import type { HassEntity } from "home-assistant-js-websocket";
import {
customElement,

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
@ -192,7 +192,7 @@ class HaEntityPicker extends LitElement {
>
${this.value
? html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
@ -203,11 +203,11 @@ class HaEntityPicker extends LitElement {
no-ripple
>
Clear
</paper-icon-button>
</ha-icon-button>
`
: ""}
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.show_entities"
)}
@ -216,7 +216,7 @@ class HaEntityPicker extends LitElement {
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
</ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`;
@ -252,9 +252,8 @@ class HaEntityPicker extends LitElement {
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
width: 24px;
height: 24px;
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
}

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../ha-icon-button";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
@ -37,20 +37,20 @@ class HaEntityToggle extends LitElement {
if (this.stateObj.attributes.assumed_state) {
return html`
<paper-icon-button
<ha-icon-button
aria-label=${`Turn ${computeStateName(this.stateObj)} off`}
icon="hass:flash-off"
.disabled=${this.stateObj.state === UNAVAILABLE}
@click=${this._turnOff}
?state-active=${!this._isOn}
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
aria-label=${`Turn ${computeStateName(this.stateObj)} on`}
icon="hass:flash"
.disabled=${this.stateObj.state === UNAVAILABLE}
@click=${this._turnOn}
?state-active=${this._isOn}
></paper-icon-button>
></ha-icon-button>
`;
}
@ -144,15 +144,12 @@ class HaEntityToggle extends LitElement {
white-space: nowrap;
min-width: 38px;
}
paper-icon-button {
color: var(
--paper-icon-button-inactive-color,
var(--primary-text-color)
);
ha-icon-button {
color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
transition: color 0.5s;
}
paper-icon-button[state-active] {
color: var(--paper-icon-button-active-color, var(--primary-color));
ha-icon-button[state-active] {
color: var(--ha-icon-button-active-color, var(--primary-color));
}
ha-switch {
padding: 13px 5px;

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "./ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@ -117,7 +117,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
>
${this.value
? html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.area-picker.clear"
)}
@ -128,12 +128,12 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
no-ripple
>
${this.hass.localize("ui.components.area-picker.clear")}
</paper-icon-button>
</ha-icon-button>
`
: ""}
${this._areas.length > 0
? html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.area-picker.show_areas"
)}
@ -142,7 +142,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
${this.hass.localize("ui.components.area-picker.toggle")}
</paper-icon-button>
</ha-icon-button>
`
: ""}
</paper-input>
@ -214,9 +214,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
width: 24px;
height: 24px;
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}

View File

@ -1,5 +1,5 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-icon-button/paper-icon-button";
import "./ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -31,9 +31,8 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
font-size: 200%;
text-align: right;
}
paper-icon-button {
height: 48px;
width: 48px;
ha-icon-button {
--mdc-icon-size: 32px;
}
</style>
@ -41,16 +40,16 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
<div id="target_temperature">[[value]] [[units]]</div>
<div class="control-buttons">
<div>
<paper-icon-button
<ha-icon-button
icon="hass:chevron-up"
on-click="incrementValue"
></paper-icon-button>
></ha-icon-button>
</div>
<div>
<paper-icon-button
<ha-icon-button
icon="hass:chevron-down"
on-click="decrementValue"
></paper-icon-button>
></ha-icon-button>
</div>
</div>
`;

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "./ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag";
@ -11,7 +11,7 @@ class HaComboBox extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
paper-input > paper-icon-button {
paper-input > ha-icon-button {
width: 24px;
height: 24px;
padding: 2px;
@ -36,19 +36,19 @@ class HaComboBox extends EventsMixin(PolymerElement) {
class="input"
value="[[value]]"
>
<paper-icon-button
<ha-icon-button
slot="suffix"
class="clear-button"
icon="hass:close"
hidden$="[[!value]]"
>Clear</paper-icon-button
>Clear</ha-icon-button
>
<paper-icon-button
<ha-icon-button
slot="suffix"
class="toggle-button"
icon="[[_computeToggleIcon(opened)]]"
hidden$="[[!items.length]]"
>Toggle</paper-icon-button
>Toggle</ha-icon-button
>
</paper-input>
<template>

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "./ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -18,27 +18,27 @@ class HaCoverControls extends PolymerElement {
</style>
<div class="state">
<paper-icon-button
<ha-icon-button
aria-label="Open cover"
icon="[[computeOpenIcon(stateObj)]]"
on-click="onOpenTap"
invisible$="[[!entityObj.supportsOpen]]"
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
aria-label="Stop the cover from moving"
icon="hass:stop"
on-click="onStopTap"
invisible$="[[!entityObj.supportsStop]]"
disabled="[[computStopDisabled(stateObj)]]"
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
aria-label="Close cover"
icon="[[computeCloseIcon(stateObj)]]"
on-click="onCloseTap"
invisible$="[[!entityObj.supportsClose]]"
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
></paper-icon-button>
></ha-icon-button>
</div>
`;
}

View File

@ -1,5 +1,5 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-icon-button/paper-icon-button";
import "./ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -18,30 +18,30 @@ class HaCoverTiltControls extends PolymerElement {
visibility: hidden !important;
}
</style>
<paper-icon-button
<ha-icon-button
aria-label="Open cover tilt"
icon="hass:arrow-top-right"
on-click="onOpenTiltTap"
title="Open tilt"
invisible$="[[!entityObj.supportsOpenTilt]]"
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
aria-label="Stop cover from moving"
icon="hass:stop"
on-click="onStopTiltTap"
invisible$="[[!entityObj.supportsStopTilt]]"
disabled="[[computStopDisabled(stateObj)]]"
title="Stop tilt"
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
aria-label="Close cover tilt"
icon="hass:arrow-bottom-left"
on-click="onCloseTiltTap"
title="Close tilt"
invisible$="[[!entityObj.supportsCloseTilt]]"
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
></paper-icon-button>
></ha-icon-button>
`;
}

View File

@ -1,7 +1,7 @@
import "@material/mwc-dialog";
import type { Dialog } from "@material/mwc-dialog";
import { style } from "@material/mwc-dialog/mwc-dialog-css";
import "@polymer/paper-icon-button/paper-icon-button";
import "./ha-icon-button";
import { css, CSSResult, customElement, html } from "lit-element";
import type { Constructor, HomeAssistant } from "../types";
@ -9,12 +9,12 @@ const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
${title}
<paper-icon-button
<ha-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")}
icon="hass:close"
dialogAction="close"
class="close_button"
></paper-icon-button>
></ha-icon-button>
`;
@customElement("ha-dialog")

View File

@ -77,7 +77,12 @@ export class HaFormInteger extends LitElement implements HaFormElement {
}
private get _value() {
return this.data || this.schema.default || 0;
return (
this.data ||
this.schema.description?.suggested_value ||
this.schema.default ||
0
);
}
private _handleCheckboxChange(ev: Event) {

View File

@ -1,4 +1,3 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
@ -21,6 +20,7 @@ import {
HaFormMultiSelectData,
HaFormMultiSelectSchema,
} from "./ha-form";
import "../ha-icon";
@customElement("ha-form-multi_select")
export class HaFormMultiSelect extends LitElement implements HaFormElement {
@ -64,11 +64,11 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
input-aria-haspopup="listbox"
autocomplete="off"
>
<iron-icon
<ha-icon
icon="paper-dropdown-menu:arrow-drop-down"
suffix
slot="suffix"
></iron-icon>
></ha-icon>
</paper-input>
</div>
<paper-listbox

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../ha-icon-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
@ -47,7 +47,7 @@ export class HaFormString extends LitElement implements HaFormElement {
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
>
<paper-icon-button
<ha-icon-button
toggles
.active=${this._unmaskedPassword}
slot="suffix"
@ -56,7 +56,7 @@ export class HaFormString extends LitElement implements HaFormElement {
title="Click to toggle between masked and clear password"
@click=${this._toggleUnmaskedPassword}
>
</paper-icon-button>
</ha-icon-button>
</paper-input>
`
: html`

View File

@ -30,7 +30,7 @@ export interface HaFormBaseSchema {
default?: HaFormData;
required?: boolean;
optional?: boolean;
description?: { suffix?: string };
description?: { suffix?: string; suggested_value?: HaFormData };
}
export interface HaFormIntegerSchema extends HaFormBaseSchema {

View File

@ -0,0 +1,23 @@
import { HaIconButton } from "./ha-icon-button";
export class HaIconButtonArrowNext extends HaIconButton {
public connectedCallback() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this.icon =
window.getComputedStyle(this).direction === "ltr"
? "hass:arrow-right"
: "hass:arrow-left";
}, 100);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon-button-arrow-next": HaIconButtonArrowNext;
}
}
customElements.define("ha-icon-button-arrow-next", HaIconButtonArrowNext);

View File

@ -0,0 +1,23 @@
import { HaIconButton } from "./ha-icon-button";
export class HaIconButtonArrowPrev extends HaIconButton {
public connectedCallback() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this.icon =
window.getComputedStyle(this).direction === "ltr"
? "hass:arrow-left"
: "hass:arrow-right";
}, 100);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon-button-arrow-prev": HaIconButtonArrowPrev;
}
}
customElements.define("ha-icon-button-arrow-prev", HaIconButtonArrowPrev);

View File

@ -0,0 +1,23 @@
import { HaIconButton } from "./ha-icon-button";
export class HaIconButtonNext extends HaIconButton {
public connectedCallback() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this.icon =
window.getComputedStyle(this).direction === "ltr"
? "hass:chevron-right"
: "hass:chevron-left";
}, 100);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon-button-next": HaIconButtonNext;
}
}
customElements.define("ha-icon-button-next", HaIconButtonNext);

View File

@ -0,0 +1,23 @@
import { HaIconButton } from "./ha-icon-button";
export class HaIconButtonPrev extends HaIconButton {
public connectedCallback() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this.icon =
window.getComputedStyle(this).direction === "ltr"
? "hass:chevron-left"
: "hass:chevron-right";
}, 100);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon-button-prev": HaIconButtonPrev;
}
}
customElements.define("ha-icon-button-prev", HaIconButtonPrev);

View File

@ -0,0 +1,56 @@
import "@material/mwc-icon-button";
import {
customElement,
html,
TemplateResult,
property,
LitElement,
CSSResult,
css,
} from "lit-element";
import "./ha-icon";
@customElement("ha-icon-button")
export class HaIconButton extends LitElement {
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: String }) icon = "";
@property({ type: String }) label = "";
protected render(): TemplateResult {
return html`
<mwc-icon-button
.label=${this.label || this.icon}
?disabled=${this.disabled}
@click=${this._handleClick}
>
<ha-icon .icon=${this.icon}></ha-icon>
</mwc-icon-button>
`;
}
private _handleClick(ev) {
if (this.disabled) {
ev.stopPropagation();
}
}
static get styles(): CSSResult {
return css`
:host {
display: inline-block;
}
ha-icon {
display: inline-flex;
vertical-align: initial;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon-button": HaIconButton;
}
}

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