mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-10 13:39:43 +00:00
Compare commits
1 Commits
checkbox-s
...
thomaslove
Author | SHA1 | Date | |
---|---|---|---|
![]() |
813c9014e5 |
@@ -28,7 +28,9 @@
|
|||||||
"__BUILD__": false,
|
"__BUILD__": false,
|
||||||
"__VERSION__": false,
|
"__VERSION__": false,
|
||||||
"__STATIC_PATH__": false,
|
"__STATIC_PATH__": false,
|
||||||
"Polymer": true
|
"Polymer": true,
|
||||||
|
"webkitSpeechRecognition": false,
|
||||||
|
"ResizeObserver": false
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
@@ -104,6 +106,5 @@
|
|||||||
"lit/attribute-value-entities": 0
|
"lit/attribute-value-entities": 0
|
||||||
},
|
},
|
||||||
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
|
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
|
||||||
"processor": "disable/disable",
|
"processor": "disable/disable"
|
||||||
"ignorePatterns": ["src/resources/lit-virtualizer/*"]
|
|
||||||
}
|
}
|
||||||
|
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -6,6 +6,7 @@ on:
|
|||||||
- published
|
- published
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
WHEELS_TAG: 3.8-alpine3.12
|
||||||
PYTHON_VERSION: 3.8
|
PYTHON_VERSION: 3.8
|
||||||
NODE_VERSION: 12.1
|
NODE_VERSION: 12.1
|
||||||
|
|
||||||
@@ -63,9 +64,6 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
|
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
|
||||||
tag:
|
|
||||||
- "3.8-alpine3.12"
|
|
||||||
- "3.9-alpine3.13"
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download requirements.txt
|
- name: Download requirements.txt
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v2
|
||||||
@@ -75,7 +73,7 @@ jobs:
|
|||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@master
|
uses: home-assistant/wheels@master
|
||||||
with:
|
with:
|
||||||
tag: ${{ matrix.tag }}
|
tag: ${{ env.WHEELS_TAG }}
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-host: ${{ secrets.WHEELS_HOST }}
|
wheels-host: ${{ secrets.WHEELS_HOST }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
|
21
.gitignore
vendored
21
.gitignore
vendored
@@ -1,17 +1,10 @@
|
|||||||
.DS_Store
|
|
||||||
.reify-cache
|
|
||||||
|
|
||||||
# build
|
|
||||||
build
|
build
|
||||||
build-translations/*
|
build-translations/*
|
||||||
hass_frontend/*
|
|
||||||
dist
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
.yarn
|
|
||||||
yarn-error.log
|
|
||||||
node_modules/*
|
node_modules/*
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
.DS_Store
|
||||||
|
hass_frontend/*
|
||||||
|
.reify-cache
|
||||||
|
|
||||||
# Python stuff
|
# Python stuff
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
@@ -21,8 +14,11 @@ npm-debug.log
|
|||||||
# venv stuff
|
# venv stuff
|
||||||
pyvenv.cfg
|
pyvenv.cfg
|
||||||
pip-selfcheck.json
|
pip-selfcheck.json
|
||||||
venv/*
|
venv
|
||||||
.venv
|
.venv
|
||||||
|
lib
|
||||||
|
bin
|
||||||
|
dist
|
||||||
|
|
||||||
# vscode
|
# vscode
|
||||||
.vscode/*
|
.vscode/*
|
||||||
@@ -35,8 +31,9 @@ src/cast/dev_const.ts
|
|||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
.lokalise_token
|
.lokalise_token
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
# asdf
|
#asdf
|
||||||
.tool-versions
|
.tool-versions
|
||||||
|
|
||||||
# Home Assistant config
|
# Home Assistant config
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
require: "test-mocha/testconf.js",
|
|
||||||
timeout: 10000,
|
|
||||||
};
|
|
@@ -52,7 +52,6 @@ module.exports.terserOptions = (latestBuild) => ({
|
|||||||
|
|
||||||
module.exports.babelOptions = ({ latestBuild }) => ({
|
module.exports.babelOptions = ({ latestBuild }) => ({
|
||||||
babelrc: false,
|
babelrc: false,
|
||||||
compact: false,
|
|
||||||
presets: [
|
presets: [
|
||||||
!latestBuild && [
|
!latestBuild && [
|
||||||
"@babel/preset-env",
|
"@babel/preset-env",
|
||||||
@@ -80,6 +79,12 @@ module.exports.babelOptions = ({ latestBuild }) => ({
|
|||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Are already ES5, cause warnings when babelified.
|
||||||
|
module.exports.babelExclude = () => [
|
||||||
|
require.resolve("@mdi/js/mdi.js"),
|
||||||
|
require.resolve("hls.js"),
|
||||||
|
];
|
||||||
|
|
||||||
const outputPath = (outputRoot, latestBuild) =>
|
const outputPath = (outputRoot, latestBuild) =>
|
||||||
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
|
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
|
||||||
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
// Tasks to run webpack.
|
// Tasks to run webpack.
|
||||||
const fs = require("fs");
|
|
||||||
const gulp = require("gulp");
|
const gulp = require("gulp");
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const WebpackDevServer = require("webpack-dev-server");
|
const WebpackDevServer = require("webpack-dev-server");
|
||||||
@@ -19,11 +18,6 @@ const bothBuilds = (createConfigFunc, params) => [
|
|||||||
createConfigFunc({ ...params, latestBuild: false }),
|
createConfigFunc({ ...params, latestBuild: false }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const isWsl = fs
|
|
||||||
.readFileSync("/proc/version", "utf-8")
|
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes("microsoft");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
* compiler: import("webpack").Compiler,
|
* compiler: import("webpack").Compiler,
|
||||||
@@ -85,7 +79,7 @@ const prodBuild = (conf) =>
|
|||||||
gulp.task("webpack-watch-app", () => {
|
gulp.task("webpack-watch-app", () => {
|
||||||
// This command will run forever because we don't close compiler
|
// This command will run forever because we don't close compiler
|
||||||
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
|
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
|
||||||
{ ignored: /build-translations/, poll: isWsl },
|
{ ignored: /build-translations/ },
|
||||||
doneHandler()
|
doneHandler()
|
||||||
);
|
);
|
||||||
gulp.watch(
|
gulp.watch(
|
||||||
@@ -143,7 +137,7 @@ gulp.task("webpack-watch-hassio", () => {
|
|||||||
isProdBuild: false,
|
isProdBuild: false,
|
||||||
latestBuild: true,
|
latestBuild: true,
|
||||||
})
|
})
|
||||||
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler());
|
).watch({ ignored: /build-translations/ }, doneHandler());
|
||||||
|
|
||||||
gulp.watch(
|
gulp.watch(
|
||||||
path.join(paths.translations_src, "en.json"),
|
path.join(paths.translations_src, "en.json"),
|
||||||
|
@@ -52,11 +52,16 @@ const createRollupConfig = ({
|
|||||||
browser: true,
|
browser: true,
|
||||||
rootDir: paths.polymer_dir,
|
rootDir: paths.polymer_dir,
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs({
|
||||||
|
namedExports: {
|
||||||
|
"js-yaml": ["safeDump", "safeLoad"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
json(),
|
json(),
|
||||||
babel({
|
babel({
|
||||||
...bundle.babelOptions({ latestBuild }),
|
...bundle.babelOptions({ latestBuild }),
|
||||||
extensions,
|
extensions,
|
||||||
|
exclude: bundle.babelExclude(),
|
||||||
babelHelpers: isWDS ? "inline" : "bundled",
|
babelHelpers: isWDS ? "inline" : "bundled",
|
||||||
}),
|
}),
|
||||||
string({
|
string({
|
||||||
|
@@ -47,6 +47,7 @@ const createWebpackConfig = ({
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.m?js$|\.ts$/,
|
test: /\.m?js$|\.ts$/,
|
||||||
|
exclude: bundle.babelExclude(),
|
||||||
use: {
|
use: {
|
||||||
loader: "babel-loader",
|
loader: "babel-loader",
|
||||||
options: bundle.babelOptions({ latestBuild }),
|
options: bundle.babelOptions({ latestBuild }),
|
||||||
@@ -115,9 +116,8 @@ const createWebpackConfig = ({
|
|||||||
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
|
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
|
||||||
new webpack.NormalModuleReplacementPlugin(
|
new webpack.NormalModuleReplacementPlugin(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
path.resolve(
|
require.resolve(
|
||||||
paths.polymer_dir,
|
"@lit-labs/virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
|
||||||
"src/resources/lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")
|
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")
|
||||||
|
@@ -70,7 +70,7 @@ class HaDemo extends HomeAssistantAppEl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(href);
|
navigate(this, href);
|
||||||
},
|
},
|
||||||
{ capture: true }
|
{ capture: true }
|
||||||
);
|
);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
/* eslint-plugin-disable lit */
|
/* eslint-plugin-disable lit */
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
import { load } from "js-yaml";
|
import { safeLoad } from "js-yaml";
|
||||||
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
|
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
|
||||||
|
|
||||||
class DemoCard extends PolymerElement {
|
class DemoCard extends PolymerElement {
|
||||||
@@ -80,7 +80,7 @@ class DemoCard extends PolymerElement {
|
|||||||
card.removeChild(card.lastChild);
|
card.removeChild(card.lastChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = this._createCardElement(load(config.config)[0]);
|
const el = this._createCardElement(safeLoad(config.config)[0]);
|
||||||
card.appendChild(el);
|
card.appendChild(el);
|
||||||
this._getSize(el);
|
this._getSize(el);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { dump } from "js-yaml";
|
import { safeDump } from "js-yaml";
|
||||||
import { html, css, LitElement, TemplateResult } from "lit";
|
import { html, css, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
@@ -56,7 +56,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
(conf) => html`
|
(conf) => html`
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<span>${describeAction(this.hass, conf as any)}</span>
|
<span>${describeAction(this.hass, conf as any)}</span>
|
||||||
<pre>${dump(conf)}</pre>
|
<pre>${safeDump(conf)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { dump } from "js-yaml";
|
import { safeDump } from "js-yaml";
|
||||||
import { html, css, LitElement, TemplateResult } from "lit";
|
import { html, css, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
@@ -26,7 +26,7 @@ export class DemoAutomationDescribeCondition extends LitElement {
|
|||||||
(conf) => html`
|
(conf) => html`
|
||||||
<div class="condition">
|
<div class="condition">
|
||||||
<span>${describeCondition(conf as any)}</span>
|
<span>${describeCondition(conf as any)}</span>
|
||||||
<pre>${dump(conf)}</pre>
|
<pre>${safeDump(conf)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { dump } from "js-yaml";
|
import { safeDump } from "js-yaml";
|
||||||
import { html, css, LitElement, TemplateResult } from "lit";
|
import { html, css, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
@@ -29,7 +29,7 @@ export class DemoAutomationDescribeTrigger extends LitElement {
|
|||||||
(conf) => html`
|
(conf) => html`
|
||||||
<div class="trigger">
|
<div class="trigger">
|
||||||
<span>${describeTrigger(conf as any)}</span>
|
<span>${describeTrigger(conf as any)}</span>
|
||||||
<pre>${dump(conf)}</pre>
|
<pre>${safeDump(conf)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
@@ -28,11 +28,10 @@ const createConfigEntry = (
|
|||||||
title,
|
title,
|
||||||
source: "zeroconf",
|
source: "zeroconf",
|
||||||
state: "loaded",
|
state: "loaded",
|
||||||
|
connection_class: "local_push",
|
||||||
supports_options: false,
|
supports_options: false,
|
||||||
supports_unload: true,
|
supports_unload: true,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
pref_disable_new_entities: false,
|
|
||||||
pref_disable_polling: false,
|
|
||||||
reason: null,
|
reason: null,
|
||||||
...override,
|
...override,
|
||||||
});
|
});
|
||||||
@@ -65,9 +64,6 @@ const configPanelEntry = createConfigEntry("Config Panel", {
|
|||||||
const optionsFlowEntry = createConfigEntry("Options Flow", {
|
const optionsFlowEntry = createConfigEntry("Options Flow", {
|
||||||
supports_options: true,
|
supports_options: true,
|
||||||
});
|
});
|
||||||
const disabledPollingEntry = createConfigEntry("Disabled Polling", {
|
|
||||||
pref_disable_polling: true,
|
|
||||||
});
|
|
||||||
const setupErrorEntry = createConfigEntry("Setup Error", {
|
const setupErrorEntry = createConfigEntry("Setup Error", {
|
||||||
state: "setup_error",
|
state: "setup_error",
|
||||||
});
|
});
|
||||||
@@ -140,7 +136,6 @@ const configEntries: Array<{
|
|||||||
{ items: [loadedEntry] },
|
{ items: [loadedEntry] },
|
||||||
{ items: [configPanelEntry] },
|
{ items: [configPanelEntry] },
|
||||||
{ items: [optionsFlowEntry] },
|
{ items: [optionsFlowEntry] },
|
||||||
{ items: [disabledPollingEntry] },
|
|
||||||
{ items: [nameAsDomainEntry] },
|
{ items: [nameAsDomainEntry] },
|
||||||
{ items: [longNameEntry] },
|
{ items: [longNameEntry] },
|
||||||
{ items: [longNonBreakingNameEntry] },
|
{ items: [longNonBreakingNameEntry] },
|
||||||
|
@@ -120,7 +120,7 @@ class HassioAddonRepositoryEl extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _addonTapped(ev) {
|
private _addonTapped(ev) {
|
||||||
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}`);
|
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@@ -138,7 +138,7 @@ class HassioAddonStore extends LitElement {
|
|||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
const repositoryUrl = extractSearchParam("repository_url");
|
const repositoryUrl = extractSearchParam("repository_url");
|
||||||
navigate("/hassio/store", { replace: true });
|
navigate(this, "/hassio/store", true);
|
||||||
if (repositoryUrl) {
|
if (repositoryUrl) {
|
||||||
this._manageRepositories(repositoryUrl);
|
this._manageRepositories(repositoryUrl);
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,6 @@ import { ActionDetail } from "@material/mwc-list";
|
|||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiDotsVertical } from "@mdi/js";
|
import { mdiDotsVertical } from "@mdi/js";
|
||||||
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
|
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
|
||||||
import { DEFAULT_SCHEMA, Type } from "js-yaml";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -12,7 +11,7 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state, query } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
@@ -28,7 +27,6 @@ import {
|
|||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
HassioAddonSetOptionParams,
|
HassioAddonSetOptionParams,
|
||||||
setHassioAddonOption,
|
setHassioAddonOption,
|
||||||
validateHassioAddonOption,
|
|
||||||
} from "../../../../src/data/hassio/addon";
|
} from "../../../../src/data/hassio/addon";
|
||||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
@@ -40,13 +38,6 @@ import { hassioStyle } from "../../resources/hassio-style";
|
|||||||
|
|
||||||
const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"];
|
const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"];
|
||||||
|
|
||||||
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
|
||||||
new Type("!secret", {
|
|
||||||
kind: "scalar",
|
|
||||||
construct: (data) => `!secret ${data}`,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
@customElement("hassio-addon-config")
|
@customElement("hassio-addon-config")
|
||||||
class HassioAddonConfig extends LitElement {
|
class HassioAddonConfig extends LitElement {
|
||||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||||
@@ -134,7 +125,6 @@ class HassioAddonConfig extends LitElement {
|
|||||||
></ha-form>`
|
></ha-form>`
|
||||||
: html` <ha-yaml-editor
|
: html` <ha-yaml-editor
|
||||||
@value-changed=${this._configChanged}
|
@value-changed=${this._configChanged}
|
||||||
.schema=${ADDON_YAML_SCHEMA}
|
|
||||||
></ha-yaml-editor>`}
|
></ha-yaml-editor>`}
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||||
${!this._yamlMode ||
|
${!this._yamlMode ||
|
||||||
@@ -279,14 +269,6 @@ class HassioAddonConfig extends LitElement {
|
|||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const validation = await validateHassioAddonOption(
|
|
||||||
this.hass,
|
|
||||||
this.addon.slug,
|
|
||||||
this._editor?.value
|
|
||||||
);
|
|
||||||
if (!validation.valid) {
|
|
||||||
throw Error(validation.message);
|
|
||||||
}
|
|
||||||
await setHassioAddonOption(this.hass, this.addon.slug, {
|
await setHassioAddonOption(this.hass, this.addon.slug, {
|
||||||
options: this._yamlMode ? this._editor?.value : this._options,
|
options: this._yamlMode ? this._editor?.value : this._options,
|
||||||
});
|
});
|
||||||
|
@@ -175,7 +175,7 @@ class HassioAddonDashboard extends LitElement {
|
|||||||
if (!validAddon) {
|
if (!validAddon) {
|
||||||
this._error = this.supervisor.localize("my.error_addon_not_found");
|
this._error = this.supervisor.localize("my.error_addon_not_found");
|
||||||
} else {
|
} else {
|
||||||
navigate(`/hassio/addon/${requestedAddon}`, { replace: true });
|
navigate(this, `/hassio/addon/${requestedAddon}`, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -761,7 +761,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _openIngress(): void {
|
private _openIngress(): void {
|
||||||
navigate(`/hassio/ingress/${this.addon.slug}`);
|
navigate(this, `/hassio/ingress/${this.addon.slug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _computeShowIngressUI(): boolean {
|
private get _computeShowIngressUI(): boolean {
|
||||||
@@ -977,7 +977,6 @@ class HassioAddonInfo extends LitElement {
|
|||||||
showDialogSupervisorUpdate(this, {
|
showDialogSupervisorUpdate(this, {
|
||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
name: this.addon.name,
|
name: this.addon.name,
|
||||||
slug: this.addon.slug,
|
|
||||||
version: this.addon.version_latest,
|
version: this.addon.version_latest,
|
||||||
snapshotParams: {
|
snapshotParams: {
|
||||||
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
||||||
@@ -1052,7 +1051,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _openConfiguration(): void {
|
private _openConfiguration(): void {
|
||||||
navigate(`/hassio/addon/${this.addon.slug}/config`);
|
navigate(this, `/hassio/addon/${this.addon.slug}/config`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
||||||
|
@@ -1,54 +0,0 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import "../../../src/components/ha-svg-icon";
|
|
||||||
|
|
||||||
@customElement("supervisor-formfield-label")
|
|
||||||
class SupervisorFormfieldLabel extends LitElement {
|
|
||||||
@property({ type: String }) public label!: string;
|
|
||||||
|
|
||||||
@property({ type: String }) public imageUrl?: string;
|
|
||||||
|
|
||||||
@property({ type: String }) public iconPath?: string;
|
|
||||||
|
|
||||||
@property({ type: String }) public version?: string;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
${this.imageUrl
|
|
||||||
? html`<img loading="lazy" .src=${this.imageUrl} class="icon" />`
|
|
||||||
: this.iconPath
|
|
||||||
? html`<ha-svg-icon .path=${this.iconPath} class="icon"></ha-svg-icon>`
|
|
||||||
: ""}
|
|
||||||
<span class="label">${this.label}</span>
|
|
||||||
${this.version
|
|
||||||
? html`<span class="version">(${this.version})</span>`
|
|
||||||
: ""}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.label {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
.version {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
max-height: 22px;
|
|
||||||
max-width: 22px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"supervisor-formfield-label": SupervisorFormfieldLabel;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -64,7 +64,6 @@ class SupervisorMetric extends LitElement {
|
|||||||
.value {
|
.value {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -1,450 +0,0 @@
|
|||||||
import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js";
|
|
||||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { atLeastVersion } from "../../../src/common/config/version";
|
|
||||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
|
||||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
|
||||||
import { LocalizeFunc } from "../../../src/common/translations/localize";
|
|
||||||
import "../../../src/components/ha-checkbox";
|
|
||||||
import "../../../src/components/ha-formfield";
|
|
||||||
import "../../../src/components/ha-radio";
|
|
||||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
|
||||||
import {
|
|
||||||
HassioFullSnapshotCreateParams,
|
|
||||||
HassioPartialSnapshotCreateParams,
|
|
||||||
HassioSnapshotDetail,
|
|
||||||
} from "../../../src/data/hassio/snapshot";
|
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
|
||||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
|
||||||
import { HomeAssistant } from "../../../src/types";
|
|
||||||
import "./supervisor-formfield-label";
|
|
||||||
|
|
||||||
interface CheckboxItem {
|
|
||||||
slug: string;
|
|
||||||
checked: boolean;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AddonCheckboxItem extends CheckboxItem {
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _computeFolders = (folders): CheckboxItem[] => {
|
|
||||||
const list: CheckboxItem[] = [];
|
|
||||||
if (folders.includes("homeassistant")) {
|
|
||||||
list.push({
|
|
||||||
slug: "homeassistant",
|
|
||||||
name: "Home Assistant configuration",
|
|
||||||
checked: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (folders.includes("ssl")) {
|
|
||||||
list.push({ slug: "ssl", name: "SSL", checked: false });
|
|
||||||
}
|
|
||||||
if (folders.includes("share")) {
|
|
||||||
list.push({ slug: "share", name: "Share", checked: false });
|
|
||||||
}
|
|
||||||
if (folders.includes("media")) {
|
|
||||||
list.push({ slug: "media", name: "Media", checked: false });
|
|
||||||
}
|
|
||||||
if (folders.includes("addons/local")) {
|
|
||||||
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
|
|
||||||
}
|
|
||||||
return list.sort((a, b) => (a.name > b.name ? 1 : -1));
|
|
||||||
};
|
|
||||||
|
|
||||||
const _computeAddons = (addons): AddonCheckboxItem[] =>
|
|
||||||
addons
|
|
||||||
.map((addon) => ({
|
|
||||||
slug: addon.slug,
|
|
||||||
name: addon.name,
|
|
||||||
version: addon.version,
|
|
||||||
checked: false,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => (a.name > b.name ? 1 : -1));
|
|
||||||
|
|
||||||
@customElement("supervisor-snapshot-content")
|
|
||||||
export class SupervisorSnapshotContent extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public localize?: LocalizeFunc;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public snapshot?: HassioSnapshotDetail;
|
|
||||||
|
|
||||||
@property() public snapshotType: HassioSnapshotDetail["type"] = "full";
|
|
||||||
|
|
||||||
@property({ attribute: false }) public folders?: CheckboxItem[];
|
|
||||||
|
|
||||||
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public homeAssistant = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public snapshotHasPassword = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public onboarding = false;
|
|
||||||
|
|
||||||
@property() public snapshotName = "";
|
|
||||||
|
|
||||||
@property() public snapshotPassword = "";
|
|
||||||
|
|
||||||
@property() public confirmSnapshotPassword = "";
|
|
||||||
|
|
||||||
public willUpdate(changedProps) {
|
|
||||||
super.willUpdate(changedProps);
|
|
||||||
if (!this.hasUpdated) {
|
|
||||||
this.folders = _computeFolders(
|
|
||||||
this.snapshot
|
|
||||||
? this.snapshot.folders
|
|
||||||
: ["homeassistant", "ssl", "share", "media", "addons/local"]
|
|
||||||
);
|
|
||||||
this.addons = _computeAddons(
|
|
||||||
this.snapshot
|
|
||||||
? this.snapshot.addons
|
|
||||||
: this.supervisor?.supervisor.addons
|
|
||||||
);
|
|
||||||
this.snapshotType = this.snapshot?.type || "full";
|
|
||||||
this.snapshotName = this.snapshot?.name || "";
|
|
||||||
this.snapshotHasPassword = this.snapshot?.protected || false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _localize = (string: string) =>
|
|
||||||
this.supervisor?.localize(`snapshot.${string}`) ||
|
|
||||||
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (!this.onboarding && !this.supervisor) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
const foldersSection =
|
|
||||||
this.snapshotType === "partial" ? this._getSection("folders") : undefined;
|
|
||||||
const addonsSection =
|
|
||||||
this.snapshotType === "partial" ? this._getSection("addons") : undefined;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${this.snapshot
|
|
||||||
? html`<div class="details">
|
|
||||||
${this.snapshot.type === "full"
|
|
||||||
? this._localize("full_snapshot")
|
|
||||||
: this._localize("partial_snapshot")}
|
|
||||||
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
|
|
||||||
${this.hass
|
|
||||||
? formatDateTime(new Date(this.snapshot.date), this.hass.locale)
|
|
||||||
: this.snapshot.date}
|
|
||||||
</div>`
|
|
||||||
: html`<paper-input
|
|
||||||
name="snapshotName"
|
|
||||||
.label=${this.supervisor?.localize("snapshot.name") || "Name"}
|
|
||||||
.value=${this.snapshotName}
|
|
||||||
@value-changed=${this._handleTextValueChanged}
|
|
||||||
>
|
|
||||||
</paper-input>`}
|
|
||||||
${!this.snapshot || this.snapshot.type === "full"
|
|
||||||
? html`<div class="sub-header">
|
|
||||||
${!this.snapshot
|
|
||||||
? this._localize("type")
|
|
||||||
: this._localize("select_type")}
|
|
||||||
</div>
|
|
||||||
<div class="snapshot-types">
|
|
||||||
<ha-formfield .label=${this._localize("full_snapshot")}>
|
|
||||||
<ha-radio
|
|
||||||
@change=${this._handleRadioValueChanged}
|
|
||||||
value="full"
|
|
||||||
name="snapshotType"
|
|
||||||
.checked=${this.snapshotType === "full"}
|
|
||||||
>
|
|
||||||
</ha-radio>
|
|
||||||
</ha-formfield>
|
|
||||||
<ha-formfield .label=${this._localize("partial_snapshot")}>
|
|
||||||
<ha-radio
|
|
||||||
@change=${this._handleRadioValueChanged}
|
|
||||||
value="partial"
|
|
||||||
name="snapshotType"
|
|
||||||
.checked=${this.snapshotType === "partial"}
|
|
||||||
>
|
|
||||||
</ha-radio>
|
|
||||||
</ha-formfield>
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
${this.snapshotType === "partial"
|
|
||||||
? html`<div class="partial-picker">
|
|
||||||
${this.snapshot && this.snapshot.homeassistant
|
|
||||||
? html`
|
|
||||||
<ha-formfield
|
|
||||||
.label=${html`<supervisor-formfield-label
|
|
||||||
label="Home Assistant"
|
|
||||||
.iconPath=${mdiHomeAssistant}
|
|
||||||
.version=${this.snapshot.homeassistant}
|
|
||||||
>
|
|
||||||
</supervisor-formfield-label>`}
|
|
||||||
>
|
|
||||||
<ha-checkbox
|
|
||||||
.checked=${this.homeAssistant}
|
|
||||||
@click=${() => {
|
|
||||||
this.homeAssistant = !this.homeAssistant;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</ha-formfield>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${foldersSection?.templates.length
|
|
||||||
? html`
|
|
||||||
<ha-formfield
|
|
||||||
.label=${html`<supervisor-formfield-label
|
|
||||||
.label=${this._localize("folders")}
|
|
||||||
.iconPath=${mdiFolder}
|
|
||||||
>
|
|
||||||
</supervisor-formfield-label>`}
|
|
||||||
>
|
|
||||||
<ha-checkbox
|
|
||||||
@change=${this._toggleSection}
|
|
||||||
.checked=${foldersSection.checked}
|
|
||||||
.indeterminate=${foldersSection.indeterminate}
|
|
||||||
.section=${"folders"}
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</ha-formfield>
|
|
||||||
<div class="section-content">${foldersSection.templates}</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${addonsSection?.templates.length
|
|
||||||
? html`
|
|
||||||
<ha-formfield
|
|
||||||
.label=${html`<supervisor-formfield-label
|
|
||||||
.label=${this._localize("addons")}
|
|
||||||
.iconPath=${mdiPuzzle}
|
|
||||||
>
|
|
||||||
</supervisor-formfield-label>`}
|
|
||||||
>
|
|
||||||
<ha-checkbox
|
|
||||||
@change=${this._toggleSection}
|
|
||||||
.checked=${addonsSection.checked}
|
|
||||||
.indeterminate=${addonsSection.indeterminate}
|
|
||||||
.section=${"addons"}
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</ha-formfield>
|
|
||||||
<div class="section-content">${addonsSection.templates}</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div> `
|
|
||||||
: ""}
|
|
||||||
${this.snapshotType === "partial" &&
|
|
||||||
(!this.snapshot || this.snapshotHasPassword)
|
|
||||||
? html`<hr />`
|
|
||||||
: ""}
|
|
||||||
${!this.snapshot
|
|
||||||
? html`<ha-formfield
|
|
||||||
class="password"
|
|
||||||
.label=${this._localize("password_protection")}
|
|
||||||
>
|
|
||||||
<ha-checkbox
|
|
||||||
.checked=${this.snapshotHasPassword}
|
|
||||||
@change=${this._toggleHasPassword}
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</ha-formfield>`
|
|
||||||
: ""}
|
|
||||||
${this.snapshotHasPassword
|
|
||||||
? html`
|
|
||||||
<paper-input
|
|
||||||
.label=${this._localize("password")}
|
|
||||||
type="password"
|
|
||||||
name="snapshotPassword"
|
|
||||||
.value=${this.snapshotPassword}
|
|
||||||
@value-changed=${this._handleTextValueChanged}
|
|
||||||
>
|
|
||||||
</paper-input>
|
|
||||||
${!this.snapshot
|
|
||||||
? html` <paper-input
|
|
||||||
.label=${this.supervisor?.localize("confirm_password")}
|
|
||||||
type="password"
|
|
||||||
name="confirmSnapshotPassword"
|
|
||||||
.value=${this.confirmSnapshotPassword}
|
|
||||||
@value-changed=${this._handleTextValueChanged}
|
|
||||||
>
|
|
||||||
</paper-input>`
|
|
||||||
: ""}
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
.partial-picker ha-formfield {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.partial-picker ha-checkbox {
|
|
||||||
--mdc-checkbox-touch-target-size: 32px;
|
|
||||||
}
|
|
||||||
.partial-picker {
|
|
||||||
display: block;
|
|
||||||
margin: 0px -6px;
|
|
||||||
}
|
|
||||||
supervisor-formfield-label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
hr {
|
|
||||||
border-color: var(--divider-color);
|
|
||||||
border-bottom: none;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
.details {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
.section-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
ha-formfield.password {
|
|
||||||
display: block;
|
|
||||||
margin: 0 -14px -16px;
|
|
||||||
}
|
|
||||||
.snapshot-types {
|
|
||||||
display: flex;
|
|
||||||
margin-left: -13px;
|
|
||||||
}
|
|
||||||
.sub-header {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public snapshotDetails():
|
|
||||||
| HassioPartialSnapshotCreateParams
|
|
||||||
| HassioFullSnapshotCreateParams {
|
|
||||||
const data: any = {};
|
|
||||||
|
|
||||||
if (!this.snapshot) {
|
|
||||||
data.name = this.snapshotName || formatDate(new Date(), this.hass.locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.snapshotHasPassword) {
|
|
||||||
data.password = this.snapshotPassword;
|
|
||||||
if (!this.snapshot) {
|
|
||||||
data.confirm_password = this.confirmSnapshotPassword;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.snapshotType === "full") {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const addons = this.addons
|
|
||||||
?.filter((addon) => addon.checked)
|
|
||||||
.map((addon) => addon.slug);
|
|
||||||
const folders = this.folders
|
|
||||||
?.filter((folder) => folder.checked)
|
|
||||||
.map((folder) => folder.slug);
|
|
||||||
|
|
||||||
if (addons?.length) {
|
|
||||||
data.addons = addons;
|
|
||||||
}
|
|
||||||
if (folders?.length) {
|
|
||||||
data.folders = folders;
|
|
||||||
}
|
|
||||||
if (this.homeAssistant) {
|
|
||||||
data.homeassistant = this.homeAssistant;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getSection(section: string) {
|
|
||||||
const templates: TemplateResult[] = [];
|
|
||||||
const addons =
|
|
||||||
section === "addons"
|
|
||||||
? new Map(
|
|
||||||
this.supervisor?.addon.addons.map((item) => [item.slug, item])
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
let checkedItems = 0;
|
|
||||||
this[section].forEach((item) => {
|
|
||||||
templates.push(html`<ha-formfield
|
|
||||||
.label=${html`<supervisor-formfield-label
|
|
||||||
.label=${item.name}
|
|
||||||
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
|
|
||||||
.imageUrl=${section === "addons" &&
|
|
||||||
!this.onboarding &&
|
|
||||||
atLeastVersion(this.hass.config.version, 0, 105) &&
|
|
||||||
addons?.get(item.slug)?.icon
|
|
||||||
? `/api/hassio/addons/${item.slug}/icon`
|
|
||||||
: undefined}
|
|
||||||
.version=${item.version}
|
|
||||||
>
|
|
||||||
</supervisor-formfield-label>`}
|
|
||||||
>
|
|
||||||
<ha-checkbox
|
|
||||||
.item=${item}
|
|
||||||
.checked=${item.checked}
|
|
||||||
.section=${section}
|
|
||||||
@change=${this._updateSectionEntry}
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</ha-formfield>`);
|
|
||||||
|
|
||||||
if (item.checked) {
|
|
||||||
checkedItems++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const checked = checkedItems === this[section].length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
templates,
|
|
||||||
checked,
|
|
||||||
indeterminate: !checked && checkedItems !== 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleRadioValueChanged(ev: CustomEvent) {
|
|
||||||
const input = ev.currentTarget as HaRadio;
|
|
||||||
this[input.name] = input.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
|
|
||||||
const input = ev.currentTarget as PaperInputElement;
|
|
||||||
this[input.name!] = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleHasPassword(): void {
|
|
||||||
this.snapshotHasPassword = !this.snapshotHasPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleSection(ev): void {
|
|
||||||
const section = ev.currentTarget.section;
|
|
||||||
|
|
||||||
this[section] = (section === "addons" ? this.addons : this.folders)!.map(
|
|
||||||
(item) => ({
|
|
||||||
...item,
|
|
||||||
checked: ev.currentTarget.checked,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateSectionEntry(ev): void {
|
|
||||||
const item = ev.currentTarget.item;
|
|
||||||
const section = ev.currentTarget.section;
|
|
||||||
this[section] = this[section].map((entry) =>
|
|
||||||
entry.slug === item.slug
|
|
||||||
? {
|
|
||||||
...entry,
|
|
||||||
checked: ev.currentTarget.checked,
|
|
||||||
}
|
|
||||||
: entry
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"supervisor-snapshot-content": SupervisorSnapshotContent;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -96,11 +96,11 @@ class HassioAddons extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _addonTapped(ev: any): void {
|
private _addonTapped(ev: any): void {
|
||||||
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}/info`);
|
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}/info`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openStore(): void {
|
private _openStore(): void {
|
||||||
navigate("/hassio/store");
|
navigate(this, "/hassio/store");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -161,7 +161,6 @@ export class HassioUpdate extends LitElement {
|
|||||||
showDialogSupervisorUpdate(this, {
|
showDialogSupervisorUpdate(this, {
|
||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
name: "Home Assistant Core",
|
name: "Home Assistant Core",
|
||||||
slug: "core",
|
|
||||||
version: this.supervisor.core.version_latest,
|
version: this.supervisor.core.version_latest,
|
||||||
snapshotParams: {
|
snapshotParams: {
|
||||||
name: `core_${this.supervisor.core.version}`,
|
name: `core_${this.supervisor.core.version}`,
|
||||||
|
@@ -1,194 +0,0 @@
|
|||||||
import { mdiClose } from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
|
||||||
import "../../../../src/common/search/search-input";
|
|
||||||
import { compare } from "../../../../src/common/string/compare";
|
|
||||||
import "../../../../src/components/ha-dialog";
|
|
||||||
import "../../../../src/components/ha-expansion-panel";
|
|
||||||
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
|
|
||||||
import { dump } from "../../../../src/resources/js-yaml-dump";
|
|
||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
|
||||||
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
|
|
||||||
|
|
||||||
const _filterDevices = memoizeOne(
|
|
||||||
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
|
|
||||||
hardware.devices
|
|
||||||
.filter(
|
|
||||||
(device) =>
|
|
||||||
(showAdvanced ||
|
|
||||||
["tty", "gpio", "input"].includes(device.subsystem)) &&
|
|
||||||
(device.by_id?.toLowerCase().includes(filter) ||
|
|
||||||
device.name.toLowerCase().includes(filter) ||
|
|
||||||
device.dev_path.toLocaleLowerCase().includes(filter) ||
|
|
||||||
JSON.stringify(device.attributes)
|
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes(filter))
|
|
||||||
)
|
|
||||||
.sort((a, b) => compare(a.name, b.name))
|
|
||||||
);
|
|
||||||
|
|
||||||
@customElement("dialog-hassio-hardware")
|
|
||||||
class HassioHardwareDialog extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@state() private _dialogParams?: HassioHardwareDialogParams;
|
|
||||||
|
|
||||||
@state() private _filter?: string;
|
|
||||||
|
|
||||||
public showDialog(params: HassioHardwareDialogParams) {
|
|
||||||
this._dialogParams = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
public closeDialog() {
|
|
||||||
this._dialogParams = undefined;
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (!this._dialogParams) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
const devices = _filterDevices(
|
|
||||||
this.hass.userData?.showAdvanced || false,
|
|
||||||
this._dialogParams.hardware,
|
|
||||||
(this._filter || "").toLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-dialog
|
|
||||||
open
|
|
||||||
scrimClickAction
|
|
||||||
hideActions
|
|
||||||
@closed=${this.closeDialog}
|
|
||||||
.heading=${true}
|
|
||||||
>
|
|
||||||
<div class="header" slot="heading">
|
|
||||||
<h2>
|
|
||||||
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
|
|
||||||
</h2>
|
|
||||||
<mwc-icon-button dialogAction="close">
|
|
||||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
|
||||||
</mwc-icon-button>
|
|
||||||
<search-input
|
|
||||||
autofocus
|
|
||||||
no-label-float
|
|
||||||
.filter=${this._filter}
|
|
||||||
@value-changed=${this._handleSearchChange}
|
|
||||||
.label=${this._dialogParams.supervisor.localize(
|
|
||||||
"dialog.hardware.search"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
</search-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${devices.map(
|
|
||||||
(device) =>
|
|
||||||
html`<ha-expansion-panel
|
|
||||||
.header=${device.name}
|
|
||||||
.secondary=${device.by_id || undefined}
|
|
||||||
outlined
|
|
||||||
>
|
|
||||||
<div class="device-property">
|
|
||||||
<span>
|
|
||||||
${this._dialogParams!.supervisor.localize(
|
|
||||||
"dialog.hardware.subsystem"
|
|
||||||
)}:
|
|
||||||
</span>
|
|
||||||
<span>${device.subsystem}</span>
|
|
||||||
</div>
|
|
||||||
<div class="device-property">
|
|
||||||
<span>
|
|
||||||
${this._dialogParams!.supervisor.localize(
|
|
||||||
"dialog.hardware.device_path"
|
|
||||||
)}:
|
|
||||||
</span>
|
|
||||||
<code>${device.dev_path}</code>
|
|
||||||
</div>
|
|
||||||
${device.by_id
|
|
||||||
? html` <div class="device-property">
|
|
||||||
<span>
|
|
||||||
${this._dialogParams!.supervisor.localize(
|
|
||||||
"dialog.hardware.id"
|
|
||||||
)}:
|
|
||||||
</span>
|
|
||||||
<code>${device.by_id}</code>
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
<div class="attributes">
|
|
||||||
<span>
|
|
||||||
${this._dialogParams!.supervisor.localize(
|
|
||||||
"dialog.hardware.attributes"
|
|
||||||
)}:
|
|
||||||
</span>
|
|
||||||
<pre>${dump(device.attributes, { indent: 2 })}</pre>
|
|
||||||
</div>
|
|
||||||
</ha-expansion-panel>`
|
|
||||||
)}
|
|
||||||
</ha-dialog>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleSearchChange(ev: CustomEvent) {
|
|
||||||
this._filter = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
haStyleDialog,
|
|
||||||
css`
|
|
||||||
mwc-icon-button {
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
top: 10px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
margin: 18px 42px 0 18px;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-expansion-panel {
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
pre,
|
|
||||||
code {
|
|
||||||
background-color: var(--markdown-code-background-color, none);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
padding: 16px;
|
|
||||||
overflow: auto;
|
|
||||||
line-height: 1.45;
|
|
||||||
font-family: var(--code-font-family, monospace);
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
font-size: 85%;
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
}
|
|
||||||
search-input {
|
|
||||||
margin: 0 16px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.device-property {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.attributes {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"dialog-hassio-hardware": HassioHardwareDialog;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
|
||||||
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
|
|
||||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
|
||||||
|
|
||||||
export interface HassioHardwareDialogParams {
|
|
||||||
supervisor: Supervisor;
|
|
||||||
hardware: HassioHardwareInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const showHassioHardwareDialog = (
|
|
||||||
element: HTMLElement,
|
|
||||||
dialogParams: HassioHardwareDialogParams
|
|
||||||
): void => {
|
|
||||||
fireEvent(element, "show-dialog", {
|
|
||||||
dialogTag: "dialog-hassio-hardware",
|
|
||||||
dialogImport: () => import("./dialog-hassio-hardware"),
|
|
||||||
dialogParams,
|
|
||||||
});
|
|
||||||
};
|
|
@@ -45,7 +45,7 @@ class HassioRegistriesDialog extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
.open=${this._opened}
|
.open=${this._opened}
|
||||||
@closed=${this.closeDialog}
|
@closing=${this.closeDialog}
|
||||||
scrimClickAction
|
scrimClickAction
|
||||||
escapeKeyAction
|
escapeKeyAction
|
||||||
.heading=${createCloseHeading(
|
.heading=${createCloseHeading(
|
||||||
@@ -244,6 +244,9 @@ class HassioRegistriesDialog extends LitElement {
|
|||||||
mwc-list-item span[slot="secondary"] {
|
mwc-list-item span[slot="secondary"] {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
ha-paper-dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -67,7 +67,7 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
.open=${this._opened}
|
.open=${this._opened}
|
||||||
@closed=${this.closeDialog}
|
@closing=${this.closeDialog}
|
||||||
scrimClickAction
|
scrimClickAction
|
||||||
escapeKeyAction
|
escapeKeyAction
|
||||||
.heading=${createCloseHeading(
|
.heading=${createCloseHeading(
|
||||||
@@ -150,6 +150,9 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
mwc-button {
|
mwc-button {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
ha-paper-dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
ha-circular-progress {
|
ha-circular-progress {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 32px;
|
margin: 32px;
|
||||||
|
@@ -1,43 +1,91 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
|
import "@polymer/paper-input/paper-input";
|
||||||
|
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { formatDate } from "../../../../src/common/datetime/format_date";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import { compare } from "../../../../src/common/string/compare";
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
|
import "../../../../src/components/ha-checkbox";
|
||||||
|
import type { HaCheckbox } from "../../../../src/components/ha-checkbox";
|
||||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||||
|
import "../../../../src/components/ha-formfield";
|
||||||
|
import "../../../../src/components/ha-radio";
|
||||||
|
import type { HaRadio } from "../../../../src/components/ha-radio";
|
||||||
|
import "../../../../src/components/ha-settings-row";
|
||||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
createHassioFullSnapshot,
|
createHassioFullSnapshot,
|
||||||
createHassioPartialSnapshot,
|
createHassioPartialSnapshot,
|
||||||
|
HassioFullSnapshotCreateParams,
|
||||||
|
HassioPartialSnapshotCreateParams,
|
||||||
|
HassioSnapshot,
|
||||||
} from "../../../../src/data/hassio/snapshot";
|
} from "../../../../src/data/hassio/snapshot";
|
||||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||||
|
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import "../../components/supervisor-snapshot-content";
|
|
||||||
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
|
|
||||||
import { HassioCreateSnapshotDialogParams } from "./show-dialog-hassio-create-snapshot";
|
import { HassioCreateSnapshotDialogParams } from "./show-dialog-hassio-create-snapshot";
|
||||||
|
|
||||||
|
interface CheckboxItem {
|
||||||
|
slug: string;
|
||||||
|
checked: boolean;
|
||||||
|
name?: string;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderList = () => [
|
||||||
|
{
|
||||||
|
slug: "homeassistant",
|
||||||
|
checked: true,
|
||||||
|
},
|
||||||
|
{ slug: "ssl", checked: true },
|
||||||
|
{ slug: "share", checked: true },
|
||||||
|
{ slug: "media", checked: true },
|
||||||
|
{ slug: "addons/local", checked: true },
|
||||||
|
];
|
||||||
|
|
||||||
@customElement("dialog-hassio-create-snapshot")
|
@customElement("dialog-hassio-create-snapshot")
|
||||||
class HassioCreateSnapshotDialog extends LitElement {
|
class HassioCreateSnapshotDialog extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _snapshotName = "";
|
||||||
|
|
||||||
|
@state() private _snapshotPassword = "";
|
||||||
|
|
||||||
|
@state() private _snapshotHasPassword = false;
|
||||||
|
|
||||||
|
@state() private _snapshotType: HassioSnapshot["type"] = "full";
|
||||||
|
|
||||||
@state() private _dialogParams?: HassioCreateSnapshotDialogParams;
|
@state() private _dialogParams?: HassioCreateSnapshotDialogParams;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _addonList: CheckboxItem[] = [];
|
||||||
|
|
||||||
@state() private _creatingSnapshot = false;
|
@state() private _folderList: CheckboxItem[] = folderList();
|
||||||
|
|
||||||
@query("supervisor-snapshot-content")
|
@state() private _error = "";
|
||||||
private _snapshotContent!: SupervisorSnapshotContent;
|
|
||||||
|
|
||||||
public showDialog(params: HassioCreateSnapshotDialogParams) {
|
public showDialog(params: HassioCreateSnapshotDialogParams) {
|
||||||
this._dialogParams = params;
|
this._dialogParams = params;
|
||||||
this._creatingSnapshot = false;
|
this._addonList = this._dialogParams.supervisor.supervisor.addons
|
||||||
|
.map((addon) => ({
|
||||||
|
slug: addon.slug,
|
||||||
|
name: addon.name,
|
||||||
|
version: addon.version,
|
||||||
|
checked: true,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => compare(a.name, b.name));
|
||||||
|
this._snapshotType = "full";
|
||||||
|
this._error = "";
|
||||||
|
this._folderList = folderList();
|
||||||
|
this._snapshotHasPassword = false;
|
||||||
|
this._snapshotPassword = "";
|
||||||
|
this._snapshotName = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this._dialogParams = undefined;
|
this._dialogParams = undefined;
|
||||||
this._creatingSnapshot = false;
|
|
||||||
this._error = undefined;
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,36 +96,179 @@ class HassioCreateSnapshotDialog extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
scrimClickAction
|
@closing=${this.closeDialog}
|
||||||
@closed=${this.closeDialog}
|
|
||||||
.heading=${createCloseHeading(
|
.heading=${createCloseHeading(
|
||||||
this.hass,
|
this.hass,
|
||||||
this._dialogParams.supervisor.localize("snapshot.create_snapshot")
|
this._dialogParams.supervisor.localize("snapshot.create_snapshot")
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${this._creatingSnapshot
|
<paper-input
|
||||||
? html` <ha-circular-progress active></ha-circular-progress>`
|
name="snapshotName"
|
||||||
: html`<supervisor-snapshot-content
|
.label=${this._dialogParams.supervisor.localize("snapshot.name")}
|
||||||
.hass=${this.hass}
|
.value=${this._snapshotName}
|
||||||
.supervisor=${this._dialogParams.supervisor}
|
@value-changed=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
</supervisor-snapshot-content>`}
|
</paper-input>
|
||||||
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
|
<div class="snapshot-types">
|
||||||
|
<div>
|
||||||
|
${this._dialogParams.supervisor.localize("snapshot.type")}:
|
||||||
|
</div>
|
||||||
|
<ha-formfield
|
||||||
|
.label=${this._dialogParams.supervisor.localize(
|
||||||
|
"snapshot.full_snapshot"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-radio
|
||||||
|
@change=${this._handleRadioValueChanged}
|
||||||
|
value="full"
|
||||||
|
name="snapshotType"
|
||||||
|
.checked=${this._snapshotType === "full"}
|
||||||
|
>
|
||||||
|
</ha-radio>
|
||||||
|
</ha-formfield>
|
||||||
|
<ha-formfield
|
||||||
|
.label=${this._dialogParams.supervisor.localize(
|
||||||
|
"snapshot.partial_snapshot"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-radio
|
||||||
|
@change=${this._handleRadioValueChanged}
|
||||||
|
value="partial"
|
||||||
|
name="snapshotType"
|
||||||
|
.checked=${this._snapshotType === "partial"}
|
||||||
|
>
|
||||||
|
</ha-radio>
|
||||||
|
</ha-formfield>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${
|
||||||
|
this._snapshotType === "full"
|
||||||
|
? undefined
|
||||||
|
: html`
|
||||||
|
${this._dialogParams.supervisor.localize("snapshot.folders")}:
|
||||||
|
<div class="checkbox-section">
|
||||||
|
${this._folderList.map(
|
||||||
|
(folder, idx) => html`
|
||||||
|
<div class="checkbox-line">
|
||||||
|
<ha-checkbox
|
||||||
|
.idx=${idx}
|
||||||
|
.checked=${folder.checked}
|
||||||
|
@change=${this._folderChecked}
|
||||||
|
slot="prefix"
|
||||||
|
>
|
||||||
|
</ha-checkbox>
|
||||||
|
<span>
|
||||||
|
${this._dialogParams!.supervisor.localize(
|
||||||
|
`snapshot.folder.${folder.slug}`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this._dialogParams.supervisor.localize("snapshot.addons")}:
|
||||||
|
<div class="checkbox-section">
|
||||||
|
${this._addonList.map(
|
||||||
|
(addon, idx) => html`
|
||||||
|
<div class="checkbox-line">
|
||||||
|
<ha-checkbox
|
||||||
|
.idx=${idx}
|
||||||
|
.checked=${addon.checked}
|
||||||
|
@change=${this._addonChecked}
|
||||||
|
slot="prefix"
|
||||||
|
>
|
||||||
|
</ha-checkbox>
|
||||||
|
<span>
|
||||||
|
${addon.name}<span class="version">
|
||||||
|
(${addon.version})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
${this._dialogParams.supervisor.localize("snapshot.security")}:
|
||||||
|
<div class="checkbox-section">
|
||||||
|
<div class="checkbox-line">
|
||||||
|
<ha-checkbox
|
||||||
|
.checked=${this._snapshotHasPassword}
|
||||||
|
@change=${this._handleCheckboxValueChanged}
|
||||||
|
slot="prefix"
|
||||||
|
>
|
||||||
|
</ha-checkbox>
|
||||||
|
<span>
|
||||||
|
${this._dialogParams.supervisor.localize(
|
||||||
|
"snapshot.password_protection"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${
|
||||||
|
this._snapshotHasPassword
|
||||||
|
? html`
|
||||||
|
<paper-input
|
||||||
|
.label=${this._dialogParams.supervisor.localize(
|
||||||
|
"snapshot.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
|
||||||
|
}
|
||||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||||
${this._dialogParams.supervisor.localize("common.close")}
|
${this._dialogParams.supervisor.localize("common.close")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
<mwc-button
|
<ha-progress-button slot="primaryAction" @click=${this._createSnapshot}>
|
||||||
.disabled=${this._creatingSnapshot}
|
|
||||||
slot="primaryAction"
|
|
||||||
@click=${this._createSnapshot}
|
|
||||||
>
|
|
||||||
${this._dialogParams.supervisor.localize("snapshot.create")}
|
${this._dialogParams.supervisor.localize("snapshot.create")}
|
||||||
</mwc-button>
|
</ha-progress-button>
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSnapshot(): Promise<void> {
|
private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
|
||||||
|
const input = ev.currentTarget as PaperInputElement;
|
||||||
|
this[`_${input.name}`] = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleCheckboxValueChanged(ev: CustomEvent) {
|
||||||
|
const input = ev.currentTarget as HaCheckbox;
|
||||||
|
this._snapshotHasPassword = input.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleRadioValueChanged(ev: CustomEvent) {
|
||||||
|
const input = ev.currentTarget as HaRadio;
|
||||||
|
this[`_${input.name}`] = input.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _folderChecked(ev) {
|
||||||
|
const { idx, checked } = ev.currentTarget!;
|
||||||
|
this._folderList = this._folderList.map((folder, curIdx) =>
|
||||||
|
curIdx === idx ? { ...folder, checked } : folder
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addonChecked(ev) {
|
||||||
|
const { idx, checked } = ev.currentTarget!;
|
||||||
|
this._addonList = this._addonList.map((addon, curIdx) =>
|
||||||
|
curIdx === idx ? { ...addon, checked } : addon
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createSnapshot(ev: CustomEvent): Promise<void> {
|
||||||
if (this._dialogParams!.supervisor.info.state !== "running") {
|
if (this._dialogParams!.supervisor.info.state !== "running") {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this._dialogParams!.supervisor.localize(
|
title: this._dialogParams!.supervisor.localize(
|
||||||
@@ -91,35 +282,40 @@ class HassioCreateSnapshotDialog extends LitElement {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const snapshotDetails = this._snapshotContent.snapshotDetails();
|
const button = ev.currentTarget as any;
|
||||||
this._creatingSnapshot = true;
|
button.progress = true;
|
||||||
|
|
||||||
this._error = "";
|
this._error = "";
|
||||||
if (snapshotDetails.password && !snapshotDetails.password.length) {
|
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
|
||||||
this._error = this._dialogParams!.supervisor.localize(
|
this._error = this._dialogParams!.supervisor.localize(
|
||||||
"snapshot.enter_password"
|
"snapshot.enter_password"
|
||||||
);
|
);
|
||||||
this._creatingSnapshot = false;
|
button.progress = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
const name = this._snapshotName || formatDate(new Date(), this.hass.locale);
|
||||||
snapshotDetails.password &&
|
|
||||||
snapshotDetails.password !== snapshotDetails.confirm_password
|
|
||||||
) {
|
|
||||||
this._error = this._dialogParams!.supervisor.localize(
|
|
||||||
"snapshot.passwords_not_matching"
|
|
||||||
);
|
|
||||||
this._creatingSnapshot = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete snapshotDetails.confirm_password;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this._snapshotContent.snapshotType === "full") {
|
if (this._snapshotType === "full") {
|
||||||
await createHassioFullSnapshot(this.hass, snapshotDetails);
|
const data: HassioFullSnapshotCreateParams = { name };
|
||||||
|
if (this._snapshotHasPassword) {
|
||||||
|
data.password = this._snapshotPassword;
|
||||||
|
}
|
||||||
|
await createHassioFullSnapshot(this.hass, data);
|
||||||
} else {
|
} else {
|
||||||
await createHassioPartialSnapshot(this.hass, snapshotDetails);
|
const data: HassioPartialSnapshotCreateParams = {
|
||||||
|
name,
|
||||||
|
folders: this._folderList
|
||||||
|
.filter((folder) => folder.checked)
|
||||||
|
.map((folder) => folder.slug),
|
||||||
|
addons: this._addonList
|
||||||
|
.filter((addon) => addon.checked)
|
||||||
|
.map((addon) => addon.slug),
|
||||||
|
};
|
||||||
|
if (this._snapshotHasPassword) {
|
||||||
|
data.password = this._snapshotPassword;
|
||||||
|
}
|
||||||
|
await createHassioPartialSnapshot(this.hass, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._dialogParams!.onCreate();
|
this._dialogParams!.onCreate();
|
||||||
@@ -127,7 +323,7 @@ class HassioCreateSnapshotDialog extends LitElement {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = extractApiErrorMessage(err);
|
this._error = extractApiErrorMessage(err);
|
||||||
}
|
}
|
||||||
this._creatingSnapshot = false;
|
button.progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -135,9 +331,22 @@ class HassioCreateSnapshotDialog extends LitElement {
|
|||||||
haStyle,
|
haStyle,
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-circular-progress {
|
.error {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
paper-input[type="password"] {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
margin: 4px 0 4px 16px;
|
||||||
|
}
|
||||||
|
span.version {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.checkbox-section {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.checkbox-line {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { ActionDetail } from "@material/mwc-list";
|
import "@material/mwc-button";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
|
||||||
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
import "@polymer/paper-checkbox/paper-checkbox";
|
||||||
|
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||||
|
import "@polymer/paper-input/paper-input";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { formatDateTime } from "../../../../src/common/datetime/format_date_time";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import { slugify } from "../../../../src/common/string/slugify";
|
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
|
||||||
import "../../../../src/components/ha-button-menu";
|
|
||||||
import "../../../../src/components/ha-header-bar";
|
import "../../../../src/components/ha-header-bar";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
import { getSignedPath } from "../../../../src/data/auth";
|
import { getSignedPath } from "../../../../src/data/auth";
|
||||||
@@ -15,47 +15,95 @@ import {
|
|||||||
fetchHassioSnapshotInfo,
|
fetchHassioSnapshotInfo,
|
||||||
HassioSnapshotDetail,
|
HassioSnapshotDetail,
|
||||||
} from "../../../../src/data/hassio/snapshot";
|
} from "../../../../src/data/hassio/snapshot";
|
||||||
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import { fileDownload } from "../../../../src/util/file_download";
|
|
||||||
import "../../components/supervisor-snapshot-content";
|
|
||||||
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
|
|
||||||
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
|
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
|
||||||
|
|
||||||
|
const _computeFolders = (folders) => {
|
||||||
|
const list: Array<{ slug: string; name: string; checked: boolean }> = [];
|
||||||
|
if (folders.includes("homeassistant")) {
|
||||||
|
list.push({
|
||||||
|
slug: "homeassistant",
|
||||||
|
name: "Home Assistant configuration",
|
||||||
|
checked: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (folders.includes("ssl")) {
|
||||||
|
list.push({ slug: "ssl", name: "SSL", checked: true });
|
||||||
|
}
|
||||||
|
if (folders.includes("share")) {
|
||||||
|
list.push({ slug: "share", name: "Share", checked: true });
|
||||||
|
}
|
||||||
|
if (folders.includes("addons/local")) {
|
||||||
|
list.push({ slug: "addons/local", name: "Local add-ons", checked: true });
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _computeAddons = (addons) =>
|
||||||
|
addons.map((addon) => ({
|
||||||
|
slug: addon.slug,
|
||||||
|
name: addon.name,
|
||||||
|
version: addon.version,
|
||||||
|
checked: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface AddonItem {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
checked: boolean | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FolderItem {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
checked: boolean | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("dialog-hassio-snapshot")
|
@customElement("dialog-hassio-snapshot")
|
||||||
class HassioSnapshotDialog
|
class HassioSnapshotDialog extends LitElement {
|
||||||
extends LitElement
|
|
||||||
implements HassDialog<HassioSnapshotDialogParams> {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _onboarding = false;
|
||||||
|
|
||||||
@state() private _snapshot?: HassioSnapshotDetail;
|
@state() private _snapshot?: HassioSnapshotDetail;
|
||||||
|
|
||||||
|
@state() private _folders!: FolderItem[];
|
||||||
|
|
||||||
|
@state() private _addons!: AddonItem[];
|
||||||
|
|
||||||
@state() private _dialogParams?: HassioSnapshotDialogParams;
|
@state() private _dialogParams?: HassioSnapshotDialogParams;
|
||||||
|
|
||||||
@state() private _restoringSnapshot = false;
|
@state() private _snapshotPassword!: string;
|
||||||
|
|
||||||
@query("supervisor-snapshot-content")
|
@state() private _restoreHass = true;
|
||||||
private _snapshotContent!: SupervisorSnapshotContent;
|
|
||||||
|
|
||||||
public async showDialog(params: HassioSnapshotDialogParams) {
|
public async showDialog(params: HassioSnapshotDialogParams) {
|
||||||
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
|
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
|
||||||
this._dialogParams = params;
|
this._folders = _computeFolders(
|
||||||
this._restoringSnapshot = false;
|
this._snapshot?.folders
|
||||||
}
|
).sort((a: FolderItem, b: FolderItem) => (a.name > b.name ? 1 : -1));
|
||||||
|
this._addons = _computeAddons(
|
||||||
|
this._snapshot?.addons
|
||||||
|
).sort((a: AddonItem, b: AddonItem) => (a.name > b.name ? 1 : -1));
|
||||||
|
|
||||||
public closeDialog() {
|
this._dialogParams = params;
|
||||||
this._snapshot = undefined;
|
this._onboarding = params.onboarding ?? false;
|
||||||
this._dialogParams = undefined;
|
this.supervisor = params.supervisor;
|
||||||
this._restoringSnapshot = false;
|
if (!this._snapshot.homeassistant) {
|
||||||
this._error = undefined;
|
this._restoreHass = false;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
@@ -63,54 +111,121 @@ class HassioSnapshotDialog
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog open @closing=${this._closeDialog} .heading=${true}>
|
||||||
open
|
|
||||||
scrimClickAction
|
|
||||||
@closed=${this.closeDialog}
|
|
||||||
.heading=${true}
|
|
||||||
>
|
|
||||||
<div slot="heading">
|
<div slot="heading">
|
||||||
<ha-header-bar>
|
<ha-header-bar>
|
||||||
<span slot="title">${this._snapshot.name}</span>
|
<span slot="title"> ${this._computeName} </span>
|
||||||
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
</ha-header-bar>
|
</ha-header-bar>
|
||||||
</div>
|
</div>
|
||||||
${this._restoringSnapshot
|
<div class="details">
|
||||||
? html` <ha-circular-progress active></ha-circular-progress>`
|
${this._snapshot.type === "full"
|
||||||
: html`<supervisor-snapshot-content
|
? "Full snapshot"
|
||||||
.hass=${this.hass}
|
: "Partial snapshot"}
|
||||||
.supervisor=${this._dialogParams.supervisor}
|
(${this._computeSize})<br />
|
||||||
.snapshot=${this._snapshot}
|
${formatDateTime(new Date(this._snapshot.date), this.hass.locale)}
|
||||||
.onboarding=${this._dialogParams.onboarding || false}
|
</div>
|
||||||
.localize=${this._dialogParams.localize}
|
${this._snapshot.homeassistant
|
||||||
|
? html`<div>Home Assistant:</div>
|
||||||
|
<paper-checkbox
|
||||||
|
.checked=${this._restoreHass}
|
||||||
|
@change="${(ev: Event) => {
|
||||||
|
this._restoreHass = (ev.target as PaperCheckboxElement).checked!;
|
||||||
|
}}"
|
||||||
>
|
>
|
||||||
</supervisor-snapshot-content>`}
|
Home Assistant
|
||||||
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
|
<span class="version">(${this._snapshot.homeassistant})</span>
|
||||||
|
</paper-checkbox>`
|
||||||
<mwc-button
|
|
||||||
.disabled=${this._restoringSnapshot}
|
|
||||||
slot="secondaryAction"
|
|
||||||
@click=${this._restoreClicked}
|
|
||||||
>
|
|
||||||
Restore
|
|
||||||
</mwc-button>
|
|
||||||
|
|
||||||
${!this._dialogParams.onboarding
|
|
||||||
? html`<ha-button-menu
|
|
||||||
fixed
|
|
||||||
slot="primaryAction"
|
|
||||||
@action=${this._handleMenuAction}
|
|
||||||
@closed=${(ev: Event) => ev.stopPropagation()}
|
|
||||||
>
|
|
||||||
<mwc-icon-button slot="trigger" alt="menu">
|
|
||||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
|
||||||
</mwc-icon-button>
|
|
||||||
<mwc-list-item>Download Snapshot</mwc-list-item>
|
|
||||||
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
|
|
||||||
</ha-button-menu>`
|
|
||||||
: ""}
|
: ""}
|
||||||
|
${this._folders.length
|
||||||
|
? html`
|
||||||
|
<div>Folders:</div>
|
||||||
|
<paper-dialog-scrollable class="no-margin-top">
|
||||||
|
${this._folders.map(
|
||||||
|
(item) => html`
|
||||||
|
<paper-checkbox
|
||||||
|
.checked=${item.checked}
|
||||||
|
@change="${(ev: Event) =>
|
||||||
|
this._updateFolders(
|
||||||
|
item,
|
||||||
|
(ev.target as PaperCheckboxElement).checked
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
${item.name}
|
||||||
|
</paper-checkbox>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</paper-dialog-scrollable>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._addons.length
|
||||||
|
? html`
|
||||||
|
<div>Add-on:</div>
|
||||||
|
<paper-dialog-scrollable class="no-margin-top">
|
||||||
|
${this._addons.map(
|
||||||
|
(item) => html`
|
||||||
|
<paper-checkbox
|
||||||
|
.checked=${item.checked}
|
||||||
|
@change="${(ev: Event) =>
|
||||||
|
this._updateAddons(
|
||||||
|
item,
|
||||||
|
(ev.target as PaperCheckboxElement).checked
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
${item.name}
|
||||||
|
<span class="version">(${item.version})</span>
|
||||||
|
</paper-checkbox>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</paper-dialog-scrollable>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._snapshot.protected
|
||||||
|
? html`
|
||||||
|
<paper-input
|
||||||
|
autofocus=""
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
@value-changed=${this._passwordInput}
|
||||||
|
.value=${this._snapshotPassword}
|
||||||
|
></paper-input>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""}
|
||||||
|
|
||||||
|
<div class="button-row" slot="primaryAction">
|
||||||
|
<mwc-button @click=${this._partialRestoreClicked}>
|
||||||
|
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
|
||||||
|
Restore Selected
|
||||||
|
</mwc-button>
|
||||||
|
${!this._onboarding
|
||||||
|
? html`
|
||||||
|
<mwc-button @click=${this._deleteClicked}>
|
||||||
|
<ha-svg-icon .path=${mdiDelete} class="icon warning">
|
||||||
|
</ha-svg-icon>
|
||||||
|
<span class="warning">Delete Snapshot</span>
|
||||||
|
</mwc-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div class="button-row" slot="secondaryAction">
|
||||||
|
${this._snapshot.type === "full"
|
||||||
|
? html`
|
||||||
|
<mwc-button @click=${this._fullRestoreClicked}>
|
||||||
|
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
|
||||||
|
Restore Everything
|
||||||
|
</mwc-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${!this._onboarding
|
||||||
|
? html`<mwc-button @click=${this._downloadClicked}>
|
||||||
|
<ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
|
||||||
|
Download Snapshot
|
||||||
|
</mwc-button>`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -120,53 +235,83 @@ class HassioSnapshotDialog
|
|||||||
haStyle,
|
haStyle,
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-svg-icon {
|
paper-checkbox {
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
ha-circular-progress {
|
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
margin: 4px;
|
||||||
|
}
|
||||||
|
mwc-button ha-svg-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.button-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.details {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.warning,
|
||||||
|
.error {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
.buttons li {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
.buttons .icon {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
.no-margin-top {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
span.version {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
ha-header-bar {
|
ha-header-bar {
|
||||||
--mdc-theme-on-primary: var(--primary-text-color);
|
--mdc-theme-on-primary: var(--primary-text-color);
|
||||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: block;
|
}
|
||||||
|
/* overrule the ha-style-dialog max-height on small screens */
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
ha-header-bar {
|
||||||
|
--mdc-theme-primary: var(--app-header-background-color);
|
||||||
|
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
private _updateFolders(item: FolderItem, value: boolean | null | undefined) {
|
||||||
switch (ev.detail.index) {
|
this._folders = this._folders.map((folder) => {
|
||||||
case 0:
|
if (folder.slug === item.slug) {
|
||||||
this._downloadClicked();
|
folder.checked = value;
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
this._deleteClicked();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
return folder;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _restoreClicked() {
|
private _updateAddons(item: AddonItem, value: boolean | null | undefined) {
|
||||||
const snapshotDetails = this._snapshotContent.snapshotDetails();
|
this._addons = this._addons.map((addon) => {
|
||||||
this._restoringSnapshot = true;
|
if (addon.slug === item.slug) {
|
||||||
if (this._snapshotContent.snapshotType === "full") {
|
addon.checked = value;
|
||||||
await this._fullRestoreClicked(snapshotDetails);
|
|
||||||
} else {
|
|
||||||
await this._partialRestoreClicked(snapshotDetails);
|
|
||||||
}
|
}
|
||||||
this._restoringSnapshot = false;
|
return addon;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _partialRestoreClicked(snapshotDetails) {
|
private _passwordInput(ev: PolymerChangedEvent<string>) {
|
||||||
|
this._snapshotPassword = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _partialRestoreClicked() {
|
||||||
if (
|
if (
|
||||||
this._dialogParams?.supervisor !== undefined &&
|
this.supervisor !== undefined &&
|
||||||
this._dialogParams?.supervisor.info.state !== "running"
|
this.supervisor.info.state !== "running"
|
||||||
) {
|
) {
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
title: "Could not restore snapshot",
|
title: "Could not restore snapshot",
|
||||||
text: `Restoring a snapshot is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`,
|
text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -180,17 +325,41 @@ class HassioSnapshotDialog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._dialogParams?.onboarding) {
|
const addons = this._addons
|
||||||
|
.filter((addon) => addon.checked)
|
||||||
|
.map((addon) => addon.slug);
|
||||||
|
|
||||||
|
const folders = this._folders
|
||||||
|
.filter((folder) => folder.checked)
|
||||||
|
.map((folder) => folder.slug);
|
||||||
|
|
||||||
|
const data: {
|
||||||
|
homeassistant: boolean;
|
||||||
|
addons: any;
|
||||||
|
folders: any;
|
||||||
|
password?: string;
|
||||||
|
} = {
|
||||||
|
homeassistant: this._restoreHass,
|
||||||
|
addons,
|
||||||
|
folders,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._snapshot!.protected) {
|
||||||
|
data.password = this._snapshotPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._onboarding) {
|
||||||
this.hass
|
this.hass
|
||||||
.callApi(
|
.callApi(
|
||||||
"POST",
|
"POST",
|
||||||
|
|
||||||
`hassio/snapshots/${this._snapshot!.slug}/restore/partial`,
|
`hassio/snapshots/${this._snapshot!.slug}/restore/partial`,
|
||||||
snapshotDetails
|
data
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
this.closeDialog();
|
alert("Snapshot restored!");
|
||||||
|
this._closeDialog();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this._error = error.body.message;
|
this._error = error.body.message;
|
||||||
@@ -200,20 +369,20 @@ class HassioSnapshotDialog
|
|||||||
fireEvent(this, "restoring");
|
fireEvent(this, "restoring");
|
||||||
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/partial`, {
|
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/partial`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(snapshotDetails),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
this.closeDialog();
|
this._closeDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fullRestoreClicked(snapshotDetails) {
|
private async _fullRestoreClicked() {
|
||||||
if (
|
if (
|
||||||
this._dialogParams?.supervisor !== undefined &&
|
this.supervisor !== undefined &&
|
||||||
this._dialogParams?.supervisor.info.state !== "running"
|
this.supervisor.info.state !== "running"
|
||||||
) {
|
) {
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
title: "Could not restore snapshot",
|
title: "Could not restore snapshot",
|
||||||
text: `Restoring a snapshot is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`,
|
text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -228,16 +397,20 @@ class HassioSnapshotDialog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._dialogParams?.onboarding) {
|
const data = this._snapshot!.protected
|
||||||
|
? { password: this._snapshotPassword }
|
||||||
|
: undefined;
|
||||||
|
if (!this._onboarding) {
|
||||||
this.hass
|
this.hass
|
||||||
.callApi(
|
.callApi(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/snapshots/${this._snapshot!.slug}/restore/full`,
|
`hassio/snapshots/${this._snapshot!.slug}/restore/full`,
|
||||||
snapshotDetails
|
data
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
this.closeDialog();
|
alert("Snapshot restored!");
|
||||||
|
this._closeDialog();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this._error = error.body.message;
|
this._error = error.body.message;
|
||||||
@@ -247,9 +420,9 @@ class HassioSnapshotDialog
|
|||||||
fireEvent(this, "restoring");
|
fireEvent(this, "restoring");
|
||||||
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/full`, {
|
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/full`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(snapshotDetails),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
this.closeDialog();
|
this._closeDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +445,7 @@ class HassioSnapshotDialog
|
|||||||
if (this._dialogParams!.onDelete) {
|
if (this._dialogParams!.onDelete) {
|
||||||
this._dialogParams!.onDelete();
|
this._dialogParams!.onDelete();
|
||||||
}
|
}
|
||||||
this.closeDialog();
|
this._closeDialog();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this._error = error.body.message;
|
this._error = error.body.message;
|
||||||
@@ -288,9 +461,7 @@ class HassioSnapshotDialog
|
|||||||
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
|
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await showAlertDialog(this, {
|
alert(`Error: ${extractApiErrorMessage(err)}`);
|
||||||
text: extractApiErrorMessage(err),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,11 +478,13 @@ class HassioSnapshotDialog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileDownload(
|
const name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
|
||||||
this,
|
const a = document.createElement("a");
|
||||||
signedPath.path,
|
a.href = signedPath.path;
|
||||||
`home_assistant_snapshot_${slugify(this._computeName)}.tar`
|
a.download = `Hass_io_${name}.tar`;
|
||||||
);
|
this.shadowRoot!.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
this.shadowRoot!.removeChild(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _computeName() {
|
private get _computeName() {
|
||||||
@@ -319,6 +492,18 @@ class HassioSnapshotDialog
|
|||||||
? this._snapshot.name || this._snapshot.slug
|
? this._snapshot.name || this._snapshot.slug
|
||||||
: "Unnamed snapshot";
|
: "Unnamed snapshot";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _computeSize() {
|
||||||
|
return Math.ceil(this._snapshot!.size * 10) / 10 + " MB";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _closeDialog() {
|
||||||
|
this._dialogParams = undefined;
|
||||||
|
this._snapshot = undefined;
|
||||||
|
this._snapshotPassword = "";
|
||||||
|
this._folders = [];
|
||||||
|
this._addons = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import { LocalizeFunc } from "../../../../src/common/translations/localize";
|
|
||||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
|
|
||||||
export interface HassioSnapshotDialogParams {
|
export interface HassioSnapshotDialogParams {
|
||||||
@@ -7,7 +6,6 @@ export interface HassioSnapshotDialogParams {
|
|||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
supervisor?: Supervisor;
|
supervisor?: Supervisor;
|
||||||
localize?: LocalizeFunc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showHassioSnapshotDialog = (
|
export const showHassioSnapshotDialog = (
|
||||||
|
@@ -2,32 +2,19 @@ import "@material/mwc-button/mwc-button";
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, state } from "lit/decorators";
|
import { customElement, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import "../../../../src/components/ha-checkbox";
|
|
||||||
import "../../../../src/components/ha-circular-progress";
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import "../../../../src/components/ha-dialog";
|
import "../../../../src/components/ha-dialog";
|
||||||
import "../../../../src/components/ha-settings-row";
|
import "../../../../src/components/ha-settings-row";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
|
import "../../../../src/components/ha-switch";
|
||||||
import {
|
import {
|
||||||
extractApiErrorMessage,
|
extractApiErrorMessage,
|
||||||
ignoreSupervisorError,
|
ignoreSupervisorError,
|
||||||
} from "../../../../src/data/hassio/common";
|
} from "../../../../src/data/hassio/common";
|
||||||
import {
|
|
||||||
SupervisorFrontendPrefrences,
|
|
||||||
fetchSupervisorFrontendPreferences,
|
|
||||||
saveSupervisorFrontendPreferences,
|
|
||||||
} from "../../../../src/data/supervisor/supervisor";
|
|
||||||
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
|
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
|
||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
|
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
|
|
||||||
const snapshot_before_update = memoizeOne(
|
|
||||||
(slug: string, frontendPrefrences: SupervisorFrontendPrefrences) =>
|
|
||||||
slug in frontendPrefrences.snapshot_before_update
|
|
||||||
? frontendPrefrences.snapshot_before_update[slug]
|
|
||||||
: true
|
|
||||||
);
|
|
||||||
|
|
||||||
@customElement("dialog-supervisor-update")
|
@customElement("dialog-supervisor-update")
|
||||||
class DialogSupervisorUpdate extends LitElement {
|
class DialogSupervisorUpdate extends LitElement {
|
||||||
@@ -35,12 +22,12 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _opened = false;
|
||||||
|
|
||||||
|
@state() private _createSnapshot = true;
|
||||||
|
|
||||||
@state() private _action: "snapshot" | "update" | null = null;
|
@state() private _action: "snapshot" | "update" | null = null;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _frontendPrefrences?: SupervisorFrontendPrefrences;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
|
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
|
||||||
|
|
||||||
@@ -49,17 +36,14 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this._opened = true;
|
this._opened = true;
|
||||||
this._dialogParams = params;
|
this._dialogParams = params;
|
||||||
this._frontendPrefrences = await fetchSupervisorFrontendPreferences(
|
|
||||||
this.hass
|
|
||||||
);
|
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._action = null;
|
this._action = null;
|
||||||
|
this._createSnapshot = true;
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._dialogParams = undefined;
|
this._dialogParams = undefined;
|
||||||
this._frontendPrefrences = undefined;
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +56,7 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._dialogParams || !this._frontendPrefrences) {
|
if (!this._dialogParams) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
@@ -98,16 +82,6 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ha-settings-row>
|
<ha-settings-row>
|
||||||
<ha-checkbox
|
|
||||||
.checked=${snapshot_before_update(
|
|
||||||
this._dialogParams.slug,
|
|
||||||
this._frontendPrefrences
|
|
||||||
)}
|
|
||||||
haptic
|
|
||||||
@click=${this._toggleSnapshot}
|
|
||||||
slot="prefix"
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
<span slot="heading">
|
<span slot="heading">
|
||||||
${this._dialogParams.supervisor.localize(
|
${this._dialogParams.supervisor.localize(
|
||||||
"dialog.update.snapshot"
|
"dialog.update.snapshot"
|
||||||
@@ -120,6 +94,12 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
this._dialogParams.name
|
this._dialogParams.name
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${this._createSnapshot}
|
||||||
|
haptic
|
||||||
|
@click=${this._toggleSnapshot}
|
||||||
|
>
|
||||||
|
</ha-switch>
|
||||||
</ha-settings-row>
|
</ha-settings-row>
|
||||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||||
${this._dialogParams.supervisor.localize("common.cancel")}
|
${this._dialogParams.supervisor.localize("common.cancel")}
|
||||||
@@ -153,27 +133,12 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _toggleSnapshot(): Promise<void> {
|
private _toggleSnapshot() {
|
||||||
this._frontendPrefrences!.snapshot_before_update[
|
this._createSnapshot = !this._createSnapshot;
|
||||||
this._dialogParams!.slug
|
|
||||||
] = !snapshot_before_update(
|
|
||||||
this._dialogParams!.slug,
|
|
||||||
this._frontendPrefrences!
|
|
||||||
);
|
|
||||||
|
|
||||||
await saveSupervisorFrontendPreferences(
|
|
||||||
this.hass,
|
|
||||||
this._frontendPrefrences!
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _update() {
|
private async _update() {
|
||||||
if (
|
if (this._createSnapshot) {
|
||||||
snapshot_before_update(
|
|
||||||
this._dialogParams!.slug,
|
|
||||||
this._frontendPrefrences!
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this._action = "snapshot";
|
this._action = "snapshot";
|
||||||
try {
|
try {
|
||||||
await createHassioPartialSnapshot(
|
await createHassioPartialSnapshot(
|
||||||
|
@@ -5,7 +5,6 @@ export interface SupervisorDialogSupervisorUpdateParams {
|
|||||||
supervisor: Supervisor;
|
supervisor: Supervisor;
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
slug: string;
|
|
||||||
snapshotParams: any;
|
snapshotParams: any;
|
||||||
updateHandler: () => Promise<void>;
|
updateHandler: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@@ -15,11 +15,5 @@ body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background-color: #111111;
|
|
||||||
color: #e1e1e1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(styleEl);
|
document.head.appendChild(styleEl);
|
||||||
|
@@ -4,7 +4,6 @@ import { atLeastVersion } from "../../src/common/config/version";
|
|||||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||||
import { mainWindow } from "../../src/common/dom/get_main_window";
|
|
||||||
import { navigate } from "../../src/common/navigate";
|
import { navigate } from "../../src/common/navigate";
|
||||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||||
@@ -51,7 +50,7 @@ export class HassioMain extends SupervisorBaseElement {
|
|||||||
|
|
||||||
// Joakim - April 26, 2021
|
// Joakim - April 26, 2021
|
||||||
// Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element
|
// Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element
|
||||||
mainWindow.addEventListener("location-changed", (ev) =>
|
top.addEventListener("location-changed", (ev) =>
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
fireEvent(this, ev.type, ev.detail, {
|
fireEvent(this, ev.type, ev.detail, {
|
||||||
bubbles: false,
|
bubbles: false,
|
||||||
@@ -63,7 +62,7 @@ export class HassioMain extends SupervisorBaseElement {
|
|||||||
document.body.addEventListener("click", (ev) => {
|
document.body.addEventListener("click", (ev) => {
|
||||||
const href = isNavigationClick(ev);
|
const href = isNavigationClick(ev);
|
||||||
if (href) {
|
if (href) {
|
||||||
navigate(href);
|
navigate(document.body, href);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,7 +102,7 @@ export class HassioMain extends SupervisorBaseElement {
|
|||||||
|
|
||||||
private _applyTheme() {
|
private _applyTheme() {
|
||||||
let themeName: string;
|
let themeName: string;
|
||||||
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined;
|
let options: Partial<HomeAssistant["selectedTheme"]> | undefined;
|
||||||
|
|
||||||
if (atLeastVersion(this.hass.config.version, 0, 114)) {
|
if (atLeastVersion(this.hass.config.version, 0, 114)) {
|
||||||
themeName =
|
themeName =
|
||||||
@@ -112,9 +111,9 @@ export class HassioMain extends SupervisorBaseElement {
|
|||||||
? this.hass.themes.default_dark_theme!
|
? this.hass.themes.default_dark_theme!
|
||||||
: this.hass.themes.default_theme);
|
: this.hass.themes.default_theme);
|
||||||
|
|
||||||
themeSettings = this.hass.selectedTheme;
|
options = this.hass.selectedTheme;
|
||||||
if (themeSettings?.dark === undefined) {
|
if (themeName === "default" && options?.dark === undefined) {
|
||||||
themeSettings = {
|
options = {
|
||||||
...this.hass.selectedTheme,
|
...this.hass.selectedTheme,
|
||||||
dark: this.hass.themes.darkMode,
|
dark: this.hass.themes.darkMode,
|
||||||
};
|
};
|
||||||
@@ -129,7 +128,7 @@ export class HassioMain extends SupervisorBaseElement {
|
|||||||
this.parentElement,
|
this.parentElement,
|
||||||
this.hass.themes,
|
this.hass.themes,
|
||||||
themeName,
|
themeName,
|
||||||
themeSettings
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -89,7 +89,7 @@ class HassioMyRedirect extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(url, { replace: true });
|
navigate(this, url, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
@@ -11,7 +11,6 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
import { navigate } from "../../../src/common/navigate";
|
import { navigate } from "../../../src/common/navigate";
|
||||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||||
import { nextRender } from "../../../src/common/util/render-status";
|
|
||||||
import {
|
import {
|
||||||
fetchHassioAddonInfo,
|
fetchHassioAddonInfo,
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
@@ -96,26 +95,17 @@ class HassioIngressView extends LitElement {
|
|||||||
text: extractApiErrorMessage(err),
|
text: extractApiErrorMessage(err),
|
||||||
title: requestedAddon,
|
title: requestedAddon,
|
||||||
});
|
});
|
||||||
await nextRender();
|
history.back();
|
||||||
navigate("/hassio/store", { replace: true });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!addonInfo.version) {
|
if (!addonInfo.ingress) {
|
||||||
await showAlertDialog(this, {
|
|
||||||
text: this.supervisor.localize("my.error_addon_not_installed"),
|
|
||||||
title: addonInfo.name,
|
|
||||||
});
|
|
||||||
await nextRender();
|
|
||||||
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
|
|
||||||
} else if (!addonInfo.ingress) {
|
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
text: this.supervisor.localize("my.error_addon_no_ingress"),
|
text: this.supervisor.localize("my.error_addon_no_ingress"),
|
||||||
title: addonInfo.name,
|
title: addonInfo.name,
|
||||||
});
|
});
|
||||||
await nextRender();
|
history.back();
|
||||||
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
|
|
||||||
} else {
|
} else {
|
||||||
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
|
navigate(this, `/hassio/ingress/${addonInfo.slug}`, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +140,6 @@ class HassioIngressView extends LitElement {
|
|||||||
text: "Unable to fetch add-on info to start Ingress",
|
text: "Unable to fetch add-on info to start Ingress",
|
||||||
title: "Supervisor",
|
title: "Supervisor",
|
||||||
});
|
});
|
||||||
await nextRender();
|
|
||||||
history.back();
|
history.back();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -160,7 +149,6 @@ class HassioIngressView extends LitElement {
|
|||||||
text: "Add-on does not support Ingress",
|
text: "Add-on does not support Ingress",
|
||||||
title: addon.name,
|
title: addon.name,
|
||||||
});
|
});
|
||||||
await nextRender();
|
|
||||||
history.back();
|
history.back();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -170,8 +158,7 @@ class HassioIngressView extends LitElement {
|
|||||||
text: "Add-on is not running. Please start it first",
|
text: "Add-on is not running. Please start it first",
|
||||||
title: addon.name,
|
title: addon.name,
|
||||||
});
|
});
|
||||||
await nextRender();
|
navigate(this, `/hassio/addon/${addon.slug}/info`, true);
|
||||||
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +171,6 @@ class HassioIngressView extends LitElement {
|
|||||||
text: "Unable to create an Ingress session",
|
text: "Unable to create an Ingress session",
|
||||||
title: addon.name,
|
title: addon.name,
|
||||||
});
|
});
|
||||||
await nextRender();
|
|
||||||
history.back();
|
history.back();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,15 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import { ActionDetail } from "@material/mwc-list";
|
import { ActionDetail } from "@material/mwc-list";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
import { mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { atLeastVersion } from "../../../src/common/config/version";
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
import relativeTime from "../../../src/common/datetime/relative_time";
|
import relativeTime from "../../../src/common/datetime/relative_time";
|
||||||
@@ -19,25 +17,18 @@ import { HASSDomEvent } from "../../../src/common/dom/fire_event";
|
|||||||
import {
|
import {
|
||||||
DataTableColumnContainer,
|
DataTableColumnContainer,
|
||||||
RowClickedEvent,
|
RowClickedEvent,
|
||||||
SelectionChangedEvent,
|
|
||||||
} from "../../../src/components/data-table/ha-data-table";
|
} from "../../../src/components/data-table/ha-data-table";
|
||||||
import "../../../src/components/ha-button-menu";
|
import "../../../src/components/ha-button-menu";
|
||||||
import "../../../src/components/ha-fab";
|
import "../../../src/components/ha-fab";
|
||||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
|
||||||
import {
|
import {
|
||||||
fetchHassioSnapshots,
|
fetchHassioSnapshots,
|
||||||
friendlyFolderName,
|
friendlyFolderName,
|
||||||
HassioSnapshot,
|
HassioSnapshot,
|
||||||
reloadHassioSnapshots,
|
reloadHassioSnapshots,
|
||||||
removeSnapshot,
|
|
||||||
} from "../../../src/data/hassio/snapshot";
|
} from "../../../src/data/hassio/snapshot";
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import {
|
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
showAlertDialog,
|
|
||||||
showConfirmationDialog,
|
|
||||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
|
||||||
import "../../../src/layouts/hass-tabs-subpage-data-table";
|
import "../../../src/layouts/hass-tabs-subpage-data-table";
|
||||||
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
|
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
import { HomeAssistant, Route } from "../../../src/types";
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
import { showHassioCreateSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-create-snapshot";
|
import { showHassioCreateSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-create-snapshot";
|
||||||
@@ -58,15 +49,10 @@ export class HassioSnapshots extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public isWide!: boolean;
|
@property({ type: Boolean }) public isWide!: boolean;
|
||||||
|
|
||||||
@state() private _selectedSnapshots: string[] = [];
|
private _firstUpdatedCalled = false;
|
||||||
|
|
||||||
@state() private _snapshots?: HassioSnapshot[] = [];
|
@state() private _snapshots?: HassioSnapshot[] = [];
|
||||||
|
|
||||||
@query("hass-tabs-subpage-data-table", true)
|
|
||||||
private _dataTable!: HaTabsSubpageDataTable;
|
|
||||||
|
|
||||||
private _firstUpdatedCalled = false;
|
|
||||||
|
|
||||||
public connectedCallback(): void {
|
public connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (this.hass && this._firstUpdatedCalled) {
|
if (this.hass && this._firstUpdatedCalled) {
|
||||||
@@ -167,9 +153,7 @@ export class HassioSnapshots extends LitElement {
|
|||||||
.data=${this._snapshotData(this._snapshots || [])}
|
.data=${this._snapshotData(this._snapshots || [])}
|
||||||
id="slug"
|
id="slug"
|
||||||
@row-click=${this._handleRowClicked}
|
@row-click=${this._handleRowClicked}
|
||||||
@selection-changed=${this._handleSelectionChanged}
|
|
||||||
clickable
|
clickable
|
||||||
selectable
|
|
||||||
hasFab
|
hasFab
|
||||||
main-page
|
main-page
|
||||||
supervisor
|
supervisor
|
||||||
@@ -192,45 +176,6 @@ export class HassioSnapshots extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
</ha-button-menu>
|
</ha-button-menu>
|
||||||
|
|
||||||
${this._selectedSnapshots.length
|
|
||||||
? html`<div
|
|
||||||
class=${classMap({
|
|
||||||
"header-toolbar": this.narrow,
|
|
||||||
"table-header": !this.narrow,
|
|
||||||
})}
|
|
||||||
slot="header"
|
|
||||||
>
|
|
||||||
<p class="selected-txt">
|
|
||||||
${this.supervisor.localize("snapshot.selected", {
|
|
||||||
number: this._selectedSnapshots.length,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<div class="header-btns">
|
|
||||||
${!this.narrow
|
|
||||||
? html`
|
|
||||||
<mwc-button
|
|
||||||
@click=${this._deleteSelected}
|
|
||||||
class="warning"
|
|
||||||
>
|
|
||||||
${this.supervisor.localize("snapshot.delete_selected")}
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<mwc-icon-button
|
|
||||||
id="delete-btn"
|
|
||||||
class="warning"
|
|
||||||
@click=${this._deleteSelected}
|
|
||||||
>
|
|
||||||
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
|
|
||||||
</mwc-icon-button>
|
|
||||||
<paper-tooltip animation-delay="0" for="delete-btn">
|
|
||||||
${this.supervisor.localize("snapshot.delete_selected")}
|
|
||||||
</paper-tooltip>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div> `
|
|
||||||
: ""}
|
|
||||||
|
|
||||||
<ha-fab
|
<ha-fab
|
||||||
slot="fab"
|
slot="fab"
|
||||||
@click=${this._createSnapshot}
|
@click=${this._createSnapshot}
|
||||||
@@ -254,12 +199,6 @@ export class HassioSnapshots extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleSelectionChanged(
|
|
||||||
ev: HASSDomEvent<SelectionChangedEvent>
|
|
||||||
): void {
|
|
||||||
this._selectedSnapshots = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _showUploadSnapshotDialog() {
|
private _showUploadSnapshotDialog() {
|
||||||
showSnapshotUploadDialog(this, {
|
showSnapshotUploadDialog(this, {
|
||||||
showSnapshot: (slug: string) =>
|
showSnapshot: (slug: string) =>
|
||||||
@@ -277,35 +216,6 @@ export class HassioSnapshots extends LitElement {
|
|||||||
this._snapshots = await fetchHassioSnapshots(this.hass);
|
this._snapshots = await fetchHassioSnapshots(this.hass);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _deleteSelected() {
|
|
||||||
const confirm = await showConfirmationDialog(this, {
|
|
||||||
title: this.supervisor.localize("snapshot.delete_snapshot_title"),
|
|
||||||
text: this.supervisor.localize("snapshot.delete_snapshot_text", {
|
|
||||||
number: this._selectedSnapshots.length,
|
|
||||||
}),
|
|
||||||
confirmText: this.supervisor.localize("snapshot.delete_snapshot_confirm"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
this._selectedSnapshots.map((slug) => removeSnapshot(this.hass, slug))
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
showAlertDialog(this, {
|
|
||||||
title: this.supervisor.localize("snapshot.failed_to_delete"),
|
|
||||||
text: extractApiErrorMessage(err),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await reloadHassioSnapshots(this.hass);
|
|
||||||
this._snapshots = await fetchHassioSnapshots(this.hass);
|
|
||||||
this._dataTable.clearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||||
const slug = ev.detail.id;
|
const slug = ev.detail.id;
|
||||||
showHassioSnapshotDialog(this, {
|
showHassioSnapshotDialog(this, {
|
||||||
@@ -334,45 +244,7 @@ export class HassioSnapshots extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [haStyle, hassioStyle];
|
||||||
haStyle,
|
|
||||||
hassioStyle,
|
|
||||||
css`
|
|
||||||
.table-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
height: 58px;
|
|
||||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
|
||||||
}
|
|
||||||
.header-toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
position: relative;
|
|
||||||
top: -4px;
|
|
||||||
}
|
|
||||||
.selected-txt {
|
|
||||||
font-weight: bold;
|
|
||||||
padding-left: 16px;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
.table-header .selected-txt {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.header-toolbar .selected-txt {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.header-toolbar .header-btns {
|
|
||||||
margin-right: -12px;
|
|
||||||
}
|
|
||||||
.header-btns > mwc-button,
|
|
||||||
.header-btns > mwc-icon-button {
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -164,7 +164,6 @@ class HassioCoreInfo extends LitElement {
|
|||||||
showDialogSupervisorUpdate(this, {
|
showDialogSupervisorUpdate(this, {
|
||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
name: "Home Assistant Core",
|
name: "Home Assistant Core",
|
||||||
slug: "core",
|
|
||||||
version: this.supervisor.core.version_latest,
|
version: this.supervisor.core.version_latest,
|
||||||
snapshotParams: {
|
snapshotParams: {
|
||||||
name: `core_${this.supervisor.core.version}`,
|
name: `core_${this.supervisor.core.version}`,
|
||||||
|
@@ -2,6 +2,7 @@ import "@material/mwc-button";
|
|||||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiDotsVertical } from "@mdi/js";
|
import { mdiDotsVertical } from "@mdi/js";
|
||||||
|
import { safeDump } from "js-yaml";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@@ -40,8 +41,8 @@ import {
|
|||||||
roundWithOneDecimal,
|
roundWithOneDecimal,
|
||||||
} from "../../../src/util/calculate";
|
} from "../../../src/util/calculate";
|
||||||
import "../components/supervisor-metric";
|
import "../components/supervisor-metric";
|
||||||
|
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||||
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
|
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
|
||||||
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
|
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
|
||||||
@customElement("hassio-host-info")
|
@customElement("hassio-host-info")
|
||||||
@@ -228,19 +229,20 @@ class HassioHostInfo extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _showHardware(): Promise<void> {
|
private async _showHardware(): Promise<void> {
|
||||||
let hardware;
|
|
||||||
try {
|
try {
|
||||||
hardware = await fetchHassioHardwareInfo(this.hass);
|
const content = await fetchHassioHardwareInfo(this.hass);
|
||||||
|
showHassioMarkdownDialog(this, {
|
||||||
|
title: this.supervisor.localize("system.host.hardware"),
|
||||||
|
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this.supervisor.localize(
|
title: this.supervisor.localize(
|
||||||
"system.host.failed_to_get_hardware_list"
|
"system.host.failed_to_get_hardware_list"
|
||||||
),
|
),
|
||||||
text: extractApiErrorMessage(err),
|
text: extractApiErrorMessage(err),
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
"*.ts": () => "tsc -p tsconfig.json",
|
||||||
"*.{js,ts}": "eslint --fix",
|
"*.{js,ts}": "eslint --fix",
|
||||||
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
|
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
|
||||||
};
|
};
|
||||||
|
63
package.json
63
package.json
@@ -16,13 +16,13 @@
|
|||||||
"lint:lit": "lit-analyzer \"**/src/**/*.ts\" --format markdown --outFile result.md",
|
"lint:lit": "lit-analyzer \"**/src/**/*.ts\" --format markdown --outFile result.md",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
|
||||||
"format": "yarn run format:eslint && yarn run format:prettier",
|
"format": "yarn run format:eslint && yarn run format:prettier",
|
||||||
"mocha": "ts-mocha -p test-mocha/tsconfig.test.json \"test-mocha/**/*.ts\"",
|
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
|
||||||
"test": "yarn run lint && yarn run mocha"
|
"test": "yarn run lint && yarn run mocha"
|
||||||
},
|
},
|
||||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^5.0.1",
|
"@braintree/sanitize-url": "^5.0.0",
|
||||||
"@codemirror/commands": "^0.18.0",
|
"@codemirror/commands": "^0.18.0",
|
||||||
"@codemirror/gutter": "^0.18.0",
|
"@codemirror/gutter": "^0.18.0",
|
||||||
"@codemirror/highlight": "^0.18.0",
|
"@codemirror/highlight": "^0.18.0",
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
"@codemirror/text": "^0.18.0",
|
"@codemirror/text": "^0.18.0",
|
||||||
"@codemirror/view": "^0.18.0",
|
"@codemirror/view": "^0.18.0",
|
||||||
"@formatjs/intl-getcanonicallocales": "^1.5.10",
|
"@formatjs/intl-getcanonicallocales": "^1.5.10",
|
||||||
"@formatjs/intl-locale": "^2.4.28",
|
"@formatjs/intl-locale": "^2.4.24",
|
||||||
"@formatjs/intl-pluralrules": "^4.0.22",
|
"@formatjs/intl-pluralrules": "^4.0.18",
|
||||||
"@fullcalendar/common": "5.1.0",
|
"@fullcalendar/common": "5.1.0",
|
||||||
"@fullcalendar/core": "5.1.0",
|
"@fullcalendar/core": "5.1.0",
|
||||||
"@fullcalendar/daygrid": "5.1.0",
|
"@fullcalendar/daygrid": "5.1.0",
|
||||||
@@ -66,9 +66,12 @@
|
|||||||
"@polymer/iron-autogrow-textarea": "^3.0.1",
|
"@polymer/iron-autogrow-textarea": "^3.0.1",
|
||||||
"@polymer/iron-flex-layout": "^3.0.1",
|
"@polymer/iron-flex-layout": "^3.0.1",
|
||||||
"@polymer/iron-icon": "^3.0.1",
|
"@polymer/iron-icon": "^3.0.1",
|
||||||
|
"@polymer/iron-image": "^3.0.1",
|
||||||
"@polymer/iron-input": "^3.0.1",
|
"@polymer/iron-input": "^3.0.1",
|
||||||
|
"@polymer/iron-label": "^3.0.1",
|
||||||
"@polymer/iron-overlay-behavior": "^3.0.2",
|
"@polymer/iron-overlay-behavior": "^3.0.2",
|
||||||
"@polymer/iron-resizable-behavior": "^3.0.1",
|
"@polymer/iron-resizable-behavior": "^3.0.1",
|
||||||
|
"@polymer/paper-card": "^3.0.1",
|
||||||
"@polymer/paper-checkbox": "^3.1.0",
|
"@polymer/paper-checkbox": "^3.1.0",
|
||||||
"@polymer/paper-dialog": "^3.0.1",
|
"@polymer/paper-dialog": "^3.0.1",
|
||||||
"@polymer/paper-dialog-behavior": "^3.0.1",
|
"@polymer/paper-dialog-behavior": "^3.0.1",
|
||||||
@@ -98,26 +101,25 @@
|
|||||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"chartjs-chart-timeline": "^0.4.0",
|
"chartjs-chart-timeline": "^0.4.0",
|
||||||
"comlink": "^4.3.1",
|
"comlink": "^4.3.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"cropperjs": "^1.5.11",
|
"cropperjs": "^1.5.7",
|
||||||
"deep-clone-simple": "^1.1.1",
|
"deep-clone-simple": "^1.1.1",
|
||||||
"deep-freeze": "^0.0.1",
|
"deep-freeze": "^0.0.1",
|
||||||
"fecha": "^4.2.0",
|
"fecha": "^4.2.0",
|
||||||
"fuse.js": "^6.0.0",
|
"fuse.js": "^6.0.0",
|
||||||
"google-timezones-json": "^1.0.2",
|
"google-timezones-json": "^1.0.2",
|
||||||
"hls.js": "^1.0.5",
|
"hls.js": "^1.0.3",
|
||||||
"home-assistant-js-websocket": "^5.10.0",
|
"home-assistant-js-websocket": "^5.10.0",
|
||||||
"idb-keyval": "^5.0.5",
|
"idb-keyval": "^3.2.0",
|
||||||
"intl-messageformat": "^9.6.16",
|
"intl-messageformat": "^9.6.13",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^3.13.1",
|
||||||
"leaflet": "^1.7.1",
|
"leaflet": "^1.7.1",
|
||||||
"leaflet-draw": "^1.0.4",
|
"leaflet-draw": "^1.0.4",
|
||||||
"lit": "^2.0.0-rc.2",
|
"lit": "^2.0.0-rc.2",
|
||||||
"lit-vaadin-helpers": "^0.1.3",
|
"marked": "2.0.0",
|
||||||
"marked": "^2.0.5",
|
|
||||||
"mdn-polyfills": "^5.16.0",
|
"mdn-polyfills": "^5.16.0",
|
||||||
"memoize-one": "^5.2.1",
|
"memoize-one": "^5.0.2",
|
||||||
"node-vibrant": "3.2.1-alpha.1",
|
"node-vibrant": "3.2.1-alpha.1",
|
||||||
"proxy-polyfill": "^0.3.1",
|
"proxy-polyfill": "^0.3.1",
|
||||||
"punycode": "^2.1.1",
|
"punycode": "^2.1.1",
|
||||||
@@ -127,12 +129,12 @@
|
|||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
"sortablejs": "^1.10.2",
|
"sortablejs": "^1.10.2",
|
||||||
"superstruct": "^0.15.2",
|
"superstruct": "^0.15.2",
|
||||||
"tinykeys": "^1.1.3",
|
"tinykeys": "^1.1.1",
|
||||||
"tsparticles": "^1.19.2",
|
"tsparticles": "^1.19.2",
|
||||||
"unfetch": "^4.1.0",
|
"unfetch": "^4.1.0",
|
||||||
"vis-data": "^7.1.2",
|
"vis-data": "^7.1.1",
|
||||||
"vis-network": "^8.5.4",
|
"vis-network": "^8.5.4",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.11",
|
||||||
"vue2-daterange-picker": "^0.5.1",
|
"vue2-daterange-picker": "^0.5.1",
|
||||||
"web-animations-js": "^2.3.2",
|
"web-animations-js": "^2.3.2",
|
||||||
"workbox-cacheable-response": "^6.1.5",
|
"workbox-cacheable-response": "^6.1.5",
|
||||||
@@ -144,7 +146,7 @@
|
|||||||
"xss": "^1.0.9"
|
"xss": "^1.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.3",
|
"@babel/core": "^7.14.0",
|
||||||
"@babel/plugin-external-helpers": "^7.12.13",
|
"@babel/plugin-external-helpers": "^7.12.13",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||||
"@babel/plugin-proposal-decorators": "^7.13.15",
|
"@babel/plugin-proposal-decorators": "^7.13.15",
|
||||||
@@ -153,7 +155,7 @@
|
|||||||
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
|
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||||
"@babel/preset-env": "^7.14.2",
|
"@babel/preset-env": "^7.14.0",
|
||||||
"@babel/preset-typescript": "^7.13.0",
|
"@babel/preset-typescript": "^7.13.0",
|
||||||
"@koa/cors": "^3.1.0",
|
"@koa/cors": "^3.1.0",
|
||||||
"@open-wc/dev-server-hmr": "^0.0.2",
|
"@open-wc/dev-server-hmr": "^0.0.2",
|
||||||
@@ -162,13 +164,16 @@
|
|||||||
"@rollup/plugin-json": "^4.0.3",
|
"@rollup/plugin-json": "^4.0.3",
|
||||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||||
"@rollup/plugin-replace": "^2.3.2",
|
"@rollup/plugin-replace": "^2.3.2",
|
||||||
"@types/chromecast-caf-receiver": "5.0.12",
|
"@types/chai": "^4.1.7",
|
||||||
|
"@types/chromecast-caf-receiver": "^5.0.11",
|
||||||
"@types/chromecast-caf-sender": "^1.0.3",
|
"@types/chromecast-caf-sender": "^1.0.3",
|
||||||
"@types/js-yaml": "^4.0.1",
|
"@types/js-yaml": "^3.12.1",
|
||||||
"@types/leaflet": "^1.7.0",
|
"@types/leaflet": "^1.7.0",
|
||||||
"@types/leaflet-draw": "^1.0.3",
|
"@types/leaflet-draw": "^1.0.3",
|
||||||
"@types/marked": "^2.0.3",
|
"@types/marked": "^1.2.2",
|
||||||
"@types/mocha": "^8.2.2",
|
"@types/memoize-one": "4.1.0",
|
||||||
|
"@types/mocha": "^7.0.2",
|
||||||
|
"@types/resize-observer-browser": "^0.1.3",
|
||||||
"@types/sortablejs": "^1.10.6",
|
"@types/sortablejs": "^1.10.6",
|
||||||
"@types/webspeechapi": "^0.0.29",
|
"@types/webspeechapi": "^0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||||
@@ -176,7 +181,7 @@
|
|||||||
"@web/dev-server": "^0.0.24",
|
"@web/dev-server": "^0.0.24",
|
||||||
"@web/dev-server-rollup": "^0.2.11",
|
"@web/dev-server-rollup": "^0.2.11",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.2.0",
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"del": "^4.0.0",
|
"del": "^4.0.0",
|
||||||
"eslint": "^7.25.0",
|
"eslint": "^7.25.0",
|
||||||
@@ -190,7 +195,7 @@
|
|||||||
"eslint-plugin-wc": "^1.3.0",
|
"eslint-plugin-wc": "^1.3.0",
|
||||||
"fancy-log": "^1.3.3",
|
"fancy-log": "^1.3.3",
|
||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "^7.0.1",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.0",
|
||||||
"gulp-foreach": "^0.1.0",
|
"gulp-foreach": "^0.1.0",
|
||||||
"gulp-json-transform": "^0.4.6",
|
"gulp-json-transform": "^0.4.6",
|
||||||
"gulp-merge-json": "^1.3.1",
|
"gulp-merge-json": "^1.3.1",
|
||||||
@@ -204,7 +209,7 @@
|
|||||||
"magic-string": "^0.25.7",
|
"magic-string": "^0.25.7",
|
||||||
"map-stream": "^0.0.7",
|
"map-stream": "^0.0.7",
|
||||||
"merge-stream": "^1.0.1",
|
"merge-stream": "^1.0.1",
|
||||||
"mocha": "^8.4.0",
|
"mocha": "^7.2.0",
|
||||||
"object-hash": "^2.0.3",
|
"object-hash": "^2.0.3",
|
||||||
"open": "^7.0.4",
|
"open": "^7.0.4",
|
||||||
"prettier": "^2.0.4",
|
"prettier": "^2.0.4",
|
||||||
@@ -214,13 +219,13 @@
|
|||||||
"rollup-plugin-string": "^3.0.0",
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"rollup-plugin-terser": "^5.3.0",
|
"rollup-plugin-terser": "^5.3.0",
|
||||||
"rollup-plugin-visualizer": "^4.0.4",
|
"rollup-plugin-visualizer": "^4.0.4",
|
||||||
"serve": "^11.3.2",
|
"serve": "^11.3.0",
|
||||||
"sinon": "^11.0.0",
|
"sinon": "^7.3.1",
|
||||||
"source-map-url": "^0.4.0",
|
"source-map-url": "^0.4.0",
|
||||||
"systemjs": "^6.3.2",
|
"systemjs": "^6.3.2",
|
||||||
"terser-webpack-plugin": "^5.1.2",
|
"terser-webpack-plugin": "^5.1.1",
|
||||||
"ts-lit-plugin": "^1.2.1",
|
"ts-lit-plugin": "^1.2.1",
|
||||||
"ts-mocha": "^8.0.0",
|
"ts-mocha": "^7.0.0",
|
||||||
"typescript": "^4.2.4",
|
"typescript": "^4.2.4",
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
"vinyl-source-stream": "^2.0.0",
|
"vinyl-source-stream": "^2.0.0",
|
||||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="home-assistant-frontend",
|
name="home-assistant-frontend",
|
||||||
version="20210603.0",
|
version="20210518.0",
|
||||||
description="The Home Assistant frontend",
|
description="The Home Assistant frontend",
|
||||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||||
author="The Home Assistant Authors",
|
author="The Home Assistant Authors",
|
||||||
|
@@ -1,32 +1,21 @@
|
|||||||
import { format } from "fecha";
|
import { format } from "fecha";
|
||||||
import memoizeOne from "memoize-one";
|
import { FrontendTranslationData } from "../../data/translation";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
|
||||||
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
|
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
|
||||||
|
|
||||||
const formatDateMem = memoizeOne(
|
export const formatDate = toLocaleDateStringSupportsOptions
|
||||||
(locale: FrontendLocaleData) =>
|
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
dateObj.toLocaleDateString(locales.language, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
})
|
})
|
||||||
);
|
|
||||||
|
|
||||||
export const formatDate = toLocaleDateStringSupportsOptions
|
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
formatDateMem(locale).format(dateObj)
|
|
||||||
: (dateObj: Date) => format(dateObj, "longDate");
|
: (dateObj: Date) => format(dateObj, "longDate");
|
||||||
|
|
||||||
const formatDateWeekdayMem = memoizeOne(
|
export const formatDateWeekday = toLocaleDateStringSupportsOptions
|
||||||
(locale: FrontendLocaleData) =>
|
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
dateObj.toLocaleDateString(locales.language, {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
month: "long",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
})
|
})
|
||||||
);
|
|
||||||
|
|
||||||
export const formatDateWeekday = toLocaleDateStringSupportsOptions
|
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
formatDateWeekdayMem(locale).format(dateObj)
|
|
||||||
: (dateObj: Date) => format(dateObj, "dddd, MMM D");
|
: (dateObj: Date) => format(dateObj, "dddd, MMM D");
|
||||||
|
@@ -1,42 +1,26 @@
|
|||||||
import { format } from "fecha";
|
import { format } from "fecha";
|
||||||
import memoizeOne from "memoize-one";
|
import { FrontendTranslationData } from "../../data/translation";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
|
||||||
import { toLocaleStringSupportsOptions } from "./check_options_support";
|
import { toLocaleStringSupportsOptions } from "./check_options_support";
|
||||||
import { useAmPm } from "./use_am_pm";
|
|
||||||
|
|
||||||
const formatDateTimeMem = memoizeOne(
|
export const formatDateTime = toLocaleStringSupportsOptions
|
||||||
(locale: FrontendLocaleData) =>
|
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
dateObj.toLocaleString(locales.language, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
|
||||||
})
|
})
|
||||||
);
|
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
|
||||||
|
|
||||||
export const formatDateTime = toLocaleStringSupportsOptions
|
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||||
formatDateTimeMem(locale).format(dateObj)
|
dateObj.toLocaleString(locales.language, {
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
format(dateObj, "MMMM D, YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
|
|
||||||
|
|
||||||
const formatDateTimeWithSecondsMem = memoizeOne(
|
|
||||||
(locale: FrontendLocaleData) =>
|
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
|
||||||
})
|
})
|
||||||
);
|
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm:ss");
|
||||||
|
|
||||||
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
formatDateTimeWithSecondsMem(locale).format(dateObj)
|
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
format(dateObj, "MMMM D, YYYY, HH:mm:ss" + useAmPm(locale) ? " A" : "");
|
|
||||||
|
@@ -1,52 +1,29 @@
|
|||||||
import { format } from "fecha";
|
import { format } from "fecha";
|
||||||
import memoizeOne from "memoize-one";
|
import { FrontendTranslationData } from "../../data/translation";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
|
||||||
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
|
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
|
||||||
import { useAmPm } from "./use_am_pm";
|
|
||||||
|
|
||||||
const formatTimeMem = memoizeOne(
|
|
||||||
(locale: FrontendLocaleData) =>
|
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: useAmPm(locale),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const formatTime = toLocaleTimeStringSupportsOptions
|
export const formatTime = toLocaleTimeStringSupportsOptions
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||||
formatTimeMem(locale).format(dateObj)
|
dateObj.toLocaleTimeString(locales.language, {
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
hour: "numeric",
|
||||||
format(dateObj, "shortTime" + useAmPm(locale) ? " A" : "");
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
: (dateObj: Date) => format(dateObj, "shortTime");
|
||||||
|
|
||||||
const formatTimeWithSecondsMem = memoizeOne(
|
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
||||||
(locale: FrontendLocaleData) =>
|
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
dateObj.toLocaleTimeString(locales.language, {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
|
||||||
})
|
})
|
||||||
);
|
: (dateObj: Date) => format(dateObj, "mediumTime");
|
||||||
|
|
||||||
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||||
formatTimeWithSecondsMem(locale).format(dateObj)
|
dateObj.toLocaleTimeString(locales.language, {
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
format(dateObj, "mediumTime" + useAmPm(locale) ? " A" : "");
|
|
||||||
|
|
||||||
const formatTimeWeekdayMem = memoizeOne(
|
|
||||||
(locale: FrontendLocaleData) =>
|
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
|
||||||
})
|
})
|
||||||
);
|
: (dateObj: Date) => format(dateObj, "dddd, HH:mm");
|
||||||
|
|
||||||
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
|
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
formatTimeWeekdayMem(locale).format(dateObj)
|
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
format(dateObj, "dddd, HH:mm" + useAmPm(locale) ? " A" : "");
|
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
import { FrontendLocaleData, TimeFormat } from "../../data/translation";
|
|
||||||
|
|
||||||
export const useAmPm = (locale: FrontendLocaleData): boolean => {
|
|
||||||
if (
|
|
||||||
locale.time_format === TimeFormat.language ||
|
|
||||||
locale.time_format === TimeFormat.system
|
|
||||||
) {
|
|
||||||
const testLanguage =
|
|
||||||
locale.time_format === TimeFormat.language ? locale.language : undefined;
|
|
||||||
const test = new Date().toLocaleString(testLanguage);
|
|
||||||
return test.includes("AM") || test.includes("PM");
|
|
||||||
}
|
|
||||||
|
|
||||||
return locale.time_format === TimeFormat.am_pm;
|
|
||||||
};
|
|
@@ -1,4 +1,4 @@
|
|||||||
import { ThemeVars } from "../../data/ws-themes";
|
import { Theme } from "../../data/ws-themes";
|
||||||
import { darkStyles, derivedStyles } from "../../resources/styles";
|
import { darkStyles, derivedStyles } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import {
|
import {
|
||||||
@@ -23,60 +23,50 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
|
|||||||
* Apply a theme to an element by setting the CSS variables on it.
|
* Apply a theme to an element by setting the CSS variables on it.
|
||||||
*
|
*
|
||||||
* element: Element to apply theme on.
|
* element: Element to apply theme on.
|
||||||
* themes: HASS theme information.
|
* themes: HASS Theme information
|
||||||
* selectedTheme: Selected theme.
|
* selectedTheme: selected theme.
|
||||||
* themeSettings: Settings such as selected dark mode and colors.
|
|
||||||
*/
|
*/
|
||||||
export const applyThemesOnElement = (
|
export const applyThemesOnElement = (
|
||||||
element,
|
element,
|
||||||
themes: HomeAssistant["themes"],
|
themes: HomeAssistant["themes"],
|
||||||
selectedTheme?: string,
|
selectedTheme?: string,
|
||||||
themeSettings?: Partial<HomeAssistant["selectedTheme"]>
|
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
|
||||||
) => {
|
) => {
|
||||||
let cacheKey = selectedTheme;
|
let cacheKey = selectedTheme;
|
||||||
let themeRules: Partial<ThemeVars> = {};
|
let themeRules: Partial<Theme> = {};
|
||||||
|
|
||||||
if (themeSettings) {
|
if (selectedTheme === "default" && themeOptions) {
|
||||||
if (themeSettings.dark) {
|
if (themeOptions.dark) {
|
||||||
cacheKey = `${cacheKey}__dark`;
|
cacheKey = `${cacheKey}__dark`;
|
||||||
themeRules = { ...darkStyles };
|
themeRules = darkStyles;
|
||||||
}
|
if (themeOptions.primaryColor) {
|
||||||
|
|
||||||
if (selectedTheme === "default") {
|
|
||||||
// Determine the primary and accent colors from the current settings.
|
|
||||||
// Fallbacks are implicitly the HA default blue and orange or the
|
|
||||||
// derived "darkStyles" values, depending on the light vs dark mode.
|
|
||||||
const primaryColor = themeSettings.primaryColor;
|
|
||||||
const accentColor = themeSettings.accentColor;
|
|
||||||
|
|
||||||
if (themeSettings.dark && primaryColor) {
|
|
||||||
themeRules["app-header-background-color"] = hexBlend(
|
themeRules["app-header-background-color"] = hexBlend(
|
||||||
primaryColor,
|
themeOptions.primaryColor,
|
||||||
"#121212",
|
"#121212",
|
||||||
8
|
8
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (primaryColor) {
|
if (themeOptions.primaryColor) {
|
||||||
cacheKey = `${cacheKey}__primary_${primaryColor}`;
|
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
|
||||||
const rgbPrimaryColor = hex2rgb(primaryColor);
|
const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor);
|
||||||
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
|
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
|
||||||
themeRules["primary-color"] = primaryColor;
|
themeRules["primary-color"] = themeOptions.primaryColor;
|
||||||
const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
|
const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
|
||||||
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
|
themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor);
|
||||||
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
|
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
|
||||||
themeRules["text-primary-color"] =
|
themeRules["text-primary-color"] =
|
||||||
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||||
themeRules["text-light-primary-color"] =
|
themeRules["text-light-primary-color"] =
|
||||||
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
|
rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6
|
||||||
? "#fff"
|
? "#fff"
|
||||||
: "#212121";
|
: "#212121";
|
||||||
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
|
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
|
||||||
}
|
}
|
||||||
if (accentColor) {
|
if (themeOptions.accentColor) {
|
||||||
cacheKey = `${cacheKey}__accent_${accentColor}`;
|
cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`;
|
||||||
themeRules["accent-color"] = accentColor;
|
themeRules["accent-color"] = themeOptions.accentColor;
|
||||||
const rgbAccentColor = hex2rgb(accentColor);
|
const rgbAccentColor = hex2rgb(themeOptions.accentColor);
|
||||||
themeRules["text-accent-color"] =
|
themeRules["text-accent-color"] =
|
||||||
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||||
}
|
}
|
||||||
@@ -86,27 +76,9 @@ export const applyThemesOnElement = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Custom theme logic (not relevant for default theme, since it would override
|
if (selectedTheme && themes.themes[selectedTheme]) {
|
||||||
// the derived calculations from above)
|
themeRules = themes.themes[selectedTheme];
|
||||||
if (
|
|
||||||
selectedTheme &&
|
|
||||||
selectedTheme !== "default" &&
|
|
||||||
themes.themes[selectedTheme]
|
|
||||||
) {
|
|
||||||
// Apply theme vars that are relevant for all modes (but extract the "modes" section first)
|
|
||||||
const { modes, ...baseThemeRules } = themes.themes[selectedTheme];
|
|
||||||
themeRules = { ...themeRules, ...baseThemeRules };
|
|
||||||
|
|
||||||
// Apply theme vars for the specific mode if available
|
|
||||||
if (modes) {
|
|
||||||
if (themeSettings?.dark) {
|
|
||||||
themeRules = { ...themeRules, ...modes.dark };
|
|
||||||
} else {
|
|
||||||
themeRules = { ...themeRules, ...modes.light };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!element._themes?.keys && !Object.keys(themeRules).length) {
|
if (!element._themes?.keys && !Object.keys(themeRules).length) {
|
||||||
@@ -134,12 +106,12 @@ export const applyThemesOnElement = (
|
|||||||
|
|
||||||
const processTheme = (
|
const processTheme = (
|
||||||
cacheKey: string,
|
cacheKey: string,
|
||||||
theme: Partial<ThemeVars>
|
theme: Partial<Theme>
|
||||||
): ProcessedTheme | undefined => {
|
): ProcessedTheme | undefined => {
|
||||||
if (!theme || !Object.keys(theme).length) {
|
if (!theme || !Object.keys(theme).length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const combinedTheme: Partial<ThemeVars> = {
|
const combinedTheme: Partial<Theme> = {
|
||||||
...derivedStyles,
|
...derivedStyles,
|
||||||
...theme,
|
...theme,
|
||||||
};
|
};
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
import { MAIN_WINDOW_NAME } from "../../data/main_window";
|
|
||||||
|
|
||||||
export const mainWindow =
|
|
||||||
window.name === MAIN_WINDOW_NAME
|
|
||||||
? window
|
|
||||||
: parent.name === MAIN_WINDOW_NAME
|
|
||||||
? parent
|
|
||||||
: top;
|
|
@@ -6,7 +6,8 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
|||||||
|
|
||||||
export const setupLeafletMap = async (
|
export const setupLeafletMap = async (
|
||||||
mapElement: HTMLElement,
|
mapElement: HTMLElement,
|
||||||
darkMode?: boolean
|
darkMode?: boolean,
|
||||||
|
draw = false
|
||||||
): Promise<[Map, LeafletModuleType, TileLayer]> => {
|
): Promise<[Map, LeafletModuleType, TileLayer]> => {
|
||||||
if (!mapElement.parentNode) {
|
if (!mapElement.parentNode) {
|
||||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||||
@@ -16,6 +17,10 @@ export const setupLeafletMap = async (
|
|||||||
.default as LeafletModuleType;
|
.default as LeafletModuleType;
|
||||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||||
|
|
||||||
|
if (draw) {
|
||||||
|
await import("leaflet-draw");
|
||||||
|
}
|
||||||
|
|
||||||
const map = Leaflet.map(mapElement);
|
const map = Leaflet.map(mapElement);
|
||||||
const style = document.createElement("link");
|
const style = document.createElement("link");
|
||||||
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
||||||
|
@@ -1,7 +1,14 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
export const SpeechRecognition =
|
export const SpeechRecognition =
|
||||||
|
// @ts-ignore
|
||||||
window.SpeechRecognition || window.webkitSpeechRecognition;
|
window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
// @ts-ignore
|
||||||
export const SpeechGrammarList =
|
export const SpeechGrammarList =
|
||||||
|
// @ts-ignore
|
||||||
window.SpeechGrammarList || window.webkitSpeechGrammarList;
|
window.SpeechGrammarList || window.webkitSpeechGrammarList;
|
||||||
|
// @ts-ignore
|
||||||
export const SpeechRecognitionEvent =
|
export const SpeechRecognitionEvent =
|
||||||
// @ts-expect-error
|
// @ts-ignore
|
||||||
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
|
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
|
||||||
|
/* eslint-enable */
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendTranslationData } from "../../data/translation";
|
||||||
import { formatDate } from "../datetime/format_date";
|
import { formatDate } from "../datetime/format_date";
|
||||||
import { formatDateTime } from "../datetime/format_date_time";
|
import { formatDateTime } from "../datetime/format_date_time";
|
||||||
import { formatTime } from "../datetime/format_time";
|
import { formatTime } from "../datetime/format_time";
|
||||||
@@ -11,7 +11,7 @@ import { computeStateDomain } from "./compute_state_domain";
|
|||||||
export const computeStateDisplay = (
|
export const computeStateDisplay = (
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendTranslationData,
|
||||||
state?: string
|
state?: string
|
||||||
): string => {
|
): string => {
|
||||||
const compareState = state !== undefined ? state : stateObj.state;
|
const compareState = state !== undefined ? state : stateObj.state;
|
||||||
|
@@ -89,6 +89,8 @@ export const domainIcon = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.warn(`Unable to find icon for domain ${domain}`);
|
console.warn(
|
||||||
|
"Unable to find icon for domain " + domain + " (" + stateObj + ")"
|
||||||
|
);
|
||||||
return DEFAULT_DOMAIN_ICON;
|
return DEFAULT_DOMAIN_ICON;
|
||||||
};
|
};
|
||||||
|
19
src/common/entity/timer_time_remaining.ts
Normal file
19
src/common/entity/timer_time_remaining.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import durationToSeconds from "../datetime/duration_to_seconds";
|
||||||
|
|
||||||
|
export const timerTimeRemaining = (
|
||||||
|
stateObj: HassEntity
|
||||||
|
): undefined | number => {
|
||||||
|
if (!stateObj.attributes.remaining) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
|
||||||
|
|
||||||
|
if (stateObj.state === "active") {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const madeActive = new Date(stateObj.last_changed).getTime();
|
||||||
|
timeRemaining = Math.max(timeRemaining - (now - madeActive) / 1000, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeRemaining;
|
||||||
|
};
|
@@ -1,40 +1,35 @@
|
|||||||
import { fireEvent } from "./dom/fire_event";
|
import { fireEvent } from "./dom/fire_event";
|
||||||
import { mainWindow } from "./dom/get_main_window";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"location-changed": NavigateOptions;
|
"location-changed": {
|
||||||
|
replace: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigateOptions {
|
export const navigate = (_node: any, path: string, replace = false) => {
|
||||||
replace?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const navigate = (path: string, options?: NavigateOptions) => {
|
|
||||||
const replace = options?.replace || false;
|
|
||||||
|
|
||||||
if (__DEMO__) {
|
if (__DEMO__) {
|
||||||
if (replace) {
|
if (replace) {
|
||||||
mainWindow.history.replaceState(
|
top.history.replaceState(
|
||||||
mainWindow.history.state?.root ? { root: true } : null,
|
top.history.state?.root ? { root: true } : null,
|
||||||
"",
|
"",
|
||||||
`${mainWindow.location.pathname}#${path}`
|
`${top.location.pathname}#${path}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
mainWindow.location.hash = path;
|
top.location.hash = path;
|
||||||
}
|
}
|
||||||
} else if (replace) {
|
} else if (replace) {
|
||||||
mainWindow.history.replaceState(
|
top.history.replaceState(
|
||||||
mainWindow.history.state?.root ? { root: true } : null,
|
top.history.state?.root ? { root: true } : null,
|
||||||
"",
|
"",
|
||||||
path
|
path
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
mainWindow.history.pushState(null, "", path);
|
top.history.pushState(null, "", path);
|
||||||
}
|
}
|
||||||
fireEvent(mainWindow, "location-changed", {
|
fireEvent(top, "location-changed", {
|
||||||
replace,
|
replace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -1,16 +1,9 @@
|
|||||||
import "@material/mwc-icon-button/mwc-icon-button";
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
import { mdiClose, mdiMagnify } from "@mdi/js";
|
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import {
|
import { customElement, property } from "lit/decorators";
|
||||||
css,
|
import { classMap } from "lit/directives/class-map";
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, query } from "lit/decorators";
|
|
||||||
import "../../components/ha-svg-icon";
|
import "../../components/ha-svg-icon";
|
||||||
import { fireEvent } from "../dom/fire_event";
|
import { fireEvent } from "../dom/fire_event";
|
||||||
|
|
||||||
@@ -34,11 +27,18 @@ class SearchInput extends LitElement {
|
|||||||
this.shadowRoot!.querySelector("paper-input")!.focus();
|
this.shadowRoot!.querySelector("paper-input")!.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@query("paper-input", true) private _input!: PaperInputElement;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
|
<style>
|
||||||
|
.no-underline:not(.focused) {
|
||||||
|
--paper-input-container-underline: {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<paper-input
|
<paper-input
|
||||||
|
class=${classMap({ "no-underline": this.noUnderline })}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label || "Search"}
|
.label=${this.label || "Search"}
|
||||||
.value=${this.filter}
|
.value=${this.filter}
|
||||||
@@ -62,17 +62,6 @@ class SearchInput extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
|
||||||
if (
|
|
||||||
changedProps.has("noUnderline") &&
|
|
||||||
(this.noUnderline || changedProps.get("noUnderline") !== undefined)
|
|
||||||
) {
|
|
||||||
(this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
|
|
||||||
"div.unfocused-line"
|
|
||||||
) as HTMLElement).style.display = this.noUnderline ? "none" : "block";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _filterChanged(value: string) {
|
private async _filterChanged(value: string) {
|
||||||
fireEvent(this, "value-changed", { value: String(value) });
|
fireEvent(this, "value-changed", { value: String(value) });
|
||||||
}
|
}
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
export const escapeRegExp = (text: string): string =>
|
|
||||||
text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
@@ -92,7 +92,7 @@ function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
|
|||||||
return word[pos] !== wordLow[pos];
|
return word[pos] !== wordLow[pos];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPatternInWord(
|
function isPatternInWord(
|
||||||
patternLow: string,
|
patternLow: string,
|
||||||
patternPos: number,
|
patternPos: number,
|
||||||
patternLen: number,
|
patternLen: number,
|
||||||
@@ -121,7 +121,7 @@ enum Arrow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array representing a fuzzy match.
|
* An array representating a fuzzy match.
|
||||||
*
|
*
|
||||||
* 0. the score
|
* 0. the score
|
||||||
* 1. the offset at which matching started
|
* 1. the offset at which matching started
|
||||||
|
@@ -5,7 +5,7 @@ import { fuzzyScore } from "./filter";
|
|||||||
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
|
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
|
||||||
*
|
*
|
||||||
* @param {string} filter - Sequence of letters to check for
|
* @param {string} filter - Sequence of letters to check for
|
||||||
* @param {ScorableTextItem} item - Item against whose strings will be checked
|
* @param {string} word - Word to check for sequence
|
||||||
*
|
*
|
||||||
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
|
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
|
||||||
*/
|
*/
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
|
import { FrontendTranslationData, NumberFormat } from "../../data/translation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
|
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
|
||||||
@@ -9,7 +9,7 @@ import { FrontendLocaleData, NumberFormat } from "../../data/translation";
|
|||||||
*/
|
*/
|
||||||
export const formatNumber = (
|
export const formatNumber = (
|
||||||
num: string | number,
|
num: string | number,
|
||||||
locale?: FrontendLocaleData,
|
locale?: FrontendTranslationData,
|
||||||
options?: Intl.NumberFormatOptions
|
options?: Intl.NumberFormatOptions
|
||||||
): string => {
|
): string => {
|
||||||
let format: string | string[] | undefined;
|
let format: string | string[] | undefined;
|
||||||
|
@@ -1,11 +0,0 @@
|
|||||||
// https://regex101.com/r/kc5C14/2
|
|
||||||
const regExpString = "^\\d{4}-(0[1-9]|1[0-2])-([12]\\d|0[1-9]|3[01])";
|
|
||||||
|
|
||||||
const regExp = new RegExp(regExpString + "$");
|
|
||||||
// 2nd expression without the "end of string" enforced, so it can be used
|
|
||||||
// to just verify the start of a string and then based on that result e.g.
|
|
||||||
// check for a full timestamp string efficiently.
|
|
||||||
const regExpNoStringEnd = new RegExp(regExpString);
|
|
||||||
|
|
||||||
export const isDate = (input: string, allowCharsAfterDate = false): boolean =>
|
|
||||||
allowCharsAfterDate ? regExpNoStringEnd.test(input) : regExp.test(input);
|
|
@@ -1,11 +0,0 @@
|
|||||||
// https://stackoverflow.com/a/14322189/1947205
|
|
||||||
// Changes:
|
|
||||||
// 1. Do not allow a plus or minus at the start.
|
|
||||||
// 2. Enforce that we have a "T" or a blank after the date portion
|
|
||||||
// to ensure we have a timestamp and not only a date.
|
|
||||||
// 3. Disallow dates based on week number.
|
|
||||||
// 4. Disallow dates only consisting of a year.
|
|
||||||
// https://regex101.com/r/kc5C14/3
|
|
||||||
const regexp = /^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])[T| ](((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)(\8[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)$/;
|
|
||||||
|
|
||||||
export const isTimestamp = (input: string): boolean => regexp.test(input);
|
|
@@ -1,6 +0,0 @@
|
|||||||
import { refine, string } from "superstruct";
|
|
||||||
|
|
||||||
const isCustomType = (value: string) => value.startsWith("custom:");
|
|
||||||
|
|
||||||
export const customType = () =>
|
|
||||||
refine(string(), "custom element type", isCustomType);
|
|
@@ -1,6 +1,11 @@
|
|||||||
import { refine, string } from "superstruct";
|
import { refine, string } from "superstruct";
|
||||||
|
|
||||||
const isEntityId = (value: string): boolean => value.includes(".");
|
const isEntityId = (value: string): boolean => {
|
||||||
|
if (!value.includes(".")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const entityId = () =>
|
export const entityId = () =>
|
||||||
refine(string(), "entity ID (domain.entity)", isEntityId);
|
refine(string(), "entity ID (domain.entity)", isEntityId);
|
||||||
|
@@ -1,5 +1,10 @@
|
|||||||
import { refine, string } from "superstruct";
|
import { refine, string } from "superstruct";
|
||||||
|
|
||||||
const isIcon = (value: string) => value.includes(":");
|
const isIcon = (value: string) => {
|
||||||
|
if (!value.includes(":")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const icon = () => refine(string(), "icon (mdi:icon-name)", isIcon);
|
export const icon = () => refine(string(), "icon (mdi:icon-name)", isIcon);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { shouldPolyfill } from "@formatjs/intl-pluralrules/lib/should-polyfill";
|
import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill";
|
||||||
import IntlMessageFormat from "intl-messageformat";
|
import IntlMessageFormat from "intl-messageformat";
|
||||||
import { Resources } from "../../types";
|
import { Resources } from "../../types";
|
||||||
|
|
||||||
@@ -86,15 +86,11 @@ export const computeLocalize = async (
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (!translatedMessage) {
|
if (!translatedMessage) {
|
||||||
try {
|
|
||||||
translatedMessage = new IntlMessageFormat(
|
translatedMessage = new IntlMessageFormat(
|
||||||
translatedValue,
|
translatedValue,
|
||||||
language,
|
language,
|
||||||
formats
|
formats
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
return "Translation error: " + err.message;
|
|
||||||
}
|
|
||||||
cache._localizationCache[messageKey] = translatedMessage;
|
cache._localizationCache[messageKey] = translatedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,25 +4,29 @@
|
|||||||
// be triggered. The function will be called after it stops being called for
|
// be triggered. The function will be called after it stops being called for
|
||||||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||||||
// leading edge, instead of the trailing.
|
// leading edge, instead of the trailing.
|
||||||
|
// eslint-disable-next-line: ban-types
|
||||||
export const debounce = <T extends any[]>(
|
export const debounce = <T extends (...args) => unknown>(
|
||||||
func: (...args: T) => void,
|
func: T,
|
||||||
wait: number,
|
wait,
|
||||||
immediate = false
|
immediate = false
|
||||||
) => {
|
): T => {
|
||||||
let timeout: number | undefined;
|
let timeout;
|
||||||
return (...args: T): void => {
|
// @ts-ignore
|
||||||
|
return function (...args) {
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
const context = this;
|
||||||
const later = () => {
|
const later = () => {
|
||||||
timeout = undefined;
|
timeout = null;
|
||||||
if (!immediate) {
|
if (!immediate) {
|
||||||
func(...args);
|
func.apply(context, args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const callNow = immediate && !timeout;
|
const callNow = immediate && !timeout;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = window.setTimeout(later, wait);
|
timeout = setTimeout(later, wait);
|
||||||
if (callNow) {
|
if (callNow) {
|
||||||
func(...args);
|
func.apply(context, args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
export const promiseTimeout = (ms: number, promise: Promise<any>) => {
|
|
||||||
const timeout = new Promise((_resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
reject(`Timed out in ${ms} ms.`);
|
|
||||||
}, ms);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Returns a race between our timeout and the passed in promise
|
|
||||||
return Promise.race([promise, timeout]);
|
|
||||||
};
|
|
@@ -1,4 +1,4 @@
|
|||||||
import { Layout1d, scroll } from "../../resources/lit-virtualizer";
|
import { Layout1d, scroll } from "@lit-labs/virtualizer";
|
||||||
import deepClone from "deep-clone-simple";
|
import deepClone from "deep-clone-simple";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@@ -173,8 +173,8 @@ export class HaDataTable extends LitElement {
|
|||||||
this.updateComplete.then(() => this._calcTableHeight());
|
this.updateComplete.then(() => this._calcTableHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
public willUpdate(properties: PropertyValues) {
|
protected updated(properties: PropertyValues) {
|
||||||
super.willUpdate(properties);
|
super.updated(properties);
|
||||||
|
|
||||||
if (properties.has("columns")) {
|
if (properties.has("columns")) {
|
||||||
this._filterable = Object.values(this.columns).some(
|
this._filterable = Object.values(this.columns).some(
|
||||||
@@ -246,7 +246,7 @@ export class HaDataTable extends LitElement {
|
|||||||
aria-rowcount=${this._filteredData.length + 1}
|
aria-rowcount=${this._filteredData.length + 1}
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
height: this.autoHeight
|
height: this.autoHeight
|
||||||
? `${(this._filteredData.length || 1) * 53 + 53}px`
|
? `${(this._filteredData.length || 1) * 53 + 57}px`
|
||||||
: `calc(100% - ${this._headerHeight}px)`,
|
: `calc(100% - ${this._headerHeight}px)`,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@@ -340,11 +340,8 @@ export class HaDataTable extends LitElement {
|
|||||||
${scroll({
|
${scroll({
|
||||||
items: this._items,
|
items: this._items,
|
||||||
layout: Layout1d,
|
layout: Layout1d,
|
||||||
|
// @ts-expect-error
|
||||||
renderItem: (row: DataTableRowData, index) => {
|
renderItem: (row: DataTableRowData, index) => {
|
||||||
// not sure how this happens...
|
|
||||||
if (!row) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
if (row.append) {
|
if (row.append) {
|
||||||
return html`
|
return html`
|
||||||
<div class="mdc-data-table__row">${row.content}</div>
|
<div class="mdc-data-table__row">${row.content}</div>
|
||||||
@@ -477,16 +474,15 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.appendRow || this.hasFab) {
|
if (this.appendRow || this.hasFab) {
|
||||||
const items = [...data];
|
this._items = [...data];
|
||||||
|
|
||||||
if (this.appendRow) {
|
if (this.appendRow) {
|
||||||
items.push({ append: true, content: this.appendRow });
|
this._items.push({ append: true, content: this.appendRow });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasFab) {
|
if (this.hasFab) {
|
||||||
items.push({ empty: true });
|
this._items.push({ empty: true });
|
||||||
}
|
}
|
||||||
this._items = items;
|
|
||||||
} else {
|
} else {
|
||||||
this._items = data;
|
this._items = data;
|
||||||
}
|
}
|
||||||
@@ -919,11 +915,13 @@ export class HaDataTable extends LitElement {
|
|||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
.scroller {
|
.scroller {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
contain: strict;
|
||||||
height: calc(100% - 57px);
|
height: calc(100% - 57px);
|
||||||
}
|
}
|
||||||
|
.mdc-data-table__table:not(.auto-height) .scroller {
|
||||||
.mdc-data-table__table.auto-height .scroller {
|
overflow: auto;
|
||||||
overflow-y: hidden !important;
|
|
||||||
}
|
}
|
||||||
.grows {
|
.grows {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@@ -38,7 +38,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
|
|||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./ha-devices-picker";
|
import "./ha-devices-picker";
|
||||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
|
||||||
|
|
||||||
interface DevicesByArea {
|
interface DevicesByArea {
|
||||||
[areaId: string]: AreaDevices;
|
[areaId: string]: AreaDevices;
|
||||||
@@ -50,7 +49,14 @@ interface AreaDevices {
|
|||||||
devices: string[];
|
devices: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style>
|
const rowRenderer = (
|
||||||
|
root: HTMLElement,
|
||||||
|
_owner,
|
||||||
|
model: { item: AreaDevices }
|
||||||
|
) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: -10px 0;
|
margin: -10px 0;
|
||||||
@@ -68,10 +74,17 @@ const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style>
|
|||||||
</style>
|
</style>
|
||||||
<paper-item>
|
<paper-item>
|
||||||
<paper-item-body two-line="">
|
<paper-item-body two-line="">
|
||||||
<div class="name">${item.name}</div>
|
<div class='name'>[[item.name]]</div>
|
||||||
<div secondary>${item.devices.length} devices</div>
|
<div secondary>[[item.devices.length]] devices</div>
|
||||||
</paper-item-body>
|
</paper-item-body>
|
||||||
</paper-item>`;
|
</paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
root.querySelector(".name")!.textContent = model.item.name!;
|
||||||
|
root.querySelector(
|
||||||
|
"[secondary]"
|
||||||
|
)!.textContent = `${model.item.devices.length.toString()} devices`;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-area-devices-picker")
|
@customElement("ha-area-devices-picker")
|
||||||
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||||
@@ -297,7 +310,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
|||||||
item-label-path="name"
|
item-label-path="name"
|
||||||
.items=${areas}
|
.items=${areas}
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
${comboBoxRenderer(rowRenderer)}
|
.renderer=${rowRenderer}
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._areaPicked}
|
@value-changed=${this._areaPicked}
|
||||||
>
|
>
|
||||||
|
@@ -4,6 +4,7 @@ import {
|
|||||||
fetchDeviceActions,
|
fetchDeviceActions,
|
||||||
localizeDeviceAutomationAction,
|
localizeDeviceAutomationAction,
|
||||||
} from "../../data/device_automation";
|
} from "../../data/device_automation";
|
||||||
|
import "../ha-paper-dropdown-menu";
|
||||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||||
|
|
||||||
@customElement("ha-device-action-picker")
|
@customElement("ha-device-action-picker")
|
||||||
|
@@ -4,6 +4,7 @@ import {
|
|||||||
fetchDeviceConditions,
|
fetchDeviceConditions,
|
||||||
localizeDeviceAutomationCondition,
|
localizeDeviceAutomationCondition,
|
||||||
} from "../../data/device_automation";
|
} from "../../data/device_automation";
|
||||||
|
import "../ha-paper-dropdown-menu";
|
||||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||||
|
|
||||||
@customElement("ha-device-condition-picker")
|
@customElement("ha-device-condition-picker")
|
||||||
|
@@ -33,7 +33,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
|
|||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-combo-box";
|
import "../ha-combo-box";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -45,18 +44,27 @@ export type HaDevicePickerDeviceFilterFunc = (
|
|||||||
device: DeviceRegistryEntry
|
device: DeviceRegistryEntry
|
||||||
) => boolean;
|
) => boolean;
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style>
|
const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
margin: -10px 0;
|
margin: -10px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<paper-item>
|
<paper-item>
|
||||||
<paper-item-body two-line>
|
<paper-item-body two-line="">
|
||||||
${item.name}
|
<div class='name'>[[item.name]]</div>
|
||||||
<span secondary>${item.area}</span>
|
<div secondary>[[item.area]]</div>
|
||||||
</paper-item-body>
|
</paper-item-body>
|
||||||
</paper-item>`;
|
</paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelector(".name")!.textContent = model.item.name!;
|
||||||
|
root.querySelector("[secondary]")!.textContent = model.item.area!;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-device-picker")
|
@customElement("ha-device-picker")
|
||||||
export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||||
|
@@ -4,6 +4,7 @@ import {
|
|||||||
fetchDeviceTriggers,
|
fetchDeviceTriggers,
|
||||||
localizeDeviceAutomationTrigger,
|
localizeDeviceAutomationTrigger,
|
||||||
} from "../../data/device_automation";
|
} from "../../data/device_automation";
|
||||||
|
import "../ha-paper-dropdown-menu";
|
||||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||||
|
|
||||||
@customElement("ha-device-trigger-picker")
|
@customElement("ha-device-trigger-picker")
|
||||||
|
@@ -12,7 +12,6 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { PolymerChangedEvent } from "../../polymer-types";
|
import { PolymerChangedEvent } from "../../polymer-types";
|
||||||
@@ -23,13 +22,22 @@ import "./state-badge";
|
|||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
margin: -5px -10px;
|
margin: -10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<paper-item>${formatAttributeName(item)}</paper-item>`;
|
<paper-item></paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
root.querySelector("paper-item")!.textContent = formatAttributeName(
|
||||||
|
model.item
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-entity-attribute-picker")
|
@customElement("ha-entity-attribute-picker")
|
||||||
class HaEntityAttributePicker extends LitElement {
|
class HaEntityAttributePicker extends LitElement {
|
||||||
@@ -74,8 +82,8 @@ class HaEntityAttributePicker extends LitElement {
|
|||||||
<vaadin-combo-box-light
|
<vaadin-combo-box-light
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
attr-for-value="bind-value"
|
attr-for-value="bind-value"
|
||||||
${comboBoxRenderer(rowRenderer)}
|
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
|
@@ -13,7 +13,6 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
@@ -26,19 +25,32 @@ import "./state-badge";
|
|||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
|
const rowRenderer = (
|
||||||
|
root: HTMLElement,
|
||||||
|
_owner,
|
||||||
|
model: { item: HassEntity }
|
||||||
|
) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
paper-icon-item {
|
paper-icon-item {
|
||||||
margin: -10px;
|
margin: -10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<paper-icon-item>
|
<paper-icon-item>
|
||||||
<state-badge slot="item-icon" .stateObj=${item}></state-badge>
|
<state-badge slot="item-icon"></state-badge>
|
||||||
<paper-item-body two-line="">
|
<paper-item-body two-line="">
|
||||||
${computeStateName(item)}
|
<div class='name'></div>
|
||||||
<span secondary>${item.entity_id}</span>
|
<div secondary></div>
|
||||||
</paper-item-body>
|
</paper-item-body>
|
||||||
</paper-icon-item>`;
|
</paper-icon-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
root.querySelector("state-badge")!.stateObj = model.item;
|
||||||
|
root.querySelector(".name")!.textContent = computeStateName(model.item);
|
||||||
|
root.querySelector("[secondary]")!.textContent = model.item.entity_id;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-entity-picker")
|
@customElement("ha-entity-picker")
|
||||||
export class HaEntityPicker extends LitElement {
|
export class HaEntityPicker extends LitElement {
|
||||||
@@ -209,7 +221,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
item-label-path="entity_id"
|
item-label-path="entity_id"
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
${comboBoxRenderer(rowRenderer)}
|
.renderer=${rowRenderer}
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
@filter-changed=${this._filterChanged}
|
@filter-changed=${this._filterChanged}
|
||||||
|
@@ -15,7 +15,7 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
|||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
import { domainIcon } from "../../common/entity/domain_icon";
|
import { domainIcon } from "../../common/entity/domain_icon";
|
||||||
import { stateIcon } from "../../common/entity/state_icon";
|
import { stateIcon } from "../../common/entity/state_icon";
|
||||||
import { timerTimeRemaining } from "../../data/timer";
|
import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
|
||||||
import { formatNumber } from "../../common/string/format_number";
|
import { formatNumber } from "../../common/string/format_number";
|
||||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
@@ -9,20 +9,32 @@ import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
|||||||
import { PolymerChangedEvent } from "../polymer-types";
|
import { PolymerChangedEvent } from "../polymer-types";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { HaComboBox } from "./ha-combo-box";
|
import { HaComboBox } from "./ha-combo-box";
|
||||||
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`<style>
|
const rowRenderer = (
|
||||||
|
root: HTMLElement,
|
||||||
|
_owner,
|
||||||
|
model: { item: HassioAddonInfo }
|
||||||
|
) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
margin: -10px 0;
|
margin: -10px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<paper-item>
|
<paper-item>
|
||||||
<paper-item-body two-line>
|
<paper-item-body two-line="">
|
||||||
${item.name}
|
<div class='name'>[[item.name]]</div>
|
||||||
<span secondary>${item.slug}</span>
|
<div secondary>[[item.slug]]</div>
|
||||||
</paper-item-body>
|
</paper-item-body>
|
||||||
</paper-item>`;
|
</paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelector(".name")!.textContent = model.item.name;
|
||||||
|
root.querySelector("[secondary]")!.textContent = model.item.slug;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-addon-picker")
|
@customElement("ha-addon-picker")
|
||||||
class HaAddonPicker extends LitElement {
|
class HaAddonPicker extends LitElement {
|
||||||
|
@@ -14,9 +14,7 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
import { customElement, property, state, query } from "lit/decorators";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
|
||||||
import { classMap } from "lit/directives/class-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
@@ -44,9 +42,14 @@ import { HomeAssistant } from "../types";
|
|||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
|
const rowRenderer = (
|
||||||
item
|
root: HTMLElement,
|
||||||
) => html`<style>
|
_owner,
|
||||||
|
model: { item: AreaRegistryEntry }
|
||||||
|
) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
margin: -10px 0;
|
margin: -10px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -55,9 +58,20 @@ const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<paper-item class=${classMap({ "add-new": item.area_id === "add_new" })}>
|
<paper-item>
|
||||||
<paper-item-body two-line>${item.name}</paper-item-body>
|
<paper-item-body two-line>
|
||||||
</paper-item>`;
|
<div class='name'>[[item.name]]</div>
|
||||||
|
</paper-item-body>
|
||||||
|
</paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
root.querySelector(".name")!.textContent = model.item.name!;
|
||||||
|
if (model.item.area_id === "add_new") {
|
||||||
|
root.querySelector("paper-item")!.className = "add-new";
|
||||||
|
} else {
|
||||||
|
root.querySelector("paper-item")!.classList.remove("add-new");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-area-picker")
|
@customElement("ha-area-picker")
|
||||||
export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||||
@@ -326,8 +340,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
|||||||
item-id-path="area_id"
|
item-id-path="area_id"
|
||||||
item-label-path="name"
|
item-label-path="name"
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
${comboBoxRenderer(rowRenderer)}
|
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._areaChanged}
|
@value-changed=${this._areaChanged}
|
||||||
>
|
>
|
||||||
|
@@ -1,24 +1,20 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { until } from "lit/directives/until";
|
||||||
import { haStyle } from "../resources/styles";
|
import { haStyle } from "../resources/styles";
|
||||||
import { HomeAssistant } from "../types";
|
|
||||||
import hassAttributeUtil, {
|
import hassAttributeUtil, {
|
||||||
formatAttributeName,
|
formatAttributeName,
|
||||||
formatAttributeValue,
|
|
||||||
} from "../util/hass-attributes-util";
|
} from "../util/hass-attributes-util";
|
||||||
import "./ha-expansion-panel";
|
|
||||||
|
let jsYamlPromise: Promise<typeof import("js-yaml")>;
|
||||||
|
|
||||||
@customElement("ha-attributes")
|
@customElement("ha-attributes")
|
||||||
class HaAttributes extends LitElement {
|
class HaAttributes extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public stateObj?: HassEntity;
|
@property() public stateObj?: HassEntity;
|
||||||
|
|
||||||
@property({ attribute: "extra-filters" }) public extraFilters?: string;
|
@property({ attribute: "extra-filters" }) public extraFilters?: string;
|
||||||
|
|
||||||
@state() private _expanded = false;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.stateObj) {
|
if (!this.stateObj) {
|
||||||
return html``;
|
return html``;
|
||||||
@@ -34,30 +30,16 @@ class HaAttributes extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-expansion-panel
|
<hr />
|
||||||
.header=${this.hass.localize(
|
<div>
|
||||||
"ui.components.attributes.expansion_header"
|
|
||||||
)}
|
|
||||||
outlined
|
|
||||||
@expanded-will-change=${this.expandedChanged}
|
|
||||||
>
|
|
||||||
<div class="attribute-container">
|
|
||||||
${this._expanded
|
|
||||||
? html`
|
|
||||||
${attributes.map(
|
${attributes.map(
|
||||||
(attribute) => html`
|
(attribute) => html`
|
||||||
<div class="data-entry">
|
<div class="data-entry">
|
||||||
<div class="key">${formatAttributeName(attribute)}</div>
|
<div class="key">${formatAttributeName(attribute)}</div>
|
||||||
<div class="value">
|
<div class="value">${this.formatAttribute(attribute)}</div>
|
||||||
${this.formatAttribute(attribute)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</ha-expansion-panel>
|
|
||||||
${this.stateObj.attributes.attribution
|
${this.stateObj.attributes.attribution
|
||||||
? html`
|
? html`
|
||||||
<div class="attribution">
|
<div class="attribution">
|
||||||
@@ -65,6 +47,7 @@ class HaAttributes extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,9 +55,6 @@ class HaAttributes extends LitElement {
|
|||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
css`
|
css`
|
||||||
.attribute-container {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.data-entry {
|
.data-entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -91,7 +71,6 @@ class HaAttributes extends LitElement {
|
|||||||
.attribution {
|
.attribution {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 16px;
|
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
@@ -123,11 +102,38 @@ class HaAttributes extends LitElement {
|
|||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
const value = this.stateObj.attributes[attribute];
|
const value = this.stateObj.attributes[attribute];
|
||||||
return formatAttributeValue(this.hass, value);
|
return this.formatAttributeValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private expandedChanged(ev) {
|
private formatAttributeValue(value: any): string | TemplateResult {
|
||||||
this._expanded = ev.detail.expanded;
|
if (value === null) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
// YAML handling
|
||||||
|
if (
|
||||||
|
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
|
||||||
|
(!Array.isArray(value) && value instanceof Object)
|
||||||
|
) {
|
||||||
|
if (!jsYamlPromise) {
|
||||||
|
jsYamlPromise = import("js-yaml");
|
||||||
|
}
|
||||||
|
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
|
||||||
|
return html` <pre>${until(yaml, "")}</pre> `;
|
||||||
|
}
|
||||||
|
// URL handling
|
||||||
|
if (typeof value === "string" && value.startsWith("http")) {
|
||||||
|
try {
|
||||||
|
// If invalid URL, exception will be raised
|
||||||
|
const url = new URL(value);
|
||||||
|
if (url.protocol === "http:" || url.protocol === "https:")
|
||||||
|
return html`<a target="_blank" rel="noreferrer" href="${value}"
|
||||||
|
>${value}</a
|
||||||
|
>`;
|
||||||
|
} catch (_) {
|
||||||
|
// Nothing to do here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.isArray(value) ? value.join(", ") : value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,8 +12,6 @@ export class HaButtonMenu extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public fixed = false;
|
|
||||||
|
|
||||||
@query("mwc-menu", true) private _menu?: Menu;
|
@query("mwc-menu", true) private _menu?: Menu;
|
||||||
|
|
||||||
public get items() {
|
public get items() {
|
||||||
@@ -31,7 +29,6 @@ export class HaButtonMenu extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<mwc-menu
|
<mwc-menu
|
||||||
.corner=${this.corner}
|
.corner=${this.corner}
|
||||||
.fixed=${this.fixed}
|
|
||||||
.multi=${this.multi}
|
.multi=${this.multi}
|
||||||
.activatable=${this.activatable}
|
.activatable=${this.activatable}
|
||||||
>
|
>
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { CircularProgress } from "@material/mwc-circular-progress";
|
import { CircularProgress } from "@material/mwc-circular-progress";
|
||||||
import { CSSResultGroup, css } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-circular-progress")
|
@customElement("ha-circular-progress")
|
||||||
@@ -42,17 +41,6 @@ export class HaCircularProgress extends CircularProgress {
|
|||||||
public get indeterminate() {
|
public get indeterminate() {
|
||||||
return this.active;
|
return this.active;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
super.styles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -6,20 +6,31 @@ import "@polymer/paper-item/paper-item-body";
|
|||||||
import "@polymer/paper-listbox/paper-listbox";
|
import "@polymer/paper-listbox/paper-listbox";
|
||||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
import { customElement, property, state, query } from "lit/decorators";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { PolymerChangedEvent } from "../polymer-types";
|
import { PolymerChangedEvent } from "../polymer-types";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
const defaultRowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
const defaultRowRenderer = (
|
||||||
|
root: HTMLElement,
|
||||||
|
_owner,
|
||||||
|
model: { item: any }
|
||||||
|
) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
margin: -5px -10px;
|
margin: -5px -10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<paper-item>${item}</paper-item>`;
|
<paper-item></paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelector("paper-item")!.textContent = model.item;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-combo-box")
|
@customElement("ha-combo-box")
|
||||||
export class HaComboBox extends LitElement {
|
export class HaComboBox extends LitElement {
|
||||||
@@ -42,7 +53,11 @@ export class HaComboBox extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
|
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
|
||||||
|
|
||||||
@property() public renderer?: ComboBoxLitRenderer<any>;
|
@property() public renderer?: (
|
||||||
|
root: HTMLElement,
|
||||||
|
owner: HTMLElement,
|
||||||
|
model: { item: any }
|
||||||
|
) => void;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled?: boolean;
|
@property({ type: Boolean }) public disabled?: boolean;
|
||||||
|
|
||||||
@@ -75,9 +90,9 @@ export class HaComboBox extends LitElement {
|
|||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.items=${this.items}
|
.items=${this.items}
|
||||||
.filteredItems=${this.filteredItems}
|
.filteredItems=${this.filteredItems}
|
||||||
|
.renderer=${this.renderer || defaultRowRenderer}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
${comboBoxRenderer(this.renderer || defaultRowRenderer)}
|
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@filter-changed=${this._filterChanged}
|
@filter-changed=${this._filterChanged}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
|
@@ -23,8 +23,8 @@ class HaCoverControls extends LitElement {
|
|||||||
|
|
||||||
@state() private _entityObj?: CoverEntity;
|
@state() private _entityObj?: CoverEntity;
|
||||||
|
|
||||||
public willUpdate(changedProperties: PropertyValues): void {
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
super.willUpdate(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
if (changedProperties.has("stateObj")) {
|
if (changedProperties.has("stateObj")) {
|
||||||
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
||||||
|
@@ -22,8 +22,8 @@ class HaCoverTiltControls extends LitElement {
|
|||||||
|
|
||||||
@state() private _entityObj?: CoverEntity;
|
@state() private _entityObj?: CoverEntity;
|
||||||
|
|
||||||
public willUpdate(changedProperties: PropertyValues): void {
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
super.willUpdate(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
if (changedProperties.has("stateObj")) {
|
if (changedProperties.has("stateObj")) {
|
||||||
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
||||||
|
@@ -14,7 +14,6 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { formatDateTime } from "../common/datetime/format_date_time";
|
import { formatDateTime } from "../common/datetime/format_date_time";
|
||||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
|
||||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./date-range-picker";
|
import "./date-range-picker";
|
||||||
@@ -44,7 +43,7 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
if (changedProps.has("hass")) {
|
if (changedProps.has("hass")) {
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
if (!oldHass || oldHass.locale !== this.hass.locale) {
|
if (!oldHass || oldHass.locale !== this.hass.locale) {
|
||||||
this._hour24format = !useAmPm(this.hass.locale);
|
this._hour24format = this._compute24hourFormat();
|
||||||
this._rtlDirection = computeRTLDirection(this.hass);
|
this._rtlDirection = computeRTLDirection(this.hass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,6 +106,16 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _compute24hourFormat() {
|
||||||
|
return (
|
||||||
|
new Intl.DateTimeFormat(this.hass.language, {
|
||||||
|
hour: "numeric",
|
||||||
|
})
|
||||||
|
.formatToParts(new Date(2020, 0, 1, 13))
|
||||||
|
.find((part) => part.type === "hour")!.value.length === 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private _setDateRange(ev: CustomEvent<ActionDetail>) {
|
private _setDateRange(ev: CustomEvent<ActionDetail>) {
|
||||||
const dateRange = Object.values(this.ranges!)[ev.detail.index];
|
const dateRange = Object.values(this.ranges!)[ev.detail.index];
|
||||||
const dateRangePicker = this._dateRangePicker;
|
const dateRangePicker = this._dateRangePicker;
|
||||||
|
@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { nextRender } from "../common/util/render-status";
|
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
@customElement("ha-expansion-panel")
|
@customElement("ha-expansion-panel")
|
||||||
@@ -14,17 +13,12 @@ class HaExpansionPanel extends LitElement {
|
|||||||
|
|
||||||
@property() header?: string;
|
@property() header?: string;
|
||||||
|
|
||||||
@property() secondary?: string;
|
|
||||||
|
|
||||||
@query(".container") private _container!: HTMLDivElement;
|
@query(".container") private _container!: HTMLDivElement;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="summary" @click=${this._toggleContainer}>
|
<div class="summary" @click=${this._toggleContainer}>
|
||||||
<slot class="header" name="header">
|
<slot name="header">${this.header}</slot>
|
||||||
${this.header}
|
|
||||||
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
|
||||||
</slot>
|
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
.path=${mdiChevronDown}
|
.path=${mdiChevronDown}
|
||||||
class="summary-icon ${classMap({ expanded: this.expanded })}"
|
class="summary-icon ${classMap({ expanded: this.expanded })}"
|
||||||
@@ -43,25 +37,17 @@ class HaExpansionPanel extends LitElement {
|
|||||||
this._container.style.removeProperty("height");
|
this._container.style.removeProperty("height");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _toggleContainer(): Promise<void> {
|
private _toggleContainer(): void {
|
||||||
const newExpanded = !this.expanded;
|
|
||||||
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
|
|
||||||
|
|
||||||
if (newExpanded) {
|
|
||||||
// allow for dynamic content to be rendered
|
|
||||||
await nextRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollHeight = this._container.scrollHeight;
|
const scrollHeight = this._container.scrollHeight;
|
||||||
this._container.style.height = `${scrollHeight}px`;
|
this._container.style.height = `${scrollHeight}px`;
|
||||||
|
|
||||||
if (!newExpanded) {
|
if (this.expanded) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._container.style.height = "0px";
|
this._container.style.height = "0px";
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.expanded = newExpanded;
|
this.expanded = !this.expanded;
|
||||||
fireEvent(this, "expanded-changed", { expanded: this.expanded });
|
fireEvent(this, "expanded-changed", { expanded: this.expanded });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,16 +97,6 @@ class HaExpansionPanel extends LitElement {
|
|||||||
.container.expanded {
|
.container.expanded {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary {
|
|
||||||
display: block;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,8 +111,5 @@ declare global {
|
|||||||
"expanded-changed": {
|
"expanded-changed": {
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
};
|
};
|
||||||
"expanded-will-change": {
|
|
||||||
expanded: boolean;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,17 +4,17 @@ import { ifDefined } from "lit/directives/if-defined";
|
|||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { formatNumber } from "../common/string/format_number";
|
import { formatNumber } from "../common/string/format_number";
|
||||||
import { afterNextRender } from "../common/util/render-status";
|
import { afterNextRender } from "../common/util/render-status";
|
||||||
import { FrontendLocaleData } from "../data/translation";
|
import { FrontendTranslationData } from "../data/translation";
|
||||||
import { getValueInPercentage, normalize } from "../util/calculate";
|
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||||
|
|
||||||
// Workaround for https://github.com/home-assistant/frontend/issues/6467
|
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
||||||
|
|
||||||
const getAngle = (value: number, min: number, max: number) => {
|
const getAngle = (value: number, min: number, max: number) => {
|
||||||
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||||
return (percentage * 180) / 100;
|
return (percentage * 180) / 100;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Workaround for https://github.com/home-assistant/frontend/issues/6467
|
||||||
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
@customElement("ha-gauge")
|
@customElement("ha-gauge")
|
||||||
export class Gauge extends LitElement {
|
export class Gauge extends LitElement {
|
||||||
@property({ type: Number }) public min = 0;
|
@property({ type: Number }) public min = 0;
|
||||||
@@ -23,7 +23,7 @@ export class Gauge extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Number }) public value = 0;
|
@property({ type: Number }) public value = 0;
|
||||||
|
|
||||||
@property() public locale!: FrontendLocaleData;
|
@property() public locale!: FrontendTranslationData;
|
||||||
|
|
||||||
@property() public label = "";
|
@property() public label = "";
|
||||||
|
|
||||||
|
@@ -13,11 +13,6 @@ import { nextRender } from "../common/util/render-status";
|
|||||||
import { getExternalConfig } from "../external_app/external_config";
|
import { getExternalConfig } from "../external_app/external_config";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
type HlsLite = Omit<
|
|
||||||
HlsType,
|
|
||||||
"subtitleTrackController" | "audioTrackController" | "emeController"
|
|
||||||
>;
|
|
||||||
|
|
||||||
@customElement("ha-hls-player")
|
@customElement("ha-hls-player")
|
||||||
class HaHLSPlayer extends LitElement {
|
class HaHLSPlayer extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -44,7 +39,7 @@ class HaHLSPlayer extends LitElement {
|
|||||||
|
|
||||||
@state() private _attached = false;
|
@state() private _attached = false;
|
||||||
|
|
||||||
private _hlsPolyfillInstance?: HlsLite;
|
private _hlsPolyfillInstance?: HlsType;
|
||||||
|
|
||||||
private _useExoPlayer = false;
|
private _useExoPlayer = false;
|
||||||
|
|
||||||
@@ -108,8 +103,7 @@ class HaHLSPlayer extends LitElement {
|
|||||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||||
const masterPlaylistPromise = fetch(this.url);
|
const masterPlaylistPromise = fetch(this.url);
|
||||||
|
|
||||||
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min.js"))
|
const Hls = (await import("hls.js")).default;
|
||||||
.default;
|
|
||||||
let hlsSupported = Hls.isSupported();
|
let hlsSupported = Hls.isSupported();
|
||||||
|
|
||||||
if (!hlsSupported) {
|
if (!hlsSupported) {
|
||||||
@@ -188,7 +182,7 @@ class HaHLSPlayer extends LitElement {
|
|||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
const hls = new Hls({
|
const hls = new Hls({
|
||||||
backBufferLength: 60,
|
liveBackBufferLength: 60,
|
||||||
fragLoadingTimeOut: 30000,
|
fragLoadingTimeOut: 30000,
|
||||||
manifestLoadingTimeOut: 30000,
|
manifestLoadingTimeOut: 30000,
|
||||||
levelLoadingTimeOut: 30000,
|
levelLoadingTimeOut: 30000,
|
||||||
|
@@ -125,7 +125,6 @@ export class HaIcon extends LitElement {
|
|||||||
databaseIcon = await getIcon(iconName);
|
databaseIcon = await getIcon(iconName);
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
// Firefox in private mode doesn't support IDB
|
// Firefox in private mode doesn't support IDB
|
||||||
// iOS Safari sometimes doesn't open the DB
|
|
||||||
databaseIcon = undefined;
|
databaseIcon = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,179 +0,0 @@
|
|||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
|
||||||
import {
|
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
nothing,
|
|
||||||
LitElement,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, state, property } from "lit/decorators";
|
|
||||||
import {
|
|
||||||
Adapter,
|
|
||||||
NetworkConfig,
|
|
||||||
IPv6ConfiguredAddress,
|
|
||||||
IPv4ConfiguredAddress,
|
|
||||||
} from "../data/network";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
import { haStyle } from "../resources/styles";
|
|
||||||
import { HomeAssistant } from "../types";
|
|
||||||
import "./ha-checkbox";
|
|
||||||
import type { HaCheckbox } from "./ha-checkbox";
|
|
||||||
import "./ha-settings-row";
|
|
||||||
import "./ha-icon";
|
|
||||||
|
|
||||||
const format_addresses = (
|
|
||||||
addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[]
|
|
||||||
): TemplateResult =>
|
|
||||||
html`${addresses.map((address, i) => [
|
|
||||||
html`<span>${address.address}/${address.network_prefix}</span>`,
|
|
||||||
i < addresses.length - 1 ? ", " : nothing,
|
|
||||||
])}`;
|
|
||||||
|
|
||||||
const format_auto_detected_interfaces = (
|
|
||||||
adapters: Adapter[]
|
|
||||||
): Array<TemplateResult | string> =>
|
|
||||||
adapters.map((adapter) =>
|
|
||||||
adapter.auto
|
|
||||||
? html`${adapter.name}
|
|
||||||
(${format_addresses([...adapter.ipv4, ...adapter.ipv6])})`
|
|
||||||
: ""
|
|
||||||
);
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HASSDomEvents {
|
|
||||||
"network-config-changed": { configured_adapters: string[] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@customElement("ha-network")
|
|
||||||
export class HaNetwork extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public networkConfig?: NetworkConfig;
|
|
||||||
|
|
||||||
@state() private _expanded?: boolean;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (this.networkConfig === undefined) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
const configured_adapters = this.networkConfig.configured_adapters || [];
|
|
||||||
return html`
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="prefix">
|
|
||||||
<ha-checkbox
|
|
||||||
id="auto_configure"
|
|
||||||
@change=${this._handleAutoConfigureCheckboxClick}
|
|
||||||
.checked=${!configured_adapters.length}
|
|
||||||
name="auto_configure"
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</span>
|
|
||||||
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
|
|
||||||
<span slot="description" data-for="auto_configure">
|
|
||||||
Detected:
|
|
||||||
${format_auto_detected_interfaces(this.networkConfig.adapters)}
|
|
||||||
</span>
|
|
||||||
</ha-settings-row>
|
|
||||||
${configured_adapters.length || this._expanded
|
|
||||||
? this.networkConfig.adapters.map(
|
|
||||||
(adapter) =>
|
|
||||||
html`<ha-settings-row>
|
|
||||||
<span slot="prefix">
|
|
||||||
<ha-checkbox
|
|
||||||
id=${adapter.name}
|
|
||||||
@change=${this._handleAdapterCheckboxClick}
|
|
||||||
.checked=${configured_adapters.includes(adapter.name)}
|
|
||||||
.adapter=${adapter.name}
|
|
||||||
name=${adapter.name}
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</span>
|
|
||||||
<span slot="heading">
|
|
||||||
Adapter: ${adapter.name}
|
|
||||||
${adapter.default
|
|
||||||
? html`<ha-icon .icon="hass:star"></ha-icon> (Default)`
|
|
||||||
: ""}
|
|
||||||
</span>
|
|
||||||
<span slot="description">
|
|
||||||
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
|
|
||||||
</span>
|
|
||||||
</ha-settings-row>`
|
|
||||||
)
|
|
||||||
: ""}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleAutoConfigureCheckboxClick(ev: Event) {
|
|
||||||
const checkbox = ev.currentTarget as HaCheckbox;
|
|
||||||
if (this.networkConfig === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let configured_adapters = [...this.networkConfig.configured_adapters];
|
|
||||||
|
|
||||||
if (checkbox.checked) {
|
|
||||||
this._expanded = false;
|
|
||||||
configured_adapters = [];
|
|
||||||
} else {
|
|
||||||
this._expanded = true;
|
|
||||||
for (const adapter of this.networkConfig.adapters) {
|
|
||||||
if (adapter.default) {
|
|
||||||
configured_adapters = [adapter.name];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent(this, "network-config-changed", {
|
|
||||||
configured_adapters: configured_adapters,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleAdapterCheckboxClick(ev: Event) {
|
|
||||||
const checkbox = ev.currentTarget as HaCheckbox;
|
|
||||||
const adapter_name = (checkbox as any).name;
|
|
||||||
if (this.networkConfig === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configured_adapters = [...this.networkConfig.configured_adapters];
|
|
||||||
|
|
||||||
if (checkbox.checked) {
|
|
||||||
configured_adapters.push(adapter_name);
|
|
||||||
} else {
|
|
||||||
const index = configured_adapters.indexOf(adapter_name, 0);
|
|
||||||
configured_adapters.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent(this, "network-config-changed", {
|
|
||||||
configured_adapters: configured_adapters,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
css`
|
|
||||||
.error {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-settings-row {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
span[slot="heading"],
|
|
||||||
span[slot="description"] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-network": HaNetwork;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,13 +1,10 @@
|
|||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { useAmPm } from "../../common/datetime/use_am_pm";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { TimeSelector } from "../../data/selector";
|
import { TimeSelector } from "../../data/selector";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../paper-time-input";
|
import "../paper-time-input";
|
||||||
|
|
||||||
@customElement("ha-selector-time")
|
@customElement("ha-selector-time")
|
||||||
export class HaTimeSelector extends LitElement {
|
export class HaTimeSelector extends LitElement {
|
||||||
@property() public hass!: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
@@ -20,12 +17,13 @@ export class HaTimeSelector extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
private _useAmPmMem = memoizeOne((locale: FrontendLocaleData): boolean =>
|
private _useAmPm = memoizeOne((language: string) => {
|
||||||
useAmPm(locale)
|
const test = new Date().toLocaleString(language);
|
||||||
);
|
return test.includes("AM") || test.includes("PM");
|
||||||
|
});
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const useAMPM = this._useAmPmMem(this.hass.locale);
|
const useAMPM = this._useAmPm(this.hass.locale.language);
|
||||||
|
|
||||||
const parts = this.value?.split(":") || [];
|
const parts = this.value?.split(":") || [];
|
||||||
const hours = parts[0];
|
const hours = parts[0];
|
||||||
@@ -50,7 +48,7 @@ export class HaTimeSelector extends LitElement {
|
|||||||
|
|
||||||
private _timeChanged(ev) {
|
private _timeChanged(ev) {
|
||||||
let value = ev.target.value;
|
let value = ev.target.value;
|
||||||
const useAMPM = this._useAmPmMem(this.hass.locale);
|
const useAMPM = this._useAmPm(this.hass.locale.language);
|
||||||
let hours = Number(ev.target.hour || 0);
|
let hours = Number(ev.target.hour || 0);
|
||||||
if (value && useAMPM) {
|
if (value && useAMPM) {
|
||||||
if (ev.target.amPm === "PM") {
|
if (ev.target.amPm === "PM") {
|
||||||
|
@@ -6,22 +6,33 @@ import { LocalizeFunc } from "../common/translations/localize";
|
|||||||
import { domainToName } from "../data/integration";
|
import { domainToName } from "../data/integration";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-combo-box";
|
import "./ha-combo-box";
|
||||||
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
|
const rowRenderer = (
|
||||||
item
|
root: HTMLElement,
|
||||||
) => html`<style>
|
_owner,
|
||||||
|
model: { item: { service: string; name: string } }
|
||||||
|
) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
margin: -10px 0;
|
margin: -10px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<paper-item>
|
<paper-item>
|
||||||
<paper-item-body two-line>
|
<paper-item-body two-line="">
|
||||||
${item.name}
|
<div class='name'>[[item.name]]</div>
|
||||||
<span secondary>${item.name === item.service ? "" : item.service}</span>
|
<div secondary>[[item.service]]</div>
|
||||||
</paper-item-body>
|
</paper-item-body>
|
||||||
</paper-item>`;
|
</paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelector(".name")!.textContent = model.item.name;
|
||||||
|
root.querySelector("[secondary]")!.textContent =
|
||||||
|
model.item.name === model.item.service ? "" : model.item.service;
|
||||||
|
};
|
||||||
|
|
||||||
class HaServicePicker extends LitElement {
|
class HaServicePicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
@@ -117,10 +117,7 @@ export class HaTab extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host([narrow]) {
|
:host([narrow]) {
|
||||||
min-width: 0;
|
padding: 0 16px;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml";
|
import { safeDump, safeLoad } from "js-yaml";
|
||||||
import { html, LitElement, TemplateResult } from "lit";
|
import { html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
@@ -20,8 +20,6 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
|
|||||||
export class HaYamlEditor extends LitElement {
|
export class HaYamlEditor extends LitElement {
|
||||||
@property() public value?: any;
|
@property() public value?: any;
|
||||||
|
|
||||||
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
|
|
||||||
|
|
||||||
@property() public defaultValue?: any;
|
@property() public defaultValue?: any;
|
||||||
|
|
||||||
@property() public isValid = true;
|
@property() public isValid = true;
|
||||||
@@ -32,10 +30,7 @@ export class HaYamlEditor extends LitElement {
|
|||||||
|
|
||||||
public setValue(value): void {
|
public setValue(value): void {
|
||||||
try {
|
try {
|
||||||
this._yaml =
|
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
|
||||||
value && !isEmpty(value)
|
|
||||||
? dump(value, { schema: this.yamlSchema })
|
|
||||||
: "";
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(err, value);
|
console.error(err, value);
|
||||||
@@ -72,7 +67,7 @@ export class HaYamlEditor extends LitElement {
|
|||||||
|
|
||||||
if (this._yaml) {
|
if (this._yaml) {
|
||||||
try {
|
try {
|
||||||
parsed = load(this._yaml, { schema: this.yamlSchema });
|
parsed = safeLoad(this._yaml);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Invalid YAML
|
// Invalid YAML
|
||||||
isValid = false;
|
isValid = false;
|
||||||
|
@@ -1,69 +0,0 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
|
||||||
import { property } from "lit/decorators";
|
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
|
|
||||||
class HaEntityMarker extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: "entity-id" }) public entityId?: string;
|
|
||||||
|
|
||||||
@property({ attribute: "entity-name" }) public entityName?: string;
|
|
||||||
|
|
||||||
@property({ attribute: "entity-picture" }) public entityPicture?: string;
|
|
||||||
|
|
||||||
@property({ attribute: "entity-color" }) public entityColor?: string;
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="marker"
|
|
||||||
style=${styleMap({ "border-color": this.entityColor })}
|
|
||||||
@click=${this._badgeTap}
|
|
||||||
>
|
|
||||||
${this.entityPicture
|
|
||||||
? html`<div
|
|
||||||
class="entity-picture"
|
|
||||||
style=${styleMap({
|
|
||||||
"background-image": `url(${this.entityPicture})`,
|
|
||||||
})}
|
|
||||||
></div>`
|
|
||||||
: this.entityName}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _badgeTap(ev: Event) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
if (this.entityId) {
|
|
||||||
fireEvent(this, "hass-more-info", { entityId: this.entityId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
.marker {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
font-size: var(--ha-marker-font-size, 1.5em);
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid var(--ha-marker-color, var(--primary-color));
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
background-color: var(--card-background-color);
|
|
||||||
}
|
|
||||||
.entity-picture {
|
|
||||||
background-size: cover;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-entity-marker", HaEntityMarker);
|
|
299
src/components/map/ha-location-editor.ts
Normal file
299
src/components/map/ha-location-editor.ts
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import {
|
||||||
|
Circle,
|
||||||
|
DivIcon,
|
||||||
|
DragEndEvent,
|
||||||
|
LatLng,
|
||||||
|
LeafletMouseEvent,
|
||||||
|
Map,
|
||||||
|
Marker,
|
||||||
|
TileLayer,
|
||||||
|
} from "leaflet";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
LeafletModuleType,
|
||||||
|
replaceTileLayer,
|
||||||
|
setupLeafletMap,
|
||||||
|
} from "../../common/dom/setup-leaflet-map";
|
||||||
|
import { nextRender } from "../../common/util/render-status";
|
||||||
|
import { defaultRadiusColor } from "../../data/zone";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
|
@customElement("ha-location-editor")
|
||||||
|
class LocationEditor extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Array }) public location?: [number, number];
|
||||||
|
|
||||||
|
@property({ type: Number }) public radius?: number;
|
||||||
|
|
||||||
|
@property() public radiusColor?: string;
|
||||||
|
|
||||||
|
@property() public icon?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public darkMode?: boolean;
|
||||||
|
|
||||||
|
public fitZoom = 16;
|
||||||
|
|
||||||
|
private _iconEl?: DivIcon;
|
||||||
|
|
||||||
|
private _ignoreFitToMap?: [number, number];
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
private Leaflet?: LeafletModuleType;
|
||||||
|
|
||||||
|
private _leafletMap?: Map;
|
||||||
|
|
||||||
|
private _tileLayer?: TileLayer;
|
||||||
|
|
||||||
|
private _locationMarker?: Marker | Circle;
|
||||||
|
|
||||||
|
public fitMap(): void {
|
||||||
|
if (!this._leafletMap || !this.location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._locationMarker && "getBounds" in this._locationMarker) {
|
||||||
|
this._leafletMap.fitBounds(this._locationMarker.getBounds());
|
||||||
|
} else {
|
||||||
|
this._leafletMap.setView(this.location, this.fitZoom);
|
||||||
|
}
|
||||||
|
this._ignoreFitToMap = this.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html` <div id="map"></div> `;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this._initMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
// Still loading.
|
||||||
|
if (!this.Leaflet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("location")) {
|
||||||
|
this._updateMarker();
|
||||||
|
if (
|
||||||
|
this.location &&
|
||||||
|
(!this._ignoreFitToMap ||
|
||||||
|
this._ignoreFitToMap[0] !== this.location[0] ||
|
||||||
|
this._ignoreFitToMap[1] !== this.location[1])
|
||||||
|
) {
|
||||||
|
this.fitMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changedProps.has("radius")) {
|
||||||
|
this._updateRadius();
|
||||||
|
}
|
||||||
|
if (changedProps.has("radiusColor")) {
|
||||||
|
this._updateRadiusColor();
|
||||||
|
}
|
||||||
|
if (changedProps.has("icon")) {
|
||||||
|
this._updateIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("hass")) {
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
if (!oldHass || oldHass.themes?.darkMode === this.hass.themes?.darkMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this._leafletMap || !this._tileLayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._tileLayer = replaceTileLayer(
|
||||||
|
this.Leaflet,
|
||||||
|
this._leafletMap,
|
||||||
|
this._tileLayer,
|
||||||
|
this.hass.themes?.darkMode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _mapEl(): HTMLDivElement {
|
||||||
|
return this.shadowRoot!.querySelector("div")!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initMap(): Promise<void> {
|
||||||
|
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||||
|
this._mapEl,
|
||||||
|
this.darkMode ?? this.hass.themes?.darkMode,
|
||||||
|
Boolean(this.radius)
|
||||||
|
);
|
||||||
|
this._leafletMap.addEventListener(
|
||||||
|
"click",
|
||||||
|
// @ts-ignore
|
||||||
|
(ev: LeafletMouseEvent) => this._locationUpdated(ev.latlng)
|
||||||
|
);
|
||||||
|
this._updateIcon();
|
||||||
|
this._updateMarker();
|
||||||
|
this.fitMap();
|
||||||
|
this._leafletMap.invalidateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _locationUpdated(latlng: LatLng) {
|
||||||
|
let longitude = latlng.lng;
|
||||||
|
if (Math.abs(longitude) > 180.0) {
|
||||||
|
// Normalize longitude if map provides values beyond -180 to +180 degrees.
|
||||||
|
longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0;
|
||||||
|
}
|
||||||
|
this.location = this._ignoreFitToMap = [latlng.lat, longitude];
|
||||||
|
fireEvent(this, "change", undefined, { bubbles: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _radiusUpdated() {
|
||||||
|
this._ignoreFitToMap = this.location;
|
||||||
|
this.radius = (this._locationMarker as Circle).getRadius();
|
||||||
|
fireEvent(this, "change", undefined, { bubbles: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateIcon() {
|
||||||
|
if (!this.icon) {
|
||||||
|
this._iconEl = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create icon
|
||||||
|
let iconHTML = "";
|
||||||
|
const el = document.createElement("ha-icon");
|
||||||
|
el.setAttribute("icon", this.icon);
|
||||||
|
iconHTML = el.outerHTML;
|
||||||
|
|
||||||
|
this._iconEl = this.Leaflet!.divIcon({
|
||||||
|
html: iconHTML,
|
||||||
|
iconSize: [24, 24],
|
||||||
|
className: "light leaflet-edit-move",
|
||||||
|
});
|
||||||
|
this._setIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setIcon() {
|
||||||
|
if (!this._locationMarker || !this._iconEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.radius) {
|
||||||
|
(this._locationMarker as Marker).setIcon(this._iconEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const moveMarker = this._locationMarker.editing._moveMarker;
|
||||||
|
moveMarker.setIcon(this._iconEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setupEdit() {
|
||||||
|
// @ts-ignore
|
||||||
|
this._locationMarker.editing.enable();
|
||||||
|
// @ts-ignore
|
||||||
|
const moveMarker = this._locationMarker.editing._moveMarker;
|
||||||
|
// @ts-ignore
|
||||||
|
const resizeMarker = this._locationMarker.editing._resizeMarkers[0];
|
||||||
|
this._setIcon();
|
||||||
|
moveMarker.addEventListener(
|
||||||
|
"dragend",
|
||||||
|
// @ts-ignore
|
||||||
|
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
|
||||||
|
);
|
||||||
|
resizeMarker.addEventListener(
|
||||||
|
"dragend",
|
||||||
|
// @ts-ignore
|
||||||
|
(ev: DragEndEvent) => this._radiusUpdated(ev)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateMarker(): Promise<void> {
|
||||||
|
if (!this.location) {
|
||||||
|
if (this._locationMarker) {
|
||||||
|
this._locationMarker.remove();
|
||||||
|
this._locationMarker = undefined;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._locationMarker) {
|
||||||
|
this._locationMarker.setLatLng(this.location);
|
||||||
|
if (this.radius) {
|
||||||
|
// @ts-ignore
|
||||||
|
this._locationMarker.editing.disable();
|
||||||
|
await nextRender();
|
||||||
|
this._setupEdit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.radius) {
|
||||||
|
this._locationMarker = this.Leaflet!.marker(this.location, {
|
||||||
|
draggable: true,
|
||||||
|
});
|
||||||
|
this._setIcon();
|
||||||
|
this._locationMarker.addEventListener(
|
||||||
|
"dragend",
|
||||||
|
// @ts-ignore
|
||||||
|
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
|
||||||
|
);
|
||||||
|
this._leafletMap!.addLayer(this._locationMarker);
|
||||||
|
} else {
|
||||||
|
this._locationMarker = this.Leaflet!.circle(this.location, {
|
||||||
|
color: this.radiusColor || defaultRadiusColor,
|
||||||
|
radius: this.radius,
|
||||||
|
});
|
||||||
|
this._leafletMap!.addLayer(this._locationMarker);
|
||||||
|
this._setupEdit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateRadius(): void {
|
||||||
|
if (!this._locationMarker || !this.radius) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(this._locationMarker as Circle).setRadius(this.radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateRadiusColor(): void {
|
||||||
|
if (!this._locationMarker || !this.radius) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(this._locationMarker as Circle).setStyle({ color: this.radiusColor });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
#map {
|
||||||
|
height: 100%;
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-edit-move {
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: move !important;
|
||||||
|
}
|
||||||
|
.leaflet-edit-resize {
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: nesw-resize !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-location-editor": LocationEditor;
|
||||||
|
}
|
||||||
|
}
|
@@ -3,8 +3,10 @@ import {
|
|||||||
DivIcon,
|
DivIcon,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
LatLng,
|
LatLng,
|
||||||
|
Map,
|
||||||
Marker,
|
Marker,
|
||||||
MarkerOptions,
|
MarkerOptions,
|
||||||
|
TileLayer,
|
||||||
} from "leaflet";
|
} from "leaflet";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@@ -14,13 +16,15 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
import {
|
||||||
import type { HomeAssistant } from "../../types";
|
LeafletModuleType,
|
||||||
import "./ha-map";
|
replaceTileLayer,
|
||||||
import type { HaMap } from "./ha-map";
|
setupLeafletMap,
|
||||||
|
} from "../../common/dom/setup-leaflet-map";
|
||||||
|
import { defaultRadiusColor } from "../../data/zone";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
@@ -47,40 +51,38 @@ export interface MarkerLocation {
|
|||||||
export class HaLocationsEditor extends LitElement {
|
export class HaLocationsEditor extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public locations?: MarkerLocation[];
|
@property() public locations?: MarkerLocation[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public autoFit = false;
|
public fitZoom = 16;
|
||||||
|
|
||||||
@property({ type: Number }) public zoom = 16;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public darkMode?: boolean;
|
|
||||||
|
|
||||||
@state() private _locationMarkers?: Record<string, Marker | Circle>;
|
|
||||||
|
|
||||||
@state() private _circles: Record<string, Circle> = {};
|
|
||||||
|
|
||||||
@query("ha-map", true) private map!: HaMap;
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
private Leaflet?: LeafletModuleType;
|
private Leaflet?: LeafletModuleType;
|
||||||
|
|
||||||
constructor() {
|
// eslint-disable-next-line
|
||||||
super();
|
private _leafletMap?: Map;
|
||||||
|
|
||||||
import("leaflet").then((module) => {
|
private _tileLayer?: TileLayer;
|
||||||
import("leaflet-draw").then(() => {
|
|
||||||
this.Leaflet = module.default as LeafletModuleType;
|
private _locationMarkers?: { [key: string]: Marker | Circle };
|
||||||
this._updateMarkers();
|
|
||||||
this.updateComplete.then(() => this.fitMap());
|
private _circles: Record<string, Circle> = {};
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public fitMap(): void {
|
public fitMap(): void {
|
||||||
this.map.fitMap();
|
if (
|
||||||
|
!this._leafletMap ||
|
||||||
|
!this._locationMarkers ||
|
||||||
|
!Object.keys(this._locationMarkers).length
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bounds = this.Leaflet!.latLngBounds(
|
||||||
|
Object.values(this._locationMarkers).map((item) => item.getLatLng())
|
||||||
|
);
|
||||||
|
this._leafletMap.fitBounds(bounds.pad(0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
public fitMarker(id: string): void {
|
public fitMarker(id: string): void {
|
||||||
if (!this.map.leafletMap || !this._locationMarkers) {
|
if (!this._leafletMap || !this._locationMarkers) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const marker = this._locationMarkers[id];
|
const marker = this._locationMarkers[id];
|
||||||
@@ -88,44 +90,29 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ("getBounds" in marker) {
|
if ("getBounds" in marker) {
|
||||||
this.map.leafletMap.fitBounds(marker.getBounds());
|
this._leafletMap.fitBounds(marker.getBounds());
|
||||||
(marker as Circle).bringToFront();
|
(marker as Circle).bringToFront();
|
||||||
} else {
|
} else {
|
||||||
const circle = this._circles[id];
|
const circle = this._circles[id];
|
||||||
if (circle) {
|
if (circle) {
|
||||||
this.map.leafletMap.fitBounds(circle.getBounds());
|
this._leafletMap.fitBounds(circle.getBounds());
|
||||||
} else {
|
} else {
|
||||||
this.map.leafletMap.setView(marker.getLatLng(), this.zoom);
|
this._leafletMap.setView(marker.getLatLng(), this.fitZoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`<ha-map
|
return html` <div id="map"></div> `;
|
||||||
.hass=${this.hass}
|
|
||||||
.layers=${this._getLayers(this._circles, this._locationMarkers)}
|
|
||||||
.zoom=${this.zoom}
|
|
||||||
.autoFit=${this.autoFit}
|
|
||||||
.darkMode=${this.darkMode}
|
|
||||||
></ha-map>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getLayers = memoizeOne(
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
(
|
super.firstUpdated(changedProps);
|
||||||
circles: Record<string, Circle>,
|
this._initMap();
|
||||||
markers?: Record<string, Marker | Circle>
|
|
||||||
): Array<Marker | Circle> => {
|
|
||||||
const layers: Array<Marker | Circle> = [];
|
|
||||||
Array.prototype.push.apply(layers, Object.values(circles));
|
|
||||||
if (markers) {
|
|
||||||
Array.prototype.push.apply(layers, Object.values(markers));
|
|
||||||
}
|
}
|
||||||
return layers;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues): void {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
super.willUpdate(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
// Still loading.
|
// Still loading.
|
||||||
if (!this.Leaflet) {
|
if (!this.Leaflet) {
|
||||||
@@ -135,6 +122,37 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
if (changedProps.has("locations")) {
|
if (changedProps.has("locations")) {
|
||||||
this._updateMarkers();
|
this._updateMarkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("hass")) {
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this._leafletMap || !this._tileLayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._tileLayer = replaceTileLayer(
|
||||||
|
this.Leaflet,
|
||||||
|
this._leafletMap,
|
||||||
|
this._tileLayer,
|
||||||
|
this.hass.themes.darkMode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _mapEl(): HTMLDivElement {
|
||||||
|
return this.shadowRoot!.querySelector("div")!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initMap(): Promise<void> {
|
||||||
|
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||||
|
this._mapEl,
|
||||||
|
this.hass.themes.darkMode,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
this._updateMarkers();
|
||||||
|
this.fitMap();
|
||||||
|
this._leafletMap.invalidateSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateLocation(ev: DragEndEvent) {
|
private _updateLocation(ev: DragEndEvent) {
|
||||||
@@ -171,18 +189,21 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _updateMarkers(): void {
|
private _updateMarkers(): void {
|
||||||
if (!this.locations || !this.locations.length) {
|
if (this._locationMarkers) {
|
||||||
this._circles = {};
|
Object.values(this._locationMarkers).forEach((marker) => {
|
||||||
|
marker.remove();
|
||||||
|
});
|
||||||
this._locationMarkers = undefined;
|
this._locationMarkers = undefined;
|
||||||
|
|
||||||
|
Object.values(this._circles).forEach((circle) => circle.remove());
|
||||||
|
this._circles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.locations || !this.locations.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const locationMarkers = {};
|
this._locationMarkers = {};
|
||||||
const circles = {};
|
|
||||||
|
|
||||||
const defaultZoneRadiusColor = getComputedStyle(this).getPropertyValue(
|
|
||||||
"--accent-color"
|
|
||||||
);
|
|
||||||
|
|
||||||
this.locations.forEach((location: MarkerLocation) => {
|
this.locations.forEach((location: MarkerLocation) => {
|
||||||
let icon: DivIcon | undefined;
|
let icon: DivIcon | undefined;
|
||||||
@@ -207,14 +228,14 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
const circle = this.Leaflet!.circle(
|
const circle = this.Leaflet!.circle(
|
||||||
[location.latitude, location.longitude],
|
[location.latitude, location.longitude],
|
||||||
{
|
{
|
||||||
color: location.radius_color || defaultZoneRadiusColor,
|
color: location.radius_color || defaultRadiusColor,
|
||||||
radius: location.radius,
|
radius: location.radius,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
circle.addTo(this._leafletMap!);
|
||||||
if (location.radius_editable || location.location_editable) {
|
if (location.radius_editable || location.location_editable) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
circle.editing.enable();
|
circle.editing.enable();
|
||||||
circle.addEventListener("add", () => {
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const moveMarker = circle.editing._moveMarker;
|
const moveMarker = circle.editing._moveMarker;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -243,10 +264,9 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
resizeMarker.remove();
|
resizeMarker.remove();
|
||||||
}
|
}
|
||||||
});
|
this._locationMarkers![location.id] = circle;
|
||||||
locationMarkers[location.id] = circle;
|
|
||||||
} else {
|
} else {
|
||||||
circles[location.id] = circle;
|
this._circles[location.id] = circle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -255,7 +275,6 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
) {
|
) {
|
||||||
const options: MarkerOptions = {
|
const options: MarkerOptions = {
|
||||||
title: location.name,
|
title: location.name,
|
||||||
draggable: location.location_editable,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (icon) {
|
if (icon) {
|
||||||
@@ -274,14 +293,13 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
"click",
|
"click",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(ev: MouseEvent) => this._markerClicked(ev)
|
(ev: MouseEvent) => this._markerClicked(ev)
|
||||||
);
|
)
|
||||||
|
.addTo(this._leafletMap!);
|
||||||
(marker as any).id = location.id;
|
(marker as any).id = location.id;
|
||||||
|
|
||||||
locationMarkers[location.id] = marker;
|
this._locationMarkers![location.id] = marker;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._circles = circles;
|
|
||||||
this._locationMarkers = locationMarkers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -290,9 +308,23 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
ha-map {
|
#map {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.leaflet-marker-draggable {
|
||||||
|
cursor: move !important;
|
||||||
|
}
|
||||||
|
.leaflet-edit-resize {
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: nesw-resize !important;
|
||||||
|
}
|
||||||
|
.named-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,13 @@
|
|||||||
|
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
|
||||||
import {
|
import {
|
||||||
Circle,
|
css,
|
||||||
CircleMarker,
|
CSSResultGroup,
|
||||||
LatLngTuple,
|
html,
|
||||||
Layer,
|
LitElement,
|
||||||
Map,
|
PropertyValues,
|
||||||
Marker,
|
TemplateResult,
|
||||||
Polyline,
|
} from "lit";
|
||||||
TileLayer,
|
import { customElement, property } from "lit/decorators";
|
||||||
} from "leaflet";
|
|
||||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import {
|
import {
|
||||||
LeafletModuleType,
|
LeafletModuleType,
|
||||||
replaceTileLayer,
|
replaceTileLayer,
|
||||||
@@ -17,324 +15,194 @@ import {
|
|||||||
} from "../../common/dom/setup-leaflet-map";
|
} from "../../common/dom/setup-leaflet-map";
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
import "./ha-entity-marker";
|
import { debounce } from "../../common/util/debounce";
|
||||||
|
import "../../panels/map/ha-entity-marker";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
|
||||||
|
|
||||||
const getEntityId = (entity: string | HaMapEntity): string =>
|
|
||||||
typeof entity === "string" ? entity : entity.entity_id;
|
|
||||||
|
|
||||||
export interface HaMapPaths {
|
|
||||||
points: LatLngTuple[];
|
|
||||||
color?: string;
|
|
||||||
gradualOpacity?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HaMapEntity {
|
|
||||||
entity_id: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-map")
|
@customElement("ha-map")
|
||||||
export class HaMap extends ReactiveElement {
|
class HaMap extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public entities?: string[] | HaMapEntity[];
|
@property() public entities?: string[];
|
||||||
|
|
||||||
@property({ attribute: false }) public paths?: HaMapPaths[];
|
@property() public darkMode?: boolean;
|
||||||
|
|
||||||
@property({ attribute: false }) public layers?: Layer[];
|
@property() public zoom?: number;
|
||||||
|
|
||||||
@property({ type: Boolean }) public autoFit = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public fitZones?: boolean;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public darkMode?: boolean;
|
|
||||||
|
|
||||||
@property({ type: Number }) public zoom = 14;
|
|
||||||
|
|
||||||
@state() private _loaded = false;
|
|
||||||
|
|
||||||
public leafletMap?: Map;
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
private Leaflet?: LeafletModuleType;
|
private Leaflet?: LeafletModuleType;
|
||||||
|
|
||||||
|
private _leafletMap?: Map;
|
||||||
|
|
||||||
private _tileLayer?: TileLayer;
|
private _tileLayer?: TileLayer;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
private _resizeObserver?: ResizeObserver;
|
private _resizeObserver?: ResizeObserver;
|
||||||
|
|
||||||
|
private _debouncedResizeListener = debounce(
|
||||||
|
() => {
|
||||||
|
if (!this._leafletMap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._leafletMap.invalidateSize();
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
private _mapItems: Array<Marker | Circle> = [];
|
private _mapItems: Array<Marker | Circle> = [];
|
||||||
|
|
||||||
private _mapZones: Array<Marker | Circle> = [];
|
private _mapZones: Array<Marker | Circle> = [];
|
||||||
|
|
||||||
private _mapPaths: Array<Polyline | CircleMarker> = [];
|
private _connected = false;
|
||||||
|
|
||||||
public connectedCallback(): void {
|
public connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._loadMap();
|
this._connected = true;
|
||||||
|
if (this.hasUpdated) {
|
||||||
|
this.loadMap();
|
||||||
this._attachObserver();
|
this._attachObserver();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public disconnectedCallback(): void {
|
public disconnectedCallback(): void {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
if (this.leafletMap) {
|
this._connected = false;
|
||||||
this.leafletMap.remove();
|
|
||||||
this.leafletMap = undefined;
|
if (this._leafletMap) {
|
||||||
|
this._leafletMap.remove();
|
||||||
|
this._leafletMap = undefined;
|
||||||
this.Leaflet = undefined;
|
this.Leaflet = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._loaded = false;
|
|
||||||
|
|
||||||
if (this._resizeObserver) {
|
if (this._resizeObserver) {
|
||||||
this._resizeObserver.unobserve(this);
|
this._resizeObserver.unobserve(this._mapEl);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener("resize", this._debouncedResizeListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected update(changedProps: PropertyValues) {
|
protected render(): TemplateResult {
|
||||||
super.update(changedProps);
|
if (!this.entities) {
|
||||||
|
return html``;
|
||||||
if (!this._loaded) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return html` <div id="map"></div> `;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this.loadMap();
|
||||||
|
|
||||||
|
if (this._connected) {
|
||||||
|
this._attachObserver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps) {
|
||||||
|
if (!changedProps.has("hass") || changedProps.size > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
|
||||||
if (changedProps.has("_loaded") || changedProps.has("entities")) {
|
if (!oldHass || !this.entities) {
|
||||||
this._drawEntities();
|
return true;
|
||||||
} else if (this._loaded && oldHass && this.entities) {
|
}
|
||||||
|
|
||||||
// Check if any state has changed
|
// Check if any state has changed
|
||||||
for (const entity of this.entities) {
|
for (const entity of this.entities) {
|
||||||
if (
|
if (oldHass.states[entity] !== this.hass!.states[entity]) {
|
||||||
oldHass.states[getEntityId(entity)] !==
|
return true;
|
||||||
this.hass!.states[getEntityId(entity)]
|
}
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has("hass")) {
|
||||||
this._drawEntities();
|
this._drawEntities();
|
||||||
break;
|
this._fitMap();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedProps.has("_loaded") || changedProps.has("paths")) {
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
this._drawPaths();
|
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
if (changedProps.has("_loaded") || changedProps.has("layers")) {
|
if (!this.Leaflet || !this._leafletMap || !this._tileLayer) {
|
||||||
this._drawLayers(changedProps.get("layers") as Layer[] | undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
changedProps.has("_loaded") ||
|
|
||||||
((changedProps.has("entities") || changedProps.has("layers")) &&
|
|
||||||
this.autoFit)
|
|
||||||
) {
|
|
||||||
this.fitMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedProps.has("zoom")) {
|
|
||||||
this.leafletMap!.setZoom(this.zoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!changedProps.has("darkMode") &&
|
|
||||||
(!changedProps.has("hass") ||
|
|
||||||
(oldHass && oldHass.themes.darkMode === this.hass.themes.darkMode))
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
|
|
||||||
this._tileLayer = replaceTileLayer(
|
this._tileLayer = replaceTileLayer(
|
||||||
this.Leaflet!,
|
this.Leaflet,
|
||||||
this.leafletMap!,
|
this._leafletMap,
|
||||||
this._tileLayer!,
|
this._tileLayer,
|
||||||
darkMode
|
this.hass.themes.darkMode
|
||||||
);
|
);
|
||||||
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _loadMap(): Promise<void> {
|
private get _mapEl(): HTMLDivElement {
|
||||||
let map = this.shadowRoot!.getElementById("map");
|
return this.shadowRoot!.getElementById("map") as HTMLDivElement;
|
||||||
if (!map) {
|
|
||||||
map = document.createElement("div");
|
|
||||||
map.id = "map";
|
|
||||||
this.shadowRoot!.append(map);
|
|
||||||
}
|
|
||||||
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
|
|
||||||
[this.leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
|
||||||
map,
|
|
||||||
darkMode
|
|
||||||
);
|
|
||||||
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
|
|
||||||
this._loaded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public fitMap(): void {
|
private async loadMap(): Promise<void> {
|
||||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||||
|
this._mapEl,
|
||||||
|
this.darkMode ?? this.hass.themes.darkMode
|
||||||
|
);
|
||||||
|
this._drawEntities();
|
||||||
|
this._leafletMap.invalidateSize();
|
||||||
|
this._fitMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fitMap(): void {
|
||||||
|
if (!this._leafletMap || !this.Leaflet || !this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this._mapItems.length === 0) {
|
||||||
if (!this._mapItems.length && !this.layers?.length) {
|
this._leafletMap.setView(
|
||||||
this.leafletMap.setView(
|
|
||||||
new this.Leaflet.LatLng(
|
new this.Leaflet.LatLng(
|
||||||
this.hass.config.latitude,
|
this.hass.config.latitude,
|
||||||
this.hass.config.longitude
|
this.hass.config.longitude
|
||||||
),
|
),
|
||||||
this.zoom
|
this.zoom || 14
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bounds = this.Leaflet.latLngBounds(
|
const bounds = this.Leaflet.latLngBounds(
|
||||||
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
|
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
|
||||||
);
|
);
|
||||||
|
this._leafletMap.fitBounds(bounds.pad(0.5));
|
||||||
|
|
||||||
if (this.fitZones) {
|
if (this.zoom && this._leafletMap.getZoom() > this.zoom) {
|
||||||
this._mapZones?.forEach((zone) => {
|
this._leafletMap.setZoom(this.zoom);
|
||||||
bounds.extend(
|
|
||||||
"getBounds" in zone ? zone.getBounds() : zone.getLatLng()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.layers?.forEach((layer: any) => {
|
|
||||||
bounds.extend(
|
|
||||||
"getBounds" in layer ? layer.getBounds() : layer.getLatLng()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.layers) {
|
|
||||||
bounds = bounds.pad(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.leafletMap.fitBounds(bounds, { maxZoom: this.zoom });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _drawLayers(prevLayers: Layer[] | undefined): void {
|
|
||||||
if (prevLayers) {
|
|
||||||
prevLayers.forEach((layer) => layer.remove());
|
|
||||||
}
|
|
||||||
if (!this.layers) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const map = this.leafletMap!;
|
|
||||||
this.layers.forEach((layer) => {
|
|
||||||
map.addLayer(layer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _drawPaths(): void {
|
|
||||||
const hass = this.hass;
|
|
||||||
const map = this.leafletMap;
|
|
||||||
const Leaflet = this.Leaflet;
|
|
||||||
|
|
||||||
if (!hass || !map || !Leaflet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this._mapPaths.length) {
|
|
||||||
this._mapPaths.forEach((marker) => marker.remove());
|
|
||||||
this._mapPaths = [];
|
|
||||||
}
|
|
||||||
if (!this.paths) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const darkPrimaryColor = getComputedStyle(this).getPropertyValue(
|
|
||||||
"--dark-primary-color"
|
|
||||||
);
|
|
||||||
|
|
||||||
this.paths.forEach((path) => {
|
|
||||||
let opacityStep: number;
|
|
||||||
let baseOpacity: number;
|
|
||||||
if (path.gradualOpacity) {
|
|
||||||
opacityStep = path.gradualOpacity / (path.points.length - 2);
|
|
||||||
baseOpacity = 1 - path.gradualOpacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (
|
|
||||||
let pointIndex = 0;
|
|
||||||
pointIndex < path.points.length - 1;
|
|
||||||
pointIndex++
|
|
||||||
) {
|
|
||||||
const opacity = path.gradualOpacity
|
|
||||||
? baseOpacity! + pointIndex * opacityStep!
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// DRAW point
|
|
||||||
this._mapPaths.push(
|
|
||||||
Leaflet!.circleMarker(path.points[pointIndex], {
|
|
||||||
radius: 3,
|
|
||||||
color: path.color || darkPrimaryColor,
|
|
||||||
opacity,
|
|
||||||
fillOpacity: opacity,
|
|
||||||
interactive: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// DRAW line between this and next point
|
|
||||||
this._mapPaths.push(
|
|
||||||
Leaflet!.polyline(
|
|
||||||
[path.points[pointIndex], path.points[pointIndex + 1]],
|
|
||||||
{
|
|
||||||
color: path.color || darkPrimaryColor,
|
|
||||||
opacity,
|
|
||||||
interactive: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const pointIndex = path.points.length - 1;
|
|
||||||
if (pointIndex >= 0) {
|
|
||||||
const opacity = path.gradualOpacity
|
|
||||||
? baseOpacity! + pointIndex * opacityStep!
|
|
||||||
: undefined;
|
|
||||||
// DRAW end path point
|
|
||||||
this._mapPaths.push(
|
|
||||||
Leaflet!.circleMarker(path.points[pointIndex], {
|
|
||||||
radius: 3,
|
|
||||||
color: path.color || darkPrimaryColor,
|
|
||||||
opacity,
|
|
||||||
fillOpacity: opacity,
|
|
||||||
interactive: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this._mapPaths.forEach((marker) => map.addLayer(marker));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _drawEntities(): void {
|
private _drawEntities(): void {
|
||||||
const hass = this.hass;
|
const hass = this.hass;
|
||||||
const map = this.leafletMap;
|
const map = this._leafletMap;
|
||||||
const Leaflet = this.Leaflet;
|
const Leaflet = this.Leaflet;
|
||||||
|
|
||||||
if (!hass || !map || !Leaflet) {
|
if (!hass || !map || !Leaflet) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._mapItems.length) {
|
if (this._mapItems) {
|
||||||
this._mapItems.forEach((marker) => marker.remove());
|
this._mapItems.forEach((marker) => marker.remove());
|
||||||
this._mapItems = [];
|
|
||||||
}
|
}
|
||||||
|
const mapItems: Layer[] = (this._mapItems = []);
|
||||||
|
|
||||||
if (this._mapZones.length) {
|
if (this._mapZones) {
|
||||||
this._mapZones.forEach((marker) => marker.remove());
|
this._mapZones.forEach((marker) => marker.remove());
|
||||||
this._mapZones = [];
|
|
||||||
}
|
}
|
||||||
|
const mapZones: Layer[] = (this._mapZones = []);
|
||||||
|
|
||||||
if (!this.entities) {
|
const allEntities = this.entities!.concat();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedStyles = getComputedStyle(this);
|
for (const entity of allEntities) {
|
||||||
const zoneColor = computedStyles.getPropertyValue("--accent-color");
|
const entityId = entity;
|
||||||
const darkPrimaryColor = computedStyles.getPropertyValue(
|
const stateObj = hass.states[entityId];
|
||||||
"--dark-primary-color"
|
|
||||||
);
|
|
||||||
|
|
||||||
const className =
|
|
||||||
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light";
|
|
||||||
|
|
||||||
for (const entity of this.entities) {
|
|
||||||
const stateObj = hass.states[getEntityId(entity)];
|
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -372,12 +240,13 @@ export class HaMap extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create marker with the icon
|
// create marker with the icon
|
||||||
this._mapZones.push(
|
mapZones.push(
|
||||||
Leaflet.marker([latitude, longitude], {
|
Leaflet.marker([latitude, longitude], {
|
||||||
icon: Leaflet.divIcon({
|
icon: Leaflet.divIcon({
|
||||||
html: iconHTML,
|
html: iconHTML,
|
||||||
iconSize: [24, 24],
|
iconSize: [24, 24],
|
||||||
className,
|
className:
|
||||||
|
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
|
||||||
}),
|
}),
|
||||||
interactive: false,
|
interactive: false,
|
||||||
title,
|
title,
|
||||||
@@ -385,10 +254,10 @@ export class HaMap extends ReactiveElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// create circle around it
|
// create circle around it
|
||||||
this._mapZones.push(
|
mapZones.push(
|
||||||
Leaflet.circle([latitude, longitude], {
|
Leaflet.circle([latitude, longitude], {
|
||||||
interactive: false,
|
interactive: false,
|
||||||
color: zoneColor,
|
color: "#FF9800",
|
||||||
radius,
|
radius,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -404,20 +273,17 @@ export class HaMap extends ReactiveElement {
|
|||||||
.join("")
|
.join("")
|
||||||
.substr(0, 3);
|
.substr(0, 3);
|
||||||
|
|
||||||
// create marker with the icon
|
// create market with the icon
|
||||||
this._mapItems.push(
|
mapItems.push(
|
||||||
Leaflet.marker([latitude, longitude], {
|
Leaflet.marker([latitude, longitude], {
|
||||||
icon: Leaflet.divIcon({
|
icon: Leaflet.divIcon({
|
||||||
|
// Leaflet clones this element before adding it to the map. This messes up
|
||||||
|
// our Polymer object and we can't pass data through. Thus we hack like this.
|
||||||
html: `
|
html: `
|
||||||
<ha-entity-marker
|
<ha-entity-marker
|
||||||
entity-id="${getEntityId(entity)}"
|
entity-id="${entityId}"
|
||||||
entity-name="${entityName}"
|
entity-name="${entityName}"
|
||||||
entity-picture="${entityPicture || ""}"
|
entity-picture="${entityPicture || ""}"
|
||||||
${
|
|
||||||
typeof entity !== "string"
|
|
||||||
? `entity-color="${entity.color}"`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
></ha-entity-marker>
|
></ha-entity-marker>
|
||||||
`,
|
`,
|
||||||
iconSize: [48, 48],
|
iconSize: [48, 48],
|
||||||
@@ -429,10 +295,10 @@ export class HaMap extends ReactiveElement {
|
|||||||
|
|
||||||
// create circle around if entity has accuracy
|
// create circle around if entity has accuracy
|
||||||
if (gpsAccuracy) {
|
if (gpsAccuracy) {
|
||||||
this._mapItems.push(
|
mapItems.push(
|
||||||
Leaflet.circle([latitude, longitude], {
|
Leaflet.circle([latitude, longitude], {
|
||||||
interactive: false,
|
interactive: false,
|
||||||
color: darkPrimaryColor,
|
color: "#0288D1",
|
||||||
radius: gpsAccuracy,
|
radius: gpsAccuracy,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -443,14 +309,20 @@ export class HaMap extends ReactiveElement {
|
|||||||
this._mapZones.forEach((marker) => map.addLayer(marker));
|
this._mapZones.forEach((marker) => map.addLayer(marker));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _attachObserver(): Promise<void> {
|
private _attachObserver(): void {
|
||||||
if (!this._resizeObserver) {
|
// Observe changes to map size and invalidate to prevent broken rendering
|
||||||
await installResizeObserver();
|
// Uses ResizeObserver in Chrome, otherwise window resize event
|
||||||
this._resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.leafletMap?.invalidateSize({ debounceMoveend: true });
|
// @ts-ignore
|
||||||
});
|
if (typeof ResizeObserver === "function") {
|
||||||
|
// @ts-ignore
|
||||||
|
this._resizeObserver = new ResizeObserver(() =>
|
||||||
|
this._debouncedResizeListener()
|
||||||
|
);
|
||||||
|
this._resizeObserver.observe(this._mapEl);
|
||||||
|
} else {
|
||||||
|
window.addEventListener("resize", this._debouncedResizeListener);
|
||||||
}
|
}
|
||||||
this._resizeObserver.observe(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -465,25 +337,13 @@ export class HaMap extends ReactiveElement {
|
|||||||
#map.dark {
|
#map.dark {
|
||||||
background: #090909;
|
background: #090909;
|
||||||
}
|
}
|
||||||
.light {
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
.dark {
|
.dark {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
.leaflet-marker-draggable {
|
|
||||||
cursor: move !important;
|
.light {
|
||||||
}
|
color: #000000;
|
||||||
.leaflet-edit-resize {
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: nesw-resize !important;
|
|
||||||
}
|
|
||||||
.named-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -45,6 +45,7 @@ import "../ha-button-menu";
|
|||||||
import "../ha-card";
|
import "../ha-card";
|
||||||
import "../ha-circular-progress";
|
import "../ha-circular-progress";
|
||||||
import "../ha-fab";
|
import "../ha-fab";
|
||||||
|
import "../ha-paper-dropdown-menu";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user