mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-05 23:22:59 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45d78c0c77 | |||
| d97fb19f05 | |||
| 0dd3757df2 | |||
| c32a4546f3 | |||
| 1bb025ccd0 | |||
| 2b8033a97f | |||
| 21a3a8c594 | |||
| 1026e90296 | |||
| 0eca602e61 | |||
| 7f75ca81f1 | |||
| 8af05e2726 | |||
| 0a478ee1da | |||
| a4bdc5a05f | |||
| d425767dae | |||
| c78382c119 | |||
| ee15ddfbc3 | |||
| 0af14eb77e | |||
| 583cc4bc8a | |||
| 2ee92f48e6 | |||
| d05e02ab3e | |||
| abb9f8e233 | |||
| f873ef9b59 | |||
| 1255b56522 | |||
| fd9bb4d8cc | |||
| 9328576b55 | |||
| 70a1edd1dd |
+1
-1
@@ -70,7 +70,7 @@ class HaDemo extends HomeAssistantAppEl {
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
navigate(this, href);
|
||||
navigate(href);
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
|
||||
@@ -120,7 +120,7 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
}
|
||||
|
||||
private _addonTapped(ev) {
|
||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -138,7 +138,7 @@ class HassioAddonStore extends LitElement {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
const repositoryUrl = extractSearchParam("repository_url");
|
||||
navigate(this, "/hassio/store", true);
|
||||
navigate("/hassio/store", { replace: true });
|
||||
if (repositoryUrl) {
|
||||
this._manageRepositories(repositoryUrl);
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
if (!validAddon) {
|
||||
this._error = this.supervisor.localize("my.error_addon_not_found");
|
||||
} else {
|
||||
navigate(this, `/hassio/addon/${requestedAddon}`, true);
|
||||
navigate(`/hassio/addon/${requestedAddon}`, { replace: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,7 +761,7 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private _openIngress(): void {
|
||||
navigate(this, `/hassio/ingress/${this.addon.slug}`);
|
||||
navigate(`/hassio/ingress/${this.addon.slug}`);
|
||||
}
|
||||
|
||||
private get _computeShowIngressUI(): boolean {
|
||||
@@ -1051,7 +1051,7 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private _openConfiguration(): void {
|
||||
navigate(this, `/hassio/addon/${this.addon.slug}/config`);
|
||||
navigate(`/hassio/addon/${this.addon.slug}/config`);
|
||||
}
|
||||
|
||||
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
|
||||
@customElement("supervisor-formfield-label")
|
||||
class SupervisorFormfieldLabel extends LitElement {
|
||||
@property({ type: String }) public label!: string;
|
||||
|
||||
@property({ type: String }) public imageUrl?: string;
|
||||
|
||||
@property({ type: String }) public iconPath?: string;
|
||||
|
||||
@property({ type: String }) public version?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.imageUrl
|
||||
? html`<img loading="lazy" .src=${this.imageUrl} class="icon" />`
|
||||
: this.iconPath
|
||||
? html`<ha-svg-icon .path=${this.iconPath} class="icon"></ha-svg-icon>`
|
||||
: ""}
|
||||
<span class="label">${this.label}</span>
|
||||
${this.version
|
||||
? html`<span class="version">(${this.version})</span>`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.version {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icon {
|
||||
max-height: 22px;
|
||||
max-width: 22px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-formfield-label": SupervisorFormfieldLabel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
||||
import {
|
||||
HassioFullSnapshotCreateParams,
|
||||
HassioPartialSnapshotCreateParams,
|
||||
HassioSnapshotDetail,
|
||||
} from "../../../src/data/hassio/snapshot";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "./supervisor-formfield-label";
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
checked: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AddonCheckboxItem extends CheckboxItem {
|
||||
version: string;
|
||||
}
|
||||
|
||||
const _computeFolders = (folders): CheckboxItem[] => {
|
||||
const list: CheckboxItem[] = [];
|
||||
if (folders.includes("homeassistant")) {
|
||||
list.push({
|
||||
slug: "homeassistant",
|
||||
name: "Home Assistant configuration",
|
||||
checked: false,
|
||||
});
|
||||
}
|
||||
if (folders.includes("ssl")) {
|
||||
list.push({ slug: "ssl", name: "SSL", checked: false });
|
||||
}
|
||||
if (folders.includes("share")) {
|
||||
list.push({ slug: "share", name: "Share", checked: false });
|
||||
}
|
||||
if (folders.includes("addons/local")) {
|
||||
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
|
||||
}
|
||||
return list.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
};
|
||||
|
||||
const _computeAddons = (addons): AddonCheckboxItem[] =>
|
||||
addons
|
||||
.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
version: addon.version,
|
||||
checked: false,
|
||||
}))
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
|
||||
@customElement("supervisor-snapshot-content")
|
||||
export class SupervisorSnapshotContent extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public snapshot?: HassioSnapshotDetail;
|
||||
|
||||
@property() public snapshotType: HassioSnapshotDetail["type"] = "full";
|
||||
|
||||
@property({ attribute: false }) public folders?: CheckboxItem[];
|
||||
|
||||
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
|
||||
|
||||
@property({ type: Boolean }) public homeAssistant = false;
|
||||
|
||||
@property({ type: Boolean }) public snapshotHasPassword = false;
|
||||
|
||||
@property() public snapshotName = "";
|
||||
|
||||
@property() public snapshotPassword = "";
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
this.folders = _computeFolders(
|
||||
this.snapshot
|
||||
? this.snapshot.folders
|
||||
: ["homeassistant", "ssl", "share", "media", "addons/local"]
|
||||
);
|
||||
this.addons = _computeAddons(
|
||||
this.snapshot
|
||||
? this.snapshot.addons
|
||||
: this.supervisor?.supervisor.addons
|
||||
);
|
||||
this.snapshotType = this.snapshot?.type || "full";
|
||||
this.snapshotName = this.snapshot?.name || "";
|
||||
this.snapshotHasPassword = this.snapshot?.protected || false;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.supervisor) {
|
||||
return html``;
|
||||
}
|
||||
const foldersSection =
|
||||
this.snapshotType === "partial" ? this._getSection("folders") : undefined;
|
||||
const addonsSection =
|
||||
this.snapshotType === "partial" ? this._getSection("addons") : undefined;
|
||||
|
||||
return html`
|
||||
${this.snapshot
|
||||
? html`<div class="details">
|
||||
${this.snapshot.type === "full"
|
||||
? this.supervisor.localize("snapshot.full_snapshot")
|
||||
: this.supervisor.localize("snapshot.partial_snapshot")}
|
||||
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
|
||||
${formatDateTime(new Date(this.snapshot.date), this.hass.locale)}
|
||||
</div>`
|
||||
: html`<paper-input
|
||||
name="snapshotName"
|
||||
.label=${this.supervisor.localize("snapshot.name")}
|
||||
.value=${this.snapshotName}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
>
|
||||
</paper-input>`}
|
||||
${!this.snapshot || this.snapshot.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.snapshot
|
||||
? this.supervisor.localize("snapshot.type")
|
||||
: this.supervisor.localize("snapshot.select_type")}
|
||||
</div>
|
||||
<div class="snapshot-types">
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize("snapshot.full_snapshot")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
name="snapshotType"
|
||||
.checked=${this.snapshotType === "full"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor!.localize("snapshot.partial_snapshot")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
name="snapshotType"
|
||||
.checked=${this.snapshotType === "partial"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.snapshot && this.snapshotType === "partial"
|
||||
? html`
|
||||
${this.snapshot.homeassistant
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
label="Home Assistant"
|
||||
.iconPath=${mdiHomeAssistant}
|
||||
.version=${this.snapshot.homeassistant}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.homeAssistant}
|
||||
@click=${() => {
|
||||
this.homeAssistant = !this.homeAssistant;
|
||||
}}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
${this.snapshotType === "partial"
|
||||
? html`
|
||||
${foldersSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor.localize("snapshot.folders")}
|
||||
.iconPath=${mdiFolder}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._toggleSection}
|
||||
.checked=${foldersSection.checked}
|
||||
.indeterminate=${foldersSection.indeterminate}
|
||||
.section=${"folders"}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
<div class="section-content">${foldersSection.templates}</div>
|
||||
`
|
||||
: ""}
|
||||
${addonsSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor.localize("snapshot.addons")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._toggleSection}
|
||||
.checked=${addonsSection.checked}
|
||||
.indeterminate=${addonsSection.indeterminate}
|
||||
.section=${"addons"}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
<div class="section-content">${addonsSection.templates}</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
${!this.snapshot
|
||||
? html`<ha-formfield
|
||||
.label=${this.supervisor.localize("snapshot.password_protection")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.snapshotHasPassword}
|
||||
@change=${this._toggleHasPassword}
|
||||
>
|
||||
</ha-checkbox
|
||||
></ha-formfield>`
|
||||
: ""}
|
||||
${this.snapshotHasPassword
|
||||
? html`
|
||||
<paper-input
|
||||
.label=${this.supervisor.localize("snapshot.password")}
|
||||
type="password"
|
||||
name="snapshotPassword"
|
||||
.value=${this.snapshotPassword}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-checkbox {
|
||||
--mdc-checkbox-touch-target-size: 16px;
|
||||
display: block;
|
||||
margin: 4px 12px 8px 0;
|
||||
}
|
||||
ha-formfield {
|
||||
display: contents;
|
||||
}
|
||||
supervisor-formfield-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
paper-input[type="password"] {
|
||||
display: block;
|
||||
margin: 4px 0 4px 16px;
|
||||
}
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 16px;
|
||||
}
|
||||
.security {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.snapshot-types {
|
||||
display: flex;
|
||||
}
|
||||
.sub-header {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
public snapshotDetails():
|
||||
| HassioPartialSnapshotCreateParams
|
||||
| HassioFullSnapshotCreateParams {
|
||||
const data: any = {};
|
||||
|
||||
if (!this.snapshot) {
|
||||
data.name = this.snapshotName || formatDate(new Date(), this.hass.locale);
|
||||
}
|
||||
|
||||
if (this.snapshotHasPassword) {
|
||||
data.password = this.snapshotPassword;
|
||||
}
|
||||
|
||||
if (this.snapshotType === "full") {
|
||||
return data;
|
||||
}
|
||||
|
||||
const addons = this.addons
|
||||
?.filter((addon) => addon.checked)
|
||||
.map((addon) => addon.slug);
|
||||
const folders = this.folders
|
||||
?.filter((folder) => folder.checked)
|
||||
.map((folder) => folder.slug);
|
||||
|
||||
if (addons?.length) {
|
||||
data.addons = addons;
|
||||
}
|
||||
if (folders?.length) {
|
||||
data.folders = folders;
|
||||
}
|
||||
if (this.homeAssistant) {
|
||||
data.homeassistant = this.homeAssistant;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private _getSection(section: string) {
|
||||
const templates: TemplateResult[] = [];
|
||||
const addons =
|
||||
section === "addons"
|
||||
? new Map(
|
||||
this.supervisor!.addon.addons.map((item) => [item.slug, item])
|
||||
)
|
||||
: undefined;
|
||||
let checkedItems = 0;
|
||||
this[section].forEach((item) => {
|
||||
templates.push(html`<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${item.name}
|
||||
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
|
||||
.imageUrl=${section === "addons" &&
|
||||
atLeastVersion(this.hass.config.version, 0, 105) &&
|
||||
addons?.get(item.slug)?.icon
|
||||
? `/api/hassio/addons/${item.slug}/icon`
|
||||
: undefined}
|
||||
.version=${item.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.item=${item}
|
||||
.checked=${item.checked}
|
||||
.section=${section}
|
||||
@change=${this._updateSectionEntry}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`);
|
||||
|
||||
if (item.checked) {
|
||||
checkedItems++;
|
||||
}
|
||||
});
|
||||
|
||||
const checked = checkedItems === this[section].length;
|
||||
|
||||
return {
|
||||
templates,
|
||||
checked,
|
||||
indeterminate: !checked && checkedItems !== 0,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this[input.name] = input.value;
|
||||
}
|
||||
|
||||
private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
|
||||
const input = ev.currentTarget as PaperInputElement;
|
||||
this[input.name!] = ev.detail.value;
|
||||
}
|
||||
|
||||
private _toggleHasPassword(): void {
|
||||
this.snapshotHasPassword = !this.snapshotHasPassword;
|
||||
}
|
||||
|
||||
private _toggleSection(ev): void {
|
||||
const section = ev.currentTarget.section;
|
||||
|
||||
this[section] = (section === "addons" ? this.addons : this.folders)!.map(
|
||||
(item) => ({
|
||||
...item,
|
||||
checked: ev.currentTarget.checked,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _updateSectionEntry(ev): void {
|
||||
const item = ev.currentTarget.item;
|
||||
const section = ev.currentTarget.section;
|
||||
this[section] = this[section].map((entry) =>
|
||||
entry.slug === item.slug
|
||||
? {
|
||||
...entry,
|
||||
checked: ev.currentTarget.checked,
|
||||
}
|
||||
: entry
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-snapshot-content": SupervisorSnapshotContent;
|
||||
}
|
||||
}
|
||||
@@ -96,11 +96,11 @@ class HassioAddons extends LitElement {
|
||||
}
|
||||
|
||||
private _addonTapped(ev: any): void {
|
||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}/info`);
|
||||
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}/info`);
|
||||
}
|
||||
|
||||
private _openStore(): void {
|
||||
navigate(this, "/hassio/store");
|
||||
navigate("/hassio/store");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,91 +1,43 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatDate } from "../../../../src/common/datetime/format_date";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { compare } from "../../../../src/common/string/compare";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../../src/components/ha-checkbox";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../../src/components/ha-radio";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
createHassioFullSnapshot,
|
||||
createHassioPartialSnapshot,
|
||||
HassioFullSnapshotCreateParams,
|
||||
HassioPartialSnapshotCreateParams,
|
||||
HassioSnapshot,
|
||||
} from "../../../../src/data/hassio/snapshot";
|
||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/supervisor-snapshot-content";
|
||||
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
|
||||
import { HassioCreateSnapshotDialogParams } from "./show-dialog-hassio-create-snapshot";
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
checked: boolean;
|
||||
name?: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
const folderList = () => [
|
||||
{
|
||||
slug: "homeassistant",
|
||||
checked: true,
|
||||
},
|
||||
{ slug: "ssl", checked: true },
|
||||
{ slug: "share", checked: true },
|
||||
{ slug: "media", checked: true },
|
||||
{ slug: "addons/local", checked: true },
|
||||
];
|
||||
|
||||
@customElement("dialog-hassio-create-snapshot")
|
||||
class HassioCreateSnapshotDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _snapshotName = "";
|
||||
|
||||
@state() private _snapshotPassword = "";
|
||||
|
||||
@state() private _snapshotHasPassword = false;
|
||||
|
||||
@state() private _snapshotType: HassioSnapshot["type"] = "full";
|
||||
|
||||
@state() private _dialogParams?: HassioCreateSnapshotDialogParams;
|
||||
|
||||
@state() private _addonList: CheckboxItem[] = [];
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _folderList: CheckboxItem[] = folderList();
|
||||
@state() private _creatingSnapshot = false;
|
||||
|
||||
@state() private _error = "";
|
||||
@query("supervisor-snapshot-content")
|
||||
private _snapshotContent!: SupervisorSnapshotContent;
|
||||
|
||||
public showDialog(params: HassioCreateSnapshotDialogParams) {
|
||||
this._dialogParams = params;
|
||||
this._addonList = this._dialogParams.supervisor.supervisor.addons
|
||||
.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
version: addon.version,
|
||||
checked: true,
|
||||
}))
|
||||
.sort((a, b) => compare(a.name, b.name));
|
||||
this._snapshotType = "full";
|
||||
this._error = "";
|
||||
this._folderList = folderList();
|
||||
this._snapshotHasPassword = false;
|
||||
this._snapshotPassword = "";
|
||||
this._snapshotName = "";
|
||||
this._creatingSnapshot = false;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialogParams = undefined;
|
||||
this._creatingSnapshot = false;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -96,179 +48,35 @@ class HassioCreateSnapshotDialog extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closing=${this.closeDialog}
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._dialogParams.supervisor.localize("snapshot.create_snapshot")
|
||||
)}
|
||||
>
|
||||
<paper-input
|
||||
name="snapshotName"
|
||||
.label=${this._dialogParams.supervisor.localize("snapshot.name")}
|
||||
.value=${this._snapshotName}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
<div class="snapshot-types">
|
||||
<div>
|
||||
${this._dialogParams.supervisor.localize("snapshot.type")}:
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${this._dialogParams.supervisor.localize(
|
||||
"snapshot.full_snapshot"
|
||||
)}
|
||||
${this._creatingSnapshot
|
||||
? html` <ha-circular-progress active></ha-circular-progress>`
|
||||
: html`<supervisor-snapshot-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
name="snapshotType"
|
||||
.checked=${this._snapshotType === "full"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this._dialogParams.supervisor.localize(
|
||||
"snapshot.partial_snapshot"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
name="snapshotType"
|
||||
.checked=${this._snapshotType === "partial"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
|
||||
${
|
||||
this._snapshotType === "full"
|
||||
? undefined
|
||||
: html`
|
||||
${this._dialogParams.supervisor.localize("snapshot.folders")}:
|
||||
<div class="checkbox-section">
|
||||
${this._folderList.map(
|
||||
(folder, idx) => html`
|
||||
<div class="checkbox-line">
|
||||
<ha-checkbox
|
||||
.idx=${idx}
|
||||
.checked=${folder.checked}
|
||||
@change=${this._folderChecked}
|
||||
slot="prefix"
|
||||
>
|
||||
</ha-checkbox>
|
||||
<span>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
`snapshot.folder.${folder.slug}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
${this._dialogParams.supervisor.localize("snapshot.addons")}:
|
||||
<div class="checkbox-section">
|
||||
${this._addonList.map(
|
||||
(addon, idx) => html`
|
||||
<div class="checkbox-line">
|
||||
<ha-checkbox
|
||||
.idx=${idx}
|
||||
.checked=${addon.checked}
|
||||
@change=${this._addonChecked}
|
||||
slot="prefix"
|
||||
>
|
||||
</ha-checkbox>
|
||||
<span>
|
||||
${addon.name}<span class="version">
|
||||
(${addon.version})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
${this._dialogParams.supervisor.localize("snapshot.security")}:
|
||||
<div class="checkbox-section">
|
||||
<div class="checkbox-line">
|
||||
<ha-checkbox
|
||||
.checked=${this._snapshotHasPassword}
|
||||
@change=${this._handleCheckboxValueChanged}
|
||||
slot="prefix"
|
||||
>
|
||||
</ha-checkbox>
|
||||
<span>
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"snapshot.password_protection"
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
this._snapshotHasPassword
|
||||
? html`
|
||||
<paper-input
|
||||
.label=${this._dialogParams.supervisor.localize(
|
||||
"snapshot.password"
|
||||
)}
|
||||
type="password"
|
||||
name="snapshotPassword"
|
||||
.value=${this._snapshotPassword}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
`
|
||||
: undefined
|
||||
}
|
||||
${
|
||||
this._error !== ""
|
||||
? html` <p class="error">${this._error}</p> `
|
||||
: undefined
|
||||
}
|
||||
</supervisor-snapshot-content>`}
|
||||
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
|
||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
${this._dialogParams.supervisor.localize("common.close")}
|
||||
</mwc-button>
|
||||
<ha-progress-button slot="primaryAction" @click=${this._createSnapshot}>
|
||||
<mwc-button
|
||||
.disabled=${this._creatingSnapshot}
|
||||
slot="primaryAction"
|
||||
@click=${this._createSnapshot}
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("snapshot.create")}
|
||||
</ha-progress-button>
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
|
||||
const input = ev.currentTarget as PaperInputElement;
|
||||
this[`_${input.name}`] = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCheckboxValueChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaCheckbox;
|
||||
this._snapshotHasPassword = input.checked;
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this[`_${input.name}`] = input.value;
|
||||
}
|
||||
|
||||
private _folderChecked(ev) {
|
||||
const { idx, checked } = ev.currentTarget!;
|
||||
this._folderList = this._folderList.map((folder, curIdx) =>
|
||||
curIdx === idx ? { ...folder, checked } : folder
|
||||
);
|
||||
}
|
||||
|
||||
private _addonChecked(ev) {
|
||||
const { idx, checked } = ev.currentTarget!;
|
||||
this._addonList = this._addonList.map((addon, curIdx) =>
|
||||
curIdx === idx ? { ...addon, checked } : addon
|
||||
);
|
||||
}
|
||||
|
||||
private async _createSnapshot(ev: CustomEvent): Promise<void> {
|
||||
private async _createSnapshot(): Promise<void> {
|
||||
if (this._dialogParams!.supervisor.info.state !== "running") {
|
||||
showAlertDialog(this, {
|
||||
title: this._dialogParams!.supervisor.localize(
|
||||
@@ -282,40 +90,26 @@ class HassioCreateSnapshotDialog extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
const snapshotDetails = this._snapshotContent.snapshotDetails();
|
||||
this._creatingSnapshot = true;
|
||||
|
||||
this._error = "";
|
||||
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
|
||||
if (
|
||||
this._snapshotContent.snapshotHasPassword &&
|
||||
!this._snapshotContent.snapshotPassword.length
|
||||
) {
|
||||
this._error = this._dialogParams!.supervisor.localize(
|
||||
"snapshot.enter_password"
|
||||
);
|
||||
button.progress = false;
|
||||
this._creatingSnapshot = false;
|
||||
return;
|
||||
}
|
||||
const name = this._snapshotName || formatDate(new Date(), this.hass.locale);
|
||||
|
||||
try {
|
||||
if (this._snapshotType === "full") {
|
||||
const data: HassioFullSnapshotCreateParams = { name };
|
||||
if (this._snapshotHasPassword) {
|
||||
data.password = this._snapshotPassword;
|
||||
}
|
||||
await createHassioFullSnapshot(this.hass, data);
|
||||
if (this._snapshotContent.snapshotType === "full") {
|
||||
await createHassioFullSnapshot(this.hass, snapshotDetails);
|
||||
} else {
|
||||
const data: HassioPartialSnapshotCreateParams = {
|
||||
name,
|
||||
folders: this._folderList
|
||||
.filter((folder) => folder.checked)
|
||||
.map((folder) => folder.slug),
|
||||
addons: this._addonList
|
||||
.filter((addon) => addon.checked)
|
||||
.map((addon) => addon.slug),
|
||||
};
|
||||
if (this._snapshotHasPassword) {
|
||||
data.password = this._snapshotPassword;
|
||||
}
|
||||
await createHassioPartialSnapshot(this.hass, data);
|
||||
await createHassioPartialSnapshot(this.hass, snapshotDetails);
|
||||
}
|
||||
|
||||
this._dialogParams!.onCreate();
|
||||
@@ -323,7 +117,7 @@ class HassioCreateSnapshotDialog extends LitElement {
|
||||
} catch (err) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
button.progress = false;
|
||||
this._creatingSnapshot = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -331,22 +125,9 @@ class HassioCreateSnapshotDialog extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
paper-input[type="password"] {
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 4px 0 4px 16px;
|
||||
}
|
||||
span.version {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.checkbox-section {
|
||||
display: grid;
|
||||
}
|
||||
.checkbox-line {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatDateTime } from "../../../../src/common/datetime/format_date_time";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-button-menu";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../../src/data/auth";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
@@ -15,95 +14,46 @@ import {
|
||||
fetchHassioSnapshotInfo,
|
||||
HassioSnapshotDetail,
|
||||
} from "../../../../src/data/hassio/snapshot";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/supervisor-snapshot-content";
|
||||
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
|
||||
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
|
||||
|
||||
const _computeFolders = (folders) => {
|
||||
const list: Array<{ slug: string; name: string; checked: boolean }> = [];
|
||||
if (folders.includes("homeassistant")) {
|
||||
list.push({
|
||||
slug: "homeassistant",
|
||||
name: "Home Assistant configuration",
|
||||
checked: true,
|
||||
});
|
||||
}
|
||||
if (folders.includes("ssl")) {
|
||||
list.push({ slug: "ssl", name: "SSL", checked: true });
|
||||
}
|
||||
if (folders.includes("share")) {
|
||||
list.push({ slug: "share", name: "Share", checked: true });
|
||||
}
|
||||
if (folders.includes("addons/local")) {
|
||||
list.push({ slug: "addons/local", name: "Local add-ons", checked: true });
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
const _computeAddons = (addons) =>
|
||||
addons.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
version: addon.version,
|
||||
checked: true,
|
||||
}));
|
||||
|
||||
interface AddonItem {
|
||||
slug: string;
|
||||
name: string;
|
||||
version: string;
|
||||
checked: boolean | null | undefined;
|
||||
}
|
||||
|
||||
interface FolderItem {
|
||||
slug: string;
|
||||
name: string;
|
||||
checked: boolean | null | undefined;
|
||||
}
|
||||
|
||||
@customElement("dialog-hassio-snapshot")
|
||||
class HassioSnapshotDialog extends LitElement {
|
||||
class HassioSnapshotDialog
|
||||
extends LitElement
|
||||
implements HassDialog<HassioSnapshotDialogParams> {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _onboarding = false;
|
||||
|
||||
@state() private _snapshot?: HassioSnapshotDetail;
|
||||
|
||||
@state() private _folders!: FolderItem[];
|
||||
|
||||
@state() private _addons!: AddonItem[];
|
||||
|
||||
@state() private _dialogParams?: HassioSnapshotDialogParams;
|
||||
|
||||
@state() private _snapshotPassword!: string;
|
||||
@state() private _restoringSnapshot = false;
|
||||
|
||||
@state() private _restoreHass = true;
|
||||
@query("supervisor-snapshot-content")
|
||||
private _snapshotContent!: SupervisorSnapshotContent;
|
||||
|
||||
public async showDialog(params: HassioSnapshotDialogParams) {
|
||||
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
|
||||
this._folders = _computeFolders(
|
||||
this._snapshot?.folders
|
||||
).sort((a: FolderItem, b: FolderItem) => (a.name > b.name ? 1 : -1));
|
||||
this._addons = _computeAddons(
|
||||
this._snapshot?.addons
|
||||
).sort((a: AddonItem, b: AddonItem) => (a.name > b.name ? 1 : -1));
|
||||
|
||||
this._dialogParams = params;
|
||||
this._onboarding = params.onboarding ?? false;
|
||||
this.supervisor = params.supervisor;
|
||||
if (!this._snapshot.homeassistant) {
|
||||
this._restoreHass = false;
|
||||
}
|
||||
this._restoringSnapshot = false;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._snapshot = undefined;
|
||||
this._dialogParams = undefined;
|
||||
this._restoringSnapshot = false;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -111,121 +61,41 @@ class HassioSnapshotDialog extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog open @closing=${this._closeDialog} .heading=${true}>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title"> ${this._computeName} </span>
|
||||
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<div class="details">
|
||||
${this._snapshot.type === "full"
|
||||
? "Full snapshot"
|
||||
: "Partial snapshot"}
|
||||
(${this._computeSize})<br />
|
||||
${formatDateTime(new Date(this._snapshot.date), this.hass.locale)}
|
||||
</div>
|
||||
${this._snapshot.homeassistant
|
||||
? html`<div>Home Assistant:</div>
|
||||
<paper-checkbox
|
||||
.checked=${this._restoreHass}
|
||||
@change="${(ev: Event) => {
|
||||
this._restoreHass = (ev.target as PaperCheckboxElement).checked!;
|
||||
}}"
|
||||
>
|
||||
Home Assistant
|
||||
<span class="version">(${this._snapshot.homeassistant})</span>
|
||||
</paper-checkbox>`
|
||||
: ""}
|
||||
${this._folders.length
|
||||
? html`
|
||||
<div>Folders:</div>
|
||||
<paper-dialog-scrollable class="no-margin-top">
|
||||
${this._folders.map(
|
||||
(item) => html`
|
||||
<paper-checkbox
|
||||
.checked=${item.checked}
|
||||
@change="${(ev: Event) =>
|
||||
this._updateFolders(
|
||||
item,
|
||||
(ev.target as PaperCheckboxElement).checked
|
||||
)}"
|
||||
>
|
||||
${item.name}
|
||||
</paper-checkbox>
|
||||
`
|
||||
)}
|
||||
</paper-dialog-scrollable>
|
||||
`
|
||||
: ""}
|
||||
${this._addons.length
|
||||
? html`
|
||||
<div>Add-on:</div>
|
||||
<paper-dialog-scrollable class="no-margin-top">
|
||||
${this._addons.map(
|
||||
(item) => html`
|
||||
<paper-checkbox
|
||||
.checked=${item.checked}
|
||||
@change="${(ev: Event) =>
|
||||
this._updateAddons(
|
||||
item,
|
||||
(ev.target as PaperCheckboxElement).checked
|
||||
)}"
|
||||
>
|
||||
${item.name}
|
||||
<span class="version">(${item.version})</span>
|
||||
</paper-checkbox>
|
||||
`
|
||||
)}
|
||||
</paper-dialog-scrollable>
|
||||
`
|
||||
: ""}
|
||||
${this._snapshot.protected
|
||||
? html`
|
||||
<paper-input
|
||||
autofocus=""
|
||||
label="Password"
|
||||
type="password"
|
||||
@value-changed=${this._passwordInput}
|
||||
.value=${this._snapshotPassword}
|
||||
></paper-input>
|
||||
`
|
||||
: ""}
|
||||
${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""}
|
||||
<ha-dialog
|
||||
open
|
||||
@closing=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this._computeName)}
|
||||
>
|
||||
${this._restoringSnapshot
|
||||
? html` <ha-circular-progress active></ha-circular-progress>`
|
||||
: html`<supervisor-snapshot-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
.snapshot=${this._snapshot}
|
||||
>
|
||||
</supervisor-snapshot-content>`}
|
||||
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
|
||||
|
||||
<div class="button-row" slot="primaryAction">
|
||||
<mwc-button @click=${this._partialRestoreClicked}>
|
||||
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
|
||||
Restore Selected
|
||||
</mwc-button>
|
||||
${!this._onboarding
|
||||
? html`
|
||||
<mwc-button @click=${this._deleteClicked}>
|
||||
<ha-svg-icon .path=${mdiDelete} class="icon warning">
|
||||
</ha-svg-icon>
|
||||
<span class="warning">Delete Snapshot</span>
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="button-row" slot="secondaryAction">
|
||||
${this._snapshot.type === "full"
|
||||
? html`
|
||||
<mwc-button @click=${this._fullRestoreClicked}>
|
||||
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
|
||||
Restore Everything
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
${!this._onboarding
|
||||
? html`<mwc-button @click=${this._downloadClicked}>
|
||||
<ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
|
||||
Download Snapshot
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button
|
||||
.disabled=${this._restoringSnapshot}
|
||||
slot="secondaryAction"
|
||||
@click=${this._restoreClicked}
|
||||
>
|
||||
Restore
|
||||
</mwc-button>
|
||||
|
||||
<ha-button-menu
|
||||
fixed
|
||||
slot="primaryAction"
|
||||
@action=${this._handleMenuAction}
|
||||
@closing=${(ev: Event) => ev.stopPropagation()}
|
||||
>
|
||||
<mwc-icon-button slot="trigger" alt="menu">
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item>Download Snapshot</mwc-list-item>
|
||||
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -235,83 +105,47 @@ class HassioSnapshotDialog extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
paper-checkbox {
|
||||
ha-svg-icon {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
}
|
||||
mwc-button ha-svg-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.button-row {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.warning,
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.buttons li {
|
||||
list-style-type: none;
|
||||
}
|
||||
.buttons .icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
.no-margin-top {
|
||||
margin-top: 0;
|
||||
}
|
||||
span.version {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _updateFolders(item: FolderItem, value: boolean | null | undefined) {
|
||||
this._folders = this._folders.map((folder) => {
|
||||
if (folder.slug === item.slug) {
|
||||
folder.checked = value;
|
||||
}
|
||||
return folder;
|
||||
});
|
||||
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._downloadClicked();
|
||||
break;
|
||||
case 1:
|
||||
this._deleteClicked();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateAddons(item: AddonItem, value: boolean | null | undefined) {
|
||||
this._addons = this._addons.map((addon) => {
|
||||
if (addon.slug === item.slug) {
|
||||
addon.checked = value;
|
||||
}
|
||||
return addon;
|
||||
});
|
||||
private async _restoreClicked() {
|
||||
const snapshotDetails = this._snapshotContent.snapshotDetails();
|
||||
this._restoringSnapshot = true;
|
||||
if (this._snapshotContent.snapshotType === "full") {
|
||||
await this._fullRestoreClicked(snapshotDetails);
|
||||
} else {
|
||||
await this._partialRestoreClicked(snapshotDetails);
|
||||
}
|
||||
this._restoringSnapshot = false;
|
||||
}
|
||||
|
||||
private _passwordInput(ev: PolymerChangedEvent<string>) {
|
||||
this._snapshotPassword = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _partialRestoreClicked() {
|
||||
private async _partialRestoreClicked(snapshotDetails) {
|
||||
if (
|
||||
this.supervisor !== undefined &&
|
||||
this.supervisor.info.state !== "running"
|
||||
this._dialogParams?.supervisor !== undefined &&
|
||||
this._dialogParams?.supervisor.info.state !== "running"
|
||||
) {
|
||||
await showAlertDialog(this, {
|
||||
title: "Could not restore snapshot",
|
||||
text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
|
||||
text: `Restoring a snapshot is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -325,41 +159,17 @@ class HassioSnapshotDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const addons = this._addons
|
||||
.filter((addon) => addon.checked)
|
||||
.map((addon) => addon.slug);
|
||||
|
||||
const folders = this._folders
|
||||
.filter((folder) => folder.checked)
|
||||
.map((folder) => folder.slug);
|
||||
|
||||
const data: {
|
||||
homeassistant: boolean;
|
||||
addons: any;
|
||||
folders: any;
|
||||
password?: string;
|
||||
} = {
|
||||
homeassistant: this._restoreHass,
|
||||
addons,
|
||||
folders,
|
||||
};
|
||||
|
||||
if (this._snapshot!.protected) {
|
||||
data.password = this._snapshotPassword;
|
||||
}
|
||||
|
||||
if (!this._onboarding) {
|
||||
if (!this._dialogParams?.onboarding) {
|
||||
this.hass
|
||||
.callApi(
|
||||
"POST",
|
||||
|
||||
`hassio/snapshots/${this._snapshot!.slug}/restore/partial`,
|
||||
data
|
||||
snapshotDetails
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
alert("Snapshot restored!");
|
||||
this._closeDialog();
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
@@ -369,20 +179,20 @@ class HassioSnapshotDialog extends LitElement {
|
||||
fireEvent(this, "restoring");
|
||||
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/partial`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
body: JSON.stringify(snapshotDetails),
|
||||
});
|
||||
this._closeDialog();
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fullRestoreClicked() {
|
||||
private async _fullRestoreClicked(snapshotDetails) {
|
||||
if (
|
||||
this.supervisor !== undefined &&
|
||||
this.supervisor.info.state !== "running"
|
||||
this._dialogParams?.supervisor !== undefined &&
|
||||
this._dialogParams?.supervisor.info.state !== "running"
|
||||
) {
|
||||
await showAlertDialog(this, {
|
||||
title: "Could not restore snapshot",
|
||||
text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
|
||||
text: `Restoring a snapshot is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -397,20 +207,16 @@ class HassioSnapshotDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this._snapshot!.protected
|
||||
? { password: this._snapshotPassword }
|
||||
: undefined;
|
||||
if (!this._onboarding) {
|
||||
if (!this._dialogParams?.onboarding) {
|
||||
this.hass
|
||||
.callApi(
|
||||
"POST",
|
||||
`hassio/snapshots/${this._snapshot!.slug}/restore/full`,
|
||||
data
|
||||
snapshotDetails
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
alert("Snapshot restored!");
|
||||
this._closeDialog();
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
@@ -420,9 +226,9 @@ class HassioSnapshotDialog extends LitElement {
|
||||
fireEvent(this, "restoring");
|
||||
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/full`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
body: JSON.stringify(snapshotDetails),
|
||||
});
|
||||
this._closeDialog();
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,7 +251,7 @@ class HassioSnapshotDialog extends LitElement {
|
||||
if (this._dialogParams!.onDelete) {
|
||||
this._dialogParams!.onDelete();
|
||||
}
|
||||
this._closeDialog();
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
@@ -461,7 +267,9 @@ class HassioSnapshotDialog extends LitElement {
|
||||
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
|
||||
);
|
||||
} catch (err) {
|
||||
alert(`Error: ${extractApiErrorMessage(err)}`);
|
||||
await showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -492,18 +300,6 @@ class HassioSnapshotDialog extends LitElement {
|
||||
? this._snapshot.name || this._snapshot.slug
|
||||
: "Unnamed snapshot";
|
||||
}
|
||||
|
||||
private get _computeSize() {
|
||||
return Math.ceil(this._snapshot!.size * 10) / 10 + " MB";
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._dialogParams = undefined;
|
||||
this._snapshot = undefined;
|
||||
this._snapshotPassword = "";
|
||||
this._folders = [];
|
||||
this._addons = [];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -15,5 +15,11 @@ body {
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
|
||||
+13
-10
@@ -4,6 +4,7 @@ import { atLeastVersion } from "../../src/common/config/version";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { mainWindow } from "../../src/common/dom/get_main_window";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
@@ -50,7 +51,7 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
|
||||
// Joakim - April 26, 2021
|
||||
// Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element
|
||||
top.addEventListener("location-changed", (ev) =>
|
||||
mainWindow.addEventListener("location-changed", (ev) =>
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail, {
|
||||
bubbles: false,
|
||||
@@ -62,7 +63,7 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
document.body.addEventListener("click", (ev) => {
|
||||
const href = isNavigationClick(ev);
|
||||
if (href) {
|
||||
navigate(document.body, href);
|
||||
navigate(href);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,25 +103,27 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
|
||||
private _applyTheme() {
|
||||
let themeName: string;
|
||||
let options: 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);
|
||||
|
||||
options = this.hass.selectedTheme;
|
||||
if (themeName === "default" && options?.dark === undefined) {
|
||||
options = {
|
||||
...this.hass.selectedTheme,
|
||||
themeSettings = this.hass.selectedThemeSettings;
|
||||
if (themeSettings?.dark === undefined) {
|
||||
themeSettings = {
|
||||
...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;
|
||||
}
|
||||
|
||||
@@ -128,7 +131,7 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
options
|
||||
themeSettings
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class HassioMyRedirect extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(this, url, true);
|
||||
navigate(url, { replace: true });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import { nextRender } from "../../../src/common/util/render-status";
|
||||
import {
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
@@ -95,6 +96,7 @@ class HassioIngressView extends LitElement {
|
||||
text: extractApiErrorMessage(err),
|
||||
title: requestedAddon,
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
@@ -103,9 +105,10 @@ class HassioIngressView extends LitElement {
|
||||
text: this.supervisor.localize("my.error_addon_no_ingress"),
|
||||
title: addonInfo.name,
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
} else {
|
||||
navigate(this, `/hassio/ingress/${addonInfo.slug}`, true);
|
||||
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +143,7 @@ class HassioIngressView extends LitElement {
|
||||
text: "Unable to fetch add-on info to start Ingress",
|
||||
title: "Supervisor",
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
@@ -149,6 +153,7 @@ class HassioIngressView extends LitElement {
|
||||
text: "Add-on does not support Ingress",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
@@ -158,7 +163,8 @@ class HassioIngressView extends LitElement {
|
||||
text: "Add-on is not running. Please start it first",
|
||||
title: addon.name,
|
||||
});
|
||||
navigate(this, `/hassio/addon/${addon.slug}/info`, true);
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,6 +177,7 @@ class HassioIngressView extends LitElement {
|
||||
text: "Unable to create an Ingress session",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"lit": "^2.0.0-rc.2",
|
||||
"lit-vaadin-helpers": "^0.1.3",
|
||||
"marked": "2.0.0",
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import { format } from "fecha";
|
||||
import { FrontendTranslationData } from "../../data/translation";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
const formatDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDate = toLocaleDateStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleDateString(locales.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateMem(locale).format(dateObj)
|
||||
: (dateObj: Date) => format(dateObj, "longDate");
|
||||
|
||||
const formatDateWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateWeekday = toLocaleDateStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleDateString(locales.language, {
|
||||
weekday: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateWeekdayMem(locale).format(dateObj)
|
||||
: (dateObj: Date) => format(dateObj, "dddd, MMM D");
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
import { format } from "fecha";
|
||||
import { FrontendTranslationData } from "../../data/translation";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { toLocaleStringSupportsOptions } from "./check_options_support";
|
||||
import { useAmPm } from "./use_am_pm";
|
||||
|
||||
const formatDateTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateTime = toLocaleStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleString(locales.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateTimeMem(locale).format(dateObj)
|
||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
format(dateObj, "MMMM D, YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
|
||||
|
||||
const formatDateTimeWithSecondsMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleString(locales.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm:ss");
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateTimeWithSecondsMem(locale).format(dateObj)
|
||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
format(dateObj, "MMMM D, YYYY, HH:mm:ss" + useAmPm(locale) ? " A" : "");
|
||||
|
||||
@@ -1,29 +1,52 @@
|
||||
import { format } from "fecha";
|
||||
import { FrontendTranslationData } from "../../data/translation";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
|
||||
import { useAmPm } from "./use_am_pm";
|
||||
|
||||
const formatTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatTime = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleTimeString(locales.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "shortTime");
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatTimeMem(locale).format(dateObj)
|
||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
format(dateObj, "shortTime" + useAmPm(locale) ? " A" : "");
|
||||
|
||||
const formatTimeWithSecondsMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleTimeString(locales.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "mediumTime");
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatTimeWithSecondsMem(locale).format(dateObj)
|
||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
format(dateObj, "mediumTime" + useAmPm(locale) ? " A" : "");
|
||||
|
||||
const formatTimeWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleTimeString(locales.language, {
|
||||
weekday: "long",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "dddd, HH:mm");
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatTimeWeekdayMem(locale).format(dateObj)
|
||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
format(dateObj, "dddd, HH:mm" + useAmPm(locale) ? " A" : "");
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FrontendLocaleData, TimeFormat } from "../../data/translation";
|
||||
|
||||
export const useAmPm = (locale: FrontendLocaleData): boolean => {
|
||||
if (
|
||||
locale.time_format === TimeFormat.language ||
|
||||
locale.time_format === TimeFormat.system
|
||||
) {
|
||||
const testLanguage =
|
||||
locale.time_format === TimeFormat.language ? locale.language : undefined;
|
||||
const test = new Date().toLocaleString(testLanguage);
|
||||
return test.includes("AM") || test.includes("PM");
|
||||
}
|
||||
|
||||
return locale.time_format === TimeFormat.am_pm;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Theme } from "../../data/ws-themes";
|
||||
import { ThemeVars } from "../../data/ws-themes";
|
||||
import { darkStyles, derivedStyles } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
@@ -23,62 +23,90 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
|
||||
* Apply a theme to an element by setting the CSS variables on it.
|
||||
*
|
||||
* element: Element to apply theme on.
|
||||
* themes: HASS Theme information
|
||||
* selectedTheme: selected theme.
|
||||
* themes: HASS theme information.
|
||||
* selectedTheme: Selected theme.
|
||||
* themeSettings: Settings such as selected dark mode and colors.
|
||||
*/
|
||||
export const applyThemesOnElement = (
|
||||
element,
|
||||
themes: HomeAssistant["themes"],
|
||||
selectedTheme?: string,
|
||||
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
|
||||
themeSettings?: Partial<HomeAssistant["selectedThemeSettings"]>
|
||||
) => {
|
||||
let cacheKey = selectedTheme;
|
||||
let themeRules: Partial<Theme> = {};
|
||||
let themeRules: Partial<ThemeVars> = {};
|
||||
|
||||
if (selectedTheme === "default" && themeOptions) {
|
||||
if (themeOptions.dark) {
|
||||
if (themeSettings) {
|
||||
if (themeSettings.dark) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
themeRules = darkStyles;
|
||||
if (themeOptions.primaryColor) {
|
||||
}
|
||||
|
||||
if (selectedTheme === "default") {
|
||||
// Determine the primary and accent colors from the current settings.
|
||||
// Fallbacks are implicitly the HA default blue and orange or the
|
||||
// derived "darkStyles" values, depending on the light vs dark mode.
|
||||
const primaryColor = themeSettings.primaryColor;
|
||||
const accentColor = themeSettings.accentColor;
|
||||
|
||||
if (themeSettings.dark && primaryColor) {
|
||||
themeRules["app-header-background-color"] = hexBlend(
|
||||
themeOptions.primaryColor,
|
||||
primaryColor,
|
||||
"#121212",
|
||||
8
|
||||
);
|
||||
}
|
||||
}
|
||||
if (themeOptions.primaryColor) {
|
||||
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
|
||||
const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor);
|
||||
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
|
||||
themeRules["primary-color"] = themeOptions.primaryColor;
|
||||
const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
|
||||
themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor);
|
||||
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
|
||||
themeRules["text-primary-color"] =
|
||||
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
themeRules["text-light-primary-color"] =
|
||||
rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6
|
||||
? "#fff"
|
||||
: "#212121";
|
||||
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
|
||||
}
|
||||
if (themeOptions.accentColor) {
|
||||
cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`;
|
||||
themeRules["accent-color"] = themeOptions.accentColor;
|
||||
const rgbAccentColor = hex2rgb(themeOptions.accentColor);
|
||||
themeRules["text-accent-color"] =
|
||||
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
}
|
||||
|
||||
// Nothing was changed
|
||||
if (element._themes?.cacheKey === cacheKey) {
|
||||
return;
|
||||
if (primaryColor) {
|
||||
cacheKey = `${cacheKey}__primary_${primaryColor}`;
|
||||
const rgbPrimaryColor = hex2rgb(primaryColor);
|
||||
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
|
||||
themeRules["primary-color"] = primaryColor;
|
||||
const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
|
||||
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
|
||||
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
|
||||
themeRules["text-primary-color"] =
|
||||
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
themeRules["text-light-primary-color"] =
|
||||
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
|
||||
? "#fff"
|
||||
: "#212121";
|
||||
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
|
||||
}
|
||||
if (accentColor) {
|
||||
cacheKey = `${cacheKey}__accent_${accentColor}`;
|
||||
themeRules["accent-color"] = accentColor;
|
||||
const rgbAccentColor = hex2rgb(accentColor);
|
||||
themeRules["text-accent-color"] =
|
||||
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
}
|
||||
|
||||
// Nothing was changed
|
||||
if (element._themes?.cacheKey === cacheKey) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTheme && themes.themes[selectedTheme]) {
|
||||
themeRules = themes.themes[selectedTheme];
|
||||
// Custom theme logic (not relevant for default theme, since it would override
|
||||
// the derived calculations from above)
|
||||
if (
|
||||
selectedTheme &&
|
||||
selectedTheme !== "default" &&
|
||||
themes.themes[selectedTheme]
|
||||
) {
|
||||
// Apply theme vars that are relevant for all modes (but extract the "modes" section first)
|
||||
const { modes, ...baseThemeRules } = themes.themes[selectedTheme];
|
||||
themeRules = { ...themeRules, ...baseThemeRules };
|
||||
|
||||
// Apply theme vars for the specific mode if available
|
||||
if (modes) {
|
||||
if (themeSettings?.dark) {
|
||||
themeRules = { ...themeRules, ...modes.dark };
|
||||
} else {
|
||||
themeRules = { ...themeRules, ...modes.light };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!element._themes?.keys && !Object.keys(themeRules).length) {
|
||||
@@ -106,12 +134,12 @@ export const applyThemesOnElement = (
|
||||
|
||||
const processTheme = (
|
||||
cacheKey: string,
|
||||
theme: Partial<Theme>
|
||||
theme: Partial<ThemeVars>
|
||||
): ProcessedTheme | undefined => {
|
||||
if (!theme || !Object.keys(theme).length) {
|
||||
return undefined;
|
||||
}
|
||||
const combinedTheme: Partial<Theme> = {
|
||||
const combinedTheme: Partial<ThemeVars> = {
|
||||
...derivedStyles,
|
||||
...theme,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { MAIN_WINDOW_NAME } from "../../data/main_window";
|
||||
|
||||
export const mainWindow =
|
||||
window.name === MAIN_WINDOW_NAME
|
||||
? window
|
||||
: parent.name === MAIN_WINDOW_NAME
|
||||
? parent
|
||||
: top;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { FrontendTranslationData } from "../../data/translation";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
@@ -11,7 +11,7 @@ import { computeStateDomain } from "./compute_state_domain";
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendTranslationData,
|
||||
locale: FrontendLocaleData,
|
||||
state?: string
|
||||
): string => {
|
||||
const compareState = state !== undefined ? state : stateObj.state;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import durationToSeconds from "../datetime/duration_to_seconds";
|
||||
|
||||
export const timerTimeRemaining = (
|
||||
stateObj: HassEntity
|
||||
): undefined | number => {
|
||||
if (!stateObj.attributes.remaining) {
|
||||
return undefined;
|
||||
}
|
||||
let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
|
||||
|
||||
if (stateObj.state === "active") {
|
||||
const now = new Date().getTime();
|
||||
const madeActive = new Date(stateObj.last_changed).getTime();
|
||||
timeRemaining = Math.max(timeRemaining - (now - madeActive) / 1000, 0);
|
||||
}
|
||||
|
||||
return timeRemaining;
|
||||
};
|
||||
+17
-12
@@ -1,35 +1,40 @@
|
||||
import { fireEvent } from "./dom/fire_event";
|
||||
import { mainWindow } from "./dom/get_main_window";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"location-changed": {
|
||||
replace: boolean;
|
||||
};
|
||||
"location-changed": NavigateOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export const navigate = (_node: any, path: string, replace = false) => {
|
||||
export interface NavigateOptions {
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
export const navigate = (path: string, options?: NavigateOptions) => {
|
||||
const replace = options?.replace || false;
|
||||
|
||||
if (__DEMO__) {
|
||||
if (replace) {
|
||||
top.history.replaceState(
|
||||
top.history.state?.root ? { root: true } : null,
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root ? { root: true } : null,
|
||||
"",
|
||||
`${top.location.pathname}#${path}`
|
||||
`${mainWindow.location.pathname}#${path}`
|
||||
);
|
||||
} else {
|
||||
top.location.hash = path;
|
||||
mainWindow.location.hash = path;
|
||||
}
|
||||
} else if (replace) {
|
||||
top.history.replaceState(
|
||||
top.history.state?.root ? { root: true } : null,
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root ? { root: true } : null,
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
top.history.pushState(null, "", path);
|
||||
mainWindow.history.pushState(null, "", path);
|
||||
}
|
||||
fireEvent(top, "location-changed", {
|
||||
fireEvent(mainWindow, "location-changed", {
|
||||
replace,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FrontendTranslationData, NumberFormat } from "../../data/translation";
|
||||
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
|
||||
|
||||
/**
|
||||
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
|
||||
@@ -9,7 +9,7 @@ import { FrontendTranslationData, NumberFormat } from "../../data/translation";
|
||||
*/
|
||||
export const formatNumber = (
|
||||
num: string | number,
|
||||
locale?: FrontendTranslationData,
|
||||
locale?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
let format: string | string[] | undefined;
|
||||
|
||||
@@ -173,8 +173,8 @@ export class HaDataTable extends LitElement {
|
||||
this.updateComplete.then(() => this._calcTableHeight());
|
||||
}
|
||||
|
||||
protected updated(properties: PropertyValues) {
|
||||
super.updated(properties);
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (properties.has("columns")) {
|
||||
this._filterable = Object.values(this.columns).some(
|
||||
@@ -342,6 +342,10 @@ export class HaDataTable extends LitElement {
|
||||
layout: Layout1d,
|
||||
// @ts-expect-error
|
||||
renderItem: (row: DataTableRowData, index) => {
|
||||
// not sure how this happens...
|
||||
if (!row) {
|
||||
return "";
|
||||
}
|
||||
if (row.append) {
|
||||
return html`
|
||||
<div class="mdc-data-table__row">${row.content}</div>
|
||||
@@ -474,15 +478,16 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
if (this.appendRow || this.hasFab) {
|
||||
this._items = [...data];
|
||||
const items = [...data];
|
||||
|
||||
if (this.appendRow) {
|
||||
this._items.push({ append: true, content: this.appendRow });
|
||||
items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
|
||||
if (this.hasFab) {
|
||||
this._items.push({ empty: true });
|
||||
items.push({ empty: true });
|
||||
}
|
||||
this._items = items;
|
||||
} else {
|
||||
this._items = data;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-devices-picker";
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||
|
||||
interface DevicesByArea {
|
||||
[areaId: string]: AreaDevices;
|
||||
@@ -49,42 +50,28 @@ interface AreaDevices {
|
||||
devices: string[];
|
||||
}
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: AreaDevices }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
width: 100%;
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
mwc-icon-button {
|
||||
float: right;
|
||||
}
|
||||
.devices {
|
||||
display: none;
|
||||
}
|
||||
.devices.visible {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'>[[item.name]]</div>
|
||||
<div secondary>[[item.devices.length]] devices</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
root.querySelector(".name")!.textContent = model.item.name!;
|
||||
root.querySelector(
|
||||
"[secondary]"
|
||||
)!.textContent = `${model.item.devices.length.toString()} devices`;
|
||||
};
|
||||
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style>
|
||||
paper-item {
|
||||
width: 100%;
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
mwc-icon-button {
|
||||
float: right;
|
||||
}
|
||||
.devices {
|
||||
display: none;
|
||||
}
|
||||
.devices.visible {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class="name">${item.name}</div>
|
||||
<div secondary>${item.devices.length} devices</div>
|
||||
</paper-item-body>
|
||||
</paper-item>`;
|
||||
|
||||
@customElement("ha-area-devices-picker")
|
||||
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -310,7 +297,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
item-label-path="name"
|
||||
.items=${areas}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
${comboBoxRenderer(rowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._areaPicked}
|
||||
>
|
||||
|
||||
@@ -33,6 +33,7 @@ import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
||||
|
||||
interface Device {
|
||||
name: string;
|
||||
@@ -44,27 +45,18 @@ export type HaDevicePickerDeviceFilterFunc = (
|
||||
device: DeviceRegistryEntry
|
||||
) => boolean;
|
||||
|
||||
const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'>[[item.name]]</div>
|
||||
<div secondary>[[item.area]]</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
|
||||
root.querySelector(".name")!.textContent = model.item.name!;
|
||||
root.querySelector("[secondary]")!.textContent = model.item.area!;
|
||||
};
|
||||
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style>
|
||||
paper-item {
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line>
|
||||
${item.name}
|
||||
<span secondary>${item.area}</span>
|
||||
</paper-item-body>
|
||||
</paper-item>`;
|
||||
|
||||
@customElement("ha-device-picker")
|
||||
export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
@@ -22,22 +23,13 @@ import "./state-badge";
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
|
||||
const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
margin: -10px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item></paper-item>
|
||||
`;
|
||||
}
|
||||
root.querySelector("paper-item")!.textContent = formatAttributeName(
|
||||
model.item
|
||||
);
|
||||
};
|
||||
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
||||
paper-item {
|
||||
margin: -5px -10px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item>${formatAttributeName(item)}</paper-item>`;
|
||||
|
||||
@customElement("ha-entity-attribute-picker")
|
||||
class HaEntityAttributePicker extends LitElement {
|
||||
@@ -82,8 +74,8 @@ class HaEntityAttributePicker extends LitElement {
|
||||
<vaadin-combo-box-light
|
||||
.value=${this._value}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.renderer=${rowRenderer}
|
||||
attr-for-value="bind-value"
|
||||
${comboBoxRenderer(rowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -25,32 +26,19 @@ import "./state-badge";
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: HassEntity }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-icon-item {
|
||||
margin: -10px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-icon-item>
|
||||
<state-badge slot="item-icon"></state-badge>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'></div>
|
||||
<div secondary></div>
|
||||
</paper-item-body>
|
||||
</paper-icon-item>
|
||||
`;
|
||||
}
|
||||
root.querySelector("state-badge")!.stateObj = model.item;
|
||||
root.querySelector(".name")!.textContent = computeStateName(model.item);
|
||||
root.querySelector("[secondary]")!.textContent = model.item.entity_id;
|
||||
};
|
||||
const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
|
||||
paper-icon-item {
|
||||
margin: -10px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-icon-item>
|
||||
<state-badge slot="item-icon" .stateObj=${item}></state-badge>
|
||||
<paper-item-body two-line="">
|
||||
${computeStateName(item)}
|
||||
<span secondary>${item.entity_id}</span>
|
||||
</paper-item-body>
|
||||
</paper-icon-item>`;
|
||||
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@@ -221,7 +209,7 @@ export class HaEntityPicker extends LitElement {
|
||||
item-label-path="entity_id"
|
||||
.value=${this._value}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.renderer=${rowRenderer}
|
||||
${comboBoxRenderer(rowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { domainIcon } from "../../common/entity/domain_icon";
|
||||
import { stateIcon } from "../../common/entity/state_icon";
|
||||
import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import { formatNumber } from "../../common/string/format_number";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
@@ -9,32 +9,20 @@ import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { HaComboBox } from "./ha-combo-box";
|
||||
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: HassioAddonInfo }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'>[[item.name]]</div>
|
||||
<div secondary>[[item.slug]]</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
|
||||
root.querySelector(".name")!.textContent = model.item.name;
|
||||
root.querySelector("[secondary]")!.textContent = model.item.slug;
|
||||
};
|
||||
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`<style>
|
||||
paper-item {
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line>
|
||||
${item.name}
|
||||
<span secondary>${item.slug}</span>
|
||||
</paper-item-body>
|
||||
</paper-item>`;
|
||||
|
||||
@customElement("ha-addon-picker")
|
||||
class HaAddonPicker extends LitElement {
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
@@ -42,36 +44,20 @@ import { HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: AreaRegistryEntry }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
paper-item.add-new {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line>
|
||||
<div class='name'>[[item.name]]</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
root.querySelector(".name")!.textContent = model.item.name!;
|
||||
if (model.item.area_id === "add_new") {
|
||||
root.querySelector("paper-item")!.className = "add-new";
|
||||
} else {
|
||||
root.querySelector("paper-item")!.classList.remove("add-new");
|
||||
}
|
||||
};
|
||||
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
|
||||
item
|
||||
) => html`<style>
|
||||
paper-item {
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
paper-item.add-new {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<paper-item class=${classMap({ "add-new": item.area_id === "add_new" })}>
|
||||
<paper-item-body two-line>${item.name}</paper-item-body>
|
||||
</paper-item>`;
|
||||
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -340,8 +326,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
item-id-path="area_id"
|
||||
item-label-path="name"
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
.disabled=${this.disabled}
|
||||
${comboBoxRenderer(rowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._areaChanged}
|
||||
>
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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,
|
||||
} 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;
|
||||
|
||||
@property() public stateObj?: HassEntity;
|
||||
|
||||
@property({ attribute: "extra-filters" }) public extraFilters?: string;
|
||||
|
||||
@state() private _expanded = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
@@ -30,24 +36,37 @@ class HaAttributes extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<hr />
|
||||
<div>
|
||||
${attributes.map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">${formatAttributeName(attribute)}</div>
|
||||
<div class="value">${this.formatAttribute(attribute)}</div>
|
||||
</div>
|
||||
`
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.attributes.expansion_header"
|
||||
)}
|
||||
${this.stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
${this.stateObj.attributes.attribution}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
outlined
|
||||
@expanded-will-change=${this.expandedChanged}
|
||||
>
|
||||
<div class="attribute-container">
|
||||
${this._expanded
|
||||
? html`
|
||||
${attributes.map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">${formatAttributeName(attribute)}</div>
|
||||
<div class="value">
|
||||
${this.formatAttribute(attribute)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
${this.stateObj.attributes.attribution}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -55,6 +74,9 @@ class HaAttributes extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.attribute-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.data-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -135,6 +157,10 @@ class HaAttributes extends LitElement {
|
||||
}
|
||||
return Array.isArray(value) ? value.join(", ") : value;
|
||||
}
|
||||
|
||||
private expandedChanged(ev) {
|
||||
this._expanded = ev.detail.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -12,6 +12,8 @@ export class HaButtonMenu extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public fixed = false;
|
||||
|
||||
@query("mwc-menu", true) private _menu?: Menu;
|
||||
|
||||
public get items() {
|
||||
@@ -29,6 +31,7 @@ export class HaButtonMenu extends LitElement {
|
||||
</div>
|
||||
<mwc-menu
|
||||
.corner=${this.corner}
|
||||
.fixed=${this.fixed}
|
||||
.multi=${this.multi}
|
||||
.activatable=${this.activatable}
|
||||
>
|
||||
|
||||
@@ -6,31 +6,20 @@ import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const defaultRowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: any }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
margin: -5px -10px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item></paper-item>
|
||||
`;
|
||||
}
|
||||
|
||||
root.querySelector("paper-item")!.textContent = model.item;
|
||||
};
|
||||
const defaultRowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
||||
paper-item {
|
||||
margin: -5px -10px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item>${item}</paper-item>`;
|
||||
|
||||
@customElement("ha-combo-box")
|
||||
export class HaComboBox extends LitElement {
|
||||
@@ -53,11 +42,7 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
|
||||
|
||||
@property() public renderer?: (
|
||||
root: HTMLElement,
|
||||
owner: HTMLElement,
|
||||
model: { item: any }
|
||||
) => void;
|
||||
@property() public renderer?: ComboBoxLitRenderer<any>;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@@ -90,9 +75,9 @@ export class HaComboBox extends LitElement {
|
||||
.value=${this.value}
|
||||
.items=${this.items}
|
||||
.filteredItems=${this.filteredItems}
|
||||
.renderer=${this.renderer || defaultRowRenderer}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.disabled=${this.disabled}
|
||||
${comboBoxRenderer(this.renderer || defaultRowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { formatDateTime } from "../common/datetime/format_date_time";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./date-range-picker";
|
||||
@@ -43,7 +44,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.locale !== this.hass.locale) {
|
||||
this._hour24format = this._compute24hourFormat();
|
||||
this._hour24format = !useAmPm(this.hass.locale);
|
||||
this._rtlDirection = computeRTLDirection(this.hass);
|
||||
}
|
||||
}
|
||||
@@ -106,16 +107,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _compute24hourFormat() {
|
||||
return (
|
||||
new Intl.DateTimeFormat(this.hass.language, {
|
||||
hour: "numeric",
|
||||
})
|
||||
.formatToParts(new Date(2020, 0, 1, 13))
|
||||
.find((part) => part.type === "hour")!.value.length === 2
|
||||
);
|
||||
}
|
||||
|
||||
private _setDateRange(ev: CustomEvent<ActionDetail>) {
|
||||
const dateRange = Object.values(this.ranges!)[ev.detail.index];
|
||||
const dateRangePicker = this._dateRangePicker;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-expansion-panel")
|
||||
@@ -37,17 +38,25 @@ class HaExpansionPanel extends LitElement {
|
||||
this._container.style.removeProperty("height");
|
||||
}
|
||||
|
||||
private _toggleContainer(): void {
|
||||
private async _toggleContainer(): Promise<void> {
|
||||
const newExpanded = !this.expanded;
|
||||
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
|
||||
|
||||
if (newExpanded) {
|
||||
// allow for dynamic content to be rendered
|
||||
await nextRender();
|
||||
}
|
||||
|
||||
const scrollHeight = this._container.scrollHeight;
|
||||
this._container.style.height = `${scrollHeight}px`;
|
||||
|
||||
if (this.expanded) {
|
||||
if (!newExpanded) {
|
||||
setTimeout(() => {
|
||||
this._container.style.height = "0px";
|
||||
}, 0);
|
||||
}
|
||||
|
||||
this.expanded = !this.expanded;
|
||||
this.expanded = newExpanded;
|
||||
fireEvent(this, "expanded-changed", { expanded: this.expanded });
|
||||
}
|
||||
|
||||
@@ -111,5 +120,8 @@ declare global {
|
||||
"expanded-changed": {
|
||||
expanded: boolean;
|
||||
};
|
||||
"expanded-will-change": {
|
||||
expanded: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { formatNumber } from "../common/string/format_number";
|
||||
import { afterNextRender } from "../common/util/render-status";
|
||||
import { FrontendTranslationData } from "../data/translation";
|
||||
import { FrontendLocaleData } from "../data/translation";
|
||||
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||
|
||||
const getAngle = (value: number, min: number, max: number) => {
|
||||
@@ -23,7 +23,7 @@ export class Gauge extends LitElement {
|
||||
|
||||
@property({ type: Number }) public value = 0;
|
||||
|
||||
@property() public locale!: FrontendTranslationData;
|
||||
@property() public locale!: FrontendLocaleData;
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { useAmPm } from "../../common/datetime/use_am_pm";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { TimeSelector } from "../../data/selector";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../paper-time-input";
|
||||
|
||||
@customElement("ha-selector-time")
|
||||
export class HaTimeSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@@ -17,13 +20,12 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _useAmPm = memoizeOne((language: string) => {
|
||||
const test = new Date().toLocaleString(language);
|
||||
return test.includes("AM") || test.includes("PM");
|
||||
});
|
||||
private _useAmPmMem = memoizeOne((locale: FrontendLocaleData): boolean =>
|
||||
useAmPm(locale)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const useAMPM = this._useAmPm(this.hass.locale.language);
|
||||
const useAMPM = this._useAmPmMem(this.hass.locale);
|
||||
|
||||
const parts = this.value?.split(":") || [];
|
||||
const hours = parts[0];
|
||||
@@ -48,7 +50,7 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
private _timeChanged(ev) {
|
||||
let value = ev.target.value;
|
||||
const useAMPM = this._useAmPm(this.hass.locale.language);
|
||||
const useAMPM = this._useAmPmMem(this.hass.locale);
|
||||
let hours = Number(ev.target.hour || 0);
|
||||
if (value && useAMPM) {
|
||||
if (ev.target.amPm === "PM") {
|
||||
|
||||
@@ -6,33 +6,22 @@ import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: { service: string; name: string } }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'>[[item.name]]</div>
|
||||
<div secondary>[[item.service]]</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
|
||||
root.querySelector(".name")!.textContent = model.item.name;
|
||||
root.querySelector("[secondary]")!.textContent =
|
||||
model.item.name === model.item.service ? "" : model.item.service;
|
||||
};
|
||||
const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
|
||||
item
|
||||
) => html`<style>
|
||||
paper-item {
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line>
|
||||
${item.name}
|
||||
<span secondary>${item.name === item.service ? "" : item.service}</span>
|
||||
</paper-item-body>
|
||||
</paper-item>`;
|
||||
|
||||
class HaServicePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -117,7 +117,10 @@ export class HaTab extends LitElement {
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
padding: 0 16px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class LocationEditor extends LitElement {
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.themes?.darkMode === this.hass.themes?.darkMode) {
|
||||
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
|
||||
return;
|
||||
}
|
||||
if (!this._leafletMap || !this._tileLayer) {
|
||||
@@ -118,7 +118,7 @@ class LocationEditor extends LitElement {
|
||||
this.Leaflet,
|
||||
this._leafletMap,
|
||||
this._tileLayer,
|
||||
this.hass.themes?.darkMode
|
||||
this.hass.themes.darkMode
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ class LocationEditor extends LitElement {
|
||||
private async _initMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
this.darkMode ?? this.hass.themes?.darkMode,
|
||||
this.darkMode ?? this.hass.themes.darkMode,
|
||||
Boolean(this.radius)
|
||||
);
|
||||
this._leafletMap.addEventListener(
|
||||
|
||||
@@ -241,12 +241,9 @@ let inititialAutomationEditorData: Partial<AutomationConfig> | undefined;
|
||||
export const getAutomationConfig = (hass: HomeAssistant, id: string) =>
|
||||
hass.callApi<AutomationConfig>("GET", `config/automation/config/${id}`);
|
||||
|
||||
export const showAutomationEditor = (
|
||||
el: HTMLElement,
|
||||
data?: Partial<AutomationConfig>
|
||||
) => {
|
||||
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
|
||||
inititialAutomationEditorData = data;
|
||||
navigate(el, "/config/automation/edit/new");
|
||||
navigate("/config/automation/edit/new");
|
||||
};
|
||||
|
||||
export const getAutomationEditorInitData = () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ export const DISCOVERY_SOURCES = [
|
||||
"zeroconf",
|
||||
"discovery",
|
||||
"mqtt",
|
||||
"hassio",
|
||||
];
|
||||
|
||||
export const ATTENTION_SOURCES = ["reauth"];
|
||||
|
||||
@@ -42,11 +42,10 @@ export interface HassioFullSnapshotCreateParams {
|
||||
name: string;
|
||||
password?: string;
|
||||
}
|
||||
export interface HassioPartialSnapshotCreateParams {
|
||||
name: string;
|
||||
export interface HassioPartialSnapshotCreateParams
|
||||
extends HassioFullSnapshotCreateParams {
|
||||
folders?: string[];
|
||||
addons?: string[];
|
||||
password?: string;
|
||||
homeassistant?: boolean;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { FrontendTranslationData } from "./translation";
|
||||
import { FrontendLocaleData } from "./translation";
|
||||
|
||||
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
|
||||
const LINE_ATTRIBUTES_TO_KEEP = [
|
||||
@@ -109,7 +109,7 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
|
||||
|
||||
const processTimelineEntity = (
|
||||
localize: LocalizeFunc,
|
||||
language: FrontendTranslationData,
|
||||
language: FrontendLocaleData,
|
||||
states: HassEntity[]
|
||||
): TimelineEntity => {
|
||||
const data: TimelineState[] = [];
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const MAIN_WINDOW_NAME = "ha-main-window";
|
||||
+2
-5
@@ -20,12 +20,9 @@ export const SCENE_IGNORED_DOMAINS = [
|
||||
|
||||
let inititialSceneEditorData: Partial<SceneConfig> | undefined;
|
||||
|
||||
export const showSceneEditor = (
|
||||
el: HTMLElement,
|
||||
data?: Partial<SceneConfig>
|
||||
) => {
|
||||
export const showSceneEditor = (data?: Partial<SceneConfig>) => {
|
||||
inititialSceneEditorData = data;
|
||||
navigate(el, "/config/scene/edit/new");
|
||||
navigate("/config/scene/edit/new");
|
||||
};
|
||||
|
||||
export const getSceneEditorInitData = () => {
|
||||
|
||||
+2
-5
@@ -182,12 +182,9 @@ export const deleteScript = (hass: HomeAssistant, objectId: string) =>
|
||||
|
||||
let inititialScriptEditorData: Partial<ScriptConfig> | undefined;
|
||||
|
||||
export const showScriptEditor = (
|
||||
el: HTMLElement,
|
||||
data?: Partial<ScriptConfig>
|
||||
) => {
|
||||
export const showScriptEditor = (data?: Partial<ScriptConfig>) => {
|
||||
inititialScriptEditorData = data;
|
||||
navigate(el, "/config/script/edit/new");
|
||||
navigate("/config/script/edit/new");
|
||||
};
|
||||
|
||||
export const getScriptEditorInitData = () => {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import {
|
||||
HassEntity,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import durationToSeconds from "../common/datetime/duration_to_seconds";
|
||||
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export type TimerEntity = HassEntityBase & {
|
||||
@@ -55,3 +59,46 @@ export const deleteTimer = (hass: HomeAssistant, id: string) =>
|
||||
type: "timer/delete",
|
||||
timer_id: id,
|
||||
});
|
||||
|
||||
export const timerTimeRemaining = (
|
||||
stateObj: HassEntity
|
||||
): undefined | number => {
|
||||
if (!stateObj.attributes.remaining) {
|
||||
return undefined;
|
||||
}
|
||||
let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
|
||||
|
||||
if (stateObj.state === "active") {
|
||||
const now = new Date().getTime();
|
||||
const madeActive = new Date(stateObj.last_changed).getTime();
|
||||
timeRemaining = Math.max(timeRemaining - (now - madeActive) / 1000, 0);
|
||||
}
|
||||
|
||||
return timeRemaining;
|
||||
};
|
||||
|
||||
export const computeDisplayTimer = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: HassEntity,
|
||||
timeRemaining?: number
|
||||
): string | null => {
|
||||
if (!stateObj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stateObj.state === "idle" || timeRemaining === 0) {
|
||||
return computeStateDisplay(hass.localize, stateObj, hass.locale);
|
||||
}
|
||||
|
||||
let display = secondsToDuration(timeRemaining || 0);
|
||||
|
||||
if (stateObj.state === "paused") {
|
||||
display = `${display} (${computeStateDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.locale
|
||||
)})`;
|
||||
}
|
||||
|
||||
return display;
|
||||
};
|
||||
|
||||
+11
-3
@@ -10,14 +10,22 @@ export enum NumberFormat {
|
||||
none = "none",
|
||||
}
|
||||
|
||||
export interface FrontendTranslationData {
|
||||
export enum TimeFormat {
|
||||
language = "language",
|
||||
system = "system",
|
||||
am_pm = "12",
|
||||
twenty_four = "24",
|
||||
}
|
||||
|
||||
export interface FrontendLocaleData {
|
||||
language: string;
|
||||
number_format: NumberFormat;
|
||||
time_format: TimeFormat;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
language: FrontendTranslationData;
|
||||
language: FrontendLocaleData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +44,7 @@ export const fetchTranslationPreferences = (hass: HomeAssistant) =>
|
||||
|
||||
export const saveTranslationPreferences = (
|
||||
hass: HomeAssistant,
|
||||
data: FrontendTranslationData
|
||||
data: FrontendLocaleData
|
||||
) => saveFrontendUserData(hass.connection, "language", data);
|
||||
|
||||
export const getHassTranslations = async (
|
||||
|
||||
+11
-1
@@ -1,6 +1,6 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
|
||||
export interface Theme {
|
||||
export interface ThemeVars {
|
||||
// Incomplete
|
||||
"primary-color": string;
|
||||
"text-primary-color": string;
|
||||
@@ -8,10 +8,20 @@ export interface Theme {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type Theme = ThemeVars & {
|
||||
modes?: {
|
||||
light?: ThemeVars;
|
||||
dark?: ThemeVars;
|
||||
};
|
||||
};
|
||||
|
||||
export interface Themes {
|
||||
default_theme: string;
|
||||
default_dark_theme: string | null;
|
||||
themes: Record<string, Theme>;
|
||||
// Currently effective dark mode. Will never be undefined. If user selected "auto"
|
||||
// in theme picker, this property will still contain either true or false based on
|
||||
// what has been determined via system preferences and support from the selected theme.
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -1,8 +1,12 @@
|
||||
import { navigate } from "../common/navigate";
|
||||
import {
|
||||
DEFAULT_ACCENT_COLOR,
|
||||
DEFAULT_PRIMARY_COLOR,
|
||||
} from "../resources/ha-style";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const defaultRadiusColor = "#FF9800";
|
||||
export const homeRadiusColor = "#03a9f4";
|
||||
export const defaultRadiusColor = DEFAULT_ACCENT_COLOR;
|
||||
export const homeRadiusColor = DEFAULT_PRIMARY_COLOR;
|
||||
export const passiveRadiusColor = "#9b9b9b";
|
||||
|
||||
export interface Zone {
|
||||
@@ -52,12 +56,9 @@ export const deleteZone = (hass: HomeAssistant, zoneId: string) =>
|
||||
|
||||
let inititialZoneEditorData: Partial<ZoneMutableParams> | undefined;
|
||||
|
||||
export const showZoneEditor = (
|
||||
el: HTMLElement,
|
||||
data?: Partial<ZoneMutableParams>
|
||||
) => {
|
||||
export const showZoneEditor = (data?: Partial<ZoneMutableParams>) => {
|
||||
inititialZoneEditorData = data;
|
||||
navigate(el, "/config/zone/new");
|
||||
navigate("/config/zone/new");
|
||||
};
|
||||
|
||||
export const getZoneEditorInitData = () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
|
||||
|
||||
declare global {
|
||||
@@ -67,25 +68,26 @@ export const showDialog = async (
|
||||
}
|
||||
|
||||
if (addHistory) {
|
||||
top.history.replaceState(
|
||||
mainWindow.history.replaceState(
|
||||
{
|
||||
dialog: dialogTag,
|
||||
open: false,
|
||||
oldState:
|
||||
top.history.state?.open && top.history.state?.dialog !== dialogTag
|
||||
? top.history.state
|
||||
mainWindow.history.state?.open &&
|
||||
mainWindow.history.state?.dialog !== dialogTag
|
||||
? mainWindow.history.state
|
||||
: null,
|
||||
},
|
||||
""
|
||||
);
|
||||
try {
|
||||
top.history.pushState(
|
||||
mainWindow.history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: dialogParams, open: true },
|
||||
""
|
||||
);
|
||||
} catch (err) {
|
||||
// dialogParams could not be cloned, probably contains callback
|
||||
top.history.pushState(
|
||||
mainWindow.history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: null, open: true },
|
||||
""
|
||||
);
|
||||
@@ -96,7 +98,10 @@ export const showDialog = async (
|
||||
};
|
||||
|
||||
export const replaceDialog = () => {
|
||||
top.history.replaceState({ ...top.history.state, replaced: true }, "");
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, replaced: true },
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
export const closeDialog = async (dialogTag: string): Promise<boolean> => {
|
||||
|
||||
@@ -65,6 +65,7 @@ class MoreInfoCover extends LocalizeMixin(PolymerElement) {
|
||||
</div>
|
||||
</div>
|
||||
<ha-attributes
|
||||
hass="[[hass]]"
|
||||
state-obj="[[stateObj]]"
|
||||
extra-filters="current_position,current_tilt_position"
|
||||
></ha-attributes>
|
||||
|
||||
@@ -15,7 +15,10 @@ class MoreInfoDefault extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html` <ha-attributes .stateObj=${this.stateObj}></ha-attributes> `;
|
||||
return html`<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-attributes>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
</div>
|
||||
|
||||
<ha-attributes
|
||||
hass="[[hass]]"
|
||||
state-obj="[[stateObj]]"
|
||||
extra-filters="percentage_step,speed,preset_mode,preset_modes,speed_list,percentage,oscillating,direction"
|
||||
></ha-attributes>
|
||||
|
||||
@@ -226,6 +226,7 @@ class MoreInfoLight extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,rgbw_color,rgbww_color,xy_color,min_mireds,max_mireds,entity_id,supported_color_modes,color_mode"
|
||||
></ha-attributes>
|
||||
@@ -552,8 +553,6 @@ class MoreInfoLight extends LitElement {
|
||||
|
||||
.content > * {
|
||||
width: 100%;
|
||||
max-height: 84px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.color_temp {
|
||||
|
||||
@@ -39,6 +39,7 @@ class MoreInfoLock extends LocalizeMixin(PolymerElement) {
|
||||
>
|
||||
</template>
|
||||
<ha-attributes
|
||||
hass="[[hass]]"
|
||||
state-obj="[[stateObj]]"
|
||||
extra-filters="code_format"
|
||||
></ha-attributes>
|
||||
|
||||
@@ -24,6 +24,7 @@ class MoreInfoPerson extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="id,user_id,editable"
|
||||
></ha-attributes>
|
||||
@@ -54,7 +55,7 @@ class MoreInfoPerson extends LitElement {
|
||||
}
|
||||
|
||||
private _handleAction() {
|
||||
showZoneEditor(this, {
|
||||
showZoneEditor({
|
||||
latitude: this.stateObj!.attributes.latitude,
|
||||
longitude: this.stateObj!.attributes.longitude,
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ class MoreInfoRemote extends LitElement {
|
||||
: ""}
|
||||
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.extraFilters=${filterExtraAttributes}
|
||||
></ha-attributes>
|
||||
|
||||
@@ -18,6 +18,7 @@ class MoreInfoTimer extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="remaining"
|
||||
></ha-attributes>
|
||||
|
||||
@@ -190,6 +190,7 @@ class MoreInfoVacuum extends LitElement {
|
||||
: ""}
|
||||
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.extraFilters=${filterExtraAttributes}
|
||||
></ha-attributes>
|
||||
|
||||
@@ -306,7 +306,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
idToPassThroughUrl = stateObj.attributes.id;
|
||||
}
|
||||
|
||||
navigate(this, `/config/${domain}/edit/${idToPassThroughUrl}`);
|
||||
navigate(`/config/${domain}/edit/${idToPassThroughUrl}`);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -557,7 +557,7 @@ export class QuickBar extends LitElement {
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
action: () => navigate(this, item.path),
|
||||
action: () => navigate(item.path),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -117,9 +117,13 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
href="${this._agentInfo.onboarding.url}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
><mwc-button unelevated>Yes!</mwc-button></a
|
||||
><mwc-button unelevated
|
||||
>${this.hass.localize("ui.common.yes")}!</mwc-button
|
||||
></a
|
||||
>
|
||||
<mwc-button outlined
|
||||
>${this.hass.localize("ui.common.no")}</mwc-button
|
||||
>
|
||||
<mwc-button outlined>No</mwc-button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -155,7 +159,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
<div class="input">
|
||||
<paper-input
|
||||
@keyup=${this._handleKeyUp}
|
||||
label="${this.hass!.localize(
|
||||
.label="${this.hass.localize(
|
||||
`ui.dialogs.voice_command.${
|
||||
SpeechRecognition ? "label_voice" : "label"
|
||||
}`
|
||||
|
||||
@@ -25,6 +25,9 @@ import { subscribeUser } from "../data/ws-user";
|
||||
import type { ExternalAuth } from "../external_app/external_auth";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { MAIN_WINDOW_NAME } from "../data/main_window";
|
||||
|
||||
window.name = MAIN_WINDOW_NAME;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
||||
@@ -47,7 +47,13 @@ function initialize(
|
||||
) {
|
||||
const style = document.createElement("style");
|
||||
|
||||
style.innerHTML = "body{margin:0}";
|
||||
style.innerHTML = `body { margin:0; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
}`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const config = panel.config._panel_custom;
|
||||
@@ -82,7 +88,7 @@ function initialize(
|
||||
if (window.parent.customPanel) {
|
||||
window.parent.customPanel.navigate(
|
||||
window.location.pathname,
|
||||
ev.detail ? ev.detail.replace : false
|
||||
ev.detail
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -116,7 +122,7 @@ function initialize(
|
||||
document.body.addEventListener("click", (ev) => {
|
||||
const href = isNavigationClick(ev);
|
||||
if (href) {
|
||||
navigate(document.body, href);
|
||||
navigate(href);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
import { computeLocalize } from "../common/translations/localize";
|
||||
import { DEFAULT_PANEL } from "../data/panel";
|
||||
import { NumberFormat } from "../data/translation";
|
||||
import { NumberFormat, TimeFormat } from "../data/translation";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { getLocalLanguage, getTranslation } from "../util/hass-translation";
|
||||
@@ -215,6 +215,7 @@ export const provideHass = (
|
||||
locale: {
|
||||
language: localLanguage,
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
},
|
||||
resources: null as any,
|
||||
localize: () => "",
|
||||
@@ -276,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: {
|
||||
@@ -284,11 +285,11 @@ export const provideHass = (
|
||||
},
|
||||
},
|
||||
});
|
||||
const { themes, selectedTheme } = hass();
|
||||
const { themes, selectedThemeSettings } = hass();
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
themes,
|
||||
selectedTheme!.theme
|
||||
selectedThemeSettings!.theme
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ export class HassRouterPage extends ReactiveElement {
|
||||
const defaultPage = routerOptions.defaultPage;
|
||||
|
||||
if (route && route.path === "" && defaultPage !== undefined) {
|
||||
navigate(this, `${route.prefix}/${defaultPage}`, true);
|
||||
navigate(`${route.prefix}/${defaultPage}`, { replace: true });
|
||||
}
|
||||
|
||||
let newPage = route
|
||||
@@ -127,7 +127,7 @@ export class HassRouterPage extends ReactiveElement {
|
||||
|
||||
// Update the url if we know where we're mounted.
|
||||
if (route) {
|
||||
navigate(this, `${route.prefix}/${result}`, true);
|
||||
navigate(`${route.prefix}/${result}`, { replace: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,10 @@ class HassTabsSubpage extends LitElement {
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
:host([narrow]) .toolbar a {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#tabbar {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
@@ -242,7 +246,7 @@ class HassTabsSubpage extends LitElement {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
justify-content: space-between;
|
||||
justify-content: space-around;
|
||||
z-index: 2;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
@@ -258,10 +262,6 @@ class HassTabsSubpage extends LitElement {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
ha-tab {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-menu-button,
|
||||
ha-icon-button-arrow-prev,
|
||||
::slotted([slot="toolbar-icon"]) {
|
||||
|
||||
@@ -45,7 +45,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
const path = curPath();
|
||||
|
||||
if (["", "/"].includes(path)) {
|
||||
navigate(this, `/${getStorageDefaultPanelUrlPath()}`, true);
|
||||
navigate(`/${getStorageDefaultPanelUrlPath()}`, { replace: true });
|
||||
}
|
||||
this._route = {
|
||||
prefix: "",
|
||||
@@ -106,7 +106,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
window.addEventListener("click", (ev) => {
|
||||
const href = isNavigationClick(ev);
|
||||
if (href) {
|
||||
navigate(this, href);
|
||||
navigate(href);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,25 +81,27 @@ class SupervisorErrorScreen extends LitElement {
|
||||
|
||||
private _applyTheme() {
|
||||
let themeName: string;
|
||||
let options: 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);
|
||||
|
||||
options = this.hass.selectedTheme;
|
||||
if (themeName === "default" && options?.dark === undefined) {
|
||||
options = {
|
||||
...this.hass.selectedTheme,
|
||||
themeSettings = this.hass.selectedThemeSettings;
|
||||
if (themeName === "default" && themeSettings?.dark === undefined) {
|
||||
themeSettings = {
|
||||
...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;
|
||||
}
|
||||
|
||||
@@ -107,7 +109,7 @@ class SupervisorErrorScreen extends LitElement {
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
options
|
||||
themeSettings
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export default dedupingMixin(
|
||||
(superClass) =>
|
||||
class extends superClass {
|
||||
navigate(...args) {
|
||||
navigate(this, ...args);
|
||||
navigate(...args);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -63,7 +63,7 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
|
||||
});
|
||||
if (response.status === 401) {
|
||||
// If we get a unauthorized response, the restore is done
|
||||
navigate(this, "/", true);
|
||||
navigate("/", { replace: true });
|
||||
location.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -149,7 +149,7 @@ class DialogAreaDetail extends LitElement {
|
||||
this._submitting = false;
|
||||
}
|
||||
|
||||
navigate(this, "/config/areas/dashboard");
|
||||
navigate("/config/areas/dashboard");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -192,7 +192,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const areaId = ev.detail.id;
|
||||
navigate(this, `/config/areas/area/${areaId}`);
|
||||
navigate(`/config/areas/area/${areaId}`);
|
||||
}
|
||||
|
||||
private _openDialog(entry?: AreaRegistryEntry) {
|
||||
|
||||
@@ -108,7 +108,7 @@ class DialogNewAutomation extends LitElement implements HassDialog {
|
||||
replaceDialog();
|
||||
showThingtalkDialog(this, {
|
||||
callback: (config: Partial<AutomationConfig> | undefined) =>
|
||||
showAutomationEditor(this, config),
|
||||
showAutomationEditor(config),
|
||||
input: this.shadowRoot!.querySelector("paper-input")!.value as string,
|
||||
});
|
||||
this.closeDialog();
|
||||
@@ -117,13 +117,13 @@ class DialogNewAutomation extends LitElement implements HassDialog {
|
||||
private async _blueprintPicked(ev: CustomEvent) {
|
||||
this.closeDialog();
|
||||
await nextRender();
|
||||
showAutomationEditor(this, { use_blueprint: { path: ev.detail.value } });
|
||||
showAutomationEditor({ use_blueprint: { path: ev.detail.value } });
|
||||
}
|
||||
|
||||
private async _blank() {
|
||||
this.closeDialog();
|
||||
await nextRender();
|
||||
showAutomationEditor(this);
|
||||
showAutomationEditor();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -450,7 +450,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
// Wait for dialog to complate closing
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
showAutomationEditor(this, {
|
||||
showAutomationEditor({
|
||||
...this._config,
|
||||
id: undefined,
|
||||
alias: `${this._config?.alias} (${this.hass.localize(
|
||||
@@ -503,7 +503,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
this._dirty = false;
|
||||
|
||||
if (!this.automationId) {
|
||||
navigate(this, `/config/automation/edit/${id}`, true);
|
||||
navigate(`/config/automation/edit/${id}`, { replace: true });
|
||||
}
|
||||
},
|
||||
(errors) => {
|
||||
|
||||
@@ -324,7 +324,7 @@ class HaAutomationPicker extends LitElement {
|
||||
) {
|
||||
showNewAutomationDialog(this);
|
||||
} else {
|
||||
navigate(this, "/config/automation/edit/new");
|
||||
navigate("/config/automation/edit/new");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,11 +46,8 @@ interface BlueprintMetaDataPath extends BlueprintMetaData {
|
||||
}
|
||||
|
||||
const createNewFunctions = {
|
||||
automation: (
|
||||
context: HaBlueprintOverview,
|
||||
blueprintMeta: BlueprintMetaDataPath
|
||||
) => {
|
||||
showAutomationEditor(context, {
|
||||
automation: (blueprintMeta: BlueprintMetaDataPath) => {
|
||||
showAutomationEditor({
|
||||
alias: blueprintMeta.name,
|
||||
use_blueprint: { path: blueprintMeta.path },
|
||||
});
|
||||
@@ -183,7 +180,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
super.firstUpdated(changedProps);
|
||||
if (this.route.path === "/import") {
|
||||
const url = extractSearchParam("blueprint_url");
|
||||
navigate(this, "/config/blueprint/dashboard", true);
|
||||
navigate("/config/blueprint/dashboard", { replace: true });
|
||||
if (url) {
|
||||
this._addBlueprint(url);
|
||||
}
|
||||
@@ -280,7 +277,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
|
||||
private _createNew(ev) {
|
||||
const blueprint = ev.currentTarget.blueprint as BlueprintMetaDataPath;
|
||||
createNewFunctions[blueprint.domain](this, blueprint);
|
||||
createNewFunctions[blueprint.domain](blueprint);
|
||||
}
|
||||
|
||||
private _share(ev) {
|
||||
|
||||
@@ -92,7 +92,9 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
<paper-item-body
|
||||
>[[localize('ui.panel.config.cloud.account.connection_status')]]</paper-item-body
|
||||
>
|
||||
<div class="status">[[cloudStatus.cloud]]</div>
|
||||
<div class="status">
|
||||
[[_computeConnectionStatus(cloudStatus.cloud)]]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
@@ -189,10 +191,12 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
this._fetchSubscriptionInfo();
|
||||
}
|
||||
|
||||
_computeRemoteConnected(connected) {
|
||||
return connected
|
||||
_computeConnectionStatus(status) {
|
||||
return status === "connected"
|
||||
? this.hass.localize("ui.panel.config.cloud.account.connected")
|
||||
: this.hass.localize("ui.panel.config.cloud.account.not_connected");
|
||||
: status === "disconnected"
|
||||
? this.hass.localize("ui.panel.config.cloud.account.not_connected")
|
||||
: this.hass.localize("ui.panel.config.cloud.account.connecting");
|
||||
}
|
||||
|
||||
async _fetchSubscriptionInfo() {
|
||||
|
||||
@@ -81,7 +81,7 @@ class HaConfigCloud extends HassRouterPage {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("cloud-done", (ev) => {
|
||||
this._flashMessage = (ev as any).detail.flashMessage;
|
||||
navigate(this, "/config/cloud/login");
|
||||
navigate("/config/cloud/login");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class HaConfigCloud extends HassRouterPage {
|
||||
if (oldStatus === undefined) {
|
||||
this._resolveCloudStatusLoaded();
|
||||
} else if (oldStatus.logged_in !== this.cloudStatus.logged_in) {
|
||||
navigate(this, this.route.prefix, true);
|
||||
navigate(this.route.prefix, { replace: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +72,12 @@ export abstract class HaDeviceAutomationCard<
|
||||
return;
|
||||
}
|
||||
if (this.script) {
|
||||
showScriptEditor(this, { sequence: [automation as DeviceAction] });
|
||||
showScriptEditor({ sequence: [automation as DeviceAction] });
|
||||
return;
|
||||
}
|
||||
const data = {};
|
||||
data[this.type] = [automation];
|
||||
showAutomationEditor(this, data);
|
||||
showAutomationEditor(data);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
-1
@@ -66,7 +66,6 @@ export class HaDeviceActionsOzw extends LitElement {
|
||||
|
||||
private async _nodeDetailsClicked() {
|
||||
navigate(
|
||||
this,
|
||||
`/config/ozw/network/${this.ozw_instance}/node/${this.node_id}/dashboard`
|
||||
);
|
||||
}
|
||||
|
||||
+2
-5
@@ -107,14 +107,11 @@ export class HaDeviceActionsZha extends LitElement {
|
||||
}
|
||||
|
||||
private _onAddDevicesClick() {
|
||||
navigate(this, "/config/zha/add/" + this._zhaDevice!.ieee);
|
||||
navigate(`/config/zha/add/${this._zhaDevice!.ieee}`);
|
||||
}
|
||||
|
||||
private _onViewInVisualizationClick() {
|
||||
navigate(
|
||||
this,
|
||||
"/config/zha/visualization/" + this._zhaDevice!.device_reg_id
|
||||
);
|
||||
navigate(`/config/zha/visualization/${this._zhaDevice!.device_reg_id}`);
|
||||
}
|
||||
|
||||
private async _handleZigbeeInfoClicked() {
|
||||
|
||||
@@ -533,7 +533,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
this._entities(this.deviceId, this.entities).forEach((entity) => {
|
||||
entities[entity.entity_id] = "";
|
||||
});
|
||||
showSceneEditor(this, {
|
||||
showSceneEditor({
|
||||
entities,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,6 +68,30 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
@state() private _numHiddenDevices = 0;
|
||||
|
||||
private _ignoreLocationChange = false;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
window.addEventListener("location-changed", () => {
|
||||
if (this._ignoreLocationChange) {
|
||||
this._ignoreLocationChange = false;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
window.location.search.substring(1) !== this._searchParms.toString()
|
||||
) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
}
|
||||
});
|
||||
window.addEventListener("popstate", () => {
|
||||
if (
|
||||
window.location.search.substring(1) !== this._searchParms.toString()
|
||||
) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _activeFilters = memoizeOne(
|
||||
(
|
||||
entries: ConfigEntry[],
|
||||
@@ -78,10 +102,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
filters.forEach((value, key) => {
|
||||
switch (key) {
|
||||
case "config_entry": {
|
||||
// If we are requested to show the devices for a given config entry,
|
||||
// also show the disabled ones by default.
|
||||
this._showDisabled = true;
|
||||
|
||||
const configEntry = entries.find(
|
||||
(entry) => entry.entry_id === value
|
||||
);
|
||||
@@ -118,7 +138,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
) => {
|
||||
// Some older installations might have devices pointing at invalid entryIDs
|
||||
// So we guard for that.
|
||||
|
||||
let outputDevices: DeviceRowData[] = devices;
|
||||
|
||||
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
|
||||
@@ -315,14 +334,14 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
window.addEventListener("location-changed", () => {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
});
|
||||
window.addEventListener("popstate", () => {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
});
|
||||
public willUpdate(changedProps) {
|
||||
if (changedProps.has("_searchParms")) {
|
||||
if (this._searchParms.get("config_entry")) {
|
||||
// If we are requested to show the devices for a given config entry,
|
||||
// also show the disabled ones by default.
|
||||
this._showDisabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -435,7 +454,8 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const deviceId = ev.detail.id;
|
||||
navigate(this, `/config/devices/device/${deviceId}`);
|
||||
this._ignoreLocationChange = true;
|
||||
navigate(`/config/devices/device/${deviceId}`);
|
||||
}
|
||||
|
||||
private _showDisabledChanged(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
@@ -453,7 +473,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
if (
|
||||
this._activeFilters(this.entries, this._searchParms, this.hass.localize)
|
||||
) {
|
||||
navigate(this, window.location.pathname, true);
|
||||
navigate(window.location.pathname, { replace: true });
|
||||
}
|
||||
this._showDisabled = true;
|
||||
}
|
||||
|
||||
@@ -391,10 +391,18 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
public constructor() {
|
||||
super();
|
||||
window.addEventListener("location-changed", () => {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
if (
|
||||
window.location.search.substring(1) !== this._searchParms.toString()
|
||||
) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
}
|
||||
});
|
||||
window.addEventListener("popstate", () => {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
if (
|
||||
window.location.search.substring(1) !== this._searchParms.toString()
|
||||
) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -623,8 +631,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
loadEntityEditorDialog();
|
||||
}
|
||||
|
||||
protected updated(changedProps): void {
|
||||
super.updated(changedProps);
|
||||
public willUpdate(changedProps): void {
|
||||
super.willUpdate(changedProps);
|
||||
const oldHass = changedProps.get("hass");
|
||||
let changed = false;
|
||||
if (!this.hass || !this._entities) {
|
||||
@@ -829,7 +837,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
if (
|
||||
this._activeFilters(this._searchParms, this.hass.localize, this._entries)
|
||||
) {
|
||||
navigate(this, window.location.pathname, true);
|
||||
navigate(window.location.pathname, { replace: true });
|
||||
}
|
||||
this._showDisabled = true;
|
||||
this._showReadOnly = true;
|
||||
|
||||
@@ -191,7 +191,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
): [
|
||||
Map<string, ConfigEntryExtended[]>,
|
||||
ConfigEntryExtended[],
|
||||
Map<string, ConfigEntryExtended[]>
|
||||
Map<string, ConfigEntryExtended[]>,
|
||||
// Counter for disabled integrations since the tuple element above will
|
||||
// be grouped by the integration name and therefore not provide a valid count
|
||||
number
|
||||
] => {
|
||||
const filteredConfigEnties = this._filterConfigEntries(
|
||||
configEntries,
|
||||
@@ -210,6 +213,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
groupByIntegration(filteredConfigEnties),
|
||||
ignored,
|
||||
groupByIntegration(disabled),
|
||||
disabled.length,
|
||||
];
|
||||
}
|
||||
);
|
||||
@@ -267,6 +271,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
groupedConfigEntries,
|
||||
ignoredConfigEntries,
|
||||
disabledConfigEntries,
|
||||
disabledCount,
|
||||
] = this._filterGroupConfigEntries(this._configEntries, this._filter);
|
||||
const configEntriesInProgress = this._filterConfigEntriesInProgress(
|
||||
this._configEntriesInProgress,
|
||||
@@ -338,11 +343,11 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.integrations.search"
|
||||
)}
|
||||
></search-input>
|
||||
${!this._showDisabled && disabledConfigEntries.size
|
||||
${!this._showDisabled && disabledCount
|
||||
? html`<div class="active-filters">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.disable.disabled_integrations",
|
||||
{ number: disabledConfigEntries.size }
|
||||
{ number: disabledCount }
|
||||
)}
|
||||
<mwc-button
|
||||
@click=${this._toggleShowDisabled}
|
||||
@@ -594,7 +599,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
|
||||
private async _handleAdd(localizePromise: Promise<LocalizeFunc>) {
|
||||
const domain = extractSearchParam("domain");
|
||||
navigate(this, "/config/integrations", true);
|
||||
navigate("/config/integrations", { replace: true });
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,11 +143,9 @@ class OZWConfigDashboard extends LitElement {
|
||||
private async _fetchData() {
|
||||
this._instances = await fetchOZWInstances(this.hass!);
|
||||
if (this._instances.length === 1) {
|
||||
navigate(
|
||||
this,
|
||||
`/config/ozw/network/${this._instances[0].ozw_instance}`,
|
||||
true
|
||||
);
|
||||
navigate(`/config/ozw/network/${this._instances[0].ozw_instance}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class OZWNetworkDashboard extends LitElement {
|
||||
|
||||
protected firstUpdated() {
|
||||
if (!this.ozwInstance) {
|
||||
navigate(this, "/config/ozw/dashboard", true);
|
||||
navigate("/config/ozw/dashboard", { replace: true });
|
||||
} else if (this.hass) {
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class OZWNetworkNodes extends LitElement {
|
||||
|
||||
protected firstUpdated() {
|
||||
if (!this.ozwInstance) {
|
||||
navigate(this, "/config/ozw/dashboard", true);
|
||||
navigate("/config/ozw/dashboard", { replace: true });
|
||||
} else if (this.hass) {
|
||||
this._fetchData();
|
||||
}
|
||||
@@ -117,7 +117,7 @@ class OZWNetworkNodes extends LitElement {
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const nodeId = ev.detail.id;
|
||||
navigate(this, `/config/ozw/network/${this.ozwInstance}/node/${nodeId}`);
|
||||
navigate(`/config/ozw/network/${this.ozwInstance}/node/${nodeId}`);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -47,9 +47,11 @@ class OZWNodeConfig extends LitElement {
|
||||
|
||||
protected firstUpdated() {
|
||||
if (!this.ozwInstance) {
|
||||
navigate(this, "/config/ozw/dashboard", true);
|
||||
navigate("/config/ozw/dashboard", { replace: true });
|
||||
} else if (!this.nodeId) {
|
||||
navigate(this, `/config/ozw/network/${this.ozwInstance}/nodes`, true);
|
||||
navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
@@ -43,9 +43,11 @@ class OZWNodeDashboard extends LitElement {
|
||||
|
||||
protected firstUpdated() {
|
||||
if (!this.ozwInstance) {
|
||||
navigate(this, "/config/ozw/dashboard", true);
|
||||
navigate("/config/ozw/dashboard", { replace: true });
|
||||
} else if (!this.nodeId) {
|
||||
navigate(this, `/config/ozw/network/${this.ozwInstance}/nodes`, true);
|
||||
navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, {
|
||||
replace: true,
|
||||
});
|
||||
} else if (this.hass) {
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
@@ -68,11 +68,10 @@ class OZWNodeRouter extends HassRouterPage {
|
||||
if (this._configEntry && !searchParams.has("config_entry")) {
|
||||
searchParams.append("config_entry", this._configEntry);
|
||||
navigate(
|
||||
this,
|
||||
`${this.routeTail.prefix}${
|
||||
this.routeTail.path
|
||||
}?${searchParams.toString()}`,
|
||||
true
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
this._processingAdd = false;
|
||||
this._groupName = "";
|
||||
this._zhaDevicesDataTable.clearSelection();
|
||||
navigate(this, `/config/zha/group/${group.group_id}`, true);
|
||||
navigate(`/config/zha/group/${group.group_id}`, { replace: true });
|
||||
}
|
||||
|
||||
private _handleNameChange(ev: PolymerChangedEvent<string>) {
|
||||
|
||||
+1
-2
@@ -67,11 +67,10 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
if (this._configEntry && !searchParams.has("config_entry")) {
|
||||
searchParams.append("config_entry", this._configEntry);
|
||||
navigate(
|
||||
this,
|
||||
`${this.routeTail.prefix}${
|
||||
this.routeTail.path
|
||||
}?${searchParams.toString()}`,
|
||||
true
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export class ZHAGroupPage extends LitElement {
|
||||
|
||||
private async _deleteGroup(): Promise<void> {
|
||||
await removeGroups(this.hass, [this.groupId]);
|
||||
navigate(this, `/config/zha/groups`, true);
|
||||
navigate(`/config/zha/groups`, { replace: true });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user