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;
}
}

View File

@ -1,18 +1,16 @@
import "@polymer/iron-icon/iron-icon";
// Not duplicate, this is for typing.
// eslint-disable-next-line
import { HaIcon } from "./ha-icon";
import { HaSvgIcon } from "./ha-svg-icon";
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
export class HaIconNext extends HaIcon {
export class HaIconNext extends HaSvgIcon {
public connectedCallback() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this.icon =
this.path =
window.getComputedStyle(this).direction === "ltr"
? "hass:chevron-right"
: "hass:chevron-left";
? mdiChevronRight
: mdiChevronLeft;
}, 100);
}
}

View File

@ -1,18 +1,16 @@
import "@polymer/iron-icon/iron-icon";
// Not duplicate, this is for typing.
// eslint-disable-next-line
import { HaIcon } from "./ha-icon";
import { HaSvgIcon } from "./ha-svg-icon";
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
export class HaIconPrev extends HaIcon {
export class HaIconPrev extends HaSvgIcon {
public connectedCallback() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this.icon =
this.path =
window.getComputedStyle(this).direction === "ltr"
? "hass:chevron-left"
: "hass:chevron-right";
? mdiChevronLeft
: mdiChevronRight;
}, 100);
}
}

View File

@ -1,34 +1,133 @@
import "@polymer/iron-icon/iron-icon";
import type { IronIconElement } from "@polymer/iron-icon/iron-icon";
import { Constructor } from "../types";
import { get, Store } from "idb-keyval";
import {
customElement,
LitElement,
property,
PropertyValues,
html,
TemplateResult,
css,
CSSResult,
} from "lit-element";
import "./ha-svg-icon";
import { debounce } from "../common/util/debounce";
import { iconMetadata } from "../resources/icon-metadata";
import { IconMetadata } from "../types";
const ironIconClass = customElements.get("iron-icon") as Constructor<
IronIconElement
>;
let loaded = false;
export class HaIcon extends ironIconClass {
private _iconsetName?: string;
public listen(
node: EventTarget | null,
eventName: string,
methodName: string
): void {
super.listen(node, eventName, methodName);
if (!loaded && this._iconsetName === "mdi") {
loaded = true;
import(/* webpackChunkName: "mdi-icons" */ "../resources/mdi-icons");
}
}
interface Icons {
[key: string]: string;
}
interface Chunks {
[key: string]: Promise<Icons>;
}
const iconStore = new Store("hass-icon-db", "mdi-icon-store");
const chunks: Chunks = {};
const MDI_PREFIXES = ["mdi", "hass", "hassio"];
const findIconChunk = (icon): string => {
let lastChunk: IconMetadata;
for (const chunk of iconMetadata) {
if (chunk.start !== undefined && icon < chunk.start) {
break;
}
lastChunk = chunk;
}
return lastChunk!.file;
};
const debouncedWriteCache = debounce(async () => {
const keys = Object.keys(chunks);
const iconsSets: Icons[] = await Promise.all(Object.values(chunks));
// We do a batch opening the store just once, for (considerable) performance
iconStore._withIDBStore("readwrite", (store) => {
iconsSets.forEach((icons, idx) => {
Object.entries(icons).forEach(([name, path]) => {
store.put(path, name);
});
delete chunks[keys[idx]];
});
});
}, 2000);
@customElement("ha-icon")
export class HaIcon extends LitElement {
@property() public icon?: string;
@property() private _path?: string;
@property() private _noMdi = false;
protected updated(changedProps: PropertyValues) {
if (changedProps.has("icon")) {
this._loadIcon();
}
}
protected render(): TemplateResult {
if (!this.icon) {
return html``;
}
if (this._noMdi) {
return html`<iron-icon .icon=${this.icon}></iron-icon>`;
}
return html`<ha-svg-icon .path=${this._path}></ha-svg-icon>`;
}
private async _loadIcon() {
if (!this.icon) {
return;
}
const icon = this.icon.split(":", 2);
if (!MDI_PREFIXES.includes(icon[0])) {
this._noMdi = true;
return;
}
this._noMdi = false;
const iconName = icon[1];
const cachedPath: string = await get(iconName, iconStore);
if (cachedPath) {
this._path = cachedPath;
return;
}
const chunk = findIconChunk(iconName);
if (chunk in chunks) {
this._setPath(chunks[chunk], iconName);
return;
}
const iconPromise = fetch(`/static/mdi/${chunk}.json`).then((response) =>
response.json()
);
chunks[chunk] = iconPromise;
this._setPath(iconPromise, iconName);
debouncedWriteCache();
}
private async _setPath(promise: Promise<Icons>, iconName: string) {
const iconPack = await promise;
this._path = iconPack[iconName];
}
static get styles(): CSSResult {
return css`
:host {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
vertical-align: middle;
fill: currentcolor;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon": HaIcon;
}
}
customElements.define("ha-icon", HaIcon);

View File

@ -1,33 +0,0 @@
import "@polymer/iron-iconset-svg/iron-iconset-svg";
const IronIconsetClass = customElements.get("iron-iconset-svg");
class HaIconset extends IronIconsetClass {
/**
* Fire 'iron-iconset-added' event at next microtask.
*/
_fireIronIconsetAdded() {
this.async(() => this.fire("iron-iconset-added", this, { node: window }));
}
/**
*
* When name is changed, register iconset metadata
*
*/
_nameChanged() {
this._meta.value = null;
this._meta.key = this.name;
this._meta.value = this;
if (this.ownerDocument && this.ownerDocument.readyState === "loading") {
// Document still loading. It could be that not all icons in the iconset are parsed yet.
this.ownerDocument.addEventListener("DOMContentLoaded", () => {
this._fireIronIconsetAdded();
});
} else {
this._fireIronIconsetAdded();
}
}
}
customElements.define("ha-iconset-svg", HaIconset);

View File

@ -65,6 +65,7 @@ class HaLabelBadge extends LitElement {
display: inline-block;
text-align: center;
vertical-align: top;
padding: var(--ha-label-badge-padding, 0 0 0 0);
}
.label-badge {
position: relative;

View File

@ -12,6 +12,8 @@ class HaMarkdown extends UpdatingElement {
@property({ type: Boolean }) public allowSvg = false;
@property({ type: Boolean }) public breaks = false;
protected update(changedProps) {
super.update(changedProps);
@ -26,7 +28,7 @@ class HaMarkdown extends UpdatingElement {
this.innerHTML = await worker.renderMarkdown(
this.content,
{
breaks: true,
breaks: this.breaks,
gfm: true,
tables: true,
},

View File

@ -1,4 +1,3 @@
import "@polymer/paper-icon-button/paper-icon-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@ -13,6 +12,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { subscribeNotifications } from "../data/persistent_notification";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
@customElement("ha-menu-button")
class HaMenuButton extends LitElement {
@ -55,11 +55,11 @@ class HaMenuButton extends LitElement {
(entityId) => computeDomain(entityId) === "configurator"
));
return html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.icon=${this.hassio ? "hassio:menu" : "hass:menu"}
icon="hass:menu"
@click=${this._toggleMenu}
></paper-icon-button>
></ha-icon-button>
${hasNotifications ? html` <div class="dot"></div> ` : ""}
`;
}

View File

@ -1,32 +0,0 @@
import "@polymer/paper-icon-button/paper-icon-button";
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
import type { Constructor } from "../types";
const paperIconButtonClass = customElements.get(
"paper-icon-button"
) as Constructor<PaperIconButtonElement>;
export class HaPaperIconButtonArrowNext extends paperIconButtonClass {
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-paper-icon-button-arrow-next": HaPaperIconButtonArrowNext;
}
}
customElements.define(
"ha-paper-icon-button-arrow-next",
HaPaperIconButtonArrowNext
);

View File

@ -1,38 +0,0 @@
import "@polymer/paper-icon-button/paper-icon-button";
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
import type { Constructor } from "../types";
const paperIconButtonClass = customElements.get(
"paper-icon-button"
) as Constructor<PaperIconButtonElement>;
export class HaPaperIconButtonArrowPrev extends paperIconButtonClass {
public hassio?: boolean;
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"
? this.hassio
? "hassio:arrow-left"
: "hass:arrow-left"
: this.hassio
? "hassio:arrow-right"
: "hass:arrow-right";
}, 100);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-paper-icon-button-arrow-prev": HaPaperIconButtonArrowPrev;
}
}
customElements.define(
"ha-paper-icon-button-arrow-prev",
HaPaperIconButtonArrowPrev
);

View File

@ -1,29 +0,0 @@
import "@polymer/paper-icon-button/paper-icon-button";
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
import type { Constructor } from "../types";
const paperIconButtonClass = customElements.get(
"paper-icon-button"
) as Constructor<PaperIconButtonElement>;
export class HaPaperIconButtonNext extends paperIconButtonClass {
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-paper-icon-button-next": HaPaperIconButtonNext;
}
}
customElements.define("ha-paper-icon-button-next", HaPaperIconButtonNext);

View File

@ -1,29 +0,0 @@
import "@polymer/paper-icon-button/paper-icon-button";
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
import type { Constructor } from "../types";
const paperIconButtonClass = customElements.get(
"paper-icon-button"
) as Constructor<PaperIconButtonElement>;
export class HaPaperIconButtonPrev extends paperIconButtonClass {
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-paper-icon-button-prev": HaPaperIconButtonPrev;
}
}
customElements.define("ha-paper-icon-button-prev", HaPaperIconButtonPrev);

View File

@ -1,5 +1,6 @@
import { mdiBell, mdiCellphoneSettingsVariant } from "@mdi/js";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "./ha-icon-button";
import "@polymer/paper-item/paper-icon-item";
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
@ -28,6 +29,7 @@ import {
getExternalConfig,
} from "../external_app/external_config";
import type { HomeAssistant, PanelInfo } from "../types";
import "./ha-svg-icon";
import "./ha-icon";
import "./ha-menu-button";
import "./user/ha-user-badge";
@ -151,13 +153,13 @@ class HaSidebar extends LitElement {
<div class="menu">
${!this.narrow
? html`
<paper-icon-button
<ha-icon-button
aria-label=${hass.localize("ui.sidebar.sidebar_toggle")}
.icon=${hass.dockedSidebar === "docked"
? "hass:menu-open"
: "hass:menu"}
@click=${this._toggleSidebar}
></paper-icon-button>
></ha-icon-button>
`
: ""}
<span class="title">Home Assistant</span>
@ -205,10 +207,10 @@ class HaSidebar extends LitElement {
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-icon
<ha-svg-icon
slot="item-icon"
icon="hass:cellphone-settings-variant"
></ha-icon>
.path=${mdiCellphoneSettingsVariant}
></ha-svg-icon>
<span class="item-text">
${hass.localize("ui.sidebar.external_app_configuration")}
</span>
@ -230,7 +232,7 @@ class HaSidebar extends LitElement {
aria-role="option"
@click=${this._handleShowNotificationDrawer}
>
<ha-icon slot="item-icon" icon="hass:bell"></ha-icon>
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
${!this.expanded && notificationCount > 0
? html`
<span class="notification-badge" slot="item-icon">
@ -494,13 +496,13 @@ class HaSidebar extends LitElement {
width: 256px;
}
.menu paper-icon-button {
.menu ha-icon-button {
color: var(--sidebar-icon-color);
}
:host([expanded]) .menu paper-icon-button {
:host([expanded]) .menu ha-icon-button {
margin-right: 23px;
}
:host([expanded][_rtl]) .menu paper-icon-button {
:host([expanded][_rtl]) .menu ha-icon-button {
margin-right: 0px;
margin-left: 23px;
}
@ -562,7 +564,8 @@ class HaSidebar extends LitElement {
padding-right: 12px;
}
ha-icon[slot="item-icon"] {
ha-icon[slot="item-icon"],
ha-svg-icon[slot="item-icon"] {
color: var(--sidebar-icon-color);
}
@ -603,7 +606,8 @@ class HaSidebar extends LitElement {
font-size: 14px;
}
a.iron-selected paper-icon-item ha-icon {
a.iron-selected paper-icon-item ha-icon,
a.iron-selected paper-icon-item ha-svg-icon {
color: var(--sidebar-selected-icon-color);
}
@ -665,7 +669,7 @@ class HaSidebar extends LitElement {
padding: 0px 6px;
color: var(--text-primary-color);
}
ha-icon + .notification-badge {
ha-svg-icon + .notification-badge {
position: absolute;
bottom: 14px;
left: 26px;
@ -710,7 +714,7 @@ class HaSidebar extends LitElement {
font-weight: 500;
}
:host([_rtl]) .menu paper-icon-button {
:host([_rtl]) .menu ha-icon-button {
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}

View File

@ -0,0 +1,52 @@
import {
css,
CSSResult,
customElement,
LitElement,
property,
svg,
SVGTemplateResult,
} from "lit-element";
@customElement("ha-svg-icon")
export class HaSvgIcon extends LitElement {
@property() public path?: string;
protected render(): SVGTemplateResult {
return svg`
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false">
<g>
${this.path ? svg`<path d=${this.path}></path>` : ""}
</g>
</svg>`;
}
static get styles(): CSSResult {
return css`
:host {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
vertical-align: middle;
fill: currentcolor;
width: var(--mdc-icon-size, 24px);
height: var(--mdc-icon-size, 24px);
}
svg {
width: 100%;
height: 100%;
pointer-events: none;
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-svg-icon": HaSvgIcon;
}
}

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";
@ -30,7 +30,7 @@ class HaWaterHeaterControl extends EventsMixin(PolymerElement) {
font-size: 200%;
text-align: right;
}
paper-icon-button {
ha-icon-button {
height: 48px;
width: 48px;
}
@ -40,16 +40,16 @@ class HaWaterHeaterControl 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 { Circle, Layer, Map, Marker } from "leaflet";
import {
css,

View File

@ -1,5 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
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";

View File

@ -120,7 +120,7 @@ export type Trigger =
| DeviceTrigger;
export interface LogicalCondition {
condition: "and" | "or";
condition: "and" | "not" | "or";
conditions: Condition[];
}

View File

@ -10,6 +10,7 @@ export const DISCOVERY_SOURCES = ["unignore", "homekit", "ssdp", "zeroconf"];
export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", {
handler,
show_advanced_options: Boolean(hass.userData?.showAdvanced),
});
export const fetchConfigFlow = (hass: HomeAssistant, flowId: string) =>

View File

@ -15,6 +15,7 @@ export interface DeviceRegistryEntry {
via_device_id?: string;
area_id?: string;
name_by_user?: string;
entry_type: "service" | null;
}
export interface DeviceEntityLookup {

View File

@ -26,6 +26,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
auto_update: boolean;
url: null | string;
detached: boolean;
documentation: boolean;
available: boolean;
arch: "armhf" | "aarch64" | "i386" | "amd64";
machine: any;
@ -46,6 +47,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
auto_uart: boolean;
icon: boolean;
logo: boolean;
stage: "stable" | "experimental" | "deprecated";
changelog: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
@ -135,6 +137,13 @@ export const fetchHassioAddonLogs = async (
return hass.callApi<string>("GET", `hassio/addons/${slug}/logs`);
};
export const fetchHassioAddonDocumentation = async (
hass: HomeAssistant,
slug: string
) => {
return hass.callApi<string>("GET", `hassio/addons/${slug}/documentation`);
};
export const setHassioAddonOption = async (
hass: HomeAssistant,
slug: string,
@ -166,6 +175,13 @@ export const installHassioAddon = async (hass: HomeAssistant, slug: string) => {
);
};
export const restartHassioAddon = async (hass: HomeAssistant, slug: string) => {
return hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/restart`
);
};
export const uninstallHassioAddon = async (
hass: HomeAssistant,
slug: string

View File

@ -27,3 +27,23 @@ export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => {
)
);
};
export const rebootHost = async (hass: HomeAssistant) => {
return hass.callApi<HassioResponse<void>>("POST", "hassio/host/reboot");
};
export const shutdownHost = async (hass: HomeAssistant) => {
return hass.callApi<HassioResponse<void>>("POST", "hassio/host/shutdown");
};
export const updateOS = async (hass: HomeAssistant) => {
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/update");
};
export const changeHostOptions = async (hass: HomeAssistant, options: any) => {
return hass.callApi<HassioResponse<void>>(
"POST",
"hassio/host/options",
options
);
};

View File

@ -37,8 +37,11 @@ export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => {
);
};
export const fetchSupervisorLogs = async (hass: HomeAssistant) => {
return hass.callApi<string>("GET", "hassio/supervisor/logs");
export const fetchHassioLogs = async (
hass: HomeAssistant,
provider: string
) => {
return hass.callApi<string>("GET", `hassio/${provider}/logs`);
};
export const createHassioSession = async (hass: HomeAssistant) => {

View File

@ -7,6 +7,7 @@ export const createOptionsFlow = (hass: HomeAssistant, handler: string) =>
"config/config_entries/options/flow",
{
handler,
show_advanced_options: Boolean(hass.userData?.showAdvanced),
}
);

View File

@ -3,22 +3,22 @@ import { HomeAssistant, WeatherEntity } from "../types";
export const weatherImages = {
"clear-night": "/static/images/weather/night.png",
cloudy: "/static/images/weather/cloudy.png",
fog: "/static/images/weather/cloudy.png",
lightning: "/static/images/weather/lightning.png",
"lightning-rainy": "/static/images/weather/lightning-rainy.png",
partlycloudy: "/static/images/weather/partly-cloudy.png",
pouring: "/static/images/weather/pouring.png",
rainy: "/static/images/weather/rainy.png",
hail: "/static/images/weather/rainy.png",
snowy: "/static/images/weather/snowy.png",
"snowy-rainy": "/static/images/weather/snowy.png",
sunny: "/static/images/weather/sunny.png",
windy: "/static/images/weather/windy.png",
"windy-variant": "/static/images/weather/windy.png",
};
export const weatherIcons = {
exceptional: "hass:alert-circle-outline",
fog: "hass:weather-fog",
hail: "hass:weather-hail",
"snowy-rainy": "hass:weather-snowy-rainy",
"windy-variant": "hass:weather-windy-variant",
};
export const cardinalDirections = [

View File

@ -1,6 +1,6 @@
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-tooltip/paper-tooltip";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
@ -146,13 +146,13 @@ class DataEntryFlowDialog extends LitElement {
// to reset the element.
""
: html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.dismiss"
)}
icon="hass:close"
dialog-dismiss
></paper-icon-button>
></ha-icon-button>
${this._step === null
? // Show handler picker
html`
@ -336,7 +336,7 @@ class DataEntryFlowDialog extends LitElement {
display: block;
padding: 0;
}
paper-icon-button {
ha-icon-button {
display: inline-block;
padding: 8px;
margin: 16px 16px 0 0;

View File

@ -59,7 +59,9 @@ export const showConfigFlowDialog = (
);
return description
? html` <ha-markdown allowsvg .content=${description}></ha-markdown> `
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
@ -78,7 +80,9 @@ export const showConfigFlowDialog = (
step.description_placeholders
);
return description
? html` <ha-markdown allowsvg .content=${description}></ha-markdown> `
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
@ -112,7 +116,13 @@ export const showConfigFlowDialog = (
)}
</p>
${description
? html` <ha-markdown allowsvg .content=${description}></ha-markdown> `
? html`
<ha-markdown
allowsvg
breaks
.content=${description}
></ha-markdown>
`
: ""}
`;
},
@ -128,7 +138,13 @@ export const showConfigFlowDialog = (
return html`
${description
? html` <ha-markdown allowsvg .content=${description}></ha-markdown> `
? html`
<ha-markdown
allowsvg
breaks
.content=${description}
></ha-markdown>
`
: ""}
<p>
${hass.localize(

View File

@ -50,7 +50,13 @@ export const showOptionsFlowDialog = (
);
return description
? html` <ha-markdown allowsvg .content=${description}></ha-markdown> `
? html`
<ha-markdown
breaks
allowsvg
.content=${description}
></ha-markdown>
`
: "";
},
@ -69,7 +75,13 @@ export const showOptionsFlowDialog = (
step.description_placeholders
);
return description
? html` <ha-markdown allowsvg .content=${description}></ha-markdown> `
? html`
<ha-markdown
allowsvg
breaks
.content=${description}
></ha-markdown>
`
: "";
},

View File

@ -122,10 +122,14 @@ class StepFlowForm extends LitElement {
const data = {};
this.step.data_schema.forEach((field) => {
if ("default" in field) {
if (field.description?.suggested_value) {
data[field.name] = field.description.suggested_value;
} else if ("default" in field) {
data[field.name] = field.default;
}
});
this._stepData = data;
return data;
}

View File

@ -41,8 +41,8 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
more-info-controls {
--more-info-header-background: var(--primary-color);
--more-info-header-color: var(--text-primary-color);
--more-info-header-background: var(--app-header-background-color);
--more-info-header-color: var(--app-header-text-color, white);
}
:host {
width: 100% !important;

View File

@ -443,12 +443,6 @@ class MoreInfoClimate extends LitElement {
color: var(--primary-text-color);
}
.container-hvac_modes iron-icon,
.container-fan_list iron-icon,
.container-swing_list iron-icon {
margin: 22px 16px 0 0;
}
ha-paper-dropdown-menu {
width: 100%;
}

View File

@ -52,6 +52,7 @@ class MoreInfoConfigurator extends PolymerElement {
<div class="layout vertical">
<template is="dom-if" if="[[isConfigurable]]">
<ha-markdown
breaks
content="[[stateObj.attributes.description]]"
></ha-markdown>

View File

@ -1,5 +1,4 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";

View File

@ -1,5 +1,5 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
@ -78,18 +78,18 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="container-direction">
<div class="direction">
<div>[[localize('ui.card.fan.direction')]]</div>
<paper-icon-button
<ha-icon-button
icon="hass:rotate-left"
on-click="onDirectionReverse"
title="[[localize('ui.card.fan.reverse')]]"
disabled="[[computeIsRotatingReverse(stateObj)]]"
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
icon="hass:rotate-right"
on-click="onDirectionForward"
title="[[localize('ui.card.fan.forward')]]"
disabled="[[computeIsRotatingForward(stateObj)]]"
></paper-icon-button>
></ha-icon-button>
</div>
</div>
</div>

View File

@ -11,6 +11,7 @@ import "../../../components/ha-labeled-slider";
import "../../../components/ha-paper-dropdown-menu";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../components/ha-icon-button";
const FEATURE_CLASS_NAMES = {
1: "has-brightness",
@ -168,11 +169,11 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
saturation-segments="{{saturationSegments}}"
>
</ha-color-picker>
<paper-icon-button
<ha-icon-button
icon="mdi:palette"
on-click="segmentClick"
class="control segmentationButton"
></paper-icon-button>
></ha-icon-button>
</div>
<div class="control effect_list">

View File

@ -1,6 +1,5 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
@ -11,6 +10,7 @@ import { attributeClassNames } from "../../../common/entity/attribute_class_name
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-paper-slider";
import "../../../components/ha-icon";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
@ -28,7 +28,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
text-transform: capitalize;
}
paper-icon-button[highlight] {
ha-icon-button[highlight] {
color: var(--accent-color);
}
@ -44,7 +44,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
max-height: 40px;
}
iron-icon.source-input {
ha-icon.source-input {
padding: 7px;
margin-top: 15px;
}
@ -65,34 +65,34 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class$="[[computeClassNames(stateObj)]]">
<div class="layout horizontal">
<div class="flex">
<paper-icon-button
<ha-icon-button
icon="hass:power"
highlight$="[[playerObj.isOff]]"
on-click="handleTogglePower"
hidden$="[[computeHidePowerButton(playerObj)]]"
></paper-icon-button>
></ha-icon-button>
</div>
<div>
<template
is="dom-if"
if="[[computeShowPlaybackControls(playerObj)]]"
>
<paper-icon-button
<ha-icon-button
icon="hass:skip-previous"
on-click="handlePrevious"
hidden$="[[!playerObj.supportsPreviousTrack]]"
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
icon="[[computePlaybackControlIcon(playerObj)]]"
on-click="handlePlaybackControl"
hidden$="[[!computePlaybackControlIcon(playerObj)]]"
highlight=""
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
icon="hass:skip-next"
on-click="handleNext"
hidden$="[[!playerObj.supportsNextTrack]]"
></paper-icon-button>
></ha-icon-button>
</template>
</div>
</div>
@ -101,36 +101,36 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
class="volume_buttons center horizontal layout"
hidden$="[[computeHideVolumeButtons(playerObj)]]"
>
<paper-icon-button
<ha-icon-button
on-click="handleVolumeTap"
icon="hass:volume-off"
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
id="volumeDown"
disabled$="[[playerObj.isMuted]]"
on-mousedown="handleVolumeDown"
on-touchstart="handleVolumeDown"
on-touchend="handleVolumeTouchEnd"
icon="hass:volume-medium"
></paper-icon-button>
<paper-icon-button
></ha-icon-button>
<ha-icon-button
id="volumeUp"
disabled$="[[playerObj.isMuted]]"
on-mousedown="handleVolumeUp"
on-touchstart="handleVolumeUp"
on-touchend="handleVolumeTouchEnd"
icon="hass:volume-high"
></paper-icon-button>
></ha-icon-button>
</div>
<div
class="volume center horizontal layout"
hidden$="[[!playerObj.supportsVolumeSet]]"
>
<paper-icon-button
<ha-icon-button
on-click="handleVolumeTap"
hidden$="[[playerObj.supportsVolumeButtons]]"
icon="[[computeMuteVolumeIcon(playerObj)]]"
></paper-icon-button>
></ha-icon-button>
<ha-paper-slider
disabled$="[[playerObj.isMuted]]"
min="0"
@ -148,7 +148,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
class="controls layout horizontal justified"
hidden$="[[computeHideSelectSource(playerObj)]]"
>
<iron-icon class="source-input" icon="hass:login-variant"></iron-icon>
<ha-icon class="source-input" icon="hass:login-variant"></ha-icon>
<ha-paper-dropdown-menu
class="flex source-input"
dynamic-align=""
@ -170,7 +170,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
<!-- SOUND MODE PICKER -->
<template is="dom-if" if="[[!computeHideSelectSoundMode(playerObj)]]">
<div class="controls layout horizontal justified">
<iron-icon class="source-input" icon="hass:music-note"></iron-icon>
<ha-icon class="source-input" icon="hass:music-note"></ha-icon>
<ha-paper-dropdown-menu
class="flex source-input"
dynamic-align
@ -202,10 +202,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
value="{{ttsMessage}}"
on-keydown="ttsCheckForEnter"
></paper-input>
<paper-icon-button
icon="hass:send"
on-click="sendTTS"
></paper-icon-button>
<ha-icon-button icon="hass:send" on-click="sendTTS"></ha-icon-button>
</div>
</div>
`;

View File

@ -1,5 +1,5 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
@ -122,9 +122,7 @@ class MoreInfoVacuum extends LitElement {
? html`
<div>
<span>
<iron-icon
.icon=${stateObj.attributes.battery_icon}
></iron-icon>
<ha-icon .icon=${stateObj.attributes.battery_icon}></ha-icon>
${stateObj.attributes.battery_level}%
</span>
</div>
@ -147,14 +145,14 @@ class MoreInfoVacuum extends LitElement {
).map(
(item) => html`
<div>
<paper-icon-button
<ha-icon-button
.icon=${item.icon}
.entry=${item}
@click=${this.callService}
.title=${this.hass!.localize(
`ui.dialogs.more_info_control.vacuum.${item.translationKey}`
)}
></paper-icon-button>
></ha-icon-button>
</div>
`
)}
@ -190,7 +188,7 @@ class MoreInfoVacuum extends LitElement {
style="justify-content: center; align-self: center; padding-top: 1.3em"
>
<span>
<iron-icon icon="hass:fan"></iron-icon>
<ha-icon icon="hass:fan"></ha-icon>
${stateObj.attributes.fan_speed}
</span>
</div>

View File

@ -28,16 +28,6 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
color: var(--primary-text-color);
}
.container-away_mode,
.container-temperature,
.container-operation_list,
.has-away_mode .container-away_mode,
.has-target_temperature .container-temperature,
.has-operation_mode .container-operation_list,
.container-operation_list iron-icon,
ha-paper-dropdown-menu {
width: 100%;
}

View File

@ -1,4 +1,3 @@
import "@polymer/iron-icon/iron-icon";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
@ -10,6 +9,7 @@ import {
} from "lit-element";
import { html, TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-icon";
const cardinalDirections = [
"N",
@ -79,7 +79,7 @@ class MoreInfoWeather extends LitElement {
return html`
<div class="flex">
<iron-icon icon="hass:thermometer"></iron-icon>
<ha-icon icon="hass:thermometer"></ha-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.temperature")}
</div>
@ -90,7 +90,7 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.pressure)
? html`
<div class="flex">
<iron-icon icon="hass:gauge"></iron-icon>
<ha-icon icon="hass:gauge"></ha-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.air_pressure")}
</div>
@ -104,7 +104,7 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.humidity)
? html`
<div class="flex">
<iron-icon icon="hass:water-percent"></iron-icon>
<ha-icon icon="hass:water-percent"></ha-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.humidity")}
</div>
@ -115,7 +115,7 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.wind_speed)
? html`
<div class="flex">
<iron-icon icon="hass:weather-windy"></iron-icon>
<ha-icon icon="hass:weather-windy"></ha-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.wind_speed")}
</div>
@ -131,7 +131,7 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.visibility)
? html`
<div class="flex">
<iron-icon icon="hass:eye"></iron-icon>
<ha-icon icon="hass:eye"></ha-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.visibility")}
</div>
@ -151,9 +151,9 @@ class MoreInfoWeather extends LitElement {
<div class="flex">
${item.condition
? html`
<iron-icon
<ha-icon
.icon="${weatherIcons[item.condition]}"
></iron-icon>
></ha-icon>
`
: ""}
${!this._showValue(item.templow)
@ -193,7 +193,7 @@ class MoreInfoWeather extends LitElement {
static get styles(): CSSResult {
return css`
iron-icon {
ha-icon {
color: var(--paper-item-icon-color);
}
.section {

View File

@ -1,7 +1,7 @@
import "@material/mwc-button";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../components/ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -78,27 +78,27 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
</style>
<app-toolbar>
<paper-icon-button
<ha-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.dismiss')]]"
icon="hass:close"
dialog-dismiss
></paper-icon-button>
></ha-icon-button>
<div class="main-title" main-title="" on-click="enlarge">
[[_computeStateName(stateObj)]]
</div>
<template is="dom-if" if="[[_computeConfig(hass)]]">
<paper-icon-button
<ha-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.settings')]]"
icon="hass:settings"
on-click="_gotoSettings"
></paper-icon-button>
></ha-icon-button>
</template>
<template is="dom-if" if="[[_computeEdit(hass, stateObj)]]">
<paper-icon-button
<ha-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.edit')]]"
icon="hass:pencil"
on-click="_gotoEdit"
></paper-icon-button>
></ha-icon-button>
</template>
</app-toolbar>

View File

@ -1,12 +1,11 @@
import "@material/mwc-button";
import "@polymer/app-layout/app-drawer/app-drawer";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeDomain } from "../../common/entity/compute_domain";
import "../../components/ha-paper-icon-button-prev";
import "../../components/ha-icon-button-prev";
import { subscribeNotifications } from "../../data/persistent_notification";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
@ -51,7 +50,7 @@ export class HuiNotificationDrawer extends EventsMixin(
<app-drawer id="drawer" opened="{{open}}" disable-swipe align="start">
<app-toolbar>
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
<ha-paper-icon-button-prev on-click="_closeDrawer" aria-label$="[[localize('ui.notification_drawer.close')]]"></paper-icon-button>
<ha-icon-button-prev on-click="_closeDrawer" aria-label$="[[localize('ui.notification_drawer.close')]]"></ha-icon-button-prev>
</app-toolbar>
<div class="notifications">
<template is="dom-if" if="[[!_empty(notifications)]]">

View File

@ -32,7 +32,10 @@ export class HuiPersistentNotificationItem extends LitElement {
${this.notification.title}
</span>
<ha-markdown content="${this.notification.message}"></ha-markdown>
<ha-markdown
breaks
content="${this.notification.message}"
></ha-markdown>
<div class="time">
<span>

View File

@ -1,7 +1,6 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import type { PaperDialogScrollableElement } from "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
@ -176,12 +175,12 @@ export class HaVoiceCommandDialog extends LitElement {
</div>
`
: ""}
<paper-icon-button
<ha-icon-button
.active=${Boolean(this.results)}
icon="hass:microphone"
@click=${this._toggleListening}
>
</paper-icon-button>
</ha-icon-button>
</span>
`
: ""}
@ -369,11 +368,11 @@ export class HaVoiceCommandDialog extends LitElement {
z-index: 103;
}
paper-icon-button {
ha-icon-button {
color: var(--secondary-text-color);
}
paper-icon-button[active] {
ha-icon-button[active] {
color: var(--primary-color);
}

View File

@ -2,9 +2,6 @@
/* eslint-disable import/first */
import "@polymer/paper-styles/typography";
import { setPassiveTouchGestures } from "@polymer/polymer/lib/utils/settings";
// For MDI icons. Needs to be part of main bundle or else it won't hook
// properly into iron-meta, which is used to transfer iconsets to iron-icon.
import "../components/ha-iconset-svg";
import "../layouts/home-assistant";
import "../resources/html-import/polyfill";
import "../resources/roboto";

View File

@ -1,7 +1,6 @@
import "@polymer/polymer/lib/elements/dom-if";
import "@polymer/polymer/lib/elements/dom-repeat";
import "../auth/ha-authorize";
import "../components/ha-iconset-svg";
import "../resources/ha-style";
import "../resources/roboto";

View File

@ -1 +0,0 @@
import "../resources/hass-icons";

View File

@ -1,4 +1,3 @@
import "../components/ha-iconset-svg";
import "../onboarding/ha-onboarding";
import "../resources/ha-style";
import "../resources/roboto";

View File

@ -49,7 +49,6 @@
<script type="module" crossorigin="use-credentials">
import "<%= latestPageJS %>";
import "<%= latestHassIconsJS %>";
window.providersPromise = fetch("/auth/providers", {
credentials: "same-origin",
});
@ -62,7 +61,6 @@
_ls("/static/polyfills/custom-elements-es5-adapter.js");
_ls("<%= es5Compatibility %>");
_ls("<%= es5PageJS %>");
_ls("<%= es5HassIconsJS %>");
}
})();
</script>

View File

@ -51,6 +51,9 @@
height: 112px;
background-color: #THEMEC;
}
html {
background-color: var(--primary-background-color, #fafafa);
}
</style>
</head>
<body>
@ -62,7 +65,6 @@
<script type="module" crossorigin="use-credentials">
import "<%= latestCoreJS %>";
import "<%= latestAppJS %>";
import "<%= latestHassIconsJS %>";
window.customPanelJS = "<%= latestCustomPanelJS %>";
</script>
{% for extra_module in extra_modules -%}
@ -79,7 +81,6 @@
_ls("<%= es5Compatibility %>");
_ls("<%= es5CoreJS %>");
_ls("<%= es5AppJS %>");
_ls("<%= es5HassIconsJS %>");
{% for extra_script in extra_js_es5 -%}
_ls("{{ extra_script }}");
{% endfor -%}

View File

@ -51,7 +51,6 @@
<script type="module" crossorigin="use-credentials">
import "<%= latestPageJS %>";
import "<%= latestHassIconsJS %>";
window.stepsPromise = fetch("/api/onboarding", {
credentials: "same-origin",
});
@ -64,7 +63,6 @@
_ls("/static/polyfills/custom-elements-es5-adapter.js");
_ls("<%= es5Compatibility %>");
_ls("<%= es5PageJS %>");
_ls("<%= es5HassIconsJS %>");
}
})();
</script>

View File

@ -10,7 +10,7 @@ import {
TemplateResult,
} from "lit-element";
import "../components/ha-menu-button";
import "../components/ha-paper-icon-button-arrow-prev";
import "../components/ha-icon-button-arrow-prev";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
@ -33,9 +33,9 @@ class HassLoadingScreen extends LitElement {
></ha-menu-button>
`
: html`
<ha-paper-icon-button-arrow-prev
<ha-icon-button-arrow-prev
@click=${this._handleBack}
></ha-paper-icon-button-arrow-prev>
></ha-icon-button-arrow-prev>
`}
</app-toolbar>
<div class="content">

View File

@ -9,7 +9,7 @@ import {
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "../components/ha-menu-button";
import "../components/ha-paper-icon-button-arrow-prev";
import "../components/ha-icon-button-arrow-prev";
@customElement("hass-subpage")
class HassSubpage extends LitElement {
@ -25,12 +25,11 @@ class HassSubpage extends LitElement {
protected render(): TemplateResult {
return html`
<div class="toolbar">
<ha-paper-icon-button-arrow-prev
<ha-icon-button-arrow-prev
aria-label="Back"
.hassio=${this.hassio}
@click=${this._backTapped}
class=${classMap({ hidden: !this.showBackButton })}
></ha-paper-icon-button-arrow-prev>
></ha-icon-button-arrow-prev>
<div main-title>${this.header}</div>
<slot name="toolbar-icon"></slot>
@ -66,12 +65,12 @@ class HassSubpage extends LitElement {
}
ha-menu-button,
ha-paper-icon-button-arrow-prev,
ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
pointer-events: auto;
}
ha-paper-icon-button-arrow-prev.hidden {
ha-icon-button-arrow-prev.hidden {
visibility: hidden;
}

View File

@ -14,7 +14,7 @@ import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { navigate } from "../common/navigate";
import "../components/ha-menu-button";
import "../components/ha-paper-icon-button-arrow-prev";
import "../components/ha-icon-button-arrow-prev";
import { HomeAssistant, Route } from "../types";
import "../components/ha-icon";
@ -39,6 +39,8 @@ class HassTabsSubpage extends LitElement {
@property({ type: Boolean }) public hassio = false;
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
@property() public route!: Route;
@property() public tabs!: PageNavigation[];
@ -82,7 +84,7 @@ class HassTabsSubpage extends LitElement {
<span class="name"
>${page.translationKey
? this.hass.localize(page.translationKey)
: name}</span
: page.name}</span
>
`
: ""}
@ -97,7 +99,7 @@ class HassTabsSubpage extends LitElement {
super.updated(changedProperties);
if (changedProperties.has("route")) {
this._activeTab = this.tabs.find((tab) =>
this.route.prefix.includes(tab.path)
`${this.route.prefix}${this.route.path}`.includes(tab.path)
);
}
}
@ -114,11 +116,20 @@ class HassTabsSubpage extends LitElement {
return html`
<div class="toolbar">
<ha-paper-icon-button-arrow-prev
aria-label="Back"
.hassio=${this.hassio}
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
${this.mainPage
? html`
<ha-menu-button
.hass=${this.hass}
.hassio=${this.hassio}
.narrow=${this.narrow}
></ha-menu-button>
`
: html`
<ha-icon-button-arrow-prev
aria-label="Back"
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
${this.narrow
? html` <div class="main-title"><slot name="header"></slot></div> `
: ""}
@ -234,7 +245,7 @@ class HassTabsSubpage extends LitElement {
}
ha-menu-button,
ha-paper-icon-button-arrow-prev,
ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
flex-shrink: 0;
pointer-events: auto;

View File

@ -22,7 +22,7 @@ class ActionBadge extends LitElement {
protected render(): TemplateResult {
return html`
<div class="icon">
<iron-icon .icon=${this.icon}></iron-icon>
<ha-icon .icon=${this.icon}></ha-icon>
${this.badgeIcon
? html` <ha-icon class="badge" .icon=${this.badgeIcon}></ha-icon> `
: ""}

View File

@ -104,12 +104,12 @@ class HaConfigAreaPage extends LitElement {
`
: ""}
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
icon="hass:settings"
.entry=${area}
@click=${this._showSettings}
></paper-icon-button>
></ha-icon-button>
<div class="container">
${!this.narrow

View File

@ -114,11 +114,11 @@ export class HaConfigAreasDashboard extends LitElement {
id="area_id"
hasFab
>
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
icon="hass:help-circle"
@click=${this._showHelp}
></paper-icon-button>
></ha-icon-button>
</hass-tabs-subpage-data-table>
<ha-fab
?is-wide=${this.isWide}

View File

@ -1,5 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
@ -96,18 +96,18 @@ export default class HaAutomationActionRow extends LitElement {
<div class="card-menu">
${this.index !== 0
? html`
<paper-icon-button
<ha-icon-button
icon="hass:arrow-up"
@click=${this._moveUp}
></paper-icon-button>
></ha-icon-button>
`
: ""}
${this.index !== this.totalActions - 1
? html`
<paper-icon-button
<ha-icon-button
icon="hass:arrow-down"
@click=${this._moveDown}
></paper-icon-button>
></ha-icon-button>
`
: ""}
<paper-menu-button
@ -117,10 +117,10 @@ export default class HaAutomationActionRow extends LitElement {
vertical-offset="-5"
close-on-activate
>
<paper-icon-button
<ha-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></paper-icon-button>
></ha-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item
@tap=${this._switchYamlMode}

View File

@ -11,6 +11,7 @@ import type { HomeAssistant } from "../../../../types";
import "../../../../components/ha-yaml-editor";
import "./types/ha-automation-condition-and";
import "./types/ha-automation-condition-device";
import "./types/ha-automation-condition-not";
import "./types/ha-automation-condition-numeric_state";
import "./types/ha-automation-condition-or";
import "./types/ha-automation-condition-state";
@ -23,6 +24,7 @@ const OPTIONS = [
"device",
"and",
"or",
"not",
"state",
"numeric_state",
"sun",

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-menu-button/paper-menu-button";
import {

View File

@ -0,0 +1,11 @@
import { customElement } from "lit-element";
import { HaLogicalCondition } from "./ha-automation-condition-logical";
@customElement("ha-automation-condition-not")
export class HaNotCondition extends HaLogicalCondition {}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-condition-not": HaNotCondition;
}
}

View File

@ -1,6 +1,6 @@
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 "../../../components/ha-icon-button";
import {
css,
CSSResult,
@ -15,7 +15,6 @@ import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-paper-icon-button-arrow-prev";
import {
AutomationConfig,
AutomationEntity,
@ -78,14 +77,14 @@ export class HaAutomationEditor extends LitElement {
${!this.automationId
? ""
: html`
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
title="${this.hass.localize(
"ui.panel.config.automation.picker.delete_automation"
)}"
icon="hass:delete"
@click=${this._deleteConfirm}
></paper-icon-button>
></ha-icon-button>
`}
${this._config
? html`

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
@ -105,14 +105,14 @@ class HaAutomationPicker extends LitElement {
title: "",
type: "icon-button",
template: (_info, automation) => html`
<paper-icon-button
<ha-icon-button
.automation=${automation}
@click=${this._showInfo}
icon="hass:information-outline"
title="${this.hass.localize(
"ui.panel.config.automation.picker.show_info_automation"
)}"
></paper-icon-button>
></ha-icon-button>
`,
};
columns.edit = {
@ -126,7 +126,7 @@ class HaAutomationPicker extends LitElement {
: undefined
)}
>
<paper-icon-button
<ha-icon-button
.icon=${automation.attributes.id
? "hass:pencil"
: "hass:pencil-off"}
@ -134,7 +134,7 @@ class HaAutomationPicker extends LitElement {
title="${this.hass.localize(
"ui.panel.config.automation.picker.show_info_automation"
)}"
></paper-icon-button>
></ha-icon-button>
</a>
${!automation.attributes.id
? html`

View File

@ -12,6 +12,13 @@ import "./ha-automation-editor";
import "./ha-automation-picker";
import { debounce } from "../../../common/util/debounce";
const equal = (a: AutomationEntity[], b: AutomationEntity[]): boolean => {
if (a.length !== b.length) {
return false;
}
return a.every((automation, index) => automation === b[index]);
};
@customElement("ha-config-automation")
class HaConfigAutomation extends HassRouterPage {
@property() public hass!: HomeAssistant;
@ -26,7 +33,7 @@ class HaConfigAutomation extends HassRouterPage {
private _debouncedUpdateAutomations = debounce((pageEl) => {
const newAutomations = this._getAutomations(this.hass.states);
if (!this._equal(newAutomations, pageEl.automations)) {
if (!equal(newAutomations, pageEl.automations)) {
pageEl.automations = newAutomations;
}
}, 10);
@ -82,13 +89,6 @@ class HaConfigAutomation extends HassRouterPage {
pageEl.automationId = automationId === "new" ? null : automationId;
}
}
private _equal(a: AutomationEntity[], b: AutomationEntity[]): boolean {
if (a.length !== b.length) {
return false;
}
return a.every((automation, index) => automation === b[index]);
}
}
declare global {

View File

@ -1,5 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
@ -97,10 +97,10 @@ export default class HaAutomationTriggerRow extends LitElement {
vertical-offset="-5"
close-on-activate
>
<paper-icon-button
<ha-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></paper-icon-button>
></ha-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item
@tap=${this._switchYamlMode}

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button";
import "../../../../components/ha-icon-button";
import {
css,
CSSResult,
@ -154,11 +154,11 @@ class CloudAlexa extends LitElement {
${
emptyFilter
? html`
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
icon="hass:tune"
@click=${this._openDomainToggler}
></paper-icon-button>
></ha-icon-button>
`
: ""
}

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button";
import "../../../../components/ha-icon-button";
import {
css,
CSSResult,
@ -167,11 +167,11 @@ class CloudGoogleAssistant extends LitElement {
${
emptyFilter
? html`
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
icon="hass:tune"
@click=${this._openDomainToggler}
></paper-icon-button>
></ha-icon-button>
`
: ""
}

View File

@ -1,5 +1,5 @@
import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@ -65,7 +65,7 @@ class CloudLogin extends LocalizeMixin(
.flash-msg {
padding-right: 44px;
}
.flash-msg paper-icon-button {
.flash-msg ha-icon-button {
position: absolute;
top: 8px;
right: 8px;
@ -110,8 +110,8 @@ class CloudLogin extends LocalizeMixin(
<ha-card hidden$="[[!flashMessage]]">
<div class="card-content flash-msg">
[[flashMessage]]
<paper-icon-button icon="hass:close" on-click="_dismissFlash"
>[[localize('ui.panel.config.cloud.login.dismiss')]]</paper-icon-button
<ha-icon-button icon="hass:close" on-click="_dismissFlash"
>[[localize('ui.panel.config.cloud.login.dismiss')]]</ha-icon-button
>
<paper-ripple id="flashRipple" noink=""></paper-ripple>
</div>

View File

@ -1,6 +1,6 @@
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 "../../../components/ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";

View File

@ -1,11 +1,9 @@
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { sortStatesByName } from "../../../common/entity/states_sort_by_name";
import "../../../components/ha-paper-icon-button-arrow-prev";
import "../../../layouts/hass-tabs-subpage";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../resources/ha-style";
@ -20,12 +18,7 @@ import "./ha-form-customize";
class HaConfigCustomize extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
ha-paper-icon-button-arrow-prev[hide] {
visibility: hidden;
}
</style>
<style include="ha-style"></style>
<hass-tabs-subpage
hass="[[hass]]"
narrow="[[narrow]]"

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -28,11 +28,11 @@ class HaCustomizeAttribute extends PolymerElement {
}
</style>
<div id="wrapper" class="form-group"></div>
<paper-icon-button
<ha-icon-button
class="button"
icon="[[getIcon(item.secondary)]]"
on-click="tapButton"
></paper-icon-button>
></ha-icon-button>
`;
}

View File

@ -1,8 +1,8 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../components/ha-icon";
class HaCustomizeIcon extends PolymerElement {
static get template() {
@ -18,7 +18,7 @@ class HaCustomizeIcon extends PolymerElement {
margin-top: 10px;
}
</style>
<iron-icon class="icon-image" icon="[[item.value]]"></iron-icon>
<ha-icon class="icon-image" icon="[[item.value]]"></ha-icon>
<paper-input
disabled="[[item.secondary]]"
label="icon"

View File

@ -1,4 +1,3 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import {

View File

@ -28,24 +28,20 @@ import { EntityRegistryStateEntry } from "../ha-config-device-page";
export class HaDeviceEntitiesCard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public deviceId!: string;
@property() public entities!: EntityRegistryStateEntry[];
@property() public narrow!: boolean;
@property() private _showDisabled = false;
@queryAll("#entities > *") private _entityRows?: LovelaceRow[];
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!changedProps.has("hass")) {
return;
protected shouldUpdate(changedProps: PropertyValues) {
if (changedProps.has("hass")) {
this._entityRows?.forEach((element) => {
element.hass = this.hass;
});
return changedProps.size > 1;
}
this._entityRows?.forEach((element) => {
element.hass = this.hass;
});
return true;
}
protected render(): TemplateResult {
@ -159,7 +155,9 @@ export class HaDeviceEntitiesCard extends LitElement {
addEntitiesToLovelaceView(
this,
this.hass,
this.entities.map((entity) => entity.entity_id)
this.entities
.filter((entity) => !entity.disabled_by)
.map((entity) => entity.entity_id)
);
}

View File

@ -167,11 +167,11 @@ export class HaConfigDevicePage extends LitElement {
: ""
}
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
icon="hass:settings"
@click=${this._showSettings}
></paper-icon-button>
></ha-icon-button>
<div class="container">
<div class="header fullwidth">
@ -259,13 +259,13 @@ export class HaConfigDevicePage extends LitElement {
${this.hass.localize(
"ui.panel.config.devices.automation.automations"
)}
<paper-icon-button
<ha-icon-button
@click=${this._showAutomationDialog}
title=${this.hass.localize(
"ui.panel.config.devices.automation.create"
)}
icon="hass:plus-circle"
></paper-icon-button>
></ha-icon-button>
</div>
${this._related?.automation?.length
? this._related.automation.map((automation) => {
@ -317,26 +317,21 @@ export class HaConfigDevicePage extends LitElement {
</div>
<div class="column">
${
isComponentLoaded(this.hass, "scene")
isComponentLoaded(this.hass, "scene") && entities.length
? html`
<ha-card>
<div class="card-header">
${this.hass.localize(
"ui.panel.config.devices.scene.scenes"
)}
${
entities.length
? html`
<paper-icon-button
<ha-icon-button
@click=${this._createScene}
title=${this.hass.localize(
"ui.panel.config.devices.scene.create"
)}
icon="hass:plus-circle"
></paper-icon-button>
`
: ""
}
></ha-icon-button>
</div>
${
@ -397,13 +392,13 @@ export class HaConfigDevicePage extends LitElement {
${this.hass.localize(
"ui.panel.config.devices.script.scripts"
)}
<paper-icon-button
<ha-icon-button
@click=${this._showScriptDialog}
title=${this.hass.localize(
"ui.panel.config.devices.script.create"
)}
icon="hass:plus-circle"
></paper-icon-button>
></ha-icon-button>
</div>
${this._related?.script?.length
? this._related.script.map((script) => {
@ -562,7 +557,7 @@ export class HaConfigDevicePage extends LitElement {
justify-content: space-between;
}
.card-header paper-icon-button {
.card-header ha-icon-button {
margin-right: -8px;
color: var(--primary-color);
height: auto;

View File

@ -1,6 +1,6 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import { HassEntity } from "home-assistant-js-websocket";
@ -94,25 +94,25 @@ export class DialogEntityEditor extends LitElement {
@close-dialog=${this.closeDialog}
>
<app-toolbar>
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.dialogs.entity_registry.dismiss"
)}
icon="hass:close"
dialog-dismiss
></paper-icon-button>
></ha-icon-button>
<div class="main-title" main-title>
${stateObj ? computeStateName(stateObj) : entry?.name || entityId}
</div>
${stateObj
? html`
<paper-icon-button
<ha-icon-button
aria-label=${this.hass.localize(
"ui.dialogs.entity_registry.control"
)}
icon="hass:tune"
@click=${this._openMoreInfo}
></paper-icon-button>
></ha-icon-button>
`
: ""}
</app-toolbar>
@ -264,8 +264,8 @@ export class DialogEntityEditor extends LitElement {
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
app-toolbar {
background-color: var(--primary-color);
color: var(--text-primary-color);
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
}
ha-paper-dialog {
height: 100%;

View File

@ -380,31 +380,31 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
>
`
: html`
<paper-icon-button
<ha-icon-button
id="enable-btn"
icon="hass:undo"
@click=${this._enableSelected}
></paper-icon-button>
></ha-icon-button>
<paper-tooltip for="enable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</paper-tooltip>
<paper-icon-button
<ha-icon-button
id="disable-btn"
icon="hass:cancel"
@click=${this._disableSelected}
></paper-icon-button>
></ha-icon-button>
<paper-tooltip for="disable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</paper-tooltip>
<paper-icon-button
<ha-icon-button
id="remove-btn"
icon="hass:delete"
@click=${this._removeSelected}
></paper-icon-button>
></ha-icon-button>
<paper-tooltip for="remove-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
@ -443,7 +443,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
</div>`
: ""}
<paper-menu-button no-animations horizontal-align="right">
<paper-icon-button
<ha-icon-button
aria-label=${this.hass!.localize(
"ui.panel.config.entities.picker.filter.filter"
)}
@ -452,7 +452,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)}"
icon="hass:filter-variant"
slot="dropdown-trigger"
></paper-icon-button>
></ha-icon-button>
<paper-listbox slot="dropdown-content">
<paper-icon-item @tap="${this._showDisabledChanged}">
<paper-checkbox
@ -745,6 +745,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
position: relative;
top: 2px;
}
.search-toolbar search-input {
margin-left: 8px;
top: 1px;
}
.search-toolbar {
display: flex;
justify-content: space-between;
@ -764,7 +768,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
font-size: 16px;
}
.header-btns > mwc-button,
.header-btns > paper-icon-button {
.header-btns > ha-icon-button {
margin: 8px;
}
.active-filters {

View File

@ -1,5 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
@ -96,14 +96,14 @@ class HaInputSelectForm extends LitElement {
return html`
<paper-item class="option">
<paper-item-body> ${option} </paper-item-body>
<paper-icon-button
<ha-icon-button
.index=${index}
.title=${this.hass.localize(
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
icon="hass:delete"
></paper-icon-button>
></ha-icon-button>
</paper-item>
`;
})

View File

@ -10,9 +10,14 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import { compare } from "../../../common/string/compare";
import memoizeOne from "memoize-one";
import * as Fuse from "fuse.js";
import { caseInsensitiveCompare } from "../../../common/string/compare";
import { computeRTL } from "../../../common/util/compute_rtl";
import { afterNextRender } from "../../../common/util/render-status";
import {
afterNextRender,
nextRender,
} from "../../../common/util/render-status";
import "../../../components/entity/ha-state-icon";
import "../../../components/ha-card";
import "../../../components/ha-fab";
@ -20,7 +25,6 @@ import {
ConfigEntry,
deleteConfigEntry,
getConfigEntries,
updateConfigEntry,
} from "../../../data/config_entries";
import {
DISCOVERY_SOURCES,
@ -29,7 +33,7 @@ import {
localizeConfigFlowTitle,
subscribeConfigFlowInProgress,
} from "../../../data/config_flow";
import { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
@ -39,19 +43,43 @@ import {
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import "../../../common/search/search-input";
import "./ha-integration-card";
import type {
ConfigEntryRemovedEvent,
ConfigEntryUpdatedEvent,
HaIntegrationCard,
} from "./ha-integration-card";
import { HASSDomEvent } from "../../../common/dom/fire_event";
interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
localized_title?: string;
}
export interface ConfigEntryExtended extends ConfigEntry {
localized_domain_name?: string;
}
const groupByIntegration = (
entries: ConfigEntryExtended[]
): Map<string, ConfigEntryExtended[]> => {
const result = new Map();
entries.forEach((entry) => {
if (result.has(entry.domain)) {
result.get(entry.domain).push(entry);
} else {
result.set(entry.domain, [entry]);
}
});
return result;
};
@customElement("ha-config-integrations")
class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@ -65,9 +93,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@property() public route!: Route;
@property() private _configEntries: ConfigEntry[] = [];
@property() private _configEntries: ConfigEntryExtended[] = [];
@property() private _configEntriesInProgress: DataEntryFlowProgress[] = [];
@property()
private _configEntriesInProgress: DataEntryFlowProgressExtended[] = [];
@property() private _entityRegistryEntries: EntityRegistryEntry[] = [];
@ -79,6 +108,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
window.location.hash.substring(1)
);
@property() private _filter?: string;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
@ -87,18 +118,91 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._deviceRegistryEntries = entries;
}),
subscribeConfigFlowInProgress(this.hass, (flowsInProgress) => {
this._configEntriesInProgress = flowsInProgress;
for (const flow of flowsInProgress) {
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
const translationsPromisses: Promise<void>[] = [];
flowsInProgress.forEach((flow) => {
// To render title placeholders
if (flow.context.title_placeholders) {
this.hass.loadBackendTranslation("config", flow.handler);
translationsPromisses.push(
this.hass.loadBackendTranslation("config", flow.handler)
);
}
}
});
await Promise.all(translationsPromisses);
await nextRender();
this._configEntriesInProgress = flowsInProgress.map((flow) => {
return {
...flow,
localized_title: localizeConfigFlowTitle(this.hass.localize, flow),
};
});
}),
];
}
private _filterConfigEntries = memoizeOne(
(
configEntries: ConfigEntryExtended[],
filter?: string
): ConfigEntryExtended[] => {
if (!filter) {
return configEntries;
}
const options: Fuse.FuseOptions<ConfigEntryExtended> = {
keys: ["domain", "localized_domain_name", "title"],
caseSensitive: false,
minMatchCharLength: 2,
threshold: 0.2,
};
const fuse = new Fuse(configEntries, options);
return fuse.search(filter);
}
);
private _filterGroupConfigEntries = memoizeOne(
(
configEntries: ConfigEntryExtended[],
filter?: string
): [Map<string, ConfigEntryExtended[]>, ConfigEntryExtended[]] => {
const filteredConfigEnties = this._filterConfigEntries(
configEntries,
filter
);
const ignored: ConfigEntryExtended[] = [];
filteredConfigEnties.forEach((item, index) => {
if (item.source === "ignore") {
ignored.push(filteredConfigEnties.splice(index, 1)[0]);
}
});
return [groupByIntegration(filteredConfigEnties), ignored];
}
);
private _filterConfigEntriesInProgress = memoizeOne(
(
configEntriesInProgress: DataEntryFlowProgressExtended[],
filter?: string
): DataEntryFlowProgressExtended[] => {
configEntriesInProgress = configEntriesInProgress.map(
(flow: DataEntryFlowProgressExtended) => ({
...flow,
title: localizeConfigFlowTitle(this.hass.localize, flow),
})
);
if (!filter) {
return configEntriesInProgress;
}
const options: Fuse.FuseOptions<DataEntryFlowProgressExtended> = {
keys: ["handler", "localized_title"],
caseSensitive: false,
minMatchCharLength: 2,
threshold: 0.2,
};
const fuse = new Fuse(configEntriesInProgress, options);
return fuse.search(filter);
}
);
protected firstUpdated(changed: PropertyValues) {
super.firstUpdated(changed);
this._loadConfigEntries();
@ -114,18 +218,37 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._configEntries.length
) {
afterNextRender(() => {
const card = this.shadowRoot!.getElementById(
this._searchParms.get("config_entry")!
const entryId = this._searchParms.get("config_entry")!;
const configEntry = this._configEntries.find(
(entry) => entry.entry_id === entryId
);
if (!configEntry) {
return;
}
const card: HaIntegrationCard = this.shadowRoot!.querySelector(
`[data-domain=${configEntry?.domain}]`
) as HaIntegrationCard;
if (card) {
card.scrollIntoView();
card.scrollIntoView({
block: "center",
});
card.classList.add("highlight");
card.selectedConfigEntryId = entryId;
}
});
}
}
protected render(): TemplateResult {
const [
groupedConfigEntries,
ignoredConfigEntries,
] = this._filterGroupConfigEntries(this._configEntries, this._filter);
const configEntriesInProgress = this._filterConfigEntriesInProgress(
this._configEntriesInProgress,
this._filter
);
return html`
<hass-tabs-subpage
.hass=${this.hass}
@ -134,6 +257,21 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.route=${this.route}
.tabs=${configSections.integrations}
>
${this.narrow
? html`
<div slot="header">
<slot name="header">
<search-input
.filter=${this._filter}
class="header"
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
></search-input>
</slot>
</div>
`
: ""}
<paper-menu-button
close-on-activate
no-animations
@ -141,16 +279,12 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
horizontal-offset="-5"
slot="toolbar-icon"
>
<paper-icon-button
<ha-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
alt="menu"
></paper-icon-button>
<paper-listbox
slot="dropdown-content"
role="listbox"
selected="{{selectedItem}}"
>
></ha-icon-button>
<paper-listbox slot="dropdown-content" role="listbox">
<paper-item @tap=${this._toggleShowIgnored}>
${this.hass.localize(
this._showIgnored
@ -161,48 +295,63 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
</paper-listbox>
</paper-menu-button>
<div class="container">
${!this.narrow
? html`
<div class="search">
<search-input
no-label-float
no-underline
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
></search-input>
</div>
`
: ""}
<div
class="container"
@entry-removed=${this._handleRemoved}
@entry-updated=${this._handleUpdated}
>
${this._showIgnored
? this._configEntries
.filter((item) => item.source === "ignore")
.map(
(item: ConfigEntry) => html`
<ha-card class="ignored">
<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored"
? ignoredConfigEntries.map(
(item: ConfigEntryExtended) => html`
<ha-card class="ignored">
<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored"
)}
</div>
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>
${item.localized_domain_name}
</h2>
<mwc-button
@click=${this._removeIgnoredIntegration}
.entry=${item}
aria-label=${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}
</div>
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>
${domainToName(this.hass.localize, item.domain)}
</h2>
<mwc-button
@click=${this._removeIgnoredIntegration}
.entry=${item}
aria-label=${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}
>${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}</mwc-button
>
</div>
</ha-card>
`
)
>${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}</mwc-button
>
</div>
</ha-card>
`
)
: ""}
${this._configEntriesInProgress.length
? this._configEntriesInProgress.map(
(flow) => html`
${configEntriesInProgress.length
? configEntriesInProgress.map(
(flow: DataEntryFlowProgressExtended) => html`
<ha-card class="discovered">
<div class="header">
${this.hass.localize(
@ -219,153 +368,51 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
/>
</div>
<h2>
${localizeConfigFlowTitle(this.hass.localize, flow)}
${flow.localized_title}
</h2>
<mwc-button
unelevated
@click=${this._continueFlow}
.flowId=${flow.flow_id}
>
${this.hass.localize(
"ui.panel.config.integrations.configure"
)}
</mwc-button>
${DISCOVERY_SOURCES.includes(flow.context.source) &&
flow.context.unique_id
? html`
<mwc-button
@click=${this._ignoreFlow}
.flow=${flow}
>
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignore"
)}
</mwc-button>
`
: ""}
<div>
<mwc-button
unelevated
@click=${this._continueFlow}
.flowId=${flow.flow_id}
>
${this.hass.localize(
"ui.panel.config.integrations.configure"
)}
</mwc-button>
${DISCOVERY_SOURCES.includes(flow.context.source) &&
flow.context.unique_id
? html`
<mwc-button
@click=${this._ignoreFlow}
.flow=${flow}
>
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignore"
)}
</mwc-button>
`
: ""}
</div>
</div>
</ha-card>
`
)
: ""}
${this._configEntries.length
? this._configEntries.map((item: any) => {
const devices = this._getDevices(item);
const entities = this._getEntities(item);
const integrationName = domainToName(
this.hass.localize,
item.domain
);
return item.source === "ignore"
? ""
: html`
<ha-card
class="integration"
.configEntry=${item}
.id=${item.entry_id}
>
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h1>
${integrationName}
</h1>
<h2>
${integrationName === item.title
? html`&nbsp;`
: item.title}
</h2>
${devices.length || entities.length
? html`
<div>
${devices.length
? html`
<a
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.devices",
"count",
devices.length
)}</a
>
`
: ""}
${devices.length && entities.length
? "and"
: ""}
${entities.length
? html`
<a
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
"count",
entities.length
)}</a
>
`
: ""}
</div>
`
: ""}
</div>
<div class="card-actions">
<div>
<mwc-button @click=${this._editEntryName}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}</mwc-button
>
${item.supports_options
? html`
<mwc-button @click=${this._showOptions}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.options"
)}</mwc-button
>
`
: ""}
</div>
<paper-menu-button
horizontal-align="right"
vertical-align="top"
vertical-offset="40"
close-on-activate
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
aria-label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.options"
)}
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @tap=${this._showSystemOptions}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}</paper-item
>
<paper-item
class="warning"
@tap=${this._removeIntegration}
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}</paper-item
>
</paper-listbox>
</paper-menu-button>
</div>
</ha-card>
`;
})
: html`
${groupedConfigEntries.size
? Array.from(groupedConfigEntries.entries()).map(
([domain, items]) =>
html`<ha-integration-card
data-domain=${domain}
.hass=${this.hass}
.domain=${domain}
.items=${items}
.entityRegistryEntries=${this._entityRegistryEntries}
.deviceRegistryEntries=${this._deviceRegistryEntries}
></ha-integration-card>`
)
: !this._configEntries.length
? html`
<ha-card>
<div class="card-content">
<h1>
@ -383,7 +430,27 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
>
</div>
</ha-card>
`}
`
: ""}
${this._filter &&
!configEntriesInProgress.length &&
!groupedConfigEntries.size &&
this._configEntries.length
? html`
<div class="none-found">
<h1>
${this.hass.localize(
"ui.panel.config.integrations.none_found"
)}
</h1>
<p>
${this.hass.localize(
"ui.panel.config.integrations.none_found_detail"
)}
</p>
</div>
`
: ""}
</div>
<ha-fab
icon="hass:plus"
@ -400,12 +467,40 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
private _loadConfigEntries() {
getConfigEntries(this.hass).then((configEntries) => {
this._configEntries = configEntries.sort((conf1, conf2) =>
compare(conf1.domain + conf1.title, conf2.domain + conf2.title)
);
this._configEntries = configEntries
.map(
(entry: ConfigEntry): ConfigEntryExtended => ({
...entry,
localized_domain_name: domainToName(
this.hass.localize,
entry.domain
),
})
)
.sort((conf1, conf2) =>
caseInsensitiveCompare(
conf1.localized_domain_name + conf1.title,
conf2.localized_domain_name + conf2.title
)
);
});
}
private _handleRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
this._configEntries = this._configEntries.filter(
(entry) => entry.entry_id !== ev.detail.entryId
);
}
private _handleUpdated(ev: HASSDomEvent<ConfigEntryUpdatedEvent>) {
const newEntry = ev.detail.entry;
this._configEntries = this._configEntries!.map((entry) =>
entry.entry_id === newEntry.entry_id
? { ...newEntry, localized_domain_name: entry.localized_domain_name }
: entry
);
}
private _createFlow() {
showConfigFlowDialog(this, {
dialogClosedCallback: () => {
@ -428,9 +523,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
});
}
private _ignoreFlow(ev: Event) {
private async _ignoreFlow(ev: Event) {
const flow = (ev.target! as any).flow;
showConfirmationDialog(this, {
const confirmed = await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_ignore_title",
"name",
@ -442,11 +537,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
confirmText: this.hass!.localize(
"ui.panel.config.integrations.ignore.ignore"
),
confirm: () => {
ignoreConfigFlow(this.hass, flow.flow_id);
getConfigFlowInProgressCollection(this.hass.connection).refresh();
},
});
if (!confirmed) {
return;
}
await ignoreConfigFlow(this.hass, flow.flow_id);
this._loadConfigEntries();
getConfigFlowInProgressCollection(this.hass.connection).refresh();
}
private _toggleShowIgnored() {
@ -459,7 +556,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
title: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_delete_ignore_title",
"name",
this.hass.localize(`component.${entry.domain}.config.title`)
this.hass.localize(`component.${entry.domain}.title`)
),
text: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_delete_ignore"
@ -481,22 +578,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
});
}
private _getEntities(configEntry: ConfigEntry): EntityRegistryEntry[] {
if (!this._entityRegistryEntries) {
return [];
}
return this._entityRegistryEntries.filter(
(entity) => entity.config_entry_id === configEntry.entry_id
);
}
private _getDevices(configEntry: ConfigEntry): DeviceRegistryEntry[] {
if (!this._deviceRegistryEntries) {
return [];
}
return this._deviceRegistryEntries.filter((device) =>
device.config_entries.includes(configEntry.entry_id)
);
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
private _onImageLoad(ev) {
@ -507,64 +590,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
ev.target.style.visibility = "hidden";
}
private _showOptions(ev) {
showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
}
private _showSystemOptions(ev) {
showConfigEntrySystemOptionsDialog(this, {
entry: ev.target.closest("ha-card").configEntry,
});
}
private async _editEntryName(ev) {
const configEntry = ev.target.closest("ha-card").configEntry;
const newName = await showPromptDialog(this, {
title: this.hass.localize("ui.panel.config.integrations.rename_dialog"),
defaultValue: configEntry.title,
inputLabel: this.hass.localize(
"ui.panel.config.integrations.rename_input_label"
),
});
if (newName === null) {
return;
}
const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, {
title: newName,
});
this._configEntries = this._configEntries!.map((entry) =>
entry.entry_id === newEntry.entry_id ? newEntry : entry
);
}
private async _removeIntegration(ev) {
const entryId = ev.target.closest("ha-card").configEntry.entry_id;
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm"
),
});
if (!confirmed) {
return;
}
deleteConfigEntry(this.hass, entryId).then((result) => {
this._configEntries = this._configEntries.filter(
(entry) => entry.entry_id !== entryId
);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.restart_confirm"
),
});
}
});
}
static get styles(): CSSResult[] {
return [
haStyle,
@ -573,7 +598,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 16px 16px;
padding: 16px;
padding: 8px 16px 16px;
margin-bottom: 64px;
}
ha-card {
@ -582,9 +607,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
flex-direction: column;
justify-content: space-between;
}
ha-card.highlight {
border: 1px solid var(--accent-color);
}
.discovered {
border: 1px solid var(--primary-color);
}
@ -597,6 +619,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.ignored {
border: 1px solid var(--light-theme-disabled-color);
}
.ignored img {
filter: grayscale(1);
}
.ignored .header {
background: var(--light-theme-disabled-color);
color: var(--text-primary-color);
@ -604,23 +629,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
text-align: center;
}
.card-content {
display: flex;
height: 100%;
margin-top: 0;
padding: 16px;
text-align: center;
}
ha-card.integration .card-content {
padding-bottom: 3px;
}
.card-actions {
border-top: none;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding-right: 5px;
}
.helper {
display: inline-block;
height: 100%;
vertical-align: middle;
}
.image {
display: flex;
@ -630,12 +645,34 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
margin-bottom: 16px;
vertical-align: middle;
}
.none-found {
margin: auto;
text-align: center;
}
search-input.header {
display: block;
position: relative;
left: -8px;
top: -7px;
color: var(--secondary-text-color);
margin-left: 16px;
}
.search {
padding: 0 16px;
background: var(--sidebar-background-color);
border-bottom: 1px solid var(--divider-color);
}
.search search-input {
position: relative;
top: 2px;
}
img {
max-height: 60px;
max-height: 100%;
max-width: 90%;
}
a {
color: var(--primary-color);
.none-found {
margin: auto;
text-align: center;
}
h1 {
margin-bottom: 0;
@ -665,10 +702,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
left: 24px;
right: auto;
}
paper-menu-button {
color: var(--secondary-text-color);
padding: 0;
}
`,
];
}

View File

@ -0,0 +1,404 @@
import {
customElement,
LitElement,
property,
html,
CSSResult,
css,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../types";
import { ConfigEntryExtended } from "./ha-config-integrations";
import { domainToName } from "../../../data/integration";
import {
ConfigEntry,
updateConfigEntry,
deleteConfigEntry,
} from "../../../data/config_entries";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { DeviceRegistryEntry } from "../../../data/device_registry";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
import {
showPromptDialog,
showConfirmationDialog,
showAlertDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import "../../../components/ha-icon-next";
import { fireEvent } from "../../../common/dom/fire_event";
export interface ConfigEntryUpdatedEvent {
entry: ConfigEntry;
}
export interface ConfigEntryRemovedEvent {
entryId: string;
}
declare global {
// for fire event
interface HASSDomEvents {
"entry-updated": ConfigEntryUpdatedEvent;
"entry-removed": ConfigEntryRemovedEvent;
}
}
@customElement("ha-integration-card")
export class HaIntegrationCard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public domain!: string;
@property() public items!: ConfigEntryExtended[];
@property() public entityRegistryEntries!: EntityRegistryEntry[];
@property() public deviceRegistryEntries!: DeviceRegistryEntry[];
@property() public selectedConfigEntryId?: string;
protected render(): TemplateResult {
if (this.items.length === 1) {
return this._renderSingleEntry(this.items[0]);
}
if (this.selectedConfigEntryId) {
const configEntry = this.items.find(
(entry) => entry.entry_id === this.selectedConfigEntryId
);
if (configEntry) {
return this._renderSingleEntry(configEntry);
}
}
return this._renderGroupedIntegration();
}
private _renderGroupedIntegration(): TemplateResult {
return html`
<ha-card class="group">
<div class="group-header">
<img
src="https://brands.home-assistant.io/${this.domain}/icon.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
<h1>
${domainToName(this.hass.localize, this.domain)}
</h1>
</div>
<paper-listbox>
${this.items.map(
(item) =>
html`<paper-item
.entryId=${item.entry_id}
@click=${this._selectConfigEntry}
><paper-item-body>${item.title}</paper-item-body
><ha-icon-next></ha-icon-next
></paper-item>`
)}
</paper-listbox>
</ha-card>
`;
}
private _renderSingleEntry(item: ConfigEntryExtended): TemplateResult {
const devices = this._getDevices(item);
const entities = this._getEntities(item);
return html`
<ha-card
class="single integration"
.configEntry=${item}
.id=${item.entry_id}
>
${this.items.length > 1
? html`<ha-icon-button
class="back-btn"
icon="hass:chevron-left"
@click=${this._back}
></ha-icon-button>`
: ""}
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h1>
${item.localized_domain_name}
</h1>
<h2>
${item.localized_domain_name === item.title ? "" : item.title}
</h2>
${devices.length || entities.length
? html`
<div>
${devices.length
? html`
<a
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.devices",
"count",
devices.length
)}</a
>
`
: ""}
${devices.length && entities.length ? "and" : ""}
${entities.length
? html`
<a
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
"count",
entities.length
)}</a
>
`
: ""}
</div>
`
: ""}
</div>
<div class="card-actions">
<div>
<mwc-button @click=${this._editEntryName}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}</mwc-button
>
${item.supports_options
? html`
<mwc-button @click=${this._showOptions}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.options"
)}</mwc-button
>
`
: ""}
</div>
<paper-menu-button
horizontal-align="right"
vertical-align="top"
vertical-offset="40"
close-on-activate
>
<ha-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
aria-label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.options"
)}
></ha-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @tap=${this._showSystemOptions}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}</paper-item
>
<paper-item class="warning" @tap=${this._removeIntegration}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}</paper-item
>
</paper-listbox>
</paper-menu-button>
</div>
</ha-card>
`;
}
private _selectConfigEntry(ev: Event) {
this.selectedConfigEntryId = (ev.currentTarget as any).entryId;
}
private _back() {
this.selectedConfigEntryId = undefined;
this.classList.remove("highlight");
}
private _getEntities(configEntry: ConfigEntry): EntityRegistryEntry[] {
if (!this.entityRegistryEntries) {
return [];
}
return this.entityRegistryEntries.filter(
(entity) => entity.config_entry_id === configEntry.entry_id
);
}
private _getDevices(configEntry: ConfigEntry): DeviceRegistryEntry[] {
if (!this.deviceRegistryEntries) {
return [];
}
return this.deviceRegistryEntries.filter((device) =>
device.config_entries.includes(configEntry.entry_id)
);
}
private _onImageLoad(ev) {
ev.target.style.visibility = "initial";
}
private _onImageError(ev) {
ev.target.style.visibility = "hidden";
}
private _showOptions(ev) {
showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
}
private _showSystemOptions(ev) {
showConfigEntrySystemOptionsDialog(this, {
entry: ev.target.closest("ha-card").configEntry,
});
}
private async _editEntryName(ev) {
const configEntry = ev.target.closest("ha-card").configEntry;
const newName = await showPromptDialog(this, {
title: this.hass.localize("ui.panel.config.integrations.rename_dialog"),
defaultValue: configEntry.title,
inputLabel: this.hass.localize(
"ui.panel.config.integrations.rename_input_label"
),
});
if (newName === null) {
return;
}
const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, {
title: newName,
});
fireEvent(this, "entry-updated", { entry: newEntry });
}
private async _removeIntegration(ev) {
const entryId = ev.target.closest("ha-card").configEntry.entry_id;
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm"
),
});
if (!confirmed) {
return;
}
deleteConfigEntry(this.hass, entryId).then((result) => {
fireEvent(this, "entry-removed", { entryId });
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.restart_confirm"
),
});
}
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
max-width: 500px;
}
ha-card {
display: flex;
flex-direction: column;
height: 100%;
}
ha-card.single {
justify-content: space-between;
}
:host(.highlight) ha-card {
border: 1px solid var(--accent-color);
}
.card-content {
padding: 16px;
text-align: center;
}
ha-card.integration .card-content {
padding-bottom: 3px;
}
.card-actions {
border-top: none;
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 5px;
}
.group-header {
display: flex;
align-items: center;
height: 40px;
padding: 16px 16px 8px 16px;
vertical-align: middle;
}
.group-header h1 {
margin: 0;
}
.group-header img {
margin-right: 8px;
}
.image {
display: flex;
align-items: center;
justify-content: center;
height: 60px;
margin-bottom: 16px;
vertical-align: middle;
}
img {
max-height: 100%;
max-width: 90%;
}
.none-found {
margin: auto;
text-align: center;
}
a {
color: var(--primary-color);
}
h1 {
margin-bottom: 0;
}
h2 {
margin-top: 0;
min-height: 24px;
}
paper-menu-button {
color: var(--secondary-text-color);
padding: 0;
}
@media (min-width: 563px) {
paper-listbox {
max-height: 150px;
overflow: auto;
}
}
paper-item {
cursor: pointer;
min-height: 35px;
}
.back-btn {
position: absolute;
background: #ffffffe0;
border-radius: 50%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-integration-card": HaIntegrationCard;
}
}

View File

@ -18,6 +18,7 @@ import {
} from "../../../../components/data-table/ha-data-table";
import "../../../../components/ha-fab";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button";
import {
createDashboard,
deleteDashboard,
@ -153,11 +154,11 @@ export class HaConfigLovelaceDashboards extends LitElement {
template: (urlPath) =>
narrow
? html`
<paper-icon-button
<ha-icon-button
icon="hass:open-in-new"
.urlPath=${urlPath}
@click=${this._navigate}
></paper-icon-button>
></ha-icon-button>
`
: html`
<mwc-button .urlPath=${urlPath} @click=${this._navigate}

View File

@ -79,6 +79,7 @@ class DialogPersonDetail extends LitElement {
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="form">
<paper-input
dialogInitialFocus
.value=${this._name}
@value-changed=${this._nameChanged}
label="${this.hass!.localize(
@ -87,7 +88,8 @@ class DialogPersonDetail extends LitElement {
error-message="${this.hass!.localize(
"ui.panel.config.person.detail.name_error_msg"
)}"
.invalid=${nameInvalid}
required
auto-validate
></paper-input>
<ha-user-picker
label="${this.hass!.localize(

View File

@ -10,6 +10,14 @@ import {
import { HomeAssistant } from "../../../types";
import "./ha-scene-dashboard";
import "./ha-scene-editor";
import { debounce } from "../../../common/util/debounce";
const equal = (a: SceneEntity[], b: SceneEntity[]): boolean => {
if (a.length !== b.length) {
return false;
}
return a.every((scene, index) => scene === b[index]);
};
@customElement("ha-config-scene")
class HaConfigScene extends HassRouterPage {
@ -36,15 +44,18 @@ class HaConfigScene extends HassRouterPage {
},
};
private _computeScenes = memoizeOne((states: HassEntities) => {
const scenes: SceneEntity[] = [];
Object.values(states).forEach((state) => {
if (computeStateDomain(state) === "scene" && !state.attributes.hidden) {
scenes.push(state as SceneEntity);
}
});
private _debouncedUpdateScenes = debounce((pageEl) => {
const newScenes = this._getScenes(this.hass.states);
if (!equal(newScenes, pageEl.scenes)) {
pageEl.scenes = newScenes;
}
}, 10);
return scenes;
private _getScenes = memoizeOne((states: HassEntities): SceneEntity[] => {
return Object.values(states).filter(
(entity) =>
computeStateDomain(entity) === "scene" && !entity.attributes.hidden
) as SceneEntity[];
});
protected updatePageEl(pageEl, changedProps: PropertyValues) {
@ -55,7 +66,11 @@ class HaConfigScene extends HassRouterPage {
pageEl.showAdvanced = this.showAdvanced;
if (this.hass) {
pageEl.scenes = this._computeScenes(this.hass.states);
if (!pageEl.scenes || !changedProps) {
pageEl.scenes = this._getScenes(this.hass.states);
} else if (changedProps && changedProps.has("hass")) {
this._debouncedUpdateScenes(pageEl);
}
}
if (
@ -64,13 +79,7 @@ class HaConfigScene extends HassRouterPage {
) {
pageEl.creatingNew = undefined;
const sceneId = this.routeTail.path.substr(1);
pageEl.creatingNew = sceneId === "new";
pageEl.scene =
sceneId === "new"
? undefined
: pageEl.scenes.find(
(entity: SceneEntity) => entity.attributes.id === sceneId
);
pageEl.sceneId = sceneId === "new" ? null : sceneId;
}
}
}

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
@ -54,14 +54,14 @@ class HaSceneDashboard extends LitElement {
type: "icon-button",
template: (_toggle, scene) =>
html`
<paper-icon-button
<ha-icon-button
.scene=${scene}
icon="hass:play"
title="${this.hass.localize(
"ui.panel.config.scene.picker.activate_scene"
)}"
@click=${(ev: Event) => this._activateScene(ev)}
></paper-icon-button>
></ha-icon-button>
`,
},
name: {
@ -77,14 +77,14 @@ class HaSceneDashboard extends LitElement {
title: "",
type: "icon-button",
template: (_info, scene) => html`
<paper-icon-button
<ha-icon-button
.scene=${scene}
@click=${this._showInfo}
icon="hass:information-outline"
title="${this.hass.localize(
"ui.panel.config.scene.picker.show_info_scene"
)}"
></paper-icon-button>
></ha-icon-button>
`,
},
edit: {
@ -98,13 +98,13 @@ class HaSceneDashboard extends LitElement {
: undefined
)}
>
<paper-icon-button
<ha-icon-button
.icon=${scene.attributes.id ? "hass:pencil" : "hass:pencil-off"}
.disabled=${!scene.attributes.id}
title="${this.hass.localize(
"ui.panel.config.scene.picker.edit_scene"
)}"
></paper-icon-button>
></ha-icon-button>
</a>
${!scene.attributes.id
? html`
@ -137,11 +137,11 @@ class HaSceneDashboard extends LitElement {
)}
hasFab
>
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
icon="hass:help-circle"
@click=${this._showHelp}
></paper-icon-button>
></ha-icon-button>
</hass-tabs-subpage-data-table>
<a href="/config/scene/edit/new">
<ha-fab

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@ -24,7 +24,6 @@ import "../../../components/device/ha-device-picker";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-paper-icon-button-arrow-prev";
import {
computeDeviceName,
DeviceRegistryEntry,
@ -46,7 +45,10 @@ import {
SceneEntity,
SCENE_IGNORED_DOMAINS,
} from "../../../data/scene";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import {
showConfirmationDialog,
showAlertDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
@ -73,13 +75,13 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
@property() public route!: Route;
@property() public scene?: SceneEntity;
@property() public sceneId?: string;
@property() public creatingNew?: boolean;
@property() public scenes!: SceneEntity[];
@property() public showAdvanced!: boolean;
@property() private _dirty?: boolean;
@property() private _dirty = false;
@property() private _errors?: string;
@ -93,6 +95,8 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
@property() private _entityRegistryEntries: EntityRegistryEntry[] = [];
@property() private _scene?: SceneEntity;
private _storedStates: SceneEntities = {};
private _unsubscribeEvents?: () => void;
@ -172,8 +176,8 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
this._deviceEntityLookup,
this._deviceRegistryEntries
);
const name = this.scene
? computeStateName(this.scene)
const name = this._scene
? computeStateName(this._scene)
: this.hass.localize("ui.panel.config.scene.editor.default_name");
return html`
@ -184,17 +188,17 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
.backCallback=${() => this._backTapped()}
.tabs=${configSections.automation}
>
${this.creatingNew
${!this.sceneId
? ""
: html`
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
title="${this.hass.localize(
"ui.panel.config.scene.picker.delete_scene"
)}"
icon="hass:delete"
@click=${this._deleteTapped}
></paper-icon-button>
></ha-icon-button>
`}
${this._errors ? html` <div class="errors">${this._errors}</div> ` : ""}
${this.narrow ? html` <span slot="header">${name}</span> ` : ""}
@ -212,7 +216,7 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
<ha-card>
<div class="card-content">
<paper-input
.value=${this.scene ? computeStateName(this.scene) : ""}
.value=${this._scene ? computeStateName(this._scene) : ""}
@value-changed=${this._nameChanged}
label=${this.hass.localize(
"ui.panel.config.scene.editor.name"
@ -240,18 +244,18 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
<ha-card>
<div class="card-header">
${device.name}
<paper-icon-button
<ha-icon-button
icon="hass:delete"
title="${this.hass.localize(
"ui.panel.config.scene.editor.devices.delete"
)}"
.device=${device.id}
@click=${this._deleteDevice}
></paper-icon-button>
></ha-icon-button>
</div>
${device.entities.map((entityId) => {
const stateObj = this.hass.states[entityId];
if (!stateObj) {
const entityStateObj = this.hass.states[entityId];
if (!entityStateObj) {
return html``;
}
return html`
@ -261,11 +265,11 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
class="device-entity"
>
<state-badge
.stateObj=${stateObj}
.stateObj=${entityStateObj}
slot="item-icon"
></state-badge>
<paper-item-body>
${computeStateName(stateObj)}
${computeStateName(entityStateObj)}
</paper-item-body>
</paper-icon-item>
`;
@ -313,8 +317,8 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
)}
>
${entities.map((entityId) => {
const stateObj = this.hass.states[entityId];
if (!stateObj) {
const entityStateObj = this.hass.states[entityId];
if (!entityStateObj) {
return html``;
}
return html`
@ -324,20 +328,20 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
class="device-entity"
>
<state-badge
.stateObj=${stateObj}
.stateObj=${entityStateObj}
slot="item-icon"
></state-badge>
<paper-item-body>
${computeStateName(stateObj)}
${computeStateName(entityStateObj)}
</paper-item-body>
<paper-icon-button
<ha-icon-button
icon="hass:delete"
.entityId=${entityId}
.title="${this.hass.localize(
"ui.panel.config.scene.editor.entities.delete"
)}"
@click=${this._deleteEntity}
></paper-icon-button>
></ha-icon-button>
</paper-icon-item>
`;
})}
@ -386,19 +390,19 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
const oldscene = changedProps.get("scene") as SceneEntity;
const oldscene = changedProps.get("sceneId");
if (
changedProps.has("scene") &&
this.scene &&
changedProps.has("sceneId") &&
this.sceneId &&
this.hass &&
// Only refresh config if we picked a new scene. If same ID, don't fetch it.
(!oldscene || oldscene.attributes.id !== this.scene.attributes.id)
(!oldscene || oldscene !== this.sceneId)
) {
this._loadConfig();
}
if (changedProps.has("creatingNew") && this.creatingNew && this.hass) {
if (changedProps.has("sceneId") && !this.sceneId && this.hass) {
this._dirty = false;
const initData = getSceneEditorInitData();
this._config = {
@ -436,6 +440,29 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
}
}
}
if (
changedProps.has("scenes") &&
this.sceneId &&
this._config &&
!this._scene
) {
this._setScene();
}
}
private async _setScene() {
const scene = this.scenes.find(
(entity: SceneEntity) => entity.attributes.id === this.sceneId
);
if (!scene) {
return;
}
this._scene = scene;
const { context } = await activateScene(this.hass, this._scene.entity_id);
this._activateContextId = context.id;
this._unsubscribeEvents = await this.hass!.connection.subscribeEvents<
HassEvent
>((event) => this._stateChanged(event), "state_changed");
}
private _showMoreInfo(ev: Event) {
@ -446,20 +473,20 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
private async _loadConfig() {
let config: SceneConfig;
try {
config = await getSceneConfig(this.hass, this.scene!.attributes.id!);
config = await getSceneConfig(this.hass, this.sceneId!);
} catch (err) {
alert(
err.status_code === 404
? this.hass.localize(
"ui.panel.config.scene.editor.load_error_not_editable"
)
: this.hass.localize(
"ui.panel.config.scene.editor.load_error_unknown",
"err_no",
err.status_code
)
);
history.back();
showAlertDialog(this, {
text:
err.status_code === 404
? this.hass.localize(
"ui.panel.config.scene.editor.load_error_not_editable"
)
: this.hass.localize(
"ui.panel.config.scene.editor.load_error_unknown",
"err_no",
err.status_code
),
}).then(() => history.back());
return;
}
@ -469,13 +496,7 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
this._initEntities(config);
const { context } = await activateScene(this.hass, this.scene!.entity_id);
this._activateContextId = context.id;
this._unsubscribeEvents = await this.hass!.connection.subscribeEvents<
HassEvent
>((event) => this._stateChanged(event), "state_changed");
this._setScene();
this._dirty = false;
this._config = config;
@ -598,7 +619,7 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
}
private async _delete(): Promise<void> {
await deleteScene(this.hass, this.scene!.attributes.id!);
await deleteScene(this.hass, this.sceneId!);
applyScene(this.hass, this._storedStates);
history.back();
}
@ -634,13 +655,13 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
}
private async _saveScene(): Promise<void> {
const id = this.creatingNew ? "" + Date.now() : this.scene!.attributes.id!;
const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
this._config = { ...this._config, entities: this._calculateStates() };
try {
await saveScene(this.hass, id, this._config);
this._dirty = false;
if (this.creatingNew) {
if (!this.sceneId) {
navigate(this, `/config/scene/edit/${id}`, true);
}
} catch (err) {
@ -693,10 +714,10 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
paper-icon-item {
padding: 8px 16px;
}
ha-card paper-icon-button {
ha-card ha-icon-button {
color: var(--secondary-text-color);
}
.card-header > paper-icon-button {
.card-header > ha-icon-button {
float: right;
position: relative;
top: -8px;

View File

@ -1,4 +1,4 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { HassEntities } from "home-assistant-js-websocket";
import { customElement, property, PropertyValues } from "lit-element";
import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
@ -9,6 +9,15 @@ import {
import { HomeAssistant } from "../../../types";
import "./ha-script-editor";
import "./ha-script-picker";
import { debounce } from "../../../common/util/debounce";
import { ScriptEntity } from "../../../data/script";
const equal = (a: ScriptEntity[], b: ScriptEntity[]): boolean => {
if (a.length !== b.length) {
return false;
}
return a.every((enityA, index) => enityA === b[index]);
};
@customElement("ha-config-script")
class HaConfigScript extends HassRouterPage {
@ -20,7 +29,7 @@ class HaConfigScript extends HassRouterPage {
@property() public showAdvanced!: boolean;
@property() public scripts: HassEntity[] = [];
@property() public scripts: ScriptEntity[] = [];
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
@ -35,15 +44,18 @@ class HaConfigScript extends HassRouterPage {
},
};
private _computeScripts = memoizeOne((states: HassEntities) => {
const scripts: HassEntity[] = [];
Object.values(states).forEach((state) => {
if (computeStateDomain(state) === "script" && !state.attributes.hidden) {
scripts.push(state);
}
});
private _debouncedUpdateScripts = debounce((pageEl) => {
const newScript = this._getScripts(this.hass.states);
if (!equal(newScript, pageEl.scripts)) {
pageEl.scripts = newScript;
}
}, 10);
return scripts;
private _getScripts = memoizeOne((states: HassEntities): ScriptEntity[] => {
return Object.values(states).filter(
(entity) =>
computeStateDomain(entity) === "script" && !entity.attributes.hidden
) as ScriptEntity[];
});
protected firstUpdated(changedProps) {
@ -59,7 +71,11 @@ class HaConfigScript extends HassRouterPage {
pageEl.showAdvanced = this.showAdvanced;
if (this.hass) {
pageEl.scripts = this._computeScripts(this.hass.states);
if (!pageEl.scripts || !changedProps) {
pageEl.scripts = this._getScripts(this.hass.states);
} else if (changedProps && changedProps.has("hass")) {
this._debouncedUpdateScripts(pageEl);
}
}
if (
@ -68,13 +84,7 @@ class HaConfigScript extends HassRouterPage {
) {
pageEl.creatingNew = undefined;
const scriptEntityId = this.routeTail.path.substr(1);
pageEl.creatingNew = scriptEntityId === "new";
pageEl.script =
scriptEntityId === "new"
? undefined
: pageEl.scripts.find(
(entity: HassEntity) => entity.entity_id === scriptEntityId
);
pageEl.scriptEntityId = scriptEntityId === "new" ? null : scriptEntityId;
}
}
}

View File

@ -1,6 +1,6 @@
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 "../../../components/ha-icon-button";
import {
css,
CSSResult,
@ -16,13 +16,11 @@ import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-paper-icon-button-arrow-prev";
import {
Action,
deleteScript,
getScriptEditorInitData,
ScriptConfig,
ScriptEntity,
} from "../../../data/script";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/ha-app-layout";
@ -36,16 +34,14 @@ import { configSections } from "../ha-panel-config";
export class HaScriptEditor extends LitElement {
@property() public hass!: HomeAssistant;
@property() public script!: ScriptEntity;
@property() public scriptEntityId!: string;
@property() public route!: Route;
@property() public isWide?: boolean;
@property() public narrow!: boolean;
@property() public route!: Route;
@property() public creatingNew?: boolean;
@property() private _config?: ScriptConfig;
@property() private _dirty?: boolean;
@ -61,17 +57,17 @@ export class HaScriptEditor extends LitElement {
.backCallback=${() => this._backTapped()}
.tabs=${configSections.automation}
>
${this.creatingNew
${!this.scriptEntityId
? ""
: html`
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
title="${this.hass.localize(
"ui.panel.config.script.editor.delete_script"
)}"
icon="hass:delete"
@click=${this._deleteConfirm}
></paper-icon-button>
></ha-icon-button>
`}
${this.narrow
? html` <span slot="header">${this._config?.alias}</span> `
@ -161,18 +157,18 @@ export class HaScriptEditor extends LitElement {
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
const oldScript = changedProps.get("script") as ScriptEntity;
const oldScript = changedProps.get("scriptEntityId");
if (
changedProps.has("script") &&
this.script &&
changedProps.has("scriptEntityId") &&
this.scriptEntityId &&
this.hass &&
// Only refresh config if we picked a new script. If same ID, don't fetch it.
(!oldScript || oldScript.entity_id !== this.script.entity_id)
(!oldScript || oldScript !== this.scriptEntityId)
) {
this.hass
.callApi<ScriptConfig>(
"GET",
`config/script/config/${computeObjectId(this.script.entity_id)}`
`config/script/config/${computeObjectId(this.scriptEntityId)}`
)
.then(
(config) => {
@ -202,7 +198,11 @@ export class HaScriptEditor extends LitElement {
);
}
if (changedProps.has("creatingNew") && this.creatingNew && this.hass) {
if (
changedProps.has("scriptEntityId") &&
!this.scriptEntityId &&
this.hass
) {
const initData = getScriptEditorInitData();
this._dirty = !!initData;
this._config = {
@ -259,19 +259,19 @@ export class HaScriptEditor extends LitElement {
}
private async _delete() {
await deleteScript(this.hass, computeObjectId(this.script.entity_id));
await deleteScript(this.hass, computeObjectId(this.scriptEntityId));
history.back();
}
private _saveScript(): void {
const id = this.creatingNew
? "" + Date.now()
: computeObjectId(this.script.entity_id);
const id = this.scriptEntityId
? computeObjectId(this.scriptEntityId)
: Date.now();
this.hass!.callApi("POST", "config/script/config/" + id, this._config).then(
() => {
this._dirty = false;
if (this.creatingNew) {
if (!this.scriptEntityId) {
navigate(this, `/config/script/edit/${id}`, true);
}
},

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
@ -53,14 +53,14 @@ class HaScriptPicker extends LitElement {
type: "icon-button",
template: (_toggle, script) =>
html`
<paper-icon-button
<ha-icon-button
.script=${script}
icon="hass:play"
title="${this.hass.localize(
"ui.panel.config.script.picker.activate_script"
)}"
@click=${(ev: Event) => this._runScript(ev)}
></paper-icon-button>
></ha-icon-button>
`,
},
name: {
@ -88,14 +88,14 @@ class HaScriptPicker extends LitElement {
title: "",
type: "icon-button",
template: (_info, script) => html`
<paper-icon-button
<ha-icon-button
.script=${script}
@click=${this._showInfo}
icon="hass:information-outline"
title="${this.hass.localize(
"ui.panel.config.script.picker.show_info"
)}"
></paper-icon-button>
></ha-icon-button>
`,
},
edit: {
@ -103,12 +103,12 @@ class HaScriptPicker extends LitElement {
type: "icon-button",
template: (_info, script: any) => html`
<a href="/config/script/edit/${script.entity_id}">
<paper-icon-button
<ha-icon-button
icon="hass:pencil"
title="${this.hass.localize(
"ui.panel.config.script.picker.edit_script"
)}"
></paper-icon-button>
></ha-icon-button>
</a>
`,
},
@ -132,11 +132,11 @@ class HaScriptPicker extends LitElement {
)}
hasFab
>
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
icon="hass:help-circle"
@click=${this._showHelp}
></paper-icon-button>
></ha-icon-button>
</hass-tabs-subpage-data-table>
<a href="/config/script/edit/new">
<ha-fab

View File

@ -1,6 +1,5 @@
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 { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";

View File

@ -1,5 +1,5 @@
import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import {
css,
@ -86,11 +86,11 @@ class ZHAAddDevicesPage extends LitElement {
"ui.panel.config.zha.add_device_page.search_again"
)}
</mwc-button>
<paper-icon-button
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
></paper-icon-button>
></ha-icon-button>
${this._showHelp
? html`
<ha-service-description

View File

@ -1,6 +1,6 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@ -74,12 +74,12 @@ export class ZHAClusterAttributes extends LitElement {
"ui.panel.config.zha.cluster_attributes.header"
)}
</span>
<paper-icon-button
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
>
</paper-icon-button>
</ha-icon-button>
</div>
<span slot="introduction">
${this.hass!.localize(

View File

@ -1,5 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@ -68,12 +68,12 @@ export class ZHAClusterCommands extends LitElement {
"ui.panel.config.zha.cluster_commands.header"
)}
</span>
<paper-icon-button
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
>
</paper-icon-button>
</ha-icon-button>
</div>
<span slot="introduction">
${this.hass!.localize(

View File

@ -1,5 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
@ -63,12 +63,12 @@ export class ZHAClusters extends LitElement {
<span>
${this.hass!.localize("ui.panel.config.zha.clusters.header")}
</span>
<paper-icon-button
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
>
</paper-icon-button>
</ha-icon-button>
</div>
<span slot="introduction">
${this.hass!.localize("ui.panel.config.zha.clusters.introduction")}

View File

@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
@ -50,12 +50,12 @@ export class ZHADeviceBindingControl extends LitElement {
<ha-config-section .isWide="${this.isWide}">
<div class="header" slot="header">
<span>Device Binding</span>
<paper-icon-button
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
>
</paper-icon-button>
</ha-icon-button>
</div>
<span slot="introduction">Bind and unbind devices.</span>

View File

@ -1,4 +1,3 @@
import "@polymer/paper-icon-button/paper-icon-button";
import {
css,
CSSResult,
@ -10,7 +9,6 @@ import {
TemplateResult,
} from "lit-element";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-paper-icon-button-arrow-prev";
import {
Cluster,
fetchBindableDevices,

View File

@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
@ -80,12 +80,12 @@ export class ZHAGroupBindingControl extends LitElement {
"ui.panel.config.zha.group_binding.header"
)}</span
>
<paper-icon-button
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
>
</paper-icon-button>
</ha-icon-button>
</div>
<span slot="introduction"
>${this.hass!.localize(

View File

@ -1,5 +1,5 @@
import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import {
css,
@ -101,11 +101,11 @@ export class ZHAGroupPage extends LitElement {
return html`
<hass-subpage .header=${this.group.name}>
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
icon="hass:delete"
@click=${this._deleteGroup}
></paper-icon-button>
></ha-icon-button>
<ha-config-section .isWide=${this.isWide}>
<div class="header">
${this.hass.localize("ui.panel.config.zha.groups.group_info")}

View File

@ -1,5 +1,5 @@
import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import {
css,
@ -56,11 +56,11 @@ export class ZHAGroupsDashboard extends LitElement {
"ui.panel.config.zha.groups.groups-header"
)}
>
<paper-icon-button
<ha-icon-button
slot="toolbar-icon"
icon="hass:plus"
@click=${this._addGroup}
></paper-icon-button>
></ha-icon-button>
<div class="content">
${this._groups

View File

@ -1,4 +1,4 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import {
css,
CSSResult,
@ -37,11 +37,11 @@ export class ZHANode extends LitElement {
"ui.panel.config.zha.node_management.header"
)}</span
>
<paper-icon-button
<ha-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
></paper-icon-button>
></ha-icon-button>
</div>
<span slot="introduction">
${this.hass!.localize(

View File

@ -105,6 +105,7 @@ class DialogZoneDetail extends LitElement {
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="form">
<paper-input
dialogInitialFocus
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
@ -114,7 +115,8 @@ class DialogZoneDetail extends LitElement {
.errorMessage="${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg"
)}"
.invalid=${nameValid}
required
auto-validate
></paper-input>
<paper-input
.value=${this._icon}

View File

@ -152,11 +152,11 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
</paper-item-body>
${!this.narrow
? html`
<paper-icon-button
<ha-icon-button
icon="hass:pencil"
.entry=${entry}
@click=${this._openEditEntry}
></paper-icon-button>
></ha-icon-button>
`
: ""}
</paper-icon-item>
@ -174,7 +174,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
${state.attributes.friendly_name || state.entity_id}
</paper-item-body>
<div style="display:inline-block">
<paper-icon-button
<ha-icon-button
.entityId=${state.entity_id}
icon="hass:pencil"
@click=${this._openCoreConfig}
@ -185,7 +185,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
? undefined
: true
)}
></paper-icon-button>
></ha-icon-button>
<paper-tooltip position="left">
${state.entity_id === "zone.home"
? this.hass.localize(
@ -474,7 +474,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
overflow: hidden;
}
ha-icon,
paper-icon-button:not([disabled]) {
ha-icon-button:not([disabled]) {
color: var(--secondary-text-color);
}
.empty {

View File

@ -1,7 +1,7 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@ -14,7 +14,7 @@ import { sortStatesByName } from "../../../common/entity/states_sort_by_name";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-menu-button";
import "../../../components/ha-paper-icon-button-arrow-prev";
import "../../../components/ha-icon-button-arrow-prev";
import "../../../components/ha-service-description";
import "../../../layouts/ha-app-layout";
import { EventsMixin } from "../../../mixins/events-mixin";
@ -88,9 +88,9 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-app-layout has-scrolling-region="">
<app-header slot="header" fixed="">
<app-toolbar>
<ha-paper-icon-button-arrow-prev
<ha-icon-button-arrow-prev
on-click="_backTapped"
></ha-paper-icon-button-arrow-prev>
></ha-icon-button-arrow-prev>
<div main-title="">
[[localize('component.zwave.title')]]
</div>
@ -107,11 +107,11 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-config-section is-wide="[[isWide]]">
<div class="sectionHeader" slot="header">
<span>Z-Wave Node Management</span>
<paper-icon-button
<ha-icon-button
class="toggle-help-icon"
on-click="toggleHelp"
icon="hass:help-circle"
></paper-icon-button>
></ha-icon-button>
</div>
<span slot="introduction">
Run Z-Wave commands that affect a single node. Pick a node to see a

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