mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-17 13:19:26 +00:00
Compare commits
1 Commits
limit-quic
...
Fix-Locati
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c363e1a2db |
@@ -3,10 +3,10 @@ const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||
const log = require("fancy-log");
|
||||
const WebpackBar = require("webpackbar");
|
||||
const paths = require("./paths.js");
|
||||
const bundle = require("./bundle.js");
|
||||
const log = require("fancy-log");
|
||||
const WebpackBar = require("webpackbar");
|
||||
|
||||
class LogStartCompilePlugin {
|
||||
ignoredFirst = false;
|
||||
@@ -138,8 +138,6 @@ const createWebpackConfig = ({
|
||||
"lit/directives/cache$": "lit/directives/cache.js",
|
||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
|
@@ -8,7 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import type { ConditionWithShorthand } from "../../../../src/data/automation";
|
||||
import type { Condition } from "../../../../src/data/automation";
|
||||
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
|
||||
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
|
||||
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
|
||||
@@ -20,7 +20,7 @@ import { HaTimeCondition } from "../../../../src/panels/config/automation/condit
|
||||
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
|
||||
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
|
||||
|
||||
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
|
||||
const SCHEMAS: { name: string; conditions: Condition[] }[] = [
|
||||
{
|
||||
name: "State",
|
||||
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
|
||||
@@ -69,14 +69,6 @@ const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
|
||||
name: "Trigger",
|
||||
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Shorthand",
|
||||
conditions: [
|
||||
{ and: HaLogicalCondition.defaultConfig.conditions },
|
||||
{ or: HaLogicalCondition.defaultConfig.conditions },
|
||||
{ not: HaLogicalCondition.defaultConfig.conditions },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-condition")
|
||||
|
@@ -2,7 +2,6 @@ import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-ansi-to-html";
|
||||
import "../../../../src/components/ha-card";
|
||||
import {
|
||||
fetchHassioAddonLogs,
|
||||
@@ -12,6 +11,7 @@ import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-ansi-to-html";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-addon-logs")
|
||||
@@ -40,9 +40,9 @@ class HassioAddonLogs extends LitElement {
|
||||
: ""}
|
||||
<div class="card-content">
|
||||
${this._content
|
||||
? html`<ha-ansi-to-html
|
||||
? html`<hassio-ansi-to-html
|
||||
.content=${this._content}
|
||||
></ha-ansi-to-html>`
|
||||
></hassio-ansi-to-html>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import "@material/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -166,15 +166,7 @@ export class HassioBackups extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? [
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
]
|
||||
: supervisorTabs(this.hass)}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.searchLabel=${this.supervisor.localize("search")}
|
||||
@@ -190,9 +182,7 @@ export class HassioBackups extends LitElement {
|
||||
selectable
|
||||
hasFab
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? "/config/system"
|
||||
: "/config"}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu
|
||||
|
@@ -10,8 +10,8 @@ interface State {
|
||||
backgroundColor: null | string;
|
||||
}
|
||||
|
||||
@customElement("ha-ansi-to-html")
|
||||
class HaAnsiToHtml extends LitElement {
|
||||
@customElement("hassio-ansi-to-html")
|
||||
class HassioAnsiToHtml extends LitElement {
|
||||
@property() public content!: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
@@ -241,6 +241,6 @@ class HaAnsiToHtml extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-ansi-to-html": HaAnsiToHtml;
|
||||
"hassio-ansi-to-html": HassioAnsiToHtml;
|
||||
}
|
||||
}
|
@@ -10,7 +10,6 @@ import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addons";
|
||||
import "./hassio-update";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
|
||||
@customElement("hassio-dashboard")
|
||||
class HassioDashboard extends LitElement {
|
||||
@@ -23,31 +22,6 @@ class HassioDashboard extends LitElement {
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.header=${this.supervisor.localize("panel.addons")}
|
||||
>
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-addons>
|
||||
<a href="/hassio/store">
|
||||
<ha-fab
|
||||
.label=${this.supervisor.localize("panel.store")}
|
||||
extended
|
||||
class="non-tabs"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiStorePlus}
|
||||
></ha-svg-icon> </ha-fab
|
||||
></a>
|
||||
</hass-subpage>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
@@ -100,12 +74,6 @@ class HassioDashboard extends LitElement {
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-fab.non-tabs {
|
||||
position: fixed;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@ import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import { mainWindow } from "../../src/common/dom/get_main_window";
|
||||
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 { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
@@ -73,14 +73,6 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
});
|
||||
});
|
||||
|
||||
// Forward keydown events to the main window for quickbar access
|
||||
document.body.addEventListener("keydown", (ev) => {
|
||||
// @ts-ignore
|
||||
fireEvent(mainWindow, "hass-quick-bar-trigger", ev, {
|
||||
bubbles: false,
|
||||
});
|
||||
});
|
||||
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
}
|
||||
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
} from "../../src/panels/my/ha-panel-my";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
|
||||
export const REDIRECTS: Redirects = {
|
||||
const REDIRECTS: Redirects = {
|
||||
supervisor: {
|
||||
redirect: "/hassio/dashboard",
|
||||
},
|
||||
|
@@ -8,27 +8,24 @@ import { atLeastVersion } from "../../src/common/config/version";
|
||||
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
|
||||
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
|
||||
atLeastVersion(hass.config.version, 2022, 5)
|
||||
? []
|
||||
: [
|
||||
{
|
||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
iconPath: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? mdiPuzzle
|
||||
: mdiViewDashboard,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.system",
|
||||
path: `/hassio/system`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
];
|
||||
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [
|
||||
{
|
||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
iconPath: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? mdiPuzzle
|
||||
: mdiViewDashboard,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.system",
|
||||
path: `/hassio/system`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
];
|
||||
|
@@ -23,10 +23,6 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
UNHEALTHY_REASON_URL,
|
||||
UNSUPPORTED_REASON_URL,
|
||||
} from "../../../src/panels/config/system-health/ha-config-system-health";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
@@ -34,6 +30,11 @@ import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import "../components/supervisor-metric";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
const UNSUPPORTED_REASON_URL = {};
|
||||
const UNHEALTHY_REASON_URL = {
|
||||
privileged: "/more-info/unsupported/privileged",
|
||||
};
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "../../../src/components/ha-ansi-to-html";
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -12,6 +11,7 @@ import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-ansi-to-html";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
interface LogProvider {
|
||||
@@ -89,8 +89,8 @@ class HassioSupervisorLog extends LitElement {
|
||||
|
||||
<div class="card-content" id="content">
|
||||
${this._content
|
||||
? html`<ha-ansi-to-html .content=${this._content}>
|
||||
</ha-ansi-to-html>`
|
||||
? html`<hassio-ansi-to-html .content=${this._content}>
|
||||
</hassio-ansi-to-html>`
|
||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
@@ -106,7 +106,6 @@
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"fuse.js": "^6.0.0",
|
||||
"fuzzysort": "^1.2.1",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.1.5",
|
||||
"home-assistant-js-websocket": "^7.0.3",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220427.0
|
||||
version = 20220420.0
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
@@ -1,16 +0,0 @@
|
||||
import secondsToDuration from "./seconds_to_duration";
|
||||
|
||||
const DAY_IN_SECONDS = 86400;
|
||||
const HOUR_IN_SECONDS = 3600;
|
||||
const MINUTE_IN_SECONDS = 60;
|
||||
|
||||
export const UNIT_TO_SECOND_CONVERT = {
|
||||
s: 1,
|
||||
min: MINUTE_IN_SECONDS,
|
||||
h: HOUR_IN_SECONDS,
|
||||
d: DAY_IN_SECONDS,
|
||||
};
|
||||
|
||||
export const formatDuration = (duration: string, units: string): string =>
|
||||
secondsToDuration(parseFloat(duration) * UNIT_TO_SECOND_CONVERT[units]) ||
|
||||
"0";
|
@@ -13,7 +13,6 @@ import { formatNumber, isNumericState } from "../number/format_number";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -29,21 +28,6 @@ export const computeStateDisplay = (
|
||||
|
||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||
if (isNumericState(stateObj)) {
|
||||
// state is duration
|
||||
if (
|
||||
stateObj.attributes.device_class === "duration" &&
|
||||
stateObj.attributes.unit_of_measurement &&
|
||||
UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
|
||||
) {
|
||||
try {
|
||||
return formatDuration(
|
||||
compareState,
|
||||
stateObj.attributes.unit_of_measurement
|
||||
);
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
if (stateObj.attributes.device_class === "monetary") {
|
||||
try {
|
||||
return formatNumber(compareState, locale, {
|
||||
|
244
src/common/string/filter/char-code.ts
Normal file
244
src/common/string/filter/char-code.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
|
||||
|
||||
/**
|
||||
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
|
||||
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
|
||||
*/
|
||||
export enum CharCode {
|
||||
Null = 0,
|
||||
/**
|
||||
* The `\b` character.
|
||||
*/
|
||||
Backspace = 8,
|
||||
/**
|
||||
* The `\t` character.
|
||||
*/
|
||||
Tab = 9,
|
||||
/**
|
||||
* The `\n` character.
|
||||
*/
|
||||
LineFeed = 10,
|
||||
/**
|
||||
* The `\r` character.
|
||||
*/
|
||||
CarriageReturn = 13,
|
||||
Space = 32,
|
||||
/**
|
||||
* The `!` character.
|
||||
*/
|
||||
ExclamationMark = 33,
|
||||
/**
|
||||
* The `"` character.
|
||||
*/
|
||||
DoubleQuote = 34,
|
||||
/**
|
||||
* The `#` character.
|
||||
*/
|
||||
Hash = 35,
|
||||
/**
|
||||
* The `$` character.
|
||||
*/
|
||||
DollarSign = 36,
|
||||
/**
|
||||
* The `%` character.
|
||||
*/
|
||||
PercentSign = 37,
|
||||
/**
|
||||
* The `&` character.
|
||||
*/
|
||||
Ampersand = 38,
|
||||
/**
|
||||
* The `'` character.
|
||||
*/
|
||||
SingleQuote = 39,
|
||||
/**
|
||||
* The `(` character.
|
||||
*/
|
||||
OpenParen = 40,
|
||||
/**
|
||||
* The `)` character.
|
||||
*/
|
||||
CloseParen = 41,
|
||||
/**
|
||||
* The `*` character.
|
||||
*/
|
||||
Asterisk = 42,
|
||||
/**
|
||||
* The `+` character.
|
||||
*/
|
||||
Plus = 43,
|
||||
/**
|
||||
* The `,` character.
|
||||
*/
|
||||
Comma = 44,
|
||||
/**
|
||||
* The `-` character.
|
||||
*/
|
||||
Dash = 45,
|
||||
/**
|
||||
* The `.` character.
|
||||
*/
|
||||
Period = 46,
|
||||
/**
|
||||
* The `/` character.
|
||||
*/
|
||||
Slash = 47,
|
||||
|
||||
Digit0 = 48,
|
||||
Digit1 = 49,
|
||||
Digit2 = 50,
|
||||
Digit3 = 51,
|
||||
Digit4 = 52,
|
||||
Digit5 = 53,
|
||||
Digit6 = 54,
|
||||
Digit7 = 55,
|
||||
Digit8 = 56,
|
||||
Digit9 = 57,
|
||||
|
||||
/**
|
||||
* The `:` character.
|
||||
*/
|
||||
Colon = 58,
|
||||
/**
|
||||
* The `;` character.
|
||||
*/
|
||||
Semicolon = 59,
|
||||
/**
|
||||
* The `<` character.
|
||||
*/
|
||||
LessThan = 60,
|
||||
/**
|
||||
* The `=` character.
|
||||
*/
|
||||
Equals = 61,
|
||||
/**
|
||||
* The `>` character.
|
||||
*/
|
||||
GreaterThan = 62,
|
||||
/**
|
||||
* The `?` character.
|
||||
*/
|
||||
QuestionMark = 63,
|
||||
/**
|
||||
* The `@` character.
|
||||
*/
|
||||
AtSign = 64,
|
||||
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
|
||||
/**
|
||||
* The `[` character.
|
||||
*/
|
||||
OpenSquareBracket = 91,
|
||||
/**
|
||||
* The `\` character.
|
||||
*/
|
||||
Backslash = 92,
|
||||
/**
|
||||
* The `]` character.
|
||||
*/
|
||||
CloseSquareBracket = 93,
|
||||
/**
|
||||
* The `^` character.
|
||||
*/
|
||||
Caret = 94,
|
||||
/**
|
||||
* The `_` character.
|
||||
*/
|
||||
Underline = 95,
|
||||
/**
|
||||
* The ``(`)`` character.
|
||||
*/
|
||||
BackTick = 96,
|
||||
|
||||
a = 97,
|
||||
b = 98,
|
||||
c = 99,
|
||||
d = 100,
|
||||
e = 101,
|
||||
f = 102,
|
||||
g = 103,
|
||||
h = 104,
|
||||
i = 105,
|
||||
j = 106,
|
||||
k = 107,
|
||||
l = 108,
|
||||
m = 109,
|
||||
n = 110,
|
||||
o = 111,
|
||||
p = 112,
|
||||
q = 113,
|
||||
r = 114,
|
||||
s = 115,
|
||||
t = 116,
|
||||
u = 117,
|
||||
v = 118,
|
||||
w = 119,
|
||||
x = 120,
|
||||
y = 121,
|
||||
z = 122,
|
||||
|
||||
/**
|
||||
* The `{` character.
|
||||
*/
|
||||
OpenCurlyBrace = 123,
|
||||
/**
|
||||
* The `|` character.
|
||||
*/
|
||||
Pipe = 124,
|
||||
/**
|
||||
* The `}` character.
|
||||
*/
|
||||
CloseCurlyBrace = 125,
|
||||
/**
|
||||
* The `~` character.
|
||||
*/
|
||||
Tilde = 126,
|
||||
}
|
551
src/common/string/filter/filter.ts
Normal file
551
src/common/string/filter/filter.ts
Normal file
@@ -0,0 +1,551 @@
|
||||
/* eslint-disable no-console */
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { CharCode } from "./char-code";
|
||||
|
||||
const _debug = false;
|
||||
|
||||
export interface Match {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
const _maxLen = 128;
|
||||
|
||||
function initTable() {
|
||||
const table: number[][] = [];
|
||||
const row: number[] = [];
|
||||
for (let i = 0; i <= _maxLen; i++) {
|
||||
row[i] = 0;
|
||||
}
|
||||
for (let i = 0; i <= _maxLen; i++) {
|
||||
table.push(row.slice(0));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
function isSeparatorAtPos(value: string, index: number): boolean {
|
||||
if (index < 0 || index >= value.length) {
|
||||
return false;
|
||||
}
|
||||
const code = value.codePointAt(index);
|
||||
switch (code) {
|
||||
case CharCode.Underline:
|
||||
case CharCode.Dash:
|
||||
case CharCode.Period:
|
||||
case CharCode.Space:
|
||||
case CharCode.Slash:
|
||||
case CharCode.Backslash:
|
||||
case CharCode.SingleQuote:
|
||||
case CharCode.DoubleQuote:
|
||||
case CharCode.Colon:
|
||||
case CharCode.DollarSign:
|
||||
case CharCode.LessThan:
|
||||
case CharCode.OpenParen:
|
||||
case CharCode.OpenSquareBracket:
|
||||
return true;
|
||||
case undefined:
|
||||
return false;
|
||||
default:
|
||||
if (isEmojiImprecise(code)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isWhitespaceAtPos(value: string, index: number): boolean {
|
||||
if (index < 0 || index >= value.length) {
|
||||
return false;
|
||||
}
|
||||
const code = value.charCodeAt(index);
|
||||
switch (code) {
|
||||
case CharCode.Space:
|
||||
case CharCode.Tab:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
|
||||
return word[pos] !== wordLow[pos];
|
||||
}
|
||||
|
||||
export function isPatternInWord(
|
||||
patternLow: string,
|
||||
patternPos: number,
|
||||
patternLen: number,
|
||||
wordLow: string,
|
||||
wordPos: number,
|
||||
wordLen: number,
|
||||
fillMinWordPosArr = false
|
||||
): boolean {
|
||||
while (patternPos < patternLen && wordPos < wordLen) {
|
||||
if (patternLow[patternPos] === wordLow[wordPos]) {
|
||||
if (fillMinWordPosArr) {
|
||||
// Remember the min word position for each pattern position
|
||||
_minWordMatchPos[patternPos] = wordPos;
|
||||
}
|
||||
patternPos += 1;
|
||||
}
|
||||
wordPos += 1;
|
||||
}
|
||||
return patternPos === patternLen; // pattern must be exhausted
|
||||
}
|
||||
|
||||
enum Arrow {
|
||||
Diag = 1,
|
||||
Left = 2,
|
||||
LeftLeft = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* An array representing a fuzzy match.
|
||||
*
|
||||
* 0. the score
|
||||
* 1. the offset at which matching started
|
||||
* 2. `<match_pos_N>`
|
||||
* 3. `<match_pos_1>`
|
||||
* 4. `<match_pos_0>` etc
|
||||
*/
|
||||
// export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
|
||||
export type FuzzyScore = Array<number>;
|
||||
|
||||
export function fuzzyScore(
|
||||
pattern: string,
|
||||
patternLow: string,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordLow: string,
|
||||
wordStart: number,
|
||||
firstMatchCanBeWeak: boolean
|
||||
): FuzzyScore | undefined {
|
||||
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
|
||||
const wordLen = word.length > _maxLen ? _maxLen : word.length;
|
||||
|
||||
if (
|
||||
patternStart >= patternLen ||
|
||||
wordStart >= wordLen ||
|
||||
patternLen - patternStart > wordLen - wordStart
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Run a simple check if the characters of pattern occur
|
||||
// (in order) at all in word. If that isn't the case we
|
||||
// stop because no match will be possible
|
||||
if (
|
||||
!isPatternInWord(
|
||||
patternLow,
|
||||
patternStart,
|
||||
patternLen,
|
||||
wordLow,
|
||||
wordStart,
|
||||
wordLen,
|
||||
true
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the max matching word position for each pattern position
|
||||
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
|
||||
_fillInMaxWordMatchPos(
|
||||
patternLen,
|
||||
wordLen,
|
||||
patternStart,
|
||||
wordStart,
|
||||
patternLow,
|
||||
wordLow
|
||||
);
|
||||
|
||||
let row: number;
|
||||
let column = 1;
|
||||
let patternPos: number;
|
||||
let wordPos: number;
|
||||
|
||||
const hasStrongFirstMatch = [false];
|
||||
|
||||
// There will be a match, fill in tables
|
||||
for (
|
||||
row = 1, patternPos = patternStart;
|
||||
patternPos < patternLen;
|
||||
row++, patternPos++
|
||||
) {
|
||||
// Reduce search space to possible matching word positions and to possible access from next row
|
||||
const minWordMatchPos = _minWordMatchPos[patternPos];
|
||||
const maxWordMatchPos = _maxWordMatchPos[patternPos];
|
||||
const nextMaxWordMatchPos =
|
||||
patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen;
|
||||
|
||||
for (
|
||||
column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
|
||||
wordPos < nextMaxWordMatchPos;
|
||||
column++, wordPos++
|
||||
) {
|
||||
let score = Number.MIN_SAFE_INTEGER;
|
||||
let canComeDiag = false;
|
||||
|
||||
if (wordPos <= maxWordMatchPos) {
|
||||
score = _doScore(
|
||||
pattern,
|
||||
patternLow,
|
||||
patternPos,
|
||||
patternStart,
|
||||
word,
|
||||
wordLow,
|
||||
wordPos,
|
||||
wordLen,
|
||||
wordStart,
|
||||
_diag[row - 1][column - 1] === 0,
|
||||
hasStrongFirstMatch
|
||||
);
|
||||
}
|
||||
|
||||
let diagScore = 0;
|
||||
if (score !== Number.MAX_SAFE_INTEGER) {
|
||||
canComeDiag = true;
|
||||
diagScore = score + _table[row - 1][column - 1];
|
||||
}
|
||||
|
||||
const canComeLeft = wordPos > minWordMatchPos;
|
||||
const leftScore = canComeLeft
|
||||
? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
|
||||
: 0; // penalty for a gap start
|
||||
|
||||
const canComeLeftLeft =
|
||||
wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
|
||||
const leftLeftScore = canComeLeftLeft
|
||||
? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
|
||||
: 0; // penalty for a gap start
|
||||
|
||||
if (
|
||||
canComeLeftLeft &&
|
||||
(!canComeLeft || leftLeftScore >= leftScore) &&
|
||||
(!canComeDiag || leftLeftScore >= diagScore)
|
||||
) {
|
||||
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
|
||||
_table[row][column] = leftLeftScore;
|
||||
_arrows[row][column] = Arrow.LeftLeft;
|
||||
_diag[row][column] = 0;
|
||||
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
|
||||
// always prefer choosing left since that means a match is earlier in the word
|
||||
_table[row][column] = leftScore;
|
||||
_arrows[row][column] = Arrow.Left;
|
||||
_diag[row][column] = 0;
|
||||
} else if (canComeDiag) {
|
||||
_table[row][column] = diagScore;
|
||||
_arrows[row][column] = Arrow.Diag;
|
||||
_diag[row][column] = _diag[row - 1][column - 1] + 1;
|
||||
} else {
|
||||
throw new Error(`not possible`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_debug) {
|
||||
printTables(pattern, patternStart, word, wordStart);
|
||||
}
|
||||
|
||||
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
row--;
|
||||
column--;
|
||||
|
||||
const result: FuzzyScore = [_table[row][column], wordStart];
|
||||
|
||||
let backwardsDiagLength = 0;
|
||||
let maxMatchColumn = 0;
|
||||
|
||||
while (row >= 1) {
|
||||
// Find the column where we go diagonally up
|
||||
let diagColumn = column;
|
||||
do {
|
||||
const arrow = _arrows[row][diagColumn];
|
||||
if (arrow === Arrow.LeftLeft) {
|
||||
diagColumn -= 2;
|
||||
} else if (arrow === Arrow.Left) {
|
||||
diagColumn -= 1;
|
||||
} else {
|
||||
// found the diagonal
|
||||
break;
|
||||
}
|
||||
} while (diagColumn >= 1);
|
||||
|
||||
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
|
||||
if (
|
||||
backwardsDiagLength > 1 && // only if we would have a contiguous match of 3 characters
|
||||
patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] && // only if we can do a contiguous match diagonally
|
||||
!isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) && // only if the forwards chose diagonal is not an uppercase
|
||||
backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
|
||||
) {
|
||||
diagColumn = column;
|
||||
}
|
||||
|
||||
if (diagColumn === column) {
|
||||
// this is a contiguous match
|
||||
backwardsDiagLength++;
|
||||
} else {
|
||||
backwardsDiagLength = 1;
|
||||
}
|
||||
|
||||
if (!maxMatchColumn) {
|
||||
// remember the last matched column
|
||||
maxMatchColumn = diagColumn;
|
||||
}
|
||||
|
||||
row--;
|
||||
column = diagColumn - 1;
|
||||
result.push(column);
|
||||
}
|
||||
|
||||
if (wordLen === patternLen) {
|
||||
// the word matches the pattern with all characters!
|
||||
// giving the score a total match boost (to come up ahead other words)
|
||||
result[0] += 2;
|
||||
}
|
||||
|
||||
// Add 1 penalty for each skipped character in the word
|
||||
const skippedCharsCount = maxMatchColumn - patternLen;
|
||||
result[0] -= skippedCharsCount;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function _doScore(
|
||||
pattern: string,
|
||||
patternLow: string,
|
||||
patternPos: number,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordLow: string,
|
||||
wordPos: number,
|
||||
wordLen: number,
|
||||
wordStart: number,
|
||||
newMatchStart: boolean,
|
||||
outFirstMatchStrong: boolean[]
|
||||
): number {
|
||||
if (patternLow[patternPos] !== wordLow[wordPos]) {
|
||||
return Number.MIN_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
let score = 1;
|
||||
let isGapLocation = false;
|
||||
if (wordPos === patternPos - patternStart) {
|
||||
// common prefix: `foobar <-> foobaz`
|
||||
// ^^^^^
|
||||
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
|
||||
} else if (
|
||||
isUpperCaseAtPos(wordPos, word, wordLow) &&
|
||||
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
|
||||
) {
|
||||
// hitting upper-case: `foo <-> forOthers`
|
||||
// ^^ ^
|
||||
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
|
||||
isGapLocation = true;
|
||||
} else if (
|
||||
isSeparatorAtPos(wordLow, wordPos) &&
|
||||
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
|
||||
) {
|
||||
// hitting a separator: `. <-> foo.bar`
|
||||
// ^
|
||||
score = 5;
|
||||
} else if (
|
||||
isSeparatorAtPos(wordLow, wordPos - 1) ||
|
||||
isWhitespaceAtPos(wordLow, wordPos - 1)
|
||||
) {
|
||||
// post separator: `foo <-> bar_foo`
|
||||
// ^^^
|
||||
score = 5;
|
||||
isGapLocation = true;
|
||||
}
|
||||
|
||||
if (score > 1 && patternPos === patternStart) {
|
||||
outFirstMatchStrong[0] = true;
|
||||
}
|
||||
|
||||
if (!isGapLocation) {
|
||||
isGapLocation =
|
||||
isUpperCaseAtPos(wordPos, word, wordLow) ||
|
||||
isSeparatorAtPos(wordLow, wordPos - 1) ||
|
||||
isWhitespaceAtPos(wordLow, wordPos - 1);
|
||||
}
|
||||
|
||||
//
|
||||
if (patternPos === patternStart) {
|
||||
// first character in pattern
|
||||
if (wordPos > wordStart) {
|
||||
// the first pattern character would match a word character that is not at the word start
|
||||
// so introduce a penalty to account for the gap preceding this match
|
||||
score -= isGapLocation ? 3 : 5;
|
||||
}
|
||||
} else if (newMatchStart) {
|
||||
// this would be the beginning of a new match (i.e. there would be a gap before this location)
|
||||
score += isGapLocation ? 2 : 0;
|
||||
} else {
|
||||
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
|
||||
score += isGapLocation ? 0 : 1;
|
||||
}
|
||||
|
||||
if (wordPos + 1 === wordLen) {
|
||||
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
|
||||
// so pretend there is a gap after the last character in the word to normalize things
|
||||
score -= isGapLocation ? 3 : 5;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
function printTable(
|
||||
table: number[][],
|
||||
pattern: string,
|
||||
patternLen: number,
|
||||
word: string,
|
||||
wordLen: number
|
||||
): string {
|
||||
function pad(s: string, n: number, _pad = " ") {
|
||||
while (s.length < n) {
|
||||
s = _pad + s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
let ret = ` | |${word
|
||||
.split("")
|
||||
.map((c) => pad(c, 3))
|
||||
.join("|")}\n`;
|
||||
|
||||
for (let i = 0; i <= patternLen; i++) {
|
||||
if (i === 0) {
|
||||
ret += " |";
|
||||
} else {
|
||||
ret += `${pattern[i - 1]}|`;
|
||||
}
|
||||
ret +=
|
||||
table[i]
|
||||
.slice(0, wordLen + 1)
|
||||
.map((n) => pad(n.toString(), 3))
|
||||
.join("|") + "\n";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function printTables(
|
||||
pattern: string,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordStart: number
|
||||
): void {
|
||||
pattern = pattern.substr(patternStart);
|
||||
word = word.substr(wordStart);
|
||||
console.log(printTable(_table, pattern, pattern.length, word, word.length));
|
||||
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
|
||||
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
|
||||
}
|
||||
|
||||
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
|
||||
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
|
||||
const _diag = initTable(); // the length of a contiguous diagonal match
|
||||
const _table = initTable();
|
||||
const _arrows = <Arrow[][]>initTable();
|
||||
|
||||
function initArr(maxLen: number) {
|
||||
const row: number[] = [];
|
||||
for (let i = 0; i <= maxLen; i++) {
|
||||
row[i] = 0;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function _fillInMaxWordMatchPos(
|
||||
patternLen: number,
|
||||
wordLen: number,
|
||||
patternStart: number,
|
||||
wordStart: number,
|
||||
patternLow: string,
|
||||
wordLow: string
|
||||
) {
|
||||
let patternPos = patternLen - 1;
|
||||
let wordPos = wordLen - 1;
|
||||
while (patternPos >= patternStart && wordPos >= wordStart) {
|
||||
if (patternLow[patternPos] === wordLow[wordPos]) {
|
||||
_maxWordMatchPos[patternPos] = wordPos;
|
||||
patternPos--;
|
||||
}
|
||||
wordPos--;
|
||||
}
|
||||
}
|
||||
|
||||
export interface FuzzyScorer {
|
||||
(
|
||||
pattern: string,
|
||||
lowPattern: string,
|
||||
patternPos: number,
|
||||
word: string,
|
||||
lowWord: string,
|
||||
wordPos: number,
|
||||
firstMatchCanBeWeak: boolean
|
||||
): FuzzyScore | undefined;
|
||||
}
|
||||
|
||||
export function createMatches(score: undefined | FuzzyScore): Match[] {
|
||||
if (typeof score === "undefined") {
|
||||
return [];
|
||||
}
|
||||
const res: Match[] = [];
|
||||
const wordPos = score[1];
|
||||
for (let i = score.length - 1; i > 1; i--) {
|
||||
const pos = score[i] + wordPos;
|
||||
const last = res[res.length - 1];
|
||||
if (last && last.end === pos) {
|
||||
last.end = pos + 1;
|
||||
} else {
|
||||
res.push({ start: pos, end: pos + 1 });
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fast function (therefore imprecise) to check if code points are emojis.
|
||||
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js
|
||||
*/
|
||||
export function isEmojiImprecise(x: number): boolean {
|
||||
return (
|
||||
(x >= 0x1f1e6 && x <= 0x1f1ff) ||
|
||||
x === 8986 ||
|
||||
x === 8987 ||
|
||||
x === 9200 ||
|
||||
x === 9203 ||
|
||||
(x >= 9728 && x <= 10175) ||
|
||||
x === 11088 ||
|
||||
x === 11093 ||
|
||||
(x >= 127744 && x <= 128591) ||
|
||||
(x >= 128640 && x <= 128764) ||
|
||||
(x >= 128992 && x <= 129003) ||
|
||||
(x >= 129280 && x <= 129535) ||
|
||||
(x >= 129648 && x <= 129750)
|
||||
);
|
||||
}
|
@@ -1,4 +1,52 @@
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { fuzzyScore } from "./filter";
|
||||
|
||||
/**
|
||||
* Determine whether a sequence of letters exists in another string,
|
||||
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
|
||||
*
|
||||
* @param {string} filter - Sequence of letters to check for
|
||||
* @param {ScorableTextItem} item - Item against whose strings will be checked
|
||||
*
|
||||
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
|
||||
*/
|
||||
|
||||
export const fuzzySequentialMatch = (
|
||||
filter: string,
|
||||
item: ScorableTextItem
|
||||
) => {
|
||||
let topScore = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const word of item.strings) {
|
||||
const scores = fuzzyScore(
|
||||
filter,
|
||||
filter.toLowerCase(),
|
||||
0,
|
||||
word,
|
||||
word.toLowerCase(),
|
||||
0,
|
||||
true
|
||||
);
|
||||
|
||||
if (!scores) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The VS Code implementation of filter returns a 0 for a weak match.
|
||||
// But if .filter() sees a "0", it considers that a failed match and will remove it.
|
||||
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
|
||||
const score = scores[0] === 0 ? 1 : scores[0];
|
||||
|
||||
if (score > topScore) {
|
||||
topScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
if (topScore === Number.NEGATIVE_INFINITY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return topScore;
|
||||
};
|
||||
|
||||
/**
|
||||
* An interface that objects must extend in order to use the fuzzy sequence matcher
|
||||
@@ -18,48 +66,18 @@ export interface ScorableTextItem {
|
||||
strings: string[];
|
||||
}
|
||||
|
||||
export type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
filter: string,
|
||||
items: T[]
|
||||
) => T[];
|
||||
|
||||
export function fuzzyMatcher(search: string | null): (string) => boolean {
|
||||
const scorer = fuzzyScorer(search);
|
||||
return (value: string) => scorer([value]) !== Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
export function fuzzyScorer(
|
||||
search: string | null
|
||||
): (values: string[]) => number {
|
||||
const searchTerms = (search || "").match(/("[^"]+"|[^"\s]+)/g);
|
||||
if (!searchTerms) {
|
||||
return () => 0;
|
||||
}
|
||||
return (values) =>
|
||||
searchTerms
|
||||
.map((term) => {
|
||||
const resultsForTerm = fuzzysort.go(term, values, {
|
||||
allowTypo: true,
|
||||
});
|
||||
if (resultsForTerm.length > 0) {
|
||||
return Math.max(...resultsForTerm.map((result) => result.score));
|
||||
}
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
})
|
||||
.reduce((partial, current) => partial + current, 0);
|
||||
}
|
||||
|
||||
export const fuzzySortFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||
const scorer = fuzzyScorer(filter);
|
||||
return items
|
||||
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) =>
|
||||
items
|
||||
.map((item) => {
|
||||
item.score = scorer(item.strings);
|
||||
item.score = fuzzySequentialMatch(filter, item);
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.score !== undefined && item.score > -100000)
|
||||
.filter((item) => item.score !== undefined)
|
||||
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
||||
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
|
||||
);
|
||||
};
|
||||
|
||||
export const defaultFuzzyFilterSort = fuzzySortFilterSort;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
|
||||
export const promiseTimeout = (ms: number, promise: Promise<any>) => {
|
||||
const timeout = new Promise((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(`Timed out in ${ms} ms.`);
|
||||
|
@@ -1,18 +0,0 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export const subscribePollingCollection = (
|
||||
hass: HomeAssistant,
|
||||
updateData: (hass: HomeAssistant) => void,
|
||||
interval: number
|
||||
) => {
|
||||
let timeout;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
await updateData(hass);
|
||||
} finally {
|
||||
timeout = setTimeout(() => fetchData(), interval);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
return () => clearTimeout(timeout);
|
||||
};
|
@@ -1,167 +1,165 @@
|
||||
export const currencies = [
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
"AMD",
|
||||
"ANG",
|
||||
"AOA",
|
||||
"ARS",
|
||||
"AUD",
|
||||
"AWG",
|
||||
"AZN",
|
||||
"BAM",
|
||||
"BBD",
|
||||
"BDT",
|
||||
"BGN",
|
||||
"BHD",
|
||||
"BIF",
|
||||
"BMD",
|
||||
"BND",
|
||||
"BOB",
|
||||
"BRL",
|
||||
"BSD",
|
||||
"BTN",
|
||||
"BWP",
|
||||
"BYN",
|
||||
"BYR",
|
||||
"BZD",
|
||||
"CAD",
|
||||
"CDF",
|
||||
"CHF",
|
||||
"CLP",
|
||||
"CNY",
|
||||
"COP",
|
||||
"CRC",
|
||||
"CUP",
|
||||
"CVE",
|
||||
"CZK",
|
||||
"DJF",
|
||||
"DKK",
|
||||
"DOP",
|
||||
"DZD",
|
||||
"EGP",
|
||||
"ERN",
|
||||
"ETB",
|
||||
"EUR",
|
||||
"FJD",
|
||||
"FKP",
|
||||
"GBP",
|
||||
"GEL",
|
||||
"GHS",
|
||||
"GIP",
|
||||
"GMD",
|
||||
"GNF",
|
||||
"GTQ",
|
||||
"GYD",
|
||||
"HKD",
|
||||
"HNL",
|
||||
"HRK",
|
||||
"HTG",
|
||||
"HUF",
|
||||
"IDR",
|
||||
"ILS",
|
||||
"INR",
|
||||
"IQD",
|
||||
"IRR",
|
||||
"ISK",
|
||||
"JMD",
|
||||
"JOD",
|
||||
"JPY",
|
||||
"KES",
|
||||
"KGS",
|
||||
"KHR",
|
||||
"KMF",
|
||||
"KPW",
|
||||
"KRW",
|
||||
"KWD",
|
||||
"KYD",
|
||||
"KZT",
|
||||
"LAK",
|
||||
"LBP",
|
||||
"LKR",
|
||||
"LRD",
|
||||
"LSL",
|
||||
"LTL",
|
||||
"LYD",
|
||||
"MAD",
|
||||
"MDL",
|
||||
"MGA",
|
||||
"MKD",
|
||||
"MMK",
|
||||
"MNT",
|
||||
"MOP",
|
||||
"MRO",
|
||||
"MUR",
|
||||
"MVR",
|
||||
"MWK",
|
||||
"MXN",
|
||||
"MYR",
|
||||
"MZN",
|
||||
"NAD",
|
||||
"NGN",
|
||||
"NIO",
|
||||
"NOK",
|
||||
"NPR",
|
||||
"NZD",
|
||||
"OMR",
|
||||
"PAB",
|
||||
"PEN",
|
||||
"PGK",
|
||||
"PHP",
|
||||
"PKR",
|
||||
"PLN",
|
||||
"PYG",
|
||||
"QAR",
|
||||
"RON",
|
||||
"RSD",
|
||||
"RUB",
|
||||
"RWF",
|
||||
"SAR",
|
||||
"SBD",
|
||||
"SCR",
|
||||
"SDG",
|
||||
"SEK",
|
||||
"SGD",
|
||||
"SHP",
|
||||
"SLL",
|
||||
"SOS",
|
||||
"SRD",
|
||||
"SSP",
|
||||
"STD",
|
||||
"SYP",
|
||||
"SZL",
|
||||
"THB",
|
||||
"TJS",
|
||||
"TMT",
|
||||
"TND",
|
||||
"TOP",
|
||||
"TRY",
|
||||
"TTD",
|
||||
"TWD",
|
||||
"TZS",
|
||||
"UAH",
|
||||
"UGX",
|
||||
"USD",
|
||||
"UYU",
|
||||
"UZS",
|
||||
"VEF",
|
||||
"VND",
|
||||
"VUV",
|
||||
"WST",
|
||||
"XAF",
|
||||
"XCD",
|
||||
"XOF",
|
||||
"XPF",
|
||||
"YER",
|
||||
"ZAR",
|
||||
"ZMK",
|
||||
"ZWL",
|
||||
];
|
||||
|
||||
export const createCurrencyListEl = () => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "currencies";
|
||||
for (const currency of currencies) {
|
||||
for (const currency of [
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
"AMD",
|
||||
"ANG",
|
||||
"AOA",
|
||||
"ARS",
|
||||
"AUD",
|
||||
"AWG",
|
||||
"AZN",
|
||||
"BAM",
|
||||
"BBD",
|
||||
"BDT",
|
||||
"BGN",
|
||||
"BHD",
|
||||
"BIF",
|
||||
"BMD",
|
||||
"BND",
|
||||
"BOB",
|
||||
"BRL",
|
||||
"BSD",
|
||||
"BTN",
|
||||
"BWP",
|
||||
"BYN",
|
||||
"BYR",
|
||||
"BZD",
|
||||
"CAD",
|
||||
"CDF",
|
||||
"CHF",
|
||||
"CLP",
|
||||
"CNY",
|
||||
"COP",
|
||||
"CRC",
|
||||
"CUP",
|
||||
"CVE",
|
||||
"CZK",
|
||||
"DJF",
|
||||
"DKK",
|
||||
"DOP",
|
||||
"DZD",
|
||||
"EGP",
|
||||
"ERN",
|
||||
"ETB",
|
||||
"EUR",
|
||||
"FJD",
|
||||
"FKP",
|
||||
"GBP",
|
||||
"GEL",
|
||||
"GHS",
|
||||
"GIP",
|
||||
"GMD",
|
||||
"GNF",
|
||||
"GTQ",
|
||||
"GYD",
|
||||
"HKD",
|
||||
"HNL",
|
||||
"HRK",
|
||||
"HTG",
|
||||
"HUF",
|
||||
"IDR",
|
||||
"ILS",
|
||||
"INR",
|
||||
"IQD",
|
||||
"IRR",
|
||||
"ISK",
|
||||
"JMD",
|
||||
"JOD",
|
||||
"JPY",
|
||||
"KES",
|
||||
"KGS",
|
||||
"KHR",
|
||||
"KMF",
|
||||
"KPW",
|
||||
"KRW",
|
||||
"KWD",
|
||||
"KYD",
|
||||
"KZT",
|
||||
"LAK",
|
||||
"LBP",
|
||||
"LKR",
|
||||
"LRD",
|
||||
"LSL",
|
||||
"LTL",
|
||||
"LYD",
|
||||
"MAD",
|
||||
"MDL",
|
||||
"MGA",
|
||||
"MKD",
|
||||
"MMK",
|
||||
"MNT",
|
||||
"MOP",
|
||||
"MRO",
|
||||
"MUR",
|
||||
"MVR",
|
||||
"MWK",
|
||||
"MXN",
|
||||
"MYR",
|
||||
"MZN",
|
||||
"NAD",
|
||||
"NGN",
|
||||
"NIO",
|
||||
"NOK",
|
||||
"NPR",
|
||||
"NZD",
|
||||
"OMR",
|
||||
"PAB",
|
||||
"PEN",
|
||||
"PGK",
|
||||
"PHP",
|
||||
"PKR",
|
||||
"PLN",
|
||||
"PYG",
|
||||
"QAR",
|
||||
"RON",
|
||||
"RSD",
|
||||
"RUB",
|
||||
"RWF",
|
||||
"SAR",
|
||||
"SBD",
|
||||
"SCR",
|
||||
"SDG",
|
||||
"SEK",
|
||||
"SGD",
|
||||
"SHP",
|
||||
"SLL",
|
||||
"SOS",
|
||||
"SRD",
|
||||
"SSP",
|
||||
"STD",
|
||||
"SYP",
|
||||
"SZL",
|
||||
"THB",
|
||||
"TJS",
|
||||
"TMT",
|
||||
"TND",
|
||||
"TOP",
|
||||
"TRY",
|
||||
"TTD",
|
||||
"TWD",
|
||||
"TZS",
|
||||
"UAH",
|
||||
"UGX",
|
||||
"USD",
|
||||
"UYU",
|
||||
"UZS",
|
||||
"VEF",
|
||||
"VND",
|
||||
"VUV",
|
||||
"WST",
|
||||
"XAF",
|
||||
"XCD",
|
||||
"XOF",
|
||||
"XPF",
|
||||
"YER",
|
||||
"ZAR",
|
||||
"ZMK",
|
||||
"ZWL",
|
||||
]) {
|
||||
const option = document.createElement("option");
|
||||
option.value = currency;
|
||||
option.innerHTML = currency;
|
||||
|
@@ -7,26 +7,25 @@ import type {
|
||||
SortableColumnContainer,
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
import { fuzzyMatcher } from "../../common/string/filter/sequence-matching";
|
||||
|
||||
const filterData = (
|
||||
data: DataTableRowData[],
|
||||
columns: SortableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
const matcher = fuzzyMatcher(filter);
|
||||
filter = filter.toUpperCase();
|
||||
return data.filter((row) =>
|
||||
Object.entries(columns).some((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
if (column.filterable) {
|
||||
if (
|
||||
matcher(
|
||||
String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
)
|
||||
String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
)
|
||||
.toUpperCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@@ -198,10 +198,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
this.hass,
|
||||
deviceEntityLookup[device.id]
|
||||
),
|
||||
area:
|
||||
device.area_id && areaLookup[device.area_id]
|
||||
? areaLookup[device.area_id].name
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
area: device.area_id
|
||||
? areaLookup[device.area_id].name
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
}));
|
||||
if (!outputDevices.length) {
|
||||
return [
|
||||
|
@@ -15,7 +15,6 @@ import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import { defaultFuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||
|
||||
interface HassEntityWithCachedName extends HassEntity {
|
||||
friendly_name: string;
|
||||
@@ -337,18 +336,11 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const filterString = ev.detail.value;
|
||||
|
||||
const sortableEntityStates = this._states.map((entityState) => ({
|
||||
strings: [entityState.entity_id, computeStateName(entityState)],
|
||||
entityState: entityState,
|
||||
}));
|
||||
const sortedEntityStates = defaultFuzzyFilterSort(
|
||||
filterString,
|
||||
sortableEntityStates
|
||||
);
|
||||
(this.comboBox as any).filteredItems = sortedEntityStates.map(
|
||||
(sortableItem) => sortableItem.entityState
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
(this.comboBox as any).filteredItems = this._states.filter(
|
||||
(entityState) =>
|
||||
entityState.entity_id.toLowerCase().includes(filterString) ||
|
||||
computeStateName(entityState).toLowerCase().includes(filterString)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -409,7 +409,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
name,
|
||||
});
|
||||
this._areas = [...this._areas!, area];
|
||||
(this.comboBox as any).filteredItems = this._getAreas(
|
||||
(this.comboBox as any).items = this._getAreas(
|
||||
this._areas!,
|
||||
this._devices!,
|
||||
this._entities!,
|
||||
|
@@ -310,7 +310,6 @@ export class HaBaseTimeInput extends LitElement {
|
||||
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-textfield {
|
||||
width: 40px;
|
||||
|
@@ -1,79 +0,0 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { roundWithOneDecimal } from "../util/calculate";
|
||||
import "./ha-bar";
|
||||
import "./ha-settings-row";
|
||||
|
||||
@customElement("ha-metric")
|
||||
class HaMetric extends LitElement {
|
||||
@property({ type: Number }) public value!: number;
|
||||
|
||||
@property({ type: String }) public heading!: string;
|
||||
|
||||
@property({ type: String }) public tooltip?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const roundedValue = roundWithOneDecimal(this.value);
|
||||
return html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading"> ${this.heading} </span>
|
||||
<div slot="description" .title=${this.tooltip ?? ""}>
|
||||
<span class="value"> ${roundedValue} % </span>
|
||||
<ha-bar
|
||||
class=${classMap({
|
||||
"target-warning": roundedValue > 50,
|
||||
"target-critical": roundedValue > 85,
|
||||
})}
|
||||
.value=${this.value}
|
||||
></ha-bar>
|
||||
</div>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row > div[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-bar {
|
||||
--ha-bar-primary-color: var(
|
||||
--metric-bar-ok-color,
|
||||
var(--success-color)
|
||||
);
|
||||
}
|
||||
.target-warning {
|
||||
--ha-bar-primary-color: var(
|
||||
--metric-bar-warning-color,
|
||||
var(--warning-color)
|
||||
);
|
||||
}
|
||||
.target-critical {
|
||||
--ha-bar-primary-color: var(
|
||||
--metric-bar-critical-color,
|
||||
var(--error-color)
|
||||
);
|
||||
}
|
||||
.value {
|
||||
width: 48px;
|
||||
padding-right: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-metric": HaMetric;
|
||||
}
|
||||
}
|
@@ -4,9 +4,9 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-clickable-list-item";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-clickable-list-item";
|
||||
|
||||
@customElement("ha-navigation-list")
|
||||
class HaNavigationList extends LitElement {
|
||||
@@ -56,9 +56,6 @@ class HaNavigationList extends LitElement {
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
@@ -71,7 +68,6 @@ class HaNavigationList extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: block;
|
||||
}
|
||||
ha-svg-icon {
|
||||
padding: 8px;
|
||||
@@ -82,10 +78,9 @@ class HaNavigationList extends LitElement {
|
||||
.icon-background ha-svg-icon {
|
||||
color: #fff;
|
||||
}
|
||||
ha-clickable-list-item {
|
||||
mwc-list-item {
|
||||
cursor: pointer;
|
||||
font-size: var(--navigation-list-item-title-font-size);
|
||||
padding: var(--navigation-list-item-padding) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -163,9 +163,6 @@ export class HaNetwork extends LitElement {
|
||||
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-display: contents;
|
||||
--settings-row-prefix-display: contents;
|
||||
}
|
||||
|
||||
span[slot="heading"],
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -76,13 +76,6 @@ export class HaLocationSelector extends LitElement {
|
||||
const radius = ev.detail.radius;
|
||||
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 400px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -107,7 +107,6 @@ export class HaNumberSelector extends LitElement {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-slider {
|
||||
flex: 1;
|
||||
|
@@ -472,7 +472,6 @@ export class HaServiceControl extends LitElement {
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
|
@@ -47,7 +47,7 @@ export class HaSettingsRow extends LitElement {
|
||||
display: contents;
|
||||
}
|
||||
:host(:not([narrow])) .content {
|
||||
display: var(--settings-row-content-display, flex);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
padding: 16px 0;
|
||||
@@ -68,7 +68,7 @@ export class HaSettingsRow extends LitElement {
|
||||
white-space: normal;
|
||||
}
|
||||
.prefix-wrap {
|
||||
display: var(--settings-row-prefix-display);
|
||||
display: contents;
|
||||
}
|
||||
:host([narrow]) .prefix-wrap {
|
||||
display: flex;
|
||||
|
@@ -1051,6 +1051,9 @@ class HaSidebar extends LitElement {
|
||||
padding: 0px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
.configuration-badge {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
ha-svg-icon + .notification-badge,
|
||||
ha-svg-icon + .configuration-badge {
|
||||
position: absolute;
|
||||
|
@@ -151,7 +151,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
ha-media-player-browse {
|
||||
--media-browser-max-height: calc(100vh - 65px);
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
@@ -164,7 +163,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
ha-media-player-browse {
|
||||
position: initial;
|
||||
--media-browser-max-height: 100vh - 137px;
|
||||
height: 100vh - 137px;
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
@@ -3,8 +3,6 @@ import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { grid } from "@lit-labs/virtualizer/layouts/grid";
|
||||
import "@lit-labs/virtualizer";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -18,13 +16,16 @@ import {
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import {
|
||||
browseMediaPlayer,
|
||||
@@ -39,18 +40,18 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import type { HaCard } from "../ha-card";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-fab";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-browse-media-tts";
|
||||
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -100,6 +101,8 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@query(".content") private _content?: HTMLDivElement;
|
||||
|
||||
@queryAll(".lazythumbnail") private _thumbnails?: HaCard[];
|
||||
|
||||
private _headerOffsetHeight = 0;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
@@ -145,6 +148,326 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`
|
||||
<div class="container">${this._renderError(this._error)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this._currentItem) {
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
const currentItem = this._currentItem;
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
`ui.components.media-browser.class.${currentItem.media_class}`
|
||||
);
|
||||
const children = currentItem.children || [];
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
||||
const childrenMediaClass = currentItem.children_media_class
|
||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||
: MediaClassBrowserSettings.directory;
|
||||
|
||||
return html`
|
||||
${
|
||||
currentItem.can_play
|
||||
? html` <div
|
||||
class="header ${classMap({
|
||||
"no-img": !currentItem.thumbnail,
|
||||
"no-dialog": !this.dialog,
|
||||
})}"
|
||||
@transitionend=${this._setHeaderHeight}
|
||||
>
|
||||
<div class="header-content">
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style=${styleMap({
|
||||
backgroundImage: currentItem.thumbnail
|
||||
? `url(${currentItem.thumbnail})`
|
||||
: "none",
|
||||
})}
|
||||
>
|
||||
${this._narrow && currentItem?.can_play
|
||||
? html`
|
||||
<ha-fab
|
||||
mini
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</ha-fab>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
<div class="breadcrumb">
|
||||
<h1 class="title">${currentItem.title}</h1>
|
||||
${subtitle
|
||||
? html` <h2 class="subtitle">${subtitle}</h2> `
|
||||
: ""}
|
||||
</div>
|
||||
${currentItem.can_play &&
|
||||
(!currentItem.thumbnail || !this._narrow)
|
||||
? html`
|
||||
<mwc-button
|
||||
raised
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
<div
|
||||
class="content"
|
||||
@scroll=${this._scroll}
|
||||
@touchmove=${this._scroll}
|
||||
>
|
||||
${
|
||||
this._error
|
||||
? html`
|
||||
<div class="container">
|
||||
${this._renderError(this._error)}
|
||||
</div>
|
||||
`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
? html`
|
||||
<ha-browse-media-tts
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: !children.length && !currentItem.not_shown
|
||||
? html`
|
||||
<div class="container no-items">
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`
|
||||
<div class="highlight-add-button">
|
||||
<span>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowUpRight}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.highlight_button"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: childrenMediaClass.layout === "grid"
|
||||
? html`
|
||||
<div
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio === "portrait",
|
||||
})}"
|
||||
>
|
||||
${children.map(
|
||||
(child) => html`
|
||||
<div
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._childClicked}
|
||||
>
|
||||
<ha-card outlined>
|
||||
<div class="thumbnail">
|
||||
${child.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${["app", "directory"].includes(
|
||||
child.media_class
|
||||
)
|
||||
? "centered-image"
|
||||
: ""} image lazythumbnail"
|
||||
data-src=${child.thumbnail}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${MediaClassBrowserSettings[
|
||||
child.media_class === "directory"
|
||||
? child.children_media_class ||
|
||||
child.media_class
|
||||
: child.media_class
|
||||
].icon}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
${child.can_play
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="play ${classMap({
|
||||
can_expand: child.can_expand,
|
||||
})}"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
@click=${this._actionClicked}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">
|
||||
${child.title}
|
||||
<paper-tooltip
|
||||
fitToVisibleBounds
|
||||
position="top"
|
||||
offset="4"
|
||||
>${child.title}</paper-tooltip
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<div class="grid not-shown">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${children.map(
|
||||
(child) => html`
|
||||
<mwc-list-item
|
||||
@click=${this._childClicked}
|
||||
.item=${child}
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
graphic: true,
|
||||
lazythumbnail:
|
||||
mediaClass.show_list_images === true,
|
||||
})}
|
||||
data-src=${ifDefined(
|
||||
mediaClass.show_list_images && child.thumbnail
|
||||
? child.thumbnail
|
||||
: undefined
|
||||
)}
|
||||
slot="graphic"
|
||||
>
|
||||
<ha-icon-button
|
||||
class="play ${classMap({
|
||||
show:
|
||||
!mediaClass.show_list_images ||
|
||||
!child.thumbnail,
|
||||
})}"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
@click=${this._actionClicked}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<span class="title">${child.title}</span>
|
||||
</mwc-list-item>
|
||||
<li divider role="separator"></li>
|
||||
`
|
||||
)}
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<mwc-list-item
|
||||
noninteractive
|
||||
class="not-shown"
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<span class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</span>
|
||||
</mwc-list-item>
|
||||
`
|
||||
: ""}
|
||||
</mwc-list>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._measureCard();
|
||||
this._attachResizeObserver();
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.size > 1 || !changedProps.has("hass")) {
|
||||
return true;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
return oldHass === undefined || oldHass.localize !== this.hass.localize;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
@@ -260,19 +583,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.size > 1 || !changedProps.has("hass")) {
|
||||
return true;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
return oldHass === undefined || oldHass.localize !== this.hass.localize;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._measureCard();
|
||||
this._attachResizeObserver();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
@@ -280,368 +590,16 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
this._animateHeaderHeight();
|
||||
} else if (changedProps.has("_currentItem")) {
|
||||
this._setHeaderHeight();
|
||||
this._attachIntersectionObserver();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`
|
||||
<div class="container">${this._renderError(this._error)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this._currentItem) {
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
const currentItem = this._currentItem;
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
`ui.components.media-browser.class.${currentItem.media_class}`
|
||||
);
|
||||
const children = currentItem.children || [];
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
||||
const childrenMediaClass = currentItem.children_media_class
|
||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||
: MediaClassBrowserSettings.directory;
|
||||
|
||||
const backgroundImage = currentItem.thumbnail
|
||||
? this._getSignedThumbnail(currentItem.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
${
|
||||
currentItem.can_play
|
||||
? html`
|
||||
<div
|
||||
class="header ${classMap({
|
||||
"no-img": !currentItem.thumbnail,
|
||||
"no-dialog": !this.dialog,
|
||||
})}"
|
||||
@transitionend=${this._setHeaderHeight}
|
||||
>
|
||||
<div class="header-content">
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: ${until(
|
||||
backgroundImage,
|
||||
""
|
||||
)}"
|
||||
>
|
||||
${this._narrow && currentItem?.can_play
|
||||
? html`
|
||||
<ha-fab
|
||||
mini
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</ha-fab>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
<div class="breadcrumb">
|
||||
<h1 class="title">${currentItem.title}</h1>
|
||||
${subtitle
|
||||
? html` <h2 class="subtitle">${subtitle}</h2> `
|
||||
: ""}
|
||||
</div>
|
||||
${currentItem.can_play &&
|
||||
(!currentItem.thumbnail || !this._narrow)
|
||||
? html`
|
||||
<mwc-button
|
||||
raised
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<div
|
||||
class="content"
|
||||
@scroll=${this._scroll}
|
||||
@touchmove=${this._scroll}
|
||||
>
|
||||
${
|
||||
this._error
|
||||
? html`
|
||||
<div class="container">
|
||||
${this._renderError(this._error)}
|
||||
</div>
|
||||
`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
? html`
|
||||
<ha-browse-media-tts
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: !children.length && !currentItem.not_shown
|
||||
? html`
|
||||
<div class="container no-items">
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`
|
||||
<div class="highlight-add-button">
|
||||
<span>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowUpRight}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.highlight_button"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: childrenMediaClass.layout === "grid"
|
||||
? html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.layout=${grid({
|
||||
itemSize: {
|
||||
width: "175px",
|
||||
height: "225px",
|
||||
},
|
||||
gap: "16px",
|
||||
flex: { preserve: "aspect-ratio" },
|
||||
justify: "space-evenly",
|
||||
direction: "vertical",
|
||||
})}
|
||||
.items=${children}
|
||||
.renderItem=${this._renderGridItem}
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio === "portrait",
|
||||
not_shown: !!currentItem.not_shown,
|
||||
})}"
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<div class="grid not-shown">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.items=${children}
|
||||
.renderItem=${this._renderListItem}
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<mwc-list-item
|
||||
noninteractive
|
||||
class="not-shown"
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<span class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</span>
|
||||
</mwc-list-item>
|
||||
`
|
||||
: ""}
|
||||
</mwc-list>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const backgroundImage = child.thumbnail
|
||||
? this._getSignedThumbnail(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
<div class="child" .item=${child} @click=${this._childClicked}>
|
||||
<ha-card outlined>
|
||||
<div class="thumbnail">
|
||||
${child.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${["app", "directory"].includes(child.media_class)
|
||||
? "centered-image"
|
||||
: ""} image"
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${MediaClassBrowserSettings[
|
||||
child.media_class === "directory"
|
||||
? child.children_media_class || child.media_class
|
||||
: child.media_class
|
||||
].icon}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
${child.can_play
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="play ${classMap({
|
||||
can_expand: child.can_expand,
|
||||
})}"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
@click=${this._actionClicked}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">
|
||||
${child.title}
|
||||
<paper-tooltip fitToVisibleBounds position="top" offset="4"
|
||||
>${child.title}</paper-tooltip
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
private _renderListItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const currentItem = this._currentItem;
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem!.media_class];
|
||||
|
||||
const backgroundImage =
|
||||
mediaClass.show_list_images && child.thumbnail
|
||||
? this._getSignedThumbnail(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
<mwc-list-item
|
||||
@click=${this._childClicked}
|
||||
.item=${child}
|
||||
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
graphic: true,
|
||||
thumbnail: mediaClass.show_list_images === true,
|
||||
})}
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
slot="graphic"
|
||||
>
|
||||
<ha-icon-button
|
||||
class="play ${classMap({
|
||||
show: !mediaClass.show_list_images || !child.thumbnail,
|
||||
})}"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
@click=${this._actionClicked}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<span class="title">${child.title}</span>
|
||||
</mwc-list-item>
|
||||
<li divider role="separator"></li>
|
||||
`;
|
||||
};
|
||||
|
||||
private async _getSignedThumbnail(
|
||||
thumbnailUrl: string | undefined
|
||||
): Promise<string> {
|
||||
if (!thumbnailUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
return (await getSignedPath(this.hass, thumbnailUrl)).path;
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("https://brands.home-assistant.io")) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
private _actionClicked = (ev: MouseEvent): void => {
|
||||
private _actionClicked(ev: MouseEvent): void {
|
||||
ev.stopPropagation();
|
||||
const item = (ev.currentTarget as any).item;
|
||||
|
||||
this._runAction(item);
|
||||
};
|
||||
}
|
||||
|
||||
private _runAction(item: MediaPlayerItem): void {
|
||||
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
|
||||
@@ -657,7 +615,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _childClicked = async (ev: MouseEvent): Promise<void> => {
|
||||
private async _childClicked(ev: MouseEvent): Promise<void> {
|
||||
const target = ev.currentTarget as any;
|
||||
const item: MediaPlayerItem = target.item;
|
||||
|
||||
@@ -673,7 +631,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: [...this.navigateIds, item],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private async _fetchData(
|
||||
entityId: string,
|
||||
@@ -700,6 +658,55 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
this._resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load thumbnails for images on demand as they become visible.
|
||||
*/
|
||||
private async _attachIntersectionObserver(): Promise<void> {
|
||||
if (!("IntersectionObserver" in window) || !this._thumbnails) {
|
||||
return;
|
||||
}
|
||||
if (!this._intersectionObserver) {
|
||||
this._intersectionObserver = new IntersectionObserver(
|
||||
async (entries, observer) => {
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
const thumbnailCard = entry.target as HTMLElement;
|
||||
let thumbnailUrl = thumbnailCard.dataset.src;
|
||||
if (!thumbnailUrl) {
|
||||
return;
|
||||
}
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
const signedPath = await getSignedPath(this.hass, thumbnailUrl);
|
||||
thumbnailUrl = signedPath.path;
|
||||
} else if (
|
||||
thumbnailUrl.startsWith("https://brands.home-assistant.io")
|
||||
) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`;
|
||||
observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
const observer = this._intersectionObserver!;
|
||||
for (const thumbnailCard of this._thumbnails) {
|
||||
observer.observe(thumbnailCard);
|
||||
}
|
||||
}
|
||||
|
||||
private _closeDialogAction(): void {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
@@ -834,7 +841,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
@@ -920,7 +926,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.not-shown {
|
||||
font-style: italic;
|
||||
color: var(--secondary-text-color);
|
||||
padding: 8px 16px 8px;
|
||||
}
|
||||
|
||||
.grid.not-shown {
|
||||
@@ -946,11 +951,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
|
||||
mwc-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.children {
|
||||
.children {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
@@ -987,7 +988,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
|
||||
.portrait ha-card .thumbnail {
|
||||
.portrait.children ha-card .thumbnail {
|
||||
padding-bottom: 150%;
|
||||
}
|
||||
|
||||
@@ -1061,6 +1062,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-card:hover .lazythumbnail {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.child .title {
|
||||
font-size: 16px;
|
||||
padding-top: 16px;
|
||||
@@ -1122,7 +1127,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:host([narrow]) div.children {
|
||||
:host([narrow]) .children {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
|
||||
}
|
||||
|
||||
@@ -1227,16 +1232,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
--mdc-fab-box-shadow: none;
|
||||
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
height: 100%;
|
||||
overflow: overlay !important;
|
||||
contain: size layout !important;
|
||||
}
|
||||
|
||||
lit-virtualizer.not_shown {
|
||||
height: calc(100% - 36px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -152,12 +152,6 @@ export interface EventTrigger extends BaseTrigger {
|
||||
context?: ContextConstraint;
|
||||
}
|
||||
|
||||
export interface CalendarTrigger extends BaseTrigger {
|
||||
platform: "calendar";
|
||||
event: "start" | "end";
|
||||
entity_id: string;
|
||||
}
|
||||
|
||||
export type Trigger =
|
||||
| StateTrigger
|
||||
| MqttTrigger
|
||||
@@ -172,8 +166,7 @@ export type Trigger =
|
||||
| TimeTrigger
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
| DeviceTrigger
|
||||
| CalendarTrigger;
|
||||
| DeviceTrigger;
|
||||
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
@@ -233,20 +226,6 @@ export interface TriggerCondition extends BaseCondition {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type ShorthandBaseCondition = Omit<BaseCondition, "condition">;
|
||||
|
||||
export interface ShorthandAndCondition extends ShorthandBaseCondition {
|
||||
and: Condition[];
|
||||
}
|
||||
|
||||
export interface ShorthandOrCondition extends ShorthandBaseCondition {
|
||||
or: Condition[];
|
||||
}
|
||||
|
||||
export interface ShorthandNotCondition extends ShorthandBaseCondition {
|
||||
not: Condition[];
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
| StateCondition
|
||||
| NumericStateCondition
|
||||
@@ -258,12 +237,6 @@ export type Condition =
|
||||
| LogicalCondition
|
||||
| TriggerCondition;
|
||||
|
||||
export type ConditionWithShorthand =
|
||||
| Condition
|
||||
| ShorthandAndCondition
|
||||
| ShorthandOrCondition
|
||||
| ShorthandNotCondition;
|
||||
|
||||
export const triggerAutomationActions = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
|
@@ -1,9 +1,4 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface LogProvider {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const fetchErrorLog = (hass: HomeAssistant) =>
|
||||
hass.callApi<string>("GET", "error_log");
|
||||
|
@@ -1,15 +1,10 @@
|
||||
import type {
|
||||
HassEntities,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { BINARY_STATE_ON } from "../common/const";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
|
||||
export const UPDATE_SUPPORT_INSTALL = 1;
|
||||
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
|
||||
@@ -36,12 +31,8 @@ export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof entity.attributes.in_progress === "number";
|
||||
|
||||
export const updateCanInstall = (
|
||||
entity: UpdateEntity,
|
||||
showSkipped = false
|
||||
): boolean =>
|
||||
(entity.state === BINARY_STATE_ON ||
|
||||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
|
||||
export const updateCanInstall = (entity: UpdateEntity): boolean =>
|
||||
entity.state === BINARY_STATE_ON &&
|
||||
supportsFeature(entity, UPDATE_SUPPORT_INSTALL);
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
@@ -52,75 +43,3 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
||||
type: "update/release_notes",
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
||||
export const filterUpdateEntities = (entities: HassEntities) =>
|
||||
(
|
||||
Object.values(entities).filter(
|
||||
(entity) => computeStateDomain(entity) === "update"
|
||||
) as UpdateEntity[]
|
||||
).sort((a, b) => {
|
||||
if (a.attributes.title === "Home Assistant Core") {
|
||||
return -3;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Core") {
|
||||
return 3;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Operating System") {
|
||||
return -2;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Operating System") {
|
||||
return 2;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Supervisor") {
|
||||
return -1;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Supervisor") {
|
||||
return 1;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.attributes.title || a.attributes.friendly_name || "",
|
||||
b.attributes.title || b.attributes.friendly_name || ""
|
||||
);
|
||||
});
|
||||
|
||||
export const filterUpdateEntitiesWithInstall = (
|
||||
entities: HassEntities,
|
||||
showSkipped = false
|
||||
) =>
|
||||
filterUpdateEntities(entities).filter((entity) =>
|
||||
updateCanInstall(entity, showSkipped)
|
||||
);
|
||||
|
||||
export const checkForEntityUpdates = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant
|
||||
) => {
|
||||
const entities = filterUpdateEntities(hass.states).map(
|
||||
(entity) => entity.entity_id
|
||||
);
|
||||
|
||||
if (!entities.length) {
|
||||
showAlertDialog(element, {
|
||||
title: hass.localize("ui.panel.config.updates.no_update_entities.title"),
|
||||
text: hass.localize(
|
||||
"ui.panel.config.updates.no_update_entities.description"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callService("homeassistant", "update_entity", {
|
||||
entity_id: entities,
|
||||
});
|
||||
|
||||
if (filterUpdateEntitiesWithInstall(hass.states).length) {
|
||||
showToast(element, {
|
||||
message: hass.localize("ui.panel.config.updates.updates_refreshed"),
|
||||
});
|
||||
} else {
|
||||
showToast(element, {
|
||||
message: hass.localize("ui.panel.config.updates.no_new_updates"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -127,7 +127,7 @@ export interface ZWaveJSClient {
|
||||
|
||||
export interface ZWaveJSController {
|
||||
home_id: number;
|
||||
sdk_version: string;
|
||||
library_version: string;
|
||||
type: number;
|
||||
own_node_id: number;
|
||||
is_secondary: boolean;
|
||||
@@ -136,7 +136,7 @@ export interface ZWaveJSController {
|
||||
was_real_primary: boolean;
|
||||
is_static_update_controller: boolean;
|
||||
is_slave: boolean;
|
||||
firmware_version: string;
|
||||
serial_api_version: string;
|
||||
manufacturer_id: number;
|
||||
product_id: number;
|
||||
product_type: number;
|
||||
|
@@ -146,14 +146,14 @@ export const showOptionsFlowDialog = (
|
||||
renderMenuHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
`component.${step.handler}.option.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${step.handler}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.description`,
|
||||
`component.${step.handler}.option.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
@@ -169,7 +169,7 @@ export const showOptionsFlowDialog = (
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.menu_options.${option}`,
|
||||
`component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
@@ -77,7 +77,7 @@ class DialogBox extends LitElement {
|
||||
dialogInitialFocus
|
||||
.value=${this._value || ""}
|
||||
@keyup=${this._handleKeyUp}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
.label=${this._params.inputLabel
|
||||
? this._params.inputLabel
|
||||
: ""}
|
||||
|
@@ -119,15 +119,7 @@ class MoreInfoVacuum extends LitElement {
|
||||
"ui.dialogs.more_info_control.vacuum.status"
|
||||
)}:
|
||||
</span>
|
||||
<span>
|
||||
<strong>
|
||||
${stateObj.attributes.status ||
|
||||
this.hass.localize(
|
||||
`component.vacuum.state._.${stateObj.state}`
|
||||
) ||
|
||||
stateObj.state}
|
||||
</strong>
|
||||
</span>
|
||||
<span><strong>${stateObj.attributes.status}</strong></span>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
@@ -24,7 +24,7 @@ import { domainIcon } from "../../common/entity/domain_icon";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
defaultFuzzyFilterSort,
|
||||
fuzzyFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../../common/string/filter/sequence-matching";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
@@ -694,7 +694,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
private _filterItems = memoizeOne(
|
||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
||||
defaultFuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
);
|
||||
|
||||
static get styles() {
|
||||
|
@@ -130,7 +130,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
* Array of tabs to show on the page.
|
||||
* @type {Array}
|
||||
*/
|
||||
@property() public tabs: PageNavigation[] = [];
|
||||
@property() public tabs!: PageNavigation[];
|
||||
|
||||
/**
|
||||
* Force hides the filter menu.
|
||||
@@ -283,9 +283,6 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
display: block;
|
||||
}
|
||||
:host([narrow]) hass-tabs-subpage {
|
||||
--main-title-margin: 0;
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@@ -82,16 +82,6 @@ class HassTabsSubpage extends LitElement {
|
||||
(!page.advancedOnly || showAdvanced)
|
||||
);
|
||||
|
||||
if (shownTabs.length < 2) {
|
||||
if (shownTabs.length === 1) {
|
||||
const page = shownTabs[0];
|
||||
return [
|
||||
page.translationKey ? localizeFunc(page.translationKey) : page.name,
|
||||
];
|
||||
}
|
||||
return [""];
|
||||
}
|
||||
|
||||
return shownTabs.map(
|
||||
(page) =>
|
||||
html`
|
||||
@@ -144,7 +134,7 @@ class HassTabsSubpage extends LitElement {
|
||||
this.narrow,
|
||||
this.localizeFunc || this.hass.localize
|
||||
);
|
||||
const showTabs = tabs.length > 1;
|
||||
const showTabs = tabs.length > 1 || !this.narrow;
|
||||
return html`
|
||||
<div class="toolbar">
|
||||
${this.mainPage || (!this.backPath && history.state?.root)
|
||||
@@ -169,10 +159,8 @@ class HassTabsSubpage extends LitElement {
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
${this.narrow || !showTabs
|
||||
? html`<div class="main-title">
|
||||
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
|
||||
</div>`
|
||||
${this.narrow
|
||||
? html`<div class="main-title"><slot name="header"></slot></div>`
|
||||
: ""}
|
||||
${showTabs
|
||||
? html`
|
||||
@@ -295,7 +283,6 @@ class HassTabsSubpage extends LitElement {
|
||||
max-height: var(--header-height);
|
||||
line-height: 20px;
|
||||
color: var(--sidebar-text-color);
|
||||
margin: var(--main-title-margin, 0 0 0 24px);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../hassio/src/components/hassio-ansi-to-html";
|
||||
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
|
||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-ansi-to-html";
|
||||
import { fetchInstallationType } from "../data/onboarding";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
||||
@@ -86,7 +86,7 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
|
||||
padding: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
ha-ansi-to-html {
|
||||
hassio-ansi-to-html {
|
||||
display: block;
|
||||
line-height: 22px;
|
||||
padding: 0 8px;
|
||||
|
@@ -332,7 +332,6 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
`,
|
||||
|
@@ -28,7 +28,6 @@ import {
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./types/ha-automation-trigger-calendar";
|
||||
import "./types/ha-automation-trigger-device";
|
||||
import "./types/ha-automation-trigger-event";
|
||||
import "./types/ha-automation-trigger-geo_location";
|
||||
@@ -45,7 +44,6 @@ import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
|
||||
const OPTIONS = [
|
||||
"calendar",
|
||||
"device",
|
||||
"event",
|
||||
"state",
|
||||
|
@@ -1,79 +0,0 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { CalendarTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { TriggerElement } from "../ha-automation-trigger-row";
|
||||
import type { HaFormSchema } from "../../../../../components/ha-form/types";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
|
||||
@customElement("ha-automation-trigger-calendar")
|
||||
export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: CalendarTrigger;
|
||||
|
||||
private _schema = memoizeOne((localize: LocalizeFunc) => [
|
||||
{
|
||||
name: "entity_id",
|
||||
required: true,
|
||||
selector: { entity: { domain: "calendar" } },
|
||||
},
|
||||
{
|
||||
name: "event",
|
||||
type: "select",
|
||||
required: true,
|
||||
options: [
|
||||
[
|
||||
"start",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.calendar.start"
|
||||
),
|
||||
],
|
||||
[
|
||||
"end",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.calendar.end"
|
||||
),
|
||||
],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
public static get defaultConfig() {
|
||||
return {
|
||||
event: "start" as CalendarTrigger["event"],
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const schema = this._schema(this.hass.localize);
|
||||
return html`
|
||||
<ha-form
|
||||
.schema=${schema}
|
||||
.data=${this.trigger}
|
||||
.hass=${this.hass}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const newTrigger = ev.detail.value;
|
||||
fireEvent(this, "value-changed", { value: newTrigger });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.calendar.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trigger-calendar": HaCalendarTrigger;
|
||||
}
|
||||
}
|
@@ -80,7 +80,6 @@ class HaConfigBackup extends LitElement {
|
||||
actions: {
|
||||
title: "",
|
||||
width: "15%",
|
||||
type: "overflow-menu",
|
||||
template: (_: string, backup: BackupContent) =>
|
||||
html`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
@@ -127,23 +126,17 @@ class HaConfigBackup extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${[
|
||||
{
|
||||
translationKey: "ui.panel.config.backup.caption",
|
||||
path: `/config/backup`,
|
||||
},
|
||||
]}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._getItems(this._backupData.backups)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.backup.picker.search"
|
||||
)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_bakcups")}
|
||||
>
|
||||
<span slot="header"
|
||||
>${this.hass.localize("ui.panel.config.backup.caption")}</span
|
||||
>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
?disabled=${this._backupData.backing_up}
|
||||
|
@@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.tabs=${configSections.blueprints}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._processedBlueprints(this.blueprints)}
|
||||
id="entity_id"
|
||||
|
@@ -58,9 +58,9 @@ class ConfigAnalytics extends LitElement {
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
${analyticsLearnMore(this.hass)}
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="footer">${analyticsLearnMore(this.hass)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -117,10 +117,6 @@ class ConfigAnalytics extends LitElement {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.footer {
|
||||
padding: 32px 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
`, // row-reverse so we tab first to "save"
|
||||
];
|
||||
}
|
||||
|
342
src/panels/config/core/ha-config-core-form.ts
Normal file
342
src/panels/config/core/ha-config-core-form.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import "@material/mwc-button/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 { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { createCurrencyListEl } from "../../../components/currency-datalist";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import { createTimezoneListEl } from "../../../components/timezone-datalist";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { SYMBOL_TO_ISO } from "../../../data/currency";
|
||||
import type { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
|
||||
@customElement("ha-config-core-form")
|
||||
class ConfigCoreForm extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _working = false;
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
|
||||
@state() private _currency?: string;
|
||||
|
||||
@state() private _elevation?: string;
|
||||
|
||||
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
|
||||
@state() private _timeZone?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._working || !canEdit;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.form.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocation(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude,
|
||||
this._location
|
||||
)}
|
||||
@location-updated=${this._locationChanged}
|
||||
></ha-locations-editor>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
list="timezones"
|
||||
.disabled=${disabled}
|
||||
.value=${this._timeZoneValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${disabled}
|
||||
.value=${this._elevationValue}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
<span slot="suffix">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
</span>
|
||||
</paper-input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<ha-formfield
|
||||
.label=${html`${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="metric"
|
||||
.checked=${this._unitSystemValue === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._working}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.imperial_example"
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="imperial"
|
||||
.checked=${this._unitSystemValue === "imperial"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._working}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}<br />
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
list="currencies"
|
||||
.disabled=${disabled}
|
||||
.value=${this._currencyValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
const tzInput = this.shadowRoot!.querySelector(
|
||||
"[name=timeZone]"
|
||||
) as PaperInputElement;
|
||||
tzInput.inputElement.appendChild(createTimezoneListEl());
|
||||
|
||||
const cInput = this.shadowRoot!.querySelector(
|
||||
"[name=currency]"
|
||||
) as PaperInputElement;
|
||||
cInput.inputElement.appendChild(createCurrencyListEl());
|
||||
}
|
||||
|
||||
private _markerLocation = memoizeOne(
|
||||
(
|
||||
lat: number,
|
||||
lng: number,
|
||||
location?: [number, number]
|
||||
): MarkerLocation[] => [
|
||||
{
|
||||
id: "location",
|
||||
latitude: location ? location[0] : lat,
|
||||
longitude: location ? location[1] : lng,
|
||||
location_editable: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
private get _currencyValue() {
|
||||
return this._currency !== undefined
|
||||
? this._currency
|
||||
: this.hass.config.currency;
|
||||
}
|
||||
|
||||
private get _elevationValue() {
|
||||
return this._elevation !== undefined
|
||||
? this._elevation
|
||||
: this.hass.config.elevation;
|
||||
}
|
||||
|
||||
private get _timeZoneValue() {
|
||||
return this._timeZone !== undefined
|
||||
? this._timeZone
|
||||
: this.hass.config.time_zone;
|
||||
}
|
||||
|
||||
private get _unitSystemValue() {
|
||||
return this._unitSystem !== undefined
|
||||
? this._unitSystem
|
||||
: this.hass.config.unit_system.temperature === UNIT_C
|
||||
? "metric"
|
||||
: "imperial";
|
||||
}
|
||||
|
||||
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
let value = target.value;
|
||||
|
||||
if (target.name === "currency" && value) {
|
||||
if (value in SYMBOL_TO_ISO) {
|
||||
value = SYMBOL_TO_ISO[value];
|
||||
}
|
||||
}
|
||||
|
||||
this[`_${target.name}`] = value;
|
||||
}
|
||||
|
||||
private _locationChanged(ev) {
|
||||
this._location = ev.detail.location;
|
||||
}
|
||||
|
||||
private _unitSystemChanged(ev: CustomEvent) {
|
||||
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
this._working = true;
|
||||
try {
|
||||
const location = this._location || [
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude,
|
||||
];
|
||||
await saveCoreConfig(this.hass, {
|
||||
latitude: location[0],
|
||||
longitude: location[1],
|
||||
currency: this._currencyValue,
|
||||
elevation: Number(this._elevationValue),
|
||||
unit_system: this._unitSystemValue,
|
||||
time_zone: this._timeZoneValue,
|
||||
});
|
||||
} catch (err: any) {
|
||||
alert(`Error saving config: ${err.message}`);
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 -8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.row > * {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-core-form": ConfigCoreForm;
|
||||
}
|
||||
}
|
57
src/panels/config/core/ha-config-core.js
Normal file
57
src/panels/config/core/ha-config-core.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
import "./ha-config-core-form";
|
||||
import "./ha-config-name-form";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaConfigCore extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ha-config-name-form,
|
||||
ha-config-core-form {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<hass-subpage
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
header="[[localize('ui.panel.config.core.caption')]]"
|
||||
back-path="/config/system"
|
||||
>
|
||||
<div class="content">
|
||||
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
|
||||
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
narrow: Boolean,
|
||||
showAdvanced: Boolean,
|
||||
route: Object,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-core", HaConfigCore);
|
97
src/panels/config/core/ha-config-name-form.ts
Normal file
97
src/panels/config/core/ha-config-name-form.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-card";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
|
||||
@customElement("ha-config-name-form")
|
||||
class ConfigNameForm extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _working = false;
|
||||
|
||||
@state() private _name!: ConfigUpdateValues["location_name"];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._working || !canEdit;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.location_name"
|
||||
)}
|
||||
.disabled=${disabled}
|
||||
.value=${this._nameValue}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _nameValue() {
|
||||
return this._name !== undefined
|
||||
? this._name
|
||||
: this.hass.config.location_name;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
const target = ev.currentTarget as HaTextField;
|
||||
this._name = target.value;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
this._working = true;
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
location_name: this._nameValue,
|
||||
});
|
||||
} catch (err: any) {
|
||||
alert("FAIL");
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-name-form": ConfigNameForm;
|
||||
}
|
||||
}
|
@@ -40,7 +40,7 @@ class ConfigNetwork extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card outlined header="Network Adapter">
|
||||
<ha-card outlined header="Network">
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`
|
@@ -33,11 +33,6 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-config-analytics {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,313 +0,0 @@
|
||||
import timezones from "google-timezones-json";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { HaProgressButton } from "../../../components/buttons/ha-progress-button";
|
||||
import { currencies } from "../../../components/currency-datalist";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
import "../../../components/ha-settings-row";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { SYMBOL_TO_ISO } from "../../../data/currency";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-config-section-general")
|
||||
class HaConfigSectionGeneral extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
|
||||
@state() private _currency?: string;
|
||||
|
||||
@state() private _name?: string;
|
||||
|
||||
@state() private _elevation?: number;
|
||||
|
||||
@state() private _timeZone?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._submitting || !canEdit;
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.core.caption")}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.location_name"
|
||||
)}
|
||||
.disabled=${disabled}
|
||||
.value=${this._name}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.disabled=${disabled}
|
||||
.value=${this._timeZone}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${Object.keys(timezones).map(
|
||||
(tz) =>
|
||||
html`<mwc-list-item value=${tz}
|
||||
>${timezones[tz]}</mwc-list-item
|
||||
>`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${disabled}
|
||||
.value=${this._elevation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
<span slot="suffix">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
</span>
|
||||
</ha-textfield>
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="metric"
|
||||
.checked=${this._unitSystem === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.imperial_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="imperial"
|
||||
.checked=${this._unitSystem === "imperial"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<div>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.disabled=${disabled}
|
||||
.value=${this._currency}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${currencies.map(
|
||||
(currency) =>
|
||||
html`<mwc-list-item .value=${currency}
|
||||
>${currency}</mwc-list-item
|
||||
>`
|
||||
)}</ha-select
|
||||
>
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<ha-settings-row>
|
||||
<div slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_location"
|
||||
)}
|
||||
</div>
|
||||
<div slot="description" class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_location_description"
|
||||
)}
|
||||
</div>
|
||||
<mwc-button @click=${this._editLocation}
|
||||
>${this.hass.localize("ui.common.edit")}</mwc-button
|
||||
>
|
||||
</ha-settings-row>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button @click=${this._updateEntry}>
|
||||
${this.hass!.localize("ui.panel.config.zone.detail.update")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._unitSystem =
|
||||
this.hass.config.unit_system.temperature === UNIT_C
|
||||
? "metric"
|
||||
: "imperial";
|
||||
this._currency = this.hass.config.currency;
|
||||
this._elevation = this.hass.config.elevation;
|
||||
this._timeZone = this.hass.config.time_zone;
|
||||
this._name = this.hass.config.location_name;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
const target = ev.currentTarget;
|
||||
let value = target.value;
|
||||
|
||||
if (target.name === "currency" && value) {
|
||||
if (value in SYMBOL_TO_ISO) {
|
||||
value = SYMBOL_TO_ISO[value];
|
||||
}
|
||||
}
|
||||
|
||||
this[`_${target.name}`] = value;
|
||||
}
|
||||
|
||||
private _unitSystemChanged(ev: CustomEvent) {
|
||||
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
|
||||
}
|
||||
|
||||
private async _updateEntry(ev) {
|
||||
const button = ev.target as HaProgressButton;
|
||||
if (button.progress) {
|
||||
return;
|
||||
}
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
currency: this._currency,
|
||||
elevation: Number(this._elevation),
|
||||
unit_system: this._unitSystem,
|
||||
time_zone: this._timeZone,
|
||||
location_name: this._name,
|
||||
});
|
||||
button.actionSuccess();
|
||||
} catch (err: any) {
|
||||
button.actionError();
|
||||
alert(`Error saving config: ${err.message}`);
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _editLocation() {
|
||||
navigate("/config/zone");
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.card-content > * {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-select {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-general": HaConfigSectionGeneral;
|
||||
}
|
||||
}
|
@@ -1,12 +1,9 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "./ha-config-network";
|
||||
import "./ha-config-url-form";
|
||||
import "./supervisor-hostname";
|
||||
import "./supervisor-network";
|
||||
|
||||
@customElement("ha-config-section-network")
|
||||
class HaConfigSectionNetwork extends LitElement {
|
||||
@@ -25,13 +22,6 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
.header=${this.hass.localize("ui.panel.config.network.caption")}
|
||||
>
|
||||
<div class="content">
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
? html`<supervisor-hostname
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></supervisor-hostname>
|
||||
<supervisor-network .hass=${this.hass}></supervisor-network>`
|
||||
: ""}
|
||||
<ha-config-url-form .hass=${this.hass}></ha-config-url-form>
|
||||
<ha-config-network .hass=${this.hass}></ha-config-network>
|
||||
</div>
|
||||
@@ -45,14 +35,9 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
supervisor-hostname,
|
||||
supervisor-network,
|
||||
ha-config-url-form,
|
||||
ha-config-network {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 24px;
|
||||
max-width: 600px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
40
src/panels/config/core/ha-config-section-storage.ts
Normal file
40
src/panels/config/core/ha-config-section-storage.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "./ha-config-analytics";
|
||||
|
||||
@customElement("ha-config-section-storage")
|
||||
class HaConfigSectionStorage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<div class="content"></div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-storage": HaConfigSectionStorage;
|
||||
}
|
||||
}
|
@@ -1,208 +0,0 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-metric";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
fetchHassioSupervisorInfo,
|
||||
HassioSupervisorInfo,
|
||||
reloadSupervisor,
|
||||
setSupervisorOption,
|
||||
SupervisorOptions,
|
||||
} from "../../../data/hassio/supervisor";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
} from "../../../data/update";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../dashboard/ha-config-updates";
|
||||
|
||||
@customElement("ha-config-section-updates")
|
||||
class HaConfigSectionUpdates extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _showSkipped = false;
|
||||
|
||||
@state() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
fetchHassioSupervisorInfo(this.hass).then((data) => {
|
||||
this._supervisorInfo = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
);
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.updates.caption")}
|
||||
>
|
||||
<div slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.updates.check_updates"
|
||||
)}
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item id="skipped">
|
||||
${this._showSkipped
|
||||
? this.hass.localize("ui.panel.config.updates.hide_skipped")
|
||||
: this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</mwc-list-item>
|
||||
${this._supervisorInfo?.channel !== "dev"
|
||||
? html`
|
||||
<mwc-list-item id="beta">
|
||||
${this._supervisorInfo?.channel === "stable"
|
||||
? this.hass.localize("ui.panel.config.updates.join_beta")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${canInstallUpdates.length
|
||||
? html`
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
`
|
||||
: html`
|
||||
${this.hass.localize("ui.panel.config.updates.no_updates")}
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._showSkipped = !this._showSkipped;
|
||||
break;
|
||||
case 1:
|
||||
this._toggleBeta();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _toggleBeta(): Promise<void> {
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.join_beta_channel.title"),
|
||||
text: html`${this.hass.localize("ui.dialogs.join_beta_channel.warning")}
|
||||
<br />
|
||||
<b> ${this.hass.localize("ui.dialogs.join_beta_channel.backup")} </b>
|
||||
<br /><br />
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.release_items")}
|
||||
<ul>
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
</ul>
|
||||
<br />
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.confirm")}`,
|
||||
confirmText: this.hass.localize("ui.panel.config.updates.join_beta"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data: Partial<SupervisorOptions> = {
|
||||
channel: this._supervisorInfo!.channel === "stable" ? "beta" : "stable",
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await reloadSupervisor(this.hass);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkUpdates(): Promise<void> {
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
}
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean) =>
|
||||
filterUpdateEntitiesWithInstall(entities, showSkipped)
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-updates": HaConfigSectionUpdates;
|
||||
}
|
||||
}
|
@@ -1,12 +1,8 @@
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { canShowPage } from "../../../common/config/can_show_page";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-navigation-list";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -27,45 +23,29 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const pages = configSections.general
|
||||
.filter((page) => canShowPage(this.hass, page))
|
||||
.map((page) => ({
|
||||
...page,
|
||||
name: page.translationKey
|
||||
? this.hass.localize(page.translationKey)
|
||||
: page.name,
|
||||
}));
|
||||
const pages = configSections.general.map((page) => ({
|
||||
...page,
|
||||
name: page.translationKey
|
||||
? this.hass.localize(page.translationKey)
|
||||
: page.name,
|
||||
}));
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize("ui.panel.config.dashboard.system.main")}
|
||||
.header=${this.hass.localize("ui.panel.config.dashboard.system.title")}
|
||||
>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_dashboard.restart_homeassistant"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
<ha-config-section
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
full-width
|
||||
>
|
||||
<ha-card outlined>
|
||||
<ha-card>
|
||||
${this.narrow
|
||||
? html`<div class="title">
|
||||
${this.hass.localize("ui.panel.config.dashboard.system.main")}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.dashboard.system.title"
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
<ha-navigation-list
|
||||
@@ -79,25 +59,13 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.system_dashboard.confirm_restart"
|
||||
),
|
||||
confirm: () => {
|
||||
this.hass.callService("homeassistant", "restart");
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-card {
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
:host(:not([narrow])) ha-card {
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
@@ -110,8 +78,6 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
ha-card a {
|
||||
@@ -136,7 +102,6 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
|
||||
ha-navigation-list {
|
||||
--navigation-list-item-title-font-size: 16px;
|
||||
--navigation-list-item-padding: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js";
|
||||
import { mdiCloudLock, mdiDotsVertical, mdiMagnify, mdiNewBox } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -23,17 +25,15 @@ import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tip";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
UpdateEntity,
|
||||
} from "../../../data/update";
|
||||
import { updateCanInstall, UpdateEntity } from "../../../data/update";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import "../../../layouts/ha-app-layout";
|
||||
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-config-navigation";
|
||||
@@ -81,12 +81,12 @@ const randomTip = (hass: HomeAssistant) => {
|
||||
rel="noreferrer"
|
||||
>Newsletter</a
|
||||
>
|
||||
</span>`
|
||||
<ha-svg-icon class="new" .path=${mdiNewBox}></ha-svg-icon
|
||||
></span>`
|
||||
),
|
||||
weight: 2,
|
||||
},
|
||||
{ content: hass.localize("ui.tips.key_c_hint"), weight: 1 },
|
||||
{ content: hass.localize("ui.tips.key_m_hint"), weight: 1 },
|
||||
];
|
||||
|
||||
tips.forEach((tip) => {
|
||||
@@ -113,6 +113,8 @@ class HaConfigDashboard extends LitElement {
|
||||
|
||||
@state() private _tip?: string;
|
||||
|
||||
private _notifyUpdates = false;
|
||||
|
||||
private _pages = memoizeOne((clouStatus, isLoaded) => {
|
||||
const pages: PageNavigation[] = [];
|
||||
if (clouStatus && isLoaded) {
|
||||
@@ -129,8 +131,9 @@ class HaConfigDashboard extends LitElement {
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const [canInstallUpdates, totalUpdates] =
|
||||
this._filterUpdateEntitiesWithInstall(this.hass.states);
|
||||
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
|
||||
this.hass.states
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-app-layout>
|
||||
@@ -170,26 +173,15 @@ class HaConfigDashboard extends LitElement {
|
||||
full-width
|
||||
>
|
||||
${canInstallUpdates.length
|
||||
? html`<ha-card outlined>
|
||||
? html`<ha-card>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.total=${totalUpdates}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
></ha-config-updates>
|
||||
${totalUpdates > canInstallUpdates.length
|
||||
? html`<a class="button" href="/config/updates">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.more_updates",
|
||||
{
|
||||
count: totalUpdates - canInstallUpdates.length,
|
||||
}
|
||||
)}
|
||||
</a>`
|
||||
: ""}
|
||||
</ha-card>`
|
||||
: ""}
|
||||
<ha-card outlined>
|
||||
<ha-card>
|
||||
${this.narrow && canInstallUpdates.length
|
||||
? html`<div class="title">
|
||||
${this.hass.localize("panel.config")}
|
||||
@@ -217,17 +209,60 @@ class HaConfigDashboard extends LitElement {
|
||||
if (!this._tip && changedProps.has("hass")) {
|
||||
this._tip = randomTip(this.hass);
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass") || !this._notifyUpdates) {
|
||||
return;
|
||||
}
|
||||
this._notifyUpdates = false;
|
||||
if (this._filterUpdateEntitiesWithInstall(this.hass.states).length) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.updates.updates_refreshed"
|
||||
),
|
||||
});
|
||||
} else {
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.config.updates.no_new_updates"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities): [UpdateEntity[], number] => {
|
||||
const updates = filterUpdateEntitiesWithInstall(entities);
|
||||
private _filterUpdateEntities = memoizeOne((entities: HassEntities) =>
|
||||
(
|
||||
Object.values(entities).filter(
|
||||
(entity) => computeStateDomain(entity) === "update"
|
||||
) as UpdateEntity[]
|
||||
).sort((a, b) => {
|
||||
if (a.attributes.title === "Home Assistant Core") {
|
||||
return -3;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Core") {
|
||||
return 3;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Operating System") {
|
||||
return -2;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Operating System") {
|
||||
return 2;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Supervisor") {
|
||||
return -1;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Supervisor") {
|
||||
return 1;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.attributes.title || a.attributes.friendly_name || "",
|
||||
b.attributes.title || b.attributes.friendly_name || ""
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return [
|
||||
updates.slice(0, updates.length === 3 ? updates.length : 2),
|
||||
updates.length,
|
||||
];
|
||||
}
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities) =>
|
||||
this._filterUpdateEntities(entities).filter((entity) =>
|
||||
updateCanInstall(entity)
|
||||
)
|
||||
);
|
||||
|
||||
private _showQuickBar(): void {
|
||||
@@ -238,9 +273,27 @@ class HaConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
const _entities = this._filterUpdateEntities(this.hass.states).map(
|
||||
(entity) => entity.entity_id
|
||||
);
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
if (_entities.length) {
|
||||
this._notifyUpdates = true;
|
||||
await this.hass.callService("homeassistant", "update_entity", {
|
||||
entity_id: _entities,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.updates.no_update_entities.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.updates.no_update_entities.description"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -267,11 +320,6 @@ class HaConfigDashboard extends LitElement {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
a.button {
|
||||
display: block;
|
||||
color: var(--primary-color);
|
||||
padding: 16px;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
padding: 16px;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { canShowPage } from "../../../common/config/can_show_page";
|
||||
import "../../../components/ha-card";
|
||||
@@ -30,7 +30,7 @@ class HaConfigNavigation extends LitElement {
|
||||
name:
|
||||
page.name ||
|
||||
this.hass.localize(
|
||||
`ui.panel.config.dashboard.${page.translationKey}.main`
|
||||
`ui.panel.config.dashboard.${page.translationKey}.title`
|
||||
),
|
||||
description:
|
||||
page.component === "cloud" && (page.info as CloudStatus)
|
||||
@@ -51,7 +51,7 @@ class HaConfigNavigation extends LitElement {
|
||||
${
|
||||
page.description ||
|
||||
this.hass.localize(
|
||||
`ui.panel.config.dashboard.${page.translationKey}.secondary`
|
||||
`ui.panel.config.dashboard.${page.translationKey}.description`
|
||||
)
|
||||
}
|
||||
`,
|
||||
@@ -81,12 +81,6 @@ class HaConfigNavigation extends LitElement {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
ha-navigation-list {
|
||||
--navigation-list-item-title-font-size: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-icon-next";
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-config-updates")
|
||||
class HaConfigUpdates extends LitElement {
|
||||
@@ -19,60 +19,62 @@ class HaConfigUpdates extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public updateEntities?: UpdateEntity[];
|
||||
|
||||
@property({ type: Number })
|
||||
public total?: number;
|
||||
@state() private _showAll = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.updateEntities?.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const updates = this.updateEntities;
|
||||
const updates =
|
||||
this._showAll || this.updateEntities.length <= 3
|
||||
? this.updateEntities
|
||||
: this.updateEntities.slice(0, 2);
|
||||
|
||||
return html`
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: this.total || this.updateEntities.length,
|
||||
count: this.updateEntities.length,
|
||||
})}
|
||||
</div>
|
||||
<mwc-list>
|
||||
${updates.map(
|
||||
(entity) => html`
|
||||
<mwc-list-item
|
||||
twoline
|
||||
graphic="avatar"
|
||||
class=${entity.attributes.skipped_version ? "skipped" : ""}
|
||||
.entity_id=${entity.entity_id}
|
||||
.hasMeta=${!this.narrow}
|
||||
@click=${this._openMoreInfo}
|
||||
>
|
||||
${updates.map(
|
||||
(entity) => html`
|
||||
<paper-icon-item
|
||||
@click=${this._openMoreInfo}
|
||||
.entity_id=${entity.entity_id}
|
||||
>
|
||||
<span slot="item-icon" class="icon">
|
||||
<state-badge
|
||||
slot="graphic"
|
||||
.title=${entity.attributes.title ||
|
||||
entity.attributes.friendly_name}
|
||||
.stateObj=${entity}
|
||||
slot="item-icon"
|
||||
></state-badge>
|
||||
<span
|
||||
>${entity.attributes.title ||
|
||||
entity.attributes.friendly_name}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
</span>
|
||||
<paper-item-body two-line>
|
||||
${entity.attributes.title || entity.attributes.friendly_name}
|
||||
<div secondary>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.version_available",
|
||||
{
|
||||
version_available: entity.attributes.latest_version,
|
||||
}
|
||||
)}${entity.attributes.skipped_version
|
||||
? `(${this.hass.localize("ui.panel.config.updates.skipped")})`
|
||||
: ""}
|
||||
</span>
|
||||
${!this.narrow
|
||||
? html`<ha-icon-next slot="meta"></ha-icon-next>`
|
||||
: ""}
|
||||
</mwc-list-item>
|
||||
)}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
${!this._showAll && this.updateEntities.length >= 4
|
||||
? html`
|
||||
<button class="show-more" @click=${this._showAllClicked}>
|
||||
${this.hass.localize("ui.panel.config.updates.more_updates", {
|
||||
count: this.updateEntities!.length - updates.length,
|
||||
})}
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -82,19 +84,22 @@ class HaConfigUpdates extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _showAllClicked() {
|
||||
this._showAll = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup[] {
|
||||
return [
|
||||
css`
|
||||
:host {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.skipped {
|
||||
background: var(--secondary-background-color);
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
@@ -117,9 +122,8 @@ class HaConfigUpdates extends LitElement {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
mwc-list-item {
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -197,10 +197,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
model: device.model || "<unknown>",
|
||||
manufacturer: device.manufacturer || "<unknown>",
|
||||
area:
|
||||
device.area_id && areaLookup[device.area_id]
|
||||
? areaLookup[device.area_id].name
|
||||
: "—",
|
||||
area: device.area_id ? areaLookup[device.area_id].name : "—",
|
||||
integration: device.config_entries.length
|
||||
? device.config_entries
|
||||
.filter((entId) => entId in entryLookup)
|
||||
|
@@ -276,7 +276,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@selected=${this._deviceClassChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<mwc-list-item></mwc-list-item>
|
||||
${this._deviceClassesSorted(
|
||||
domain,
|
||||
this._deviceClassOptions[0],
|
||||
@@ -356,25 +355,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
)}
|
||||
</ha-select>`
|
||||
: ""}
|
||||
${this._helperConfigEntry
|
||||
? html`
|
||||
<div class="row">
|
||||
<mwc-button
|
||||
@click=${this._showOptionsFlow}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state",
|
||||
"integration",
|
||||
domainToName(
|
||||
this.hass.localize,
|
||||
this._helperConfigEntry.domain
|
||||
)
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
error-message="Domain needs to stay the same"
|
||||
.value=${this._entityId}
|
||||
@@ -392,6 +372,20 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>`
|
||||
: ""}
|
||||
${this._helperConfigEntry
|
||||
? html`
|
||||
<div class="row">
|
||||
<mwc-button
|
||||
@click=${this._showOptionsFlow}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
|
@@ -4,15 +4,14 @@ import {
|
||||
mdiBadgeAccountHorizontal,
|
||||
mdiCellphoneCog,
|
||||
mdiCog,
|
||||
mdiDatabase,
|
||||
mdiCpu32Bit,
|
||||
mdiDevices,
|
||||
mdiHeart,
|
||||
mdiHomeAssistant,
|
||||
mdiInformation,
|
||||
mdiInformationOutline,
|
||||
mdiLightningBolt,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMathLog,
|
||||
mdiMemory,
|
||||
mdiNetwork,
|
||||
mdiNfcVariant,
|
||||
mdiPalette,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
mdiPuzzle,
|
||||
mdiRobot,
|
||||
mdiScriptText,
|
||||
mdiServer,
|
||||
mdiShape,
|
||||
mdiSofa,
|
||||
mdiTools,
|
||||
@@ -68,6 +68,13 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
iconColor: "#E48629",
|
||||
components: ["zone"],
|
||||
},
|
||||
{
|
||||
path: "/config/backup",
|
||||
translationKey: "backup",
|
||||
iconPath: mdiBackupRestore,
|
||||
iconColor: "#4084CD",
|
||||
component: "backup",
|
||||
},
|
||||
{
|
||||
path: "/hassio",
|
||||
translationKey: "supervisor",
|
||||
@@ -93,7 +100,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
path: "/config/person",
|
||||
translationKey: "people",
|
||||
iconPath: mdiAccount,
|
||||
iconColor: "#832EA6",
|
||||
iconColor: "#E48629",
|
||||
components: ["person", "users"],
|
||||
},
|
||||
{
|
||||
@@ -255,39 +262,48 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
],
|
||||
general: [
|
||||
{
|
||||
path: "/config/updates",
|
||||
translationKey: "ui.panel.config.updates.caption",
|
||||
iconPath: mdiUpdate,
|
||||
iconColor: "#3B808E",
|
||||
component: "core",
|
||||
path: "/config/core",
|
||||
translationKey: "ui.panel.config.core.caption",
|
||||
iconPath: mdiHomeAssistant,
|
||||
iconColor: "#4A5963",
|
||||
core: true,
|
||||
},
|
||||
{
|
||||
component: "server_control",
|
||||
path: "/config/server_control",
|
||||
translationKey: "ui.panel.config.server_control.caption",
|
||||
iconPath: mdiServer,
|
||||
iconColor: "#4A5963",
|
||||
core: true,
|
||||
},
|
||||
{
|
||||
component: "logs",
|
||||
path: "/config/logs",
|
||||
translationKey: "ui.panel.config.logs.caption",
|
||||
iconPath: mdiMathLog,
|
||||
iconColor: "#C65326",
|
||||
iconColor: "#4A5963",
|
||||
core: true,
|
||||
},
|
||||
{
|
||||
path: "/config/backup",
|
||||
translationKey: "ui.panel.config.backup.caption",
|
||||
iconPath: mdiBackupRestore,
|
||||
iconColor: "#0D47A1",
|
||||
iconColor: "#4084CD",
|
||||
component: "backup",
|
||||
},
|
||||
{
|
||||
path: "/hassio/backups",
|
||||
translationKey: "ui.panel.config.backup.caption",
|
||||
iconPath: mdiBackupRestore,
|
||||
iconColor: "#0D47A1",
|
||||
component: "hassio",
|
||||
},
|
||||
{
|
||||
path: "/config/analytics",
|
||||
translationKey: "ui.panel.config.analytics.caption",
|
||||
iconPath: mdiShape,
|
||||
iconColor: "#f1c447",
|
||||
},
|
||||
{
|
||||
path: "/config/hardware",
|
||||
translationKey: "ui.panel.config.hardware.caption",
|
||||
iconPath: mdiCpu32Bit,
|
||||
iconColor: "#4A5963",
|
||||
},
|
||||
{
|
||||
path: "/config/network",
|
||||
translationKey: "ui.panel.config.network.caption",
|
||||
@@ -297,30 +313,14 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
{
|
||||
path: "/config/storage",
|
||||
translationKey: "ui.panel.config.storage.caption",
|
||||
iconPath: mdiDatabase,
|
||||
iconPath: mdiServer,
|
||||
iconColor: "#518C43",
|
||||
component: "hassio",
|
||||
},
|
||||
{
|
||||
path: "/config/hardware",
|
||||
translationKey: "ui.panel.config.hardware.caption",
|
||||
iconPath: mdiMemory,
|
||||
iconColor: "#301A8E",
|
||||
component: "hassio",
|
||||
},
|
||||
{
|
||||
path: "/config/system_health",
|
||||
translationKey: "ui.panel.config.system_health.caption",
|
||||
iconPath: mdiHeart,
|
||||
iconColor: "#507FfE",
|
||||
components: ["system_health", "hassio"],
|
||||
},
|
||||
{
|
||||
path: "/config/general",
|
||||
translationKey: "ui.panel.config.core.caption",
|
||||
iconPath: mdiCog,
|
||||
iconColor: "#653249",
|
||||
core: true,
|
||||
path: "/config/update",
|
||||
translationKey: "ui.panel.config.updates.caption",
|
||||
iconPath: mdiUpdate,
|
||||
iconColor: "#4A5963",
|
||||
},
|
||||
],
|
||||
about: [
|
||||
@@ -374,6 +374,10 @@ class HaPanelConfig extends HassRouterPage {
|
||||
tag: "ha-config-cloud",
|
||||
load: () => import("./cloud/ha-config-cloud"),
|
||||
},
|
||||
core: {
|
||||
tag: "ha-config-core",
|
||||
load: () => import("./core/ha-config-core"),
|
||||
},
|
||||
devices: {
|
||||
tag: "ha-config-devices",
|
||||
load: () => import("./devices/ha-config-devices"),
|
||||
@@ -404,10 +408,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
tag: "ha-config-energy",
|
||||
load: () => import("./energy/ha-config-energy"),
|
||||
},
|
||||
hardware: {
|
||||
tag: "ha-config-hardware",
|
||||
load: () => import("./hardware/ha-config-hardware"),
|
||||
},
|
||||
integrations: {
|
||||
tag: "ha-config-integrations",
|
||||
load: () => import("./integrations/ha-config-integrations"),
|
||||
@@ -418,7 +418,7 @@ class HaPanelConfig extends HassRouterPage {
|
||||
},
|
||||
network: {
|
||||
tag: "ha-config-section-network",
|
||||
load: () => import("./network/ha-config-section-network"),
|
||||
load: () => import("./core/ha-config-section-network"),
|
||||
},
|
||||
person: {
|
||||
tag: "ha-config-person",
|
||||
@@ -436,17 +436,13 @@ class HaPanelConfig extends HassRouterPage {
|
||||
tag: "ha-config-helpers",
|
||||
load: () => import("./helpers/ha-config-helpers"),
|
||||
},
|
||||
server_control: {
|
||||
tag: "ha-config-server-control",
|
||||
load: () => import("./server_control/ha-config-server-control"),
|
||||
},
|
||||
storage: {
|
||||
tag: "ha-config-section-storage",
|
||||
load: () => import("./storage/ha-config-section-storage"),
|
||||
},
|
||||
system_health: {
|
||||
tag: "ha-config-system-health",
|
||||
load: () => import("./system-health/ha-config-system-health"),
|
||||
},
|
||||
updates: {
|
||||
tag: "ha-config-section-updates",
|
||||
load: () => import("./core/ha-config-section-updates"),
|
||||
load: () => import("./core/ha-config-section-storage"),
|
||||
},
|
||||
users: {
|
||||
tag: "ha-config-users",
|
||||
@@ -456,10 +452,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
tag: "ha-config-zone",
|
||||
load: () => import("./zone/ha-config-zone"),
|
||||
},
|
||||
general: {
|
||||
tag: "ha-config-section-general",
|
||||
load: () => import("./core/ha-config-section-general"),
|
||||
},
|
||||
zha: {
|
||||
tag: "zha-config-dashboard-router",
|
||||
load: () =>
|
||||
@@ -538,10 +530,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
"--app-header-border-bottom",
|
||||
"1px solid var(--divider-color)"
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--ha-card-border-radius",
|
||||
"var(--ha-config-card-border-radius, 8px)"
|
||||
);
|
||||
}
|
||||
|
||||
protected updatePageEl(el) {
|
||||
|
@@ -1,213 +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 "../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/search-input";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
fetchHassioHardwareInfo,
|
||||
HassioHardwareInfo,
|
||||
} from "../../../data/hassio/hardware";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { dump } from "../../../resources/js-yaml-dump";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
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) => stringCompare(a.name, b.name))
|
||||
);
|
||||
|
||||
@customElement("ha-dialog-hardware-available")
|
||||
class DialogHardwareAvailable extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _hardware?: HassioHardwareInfo;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
public async showDialog(): Promise<Promise<void>> {
|
||||
try {
|
||||
this._hardware = await fetchHassioHardwareInfo(this.hass);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.failed_to_get"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._hardware = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._hardware) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const devices = _filterDevices(
|
||||
this.hass.userData?.showAdvanced || false,
|
||||
this._hardware,
|
||||
(this._filter || "").toLowerCase()
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.title"
|
||||
)}
|
||||
>
|
||||
<div class="header" slot="heading">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.title"
|
||||
)}
|
||||
</h2>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
></ha-icon-button>
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.hass.localize("common.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.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.subsystem"
|
||||
)}:
|
||||
</span>
|
||||
<span>${device.subsystem}</span>
|
||||
</div>
|
||||
<div class="device-property">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.device_path"
|
||||
)}:
|
||||
</span>
|
||||
<code>${device.dev_path}</code>
|
||||
</div>
|
||||
${device.by_id
|
||||
? html`
|
||||
<div class="device-property">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.id"
|
||||
)}:
|
||||
</span>
|
||||
<code>${device.by_id}</code>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="attributes">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_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`
|
||||
ha-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: 8px 16px 0;
|
||||
display: block;
|
||||
}
|
||||
.device-property {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.attributes {
|
||||
margin-top: 12px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-hardware-available": DialogHardwareAvailable;
|
||||
}
|
||||
}
|
@@ -1,256 +0,0 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-settings-row";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../data/hassio/common";
|
||||
import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
rebootHost,
|
||||
shutdownHost,
|
||||
} from "../../../data/hassio/host";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
|
||||
|
||||
@customElement("ha-config-hardware")
|
||||
class HaConfigHardware extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _error?: { code: string; message: string };
|
||||
|
||||
@state() private _OSData?: HassioHassOSInfo;
|
||||
|
||||
@state() private _hostData?: HassioHostInfo;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
this._load();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.hardware.caption")}
|
||||
>
|
||||
${this._error
|
||||
? html`
|
||||
<ha-alert alert-type="error"
|
||||
>${this._error.message || this._error.code}</ha-alert
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this._OSData && this._hostData
|
||||
? html`
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.hardware.board"
|
||||
)}</span
|
||||
>
|
||||
<div slot="description">
|
||||
<span class="value">${this._OSData.board}</span>
|
||||
</div>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div class="buttons">
|
||||
${this._hostData.features.includes("reboot")
|
||||
? html`
|
||||
<ha-progress-button
|
||||
class="warning"
|
||||
@click=${this._hostReboot}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.reboot_host"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
${this._hostData.features.includes("shutdown")
|
||||
? html`
|
||||
<ha-progress-button
|
||||
class="warning"
|
||||
@click=${this._hostShutdown}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.shutdown_host"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<mwc-list-item
|
||||
.action=${"hardware"}
|
||||
@click=${this._openHardware}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.title"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
try {
|
||||
this._OSData = await fetchHassioHassOsInfo(this.hass);
|
||||
this._hostData = await fetchHassioHostInfo(this.hass);
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
}
|
||||
}
|
||||
|
||||
private async _openHardware() {
|
||||
showhardwareAvailableDialog(this);
|
||||
}
|
||||
|
||||
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.hardware.reboot_host"),
|
||||
text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"),
|
||||
confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"),
|
||||
dismissText: this.hass.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await rebootHost(this.hass);
|
||||
} catch (err: any) {
|
||||
// Ignore connection errors, these are all expected
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.hardware.failed_to_reboot_host"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _hostShutdown(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.hardware.shutdown_host_confirm"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
|
||||
dismissText: this.hass.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await shutdownHost(this.hass);
|
||||
} catch (err: any) {
|
||||
// Ignore connection errors, these are all expected
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.hardware.failed_to_shutdown_host"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
ha-button-menu {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-menu-min-width: 200px;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-hardware": HaConfigHardware;
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export const loadHardwareAvailableDialog = () =>
|
||||
import("./dialog-hardware-available");
|
||||
|
||||
export const showhardwareAvailableDialog = (element: HTMLElement): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-dialog-hardware-available",
|
||||
dialogImport: loadHardwareAvailableDialog,
|
||||
dialogParams: {},
|
||||
});
|
||||
};
|
@@ -79,11 +79,9 @@ class HaInputSelectForm extends LitElement {
|
||||
"ui.dialogs.helper_settings.generic.icon"
|
||||
)}
|
||||
></ha-icon-picker>
|
||||
<div class="header">
|
||||
${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.input_select.options"
|
||||
)}:
|
||||
</div>
|
||||
${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.input_select.options"
|
||||
)}:
|
||||
${this._options.length
|
||||
? this._options.map(
|
||||
(option, index) => html`
|
||||
@@ -208,10 +206,6 @@ class HaInputSelectForm extends LitElement {
|
||||
#option_input {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.header {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,19 +1,12 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { property } from "lit/decorators";
|
||||
import "../../../components/ha-logo-svg";
|
||||
import {
|
||||
fetchHassioHostInfo,
|
||||
fetchHassioHassOsInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
} from "../../../data/hassio/host";
|
||||
import { HassioInfo, fetchHassioInfo } from "../../../data/hassio/supervisor";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "./integrations-card";
|
||||
import "./system-health-card";
|
||||
|
||||
const JS_TYPE = __BUILD__;
|
||||
const JS_VERSION = __VERSION__;
|
||||
@@ -29,12 +22,6 @@ class HaConfigInfo extends LitElement {
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@state() private _hostInfo?: HassioHostInfo;
|
||||
|
||||
@state() private _osInfo?: HassioHassOSInfo;
|
||||
|
||||
@state() private _hassioInfo?: HassioInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const hass = this.hass;
|
||||
const customUiList: Array<{ name: string; url: string; version: string }> =
|
||||
@@ -61,19 +48,7 @@ class HaConfigInfo extends LitElement {
|
||||
</ha-logo-svg>
|
||||
</a>
|
||||
<br />
|
||||
<h2>Home Assistant Core ${hass.connection.haVersion}</h2>
|
||||
${this._hassioInfo
|
||||
? html`<h2>
|
||||
Home Assistant Supervisor ${this._hassioInfo.supervisor}
|
||||
</h2>`
|
||||
: ""}
|
||||
${this._osInfo?.version
|
||||
? html`<h2>Home Assistant OS ${this._osInfo.version}</h2>`
|
||||
: ""}
|
||||
${this._hostInfo
|
||||
? html`<h4>Kernel version ${this._hostInfo.kernel}</h4>
|
||||
<h4>Agent version ${this._hostInfo.agent_version}</h4>`
|
||||
: ""}
|
||||
<h2>Home Assistant ${hass.connection.haVersion}</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.path_configuration",
|
||||
@@ -136,30 +111,33 @@ class HaConfigInfo extends LitElement {
|
||||
"type",
|
||||
JS_TYPE
|
||||
)}
|
||||
${customUiList.length > 0
|
||||
? html`
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.info.custom_uis")}
|
||||
${customUiList.map(
|
||||
(item) => html`
|
||||
<div>
|
||||
<a href=${item.url} target="_blank"> ${item.name}</a>:
|
||||
${item.version}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${
|
||||
customUiList.length > 0
|
||||
? html`
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.info.custom_uis")}
|
||||
${customUiList.map(
|
||||
(item) => html`
|
||||
<div>
|
||||
<a href=${item.url} target="_blank"> ${item.name}</a
|
||||
>: ${item.version}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<system-health-card .hass=${this.hass}></system-health-card>
|
||||
<integrations-card
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></integrations-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -173,22 +151,6 @@ class HaConfigInfo extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
this._loadSupervisorInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadSupervisorInfo(): Promise<void> {
|
||||
const [hostInfo, osInfo, hassioInfo] = await Promise.all([
|
||||
fetchHassioHostInfo(this.hass),
|
||||
fetchHassioHassOsInfo(this.hass),
|
||||
fetchHassioInfo(this.hass),
|
||||
]);
|
||||
|
||||
this._hassioInfo = hassioInfo;
|
||||
this._osInfo = osInfo;
|
||||
this._hostInfo = hostInfo;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -218,6 +180,7 @@ class HaConfigInfo extends LitElement {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
system-health-card,
|
||||
integrations-card {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
|
298
src/panels/config/info/system-health-card.ts
Normal file
298
src/panels/config/info/system-health-card.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
subscribeSystemHealthInfo,
|
||||
SystemCheckValueObject,
|
||||
SystemHealthInfo,
|
||||
} from "../../../data/system_health";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
const sortKeys = (a: string, b: string) => {
|
||||
if (a === "homeassistant") {
|
||||
return -1;
|
||||
}
|
||||
if (b === "homeassistant") {
|
||||
return 1;
|
||||
}
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (b < a) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
class SystemHealthCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _info?: SystemHealthInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
const sections: TemplateResult[] = [];
|
||||
|
||||
if (!this._info) {
|
||||
sections.push(
|
||||
html`
|
||||
<div class="loading-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
} else {
|
||||
const domains = Object.keys(this._info).sort(sortKeys);
|
||||
for (const domain of domains) {
|
||||
const domainInfo = this._info[domain];
|
||||
const keys: TemplateResult[] = [];
|
||||
|
||||
for (const key of Object.keys(domainInfo.info)) {
|
||||
let value: unknown;
|
||||
|
||||
if (
|
||||
domainInfo.info[key] &&
|
||||
typeof domainInfo.info[key] === "object"
|
||||
) {
|
||||
const info = domainInfo.info[key] as SystemCheckValueObject;
|
||||
|
||||
if (info.type === "pending") {
|
||||
value = html`
|
||||
<ha-circular-progress active size="tiny"></ha-circular-progress>
|
||||
`;
|
||||
} else if (info.type === "failed") {
|
||||
value = html`
|
||||
<span class="error">${info.error}</span>${!info.more_info
|
||||
? ""
|
||||
: html`
|
||||
–
|
||||
<a
|
||||
href=${info.more_info}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.system_health.more_info"
|
||||
)}
|
||||
</a>
|
||||
`}
|
||||
`;
|
||||
} else if (info.type === "date") {
|
||||
value = formatDateTime(new Date(info.value), this.hass.locale);
|
||||
}
|
||||
} else {
|
||||
value = domainInfo.info[key];
|
||||
}
|
||||
|
||||
keys.push(html`
|
||||
<tr>
|
||||
<td>
|
||||
${this.hass.localize(
|
||||
`component.${domain}.system_health.info.${key}`
|
||||
) || key}
|
||||
</td>
|
||||
<td>${value}</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
if (domain !== "homeassistant") {
|
||||
sections.push(
|
||||
html`
|
||||
<div class="card-header">
|
||||
<h3>${domainToName(this.hass.localize, domain)}</h3>
|
||||
${!domainInfo.manage_url
|
||||
? ""
|
||||
: html`
|
||||
<a class="manage" href=${domainInfo.manage_url}>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.system_health.manage"
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
sections.push(html`
|
||||
<table>
|
||||
${keys}
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<h1 class="card-header">
|
||||
<div class="card-header-text">
|
||||
${domainToName(this.hass.localize, "system_health")}
|
||||
</div>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
@action=${this._copyInfo}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.panel.config.info.copy_menu")}
|
||||
.path=${mdiContentCopy}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item>
|
||||
${this.hass.localize("ui.panel.config.info.copy_raw")}
|
||||
</mwc-list-item>
|
||||
<mwc-list-item>
|
||||
${this.hass.localize("ui.panel.config.info.copy_github")}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
</h1>
|
||||
<div class="card-content">${sections}</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this.hass!.loadBackendTranslation("system_health");
|
||||
|
||||
if (!isComponentLoaded(this.hass!, "system_health")) {
|
||||
this._info = {
|
||||
system_health: {
|
||||
info: {
|
||||
error: this.hass.localize(
|
||||
"ui.panel.config.info.system_health_error"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
subscribeSystemHealthInfo(this.hass!, (info) => {
|
||||
this._info = info;
|
||||
});
|
||||
}
|
||||
|
||||
private async _copyInfo(ev: CustomEvent<ActionDetail>): Promise<void> {
|
||||
const github = ev.detail.index === 1;
|
||||
let haContent: string | undefined;
|
||||
const domainParts: string[] = [];
|
||||
|
||||
for (const domain of Object.keys(this._info!).sort(sortKeys)) {
|
||||
const domainInfo = this._info![domain];
|
||||
let first = true;
|
||||
const parts = [
|
||||
`${
|
||||
github && domain !== "homeassistant"
|
||||
? `<details><summary>${domainToName(
|
||||
this.hass.localize,
|
||||
domain
|
||||
)}</summary>\n`
|
||||
: ""
|
||||
}`,
|
||||
];
|
||||
|
||||
for (const key of Object.keys(domainInfo.info)) {
|
||||
let value: unknown;
|
||||
|
||||
if (typeof domainInfo.info[key] === "object") {
|
||||
const info = domainInfo.info[key] as SystemCheckValueObject;
|
||||
|
||||
if (info.type === "pending") {
|
||||
value = "pending";
|
||||
} else if (info.type === "failed") {
|
||||
value = `failed to load: ${info.error}`;
|
||||
} else if (info.type === "date") {
|
||||
value = formatDateTime(new Date(info.value), this.hass.locale);
|
||||
}
|
||||
} else {
|
||||
value = domainInfo.info[key];
|
||||
}
|
||||
if (github && first) {
|
||||
parts.push(`${key} | ${value}\n-- | --`);
|
||||
first = false;
|
||||
} else {
|
||||
parts.push(`${key}${github ? " | " : ": "}${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "homeassistant") {
|
||||
haContent = parts.join("\n");
|
||||
} else {
|
||||
domainParts.push(parts.join("\n"));
|
||||
if (github && domain !== "homeassistant") {
|
||||
domainParts.push("</details>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await copyToClipboard(
|
||||
`${github ? "## " : ""}System Health\n${haContent}\n\n${domainParts.join(
|
||||
"\n\n"
|
||||
)}`
|
||||
);
|
||||
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
a.manage {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("system-health-card", SystemHealthCard);
|
@@ -111,7 +111,7 @@ const groupByIntegration = (
|
||||
class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@@ -709,9 +709,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host([narrow]) hass-tabs-subpage {
|
||||
--main-title-margin: 0;
|
||||
}
|
||||
ha-button-menu {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
@@ -214,9 +214,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
<br />
|
||||
<span>${item.property}</span>
|
||||
</span>
|
||||
<span slot="heading" .title=${item.metadata.label}>
|
||||
${item.metadata.label}
|
||||
</span>
|
||||
<span slot="heading">${item.metadata.label}</span>
|
||||
<span slot="description">
|
||||
${item.metadata.description}
|
||||
${item.metadata.description !== null && !item.metadata.writeable
|
||||
|
@@ -1,142 +1,98 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiRefresh } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-ansi-to-html";
|
||||
import "../../../components/ha-card";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-select";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import { fetchErrorLog } from "../../../data/error_log";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import { fetchHassioLogs } from "../../../data/hassio/supervisor";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("error-log-card")
|
||||
class ErrorLogCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public filter = "";
|
||||
|
||||
@property() public provider!: string;
|
||||
|
||||
@property({ type: Boolean, attribute: true }) public show = false;
|
||||
|
||||
@state() private _isLogLoaded = false;
|
||||
|
||||
@state() private _logHTML?: TemplateResult[] | TemplateResult | string;
|
||||
|
||||
@state() private _error?: string;
|
||||
@state() private _errorHTML!: TemplateResult[] | string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="error-log-intro">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this._logHTML
|
||||
${this._errorHTML
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.hass.localize("ui.panel.config.logs.full_logs")}
|
||||
</h2>
|
||||
<ha-icon-button
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._refresh}
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div class="card-content error-log">${this._logHTML}</div>
|
||||
<ha-card>
|
||||
<ha-icon-button
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._refreshErrorLog}
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
<div class="card-content error-log">${this._errorHTML}</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
${!this._logHTML
|
||||
? html`
|
||||
<mwc-button
|
||||
raised
|
||||
@click=${this._refreshLogs}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.logs.load_logs")}
|
||||
: html`
|
||||
<mwc-button raised @click=${this._refreshErrorLog}>
|
||||
${this.hass.localize("ui.panel.config.logs.load_full_log")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.hass?.config.safe_mode || this.show) {
|
||||
if (this.hass?.config.safe_mode) {
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
this._refreshLogs();
|
||||
this._refreshErrorLog();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("provider")) {
|
||||
this._logHTML = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
(changedProps.has("filter") && this._isLogLoaded) ||
|
||||
(changedProps.has("show") && this.show) ||
|
||||
(changedProps.has("provider") && this.show)
|
||||
) {
|
||||
this._refreshLogs();
|
||||
if (changedProps.has("filter") && this._isLogLoaded) {
|
||||
this._refreshErrorLog();
|
||||
}
|
||||
}
|
||||
|
||||
private async _refresh(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
await this._refreshLogs();
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _refreshLogs(): Promise<void> {
|
||||
this._logHTML = this.hass.localize("ui.panel.config.logs.loading_log");
|
||||
let log: string;
|
||||
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
try {
|
||||
log = await fetchHassioLogs(this.hass, this.provider);
|
||||
this._logHTML = html`<ha-ansi-to-html .content=${log}>
|
||||
</ha-ansi-to-html>`;
|
||||
this._isLogLoaded = true;
|
||||
return;
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.logs.failed_get_logs",
|
||||
"provider",
|
||||
this.provider,
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
return;
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.error-log-intro {
|
||||
text-align: center;
|
||||
margin: 16px;
|
||||
}
|
||||
} else {
|
||||
log = await fetchErrorLog(this.hass!);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.error-log {
|
||||
font-family: var(--code-font-family, monospace);
|
||||
clear: both;
|
||||
text-align: left;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.error-log > div:hover {
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private async _refreshErrorLog(): Promise<void> {
|
||||
this._errorHTML = this.hass.localize("ui.panel.config.logs.loading_log");
|
||||
const log = await fetchErrorLog(this.hass!);
|
||||
this._isLogLoaded = true;
|
||||
|
||||
this._logHTML = log
|
||||
this._errorHTML = log
|
||||
? log
|
||||
.split("\n")
|
||||
.filter((entry) => {
|
||||
@@ -163,61 +119,6 @@ class ErrorLogCard extends LitElement {
|
||||
})
|
||||
: this.hass.localize("ui.panel.config.logs.no_errors");
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
.error-log-intro {
|
||||
text-align: center;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ha-select {
|
||||
display: block;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.error-log {
|
||||
font-family: var(--code-font-family, monospace);
|
||||
clear: both;
|
||||
text-align: left;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.error-log > div {
|
||||
overflow: auto;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.error-log > div:hover {
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) mwc-button {
|
||||
direction: rtl;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"error-log-card": ErrorLogCard;
|
||||
}
|
||||
}
|
||||
customElements.define("error-log-card", ErrorLogCard);
|
||||
|
@@ -1,11 +1,7 @@
|
||||
import { mdiChevronDown } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/search-input";
|
||||
import { LogProvider } from "../../../data/error_log";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -14,51 +10,22 @@ import "./error-log-card";
|
||||
import "./system-log-card";
|
||||
import type { SystemLogCard } from "./system-log-card";
|
||||
|
||||
const logProviders: LogProvider[] = [
|
||||
{
|
||||
key: "core",
|
||||
name: "Home Assistant Core",
|
||||
},
|
||||
{
|
||||
key: "supervisor",
|
||||
name: "Supervisor",
|
||||
},
|
||||
{
|
||||
key: "host",
|
||||
name: "Host",
|
||||
},
|
||||
{
|
||||
key: "dns",
|
||||
name: "DNS",
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
name: "Audio",
|
||||
},
|
||||
{
|
||||
key: "multicast",
|
||||
name: "Multicast",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("ha-config-logs")
|
||||
export class HaConfigLogs extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
@property() public route!: Route;
|
||||
|
||||
@state() private _filter = extractSearchParam("filter") || "";
|
||||
|
||||
@query("system-log-card", true) private systemLog?: SystemLogCard;
|
||||
|
||||
@state() private _selectedLogProvider = "core";
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.systemLog && this.systemLog.loaded) {
|
||||
@@ -101,60 +68,21 @@ export class HaConfigLogs extends LitElement {
|
||||
.header=${this.hass.localize("ui.panel.config.logs.caption")}
|
||||
back-path="/config/system"
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio") &&
|
||||
this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<mwc-button
|
||||
slot="trigger"
|
||||
.label=${logProviders.find(
|
||||
(p) => p.key === this._selectedLogProvider
|
||||
)!.name}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailingIcon"
|
||||
.path=${mdiChevronDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-button>
|
||||
${logProviders.map(
|
||||
(provider) => html`
|
||||
<mwc-list-item
|
||||
?selected=${provider.key === this._selectedLogProvider}
|
||||
.provider=${provider.key}
|
||||
@click=${this._selectProvider}
|
||||
>
|
||||
${provider.name}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>
|
||||
`
|
||||
: ""}
|
||||
${search}
|
||||
<div class="content">
|
||||
${this._selectedLogProvider === "core"
|
||||
? html`
|
||||
<system-log-card
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
></system-log-card>
|
||||
`
|
||||
: ""}
|
||||
<system-log-card
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
></system-log-card>
|
||||
<error-log-card
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.provider=${this._selectedLogProvider}
|
||||
.show=${this._selectedLogProvider !== "core"}
|
||||
></error-log-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectProvider(ev) {
|
||||
this._selectedLogProvider = (ev.currentTarget as any).provider;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -180,11 +108,6 @@ export class HaConfigLogs extends LitElement {
|
||||
.content {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
mwc-button[slot="trigger"] {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
--mdc-icon-size: 36px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -18,7 +18,6 @@ import {
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
|
||||
import { formatSystemLogTime } from "./util";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
|
||||
@customElement("system-log-card")
|
||||
export class SystemLogCard extends LitElement {
|
||||
@@ -76,7 +75,7 @@ export class SystemLogCard extends LitElement {
|
||||
: [];
|
||||
return html`
|
||||
<div class="system-log-intro">
|
||||
<ha-card outlined>
|
||||
<ha-card>
|
||||
${this._items === undefined
|
||||
? html`
|
||||
<div class="loading-container">
|
||||
@@ -132,7 +131,7 @@ export class SystemLogCard extends LitElement {
|
||||
`
|
||||
)}
|
||||
|
||||
<div class="card-actions" dir=${computeRTLDirection(this.hass)}>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="system_log"
|
||||
@@ -205,10 +204,6 @@ export class SystemLogCard extends LitElement {
|
||||
.warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) .card-actions {
|
||||
direction: rtl;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiCheckCircleOutline,
|
||||
mdiDotsVertical,
|
||||
mdiOpenInNew,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/ha-clickable-list-item";
|
||||
import "../../../../components/ha-fab";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -218,7 +216,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
if (isComponentLoaded(this.hass, "energy")) {
|
||||
result.push({
|
||||
icon: "hass:lightning-bolt",
|
||||
title: this.hass.localize(`ui.panel.config.dashboard.energy.main`),
|
||||
title: this.hass.localize(`ui.panel.config.dashboard.energy.title`),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "energy",
|
||||
@@ -262,32 +260,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
hasFab
|
||||
clickable
|
||||
>
|
||||
${this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
activatable
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-clickable-list-item
|
||||
@click=${this._entryClicked}
|
||||
href="/config/lovelace/resources"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.resources.caption"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.lovelace.resources.caption"
|
||||
)}
|
||||
</ha-clickable-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: ""}
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize(
|
||||
@@ -382,8 +354,4 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _entryClicked(ev) {
|
||||
ev.currentTarget.blur();
|
||||
}
|
||||
}
|
||||
|
@@ -12,6 +12,13 @@ export const lovelaceTabs = [
|
||||
translationKey: "ui.panel.config.lovelace.dashboards.caption",
|
||||
icon: "hass:view-dashboard",
|
||||
},
|
||||
{
|
||||
component: "lovelace",
|
||||
path: "/config/lovelace/resources",
|
||||
translationKey: "ui.panel.config.lovelace.resources.caption",
|
||||
icon: "hass:file-multiple",
|
||||
advancedOnly: true,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("ha-config-lovelace")
|
||||
|
@@ -77,6 +77,7 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${lovelaceTabs}
|
||||
.columns=${this._columns(this.hass.language)}
|
||||
|
@@ -1,121 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-header-bar";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-related-items";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
changeHostOptions,
|
||||
fetchHassioHostInfo,
|
||||
} from "../../../data/hassio/host";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("supervisor-hostname")
|
||||
export class HassioHostname extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) narrow!: boolean;
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state() private _hostname?: string;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._fetchHostInfo();
|
||||
}
|
||||
|
||||
private async _fetchHostInfo() {
|
||||
const hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
this._hostname = hostInfo.hostname;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._hostname) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class="no-padding"
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.network.hostname.title")}
|
||||
>
|
||||
<div>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">Hostname</span>
|
||||
<span slot="description"
|
||||
>The name your instance will have on your network</span
|
||||
>
|
||||
<ha-textfield
|
||||
.disabled=${this._processing}
|
||||
.value=${this._hostname}
|
||||
@change=${this._handleChange}
|
||||
placeholder="homeassistant"
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${this._processing}>
|
||||
${this._processing
|
||||
? html`<ha-circular-progress active size="small">
|
||||
</ha-circular-progress>`
|
||||
: this.hass.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
this._hostname = ev.target.value;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
this._processing = true;
|
||||
try {
|
||||
await changeHostOptions(this.hass, { hostname: this._hostname });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.network.hostname.failed_to_set_hostname"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
this._processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-hostname": HassioHostname;
|
||||
}
|
||||
}
|
@@ -1,577 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-header-bar";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-related-items";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
AccessPoints,
|
||||
accesspointScan,
|
||||
fetchNetworkInfo,
|
||||
NetworkInterface,
|
||||
updateNetworkInterface,
|
||||
WifiConfiguration,
|
||||
} from "../../../data/hassio/network";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-card";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@customElement("supervisor-network")
|
||||
export class HassioNetwork extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _accessPoints?: AccessPoints;
|
||||
|
||||
@state() private _curTabIndex = 0;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _interface?: NetworkInterface;
|
||||
|
||||
@state() private _interfaces!: NetworkInterface[];
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state() private _scanning = false;
|
||||
|
||||
@state() private _wifiConfiguration?: WifiConfiguration;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._fetchNetworkInfo();
|
||||
}
|
||||
|
||||
private async _fetchNetworkInfo() {
|
||||
const network = await fetchNetworkInfo(this.hass);
|
||||
this._interfaces = network.interfaces.sort((a, b) =>
|
||||
a.primary > b.primary ? -1 : 1
|
||||
);
|
||||
this._interface = { ...this._interfaces[this._curTabIndex] };
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._interface) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.title"
|
||||
)}
|
||||
>
|
||||
${this._interfaces.length > 1
|
||||
? html`<mwc-tab-bar
|
||||
.activeIndex=${this._curTabIndex}
|
||||
@MDCTabBar:activated=${this._handleTabActivated}
|
||||
>${this._interfaces.map(
|
||||
(device) =>
|
||||
html`<mwc-tab
|
||||
.id=${device.interface}
|
||||
.label=${device.interface}
|
||||
>
|
||||
</mwc-tab>`
|
||||
)}
|
||||
</mwc-tab-bar>`
|
||||
: ""}
|
||||
${cache(this._renderTab())}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTab() {
|
||||
return html`<div class="card-content">
|
||||
${IP_VERSIONS.map((version) =>
|
||||
this._interface![version] ? this._renderIPConfiguration(version) : ""
|
||||
)}
|
||||
${this._interface?.type === "wireless"
|
||||
? html`
|
||||
<ha-expansion-panel header="Wi-Fi" outlined>
|
||||
${this._interface?.wifi?.ssid
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.connected_to",
|
||||
"ssid",
|
||||
this._interface?.wifi?.ssid
|
||||
)}
|
||||
</p>`
|
||||
: ""}
|
||||
<mwc-button
|
||||
class="scan"
|
||||
@click=${this._scanForAP}
|
||||
.disabled=${this._scanning}
|
||||
>
|
||||
${this._scanning
|
||||
? html`<ha-circular-progress active size="small">
|
||||
</ha-circular-progress>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.scan_ap"
|
||||
)}
|
||||
</mwc-button>
|
||||
${this._accessPoints &&
|
||||
this._accessPoints.accesspoints &&
|
||||
this._accessPoints.accesspoints.length !== 0
|
||||
? html`
|
||||
<mwc-list>
|
||||
${this._accessPoints.accesspoints
|
||||
.filter((ap) => ap.ssid)
|
||||
.map(
|
||||
(ap) =>
|
||||
html`
|
||||
<mwc-list-item
|
||||
twoline
|
||||
@click=${this._selectAP}
|
||||
.activated=${ap.ssid ===
|
||||
this._wifiConfiguration?.ssid}
|
||||
.ap=${ap}
|
||||
>
|
||||
<span>${ap.ssid}</span>
|
||||
<span slot="secondary">
|
||||
${ap.mac} - Strength: ${ap.signal}
|
||||
</span>
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: ""}
|
||||
${this._wifiConfiguration
|
||||
? html`
|
||||
<div class="radio-row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.open"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="open"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
undefined ||
|
||||
this._wifiConfiguration.auth === "open"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.wep"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wep"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth === "wep"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.wpa"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wpa-psk"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
"wpa-psk"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
type="password"
|
||||
id="psk"
|
||||
label="Password"
|
||||
version="wifi"
|
||||
@value-changed=${this
|
||||
._handleInputValueChangedWifi}
|
||||
>
|
||||
</paper-input>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: ""}
|
||||
${this._dirty
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.warning"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
|
||||
${this._processing
|
||||
? html`<ha-circular-progress active size="small">
|
||||
</ha-circular-progress>`
|
||||
: this.hass.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _selectAP(event) {
|
||||
this._wifiConfiguration = event.currentTarget.ap;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private async _scanForAP() {
|
||||
if (!this._interface) {
|
||||
return;
|
||||
}
|
||||
this._scanning = true;
|
||||
try {
|
||||
this._accessPoints = await accesspointScan(
|
||||
this.hass,
|
||||
this._interface.interface
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to scan for accesspoints",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
this._scanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderIPConfiguration(version: string) {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
.header=${`IPv${version.charAt(version.length - 1)}`}
|
||||
outlined
|
||||
>
|
||||
<div class="radio-row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.dhcp"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="auto"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "auto"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.static"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="static"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "static"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.disabled"
|
||||
)}
|
||||
class="warning"
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="disabled"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "disabled"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${this._interface![version].method === "static"
|
||||
? html`
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="address"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.ip_netmask"
|
||||
)}
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].address)}
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="gateway"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.gateway"
|
||||
)}
|
||||
.version=${version}
|
||||
.value=${this._interface![version].gateway}
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="nameservers"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.dns_servers"
|
||||
)}
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].nameservers)}
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
_toArray(data: string | string[]): string[] {
|
||||
if (Array.isArray(data)) {
|
||||
if (data && typeof data[0] === "string") {
|
||||
data = data[0];
|
||||
}
|
||||
}
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
if (typeof data === "string") {
|
||||
return data.replace(/ /g, "").split(",");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
_toString(data: string | string[]): string {
|
||||
if (!data) {
|
||||
return "";
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
return data.join(", ");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private async _updateNetwork() {
|
||||
this._processing = true;
|
||||
let interfaceOptions: Partial<NetworkInterface> = {};
|
||||
|
||||
IP_VERSIONS.forEach((version) => {
|
||||
interfaceOptions[version] = {
|
||||
method: this._interface![version]?.method || "auto",
|
||||
};
|
||||
if (this._interface![version]?.method === "static") {
|
||||
interfaceOptions[version] = {
|
||||
...interfaceOptions[version],
|
||||
address: this._toArray(this._interface![version]?.address),
|
||||
gateway: this._interface![version]?.gateway,
|
||||
nameservers: this._toArray(this._interface![version]?.nameservers),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (this._wifiConfiguration) {
|
||||
interfaceOptions = {
|
||||
...interfaceOptions,
|
||||
wifi: {
|
||||
ssid: this._wifiConfiguration.ssid,
|
||||
mode: this._wifiConfiguration.mode,
|
||||
auth: this._wifiConfiguration.auth || "open",
|
||||
},
|
||||
};
|
||||
if (interfaceOptions.wifi!.auth !== "open") {
|
||||
interfaceOptions.wifi = {
|
||||
...interfaceOptions.wifi,
|
||||
psk: this._wifiConfiguration.psk,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interfaceOptions.enabled =
|
||||
this._wifiConfiguration !== undefined ||
|
||||
interfaceOptions.ipv4?.method !== "disabled" ||
|
||||
interfaceOptions.ipv6?.method !== "disabled";
|
||||
|
||||
try {
|
||||
await updateNetworkInterface(
|
||||
this.hass,
|
||||
this._interface!.interface,
|
||||
interfaceOptions
|
||||
);
|
||||
this._dirty = false;
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.failed_to_change"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
this._processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
|
||||
if (this._dirty) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize("ui.panel.config.network.supervisor.unsaved"),
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
});
|
||||
if (!confirm) {
|
||||
this.requestUpdate("_interface");
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._curTabIndex = ev.detail.index;
|
||||
this._interface = { ...this._interfaces[ev.detail.index] };
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent): void {
|
||||
const value = (ev.target as any).value as "disabled" | "auto" | "static";
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._interface ||
|
||||
this._interface[version]!.method === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
|
||||
this._interface[version]!.method = value;
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _handleRadioValueChangedAp(ev: CustomEvent): void {
|
||||
const value = (ev.target as any).value as string as
|
||||
| "open"
|
||||
| "wep"
|
||||
| "wpa-psk";
|
||||
this._wifiConfiguration!.auth = value;
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_wifiConfiguration");
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(ev: CustomEvent): void {
|
||||
const value: string | null | undefined = (ev.target as PaperInputElement)
|
||||
.value;
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
const id = (ev.target as PaperInputElement).id;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._interface ||
|
||||
this._toString(this._interface[version]![id]) === this._toString(value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
this._interface[version]![id] = value;
|
||||
}
|
||||
|
||||
private _handleInputValueChangedWifi(ev: CustomEvent): void {
|
||||
const value: string | null | undefined = (ev.target as PaperInputElement)
|
||||
.value;
|
||||
const id = (ev.target as PaperInputElement).id;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._wifiConfiguration ||
|
||||
this._wifiConfiguration![id] === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
this._wifiConfiguration![id] = value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
mwc-tab-bar {
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
mwc-button.warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
mwc-button.scan {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
:host([rtl]) app-toolbar {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
paper-input {
|
||||
padding: 0 14px;
|
||||
}
|
||||
mwc-list-item {
|
||||
--mdc-list-side-padding: 10px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-network": HassioNetwork;
|
||||
}
|
||||
}
|
266
src/panels/config/server_control/ha-config-server-control.ts
Normal file
266
src/panels/config/server_control/ha-config-server-control.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { componentsWithService } from "../../../common/config/components_with_service";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import { checkCoreConfig } from "../../../data/core";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
|
||||
@customElement("ha-config-server-control")
|
||||
export class HaConfigServerControl extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
|
||||
@state() private _validating = false;
|
||||
|
||||
@state() private _reloadableDomains: string[] = [];
|
||||
|
||||
private _validateLog = "";
|
||||
|
||||
private _isValid: boolean | null = null;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
const oldHass = changedProperties.get("hass");
|
||||
if (
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass || oldHass.config.components !== this.hass.config.components)
|
||||
) {
|
||||
this._reloadableDomains = componentsWithService(
|
||||
this.hass,
|
||||
"reload"
|
||||
).sort();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.header=${this.hass.localize("ui.panel.config.server_control.caption")}
|
||||
>
|
||||
<div class="content">
|
||||
${this.showAdvanced
|
||||
? html`
|
||||
<ha-card
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.introduction"
|
||||
)}
|
||||
${!this._validateLog
|
||||
? html`
|
||||
<div
|
||||
class="validate-container layout vertical center-center"
|
||||
>
|
||||
${!this._validating
|
||||
? html`
|
||||
${this._isValid
|
||||
? html` <div
|
||||
class="validate-result"
|
||||
id="result"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.valid"
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
<mwc-button
|
||||
raised
|
||||
@click=${this._validateConfig}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.check_config"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<ha-circular-progress
|
||||
active
|
||||
></ha-circular-progress>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="config-invalid">
|
||||
<span class="text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.invalid"
|
||||
)}
|
||||
</span>
|
||||
<mwc-button raised @click=${this._validateConfig}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.check_config"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
<div id="configLog" class="validate-log">
|
||||
${this._validateLog}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-card
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.introduction"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions warning">
|
||||
<ha-call-service-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="restart"
|
||||
.confirmation=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.confirm_restart"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.restart"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
${this.showAdvanced
|
||||
? html`
|
||||
<ha-card
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.introduction"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="reload_core_config"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.core"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
${this._reloadableDomains.map(
|
||||
(domain) =>
|
||||
html`
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
service="reload"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.server_control.section.reloading.${domain}`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _validateConfig() {
|
||||
this._validating = true;
|
||||
this._validateLog = "";
|
||||
this._isValid = null;
|
||||
|
||||
const configCheck = await checkCoreConfig(this.hass);
|
||||
this._validating = false;
|
||||
this._isValid = configCheck.result === "valid";
|
||||
|
||||
if (configCheck.errors) {
|
||||
this._validateLog = configCheck.errors;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.validate-container {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.validate-result {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.config-invalid {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.config-invalid .text {
|
||||
color: var(--error-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-invalid mwc-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.validate-log {
|
||||
white-space: pre-line;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-server-control": HaConfigServerControl;
|
||||
}
|
||||
}
|
@@ -1,200 +0,0 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-select";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../data/hassio/common";
|
||||
import {
|
||||
DatadiskList,
|
||||
fetchHassioHassOsInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
listDatadisks,
|
||||
moveDatadisk,
|
||||
} from "../../../data/hassio/host";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { MoveDatadiskDialogParams } from "./show-dialog-move-datadisk";
|
||||
|
||||
const calculateMoveTime = memoizeOne((hostInfo: HassioHostInfo): number => {
|
||||
const speed = hostInfo.disk_life_time !== "" ? 30 : 10;
|
||||
const moveTime = (hostInfo.disk_used * 1000) / 60 / speed;
|
||||
const rebootTime = (hostInfo.startup_time * 4) / 60;
|
||||
return Math.ceil((moveTime + rebootTime) / 10) * 10;
|
||||
});
|
||||
|
||||
@customElement("dialog-move-datadisk")
|
||||
class MoveDatadiskDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _hostInfo?: HassioHostInfo;
|
||||
|
||||
@state() private _selectedDevice?: string;
|
||||
|
||||
@state() private _devices?: DatadiskList["devices"];
|
||||
|
||||
@state() private _osInfo?: HassioHassOSInfo;
|
||||
|
||||
@state() private _moving = false;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: MoveDatadiskDialogParams
|
||||
): Promise<Promise<void>> {
|
||||
this._hostInfo = dialogParams.hostInfo;
|
||||
|
||||
try {
|
||||
this._osInfo = await fetchHassioHassOsInfo(this.hass);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.failed_to_get"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
|
||||
listDatadisks(this.hass).then((data) => {
|
||||
this._devices = data.devices;
|
||||
});
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._selectedDevice = undefined;
|
||||
this._devices = undefined;
|
||||
this._moving = false;
|
||||
this._hostInfo = undefined;
|
||||
this._osInfo = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._hostInfo || !this._osInfo) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this._moving
|
||||
? this.hass.localize("ui.panel.config.storage.datadisk.moving")
|
||||
: this.hass.localize("ui.panel.config.storage.datadisk.title")}
|
||||
@closed=${this.closeDialog}
|
||||
?hideActions=${this._moving}
|
||||
>
|
||||
${this._moving
|
||||
? html` <ha-circular-progress alt="Moving" size="large" active>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.moving_desc"
|
||||
)}
|
||||
</p>`
|
||||
: html` ${this._devices?.length
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.description",
|
||||
{
|
||||
current_path: this._osInfo.data_disk,
|
||||
time: calculateMoveTime(this._hostInfo),
|
||||
}
|
||||
)}
|
||||
<br /><br />
|
||||
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.select_device"
|
||||
)}
|
||||
@selected=${this._select_device}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this._devices.map(
|
||||
(device) =>
|
||||
html`<mwc-list-item .value=${device}
|
||||
>${device}</mwc-list-item
|
||||
>`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: this._devices === undefined
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.loading_devices"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.no_devices"
|
||||
)}
|
||||
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.storage.datadisk.cancel")}
|
||||
</mwc-button>
|
||||
|
||||
<mwc-button
|
||||
.disabled=${!this._selectedDevice}
|
||||
slot="primaryAction"
|
||||
@click=${this._moveDatadisk}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.storage.datadisk.move")}
|
||||
</mwc-button>`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _select_device(ev) {
|
||||
this._selectedDevice = ev.target.value;
|
||||
}
|
||||
|
||||
private async _moveDatadisk() {
|
||||
this._moving = true;
|
||||
try {
|
||||
await moveDatadisk(this.hass, this._selectedDevice!);
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.failed_to_move"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-move-datadisk": MoveDatadiskDialog;
|
||||
}
|
||||
}
|
@@ -1,149 +0,0 @@
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-metric";
|
||||
import { fetchHassioHostInfo, HassioHostInfo } from "../../../data/hassio/host";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import {
|
||||
getValueInPercentage,
|
||||
roundWithOneDecimal,
|
||||
} from "../../../util/calculate";
|
||||
import "../core/ha-config-analytics";
|
||||
import { showMoveDatadiskDialog } from "./show-dialog-move-datadisk";
|
||||
|
||||
@customElement("ha-config-section-storage")
|
||||
class HaConfigSectionStorage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _error?: { code: string; message: string };
|
||||
|
||||
@state() private _hostInfo?: HassioHostInfo;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
this._load();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.storage.caption")}
|
||||
>
|
||||
${this._hostInfo
|
||||
? html`
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.overflow")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item @click=${this._moveDatadisk}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.title"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
${this._error
|
||||
? html`
|
||||
<ha-alert alert-type="error"
|
||||
>${this._error.message || this._error.code}</ha-alert
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this._hostInfo
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.storage.used_space"
|
||||
)}
|
||||
.value=${this._getUsedSpace(
|
||||
this._hostInfo?.disk_used,
|
||||
this._hostInfo?.disk_total
|
||||
)}
|
||||
.tooltip=${`${this._hostInfo.disk_used} GB/${this._hostInfo.disk_total} GB`}
|
||||
></ha-metric>
|
||||
${this._hostInfo.disk_life_time !== "" &&
|
||||
this._hostInfo.disk_life_time >= 10
|
||||
? html`
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.storage.emmc_lifetime_used"
|
||||
)}
|
||||
.value=${this._hostInfo.disk_life_time}
|
||||
.tooltip=${`${
|
||||
this._hostInfo.disk_life_time - 10
|
||||
} % -
|
||||
${this._hostInfo.disk_life_time} %`}
|
||||
class="emmc"
|
||||
></ha-metric>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
try {
|
||||
this._hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
}
|
||||
}
|
||||
|
||||
private _moveDatadisk(): void {
|
||||
showMoveDatadiskDialog(this, {
|
||||
hostInfo: this._hostInfo!,
|
||||
});
|
||||
}
|
||||
|
||||
private _getUsedSpace = (used: number, total: number) =>
|
||||
roundWithOneDecimal(getValueInPercentage(used, 0, total));
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-storage": HaConfigSectionStorage;
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { HassioHostInfo } from "../../../data/hassio/host";
|
||||
|
||||
export interface MoveDatadiskDialogParams {
|
||||
hostInfo: HassioHostInfo;
|
||||
}
|
||||
|
||||
export const showMoveDatadiskDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: MoveDatadiskDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-move-datadisk",
|
||||
dialogImport: () => import("./dialog-move-datadisk"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -1,508 +0,0 @@
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket/dist/types";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import { subscribePollingCollection } from "../../../common/util/subscribe-polling";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-metric";
|
||||
import { fetchHassioStats, HassioStats } from "../../../data/hassio/common";
|
||||
import {
|
||||
fetchHassioResolution,
|
||||
HassioResolution,
|
||||
} from "../../../data/hassio/resolution";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
subscribeSystemHealthInfo,
|
||||
SystemCheckValueObject,
|
||||
SystemHealthInfo,
|
||||
} from "../../../data/system_health";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
const sortKeys = (a: string, b: string) => {
|
||||
if (a === "homeassistant") {
|
||||
return -1;
|
||||
}
|
||||
if (b === "homeassistant") {
|
||||
return 1;
|
||||
}
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (b < a) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const UNSUPPORTED_REASON_URL = {};
|
||||
export const UNHEALTHY_REASON_URL = {
|
||||
privileged: "/more-info/unsupported/privileged",
|
||||
};
|
||||
|
||||
@customElement("ha-config-system-health")
|
||||
class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _info?: SystemHealthInfo;
|
||||
|
||||
@state() private _supervisorStats?: HassioStats;
|
||||
|
||||
@state() private _resolutionInfo?: HassioResolution;
|
||||
|
||||
@state() private _coreStats?: HassioStats;
|
||||
|
||||
@state() private _error?: { code: string; message: string };
|
||||
|
||||
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
|
||||
const subs: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> = [];
|
||||
if (isComponentLoaded(this.hass, "system_health")) {
|
||||
subs.push(
|
||||
subscribeSystemHealthInfo(this.hass!, (info) => {
|
||||
this._info = info;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
subs.push(
|
||||
subscribePollingCollection(
|
||||
this.hass,
|
||||
async () => {
|
||||
this._supervisorStats = await fetchHassioStats(
|
||||
this.hass,
|
||||
"supervisor"
|
||||
);
|
||||
this._coreStats = await fetchHassioStats(this.hass, "core");
|
||||
},
|
||||
10000
|
||||
)
|
||||
);
|
||||
fetchHassioResolution(this.hass).then((data) => {
|
||||
this._resolutionInfo = data;
|
||||
});
|
||||
}
|
||||
|
||||
return subs;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this.hass!.loadBackendTranslation("system_health");
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const sections: TemplateResult[] = [];
|
||||
|
||||
if (!this._info) {
|
||||
sections.push(
|
||||
html`
|
||||
<div class="loading-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
} else {
|
||||
const domains = Object.keys(this._info).sort(sortKeys);
|
||||
for (const domain of domains) {
|
||||
const domainInfo = this._info[domain];
|
||||
const keys: TemplateResult[] = [];
|
||||
|
||||
for (const key of Object.keys(domainInfo.info)) {
|
||||
let value: unknown;
|
||||
|
||||
if (
|
||||
domainInfo.info[key] &&
|
||||
typeof domainInfo.info[key] === "object"
|
||||
) {
|
||||
const info = domainInfo.info[key] as SystemCheckValueObject;
|
||||
|
||||
if (info.type === "pending") {
|
||||
value = html`
|
||||
<ha-circular-progress active size="tiny"></ha-circular-progress>
|
||||
`;
|
||||
} else if (info.type === "failed") {
|
||||
value = html`
|
||||
<span class="error">${info.error}</span>${!info.more_info
|
||||
? ""
|
||||
: html`
|
||||
–
|
||||
<a
|
||||
href=${info.more_info}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.system_health.more_info"
|
||||
)}
|
||||
</a>
|
||||
`}
|
||||
`;
|
||||
} else if (info.type === "date") {
|
||||
value = formatDateTime(new Date(info.value), this.hass.locale);
|
||||
}
|
||||
} else {
|
||||
value = domainInfo.info[key];
|
||||
}
|
||||
|
||||
keys.push(html`
|
||||
<tr>
|
||||
<td>
|
||||
${this.hass.localize(
|
||||
`component.${domain}.system_health.info.${key}`
|
||||
) || key}
|
||||
</td>
|
||||
<td>${value}</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
if (domain !== "homeassistant") {
|
||||
sections.push(
|
||||
html`
|
||||
<div class="card-header">
|
||||
<h3>${domainToName(this.hass.localize, domain)}</h3>
|
||||
${!domainInfo.manage_url
|
||||
? ""
|
||||
: html`
|
||||
<a class="manage" href=${domainInfo.manage_url}>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.system_health.manage"
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
sections.push(html`
|
||||
<table>
|
||||
${keys}
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
.header=${this.hass.localize("ui.panel.config.system_health.caption")}
|
||||
>
|
||||
${this._error
|
||||
? html`
|
||||
<ha-alert alert-type="error"
|
||||
>${this._error.message || this._error.code}</ha-alert
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this._info
|
||||
? html`
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
@action=${this._copyInfo}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.panel.config.info.copy_menu")}
|
||||
.path=${mdiContentCopy}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item>
|
||||
${this.hass.localize("ui.panel.config.info.copy_raw")}
|
||||
</mwc-list-item>
|
||||
<mwc-list-item>
|
||||
${this.hass.localize("ui.panel.config.info.copy_github")}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
${this._resolutionInfo
|
||||
? html`${this._resolutionInfo.unhealthy.length
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.hass.localize("ui.dialogs.unhealthy.title")}
|
||||
<mwc-button
|
||||
slot="action"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.common.learn_more"
|
||||
)}
|
||||
@click=${this._unhealthyDialog}
|
||||
>
|
||||
</mwc-button
|
||||
></ha-alert>`
|
||||
: ""}
|
||||
${this._resolutionInfo.unsupported.length
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize("ui.dialogs.unsupported.title")}
|
||||
<mwc-button
|
||||
slot="action"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.common.learn_more"
|
||||
)}
|
||||
@click=${this._unsupportedDialog}
|
||||
>
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: ""} `
|
||||
: ""}
|
||||
|
||||
<ha-card outlined>
|
||||
<div class="card-content">${sections}</div>
|
||||
</ha-card>
|
||||
${!this._coreStats && !this._supervisorStats
|
||||
? ""
|
||||
: html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${this._coreStats
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_health.core_stats"
|
||||
)}
|
||||
</h3>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.cpu_usage"
|
||||
)}
|
||||
.value=${this._coreStats.cpu_percent}
|
||||
></ha-metric>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.ram_usage"
|
||||
)}
|
||||
.value=${this._coreStats.memory_percent}
|
||||
></ha-metric>
|
||||
`
|
||||
: ""}
|
||||
${this._supervisorStats
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_health.supervisor_stats"
|
||||
)}
|
||||
</h3>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.cpu_usage"
|
||||
)}
|
||||
.value=${this._supervisorStats.cpu_percent}
|
||||
></ha-metric>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.ram_usage"
|
||||
)}
|
||||
.value=${this._supervisorStats.memory_percent}
|
||||
></ha-metric>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-card>
|
||||
`}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _unsupportedDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.unsupported.title"),
|
||||
text: html`${this.hass.localize("ui.dialogs.unsupported.description")}
|
||||
<br /><br />
|
||||
<ul>
|
||||
${this._resolutionInfo!.unsupported.map(
|
||||
(reason) => html`
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
UNSUPPORTED_REASON_URL[reason] ||
|
||||
`/more-info/unsupported/${reason}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.unsupported.reason.${reason}`
|
||||
) || reason}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _unhealthyDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.unhealthy.title"),
|
||||
text: html`${this.hass.localize("ui.dialogs.unhealthy.description")}
|
||||
<br /><br />
|
||||
<ul>
|
||||
${this._resolutionInfo!.unhealthy.map(
|
||||
(reason) => html`
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
UNHEALTHY_REASON_URL[reason] ||
|
||||
`/more-info/unhealthy/${reason}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.unhealthy.reason.${reason}`
|
||||
) || reason}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _copyInfo(ev: CustomEvent<ActionDetail>): Promise<void> {
|
||||
const github = ev.detail.index === 1;
|
||||
let haContent: string | undefined;
|
||||
const domainParts: string[] = [];
|
||||
|
||||
for (const domain of Object.keys(this._info!).sort(sortKeys)) {
|
||||
const domainInfo = this._info![domain];
|
||||
let first = true;
|
||||
const parts = [
|
||||
`${
|
||||
github && domain !== "homeassistant"
|
||||
? `<details><summary>${domainToName(
|
||||
this.hass.localize,
|
||||
domain
|
||||
)}</summary>\n`
|
||||
: ""
|
||||
}`,
|
||||
];
|
||||
|
||||
for (const key of Object.keys(domainInfo.info)) {
|
||||
let value: unknown;
|
||||
|
||||
if (typeof domainInfo.info[key] === "object") {
|
||||
const info = domainInfo.info[key] as SystemCheckValueObject;
|
||||
|
||||
if (info.type === "pending") {
|
||||
value = "pending";
|
||||
} else if (info.type === "failed") {
|
||||
value = `failed to load: ${info.error}`;
|
||||
} else if (info.type === "date") {
|
||||
value = formatDateTime(new Date(info.value), this.hass.locale);
|
||||
}
|
||||
} else {
|
||||
value = domainInfo.info[key];
|
||||
}
|
||||
if (github && first) {
|
||||
parts.push(`${key} | ${value}\n-- | --`);
|
||||
first = false;
|
||||
} else {
|
||||
parts.push(`${key}${github ? " | " : ": "}${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "homeassistant") {
|
||||
haContent = parts.join("\n");
|
||||
} else {
|
||||
domainParts.push(parts.join("\n"));
|
||||
if (github && domain !== "homeassistant") {
|
||||
domainParts.push("</details>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await copyToClipboard(
|
||||
`${github ? "## " : ""}System Health\n${haContent}\n\n${domainParts.join(
|
||||
"\n\n"
|
||||
)}`
|
||||
);
|
||||
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
a.manage {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-system-health": HaConfigSystemHealth;
|
||||
}
|
||||
}
|
@@ -157,7 +157,7 @@ export class HaConfigUsers extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
backPath="/config"
|
||||
.tabs=${configSections.persons}
|
||||
.tabs=${configSections.areas}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._users}
|
||||
@row-click=${this._editUser}
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@@ -36,10 +37,7 @@ import {
|
||||
Zone,
|
||||
ZoneMutableParams,
|
||||
} from "../../../data/zone";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
@@ -187,26 +185,36 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
<div style="display:inline-block">
|
||||
<ha-icon-button
|
||||
.entityId=${stateObject.entity_id}
|
||||
.noEdit=${stateObject.entity_id !== "zone.home" ||
|
||||
!this._canEditCore}
|
||||
@click=${this._openCoreConfig}
|
||||
disabled=${ifDefined(
|
||||
stateObject.entity_id === "zone.home" &&
|
||||
this.narrow &&
|
||||
this._canEditCore
|
||||
? undefined
|
||||
: true
|
||||
)}
|
||||
.path=${stateObject.entity_id === "zone.home" &&
|
||||
this.narrow &&
|
||||
this._canEditCore
|
||||
? mdiPencil
|
||||
: mdiPencilOff}
|
||||
.label=${stateObject.entity_id === "zone.home"
|
||||
? hass.localize("ui.panel.config.zone.edit_home")
|
||||
: hass.localize("ui.panel.config.zone.edit_zone")}
|
||||
@click=${this._openCoreConfig}
|
||||
.label=${hass.localize(
|
||||
"ui.panel.config.zone.edit_zone"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
${stateObject.entity_id !== "zone.home"
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${hass.localize(
|
||||
"ui.panel.config.zone.configured_in_yaml"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${stateObject.entity_id === "zone.home"
|
||||
? hass.localize(
|
||||
`ui.panel.config.zone.${
|
||||
this.narrow
|
||||
? "edit_home_zone_narrow"
|
||||
: "edit_home_zone"
|
||||
}`
|
||||
)
|
||||
: hass.localize(
|
||||
"ui.panel.config.zone.configured_in_yaml"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
</paper-icon-item>
|
||||
`
|
||||
@@ -383,16 +391,22 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
this._openDialog(entry);
|
||||
}
|
||||
|
||||
private async _openCoreConfig(ev) {
|
||||
if (ev.currentTarget.noEdit) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.zone.can_not_edit"),
|
||||
text: this.hass.localize("ui.panel.config.zone.configured_in_yaml"),
|
||||
confirm: () => {},
|
||||
});
|
||||
private async _openCoreConfig(ev: Event) {
|
||||
const entityId: string = (ev.currentTarget! as any).entityId;
|
||||
if (entityId !== "zone.home" || !this.narrow || !this._canEditCore) {
|
||||
return;
|
||||
}
|
||||
navigate("/config/general");
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.zone.go_to_core_config"),
|
||||
text: this.hass.localize("ui.panel.config.zone.home_zone_core_config"),
|
||||
confirmText: this.hass!.localize("ui.common.yes"),
|
||||
dismissText: this.hass!.localize("ui.common.no"),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
navigate("/config/core");
|
||||
}
|
||||
|
||||
private async _createEntry(values: ZoneMutableParams) {
|
||||
|
@@ -41,10 +41,6 @@ class DeveloperToolsRouter extends HassRouterPage {
|
||||
tag: "developer-tools-statistics",
|
||||
load: () => import("./statistics/developer-tools-statistics"),
|
||||
},
|
||||
yaml: {
|
||||
tag: "developer-yaml-config",
|
||||
load: () => import("./yaml_configuration/developer-yaml-config"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -67,9 +67,6 @@ class PanelDeveloperTools extends LitElement {
|
||||
"ui.panel.developer-tools.tabs.statistics.title"
|
||||
)}
|
||||
</paper-tab>
|
||||
<paper-tab page-name="yaml">
|
||||
${this.hass.localize("ui.panel.developer-tools.tabs.yaml.title")}
|
||||
</paper-tab>
|
||||
</ha-tabs>
|
||||
</app-header>
|
||||
<developer-tools-router
|
||||
|
@@ -1,240 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { componentsWithService } from "../../../common/config/components_with_service";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { checkCoreConfig } from "../../../data/core";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
|
||||
@customElement("developer-yaml-config")
|
||||
export class DeveloperYamlConfig extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
|
||||
@state() private _validating = false;
|
||||
|
||||
@state() private _reloadableDomains: string[] = [];
|
||||
|
||||
private _validateLog = "";
|
||||
|
||||
private _isValid: boolean | null = null;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
const oldHass = changedProperties.get("hass");
|
||||
if (
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass || oldHass.config.components !== this.hass.config.components)
|
||||
) {
|
||||
this._reloadableDomains = componentsWithService(
|
||||
this.hass,
|
||||
"reload"
|
||||
).sort();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card
|
||||
outlined
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.introduction"
|
||||
)}
|
||||
${!this._validateLog
|
||||
? html`
|
||||
<div class="validate-container layout vertical center-center">
|
||||
${!this._validating
|
||||
? html`
|
||||
${this._isValid
|
||||
? html`<div class="validate-result" id="result">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.valid"
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="config-invalid">
|
||||
<span class="text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.invalid"
|
||||
)}
|
||||
</span>
|
||||
<mwc-button raised @click=${this._validateConfig}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.check_config"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
<div id="configLog" class="validate-log">
|
||||
${this._validateLog}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
@click=${this._validateConfig}
|
||||
.disabled=${this._validateLog}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.check_config"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
class="warning"
|
||||
@click=${this._restart}
|
||||
.disabled=${this._validateLog}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.server_management.restart"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
outlined
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.reloading.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.reloading.introduction"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="reload_core_config"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.reloading.core"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
${this._reloadableDomains.map(
|
||||
(domain) =>
|
||||
html`
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
service="reload"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.developer-tools.tabs.yaml.section.reloading.${domain}`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.reloading.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _validateConfig() {
|
||||
this._validating = true;
|
||||
this._validateLog = "";
|
||||
this._isValid = null;
|
||||
|
||||
const configCheck = await checkCoreConfig(this.hass);
|
||||
this._validating = false;
|
||||
this._isValid = configCheck.result === "valid";
|
||||
|
||||
if (configCheck.errors) {
|
||||
this._validateLog = configCheck.errors;
|
||||
}
|
||||
}
|
||||
|
||||
private _restart() {
|
||||
showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.server_management.confirm_restart"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.leave"),
|
||||
dismissText: this.hass!.localize("ui.common.stay"),
|
||||
confirm: () => {
|
||||
this.hass.callService("homeassistant", "restart");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.validate-container {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.validate-result {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.config-invalid {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.config-invalid .text {
|
||||
color: var(--error-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.validate-log {
|
||||
white-space: pre-line;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"developer-yaml-config": DeveloperYamlConfig;
|
||||
}
|
||||
}
|
@@ -174,26 +174,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
if (this._config!.needle) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// new format
|
||||
let segments = this._config!.segments;
|
||||
if (segments) {
|
||||
segments = [...segments].sort((a, b) => a?.from - b?.from);
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
if (
|
||||
segment &&
|
||||
numberValue >= segment.from &&
|
||||
(i + 1 === segments.length || numberValue < segments[i + 1]?.from)
|
||||
) {
|
||||
return segment.color;
|
||||
}
|
||||
}
|
||||
return severityMap.normal;
|
||||
}
|
||||
|
||||
// old format
|
||||
const sections = this._config!.severity;
|
||||
|
||||
if (!sections) {
|
||||
@@ -226,16 +206,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _severityLevels() {
|
||||
// new format
|
||||
const segments = this._config!.segments;
|
||||
if (segments) {
|
||||
return segments.map((segment) => ({
|
||||
level: segment?.from,
|
||||
stroke: segment?.color,
|
||||
}));
|
||||
}
|
||||
|
||||
// old format
|
||||
const sections = this._config!.severity;
|
||||
|
||||
if (!sections) {
|
||||
|
@@ -176,11 +176,6 @@ export interface SeverityConfig {
|
||||
red?: number;
|
||||
}
|
||||
|
||||
export interface GaugeSegment {
|
||||
from: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface GaugeCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
@@ -190,7 +185,6 @@ export interface GaugeCardConfig extends LovelaceCardConfig {
|
||||
severity?: SeverityConfig;
|
||||
theme?: string;
|
||||
needle?: boolean;
|
||||
segments?: GaugeSegment[];
|
||||
}
|
||||
|
||||
export interface ConfigEntity extends EntityConfig {
|
||||
|
@@ -1,17 +1,10 @@
|
||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||
import { LovelaceCard, LovelaceHeaderFooter } from "../types";
|
||||
|
||||
export const computeCardSize = (
|
||||
card: LovelaceCard | LovelaceHeaderFooter
|
||||
): number | Promise<number> => {
|
||||
if (typeof card.getCardSize === "function") {
|
||||
try {
|
||||
return promiseTimeout(500, card.getCardSize()).catch(
|
||||
() => 1
|
||||
) as Promise<number>;
|
||||
} catch (_e: any) {
|
||||
return 1;
|
||||
}
|
||||
return card.getCardSize();
|
||||
}
|
||||
if (customElements.get(card.localName)) {
|
||||
return 1;
|
||||
|
@@ -2,7 +2,6 @@ import "../../../../components/ha-form/ha-form";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
array,
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
@@ -19,11 +18,6 @@ import type { GaugeCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
|
||||
const gaugeSegmentStruct = object({
|
||||
from: number(),
|
||||
color: string(),
|
||||
});
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
@@ -35,7 +29,6 @@ const cardConfigStruct = assign(
|
||||
severity: optional(object()),
|
||||
theme: optional(string()),
|
||||
needle: optional(boolean()),
|
||||
segments: optional(array(gaugeSegmentStruct)),
|
||||
})
|
||||
);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user