Compare commits

..

1 Commits

Author SHA1 Message Date
Ludeeus
45d78c0c77 Use closed instead of closing 2021-05-25 20:47:31 +00:00
245 changed files with 1767 additions and 7358 deletions

View File

@@ -28,7 +28,9 @@
"__BUILD__": false,
"__VERSION__": false,
"__STATIC_PATH__": false,
"Polymer": true
"Polymer": true,
"webkitSpeechRecognition": false,
"ResizeObserver": false
},
"env": {
"browser": true,
@@ -104,6 +106,5 @@
"lit/attribute-value-entities": 0
},
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable",
"ignorePatterns": ["src/resources/lit-virtualizer/*"]
"processor": "disable/disable"
}

21
.gitignore vendored
View File

@@ -1,17 +1,10 @@
.DS_Store
.reify-cache
# build
build
build-translations/*
hass_frontend/*
dist
# yarn
.yarn
yarn-error.log
node_modules/*
npm-debug.log
.DS_Store
hass_frontend/*
.reify-cache
# Python stuff
*.py[cod]
@@ -21,8 +14,11 @@ npm-debug.log
# venv stuff
pyvenv.cfg
pip-selfcheck.json
venv/*
venv
.venv
lib
bin
dist
# vscode
.vscode/*
@@ -35,8 +31,9 @@ src/cast/dev_const.ts
# Secrets
.lokalise_token
yarn-error.log
# asdf
#asdf
.tool-versions
# Home Assistant config

View File

@@ -1,4 +0,0 @@
module.exports = {
require: "test-mocha/testconf.js",
timeout: 10000,
};

View File

@@ -52,7 +52,11 @@ const createRollupConfig = ({
browser: true,
rootDir: paths.polymer_dir,
}),
commonjs(),
commonjs({
namedExports: {
"js-yaml": ["safeDump", "safeLoad"],
},
}),
json(),
babel({
...bundle.babelOptions({ latestBuild }),

View File

@@ -116,9 +116,8 @@ const createWebpackConfig = ({
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
new webpack.NormalModuleReplacementPlugin(
new RegExp(
path.resolve(
paths.polymer_dir,
"src/resources/lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
require.resolve(
"@lit-labs/virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
)
),
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")

View File

@@ -1,7 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { load } from "js-yaml";
import { safeLoad } from "js-yaml";
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
class DemoCard extends PolymerElement {
@@ -80,7 +80,7 @@ class DemoCard extends PolymerElement {
card.removeChild(card.lastChild);
}
const el = this._createCardElement(load(config.config)[0]);
const el = this._createCardElement(safeLoad(config.config)[0]);
card.appendChild(el);
this._getSize(el);
}

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml";
import { safeDump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card";
@@ -56,7 +56,7 @@ export class DemoAutomationDescribeAction extends LitElement {
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, conf as any)}</span>
<pre>${dump(conf)}</pre>
<pre>${safeDump(conf)}</pre>
</div>
`
)}

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml";
import { safeDump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-card";
@@ -26,7 +26,7 @@ export class DemoAutomationDescribeCondition extends LitElement {
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any)}</span>
<pre>${dump(conf)}</pre>
<pre>${safeDump(conf)}</pre>
</div>
`
)}

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml";
import { safeDump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-card";
@@ -29,7 +29,7 @@ export class DemoAutomationDescribeTrigger extends LitElement {
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any)}</span>
<pre>${dump(conf)}</pre>
<pre>${safeDump(conf)}</pre>
</div>
`
)}

View File

@@ -28,11 +28,10 @@ const createConfigEntry = (
title,
source: "zeroconf",
state: "loaded",
connection_class: "local_push",
supports_options: false,
supports_unload: true,
disabled_by: null,
pref_disable_new_entities: false,
pref_disable_polling: false,
reason: null,
...override,
});
@@ -65,9 +64,6 @@ const configPanelEntry = createConfigEntry("Config Panel", {
const optionsFlowEntry = createConfigEntry("Options Flow", {
supports_options: true,
});
const disabledPollingEntry = createConfigEntry("Disabled Polling", {
pref_disable_polling: true,
});
const setupErrorEntry = createConfigEntry("Setup Error", {
state: "setup_error",
});
@@ -140,7 +136,6 @@ const configEntries: Array<{
{ items: [loadedEntry] },
{ items: [configPanelEntry] },
{ items: [optionsFlowEntry] },
{ items: [disabledPollingEntry] },
{ items: [nameAsDomainEntry] },
{ items: [longNameEntry] },
{ items: [longNonBreakingNameEntry] },

View File

@@ -3,7 +3,6 @@ import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import { DEFAULT_SCHEMA, Type } from "js-yaml";
import {
css,
CSSResultGroup,
@@ -12,7 +11,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
@@ -28,7 +27,6 @@ import {
HassioAddonDetails,
HassioAddonSetOptionParams,
setHassioAddonOption,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
@@ -40,13 +38,6 @@ import { hassioStyle } from "../../resources/hassio-style";
const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"];
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
new Type("!secret", {
kind: "scalar",
construct: (data) => `!secret ${data}`,
}),
]);
@customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -134,7 +125,6 @@ class HassioAddonConfig extends LitElement {
></ha-form>`
: html` <ha-yaml-editor
@value-changed=${this._configChanged}
.schema=${ADDON_YAML_SCHEMA}
></ha-yaml-editor>`}
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${!this._yamlMode ||
@@ -279,14 +269,6 @@ class HassioAddonConfig extends LitElement {
this._error = undefined;
try {
const validation = await validateHassioAddonOption(
this.hass,
this.addon.slug,
this._editor?.value
);
if (!validation.valid) {
throw Error(validation.message);
}
await setHassioAddonOption(this.hass, this.addon.slug, {
options: this._yamlMode ? this._editor?.value : this._options,
});

View File

@@ -64,7 +64,6 @@ class SupervisorMetric extends LitElement {
.value {
width: 48px;
padding-right: 4px;
flex-shrink: 0;
}
`;
}

View File

@@ -1,4 +1,4 @@
import { mdiArrowUpBoldCircle, mdiPlay, mdiPuzzle, mdiStop } from "@mdi/js";
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
@@ -17,36 +17,7 @@ class HassioAddons extends LitElement {
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
protected render(): TemplateResult {
return html`<ha-card
.header=${this.supervisor.localize("dashboard.addons")}
>
<div class="addons" ?narrow=${this.narrow}>
${this.supervisor.supervisor.addons.map(
(addon) => html`<div
class="addon"
@click=${this._addonTapped}
.addon=${addon}
>
<div class="icon">
<div class="overlay">
<ha-svg-icon
.title=${addon.state}
.path=${addon.state === "started" ? mdiPlay : mdiStop}
>
</ha-svg-icon>
</div>
${addon.icon && atLeastVersion(this.hass.config.version, 0, 105)
? html`<img src="/api/hassio/addons/${addon.slug}/icon" />`
: html`<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>`}
</div>
<div class="name">${addon.name}</div>
</div>`
)}
</div>
</ha-card>`;
return html`
<div class="content">
<h1>${this.supervisor.localize("dashboard.addons")}</h1>
@@ -117,42 +88,9 @@ class HassioAddons extends LitElement {
haStyle,
hassioStyle,
css`
.addons {
display: grid;
grid-template-columns: repeat(4, auto);
padding-bottom: 16px;
}
.addons[narrow] {
grid-template-columns: repeat(2, auto);
}
.addon {
text-align: center;
max-width: 100px;
padding: 0 8px;
ha-card {
cursor: pointer;
}
.icon > *:not(.overlay) {
position: relative;
max-height: 60px;
max-width: 60px;
margin: auto;
--mdc-icon-size: 60px;
display: flex;
}
.icon {
margin-bottom: 4px;
}
.overlay {
position: absolute;
z-index: 2;
--mdc-icon-size: 24px;
color: var(--secondary-text-color);
background-color: var(--secondary-background-color);
opacity: 0.6;
border-radius: 100%;
margin-left: 12px;
border: 1px var(--secondary-text-color) solid;
}
`,
];
}

View File

@@ -5,7 +5,6 @@ import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style";
import "./hassio-addons";
import "./hassio-update";
@@ -33,17 +32,14 @@ class HassioDashboard extends LitElement {
<span slot="header">
${this.supervisor.localize("panel.dashboard")}
</span>
<div class="content">
<hassio-update
.hass=${this.hass}
.supervisor=${this.supervisor}
.narrow=${this.narrow}
></hassio-update>
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
.narrow=${this.narrow}
></hassio-addons>
</div>
</hass-tabs-subpage>
@@ -53,18 +49,9 @@ class HassioDashboard extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
.content {
display: grid;
max-width: 1400px;
justify-content: center;
grid-template-columns: repeat(2, auto);
gap: 16px;
}
.content > * {
display: block;
min-width: 400px;
margin: 0 auto;
}
`,
];

View File

@@ -1,5 +1,5 @@
import "@material/mwc-button";
import { mdiHomeAssistant, mdiPuzzle } from "@mdi/js";
import { mdiHomeAssistant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -42,15 +42,11 @@ export class HassioUpdate extends LitElement {
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
private _pendingUpdates = memoizeOne(
(supervisor: Supervisor): number =>
Object.keys(supervisor).filter(
(value) => supervisor[value].update_available
).length +
supervisor.supervisor.addons.filter((addon) => addon.update_available)
.length
).length
);
protected render(): TemplateResult {
@@ -64,38 +60,15 @@ export class HassioUpdate extends LitElement {
}
return html`
<ha-card
.header="${this.supervisor.localize(
"common.update_available",
"count",
updatesAvailable + 1
)}
🎉"
>
${this._renderUpdateRow({
type: "os",
heading: "Home Assistant Operating system",
icon: mdiHomeAssistant,
version: "5",
version_latest: "6",
})}
${this.supervisor.addon.addons
.filter((addon) => addon.update_available)
.map((addon) =>
this._renderUpdateRow({
type: "addon",
heading: addon.name,
version: addon.version_latest,
version_latest: addon.version,
image: addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined,
icon: mdiPuzzle,
})
)}
</ha-card>
<div class="content">
<h1></h1>
<h1>
${this.supervisor.localize(
"common.update_available",
"count",
updatesAvailable
)}
🎉
</h1>
<div class="card-group">
${this._renderUpdateCard(
"Home Assistant Core",
@@ -127,37 +100,6 @@ export class HassioUpdate extends LitElement {
`;
}
private _renderUpdateRow(options: {
type: "supervisor" | "os" | "core" | "addon";
heading: string;
version: string;
version_latest: string;
icon?: string;
image?: string;
release_notes?: string;
slug?: string;
}): TemplateResult {
return html`<div class="update-row">
<paper-icon-item>
<div class="icon" slot="item-icon">
${options.image && atLeastVersion(this.hass.config.version, 0, 104)
? html`<img src="${options.image}" />`
: options.icon
? html`<ha-svg-icon .path=${options.icon}></ha-svg-icon>`
: ""}
</div>
<paper-item-body two-line>
${options.heading}
<div secondary>Version ${options.version_latest} is available</div>
</paper-item-body>
</paper-icon-item>
<div class="update-row-actions" ?narrow=${false}>
<mwc-button>Releaese notes</mwc-button>
<mwc-button>Update</mwc-button>
</div>
</div>`;
}
private _renderUpdateCard(
name: string,
key: string,
@@ -289,21 +231,31 @@ export class HassioUpdate extends LitElement {
haStyle,
hassioStyle,
css`
.update-row,
paper-icon-item {
display: flex;
align-items: center;
.icon {
--mdc-icon-size: 48px;
float: right;
margin: 0 0 2px 10px;
color: var(--primary-text-color);
}
.update-row {
padding: 8px;
justify-content: space-between;
.update-heading {
font-size: var(--paper-font-subhead_-_font-size);
font-weight: 500;
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
.icon > * {
max-height: 32px;
max-width: 32px;
margin-right: 16px;
--mdc-icon-size: 32px;
.card-content {
height: calc(100% - 47px);
box-sizing: border-box;
}
.card-actions {
text-align: right;
}
a {
text-decoration: none;
}
ha-settings-row {
padding: 0;
--paper-item-body-two-line-min-height: 32px;
}
`,
];

View File

@@ -45,7 +45,7 @@ class HassioRegistriesDialog extends LitElement {
return html`
<ha-dialog
.open=${this._opened}
@closed=${this.closeDialog}
@closing=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
@@ -244,6 +244,9 @@ class HassioRegistriesDialog extends LitElement {
mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color);
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}

View File

@@ -67,7 +67,7 @@ class HassioRepositoriesDialog extends LitElement {
return html`
<ha-dialog
.open=${this._opened}
@closed=${this.closeDialog}
@closing=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
@@ -150,6 +150,9 @@ class HassioRepositoriesDialog extends LitElement {
mwc-button {
margin-left: 8px;
}
ha-paper-dropdown-menu {
display: block;
}
ha-circular-progress {
display: block;
margin: 32px;

View File

@@ -48,7 +48,6 @@ class HassioCreateSnapshotDialog extends LitElement {
return html`
<ha-dialog
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,

View File

@@ -4,7 +4,6 @@ import { mdiDotsVertical } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-button-menu";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
@@ -64,8 +63,7 @@ class HassioSnapshotDialog
return html`
<ha-dialog
open
scrimClickAction
@closed=${this.closeDialog}
@closing=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this._computeName)}
>
${this._restoringSnapshot
@@ -90,7 +88,7 @@ class HassioSnapshotDialog
fixed
slot="primaryAction"
@action=${this._handleMenuAction}
@closed=${(ev: Event) => ev.stopPropagation()}
@closing=${(ev: Event) => ev.stopPropagation()}
>
<mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
@@ -288,9 +286,10 @@ class HassioSnapshotDialog
}
}
const name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
const a = document.createElement("a");
a.href = signedPath.path;
a.download = `home_assistant_snapshot_${slugify(this._computeName)}.tar`;
a.download = `Hass_io_${name}.tar`;
this.shadowRoot!.appendChild(a);
a.click();
this.shadowRoot!.removeChild(a);

View File

@@ -103,25 +103,27 @@ export class HassioMain extends SupervisorBaseElement {
private _applyTheme() {
let themeName: string;
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined;
let themeSettings:
| Partial<HomeAssistant["selectedThemeSettings"]>
| undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName =
this.hass.selectedTheme?.theme ||
this.hass.selectedThemeSettings?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme;
themeSettings = this.hass.selectedThemeSettings;
if (themeSettings?.dark === undefined) {
themeSettings = {
...this.hass.selectedTheme,
...this.hass.selectedThemeSettings,
dark: this.hass.themes.darkMode,
};
}
} else {
themeName =
((this.hass.selectedTheme as unknown) as string) ||
((this.hass.selectedThemeSettings as unknown) as string) ||
this.hass.themes.default_theme;
}

View File

@@ -1,17 +1,15 @@
import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import { mdiDotsVertical, mdiPlus } from "@mdi/js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import relativeTime from "../../../src/common/datetime/relative_time";
@@ -19,25 +17,18 @@ import { HASSDomEvent } from "../../../src/common/dom/fire_event";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
} from "../../../src/components/data-table/ha-data-table";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-fab";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
fetchHassioSnapshots,
friendlyFolderName,
HassioSnapshot,
reloadHassioSnapshots,
removeSnapshot,
} from "../../../src/data/hassio/snapshot";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { showHassioCreateSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-create-snapshot";
@@ -58,15 +49,10 @@ export class HassioSnapshots extends LitElement {
@property({ type: Boolean }) public isWide!: boolean;
@state() private _selectedSnapshots: string[] = [];
private _firstUpdatedCalled = false;
@state() private _snapshots?: HassioSnapshot[] = [];
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
private _firstUpdatedCalled = false;
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
@@ -167,9 +153,7 @@ export class HassioSnapshots extends LitElement {
.data=${this._snapshotData(this._snapshots || [])}
id="slug"
@row-click=${this._handleRowClicked}
@selection-changed=${this._handleSelectionChanged}
clickable
selectable
hasFab
main-page
supervisor
@@ -192,45 +176,6 @@ export class HassioSnapshots extends LitElement {
: ""}
</ha-button-menu>
${this._selectedSnapshots.length
? html`<div
class=${classMap({
"header-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
<p class="selected-txt">
${this.supervisor.localize("snapshot.selected", {
number: this._selectedSnapshots.length,
})}
</p>
<div class="header-btns">
${!this.narrow
? html`
<mwc-button
@click=${this._deleteSelected}
class="warning"
>
${this.supervisor.localize("snapshot.delete_selected")}
</mwc-button>
`
: html`
<mwc-icon-button
id="delete-btn"
class="warning"
@click=${this._deleteSelected}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
<paper-tooltip animation-delay="0" for="delete-btn">
${this.supervisor.localize("snapshot.delete_selected")}
</paper-tooltip>
`}
</div>
</div> `
: ""}
<ha-fab
slot="fab"
@click=${this._createSnapshot}
@@ -254,12 +199,6 @@ export class HassioSnapshots extends LitElement {
}
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedSnapshots = ev.detail.value;
}
private _showUploadSnapshotDialog() {
showSnapshotUploadDialog(this, {
showSnapshot: (slug: string) =>
@@ -277,35 +216,6 @@ export class HassioSnapshots extends LitElement {
this._snapshots = await fetchHassioSnapshots(this.hass);
}
private async _deleteSelected() {
const confirm = await showConfirmationDialog(this, {
title: this.supervisor.localize("snapshot.delete_snapshot_title"),
text: this.supervisor.localize("snapshot.delete_snapshot_text", {
number: this._selectedSnapshots.length,
}),
confirmText: this.supervisor.localize("snapshot.delete_snapshot_confirm"),
});
if (!confirm) {
return;
}
try {
await Promise.all(
this._selectedSnapshots.map((slug) => removeSnapshot(this.hass, slug))
);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("snapshot.failed_to_delete"),
text: extractApiErrorMessage(err),
});
return;
}
await reloadHassioSnapshots(this.hass);
this._snapshots = await fetchHassioSnapshots(this.hass);
this._dataTable.clearSelection();
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const slug = ev.detail.id;
showHassioSnapshotDialog(this, {
@@ -334,45 +244,7 @@ export class HassioSnapshots extends LitElement {
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 58px;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
.header-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--secondary-text-color);
position: relative;
top: -4px;
}
.selected-txt {
font-weight: bold;
padding-left: 16px;
color: var(--primary-text-color);
}
.table-header .selected-txt {
margin-top: 20px;
}
.header-toolbar .selected-txt {
font-size: 16px;
}
.header-toolbar .header-btns {
margin-right: -12px;
}
.header-btns > mwc-button,
.header-btns > mwc-icon-button {
margin: 8px;
}
`,
];
return [haStyle, hassioStyle];
}
}

View File

@@ -2,7 +2,7 @@ import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import { dump } from "js-yaml";
import { safeDump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -233,7 +233,7 @@ class HassioHostInfo extends LitElement {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("system.host.hardware"),
content: `<pre>${dump(content, { indent: 2 })}</pre>`,
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
} catch (err) {
showAlertDialog(this, {

View File

@@ -16,13 +16,13 @@
"lint:lit": "lit-analyzer \"**/src/**/*.ts\" --format markdown --outFile result.md",
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
"format": "yarn run format:eslint && yarn run format:prettier",
"mocha": "ts-mocha -p test-mocha/tsconfig.test.json \"test-mocha/**/*.ts\"",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
"test": "yarn run lint && yarn run mocha"
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.1",
"@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.18.0",
"@codemirror/gutter": "^0.18.0",
"@codemirror/highlight": "^0.18.0",
@@ -35,8 +35,8 @@
"@codemirror/text": "^0.18.0",
"@codemirror/view": "^0.18.0",
"@formatjs/intl-getcanonicallocales": "^1.5.10",
"@formatjs/intl-locale": "^2.4.28",
"@formatjs/intl-pluralrules": "^4.0.22",
"@formatjs/intl-locale": "^2.4.24",
"@formatjs/intl-pluralrules": "^4.0.18",
"@fullcalendar/common": "5.1.0",
"@fullcalendar/core": "5.1.0",
"@fullcalendar/daygrid": "5.1.0",
@@ -71,6 +71,7 @@
"@polymer/iron-label": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.2",
"@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-card": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0",
"@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1",
@@ -100,26 +101,26 @@
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "^2.9.4",
"chartjs-chart-timeline": "^0.4.0",
"comlink": "^4.3.1",
"comlink": "^4.3.0",
"core-js": "^3.6.5",
"cropperjs": "^1.5.11",
"cropperjs": "^1.5.7",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
"fecha": "^4.2.0",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.0.4",
"hls.js": "^1.0.3",
"home-assistant-js-websocket": "^5.10.0",
"idb-keyval": "^5.0.5",
"intl-messageformat": "^9.6.16",
"js-yaml": "^4.1.0",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^9.6.13",
"js-yaml": "^3.13.1",
"leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4",
"lit": "^2.0.0-rc.2",
"lit-vaadin-helpers": "^0.1.3",
"marked": "^2.0.5",
"marked": "2.0.0",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.2.1",
"memoize-one": "^5.0.2",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
@@ -129,12 +130,12 @@
"roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2",
"superstruct": "^0.15.2",
"tinykeys": "^1.1.3",
"tinykeys": "^1.1.1",
"tsparticles": "^1.19.2",
"unfetch": "^4.1.0",
"vis-data": "^7.1.2",
"vis-data": "^7.1.1",
"vis-network": "^8.5.4",
"vue": "^2.6.12",
"vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
"workbox-cacheable-response": "^6.1.5",
@@ -146,7 +147,7 @@
"xss": "^1.0.9"
},
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/core": "^7.14.0",
"@babel/plugin-external-helpers": "^7.12.13",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.15",
@@ -155,7 +156,7 @@
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-env": "^7.14.2",
"@babel/preset-env": "^7.14.0",
"@babel/preset-typescript": "^7.13.0",
"@koa/cors": "^3.1.0",
"@open-wc/dev-server-hmr": "^0.0.2",
@@ -164,13 +165,16 @@
"@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-replace": "^2.3.2",
"@types/chromecast-caf-receiver": "5.0.12",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^5.0.11",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/js-yaml": "^4.0.1",
"@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.7.0",
"@types/leaflet-draw": "^1.0.3",
"@types/marked": "^2.0.3",
"@types/mocha": "^8.2.2",
"@types/marked": "^1.2.2",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
"@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.22.0",
@@ -178,7 +182,7 @@
"@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^8.1.0",
"chai": "^4.3.4",
"chai": "^4.2.0",
"cpx": "^1.5.0",
"del": "^4.0.0",
"eslint": "^7.25.0",
@@ -192,7 +196,7 @@
"eslint-plugin-wc": "^1.3.0",
"fancy-log": "^1.3.3",
"fs-extra": "^7.0.1",
"gulp": "^4.0.2",
"gulp": "^4.0.0",
"gulp-foreach": "^0.1.0",
"gulp-json-transform": "^0.4.6",
"gulp-merge-json": "^1.3.1",
@@ -206,7 +210,7 @@
"magic-string": "^0.25.7",
"map-stream": "^0.0.7",
"merge-stream": "^1.0.1",
"mocha": "^8.4.0",
"mocha": "^7.2.0",
"object-hash": "^2.0.3",
"open": "^7.0.4",
"prettier": "^2.0.4",
@@ -216,13 +220,13 @@
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^5.3.0",
"rollup-plugin-visualizer": "^4.0.4",
"serve": "^11.3.2",
"sinon": "^11.0.0",
"serve": "^11.3.0",
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.1.2",
"terser-webpack-plugin": "^5.1.1",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^8.0.0",
"ts-mocha": "^7.0.0",
"typescript": "^4.2.4",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",

View File

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

View File

@@ -31,7 +31,7 @@ export const applyThemesOnElement = (
element,
themes: HomeAssistant["themes"],
selectedTheme?: string,
themeSettings?: Partial<HomeAssistant["selectedTheme"]>
themeSettings?: Partial<HomeAssistant["selectedThemeSettings"]>
) => {
let cacheKey = selectedTheme;
let themeRules: Partial<ThemeVars> = {};
@@ -39,7 +39,7 @@ export const applyThemesOnElement = (
if (themeSettings) {
if (themeSettings.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
themeRules = darkStyles;
}
if (selectedTheme === "default") {

View File

@@ -1,7 +1,14 @@
/* eslint-disable */
// @ts-ignore
export const SpeechRecognition =
// @ts-ignore
window.SpeechRecognition || window.webkitSpeechRecognition;
// @ts-ignore
export const SpeechGrammarList =
// @ts-ignore
window.SpeechGrammarList || window.webkitSpeechGrammarList;
// @ts-ignore
export const SpeechRecognitionEvent =
// @ts-expect-error
// @ts-ignore
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
/* eslint-enable */

View File

@@ -89,6 +89,8 @@ export const domainIcon = (
}
// eslint-disable-next-line
console.warn(`Unable to find icon for domain ${domain}`);
console.warn(
"Unable to find icon for domain " + domain + " (" + stateObj + ")"
);
return DEFAULT_DOMAIN_ICON;
};

View File

@@ -1,2 +0,0 @@
export const escapeRegExp = (text: string): string =>
text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");

View File

@@ -92,7 +92,7 @@ function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
export function isPatternInWord(
function isPatternInWord(
patternLow: string,
patternPos: number,
patternLen: number,
@@ -121,7 +121,7 @@ enum Arrow {
}
/**
* An array representing a fuzzy match.
* An array representating a fuzzy match.
*
* 0. the score
* 1. the offset at which matching started

View File

@@ -5,7 +5,7 @@ import { fuzzyScore } from "./filter";
* 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
* @param {string} word - Word to check for sequence
*
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/

View File

@@ -1,11 +0,0 @@
// https://regex101.com/r/kc5C14/2
const regExpString = "^\\d{4}-(0[1-9]|1[0-2])-([12]\\d|0[1-9]|3[01])";
const regExp = new RegExp(regExpString + "$");
// 2nd expression without the "end of string" enforced, so it can be used
// to just verify the start of a string and then based on that result e.g.
// check for a full timestamp string efficiently.
const regExpNoStringEnd = new RegExp(regExpString);
export const isDate = (input: string, allowCharsAfterDate = false): boolean =>
allowCharsAfterDate ? regExpNoStringEnd.test(input) : regExp.test(input);

View File

@@ -1,11 +0,0 @@
// https://stackoverflow.com/a/14322189/1947205
// Changes:
// 1. Do not allow a plus or minus at the start.
// 2. Enforce that we have a "T" or a blank after the date portion
// to ensure we have a timestamp and not only a date.
// 3. Disallow dates based on week number.
// 4. Disallow dates only consisting of a year.
// https://regex101.com/r/kc5C14/3
const regexp = /^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])[T| ](((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)(\8[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)$/;
export const isTimestamp = (input: string): boolean => regexp.test(input);

View File

@@ -1,6 +0,0 @@
import { refine, string } from "superstruct";
const isCustomType = (value: string) => value.startsWith("custom:");
export const customType = () =>
refine(string(), "custom element type", isCustomType);

View File

@@ -1,6 +1,11 @@
import { refine, string } from "superstruct";
const isEntityId = (value: string): boolean => value.includes(".");
const isEntityId = (value: string): boolean => {
if (!value.includes(".")) {
return false;
}
return true;
};
export const entityId = () =>
refine(string(), "entity ID (domain.entity)", isEntityId);

View File

@@ -1,5 +1,10 @@
import { refine, string } from "superstruct";
const isIcon = (value: string) => value.includes(":");
const isIcon = (value: string) => {
if (!value.includes(":")) {
return false;
}
return true;
};
export const icon = () => refine(string(), "icon (mdi:icon-name)", isIcon);

View File

@@ -1,4 +1,4 @@
import { shouldPolyfill } from "@formatjs/intl-pluralrules/lib/should-polyfill";
import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill";
import IntlMessageFormat from "intl-messageformat";
import { Resources } from "../../types";
@@ -86,15 +86,11 @@ export const computeLocalize = async (
| undefined;
if (!translatedMessage) {
try {
translatedMessage = new IntlMessageFormat(
translatedValue,
language,
formats
);
} catch (err) {
return "Translation error: " + err.message;
}
translatedMessage = new IntlMessageFormat(
translatedValue,
language,
formats
);
cache._localizationCache[messageKey] = translatedMessage;
}

View File

@@ -4,25 +4,29 @@
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
export const debounce = <T extends any[]>(
func: (...args: T) => void,
wait: number,
// eslint-disable-next-line: ban-types
export const debounce = <T extends (...args) => unknown>(
func: T,
wait,
immediate = false
) => {
let timeout: number | undefined;
return (...args: T): void => {
): T => {
let timeout;
// @ts-ignore
return function (...args) {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
const later = () => {
timeout = undefined;
timeout = null;
if (!immediate) {
func(...args);
func.apply(context, args);
}
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = window.setTimeout(later, wait);
timeout = setTimeout(later, wait);
if (callNow) {
func(...args);
func.apply(context, args);
}
};
};

View File

@@ -1,10 +0,0 @@
export const promiseTimeout = (ms: number, promise: Promise<any>) => {
const timeout = new Promise((_resolve, reject) => {
setTimeout(() => {
reject(`Timed out in ${ms} ms.`);
}, ms);
});
// Returns a race between our timeout and the passed in promise
return Promise.race([promise, timeout]);
};

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "../../resources/lit-virtualizer";
import { Layout1d, scroll } from "@lit-labs/virtualizer";
import deepClone from "deep-clone-simple";
import {
css,
@@ -246,7 +246,7 @@ export class HaDataTable extends LitElement {
aria-rowcount=${this._filteredData.length + 1}
style=${styleMap({
height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 53}px`
? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._headerHeight}px)`,
})}
>
@@ -340,10 +340,11 @@ export class HaDataTable extends LitElement {
${scroll({
items: this._items,
layout: Layout1d,
// @ts-expect-error
renderItem: (row: DataTableRowData, index) => {
// not sure how this happens...
if (!row) {
return html``;
return "";
}
if (row.append) {
return html`
@@ -919,11 +920,13 @@ export class HaDataTable extends LitElement {
color: var(--secondary-text-color);
}
.scroller {
display: flex;
position: relative;
contain: strict;
height: calc(100% - 57px);
}
.mdc-data-table__table.auto-height .scroller {
overflow-y: hidden !important;
.mdc-data-table__table:not(.auto-height) .scroller {
overflow: auto;
}
.grows {
flex-grow: 1;

View File

@@ -4,6 +4,7 @@ import {
fetchDeviceActions,
localizeDeviceAutomationAction,
} from "../../data/device_automation";
import "../ha-paper-dropdown-menu";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-action-picker")

View File

@@ -4,6 +4,7 @@ import {
fetchDeviceConditions,
localizeDeviceAutomationCondition,
} from "../../data/device_automation";
import "../ha-paper-dropdown-menu";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-condition-picker")

View File

@@ -4,6 +4,7 @@ import {
fetchDeviceTriggers,
localizeDeviceAutomationTrigger,
} from "../../data/device_automation";
import "../ha-paper-dropdown-menu";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-trigger-picker")

View File

@@ -1,14 +1,16 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import hassAttributeUtil, {
formatAttributeName,
formatAttributeValue,
} from "../util/hass-attributes-util";
import "./ha-expansion-panel";
let jsYamlPromise: Promise<typeof import("js-yaml")>;
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -54,17 +56,17 @@ class HaAttributes extends LitElement {
</div>
`
)}
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
${this.stateObj.attributes.attribution}
</div>
`
: ""}
`
: ""}
</div>
</ha-expansion-panel>
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
${this.stateObj.attributes.attribution}
</div>
`
: ""}
`;
}
@@ -91,7 +93,6 @@ class HaAttributes extends LitElement {
.attribution {
color: var(--secondary-text-color);
text-align: center;
margin-top: 16px;
}
pre {
font-family: inherit;
@@ -123,7 +124,38 @@ class HaAttributes extends LitElement {
return "-";
}
const value = this.stateObj.attributes[attribute];
return formatAttributeValue(this.hass, value);
return this.formatAttributeValue(value);
}
private formatAttributeValue(value: any): string | TemplateResult {
if (value === null) {
return "-";
}
// YAML handling
if (
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
(!Array.isArray(value) && value instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import("js-yaml");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
return html` <pre>${until(yaml, "")}</pre> `;
}
// URL handling
if (typeof value === "string" && value.startsWith("http")) {
try {
// If invalid URL, exception will be raised
const url = new URL(value);
if (url.protocol === "http:" || url.protocol === "https:")
return html`<a target="_blank" rel="noreferrer" href="${value}"
>${value}</a
>`;
} catch (_) {
// Nothing to do here
}
}
return Array.isArray(value) ? value.join(", ") : value;
}
private expandedChanged(ev) {

View File

@@ -1,5 +1,4 @@
import { CircularProgress } from "@material/mwc-circular-progress";
import { CSSResultGroup, css } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-circular-progress")
@@ -42,17 +41,6 @@ export class HaCircularProgress extends CircularProgress {
public get indeterminate() {
return this.active;
}
static get styles(): CSSResultGroup {
return [
super.styles,
css`
:host {
overflow: hidden;
}
`,
];
}
}
declare global {

View File

@@ -23,8 +23,8 @@ class HaCoverControls extends LitElement {
@state() private _entityObj?: CoverEntity;
public willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);

View File

@@ -22,8 +22,8 @@ class HaCoverTiltControls extends LitElement {
@state() private _entityObj?: CoverEntity;
public willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);

View File

@@ -125,7 +125,6 @@ export class HaIcon extends LitElement {
databaseIcon = await getIcon(iconName);
} catch (_err) {
// Firefox in private mode doesn't support IDB
// iOS Safari sometimes doesn't open the DB
databaseIcon = undefined;
}

View File

@@ -1,179 +0,0 @@
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResultGroup,
html,
nothing,
LitElement,
TemplateResult,
} from "lit";
import { customElement, state, property } from "lit/decorators";
import {
Adapter,
NetworkConfig,
IPv6ConfiguredAddress,
IPv4ConfiguredAddress,
} from "../data/network";
import { fireEvent } from "../common/dom/fire_event";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import "./ha-settings-row";
import "./ha-icon";
const format_addresses = (
addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[]
): TemplateResult =>
html`${addresses.map((address, i) => [
html`<span>${address.address}/${address.network_prefix}</span>`,
i < addresses.length - 1 ? ", " : nothing,
])}`;
const format_auto_detected_interfaces = (
adapters: Adapter[]
): Array<TemplateResult | string> =>
adapters.map((adapter) =>
adapter.auto
? html`${adapter.name}
(${format_addresses([...adapter.ipv4, ...adapter.ipv6])})`
: ""
);
declare global {
interface HASSDomEvents {
"network-config-changed": { configured_adapters: string[] };
}
}
@customElement("ha-network")
export class HaNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public networkConfig?: NetworkConfig;
@state() private _expanded?: boolean;
protected render(): TemplateResult {
if (this.networkConfig === undefined) {
return html``;
}
const configured_adapters = this.networkConfig.configured_adapters || [];
return html`
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
id="auto_configure"
@change=${this._handleAutoConfigureCheckboxClick}
.checked=${!configured_adapters.length}
name="auto_configure"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
<span slot="description" data-for="auto_configure">
Detected:
${format_auto_detected_interfaces(this.networkConfig.adapters)}
</span>
</ha-settings-row>
${configured_adapters.length || this._expanded
? this.networkConfig.adapters.map(
(adapter) =>
html`<ha-settings-row>
<span slot="prefix">
<ha-checkbox
id=${adapter.name}
@change=${this._handleAdapterCheckboxClick}
.checked=${configured_adapters.includes(adapter.name)}
.adapter=${adapter.name}
name=${adapter.name}
>
</ha-checkbox>
</span>
<span slot="heading">
Adapter: ${adapter.name}
${adapter.default
? html`<ha-icon .icon="hass:star"></ha-icon> (Default)`
: ""}
</span>
<span slot="description">
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
</span>
</ha-settings-row>`
)
: ""}
`;
}
private _handleAutoConfigureCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
if (this.networkConfig === undefined) {
return;
}
let configured_adapters = [...this.networkConfig.configured_adapters];
if (checkbox.checked) {
this._expanded = false;
configured_adapters = [];
} else {
this._expanded = true;
for (const adapter of this.networkConfig.adapters) {
if (adapter.default) {
configured_adapters = [adapter.name];
break;
}
}
}
fireEvent(this, "network-config-changed", {
configured_adapters: configured_adapters,
});
}
private _handleAdapterCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const adapter_name = (checkbox as any).name;
if (this.networkConfig === undefined) {
return;
}
const configured_adapters = [...this.networkConfig.configured_adapters];
if (checkbox.checked) {
configured_adapters.push(adapter_name);
} else {
const index = configured_adapters.indexOf(adapter_name, 0);
configured_adapters.splice(index, 1);
}
fireEvent(this, "network-config-changed", {
configured_adapters: configured_adapters,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.error {
color: var(--error-color);
}
ha-settings-row {
padding: 0;
}
span[slot="heading"],
span[slot="description"] {
cursor: pointer;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-network": HaNetwork;
}
}

View File

@@ -1,4 +1,4 @@
import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml";
import { safeDump, safeLoad } from "js-yaml";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@@ -20,8 +20,6 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
export class HaYamlEditor extends LitElement {
@property() public value?: any;
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
@property() public defaultValue?: any;
@property() public isValid = true;
@@ -32,10 +30,7 @@ export class HaYamlEditor extends LitElement {
public setValue(value): void {
try {
this._yaml =
value && !isEmpty(value)
? dump(value, { schema: this.yamlSchema })
: "";
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
} catch (err) {
// eslint-disable-next-line no-console
console.error(err, value);
@@ -72,7 +67,7 @@ export class HaYamlEditor extends LitElement {
if (this._yaml) {
try {
parsed = load(this._yaml, { schema: this.yamlSchema });
parsed = safeLoad(this._yaml);
} catch (err) {
// Invalid YAML
isValid = false;

View File

@@ -45,6 +45,7 @@ import "../ha-button-menu";
import "../ha-card";
import "../ha-circular-progress";
import "../ha-fab";
import "../ha-paper-dropdown-menu";
import "../ha-svg-icon";
declare global {

View File

@@ -377,10 +377,6 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
major: {
fontStyle: "bold",
},
source: "auto",
sampleSize: 5,
autoSkipPadding: 20,
maxRotation: 0,
},
},
],

View File

@@ -194,10 +194,7 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
invertOnOff,
]);
}
datasets.push({
data: dataRow,
entity_id: stateInfo.entity_id,
});
datasets.push({ data: dataRow, entity_id: stateInfo.entity_id });
labels.push(entityDisplay);
});
@@ -236,14 +233,6 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
major: {
fontStyle: "bold",
},
sampleSize: 5,
autoSkipPadding: 50,
maxRotation: 0,
},
categoryPercentage: undefined,
barPercentage: undefined,
time: {
format: undefined,
},
},
],
@@ -253,17 +242,10 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
yaxe.maxWidth = yaxe.chart.width * 0.18;
},
position: this._computeRTL ? "right" : "left",
categoryPercentage: undefined,
barPercentage: undefined,
time: { format: undefined },
},
],
},
},
datasets: {
categoryPercentage: 0.8,
barPercentage: 0.9,
},
data: {
labels: labels,
datasets: datasets,

View File

@@ -1,11 +1,4 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
@@ -39,13 +32,13 @@ class UserBadge extends LitElement {
private _personEntityId?: string;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("user")) {
this._getPersonPicture();
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldHass = changedProps.get("hass");
if (
this._personEntityId &&
oldHass &&

View File

@@ -12,20 +12,20 @@ export interface ConfigEntry {
| "setup_retry"
| "not_loaded"
| "failed_unload";
connection_class: string;
supports_options: boolean;
supports_unload: boolean;
pref_disable_new_entities: boolean;
pref_disable_polling: boolean;
disabled_by: "user" | null;
reason: string | null;
}
export type ConfigEntryMutableParams = Partial<
Pick<
ConfigEntry,
"title" | "pref_disable_new_entities" | "pref_disable_polling"
>
>;
export interface ConfigEntryMutableParams {
title: string;
}
export interface ConfigEntrySystemOptions {
disable_new_entities: boolean;
}
export const getConfigEntries = (hass: HomeAssistant) =>
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
@@ -33,9 +33,9 @@ export const getConfigEntries = (hass: HomeAssistant) =>
export const updateConfigEntry = (
hass: HomeAssistant,
configEntryId: string,
updatedValues: ConfigEntryMutableParams
updatedValues: Partial<ConfigEntryMutableParams>
) =>
hass.callWS<{ require_restart: boolean; config_entry: ConfigEntry }>({
hass.callWS<ConfigEntry>({
type: "config_entries/update",
entry_id: configEntryId,
...updatedValues,
@@ -51,15 +51,13 @@ export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
require_restart: boolean;
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
export interface DisableConfigEntryResult {
require_restart: boolean;
}
export const disableConfigEntry = (
hass: HomeAssistant,
configEntryId: string
) =>
hass.callWS<DisableConfigEntryResult>({
hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: "user",
@@ -73,3 +71,23 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
entry_id: configEntryId,
disabled_by: null,
});
export const getConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string
) =>
hass.callWS<ConfigEntrySystemOptions>({
type: "config_entries/system_options/list",
entry_id: configEntryId,
});
export const updateConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string,
params: Partial<ConfigEntrySystemOptions>
) =>
hass.callWS({
type: "config_entries/system_options/update",
entry_id: configEntryId,
...params,
});

View File

@@ -212,15 +212,13 @@ export const setHassioAddonOption = async (
export const validateHassioAddonOption = async (
hass: HomeAssistant,
slug: string,
data?: any
slug: string
): Promise<{ message: string; valid: boolean }> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/options/validate`,
method: "post",
data,
});
}

View File

@@ -130,21 +130,6 @@ export const createHassioFullSnapshot = async (
);
};
export const removeSnapshot = async (hass: HomeAssistant, slug: string) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/snapshots/${slug}/remove`,
method: "post",
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/snapshots/${slug}/remove`
);
};
export const createHassioPartialSnapshot = async (
hass: HomeAssistant,
data: HassioPartialSnapshotCreateParams

View File

@@ -1,5 +1,4 @@
import { clear, get, set, createStore, promisifyRequest } from "idb-keyval";
import { promiseTimeout } from "../common/util/promise-timeout";
import { clear, get, set, Store } from "idb-keyval";
import { iconMetadata } from "../resources/icon-metadata";
import { IconMeta } from "../types";
@@ -11,44 +10,45 @@ export interface Chunks {
[key: string]: Promise<Icons>;
}
export const iconStore = createStore("hass-icon-db", "mdi-icon-store");
export const iconStore = new Store("hass-icon-db", "mdi-icon-store");
export const MDI_PREFIXES = ["mdi", "hass", "hassio", "hademo"];
let toRead: Array<
[string, (iconPath: string | undefined) => void, (e: any) => void]
> = [];
let toRead: Array<[string, (string) => void, () => void]> = [];
// Queue up as many icon fetches in 1 transaction
export const getIcon = (iconName: string) =>
new Promise<string | undefined>((resolve, reject) => {
new Promise<string>((resolve, reject) => {
toRead.push([iconName, resolve, reject]);
if (toRead.length > 1) {
return;
}
promiseTimeout(
1000,
iconStore("readonly", (store) => {
for (const [iconName_, resolve_, reject_] of toRead) {
promisifyRequest<string | undefined>(store.get(iconName_))
.then((icon) => resolve_(icon))
.catch((e) => reject_(e));
const results: Array<[(string) => void, IDBRequest]> = [];
iconStore
._withIDBStore("readonly", (store) => {
for (const [iconName_, resolve_] of toRead) {
results.push([resolve_, store.get(iconName_)]);
}
toRead = [];
})
).catch((e) => {
// Firefox in private mode doesn't support IDB
// Safari sometime doesn't open the DB so we time out
for (const [, , reject_] of toRead) {
reject_(e);
}
toRead = [];
});
.then(() => {
for (const [resolve_, request] of results) {
resolve_(request.result);
}
})
.catch(() => {
// Firefox in private mode doesn't support IDB
for (const [, , reject_] of toRead) {
reject_();
}
toRead = [];
});
});
export const findIconChunk = (icon: string): string => {
export const findIconChunk = (icon): string => {
let lastChunk: IconMeta;
for (const chunk of iconMetadata.parts) {
if (chunk.start !== undefined && icon < chunk.start) {
@@ -63,7 +63,7 @@ export const writeCache = async (chunks: Chunks) => {
const keys = Object.keys(chunks);
const iconsSets: Icons[] = await Promise.all(Object.values(chunks));
// We do a batch opening the store just once, for (considerable) performance
iconStore("readwrite", (store) => {
iconStore._withIDBStore("readwrite", (store) => {
iconsSets.forEach((icons, idx) => {
Object.entries(icons).forEach(([name, path]) => {
store.put(path, name);
@@ -73,13 +73,14 @@ export const writeCache = async (chunks: Chunks) => {
});
};
export const checkCacheVersion = async () => {
const version = await get("_version", iconStore);
if (!version) {
set("_version", iconMetadata.version, iconStore);
} else if (version !== iconMetadata.version) {
await clear(iconStore);
set("_version", iconMetadata.version, iconStore);
}
export const checkCacheVersion = () => {
get("_version", iconStore).then((version) => {
if (!version) {
set("_version", iconMetadata.version, iconStore);
} else if (version !== iconMetadata.version) {
clear(iconStore).then(() =>
set("_version", iconMetadata.version, iconStore)
);
}
});
};

View File

@@ -1,43 +0,0 @@
import { HomeAssistant } from "../types";
export interface IPv6ConfiguredAddress {
address: string;
flowinfo: number;
scope_id: number;
network_prefix: number;
}
export interface IPv4ConfiguredAddress {
address: string;
network_prefix: number;
}
export interface Adapter {
name: string;
enabled: boolean;
auto: boolean;
default: boolean;
ipv6: IPv6ConfiguredAddress[];
ipv4: IPv4ConfiguredAddress[];
}
export interface NetworkConfig {
adapters: Adapter[];
configured_adapters: string[];
}
export const getNetworkConfig = (hass: HomeAssistant) =>
hass.callWS<NetworkConfig>({
type: "network",
});
export const setNetworkConfig = (
hass: HomeAssistant,
configured_adapters: string[]
) =>
hass.callWS<string[]>({
type: "network/configure",
config: {
configured_adapters: configured_adapters,
},
});

View File

@@ -3,17 +3,17 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress";
import "../../components/ha-dialog";
import "../../components/ha-formfield";
import "../../components/ha-switch";
import type { HaSwitch } from "../../components/ha-switch";
import {
ConfigEntryMutableParams,
updateConfigEntry,
getConfigEntrySystemOptions,
updateConfigEntrySystemOptions,
} from "../../data/config_entries";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box";
import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
@customElement("dialog-config-entry-system-options")
@@ -22,12 +22,12 @@ class DialogConfigEntrySystemOptions extends LitElement {
@state() private _disableNewEntities!: boolean;
@state() private _disablePolling!: boolean;
@state() private _error?: string;
@state() private _params?: ConfigEntrySystemOptionsDialogParams;
@state() private _loading = false;
@state() private _submitting = false;
public async showDialog(
@@ -35,8 +35,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
): Promise<void> {
this._params = params;
this._error = undefined;
this._disableNewEntities = params.entry.pref_disable_new_entities;
this._disablePolling = params.entry.pref_disable_polling;
this._loading = true;
const systemOptions = await getConfigEntrySystemOptions(
this.hass,
params.entry.entry_id
);
this._loading = false;
this._disableNewEntities = systemOptions.disable_new_entities;
}
public closeDialog(): void {
@@ -61,57 +66,45 @@ class DialogConfigEntrySystemOptions extends LitElement {
this._params.entry.domain
)}
>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<ha-formfield
.label=${html`<p>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
)}
</p>
<p class="secondary">
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
"integration",
this.hass.localize(
`component.${this._params.entry.domain}.title`
) || this._params.entry.domain
)}
</p>`}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
></ha-switch>
</ha-formfield>
${this._allowUpdatePolling()
? html`
<ha-formfield
.label=${html`<p>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_polling_label"
)}
</p>
<p class="secondary">
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_polling_description",
"integration",
this.hass.localize(
`component.${this._params.entry.domain}.title`
) || this._params.entry.domain
)}
</p>`}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${!this._disablePolling}
@change=${this._disablePollingChanged}
.disabled=${this._submitting}
></ha-switch>
</ha-formfield>
`
: ""}
<div>
${this._loading
? html`
<div class="init-spinner">
<ha-circular-progress active></ha-circular-progress>
</div>
`
: html`
${this._error
? html` <div class="error">${this._error}</div> `
: ""}
<div class="form">
<ha-formfield
.label=${html`<p>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
)}
</p>
<p class="secondary">
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
"integration",
this.hass.localize(
`component.${this._params.entry.domain}.title`
) || this._params.entry.domain
)}
</p>`}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
>
</ha-switch>
</ha-formfield>
</div>
`}
</div>
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
@@ -122,7 +115,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
<mwc-button
slot="primaryAction"
@click="${this._updateEntry}"
.disabled=${this._submitting}
.disabled=${this._submitting || this._loading}
>
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
</mwc-button>
@@ -130,47 +123,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
`;
}
private _allowUpdatePolling() {
return (
this._params!.manifest &&
(this._params!.manifest.iot_class === "local_polling" ||
this._params!.manifest.iot_class === "cloud_polling")
);
}
private _disableNewEntitiesChanged(ev: Event): void {
this._error = undefined;
this._disableNewEntities = !(ev.target as HaSwitch).checked;
}
private _disablePollingChanged(ev: Event): void {
this._error = undefined;
this._disablePolling = !(ev.target as HaSwitch).checked;
}
private async _updateEntry(): Promise<void> {
this._submitting = true;
const data: ConfigEntryMutableParams = {
pref_disable_new_entities: this._disableNewEntities,
};
if (this._allowUpdatePolling()) {
data.pref_disable_polling = this._disablePolling;
}
try {
const result = await updateConfigEntry(
await updateConfigEntrySystemOptions(
this.hass,
this._params!.entry.entry_id,
data
{
disable_new_entities: this._disableNewEntities,
}
);
if (result.require_restart) {
await showAlertDialog(this, {
text: this.hass.localize(
"ui.dialogs.config_entry_system_options.restart_home_assistant"
),
});
}
this._params!.entryUpdated(result.config_entry);
this.closeDialog();
this._params = undefined;
} catch (err) {
this._error = err.message || "Unknown error";
} finally {
@@ -182,6 +150,20 @@ class DialogConfigEntrySystemOptions extends LitElement {
return [
haStyleDialog,
css`
.init-spinner {
padding: 50px 100px;
text-align: center;
}
.form {
padding-top: 6px;
padding-bottom: 24px;
color: var(--primary-text-color);
}
.secondary {
color: var(--secondary-text-color);
}
.error {
color: var(--error-color);
}

View File

@@ -1,11 +1,12 @@
import { fireEvent } from "../../common/dom/fire_event";
import { ConfigEntry } from "../../data/config_entries";
import { IntegrationManifest } from "../../data/integration";
export interface ConfigEntrySystemOptionsDialogParams {
entry: ConfigEntry;
manifest?: IntegrationManifest;
entryUpdated(entry: ConfigEntry): void;
// updateEntry: (
// updates: Partial<EntityRegistryEntryUpdateParams>
// ) => Promise<unknown>;
// removeEntry: () => Promise<boolean>;
}
export const loadConfigEntrySystemOptionsDialog = () =>

View File

@@ -49,7 +49,7 @@ class DialogBox extends LitElement {
open
?scrimClickAction=${confirmPrompt}
?escapeKeyAction=${confirmPrompt}
@closed=${this._dialogClosed}
@closing=${this._dialogClosed}
defaultAction="ignore"
.heading=${this._params.title
? this._params.title

View File

@@ -0,0 +1,74 @@
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { enableWrite } from "../common/auth/token_storage";
import LocalizeMixin from "../mixins/localize-mixin";
import "../styles/polymer-ha-style";
class HaStoreAuth extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
paper-card {
position: fixed;
padding: 8px 0;
bottom: 16px;
right: 16px;
}
.card-content {
color: var(--primary-text-color);
}
.card-actions {
text-align: right;
border-top: 0;
margin-right: -4px;
}
:host(.small) paper-card {
bottom: 0;
left: 0;
right: 0;
}
</style>
<paper-card elevation="4">
<div class="card-content">[[localize('ui.auth_store.ask')]]</div>
<div class="card-actions">
<mwc-button on-click="_done"
>[[localize('ui.auth_store.decline')]]</mwc-button
>
<mwc-button raised on-click="_save"
>[[localize('ui.auth_store.confirm')]]</mwc-button
>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
};
}
ready() {
super.ready();
this.classList.toggle("small", window.innerWidth < 600);
}
_save() {
enableWrite();
this._done();
}
_done() {
const card = this.shadowRoot.querySelector("paper-card");
card.style.transition = "bottom .25s";
card.style.bottom = `-${card.offsetHeight + 8}px`;
setTimeout(() => this.parentNode.removeChild(this), 300);
}
}
customElements.define("ha-store-auth-card", HaStoreAuth);

View File

@@ -1,78 +0,0 @@
import { LitElement, TemplateResult, html, css } from "lit";
import { property } from "lit/decorators";
import { enableWrite } from "../common/auth/token_storage";
import { HomeAssistant } from "../types";
import "../components/ha-card";
import type { HaCard } from "../components/ha-card";
import "@material/mwc-button/mwc-button";
class HaStoreAuth extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
${this.hass.localize("ui.auth_store.ask")}
</div>
<div class="card-actions">
<mwc-button @click=${this._dismiss}>
${this.hass.localize("ui.auth_store.decline")}
</mwc-button>
<mwc-button raised @click=${this._save}>
${this.hass.localize("ui.auth_store.confirm")}
</mwc-button>
</div>
</ha-card>
`;
}
firstUpdated() {
this.classList.toggle("small", window.innerWidth < 600);
}
private _save(): void {
enableWrite();
this._dismiss();
}
private _dismiss(): void {
const card = this.shadowRoot!.querySelector("ha-card") as HaCard;
card.style.bottom = `-${card.offsetHeight + 8}px`;
setTimeout(() => this.parentNode!.removeChild(this), 300);
}
static get styles() {
return css`
ha-card {
position: fixed;
padding: 8px 0;
bottom: 16px;
right: 16px;
transition: bottom 0.25s;
--ha-card-box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2),
0px 6px 10px 0px rgba(0, 0, 0, 0.14),
0px 1px 18px 0px rgba(0, 0, 0, 0.12);
}
.card-actions {
text-align: right;
border-top: 0;
}
:host(.small) ha-card {
bottom: 0;
left: 0;
right: 0;
}
`;
}
}
customElements.define("ha-store-auth-card", HaStoreAuth);
declare global {
interface HTMLElementTagNameMap {
"ha-store-auth-card": HaStoreAuth;
}
}

View File

@@ -108,7 +108,7 @@ class MoreInfoWeather extends LitElement {
this.stateObj.attributes.pressure,
this.hass.locale
)}
${getWeatherUnit(this.hass, "pressure")}
${getWeatherUnit(this.hass, "air_pressure")}
</div>
</div>
`

View File

@@ -8,7 +8,7 @@ import "../../components/state-history-charts";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import { loadTraceContexts, TraceContexts } from "../../data/trace";
import "../../panels/logbook/ha-logbook";
import { haStyle } from "../../resources/styles";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { closeDialog } from "../make-dialog-manager";
@@ -52,6 +52,7 @@ export class MoreInfoLogbook extends LitElement {
: this._logbookEntries.length
? html`
<ha-logbook
class="ha-scrollbar"
narrow
no-icon
no-name
@@ -148,6 +149,7 @@ export class MoreInfoLogbook extends LitElement {
static get styles() {
return [
haStyle,
haStyleScrollbar,
css`
.no-entries {
text-align: center;
@@ -155,11 +157,12 @@ export class MoreInfoLogbook extends LitElement {
color: var(--secondary-text-color);
}
ha-logbook {
--logbook-max-height: 250px;
max-height: 250px;
overflow: auto;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-logbook {
--logbook-max-height: unset;
max-height: unset;
}
}
ha-circular-progress {

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "../../resources/lit-virtualizer";
import { Layout1d, scroll } from "@lit-labs/virtualizer";
import "@material/mwc-list/mwc-list";
import type { List } from "@material/mwc-list/mwc-list";
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
@@ -188,6 +188,7 @@ export class QuickBar extends LitElement {
${scroll({
items,
layout: Layout1d,
// @ts-expect-error
renderItem: (item: QuickBarItem, index) =>
this._renderItem(item, index),
})}
@@ -222,9 +223,6 @@ export class QuickBar extends LitElement {
}
private _renderItem(item: QuickBarItem, index?: number) {
if (!item) {
return html``;
}
return isCommandItem(item)
? this._renderCommandItem(item, index)
: this._renderEntityItem(item as EntityItem, index);
@@ -638,6 +636,18 @@ export class QuickBar extends LitElement {
margin-left: 8px;
}
.uni-virtualizer-host {
display: block;
position: relative;
contain: strict;
overflow: auto;
height: 100%;
}
.uni-virtualizer-host > * {
box-sizing: border-box;
}
mwc-list-item {
width: 100%;
}

View File

@@ -277,7 +277,7 @@ export const provideHass = (
mockTheme(theme) {
invalidateThemeCache();
hass().updateHass({
selectedTheme: { theme: theme ? "mock" : "default" },
selectedThemeSettings: { theme: theme ? "mock" : "default" },
themes: {
...hass().themes,
themes: {
@@ -285,11 +285,11 @@ export const provideHass = (
},
},
});
const { themes, selectedTheme } = hass();
const { themes, selectedThemeSettings } = hass();
applyThemesOnElement(
document.documentElement,
themes,
selectedTheme!.theme
selectedThemeSettings!.theme
);
},

View File

@@ -229,7 +229,7 @@ class HassTabsSubpage extends LitElement {
color: var(--sidebar-text-color);
text-decoration: none;
}
.bottom-bar a {
:host([narrow]) .toolbar a {
width: 25%;
}

View File

@@ -81,25 +81,27 @@ class SupervisorErrorScreen extends LitElement {
private _applyTheme() {
let themeName: string;
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined;
let themeSettings:
| Partial<HomeAssistant["selectedThemeSettings"]>
| undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName =
this.hass.selectedTheme?.theme ||
this.hass.selectedThemeSettings?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme;
themeSettings = this.hass.selectedThemeSettings;
if (themeName === "default" && themeSettings?.dark === undefined) {
themeSettings = {
...this.hass.selectedTheme,
...this.hass.selectedThemeSettings,
dark: this.hass.themes.darkMode,
};
}
} else {
themeName =
((this.hass.selectedTheme as unknown) as string) ||
((this.hass.selectedThemeSettings as unknown) as string) ||
this.hass.themes.default_theme;
}

View File

@@ -90,9 +90,9 @@ export class HAFullCalendar extends LitElement {
@property() public initialView: FullCalendarView = "dayGridMonth";
private calendar?: Calendar;
@state() private calendar?: Calendar;
@state() private _activeView = this.initialView;
@state() private _activeView?: FullCalendarView;
public updateSize(): void {
this.calendar?.updateSize();
@@ -181,8 +181,8 @@ export class HAFullCalendar extends LitElement {
`;
}
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this.calendar) {
return;
@@ -216,6 +216,8 @@ export class HAFullCalendar extends LitElement {
initialView: this.initialView,
};
this._activeView = this.initialView;
config.dateClick = (info) => this._handleDateClick(info);
config.eventClick = (info) => this._handleEventClick(info);

View File

@@ -48,11 +48,9 @@ class PanelCalendar extends LitElement {
private _end?: Date;
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._calendars = getCalendars(this.hass);
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._calendars = getCalendars(this.hass);
}
protected render(): TemplateResult {

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml";
import { safeDump } from "js-yaml";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-code-editor";
@@ -15,7 +15,7 @@ export class HaAutomationTraceBlueprintConfig extends LitElement {
protected render(): TemplateResult {
return html`
<ha-code-editor
.value=${dump(this.trace.blueprint_inputs || "").trimRight()}
.value=${safeDump(this.trace.blueprint_inputs || "").trimRight()}
readOnly
></ha-code-editor>
`;

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml";
import { safeDump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-code-editor";
@@ -15,7 +15,7 @@ export class HaAutomationTraceConfig extends LitElement {
protected render(): TemplateResult {
return html`
<ha-code-editor
.value=${dump(this.trace.config).trimRight()}
.value=${safeDump(this.trace.config).trimRight()}
readOnly
></ha-code-editor>
`;

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml";
import { safeDump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -132,13 +132,13 @@ export class HaAutomationTracePathDetails extends LitElement {
)}<br />
${result
? html`Result:
<pre>${dump(result)}</pre>`
<pre>${safeDump(result)}</pre>`
: error
? html`<div class="error">Error: ${error}</div>`
: ""}
${Object.keys(rest).length === 0
? ""
: html`<pre>${dump(rest)}</pre>`}
: html`<pre>${safeDump(rest)}</pre>`}
`;
})
);
@@ -154,7 +154,7 @@ export class HaAutomationTracePathDetails extends LitElement {
const config = getDataFromPath(this.trace!.config, this.selected.path);
return config
? html`<ha-code-editor
.value=${dump(config).trimRight()}
.value=${safeDump(config).trimRight()}
readOnly
></ha-code-editor>`
: "Unable to find config";
@@ -171,7 +171,9 @@ export class HaAutomationTracePathDetails extends LitElement {
${idx > 0 ? html`<p>Iteration ${idx + 1}</p>` : ""}
${Object.keys(trace.changed_variables || {}).length === 0
? "No variables changed"
: html`<pre>${dump(trace.changed_variables).trimRight()}</pre>`}
: html`<pre>
${safeDump(trace.changed_variables).trimRight()}</pre
>`}
`
)}
</div>

View File

@@ -6,7 +6,7 @@ import { TagTrigger } from "../../../../../data/automation";
import { fetchTags, Tag } from "../../../../../data/tag";
import { HomeAssistant } from "../../../../../types";
import { TriggerElement } from "../ha-automation-trigger-row";
import "../../../../../components/ha-paper-dropdown-menu";
@customElement("ha-automation-trigger-tag")
export class HaTagTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;

View File

@@ -1,141 +0,0 @@
import "@material/mwc-button/mwc-button";
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-card";
import "../../../components/ha-checkbox";
import "../../../components/ha-network";
import "../../../components/ha-settings-row";
import { fetchNetworkInfo } from "../../../data/hassio/network";
import {
getNetworkConfig,
NetworkConfig,
setNetworkConfig,
} from "../../../data/network";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@customElement("ha-config-network")
class ConfigNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _networkConfig?: NetworkConfig;
@state() private _error?: string;
protected render(): TemplateResult {
if (
!this.hass.userData?.showAdvanced ||
!isComponentLoaded(this.hass, "network")
) {
return html``;
}
return html`
<ha-card header="Network">
<div class="card-content">
${this._error ? html`<div class="error">${this._error}</div>` : ""}
<p>
Configure which network adapters integrations will use. Currently
this setting only affects multicast traffic. A restart is required
for these settings to apply.
</p>
<ha-network
@network-config-changed=${this._configChanged}
.hass=${this.hass}
.networkConfig=${this._networkConfig}
></ha-network>
</div>
<div class="card-actions">
<mwc-button @click=${this._save}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.save_button"
)}
</mwc-button>
</div>
</ha-card>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (isComponentLoaded(this.hass, "network")) {
this._load();
}
}
private async _load() {
this._error = undefined;
try {
const coreNetwork = await getNetworkConfig(this.hass);
if (isComponentLoaded(this.hass, "hassio")) {
const supervisorNetwork = await fetchNetworkInfo(this.hass);
const interfaces = new Set(
supervisorNetwork.interfaces.map((int) => int.interface)
);
if (interfaces.size) {
coreNetwork.adapters = coreNetwork.adapters.filter((adapter) =>
interfaces.has(adapter.name)
);
}
}
this._networkConfig = coreNetwork;
} catch (err) {
this._error = err.message || err;
}
}
private async _save() {
this._error = undefined;
try {
await setNetworkConfig(
this.hass,
this._networkConfig?.configured_adapters || []
);
} catch (err) {
this._error = err.message || err;
}
}
private _configChanged(event: CustomEvent): void {
this._networkConfig = {
...this._networkConfig!,
configured_adapters: event.detail.configured_adapters,
};
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.error {
color: var(--error-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
}
`, // row-reverse so we tab first to "save"
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-network": ConfigNetwork;
}
}

View File

@@ -11,7 +11,6 @@ import "../ha-config-section";
import "./ha-config-analytics";
import "./ha-config-core-form";
import "./ha-config-name-form";
import "./ha-config-network";
import "./ha-config-url-form";
/*
@@ -31,7 +30,6 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
<ha-config-url-form hass="[[hass]]"></ha-config-url-form>
<ha-config-network hass="[[hass]]"></ha-config-network>
<ha-config-analytics hass="[[hass]]"></ha-config-analytics>
</ha-config-section>
`;

View File

@@ -49,7 +49,7 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
return html`
<ha-dialog
open
@closed=${this._close}
@closing=${this._close}
.heading="${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.title",
"device",

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml";
import { safeDump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -31,7 +31,7 @@ class MQTTDiscoveryPayload extends LitElement {
const payload = this.payload;
return html`
${this.showAsYaml
? html` <pre>${dump(payload)}</pre> `
? html` <pre>${safeDump(payload)}</pre> `
: html` <pre>${JSON.stringify(payload, null, 2)}</pre> `}
`;
}

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml";
import { safeDump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -92,7 +92,7 @@ class MQTTMessages extends LitElement {
return json
? html`
${this.showAsYaml
? html` <pre>${dump(json)}</pre> `
? html` <pre>${safeDump(json)}</pre> `
: html` <pre>${JSON.stringify(json, null, 2)}</pre> `}
`
: html` <code>${message.payload}</code> `;

View File

@@ -11,11 +11,7 @@ import { slugify } from "../../../common/string/slugify";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon-next";
import { AreaRegistryEntry } from "../../../data/area_registry";
import {
ConfigEntry,
disableConfigEntry,
DisableConfigEntryResult,
} from "../../../data/config_entries";
import { ConfigEntry, disableConfigEntry } from "../../../data/config_entries";
import {
computeDeviceName,
DeviceRegistryEntry,
@@ -29,10 +25,7 @@ import {
} from "../../../data/entity_registry";
import { SceneEntities, showSceneEditor } from "../../../data/scene";
import { findRelated, RelatedResult } from "../../../data/search";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
@@ -678,41 +671,13 @@ export class HaConfigDevicePage extends LitElement {
dismissText: this.hass.localize("ui.common.no"),
}))
) {
let result: DisableConfigEntryResult;
try {
// eslint-disable-next-line no-await-in-loop
result = await disableConfigEntry(this.hass, cnfg_entry);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_error"
),
text: err.message,
});
return;
}
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
),
});
}
disableConfigEntry(this.hass, cnfg_entry);
delete updates.disabled_by;
}
}
}
}
try {
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.devices.update_device_error"
),
text: err.message,
});
}
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
if (
!oldDeviceName ||

View File

@@ -68,7 +68,7 @@ export class DialogHelperDetail extends LitElement {
return html`
<ha-dialog
.open=${this._opened}
@closed=${this.closeDialog}
@closing=${this.closeDialog}
class=${classMap({ "button-left": !this._platform })}
scrimClickAction
escapeKeyAction

View File

@@ -193,6 +193,9 @@ class HaInputNumberForm extends LitElement {
.form {
color: var(--primary-text-color);
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}

View File

@@ -196,6 +196,9 @@ class HaInputSelectForm extends LitElement {
mwc-button {
margin-left: 8px;
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}

View File

@@ -179,6 +179,9 @@ class HaInputTextForm extends LitElement {
.row {
padding: 16px 0;
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}

View File

@@ -18,7 +18,6 @@ import {
ConfigEntry,
deleteConfigEntry,
disableConfigEntry,
DisableConfigEntryResult,
enableConfigEntry,
reloadConfigEntry,
updateConfigEntry,
@@ -111,7 +110,6 @@ export class HaIntegrationCard extends LitElement {
: undefined}
.localizedDomainName=${item ? item.localized_domain_name : undefined}
.manifest=${this.manifest}
.configEntry=${item}
>
${this.items.length > 1
? html`
@@ -468,11 +466,6 @@ export class HaIntegrationCard extends LitElement {
private _showSystemOptions(configEntry: ConfigEntry) {
showConfigEntrySystemOptionsDialog(this, {
entry: configEntry,
manifest: this.manifest,
entryUpdated: (entry) =>
fireEvent(this, "entry-updated", {
entry,
}),
});
}
@@ -488,18 +481,7 @@ export class HaIntegrationCard extends LitElement {
if (!confirmed) {
return;
}
let result: DisableConfigEntryResult;
try {
result = await disableConfigEntry(this.hass, entryId);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_error"
),
text: err.message,
});
return;
}
const result = await disableConfigEntry(this.hass, entryId);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
@@ -515,18 +497,7 @@ export class HaIntegrationCard extends LitElement {
private async _enableIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
let result: DisableConfigEntryResult;
try {
result = await enableConfigEntry(this.hass, entryId);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_error"
),
text: err.message,
});
return;
}
const result = await enableConfigEntry(this.hass, entryId);
if (result.require_restart) {
showAlertDialog(this, {
@@ -590,10 +561,10 @@ export class HaIntegrationCard extends LitElement {
if (newName === null) {
return;
}
const result = await updateConfigEntry(this.hass, configEntry.entry_id, {
const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, {
title: newName,
});
fireEvent(this, "entry-updated", { entry: result.config_entry });
fireEvent(this, "entry-updated", { entry: newEntry });
}
static get styles(): CSSResultGroup {

View File

@@ -1,9 +1,8 @@
import { mdiCloud, mdiPackageVariant, mdiSyncOff } from "@mdi/js";
import { mdiCloud, mdiPackageVariant } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-svg-icon";
import { ConfigEntry } from "../../../data/config_entries";
import { domainToName, IntegrationManifest } from "../../../data/integration";
import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
@@ -22,8 +21,6 @@ export class HaIntegrationHeader extends LitElement {
@property({ attribute: false }) public manifest?: IntegrationManifest;
@property({ attribute: false }) public configEntry?: ConfigEntry;
protected render(): TemplateResult {
let primary: string;
let secondary: string | undefined;
@@ -62,15 +59,6 @@ export class HaIntegrationHeader extends LitElement {
),
]);
}
if (this.configEntry?.pref_disable_polling) {
icons.push([
mdiSyncOff,
this.hass.localize(
"ui.panel.config.integrations.config_entry.disabled_polling"
),
]);
}
}
return html`

View File

@@ -79,7 +79,7 @@ class DialogOZWRefreshNode extends LitElement {
return html`
<ha-dialog
open
@closed="${this._close}"
@closing="${this._close}"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.ozw.refresh_node.title")

View File

@@ -61,7 +61,7 @@ class DialogZHACluster extends LitElement {
<ha-dialog
open
hideActions
@closed="${this._close}"
@closing="${this._close}"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.zha.clusters.header")

View File

@@ -36,7 +36,7 @@ class DialogZHADeviceZigbeeInfo extends LitElement {
<ha-dialog
open
hideActions
@closed="${this._close}"
@closing="${this._close}"
.heading=${createCloseHeading(
this.hass,
this.hass.localize(`ui.dialogs.zha_device_info.device_signature`)

View File

@@ -9,7 +9,7 @@ import {
} from "../../../../../data/zha";
import "../../../../../layouts/hass-tabs-subpage";
import type { HomeAssistant, Route } from "../../../../../types";
import { Network, Edge, Node, EdgeOptions } from "vis-network/peer";
import { Network, Edge, Node, EdgeOptions } from "vis-network";
import "../../../../../common/search/search-input";
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/ha-button-menu";

View File

@@ -74,7 +74,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
return html`
<ha-dialog
open
@closed="${this._close}"
@closing="${this._close}"
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(

View File

@@ -66,7 +66,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
return html`
<ha-dialog
open
@closed=${this._close}
@closing=${this._close}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(

View File

@@ -84,7 +84,7 @@ export class DialogAddUser extends LitElement {
return html`
<ha-dialog
open
@closed=${this._close}
@closing=${this._close}
scrimClickAction
escapeKeyAction
.heading=${this.hass.localize("ui.panel.config.users.add_user.caption")}

View File

@@ -55,7 +55,7 @@ class DialogUserDetail extends LitElement {
return html`
<ha-dialog
open
@closed=${this._close}
@closing=${this._close}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(this.hass, user.name)}

View File

@@ -4,7 +4,7 @@ import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { load } from "js-yaml";
import { safeLoad } from "js-yaml";
import "../../../components/ha-code-editor";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { EventsMixin } from "../../../mixins/events-mixin";
@@ -151,7 +151,7 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
_computeParsedEventData(eventData) {
try {
return eventData.trim() ? load(eventData) : {};
return eventData.trim() ? safeLoad(eventData) : {};
} catch (err) {
return ERROR_SENTINEL;
}

View File

@@ -1,6 +1,6 @@
import { mdiHelpCircle } from "@mdi/js";
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
import { load } from "js-yaml";
import { safeLoad } from "js-yaml";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -361,9 +361,9 @@ class HaPanelDevService extends LitElement {
const example = {};
fields.forEach((field) => {
if (field.example) {
let value: any = "";
let value = "";
try {
value = load(field.example);
value = safeLoad(field.example);
} catch (err) {
value = field.example;
}

View File

@@ -9,10 +9,9 @@ import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { dump, load } from "js-yaml";
import { safeDump, safeLoad } from "js-yaml";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import { computeRTL } from "../../../common/util/compute_rtl";
import { escapeRegExp } from "../../../common/string/escape_regexp";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-code-editor";
@@ -92,12 +91,6 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
--mdc-icon-size: 20px;
padding: 4px;
cursor: pointer;
flex-shrink: 0;
margin-right: 8px;
}
.entities td:nth-child(1) {
min-width: 300px;
width: 30%;
}
.entities td:nth-child(3) {
white-space: pre-wrap;
@@ -108,15 +101,6 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
color: var(--primary-color);
}
.entities .id-name-container {
display: flex;
flex-direction: column;
}
.entities .id-name-row {
display: flex;
align-items: center;
}
:host([narrow]) .state-wrapper {
flex-direction: column;
}
@@ -235,30 +219,19 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
<template is="dom-repeat" items="[[_entities]]" as="entity">
<tr>
<td>
<div class="id-name-container">
<div class="id-name-row">
<ha-svg-icon
on-click="copyEntity"
alt="[[localize('ui.panel.developer-tools.tabs.states.copy_id')]]"
title="[[localize('ui.panel.developer-tools.tabs.states.copy_id')]]"
path="[[clipboardOutlineIcon()]]"
></ha-svg-icon>
<a href="#" on-click="entitySelected"
>[[entity.entity_id]]</a
>
</div>
<div class="id-name-row">
<ha-svg-icon
on-click="entityMoreInfo"
alt="[[localize('ui.panel.developer-tools.tabs.states.more_info')]]"
title="[[localize('ui.panel.developer-tools.tabs.states.more_info')]]"
path="[[informationOutlineIcon()]]"
></ha-svg-icon>
<span class="secondary">
[[entity.attributes.friendly_name]]
</span>
</div>
</div>
<ha-svg-icon
on-click="entityMoreInfo"
alt="[[localize('ui.panel.developer-tools.tabs.states.more_info')]]"
title="[[localize('ui.panel.developer-tools.tabs.states.more_info')]]"
path="[[informationOutlineIcon()]]"
></ha-svg-icon>
<ha-svg-icon
on-click="copyEntity"
alt="[[localize('ui.panel.developer-tools.tabs.states.copy_id')]]"
title="[[localize('ui.panel.developer-tools.tabs.states.copy_id')]]"
path="[[clipboardOutlineIcon()]]"
></ha-svg-icon>
<a href="#" on-click="entitySelected">[[entity.entity_id]]</a>
</td>
<td>[[entity.state]]</td>
<template
@@ -359,7 +332,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
this._entityId = state.entity_id;
this._entity = state;
this._state = state.state;
this._stateAttributes = dump(state.attributes);
this._stateAttributes = safeDump(state.attributes);
ev.preventDefault();
}
@@ -376,7 +349,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
this._entity = state;
this._state = state.state;
this._stateAttributes = dump(state.attributes);
this._stateAttributes = safeDump(state.attributes);
}
entityMoreInfo(ev) {
@@ -412,68 +385,50 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter) {
const entityFilterRegExp =
_entityFilter &&
RegExp(escapeRegExp(_entityFilter).replace(/\\\*/g, ".*"), "i");
const stateFilterRegExp =
_stateFilter &&
RegExp(escapeRegExp(_stateFilter).replace(/\\\*/g, ".*"), "i");
let keyFilterRegExp;
let valueFilterRegExp;
let multiMode = false;
if (_attributeFilter) {
const colonIndex = _attributeFilter.indexOf(":");
multiMode = colonIndex !== -1;
const keyFilter = multiMode
? _attributeFilter.substring(0, colonIndex).trim()
: _attributeFilter;
const valueFilter = multiMode
? _attributeFilter.substring(colonIndex + 1).trim()
: _attributeFilter;
keyFilterRegExp = RegExp(
escapeRegExp(keyFilter).replace(/\\\*/g, ".*"),
"i"
);
valueFilterRegExp = multiMode
? RegExp(escapeRegExp(valueFilter).replace(/\\\*/g, ".*"), "i")
: keyFilterRegExp;
}
return Object.values(hass.states)
.filter((value) => {
if (
entityFilterRegExp &&
!entityFilterRegExp.test(value.entity_id) &&
(value.attributes.friendly_name === undefined ||
!entityFilterRegExp.test(value.attributes.friendly_name))
) {
return Object.keys(hass.states)
.map(function (key) {
return hass.states[key];
})
.filter(function (value) {
if (!value.entity_id.includes(_entityFilter.toLowerCase())) {
return false;
}
if (stateFilterRegExp && !stateFilterRegExp.test(value.state)) {
if (!value.state.toLowerCase().includes(_stateFilter.toLowerCase())) {
return false;
}
if (keyFilterRegExp && valueFilterRegExp) {
for (const [key, attributeValue] of Object.entries(
value.attributes
)) {
const match = keyFilterRegExp.test(key);
if (match && !multiMode) {
if (_attributeFilter !== "") {
const attributeFilter = _attributeFilter.toLowerCase();
const colonIndex = attributeFilter.indexOf(":");
const multiMode = colonIndex !== -1;
let keyFilter = attributeFilter;
let valueFilter = attributeFilter;
if (multiMode) {
// we need to filter keys and values separately
keyFilter = attributeFilter.substring(0, colonIndex).trim();
valueFilter = attributeFilter.substring(colonIndex + 1).trim();
}
const attributeKeys = Object.keys(value.attributes);
for (let i = 0; i < attributeKeys.length; i++) {
const key = attributeKeys[i];
if (key.includes(keyFilter) && !multiMode) {
return true; // in single mode we're already satisfied with this match
}
if (!match && multiMode) {
if (!key.includes(keyFilter) && multiMode) {
continue;
}
const attributeValue = value.attributes[key];
if (
attributeValue !== undefined &&
valueFilterRegExp.test(JSON.stringify(attributeValue))
JSON.stringify(attributeValue).toLowerCase().includes(valueFilter)
) {
return true;
}
@@ -538,7 +493,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
(!Array.isArray(value) && value instanceof Object)
) {
return `\n${dump(value)}`;
return `\n${safeDump(value)}`;
}
return Array.isArray(value) ? value.join(", ") : value;
}
@@ -553,7 +508,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
_computeParsedStateAttributes(stateAttributes) {
try {
return stateAttributes.trim() ? load(stateAttributes) : {};
return stateAttributes.trim() ? safeLoad(stateAttributes) : {};
} catch (err) {
return ERROR_SENTINEL;
}

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "../../resources/lit-virtualizer";
import { Layout1d, scroll } from "@lit-labs/virtualizer";
import {
css,
CSSResultGroup,
@@ -100,6 +100,7 @@ class HaLogbook extends LitElement {
? scroll({
items: this.entries,
layout: Layout1d,
// @ts-expect-error
renderItem: (item: LogbookEntry, index) =>
this._renderLogbookItem(item, index),
})
@@ -348,12 +349,16 @@ class HaLogbook extends LitElement {
color: var(--primary-color);
}
.container {
max-height: var(--logbook-max-height);
:host([virtualize]) .container {
display: block;
position: relative;
contain: strict;
height: 100%;
overflow: auto;
}
:host([virtualize]) .container {
height: 100%;
.container > * {
box-sizing: border-box;
}
.narrow .entry {

View File

@@ -18,7 +18,6 @@ import "../../../components/ha-card";
import "../../../components/ha-icon";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { formatAttributeValue } from "../../../util/hass-attributes-util";
import { computeCardSize } from "../common/compute-card-size";
import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";
@@ -125,12 +124,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
<div class="info">
<span class="value"
>${"attribute" in this._config
? stateObj.attributes[this._config.attribute!] !== undefined
? formatAttributeValue(
this.hass,
stateObj.attributes[this._config.attribute!]
)
: this.hass.localize("state.default.unknown")
? stateObj.attributes[this._config.attribute!] ??
this.hass.localize("state.default.unknown")
: stateObj.attributes.unit_of_measurement
? formatNumber(stateObj.state, this.hass.locale)
: computeStateDisplay(

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