mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-13 15:09:41 +00:00
Compare commits
59 Commits
20211108.0
...
ha-formfie
Author | SHA1 | Date | |
---|---|---|---|
![]() |
294967014d | ||
![]() |
366aa8aed1 | ||
![]() |
43011179eb | ||
![]() |
6177d2b416 | ||
![]() |
f70485bc49 | ||
![]() |
921763b5f1 | ||
![]() |
5fd4315789 | ||
![]() |
ed291b57d0 | ||
![]() |
f833701e7c | ||
![]() |
8533b90957 | ||
![]() |
c95a54c6f3 | ||
![]() |
a991640f52 | ||
![]() |
3d99b92c07 | ||
![]() |
d28ad17135 | ||
![]() |
3c67fc96b1 | ||
![]() |
4719636176 | ||
![]() |
45efee28b8 | ||
![]() |
3bcf225380 | ||
![]() |
2e81f843ce | ||
![]() |
a430142296 | ||
![]() |
6335b13c5e | ||
![]() |
6c4e987a24 | ||
![]() |
1a5c43d72a | ||
![]() |
91dbfca899 | ||
![]() |
96f103644a | ||
![]() |
5304e5a670 | ||
![]() |
390e5b3881 | ||
![]() |
9f5756c9fa | ||
![]() |
0ca35d7012 | ||
![]() |
0d19f4792f | ||
![]() |
91b009af79 | ||
![]() |
1ebd2fb9f1 | ||
![]() |
4684979ae7 | ||
![]() |
a567312bdb | ||
![]() |
1e851e0e8c | ||
![]() |
7d94615f47 | ||
![]() |
582fab7ea1 | ||
![]() |
822590ec8a | ||
![]() |
e9f0967578 | ||
![]() |
481da19c74 | ||
![]() |
b969db0c0f | ||
![]() |
a6b98fc3c3 | ||
![]() |
87c2046ab5 | ||
![]() |
4b992fb0c4 | ||
![]() |
3154011c65 | ||
![]() |
4e68383cf7 | ||
![]() |
db6ef22ebb | ||
![]() |
c238c7dbbc | ||
![]() |
d04823b4c5 | ||
![]() |
4cb45d6313 | ||
![]() |
6623e5f017 | ||
![]() |
6518aefb7f | ||
![]() |
d5600b7c08 | ||
![]() |
4789295d32 | ||
![]() |
70d54aa855 | ||
![]() |
77549efc47 | ||
![]() |
00299bc74d | ||
![]() |
b74fc5578d | ||
![]() |
9018d4cc18 |
File diff suppressed because one or more lines are too long
@@ -11,6 +11,7 @@
|
||||
--progress-color: #03a9f4;
|
||||
--splash-image: url('https://home-assistant.io/images/cast/splash.png');
|
||||
--splash-size: cover;
|
||||
--background-color: #41bdf5;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import { CastReceiverContext } from "chromecast-caf-receiver/cast.framework";
|
||||
|
||||
const castContext =
|
||||
cast.framework.CastContext.getInstance() as unknown as CastReceiverContext;
|
||||
const castContext = cast.framework.CastReceiverContext.getInstance();
|
||||
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
|
@@ -8,6 +8,9 @@ import { ReceivedMessage } from "./types";
|
||||
|
||||
const lovelaceController = new HcMain();
|
||||
document.body.append(lovelaceController);
|
||||
lovelaceController.addEventListener("cast-view-changed", (ev) => {
|
||||
playDummyMedia(ev.detail.title);
|
||||
});
|
||||
|
||||
const mediaPlayer = document.createElement("cast-media-player");
|
||||
mediaPlayer.style.display = "none";
|
||||
@@ -28,21 +31,29 @@ const setTouchControlsVisibility = (visible: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
const playDummyMedia = () => {
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
let timeOut: number | undefined;
|
||||
|
||||
const playDummyMedia = (viewTitle?: string) => {
|
||||
const loadRequestData = new cast.framework.messages.LoadRequestData();
|
||||
loadRequestData.autoplay = true;
|
||||
loadRequestData.media = new cast.framework.messages.MediaInformation();
|
||||
loadRequestData.media.contentId =
|
||||
"https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png";
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
||||
loadRequestData.media.contentType = "image/jpeg";
|
||||
loadRequestData.media.streamType = cast.framework.messages.StreamType.NONE;
|
||||
const metadata = new cast.framework.messages.GenericMediaMetadata();
|
||||
metadata.title = "Home Assistant Lovelace";
|
||||
metadata.title = viewTitle;
|
||||
loadRequestData.media.metadata = metadata;
|
||||
|
||||
loadRequestData.requestId = 0;
|
||||
playerManager.load(loadRequestData);
|
||||
if (timeOut) {
|
||||
clearTimeout(timeOut);
|
||||
timeOut = undefined;
|
||||
}
|
||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
timeOut = window.setTimeout(() => playDummyMedia(viewTitle), 540000); // repeat every 9 minutes to keep it active (gets deactivated after 10 minutes)
|
||||
}
|
||||
};
|
||||
|
||||
const showLovelaceController = () => {
|
||||
@@ -50,7 +61,6 @@ const showLovelaceController = () => {
|
||||
lovelaceController.style.display = "initial";
|
||||
document.body.setAttribute("style", "overflow-y: auto !important");
|
||||
setTouchControlsVisibility(false);
|
||||
playDummyMedia();
|
||||
};
|
||||
|
||||
const showMediaPlayer = () => {
|
||||
@@ -69,6 +79,7 @@ const showMediaPlayer = () => {
|
||||
--progress-color: #03a9f4;
|
||||
--splash-image: url('https://home-assistant.io/images/cast/splash.png');
|
||||
--splash-size: cover;
|
||||
--background-color: #41bdf5;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
@@ -81,22 +92,6 @@ options.customNamespaces = {
|
||||
[CAST_NS]: cast.framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
// The docs say we need to set options.touchScreenOptimizeApp = true
|
||||
// https://developers.google.com/cast/docs/caf_receiver/customize_ui#accessing_ui_controls
|
||||
// This doesn't work.
|
||||
// @ts-ignore
|
||||
options.touchScreenOptimizedApp = true;
|
||||
|
||||
// The class reference say we can set a uiConfig in options to set it
|
||||
// https://developers.google.com/cast/docs/reference/caf_receiver/cast.framework.CastReceiverOptions#uiConfig
|
||||
// This doesn't work either.
|
||||
// @ts-ignore
|
||||
options.uiConfig = new cast.framework.ui.UiConfig();
|
||||
// @ts-ignore
|
||||
options.uiConfig.touchScreenOptimizedApp = true;
|
||||
|
||||
castContext.setInactivityTimeout(86400); // 1 day
|
||||
|
||||
castContext.addCustomMessageListener(
|
||||
CAST_NS,
|
||||
// @ts-ignore
|
||||
@@ -123,7 +118,7 @@ playerManager.setMessageInterceptor(
|
||||
(loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
"https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png"
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png"
|
||||
) {
|
||||
return loadRequestData;
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { LovelaceConfig } from "../../../../src/data/lovelace";
|
||||
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||
import "../../../../src/panels/lovelace/views/hui-view";
|
||||
@@ -14,7 +15,7 @@ class HcLovelace extends LitElement {
|
||||
|
||||
@property() public viewPath?: string | number;
|
||||
|
||||
public urlPath?: string | null;
|
||||
@property() public urlPath: string | null = null;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const index = this._viewIndex;
|
||||
@@ -30,7 +31,7 @@ class HcLovelace extends LitElement {
|
||||
config: this.lovelaceConfig,
|
||||
rawConfig: this.lovelaceConfig,
|
||||
editMode: false,
|
||||
urlPath: this.urlPath!,
|
||||
urlPath: this.urlPath,
|
||||
enableFullEditMode: () => undefined,
|
||||
mode: "storage",
|
||||
locale: this.hass.locale,
|
||||
@@ -54,6 +55,21 @@ class HcLovelace extends LitElement {
|
||||
const index = this._viewIndex;
|
||||
|
||||
if (index !== undefined) {
|
||||
const dashboardTitle = this.lovelaceConfig.title || this.urlPath;
|
||||
|
||||
const viewTitle =
|
||||
this.lovelaceConfig.views[index].title ||
|
||||
this.lovelaceConfig.views[index].path;
|
||||
|
||||
fireEvent(this, "cast-view-changed", {
|
||||
title:
|
||||
dashboardTitle || viewTitle
|
||||
? `${dashboardTitle || ""}${
|
||||
dashboardTitle && viewTitle ? ": " : ""
|
||||
}${viewTitle || ""}`
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const configBackground =
|
||||
this.lovelaceConfig.views[index].background ||
|
||||
this.lovelaceConfig.background;
|
||||
@@ -101,8 +117,15 @@ class HcLovelace extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CastViewChanged {
|
||||
title: string | undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hc-lovelace": HcLovelace;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"cast-view-changed": CastViewChanged;
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,11 @@ import {
|
||||
ShowDemoMessage,
|
||||
ShowLovelaceViewMessage,
|
||||
} from "../../../../src/cast/receiver_messages";
|
||||
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
|
||||
import {
|
||||
ReceiverErrorCode,
|
||||
ReceiverErrorMessage,
|
||||
ReceiverStatusMessage,
|
||||
} from "../../../../src/cast/sender_messages";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
|
||||
import {
|
||||
@@ -40,9 +44,9 @@ export class HcMain extends HassElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _unsubLovelace?: UnsubscribeFunc;
|
||||
@state() private _urlPath?: string | null;
|
||||
|
||||
private _urlPath?: string | null;
|
||||
private _unsubLovelace?: UnsubscribeFunc;
|
||||
|
||||
public processIncomingMessage(msg: HassMessage) {
|
||||
if (msg.type === "connect") {
|
||||
@@ -68,8 +72,10 @@ export class HcMain extends HassElement {
|
||||
!this._lovelaceConfig ||
|
||||
this._lovelacePath === null ||
|
||||
// Guard against part of HA not being loaded yet.
|
||||
(this.hass &&
|
||||
(!this.hass.states || !this.hass.config || !this.hass.services))
|
||||
!this.hass ||
|
||||
!this.hass.states ||
|
||||
!this.hass.config ||
|
||||
!this.hass.services
|
||||
) {
|
||||
return html`
|
||||
<hc-launch-screen
|
||||
@@ -119,7 +125,7 @@ export class HcMain extends HassElement {
|
||||
|
||||
if (this.hass) {
|
||||
status.hassUrl = this.hass.auth.data.hassUrl;
|
||||
status.lovelacePath = this._lovelacePath!;
|
||||
status.lovelacePath = this._lovelacePath;
|
||||
status.urlPath = this._urlPath;
|
||||
}
|
||||
|
||||
@@ -132,6 +138,26 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _sendError(
|
||||
error_code: number,
|
||||
error_message: string,
|
||||
senderId?: string
|
||||
) {
|
||||
const error: ReceiverErrorMessage = {
|
||||
type: "receiver_error",
|
||||
error_code,
|
||||
error_message,
|
||||
};
|
||||
|
||||
if (senderId) {
|
||||
this.sendMessage(senderId, error);
|
||||
} else {
|
||||
for (const sender of castContext.getSenders()) {
|
||||
this.sendMessage(sender.id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _dialogClosed = () => {
|
||||
document.body.setAttribute("style", "overflow-y: auto !important");
|
||||
};
|
||||
@@ -154,14 +180,18 @@ export class HcMain extends HassElement {
|
||||
}),
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._error = this._getErrorMessage(err);
|
||||
const errorMessage = this._getErrorMessage(err);
|
||||
this._error = errorMessage;
|
||||
this._sendError(err, errorMessage);
|
||||
return;
|
||||
}
|
||||
let connection;
|
||||
try {
|
||||
connection = await createConnection({ auth });
|
||||
} catch (err: any) {
|
||||
this._error = this._getErrorMessage(err);
|
||||
const errorMessage = this._getErrorMessage(err);
|
||||
this._error = errorMessage;
|
||||
this._sendError(err, errorMessage);
|
||||
return;
|
||||
}
|
||||
if (this.hass) {
|
||||
@@ -173,24 +203,29 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
|
||||
private async _handleShowLovelaceMessage(msg: ShowLovelaceViewMessage) {
|
||||
this._showDemo = false;
|
||||
// We should not get this command before we are connected.
|
||||
// Means a client got out of sync. Let's send status to them.
|
||||
if (!this.hass) {
|
||||
this._sendStatus(msg.senderId!);
|
||||
this._error = "Cannot show Lovelace because we're not connected.";
|
||||
this._sendError(ReceiverErrorCode.NOT_CONNECTED, this._error);
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
if (msg.urlPath === "lovelace") {
|
||||
msg.urlPath = null;
|
||||
}
|
||||
this._lovelacePath = msg.viewPath;
|
||||
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
|
||||
this._urlPath = msg.urlPath;
|
||||
this._lovelaceConfig = undefined;
|
||||
if (this._unsubLovelace) {
|
||||
this._unsubLovelace();
|
||||
}
|
||||
const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107)
|
||||
? getLovelaceCollection(this.hass!.connection, msg.urlPath)
|
||||
: getLegacyLovelaceCollection(this.hass!.connection);
|
||||
? getLovelaceCollection(this.hass.connection, msg.urlPath)
|
||||
: getLegacyLovelaceCollection(this.hass.connection);
|
||||
// We first do a single refresh because we need to check if there is LL
|
||||
// configuration.
|
||||
try {
|
||||
@@ -199,8 +234,16 @@ export class HcMain extends HassElement {
|
||||
this._handleNewLovelaceConfig(lovelaceConfig)
|
||||
);
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line
|
||||
console.log("Error fetching Lovelace configuration", err, msg);
|
||||
if (
|
||||
atLeastVersion(this.hass.connection.haVersion, 0, 107) &&
|
||||
err.code !== "config_not_found"
|
||||
) {
|
||||
// eslint-disable-next-line
|
||||
console.log("Error fetching Lovelace configuration", err, msg);
|
||||
this._error = `Error fetching Lovelace configuration: ${err.message}`;
|
||||
this._sendError(ReceiverErrorCode.FETCH_CONFIG_FAILED, this._error);
|
||||
return;
|
||||
}
|
||||
// Generate a Lovelace config.
|
||||
this._unsubLovelace = () => undefined;
|
||||
await this._generateLovelaceConfig();
|
||||
@@ -215,8 +258,6 @@ export class HcMain extends HassElement {
|
||||
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
|
||||
}
|
||||
}
|
||||
this._showDemo = false;
|
||||
this._lovelacePath = msg.viewPath;
|
||||
|
||||
this._sendStatus();
|
||||
}
|
||||
@@ -237,7 +278,7 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
|
||||
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
|
||||
castContext.setApplicationState(lovelaceConfig.title!);
|
||||
castContext.setApplicationState(lovelaceConfig.title || "");
|
||||
this._lovelaceConfig = lovelaceConfig;
|
||||
}
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
BIN
gallery/public/images/office.jpg
Normal file
BIN
gallery/public/images/office.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
@@ -1,15 +1,19 @@
|
||||
import { html, css, LitElement, TemplateResult } from "lit";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-logo-svg";
|
||||
|
||||
const alerts: {
|
||||
title?: string;
|
||||
description: string | TemplateResult;
|
||||
type: "info" | "warning" | "error" | "success";
|
||||
dismissable?: boolean;
|
||||
action?: string;
|
||||
rtl?: boolean;
|
||||
iconSlot?: TemplateResult;
|
||||
actionSlot?: TemplateResult;
|
||||
}[] = [
|
||||
{
|
||||
title: "Test info alert",
|
||||
@@ -73,13 +77,35 @@ const alerts: {
|
||||
title: "Error with action",
|
||||
description: "This is a test error alert with action",
|
||||
type: "error",
|
||||
action: "restart",
|
||||
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
|
||||
},
|
||||
{
|
||||
title: "Unsaved data",
|
||||
description: "You have unsaved data",
|
||||
type: "warning",
|
||||
action: "save",
|
||||
actionSlot: html`<mwc-button slot="action" label="save"></mwc-button>`,
|
||||
},
|
||||
{
|
||||
title: "Slotted icon",
|
||||
description: "Alert with slotted icon",
|
||||
type: "warning",
|
||||
iconSlot: html`<span slot="icon" class="image">
|
||||
<ha-logo-svg></ha-logo-svg>
|
||||
</span>`,
|
||||
},
|
||||
{
|
||||
title: "Slotted image",
|
||||
description: "Alert with slotted image",
|
||||
type: "warning",
|
||||
iconSlot: html`<span slot="icon" class="image"
|
||||
><img src="https://www.home-assistant.io/images/home-assistant-logo.svg"
|
||||
/></span>`,
|
||||
},
|
||||
{
|
||||
title: "Slotted action",
|
||||
description: "Alert with slotted action",
|
||||
type: "info",
|
||||
actionSlot: html`<mwc-button slot="action" label="action"></mwc-button>`,
|
||||
},
|
||||
{
|
||||
description: "Dismissable information (RTL)",
|
||||
@@ -91,7 +117,7 @@ const alerts: {
|
||||
title: "Error with action",
|
||||
description: "This is a test error alert with action (RTL)",
|
||||
type: "error",
|
||||
action: "restart",
|
||||
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
|
||||
rtl: true,
|
||||
},
|
||||
{
|
||||
@@ -106,30 +132,60 @@ const alerts: {
|
||||
export class DemoHaAlert extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="ha-alert demo">
|
||||
<div class="card-content">
|
||||
${alerts.map(
|
||||
(alert) => html`
|
||||
<ha-alert
|
||||
.title=${alert.title || ""}
|
||||
.alertType=${alert.type}
|
||||
.dismissable=${alert.dismissable || false}
|
||||
.actionText=${alert.action || ""}
|
||||
.rtl=${alert.rtl || false}
|
||||
>
|
||||
${alert.description}
|
||||
</ha-alert>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-alert ${mode} demo">
|
||||
<div class="card-content">
|
||||
${alerts.map(
|
||||
(alert) => html`
|
||||
<ha-alert
|
||||
.title=${alert.title || ""}
|
||||
.alertType=${alert.type}
|
||||
.dismissable=${alert.dismissable || false}
|
||||
.rtl=${alert.rtl || false}
|
||||
>
|
||||
${alert.iconSlot} ${alert.description} ${alert.actionSlot}
|
||||
</ha-alert>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
ha-alert {
|
||||
@@ -142,8 +198,17 @@ export class DemoHaAlert extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
span {
|
||||
margin-right: 16px;
|
||||
.image {
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
max-height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
mwc-button {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-chip";
|
||||
import "../../../src/components/ha-chip-set";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
|
||||
const chips: {
|
||||
@@ -22,8 +23,8 @@ const chips: {
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-ha-chip")
|
||||
export class DemoHaChip extends LitElement {
|
||||
@customElement("demo-ha-chips")
|
||||
export class DemoHaChips extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="ha-chip demo">
|
||||
@@ -41,6 +42,23 @@ export class DemoHaChip extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card header="ha-chip-set demo">
|
||||
<div class="card-content">
|
||||
<ha-chip-set>
|
||||
${chips.map(
|
||||
(chip) => html`
|
||||
<ha-chip .hasIcon=${chip.icon !== undefined}>
|
||||
${chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: ""}
|
||||
${chip.content}
|
||||
</ha-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -50,12 +68,19 @@ export class DemoHaChip extends LitElement {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
ha-chip {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-ha-chip": DemoHaChip;
|
||||
"demo-ha-chips": DemoHaChips;
|
||||
}
|
||||
}
|
156
gallery/src/demos/demo-hui-area-card.ts
Normal file
156
gallery/src/demos/demo-hui-area-card.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, query } from "lit/decorators";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("light", "bed_light", "on", {
|
||||
friendly_name: "Bed Light",
|
||||
}),
|
||||
getEntity("switch", "bed_ac", "on", {
|
||||
friendly_name: "Ecobee",
|
||||
}),
|
||||
getEntity("sensor", "bed_temp", "72", {
|
||||
friendly_name: "Bedroom Temp",
|
||||
device_class: "temperature",
|
||||
unit_of_measurement: "°F",
|
||||
}),
|
||||
getEntity("light", "living_room_light", "off", {
|
||||
friendly_name: "Living Room Light",
|
||||
}),
|
||||
getEntity("fan", "living_room", "on", {
|
||||
friendly_name: "Living Room Fan",
|
||||
}),
|
||||
getEntity("sensor", "office_humidity", "73", {
|
||||
friendly_name: "Office Humidity",
|
||||
device_class: "humidity",
|
||||
unit_of_measurement: "%",
|
||||
}),
|
||||
getEntity("light", "office", "on", {
|
||||
friendly_name: "Office Light",
|
||||
}),
|
||||
getEntity("fan", "kitchen", "on", {
|
||||
friendly_name: "Second Office Fan",
|
||||
}),
|
||||
getEntity("binary_sensor", "kitchen_door", "on", {
|
||||
friendly_name: "Office Door",
|
||||
device_class: "door",
|
||||
}),
|
||||
];
|
||||
|
||||
// TODO: Update image here
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "Bedroom",
|
||||
config: `
|
||||
- type: area
|
||||
area: bedroom
|
||||
image: "/images/bed.png"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Living Room",
|
||||
config: `
|
||||
- type: area
|
||||
area: living_room
|
||||
image: "/images/living_room.png"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Office",
|
||||
config: `
|
||||
- type: area
|
||||
area: office
|
||||
image: "/images/office.jpg"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Kitchen",
|
||||
config: `
|
||||
- type: area
|
||||
area: kitchen
|
||||
image: "/images/kitchen.png"
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-hui-area-card")
|
||||
class DemoArea extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
hass.mockWS("config/area_registry/list", () => [
|
||||
{
|
||||
name: "Bedroom",
|
||||
area_id: "bedroom",
|
||||
},
|
||||
{
|
||||
name: "Living Room",
|
||||
area_id: "living_room",
|
||||
},
|
||||
{
|
||||
name: "Office",
|
||||
area_id: "office",
|
||||
},
|
||||
{
|
||||
name: "Second Office",
|
||||
area_id: "kitchen",
|
||||
},
|
||||
]);
|
||||
hass.mockWS("config/device_registry/list", () => []);
|
||||
hass.mockWS("config/entity_registry/list", () => [
|
||||
{
|
||||
area_id: "bedroom",
|
||||
entity_id: "light.bed_light",
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
entity_id: "switch.bed_ac",
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
entity_id: "sensor.bed_temp",
|
||||
},
|
||||
{
|
||||
area_id: "living_room",
|
||||
entity_id: "light.living_room_light",
|
||||
},
|
||||
{
|
||||
area_id: "living_room",
|
||||
entity_id: "fan.living_room",
|
||||
},
|
||||
{
|
||||
area_id: "office",
|
||||
entity_id: "light.office",
|
||||
},
|
||||
{
|
||||
area_id: "office",
|
||||
entity_id: "sensor.office_humidity",
|
||||
},
|
||||
{
|
||||
area_id: "kitchen",
|
||||
entity_id: "fan.kitchen",
|
||||
},
|
||||
{
|
||||
area_id: "kitchen",
|
||||
entity_id: "binary_sensor.kitchen_door",
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-hui-area-card": DemoArea;
|
||||
}
|
||||
}
|
@@ -176,11 +176,6 @@ class HaGallery extends PolymerElement {
|
||||
this.addEventListener("alert-dismissed-clicked", () =>
|
||||
this.$.notifications.showDialog({ message: "Alert dismissed clicked" })
|
||||
);
|
||||
|
||||
this.addEventListener("alert-action-clicked", () =>
|
||||
this.$.notifications.showDialog({ message: "Alert action clicked" })
|
||||
);
|
||||
|
||||
this.addEventListener("hass-more-info", (ev) => {
|
||||
if (ev.detail.entityId) {
|
||||
this.$.notifications.showDialog({
|
||||
|
@@ -25,11 +25,10 @@ import {
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
|
||||
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addon-repository";
|
||||
|
||||
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
||||
@@ -76,16 +75,12 @@ class HassioAddonStore extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs}
|
||||
main-page
|
||||
supervisor
|
||||
.header=${this.supervisor.localize("panel.store")}
|
||||
>
|
||||
<span slot="header"> ${this.supervisor.localize("panel.store")} </span>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
@@ -133,7 +128,7 @@ class HassioAddonStore extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</hass-tabs-subpage>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -108,7 +108,6 @@ class HassioAddonDashboard extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"}
|
||||
.route=${route}
|
||||
.tabs=${addonTabs}
|
||||
supervisor
|
||||
|
@@ -4,7 +4,7 @@ import "../../../../src/components/ha-circular-progress";
|
||||
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { HomeAssistant, Route } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import "./hassio-addon-info";
|
||||
|
||||
@@ -12,6 +12,8 @@ import "./hassio-addon-info";
|
||||
class HassioAddonInfoDashboard extends LitElement {
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
@@ -27,6 +29,7 @@ class HassioAddonInfoDashboard extends LitElement {
|
||||
<div class="content">
|
||||
<hassio-addon-info
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.addon=${this.addon}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import "@material/mwc-button";
|
||||
import {
|
||||
mdiArrowUpBoldCircle,
|
||||
mdiCheckCircle,
|
||||
mdiChip,
|
||||
mdiCircle,
|
||||
@@ -49,7 +48,6 @@ import {
|
||||
startHassioAddon,
|
||||
stopHassioAddon,
|
||||
uninstallHassioAddon,
|
||||
updateHassioAddon,
|
||||
validateHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import {
|
||||
@@ -64,14 +62,14 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { HomeAssistant, Route } from "../../../../src/types";
|
||||
import { bytesToString } from "../../../../src/util/bytes-to-string";
|
||||
import "../../components/hassio-card-content";
|
||||
import "../../components/supervisor-metric";
|
||||
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import { addonArchIsSupported } from "../../util/addon";
|
||||
import "../../update-available/update-available-card";
|
||||
import { addonArchIsSupported, extractChangelog } from "../../util/addon";
|
||||
|
||||
const STAGE_ICON = {
|
||||
stable: mdiCheckCircle,
|
||||
@@ -92,6 +90,8 @@ const RATING_ICON = {
|
||||
class HassioAddonInfo extends LitElement {
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
@@ -128,69 +128,12 @@ class HassioAddonInfo extends LitElement {
|
||||
return html`
|
||||
${this.addon.update_available
|
||||
? html`
|
||||
<ha-card
|
||||
.header="${this.supervisor.localize(
|
||||
"common.update_available",
|
||||
"count",
|
||||
1
|
||||
)}🎉"
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${this.supervisor.localize(
|
||||
"addon.dashboard.new_update_available",
|
||||
"name",
|
||||
this.addon.name,
|
||||
"version",
|
||||
this.addon.version_latest
|
||||
)}
|
||||
.description=${this.supervisor.localize(
|
||||
"common.running_version",
|
||||
"version",
|
||||
this.addon.version
|
||||
)}
|
||||
icon=${mdiArrowUpBoldCircle}
|
||||
iconClass="update"
|
||||
></hassio-card-content>
|
||||
${!this.addon.available && addonStoreInfo
|
||||
? !addonArchIsSupported(
|
||||
this.supervisor.info.supported_arch,
|
||||
this.addon.arch
|
||||
)
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch",
|
||||
"core_version_installed",
|
||||
this.supervisor.core.version,
|
||||
"core_version_needed",
|
||||
addonStoreInfo.homeassistant
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${this.addon.changelog
|
||||
? html`
|
||||
<mwc-button @click=${this._openChangelog}>
|
||||
${this.supervisor.localize("addon.dashboard.changelog")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`<span></span>`}
|
||||
<mwc-button @click=${this._updateClicked}>
|
||||
${this.supervisor.localize("common.update")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<update-available-card
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.supervisor=${this.supervisor}
|
||||
.addonSlug=${this.addon.slug}
|
||||
></update-available-card>
|
||||
`
|
||||
: ""}
|
||||
${!this.addon.protected
|
||||
@@ -200,14 +143,18 @@ class HassioAddonInfo extends LitElement {
|
||||
.title=${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.title"
|
||||
)}
|
||||
.actionText=${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.enable"
|
||||
)}
|
||||
@alert-action-clicked=${this._protectionToggled}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.content"
|
||||
)}
|
||||
<mwc-button
|
||||
slot="action"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.enable"
|
||||
)}
|
||||
@click=${this._protectionToggled}
|
||||
>
|
||||
</mwc-button>
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
@@ -899,22 +846,14 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
private async _openChangelog(): Promise<void> {
|
||||
try {
|
||||
let content = await fetchHassioAddonChangelog(this.hass, this.addon.slug);
|
||||
if (
|
||||
content.includes(`# ${this.addon.version}`) &&
|
||||
content.includes(`# ${this.addon.version_latest}`)
|
||||
) {
|
||||
const newcontent = content.split(`# ${this.addon.version}`)[0];
|
||||
if (newcontent.includes(`# ${this.addon.version_latest}`)) {
|
||||
// Only change the content if the new version still exist
|
||||
// if the changelog does not have the newests version on top
|
||||
// this will not be true, and we don't modify the content
|
||||
content = newcontent;
|
||||
}
|
||||
}
|
||||
const content = await fetchHassioAddonChangelog(
|
||||
this.hass,
|
||||
this.addon.slug
|
||||
);
|
||||
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.changelog"),
|
||||
content,
|
||||
content: extractChangelog(this.addon, content),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
@@ -989,33 +928,6 @@ class HassioAddonInfo extends LitElement {
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _updateClicked(): Promise<void> {
|
||||
showDialogSupervisorUpdate(this, {
|
||||
supervisor: this.supervisor,
|
||||
name: this.addon.name,
|
||||
version: this.addon.version_latest,
|
||||
backupParams: {
|
||||
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
||||
addons: [this.addon.slug],
|
||||
homeassistant: false,
|
||||
},
|
||||
updateHandler: async () => this._updateAddon(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateAddon(): Promise<void> {
|
||||
await updateHassioAddon(this.hass, this.addon.slug);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "addon",
|
||||
});
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "update",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
}
|
||||
|
||||
private async _startClicked(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
@@ -1244,6 +1156,17 @@ class HassioAddonInfo extends LitElement {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
ha-alert mwc-button {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
update-available-card {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
ha-chip {
|
||||
line-height: 36px;
|
||||
|
@@ -158,7 +158,7 @@ export class HassioBackups extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${supervisorTabs}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.searchLabel=${this.supervisor.localize("search")}
|
||||
|
@@ -20,7 +20,9 @@ class HassioAddons extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>${this.supervisor.localize("dashboard.addons")}</h1>
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html` <h1>${this.supervisor.localize("dashboard.addons")}</h1> `
|
||||
: ""}
|
||||
<div class="card-group">
|
||||
${!this.supervisor.supervisor.addons?.length
|
||||
? html`
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { mdiStorePlus } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import "../../../src/components/ha-fab";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
@@ -25,23 +28,37 @@ class HassioDashboard extends LitElement {
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
main-page
|
||||
supervisor
|
||||
hasFab
|
||||
>
|
||||
<span slot="header">
|
||||
${this.supervisor.localize("panel.dashboard")}
|
||||
</span>
|
||||
<div class="content">
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-update>
|
||||
${this.hass.config.version.includes("dev") ||
|
||||
!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html`
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-update>
|
||||
`
|
||||
: ""}
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-addons>
|
||||
</div>
|
||||
|
||||
<a href="/hassio/store" slot="fab">
|
||||
<ha-fab .label=${this.supervisor.localize("panel.store")} extended>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiStorePlus}
|
||||
></ha-svg-icon> </ha-fab
|
||||
></a>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
@@ -3,34 +3,18 @@ import { mdiHomeAssistant } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
HassioResponse,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { updateCore } from "../../../src/data/supervisor/core";
|
||||
import {
|
||||
Supervisor,
|
||||
supervisorApiWsRequest,
|
||||
} from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
const computeVersion = (key: string, version: string): string =>
|
||||
@@ -73,26 +57,18 @@ export class HassioUpdate extends LitElement {
|
||||
${this._renderUpdateCard(
|
||||
"Home Assistant Core",
|
||||
"core",
|
||||
this.supervisor.core,
|
||||
"hassio/homeassistant/update",
|
||||
`https://${
|
||||
this.supervisor.core.version_latest.includes("b") ? "rc" : "www"
|
||||
}.home-assistant.io/latest-release-notes/`
|
||||
this.supervisor.core
|
||||
)}
|
||||
${this._renderUpdateCard(
|
||||
"Supervisor",
|
||||
"supervisor",
|
||||
this.supervisor.supervisor,
|
||||
"hassio/supervisor/update",
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
|
||||
this.supervisor.supervisor
|
||||
)}
|
||||
${this.supervisor.host.features.includes("haos")
|
||||
? this._renderUpdateCard(
|
||||
"Operating System",
|
||||
"os",
|
||||
this.supervisor.os,
|
||||
"hassio/os/update",
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
|
||||
this.supervisor.os
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
@@ -103,9 +79,7 @@ export class HassioUpdate extends LitElement {
|
||||
private _renderUpdateCard(
|
||||
name: string,
|
||||
key: string,
|
||||
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
|
||||
apiPath: string,
|
||||
releaseNotesUrl: string
|
||||
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo
|
||||
): TemplateResult {
|
||||
if (!object.update_available) {
|
||||
return html``;
|
||||
@@ -136,96 +110,15 @@ export class HassioUpdate extends LitElement {
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a href=${releaseNotesUrl} target="_blank" rel="noreferrer">
|
||||
<mwc-button>
|
||||
${this.supervisor.localize("common.release_notes")}
|
||||
<a href="/hassio/update-available/${key}">
|
||||
<mwc-button .label=${this.supervisor.localize("common.show")}>
|
||||
</mwc-button>
|
||||
</a>
|
||||
<ha-progress-button
|
||||
.apiPath=${apiPath}
|
||||
.name=${name}
|
||||
.key=${key}
|
||||
.version=${object.version_latest}
|
||||
@click=${this._confirmUpdate}
|
||||
>
|
||||
${this.supervisor.localize("common.update")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _confirmUpdate(ev): Promise<void> {
|
||||
const item = ev.currentTarget;
|
||||
if (item.key === "core") {
|
||||
showDialogSupervisorUpdate(this, {
|
||||
supervisor: this.supervisor,
|
||||
name: "Home Assistant Core",
|
||||
version: this.supervisor.core.version_latest,
|
||||
backupParams: {
|
||||
name: `core_${this.supervisor.core.version}`,
|
||||
folders: ["homeassistant"],
|
||||
homeassistant: true,
|
||||
},
|
||||
updateHandler: async () => this._updateCore(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
item.progress = true;
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"confirm.update.title",
|
||||
"name",
|
||||
item.name
|
||||
),
|
||||
text: this.supervisor.localize(
|
||||
"confirm.update.text",
|
||||
"name",
|
||||
item.name,
|
||||
"version",
|
||||
computeVersion(item.key, item.version)
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.update"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
item.progress = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||
await supervisorApiWsRequest(this.hass.connection, {
|
||||
method: "post",
|
||||
endpoint: item.apiPath.replace("hassio", ""),
|
||||
timeout: null,
|
||||
});
|
||||
} else {
|
||||
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
|
||||
}
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: item.key,
|
||||
});
|
||||
} catch (err: any) {
|
||||
// Only show an error if the status code was not expected (user behind proxy)
|
||||
// or no status at all(connection terminated)
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("common.error.update_failed"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
item.progress = false;
|
||||
}
|
||||
|
||||
private async _updateCore(): Promise<void> {
|
||||
await updateCore(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "core",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -1,203 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../../src/data/hassio/common";
|
||||
import { createHassioPartialBackup } from "../../../../src/data/hassio/backup";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
|
||||
|
||||
@customElement("dialog-supervisor-update")
|
||||
class DialogSupervisorUpdate extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _createBackup = true;
|
||||
|
||||
@state() private _action: "backup" | "update" | null = null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state()
|
||||
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
|
||||
|
||||
public async showDialog(
|
||||
params: SupervisorDialogSupervisorUpdateParams
|
||||
): Promise<void> {
|
||||
this._opened = true;
|
||||
this._dialogParams = params;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._action = null;
|
||||
this._createBackup = true;
|
||||
this._error = undefined;
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.updateComplete.then(() =>
|
||||
(
|
||||
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
|
||||
)?.focus()
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._dialogParams) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
|
||||
${this._action === null
|
||||
? html`<slot name="heading">
|
||||
<h2 id="title" class="header_title">
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"confirm.update.title",
|
||||
"name",
|
||||
this._dialogParams.name
|
||||
)}
|
||||
</h2>
|
||||
</slot>
|
||||
<div>
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"confirm.update.text",
|
||||
"name",
|
||||
this._dialogParams.name,
|
||||
"version",
|
||||
this._dialogParams.version
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"dialog.update.backup"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"dialog.update.create_backup",
|
||||
"name",
|
||||
this._dialogParams.name
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this._createBackup}
|
||||
haptic
|
||||
@click=${this._toggleBackup}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
${this._dialogParams.supervisor.localize("common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
.disabled=${this._error !== undefined}
|
||||
@click=${this._update}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("common.update")}
|
||||
</mwc-button>`
|
||||
: html`<ha-circular-progress alt="Updating" size="large" active>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this._action === "update"
|
||||
? this._dialogParams.supervisor.localize(
|
||||
"dialog.update.updating",
|
||||
"name",
|
||||
this._dialogParams.name,
|
||||
"version",
|
||||
this._dialogParams.version
|
||||
)
|
||||
: this._dialogParams.supervisor.localize(
|
||||
"dialog.update.creating_backup",
|
||||
"name",
|
||||
this._dialogParams.name
|
||||
)}
|
||||
</p>`}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleBackup() {
|
||||
this._createBackup = !this._createBackup;
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
if (this._createBackup) {
|
||||
this._action = "backup";
|
||||
try {
|
||||
await createHassioPartialBackup(
|
||||
this.hass,
|
||||
this._dialogParams!.backupParams
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._action = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._action = "update";
|
||||
try {
|
||||
await this._dialogParams!.updateHandler!();
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._action = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
ha-settings-row {
|
||||
margin-top: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-supervisor-update": DialogSupervisorUpdate;
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface SupervisorDialogSupervisorUpdateParams {
|
||||
supervisor: Supervisor;
|
||||
name: string;
|
||||
version: string;
|
||||
backupParams: any;
|
||||
updateHandler: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const showDialogSupervisorUpdate = (
|
||||
element: HTMLElement,
|
||||
dialogParams: SupervisorDialogSupervisorUpdateParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-supervisor-update",
|
||||
dialogImport: () => import("./dialog-supervisor-update"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -10,7 +10,7 @@ import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import "../../src/layouts/hass-loading-screen";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import "./hassio-router";
|
||||
import { SupervisorBaseElement } from "./supervisor-base-element";
|
||||
|
||||
@@ -24,8 +24,6 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route?: Route;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
|
@@ -34,6 +34,9 @@ const REDIRECTS: Redirects = {
|
||||
supervisor_store: {
|
||||
redirect: "/hassio/store",
|
||||
},
|
||||
supervisor_addons: {
|
||||
redirect: "/hassio/dashboard",
|
||||
},
|
||||
supervisor_addon: {
|
||||
redirect: "/hassio/addon",
|
||||
params: {
|
||||
|
@@ -35,6 +35,10 @@ class HassioRouter extends HassRouterPage {
|
||||
backups: "dashboard",
|
||||
store: "dashboard",
|
||||
system: "dashboard",
|
||||
"update-available": {
|
||||
tag: "update-available-dashboard",
|
||||
load: () => import("./update-available/update-available-dashboard"),
|
||||
},
|
||||
addon: {
|
||||
tag: "hassio-addon-dashboard",
|
||||
load: () => import("./addon-view/hassio-addon-dashboard"),
|
||||
|
@@ -1,16 +1,22 @@
|
||||
import { mdiBackupRestore, mdiCogs, mdiStore, mdiViewDashboard } from "@mdi/js";
|
||||
import {
|
||||
mdiBackupRestore,
|
||||
mdiCogs,
|
||||
mdiPuzzle,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
|
||||
export const supervisorTabs: PageNavigation[] = [
|
||||
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [
|
||||
{
|
||||
translationKey: "panel.dashboard",
|
||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
iconPath: mdiViewDashboard,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.store",
|
||||
path: `/hassio/store`,
|
||||
iconPath: mdiStore,
|
||||
iconPath: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? mdiPuzzle
|
||||
: mdiViewDashboard,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
|
@@ -25,7 +25,7 @@ import {
|
||||
} from "../../src/data/supervisor/supervisor";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import { getTranslation } from "../../src/util/common-translation";
|
||||
|
||||
declare global {
|
||||
@@ -38,6 +38,8 @@ declare global {
|
||||
export class SupervisorBaseElement extends urlSyncMixin(
|
||||
ProvideHassLitMixin(LitElement)
|
||||
) {
|
||||
@property({ attribute: false }) public route?: Route;
|
||||
|
||||
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
|
||||
localize: () => "",
|
||||
};
|
||||
@@ -108,7 +110,9 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
||||
this._language = this.hass.language;
|
||||
}
|
||||
this._initializeLocalize();
|
||||
this._initSupervisor();
|
||||
if (this.route?.prefix === "/hassio") {
|
||||
this._initSupervisor();
|
||||
}
|
||||
}
|
||||
|
||||
private async _initializeLocalize() {
|
||||
|
@@ -2,7 +2,7 @@ import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
fetchHassioStats,
|
||||
HassioStats,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { restartCore, updateCore } from "../../../src/data/supervisor/core";
|
||||
import { restartCore } from "../../../src/data/supervisor/core";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -22,7 +22,6 @@ import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
import "../components/supervisor-metric";
|
||||
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-core-info")
|
||||
@@ -67,14 +66,15 @@ class HassioCoreInfo extends LitElement {
|
||||
<span slot="description">
|
||||
core-${this.supervisor.core.version_latest}
|
||||
</span>
|
||||
${this.supervisor.core.update_available
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
|
||||
this.supervisor.core.update_available
|
||||
? html`
|
||||
<ha-progress-button
|
||||
.title=${this.supervisor.localize("common.update")}
|
||||
@click=${this._coreUpdate}
|
||||
>
|
||||
${this.supervisor.localize("common.update")}
|
||||
</ha-progress-button>
|
||||
<a href="/hassio/update-available/core">
|
||||
<mwc-button
|
||||
.label=${this.supervisor.localize("common.show")}
|
||||
>
|
||||
</mwc-button>
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
@@ -160,27 +160,6 @@ class HassioCoreInfo extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _coreUpdate(): Promise<void> {
|
||||
showDialogSupervisorUpdate(this, {
|
||||
supervisor: this.supervisor,
|
||||
name: "Home Assistant Core",
|
||||
version: this.supervisor.core.version_latest,
|
||||
backupParams: {
|
||||
name: `core_${this.supervisor.core.version}`,
|
||||
folders: ["homeassistant"],
|
||||
homeassistant: true,
|
||||
},
|
||||
updateHandler: async () => this._updateCore(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateCore(): Promise<void> {
|
||||
await updateCore(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "core",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -239,6 +218,9 @@ class HassioCoreInfo extends LitElement {
|
||||
mwc-list-item ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -21,7 +21,6 @@ import {
|
||||
configSyncOS,
|
||||
rebootHost,
|
||||
shutdownHost,
|
||||
updateOS,
|
||||
} from "../../../src/data/hassio/host";
|
||||
import {
|
||||
fetchNetworkInfo,
|
||||
@@ -106,11 +105,15 @@ class HassioHostInfo extends LitElement {
|
||||
<span slot="description">
|
||||
${this.supervisor.host.operating_system}
|
||||
</span>
|
||||
${this.supervisor.os.update_available
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
|
||||
this.supervisor.os.update_available
|
||||
? html`
|
||||
<ha-progress-button @click=${this._osUpdate}>
|
||||
${this.supervisor.localize("commmon.update")}
|
||||
</ha-progress-button>
|
||||
<a href="/hassio/update-available/os">
|
||||
<mwc-button
|
||||
.label=${this.supervisor.localize("common.show")}
|
||||
>
|
||||
</mwc-button>
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
@@ -333,50 +336,6 @@ class HassioHostInfo extends LitElement {
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _osUpdate(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"confirm.update.title",
|
||||
"name",
|
||||
"Home Assistant Operating System"
|
||||
),
|
||||
text: this.supervisor.localize(
|
||||
"confirm.update.text",
|
||||
"name",
|
||||
"Home Assistant Operating System",
|
||||
"version",
|
||||
this.supervisor.os.version_latest
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.update"),
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateOS(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "os" });
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"common.failed_to_update_name",
|
||||
"name",
|
||||
"Home Assistant Operating System"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _changeNetworkClicked(): Promise<void> {
|
||||
showNetworkDialog(this, {
|
||||
supervisor: this.supervisor,
|
||||
@@ -494,6 +453,9 @@ class HassioHostInfo extends LitElement {
|
||||
mwc-list-item ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@ import {
|
||||
restartSupervisor,
|
||||
setSupervisorOption,
|
||||
SupervisorOptions,
|
||||
updateSupervisor,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
@@ -77,16 +76,15 @@ class HassioSupervisorInfo extends LitElement {
|
||||
<span slot="description">
|
||||
supervisor-${this.supervisor.supervisor.version_latest}
|
||||
</span>
|
||||
${this.supervisor.supervisor.update_available
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
|
||||
this.supervisor.supervisor.update_available
|
||||
? html`
|
||||
<ha-progress-button
|
||||
.title=${this.supervisor.localize(
|
||||
"system.supervisor.update_supervisor"
|
||||
)}
|
||||
@click=${this._supervisorUpdate}
|
||||
>
|
||||
${this.supervisor.localize("common.update")}
|
||||
</ha-progress-button>
|
||||
<a href="/hassio/update-available/supervisor">
|
||||
<mwc-button
|
||||
.label=${this.supervisor.localize("common.show")}
|
||||
>
|
||||
</mwc-button>
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
@@ -153,24 +151,28 @@ class HassioSupervisorInfo extends LitElement {
|
||||
></ha-switch>
|
||||
</ha-settings-row>`
|
||||
: ""
|
||||
: html`<ha-alert
|
||||
alert-type="warning"
|
||||
.actionText=${this.supervisor.localize("common.learn_more")}
|
||||
@alert-action-clicked=${this._unsupportedDialog}
|
||||
>
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.unsupported_title"
|
||||
)}
|
||||
<mwc-button
|
||||
slot="action"
|
||||
.label=${this.supervisor.localize("common.learn_more")}
|
||||
@click=${this._unsupportedDialog}
|
||||
>
|
||||
</mwc-button>
|
||||
</ha-alert>`}
|
||||
${!this.supervisor.supervisor.healthy
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.actionText=${this.supervisor.localize("common.learn_more")}
|
||||
@alert-action-clicked=${this._unhealthyDialog}
|
||||
>
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.unhealthy_title"
|
||||
)}
|
||||
<mwc-button
|
||||
slot="action"
|
||||
.label=${this.supervisor.localize("common.learn_more")}
|
||||
@click=${this._unhealthyDialog}
|
||||
>
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
</div>
|
||||
@@ -337,51 +339,6 @@ class HassioSupervisorInfo extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"confirm.update.title",
|
||||
"name",
|
||||
"Supervisor"
|
||||
),
|
||||
text: this.supervisor.localize(
|
||||
"confirm.update.text",
|
||||
"name",
|
||||
"Supervisor",
|
||||
"version",
|
||||
this.supervisor.supervisor.version_latest
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.update"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateSupervisor(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "supervisor",
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"common.failed_to_update_name",
|
||||
"name",
|
||||
"Supervisor"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _diagnosticsInformationDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
@@ -513,6 +470,12 @@ class HassioSupervisorInfo extends LitElement {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-alert mwc-button {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ class HassioSystem extends LitElement {
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
main-page
|
||||
supervisor
|
||||
>
|
||||
|
401
hassio/src/update-available/update-available-card.ts
Normal file
401
hassio/src/update-available/update-available-card.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/common/search/search-input";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-expansion-panel";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-markdown";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import "../../../src/components/ha-switch";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
updateHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
createHassioPartialBackup,
|
||||
HassioPartialBackupCreateParams,
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { updateOS } from "../../../src/data/hassio/host";
|
||||
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
|
||||
import { updateCore } from "../../../src/data/supervisor/core";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"update-complete": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
type updateType = "os" | "supervisor" | "core" | "addon";
|
||||
|
||||
const changelogUrl = (
|
||||
hass: HomeAssistant,
|
||||
entry: updateType,
|
||||
version: string
|
||||
): string | undefined => {
|
||||
if (entry === "addon") {
|
||||
return undefined;
|
||||
}
|
||||
if (entry === "core") {
|
||||
return version?.includes("dev")
|
||||
? "https://github.com/home-assistant/core/commits/dev"
|
||||
: documentationUrl(hass, "/latest-release-notes/");
|
||||
}
|
||||
if (entry === "os") {
|
||||
return version?.includes("dev")
|
||||
? "https://github.com/home-assistant/operating-system/commits/dev"
|
||||
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
|
||||
}
|
||||
if (entry === "supervisor") {
|
||||
return version?.includes("dev")
|
||||
? "https://github.com/home-assistant/supervisor/commits/main"
|
||||
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@customElement("update-available-card")
|
||||
class UpdateAvailableCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public addonSlug?: string;
|
||||
|
||||
@state() private _updateType?: updateType;
|
||||
|
||||
@state() private _changelogContent?: string;
|
||||
|
||||
@state() private _addonInfo?: HassioAddonDetails;
|
||||
|
||||
@state() private _action: "backup" | "update" | null = null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _addonStoreInfo = memoizeOne(
|
||||
(slug: string, storeAddons: StoreAddon[]) =>
|
||||
storeAddons.find((addon) => addon.slug === slug)
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this._updateType ||
|
||||
(this._updateType === "addon" && !this._addonInfo)
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const changelog = changelogUrl(this.hass, this._updateType, this._version);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.supervisor.localize("update_available.update_name", {
|
||||
name: this._name,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this._action === null
|
||||
? html`
|
||||
${this._changelogContent
|
||||
? html`
|
||||
<ha-expansion-panel header="Changelog" outlined>
|
||||
<ha-markdown .content=${this._changelogContent}>
|
||||
</ha-markdown>
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: ""}
|
||||
<div class="versions">
|
||||
<p>
|
||||
${this.supervisor.localize("update_available.description", {
|
||||
name: this._name,
|
||||
version: this._version,
|
||||
newest_version: this._version_latest,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
${["core", "addon"].includes(this._updateType)
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"update_available.create_backup"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox checked></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`<ha-circular-progress alt="Updating" size="large" active>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this._action === "update"
|
||||
? this.supervisor.localize("update_available.updating", {
|
||||
name: this._name,
|
||||
version: this._version_latest,
|
||||
})
|
||||
: this.supervisor.localize(
|
||||
"update_available.creating_backup",
|
||||
{ name: this._name }
|
||||
)}
|
||||
</p>`}
|
||||
</div>
|
||||
${this._action === null
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${changelog
|
||||
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
|
||||
<mwc-button
|
||||
.label=${this.supervisor.localize(
|
||||
"update_available.open_release_notes"
|
||||
)}
|
||||
>
|
||||
</mwc-button>
|
||||
</a>`
|
||||
: ""}
|
||||
<span></span>
|
||||
<ha-progress-button
|
||||
.disabled=${!this._version ||
|
||||
(this._shouldCreateBackup &&
|
||||
this.supervisor.info.state !== "running")}
|
||||
@click=${this._update}
|
||||
raised
|
||||
>
|
||||
${this.supervisor.localize("common.update")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
const pathPart = this.route?.path.substring(1, this.route.path.length);
|
||||
const updateType = ["core", "os", "supervisor"].includes(pathPart)
|
||||
? pathPart
|
||||
: "addon";
|
||||
this._updateType = updateType as updateType;
|
||||
|
||||
if (updateType === "addon") {
|
||||
if (!this.addonSlug) {
|
||||
this.addonSlug = pathPart;
|
||||
}
|
||||
this._loadAddonData();
|
||||
}
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true;
|
||||
}
|
||||
|
||||
get _version(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.version
|
||||
: this.supervisor[this._updateType]?.version || ""
|
||||
: "";
|
||||
}
|
||||
|
||||
get _version_latest(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.version_latest
|
||||
: this.supervisor[this._updateType]?.version_latest || ""
|
||||
: "";
|
||||
}
|
||||
|
||||
get _name(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.name
|
||||
: SUPERVISOR_UPDATE_NAMES[this._updateType]
|
||||
: "";
|
||||
}
|
||||
|
||||
private async _loadAddonData() {
|
||||
try {
|
||||
this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const addonStoreInfo =
|
||||
!this._addonInfo.detached && !this._addonInfo.available
|
||||
? this._addonStoreInfo(
|
||||
this._addonInfo.slug,
|
||||
this.supervisor.store.addons
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (this._addonInfo.changelog) {
|
||||
try {
|
||||
const content = await fetchHassioAddonChangelog(
|
||||
this.hass,
|
||||
this.addonSlug!
|
||||
);
|
||||
this._changelogContent = extractChangelog(this._addonInfo, content);
|
||||
} catch (err) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._addonInfo.available && addonStoreInfo) {
|
||||
if (
|
||||
!addonArchIsSupported(
|
||||
this.supervisor.info.supported_arch,
|
||||
this._addonInfo.arch
|
||||
)
|
||||
) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
);
|
||||
} else {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo.homeassistant,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
this._error = undefined;
|
||||
if (this._shouldCreateBackup) {
|
||||
let backupArgs: HassioPartialBackupCreateParams;
|
||||
if (this._updateType === "addon") {
|
||||
backupArgs = {
|
||||
name: `addon_${this.addonSlug}_${this._version}`,
|
||||
addons: [this.addonSlug!],
|
||||
homeassistant: false,
|
||||
};
|
||||
} else {
|
||||
backupArgs = {
|
||||
name: `${this._updateType}_${this._version}`,
|
||||
folders: ["homeassistant"],
|
||||
homeassistant: true,
|
||||
};
|
||||
}
|
||||
this._action = "backup";
|
||||
try {
|
||||
await createHassioPartialBackup(this.hass, backupArgs);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._action = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._action = "update";
|
||||
try {
|
||||
if (this._updateType === "addon") {
|
||||
await updateHassioAddon(this.hass, this.addonSlug!);
|
||||
} else if (this._updateType === "core") {
|
||||
await updateCore(this.hass);
|
||||
} else if (this._updateType === "os") {
|
||||
await updateOS(this.hass);
|
||||
} else if (this._updateType === "supervisor") {
|
||||
await updateSupervisor(this.hass);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._action = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "update-complete");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
margin: auto;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: none;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ha-markdown {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
ha-formfield {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"update-available-card": UpdateAvailableCard;
|
||||
}
|
||||
}
|
59
hassio/src/update-available/update-available-dashboard.ts
Normal file
59
hassio/src/update-available/update-available-dashboard.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import "./update-available-card";
|
||||
|
||||
@customElement("update-available-dashboard")
|
||||
class UpdateAvailableDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
>
|
||||
<update-available-card
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.route=${this.route}
|
||||
.narrow=${this.narrow}
|
||||
@update-complete=${this._updateComplete}
|
||||
></update-available-card>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _updateComplete() {
|
||||
history.back();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
hass-subpage {
|
||||
--app-header-background-color: var(--primary-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
update-available-card {
|
||||
margin: auto;
|
||||
margin-top: 16px;
|
||||
max-width: 600px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"update-available-dashboard": UpdateAvailableDashboard;
|
||||
}
|
||||
}
|
@@ -1,7 +1,30 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import { SupervisorArch } from "../../../src/data/supervisor/supervisor";
|
||||
|
||||
export const addonArchIsSupported = memoizeOne(
|
||||
(supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) =>
|
||||
addon_archs.some((arch) => supported_archs.includes(arch))
|
||||
);
|
||||
|
||||
export const extractChangelog = (
|
||||
addon: HassioAddonDetails,
|
||||
content: string
|
||||
): string => {
|
||||
if (content.startsWith("# Changelog")) {
|
||||
content = content.substr(12, content.length);
|
||||
}
|
||||
if (
|
||||
content.includes(`# ${addon.version}`) &&
|
||||
content.includes(`# ${addon.version_latest}`)
|
||||
) {
|
||||
const newcontent = content.split(`# ${addon.version}`)[0];
|
||||
if (newcontent.includes(`# ${addon.version_latest}`)) {
|
||||
// Only change the content if the new version still exist
|
||||
// if the changelog does not have the newests version on top
|
||||
// this will not be true, and we don't modify the content
|
||||
content = newcontent;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
12
package.json
12
package.json
@@ -23,16 +23,16 @@
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^5.0.2",
|
||||
"@codemirror/commands": "^0.19.5",
|
||||
"@codemirror/gutter": "^0.19.3",
|
||||
"@codemirror/gutter": "^0.19.4",
|
||||
"@codemirror/highlight": "^0.19.6",
|
||||
"@codemirror/history": "^0.19.0",
|
||||
"@codemirror/legacy-modes": "^0.19.0",
|
||||
"@codemirror/rectangular-selection": "^0.19.1",
|
||||
"@codemirror/search": "^0.19.2",
|
||||
"@codemirror/state": "^0.19.2",
|
||||
"@codemirror/state": "^0.19.4",
|
||||
"@codemirror/stream-parser": "^0.19.2",
|
||||
"@codemirror/text": "^0.19.4",
|
||||
"@codemirror/view": "^0.19.9",
|
||||
"@codemirror/text": "^0.19.5",
|
||||
"@codemirror/view": "^0.19.15",
|
||||
"@formatjs/intl-datetimeformat": "^4.2.5",
|
||||
"@formatjs/intl-getcanonicallocales": "^1.8.0",
|
||||
"@formatjs/intl-locale": "^2.4.40",
|
||||
@@ -67,8 +67,8 @@
|
||||
"@material/mwc-tab-bar": "0.25.3",
|
||||
"@material/mwc-textfield": "0.25.3",
|
||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||
"@mdi/js": "6.4.95",
|
||||
"@mdi/svg": "6.4.95",
|
||||
"@mdi/js": "6.5.95",
|
||||
"@mdi/svg": "6.5.95",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20211108.0",
|
||||
version="20211123.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/frontend",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -3,5 +3,5 @@ import { CAST_DEV_APP_ID } from "./dev_const";
|
||||
// Guard dev mode with `__dev__` so it can only ever be enabled in dev mode.
|
||||
export const CAST_DEV = __DEV__ && true;
|
||||
|
||||
export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "B12CE3CA";
|
||||
export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "A078F6B0";
|
||||
export const CAST_NS = "urn:x-cast:com.nabucasa.hast";
|
||||
|
@@ -11,4 +11,20 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
|
||||
urlPath?: string | null;
|
||||
}
|
||||
|
||||
export interface ReceiverErrorMessage extends BaseCastMessage {
|
||||
type: "receiver_error";
|
||||
error_code: ReceiverErrorCode;
|
||||
error_message: string;
|
||||
}
|
||||
|
||||
export const enum ReceiverErrorCode {
|
||||
CONNECTION_FAILED = 1,
|
||||
AUTHENTICATION_FAILED = 2,
|
||||
CONNECTION_LOST = 3,
|
||||
HASS_URL_MISSING = 4,
|
||||
NO_HTTPS = 5,
|
||||
NOT_CONNECTED = 21,
|
||||
FETCH_CONFIG_FAILED = 22,
|
||||
}
|
||||
|
||||
export type SenderMessage = ReceiverStatusMessage;
|
||||
|
@@ -7,7 +7,13 @@ export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
!hideAdvancedPage(hass, page);
|
||||
|
||||
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
!page.component || isComponentLoaded(hass, page.component);
|
||||
page.component
|
||||
? isComponentLoaded(hass, page.component)
|
||||
: page.components
|
||||
? page.components.some((integration) =>
|
||||
isComponentLoaded(hass, integration)
|
||||
)
|
||||
: true;
|
||||
const isCore = (page: PageNavigation) => page.core;
|
||||
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
|
||||
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;
|
||||
|
@@ -119,6 +119,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
current: mdiCurrentAc,
|
||||
date: mdiCalendar,
|
||||
energy: mdiLightningBolt,
|
||||
frequency: mdiSineWave,
|
||||
gas: mdiGasCylinder,
|
||||
humidity: mdiWaterPercent,
|
||||
illuminance: mdiBrightness5,
|
||||
|
@@ -678,7 +678,7 @@ export class HaDataTable extends LitElement {
|
||||
padding-left: 16px;
|
||||
/* @noflip */
|
||||
padding-right: 0;
|
||||
width: 56px;
|
||||
width: 60px;
|
||||
}
|
||||
:host([dir="rtl"]) .mdc-data-table__header-cell--checkbox,
|
||||
:host([dir="rtl"]) .mdc-data-table__cell--checkbox {
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiAlertOutline,
|
||||
@@ -23,7 +22,6 @@ const ALERT_ICONS = {
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"alert-dismissed-clicked": undefined;
|
||||
"alert-action-clicked": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +35,6 @@ class HaAlert extends LitElement {
|
||||
| "error"
|
||||
| "success" = "info";
|
||||
|
||||
@property({ attribute: "action-text" }) public actionText = "";
|
||||
|
||||
@property({ type: Boolean }) public dismissable = false;
|
||||
|
||||
@property({ type: Boolean }) public rtl = false;
|
||||
@@ -52,7 +48,9 @@ class HaAlert extends LitElement {
|
||||
})}"
|
||||
>
|
||||
<div class="icon ${this.title ? "" : "no-title"}">
|
||||
<ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon>
|
||||
<slot name="icon">
|
||||
<ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="main-content">
|
||||
@@ -60,18 +58,15 @@ class HaAlert extends LitElement {
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="action">
|
||||
${this.actionText
|
||||
? html`<mwc-button
|
||||
@click=${this._action_clicked}
|
||||
.label=${this.actionText}
|
||||
></mwc-button>`
|
||||
: this.dismissable
|
||||
? html`<ha-icon-button
|
||||
@click=${this._dismiss_clicked}
|
||||
label="Dismiss alert"
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: ""}
|
||||
<slot name="action">
|
||||
${this.dismissable
|
||||
? html`<ha-icon-button
|
||||
@click=${this._dismiss_clicked}
|
||||
label="Dismiss alert"
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: ""}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,10 +77,6 @@ class HaAlert extends LitElement {
|
||||
fireEvent(this, "alert-dismissed-clicked");
|
||||
}
|
||||
|
||||
private _action_clicked() {
|
||||
fireEvent(this, "alert-action-clicked");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.issue-type {
|
||||
position: relative;
|
||||
@@ -96,7 +87,7 @@ class HaAlert extends LitElement {
|
||||
.issue-type.rtl {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.issue-type::before {
|
||||
.issue-type::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@@ -108,17 +99,11 @@ class HaAlert extends LitElement {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.icon {
|
||||
margin-right: 8px;
|
||||
width: 24px;
|
||||
z-index: 1;
|
||||
}
|
||||
.icon.no-title {
|
||||
align-self: center;
|
||||
}
|
||||
.issue-type.rtl > .icon {
|
||||
margin-right: 0px;
|
||||
margin-left: 8px;
|
||||
width: 24px;
|
||||
}
|
||||
.issue-type.rtl > .content {
|
||||
flex-direction: row-reverse;
|
||||
text-align: right;
|
||||
@@ -129,44 +114,54 @@ class HaAlert extends LitElement {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.action {
|
||||
z-index: 1;
|
||||
width: min-content;
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
}
|
||||
.main-content {
|
||||
overflow-wrap: anywhere;
|
||||
margin-left: 8px;
|
||||
margin-right: 0;
|
||||
}
|
||||
.issue-type.rtl > .content > .main-content {
|
||||
margin-left: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.title {
|
||||
margin-top: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
mwc-button {
|
||||
.action mwc-button,
|
||||
.action ha-icon-button {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
.issue-type.info > .icon {
|
||||
color: var(--info-color);
|
||||
}
|
||||
.issue-type.info::before {
|
||||
.issue-type.info::after {
|
||||
background-color: var(--info-color);
|
||||
}
|
||||
|
||||
.issue-type.warning > .icon {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.issue-type.warning::before {
|
||||
.issue-type.warning::after {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.issue-type.error > .icon {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.issue-type.error::before {
|
||||
.issue-type.error::after {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
||||
.issue-type.success > .icon {
|
||||
color: var(--success-color);
|
||||
}
|
||||
.issue-type.success::before {
|
||||
.issue-type.success::after {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
`;
|
||||
|
@@ -172,6 +172,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
{
|
||||
area_id: "",
|
||||
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||
picture: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -295,6 +296,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
{
|
||||
area_id: "",
|
||||
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||
picture: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -306,6 +308,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
{
|
||||
area_id: "add_new",
|
||||
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||
picture: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -340,7 +343,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
item-value-path="area_id"
|
||||
item-id-path="area_id"
|
||||
item-label-path="name"
|
||||
.value=${this._value}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
${comboBoxRenderer(rowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@@ -431,12 +434,24 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
name,
|
||||
});
|
||||
this._areas = [...this._areas!, area];
|
||||
(this.comboBox as any).items = this._getAreas(
|
||||
this._areas!,
|
||||
this._devices!,
|
||||
this._entities!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd
|
||||
);
|
||||
this._setValue(area.area_id);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
title: this.hass.localize(
|
||||
"ui.components.area-picker.add_dialog.failed_create_area"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@@ -8,52 +8,31 @@ import {
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-chip";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"chip-clicked": { index: string };
|
||||
}
|
||||
}
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-chip-set")
|
||||
export class HaChipSet extends LitElement {
|
||||
@property() public items = [];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this.items.length === 0) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<div class="mdc-chip-set">
|
||||
${this.items.map(
|
||||
(item, idx) =>
|
||||
html`
|
||||
<ha-chip .index=${idx} @click=${this._handleClick}>
|
||||
${item}
|
||||
</ha-chip>
|
||||
`
|
||||
)}
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(ev): void {
|
||||
fireEvent(this, "chip-clicked", {
|
||||
index: ev.currentTarget.index,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
${unsafeCSS(chipStyles)}
|
||||
|
||||
ha-chip {
|
||||
slot::slotted(ha-chip) {
|
||||
margin: 4px;
|
||||
}
|
||||
slot::slotted(ha-chip:first-of-type) {
|
||||
margin-left: -4px;
|
||||
}
|
||||
slot::slotted(ha-chip:last-of-type) {
|
||||
margin-right: -4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -10,22 +10,13 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"chip-clicked": { index: string };
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-chip")
|
||||
export class HaChip extends LitElement {
|
||||
@property() public index = 0;
|
||||
|
||||
@property({ type: Boolean }) public hasIcon = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mdc-chip" .index=${this.index}>
|
||||
<div class="mdc-chip">
|
||||
${this.hasIcon
|
||||
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
|
||||
<slot name="icon"></slot>
|
||||
|
@@ -72,6 +72,10 @@ export class HaDialog extends Dialog {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
top: var(--dialog-surface-top);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-card-border-radius, 4px)
|
||||
);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
|
@@ -52,7 +52,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const options = Object.entries(this.schema.options);
|
||||
const options = Array.isArray(this.schema.options)
|
||||
? this.schema.options
|
||||
: Object.entries(this.schema.options);
|
||||
const data = this.data || [];
|
||||
|
||||
const renderedOptions = options.map((item: string | [string, string]) => {
|
||||
|
@@ -38,7 +38,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema {
|
||||
|
||||
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
|
||||
type: "multi_select";
|
||||
options: Record<string, string>;
|
||||
options: Record<string, string> | string[];
|
||||
}
|
||||
|
||||
export interface HaFormFloatSchema extends HaFormBaseSchema {
|
||||
|
@@ -29,315 +29,7 @@ interface DeprecatedIcon {
|
||||
};
|
||||
}
|
||||
|
||||
const mdiDeprecatedIcons: DeprecatedIcon = {
|
||||
"adobe-acrobat": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
adobe: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"amazon-alexa": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
amazon: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"android-auto": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"android-debug-bridge": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"apple-airplay": {
|
||||
newName: "cast-variant",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
bandcamp: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
battlenet: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
blogger: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"bolnisi-cross": {
|
||||
newName: "cross-bolnisi",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"boom-gate-down": {
|
||||
newName: "boom-gate-arrow-down",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"boom-gate-down-outline": {
|
||||
newName: "boom-gate-arrow-down-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
buddhism: {
|
||||
newName: "dharmachakra",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
buffer: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"cash-usd-outline": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"cash-usd": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"cellphone-android": {
|
||||
newName: "cellphone",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"cellphone-erase": {
|
||||
newName: "cellphone-remove",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"cellphone-iphone": {
|
||||
newName: "cellphone",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"celtic-cross": {
|
||||
newName: "cross-celtic",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
christianity: {
|
||||
newName: "cross",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"christianity-outline": {
|
||||
newName: "cross-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"concourse-ci": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"currency-usd-circle": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"currency-usd-circle-outline": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"do-not-disturb-off": {
|
||||
newName: "minus-circle-off",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"do-not-disturb": {
|
||||
newName: "minus-circle",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
douban: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
face: {
|
||||
newName: "face-man",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"face-outline": {
|
||||
newName: "face-man-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"face-profile-woman": {
|
||||
newName: "face-woman-profile",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"face-shimmer": {
|
||||
newName: "face-man-shimmer",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"face-shimmer-outline": {
|
||||
newName: "face-man-shimmer-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"file-pdf": {
|
||||
newName: "file-pdf-box",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"file-pdf-outline": {
|
||||
newName: "file-pdf-box",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"file-pdf-box-outline": {
|
||||
newName: "file-pdf-box",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"flash-circle": {
|
||||
newName: "lightning-bolt-circle",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"floor-lamp-variant": {
|
||||
newName: "floor-lamp-torchiere-variant",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
gif: {
|
||||
newName: "file-gif-box",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"google-photos": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
gradient: {
|
||||
newName: "gradient-vertical",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
hand: {
|
||||
newName: "hand-front-right",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"hand-left": {
|
||||
newName: "hand-back-left",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"hand-right": {
|
||||
newName: "hand-back-right",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
hinduism: {
|
||||
newName: "om",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"home-currency-usd": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
iframe: {
|
||||
newName: "application-brackets",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"iframe-outline": {
|
||||
newName: "application-brackets-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"iframe-array": {
|
||||
newName: "application-array",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"iframe-array-outline": {
|
||||
newName: "application-array-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"iframe-braces": {
|
||||
newName: "application-braces",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"iframe-braces-outline": {
|
||||
newName: "application-braces-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"iframe-parentheses": {
|
||||
newName: "application-parentheses",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"iframe-parentheses-outline": {
|
||||
newName: "application-parentheses-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"iframe-variable": {
|
||||
newName: "application-variable",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"iframe-variable-outline": {
|
||||
newName: "application-variable-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
islam: {
|
||||
newName: "star-crescent",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
judaism: {
|
||||
newName: "star-david",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"laptop-chromebook": {
|
||||
newName: "laptop",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"laptop-mac": {
|
||||
newName: "laptop",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"laptop-windows": {
|
||||
newName: "laptop",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"microsoft-edge-legacy": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"microsoft-yammer": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"monitor-clean": {
|
||||
newName: "monitor-shimmer",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"pdf-box": {
|
||||
newName: "file-pdf-box",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
pharmacy: {
|
||||
newName: "mortar-pestle-plus",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"plus-one": {
|
||||
newName: "numeric-positive-1",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"poll-box": {
|
||||
newName: "chart-box",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"poll-box-outline": {
|
||||
newName: "chart-box-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
sparkles: {
|
||||
newName: "shimmer",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"tablet-ipad": {
|
||||
newName: "tablet",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
teach: {
|
||||
newName: "human-male-board",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
telegram: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"television-clean": {
|
||||
newName: "television-shimmer",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"text-subject": {
|
||||
newName: "text-long",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"twitter-retweet": {
|
||||
newName: "repeat-variant",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
untappd: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
vk: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"voice-off": {
|
||||
newName: "account-voice-off",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"xamarian-outline": {
|
||||
newName: "xamarian",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
xing: {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"y-combinator": {
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
};
|
||||
const mdiDeprecatedIcons: DeprecatedIcon = {};
|
||||
|
||||
const chunks: Chunks = {};
|
||||
|
||||
|
39
src/components/ha-logo-svg.ts
Normal file
39
src/components/ha-logo-svg.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -51,7 +51,7 @@ import {
|
||||
} from "../external_app/external_config";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-menu-button";
|
||||
@@ -189,6 +189,8 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public alwaysExpand = false;
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
@@ -351,12 +353,19 @@ class HaSidebar extends LitElement {
|
||||
this._hiddenPanels
|
||||
);
|
||||
|
||||
// Show the update-available as beeing part of configuration
|
||||
const selectedPanel = this.route.path?.startsWith(
|
||||
"/hassio/update-available"
|
||||
)
|
||||
? "config"
|
||||
: this.hass.panelUrl;
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<paper-listbox
|
||||
attr-for-selected="data-panel"
|
||||
class="ha-scrollbar"
|
||||
.selected=${this.hass.panelUrl}
|
||||
.selected=${selectedPanel}
|
||||
@focusin=${this._listboxFocusIn}
|
||||
@focusout=${this._listboxFocusOut}
|
||||
@scroll=${this._listboxScroll}
|
||||
|
@@ -80,6 +80,9 @@ class HaWebRtcPlayer extends LitElement {
|
||||
// Some cameras (such as nest) require a data channel to establish a stream
|
||||
// however, not used by any integrations.
|
||||
peerConnection.createDataChannel("dataSendChannel");
|
||||
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
||||
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
||||
|
||||
const offerOptions: RTCOfferOptions = {
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: true,
|
||||
|
@@ -26,7 +26,9 @@ class HaEntityMarker extends LitElement {
|
||||
? html`<div
|
||||
class="entity-picture"
|
||||
style=${styleMap({
|
||||
"background-image": `url(${this.entityPicture})`,
|
||||
"background-image": `url(${this.hass.hassUrl(
|
||||
this.entityPicture
|
||||
)})`,
|
||||
})}
|
||||
></div>`
|
||||
: this.entityName}
|
||||
|
@@ -26,6 +26,7 @@ declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"location-updated": { id: string; location: [number, number] };
|
||||
"markers-updated": undefined;
|
||||
"radius-updated": { id: string; radius: number };
|
||||
"marker-clicked": { id: string };
|
||||
}
|
||||
@@ -281,6 +282,7 @@ export class HaLocationsEditor extends LitElement {
|
||||
});
|
||||
this._circles = circles;
|
||||
this._locationMarkers = locationMarkers;
|
||||
fireEvent(this, "markers-updated");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -7,10 +7,12 @@ import { HomeAssistant } from "../types";
|
||||
export interface AreaRegistryEntry {
|
||||
area_id: string;
|
||||
name: string;
|
||||
picture: string | null;
|
||||
}
|
||||
|
||||
export interface AreaRegistryEntryMutableParams {
|
||||
name: string;
|
||||
picture?: string | null;
|
||||
}
|
||||
|
||||
export const createAreaRegistryEntry = (
|
||||
|
@@ -66,7 +66,7 @@ export const computeEntityRegistryName = (
|
||||
return entry.name;
|
||||
}
|
||||
const state = hass.states[entry.entity_id];
|
||||
return state ? computeStateName(state) : null;
|
||||
return state ? computeStateName(state) : entry.entity_id;
|
||||
};
|
||||
|
||||
export const getExtendedEntityRegistryEntry = (
|
||||
|
@@ -70,6 +70,42 @@ export interface Supervisor {
|
||||
localize: LocalizeFunc;
|
||||
}
|
||||
|
||||
interface SupervisorBaseAvailableUpdates {
|
||||
panel_path?: string;
|
||||
update_type?: string;
|
||||
version_latest?: string;
|
||||
}
|
||||
|
||||
interface SupervisorAddonAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "addon";
|
||||
icon?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface SupervisorCoreAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "core";
|
||||
}
|
||||
|
||||
interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "os";
|
||||
}
|
||||
|
||||
interface SupervisorSupervisorAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "supervisor";
|
||||
}
|
||||
|
||||
export type SupervisorAvailableUpdates =
|
||||
| SupervisorAddonAvailableUpdates
|
||||
| SupervisorCoreAvailableUpdates
|
||||
| SupervisorOsAvailableUpdates
|
||||
| SupervisorSupervisorAvailableUpdates;
|
||||
|
||||
export interface SupervisorAvailableUpdatesResponse {
|
||||
available_updates: SupervisorAvailableUpdates[];
|
||||
}
|
||||
export const supervisorApiWsRequest = <T>(
|
||||
conn: Connection,
|
||||
request: supervisorApiRequest
|
||||
@@ -139,3 +175,14 @@ export const subscribeSupervisorEvents = (
|
||||
getSupervisorEventCollection(hass.connection, key, endpoint).subscribe(
|
||||
onChange
|
||||
);
|
||||
|
||||
export const fetchSupervisorAvailableUpdates = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<SupervisorAvailableUpdates[]> =>
|
||||
(
|
||||
await hass.callWS<SupervisorAvailableUpdatesResponse>({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/supervisor/available_updates",
|
||||
method: "get",
|
||||
})
|
||||
).available_updates;
|
||||
|
@@ -34,7 +34,7 @@ export interface ZHADevice {
|
||||
export interface Neighbor {
|
||||
ieee: string;
|
||||
nwk: string;
|
||||
lqi: number;
|
||||
lqi: string;
|
||||
}
|
||||
|
||||
export interface ZHADeviceEndpoint {
|
||||
|
@@ -39,6 +39,7 @@ export class HaImagecropperDialog extends LitElement {
|
||||
this._open = false;
|
||||
this._params = undefined;
|
||||
this._cropper?.destroy();
|
||||
this._cropper = undefined;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
|
@@ -4,7 +4,7 @@ export interface CropOptions {
|
||||
round: boolean;
|
||||
type?: "image/jpeg" | "image/png";
|
||||
quality?: number;
|
||||
aspectRatio: number;
|
||||
aspectRatio?: number;
|
||||
}
|
||||
|
||||
export interface HaImageCropperDialogParams {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { startOfYesterday } from "date-fns";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
@@ -22,6 +23,8 @@ export class MoreInfoHistory extends LitElement {
|
||||
|
||||
@state() private _stateHistory?: HistoryResult;
|
||||
|
||||
private _showMoreHref = "";
|
||||
|
||||
private _throttleGetStateHistory = throttle(() => {
|
||||
this._getStateHistory();
|
||||
}, 10000);
|
||||
@@ -31,14 +34,12 @@ export class MoreInfoHistory extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const href = "/history?entity_id=" + this.entityId;
|
||||
|
||||
return html`${isComponentLoaded(this.hass, "history")
|
||||
? html` <div class="header">
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.history")}
|
||||
</div>
|
||||
<a href=${href} @click=${this._close}
|
||||
<a href=${this._showMoreHref} @click=${this._close}
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.show_more"
|
||||
)}</a
|
||||
@@ -63,6 +64,10 @@ export class MoreInfoHistory extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showMoreHref = `/history?entity_id=${
|
||||
this.entityId
|
||||
}&start_date=${startOfYesterday().toISOString()}`;
|
||||
|
||||
this._throttleGetStateHistory();
|
||||
return;
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { startOfYesterday } from "date-fns";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
@@ -30,6 +31,8 @@ export class MoreInfoLogbook extends LitElement {
|
||||
|
||||
private _error?: string;
|
||||
|
||||
private _showMoreHref = "";
|
||||
|
||||
private _throttleGetLogbookEntries = throttle(() => {
|
||||
this._getLogBookData();
|
||||
}, 10000);
|
||||
@@ -44,8 +47,6 @@ export class MoreInfoLogbook extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const href = "/logbook?entity_id=" + this.entityId;
|
||||
|
||||
return html`
|
||||
${isComponentLoaded(this.hass, "logbook")
|
||||
? this._error
|
||||
@@ -67,7 +68,7 @@ export class MoreInfoLogbook extends LitElement {
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
|
||||
</div>
|
||||
<a href=${href} @click=${this._close}
|
||||
<a href=${this._showMoreHref} @click=${this._close}
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.show_more"
|
||||
)}</a
|
||||
@@ -106,6 +107,10 @@ export class MoreInfoLogbook extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showMoreHref = `/logbook?entity_id=${
|
||||
this.entityId
|
||||
}&start_date=${startOfYesterday().toISOString()}`;
|
||||
|
||||
this._throttleGetLogbookEntries();
|
||||
return;
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1,43 +1,52 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import "../components/ha-circular-progress";
|
||||
import { removeInitSkeleton } from "../util/init-skeleton";
|
||||
import { property, state } from "lit/decorators";
|
||||
|
||||
class HaInitPage extends LitElement {
|
||||
@property({ type: Boolean }) public error = false;
|
||||
|
||||
@state() showProgressIndicator = false;
|
||||
|
||||
private _showProgressIndicatorTimeout;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div>
|
||||
<img src="/static/icons/favicon-192x192.png" height="192" />
|
||||
${this.error
|
||||
? html`
|
||||
<p>Unable to connect to Home Assistant.</p>
|
||||
<mwc-button @click=${this._retry}>Retry</mwc-button>
|
||||
${location.host.includes("ui.nabu.casa")
|
||||
? html`
|
||||
<p>
|
||||
It is possible that you are seeing this screen because
|
||||
your Home Assistant is not currently connected. You can
|
||||
ask it to come online from your
|
||||
<a href="https://account.nabucasa.com/"
|
||||
>Naba Casa account page</a
|
||||
>.
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<p>Loading data</p>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
return this.error
|
||||
? html`
|
||||
<p>Unable to connect to Home Assistant.</p>
|
||||
<mwc-button @click=${this._retry}>Retry</mwc-button>
|
||||
${location.host.includes("ui.nabu.casa")
|
||||
? html`
|
||||
<p>
|
||||
It is possible that you are seeing this screen because your
|
||||
Home Assistant is not currently connected. You can ask it to
|
||||
come online from your
|
||||
<a href="https://account.nabucasa.com/"
|
||||
>Naba Casa account page</a
|
||||
>.
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<div id="progress-indicator-wrapper">
|
||||
${this.showProgressIndicator
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: ""}
|
||||
</div>
|
||||
<div id="loading-text">Loading data</div>
|
||||
`;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearTimeout(this._showProgressIndicatorTimeout);
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
removeInitSkeleton();
|
||||
this._showProgressIndicatorTimeout = setTimeout(async () => {
|
||||
await import("../components/ha-circular-progress");
|
||||
this.showProgressIndicator = true;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
private _retry() {
|
||||
@@ -46,20 +55,23 @@ class HaInitPage extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
div {
|
||||
height: 100%;
|
||||
:host {
|
||||
flex: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
ha-circular-progress {
|
||||
margin-top: 9px;
|
||||
#progress-indicator-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 25px 0;
|
||||
height: 50px;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
p {
|
||||
p,
|
||||
#loading-text {
|
||||
max-width: 350px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@@ -68,3 +80,9 @@ class HaInitPage extends LitElement {
|
||||
}
|
||||
|
||||
customElements.define("ha-init-page", HaInitPage);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-init-page": HaInitPage;
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,8 @@ class HassSubpage extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
|
||||
|
||||
@property({ type: String, attribute: "back-path" }) public backPath?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
@@ -31,6 +33,14 @@ class HassSubpage extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
: this.backPath
|
||||
? html`
|
||||
<a href=${this.backPath}>
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
></ha-icon-button-arrow-prev>
|
||||
</a>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
@@ -80,6 +90,10 @@ class HassSubpage extends LitElement {
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--app-header-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ha-menu-button,
|
||||
ha-icon-button-arrow-prev,
|
||||
|
@@ -24,10 +24,13 @@ export interface PageNavigation {
|
||||
path: string;
|
||||
translationKey?: string;
|
||||
component?: string;
|
||||
components?: string[];
|
||||
name?: string;
|
||||
core?: boolean;
|
||||
advancedOnly?: boolean;
|
||||
iconPath?: string;
|
||||
description?: string;
|
||||
iconColor?: string;
|
||||
info?: any;
|
||||
}
|
||||
|
||||
@@ -85,7 +88,7 @@ class HassTabsSubpage extends LitElement {
|
||||
<a href=${page.path}>
|
||||
<ha-tab
|
||||
.hass=${this.hass}
|
||||
.active=${page === activeTab}
|
||||
.active=${page.path === activeTab?.path}
|
||||
.narrow=${this.narrow}
|
||||
.name=${page.translationKey
|
||||
? localizeFunc(page.translationKey)
|
||||
@@ -224,7 +227,7 @@ class HassTabsSubpage extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--sidebar-text-color);
|
||||
color: var(--app-header-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bottom-bar a {
|
||||
|
@@ -88,6 +88,7 @@ class HomeAssistantMain extends LitElement {
|
||||
<ha-sidebar
|
||||
.hass=${hass}
|
||||
.narrow=${sidebarNarrow}
|
||||
.route=${this.route}
|
||||
.editMode=${this._sidebarEditMode}
|
||||
.alwaysExpand=${sidebarNarrow ||
|
||||
this.hass.dockedSidebar === "docked"}
|
||||
|
@@ -8,6 +8,10 @@ import { HassElement } from "../state/hass-element";
|
||||
import QuickBarMixin from "../state/quick-bar-mixin";
|
||||
import { HomeAssistant, Route } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import {
|
||||
renderLaunchScreenInfoBox,
|
||||
removeLaunchScreen,
|
||||
} from "../util/launch-screen";
|
||||
import {
|
||||
registerServiceWorker,
|
||||
supportsServiceWorker,
|
||||
@@ -40,6 +44,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
|
||||
private _visiblePromiseResolve?: () => void;
|
||||
|
||||
private _visibleLaunchScreen = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const path = curPath();
|
||||
@@ -55,16 +61,26 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hass = this.hass;
|
||||
if (this._isHassComplete() && this.hass) {
|
||||
return html`
|
||||
<home-assistant-main
|
||||
.hass=${this.hass}
|
||||
.route=${this._route}
|
||||
></home-assistant-main>
|
||||
`;
|
||||
}
|
||||
|
||||
return hass && hass.states && hass.config && hass.services
|
||||
? html`
|
||||
<home-assistant-main
|
||||
.hass=${this.hass}
|
||||
.route=${this._route}
|
||||
></home-assistant-main>
|
||||
`
|
||||
: html`<ha-init-page .error=${this._error}></ha-init-page>`;
|
||||
return "";
|
||||
}
|
||||
|
||||
update(changedProps) {
|
||||
super.update(changedProps);
|
||||
|
||||
// Remove launch screen if main gui is loaded
|
||||
if (this._isHassComplete() && this._visibleLaunchScreen) {
|
||||
this._visibleLaunchScreen = false;
|
||||
removeLaunchScreen();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
@@ -109,6 +125,13 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
navigate(href);
|
||||
}
|
||||
});
|
||||
|
||||
// Render launch screen info box (loading data / error message)
|
||||
if (!this._isHassComplete() && this._visibleLaunchScreen) {
|
||||
renderLaunchScreenInfoBox(
|
||||
html`<ha-init-page .error=${this._error}></ha-init-page>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
@@ -229,6 +252,14 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
this._visiblePromiseResolve = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _isHassComplete(): boolean {
|
||||
if (this.hass?.states && this.hass.config && this.hass.services) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -11,7 +11,7 @@ import { deepEqual } from "../common/util/deep-equal";
|
||||
import { getDefaultPanel } from "../data/panel";
|
||||
import { CustomPanelInfo } from "../data/panel_custom";
|
||||
import { HomeAssistant, Panels } from "../types";
|
||||
import { removeInitSkeleton } from "../util/init-skeleton";
|
||||
import { removeLaunchScreen } from "../util/launch-screen";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouteOptions,
|
||||
@@ -226,7 +226,7 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
) {
|
||||
await this.rebuild();
|
||||
await this.pageRendered;
|
||||
removeInitSkeleton();
|
||||
removeLaunchScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,13 +2,16 @@ import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { createCurrencyListEl } from "../components/currency-datalist";
|
||||
import "../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../components/map/ha-locations-editor";
|
||||
import type {
|
||||
HaLocationsEditor,
|
||||
MarkerLocation,
|
||||
} from "../components/map/ha-locations-editor";
|
||||
import { createTimezoneListEl } from "../components/timezone-datalist";
|
||||
import {
|
||||
ConfigUpdateValues,
|
||||
@@ -25,6 +28,7 @@ import type { HaRadio } from "../components/ha-radio";
|
||||
|
||||
const amsterdam: [number, number] = [52.3731339, 4.8903147];
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
const locationMarkerId = "location";
|
||||
|
||||
@customElement("onboarding-core-config")
|
||||
class OnboardingCoreConfig extends LitElement {
|
||||
@@ -46,6 +50,8 @@ class OnboardingCoreConfig extends LitElement {
|
||||
|
||||
@state() private _timeZone?: string;
|
||||
|
||||
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<p>
|
||||
@@ -268,7 +274,7 @@ class OnboardingCoreConfig extends LitElement {
|
||||
private _markerLocation = memoizeOne(
|
||||
(location: [number, number]): MarkerLocation[] => [
|
||||
{
|
||||
id: "location",
|
||||
id: locationMarkerId,
|
||||
latitude: location[0],
|
||||
longitude: location[1],
|
||||
location_editable: true,
|
||||
@@ -304,6 +310,15 @@ class OnboardingCoreConfig extends LitElement {
|
||||
const values = await detectCoreConfig(this.hass);
|
||||
|
||||
if (values.latitude && values.longitude) {
|
||||
this.map.addEventListener(
|
||||
"markers-updated",
|
||||
() => {
|
||||
this.map.fitMarker(locationMarkerId);
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
this._location = [Number(values.latitude), Number(values.longitude)];
|
||||
}
|
||||
if (values.elevation) {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -9,168 +8,116 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-form/ha-form";
|
||||
import type { HaForm } from "../components/ha-form/ha-form";
|
||||
import { HaFormDataContainer, HaFormSchema } from "../components/ha-form/types";
|
||||
import { onboardUserStep } from "../data/onboarding";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
|
||||
const CREATE_USER_SCHEMA: HaFormSchema[] = [
|
||||
{ type: "string", name: "name", required: true },
|
||||
{ type: "string", name: "username", required: true },
|
||||
{ type: "string", name: "password", required: true },
|
||||
{ type: "string", name: "password_confirm", required: true },
|
||||
];
|
||||
|
||||
@customElement("onboarding-create-user")
|
||||
class OnboardingCreateUser extends LitElement {
|
||||
@property() public localize!: LocalizeFunc;
|
||||
|
||||
@property() public language!: string;
|
||||
|
||||
@state() private _name = "";
|
||||
|
||||
@state() private _username = "";
|
||||
|
||||
@state() private _password = "";
|
||||
|
||||
@state() private _passwordConfirm = "";
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _errorMsg?: string = undefined;
|
||||
@state() private _errorMsg?: string;
|
||||
|
||||
@state() private _formError: Record<string, string> = {};
|
||||
|
||||
@state() private _newUser: HaFormDataContainer = {};
|
||||
|
||||
@query("ha-form", true) private _form?: HaForm;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<p>
|
||||
${this.localize("ui.panel.page-onboarding.intro")}
|
||||
</p>
|
||||
<p>${this.localize("ui.panel.page-onboarding.intro")}</p>
|
||||
<p>${this.localize("ui.panel.page-onboarding.user.intro")}</p>
|
||||
|
||||
<p>
|
||||
${this.localize("ui.panel.page-onboarding.user.intro")}
|
||||
</p>
|
||||
${this._errorMsg
|
||||
? html`<ha-alert alert-type="error">${this._errorMsg}</ha-alert>`
|
||||
: ""}
|
||||
|
||||
${
|
||||
this._errorMsg
|
||||
? html`
|
||||
<p class="error">
|
||||
${this.localize(
|
||||
`ui.panel.page-onboarding.user.error.${this._errorMsg}`
|
||||
) || this._errorMsg}
|
||||
</p>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<form>
|
||||
<paper-input
|
||||
name="name"
|
||||
label=${this.localize("ui.panel.page-onboarding.user.data.name")}
|
||||
.value=${this._name}
|
||||
<ha-form
|
||||
.computeLabel=${this._computeLabel(this.localize)}
|
||||
.data=${this._newUser}
|
||||
.disabled=${this._loading}
|
||||
.error=${this._formError}
|
||||
.schema=${CREATE_USER_SCHEMA}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize='on'
|
||||
.errorMessage=${this.localize(
|
||||
"ui.panel.page-onboarding.user.required_field"
|
||||
)}
|
||||
@blur=${this._maybePopulateUsername}
|
||||
></paper-input>
|
||||
></ha-form>
|
||||
|
||||
<paper-input
|
||||
name="username"
|
||||
label=${this.localize("ui.panel.page-onboarding.user.data.username")}
|
||||
value=${this._username}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize='none'
|
||||
.errorMessage=${this.localize(
|
||||
"ui.panel.page-onboarding.user.required_field"
|
||||
)}
|
||||
></paper-input>
|
||||
|
||||
<paper-input
|
||||
name="password"
|
||||
label=${this.localize("ui.panel.page-onboarding.user.data.password")}
|
||||
value=${this._password}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
required
|
||||
type='password'
|
||||
auto-validate
|
||||
.errorMessage=${this.localize(
|
||||
"ui.panel.page-onboarding.user.required_field"
|
||||
)}
|
||||
></paper-input>
|
||||
|
||||
<paper-input
|
||||
name="passwordConfirm"
|
||||
label=${this.localize(
|
||||
"ui.panel.page-onboarding.user.data.password_confirm"
|
||||
)}
|
||||
value=${this._passwordConfirm}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
required
|
||||
type='password'
|
||||
.invalid=${
|
||||
this._password !== "" &&
|
||||
this._passwordConfirm !== "" &&
|
||||
this._passwordConfirm !== this._password
|
||||
}
|
||||
.errorMessage=${this.localize(
|
||||
"ui.panel.page-onboarding.user.error.password_not_match"
|
||||
)}
|
||||
></paper-input>
|
||||
|
||||
<p class="action">
|
||||
<mwc-button
|
||||
raised
|
||||
@click=${this._submitForm}
|
||||
.disabled=${this._loading}
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.user.create_account")}
|
||||
</mwc-button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
<mwc-button
|
||||
raised
|
||||
@click=${this._submitForm}
|
||||
.disabled=${this._loading ||
|
||||
!this._newUser.name ||
|
||||
!this._newUser.username ||
|
||||
!this._newUser.password}
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.user.create_account")}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(
|
||||
() => this.shadowRoot!.querySelector("paper-input")!.focus(),
|
||||
100
|
||||
);
|
||||
setTimeout(() => this._form?.focus(), 100);
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
if (
|
||||
ev.keyCode === 13 &&
|
||||
this._newUser.name &&
|
||||
this._newUser.username &&
|
||||
this._newUser.password &&
|
||||
this._newUser.password_confirm
|
||||
) {
|
||||
this._submitForm(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev: PolymerChangedEvent<string>): void {
|
||||
const name = (ev.target as any).name;
|
||||
this[`_${name}`] = ev.detail.value;
|
||||
private _computeLabel(localize) {
|
||||
return (schema: HaFormSchema) =>
|
||||
localize(`ui.panel.page-onboarding.user.data.${schema.name}`);
|
||||
}
|
||||
|
||||
private _handleValueChanged(
|
||||
ev: PolymerChangedEvent<HaFormDataContainer>
|
||||
): void {
|
||||
this._newUser = ev.detail.value;
|
||||
this._maybePopulateUsername();
|
||||
this._formError.password_confirm =
|
||||
this._newUser.password !== this._newUser.password_confirm
|
||||
? this.localize(
|
||||
"ui.panel.page-onboarding.user.error.password_not_match"
|
||||
)
|
||||
: "";
|
||||
}
|
||||
|
||||
private _maybePopulateUsername(): void {
|
||||
if (this._username) {
|
||||
if (!this._newUser.name || this._newUser.name === this._newUser.username) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = this._name.split(" ");
|
||||
|
||||
const parts = String(this._newUser.name).split(" ");
|
||||
if (parts.length) {
|
||||
this._username = parts[0].toLowerCase();
|
||||
this._newUser.username = parts[0].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
private async _submitForm(ev): Promise<void> {
|
||||
ev.preventDefault();
|
||||
if (!this._name || !this._username || !this._password) {
|
||||
this._errorMsg = "required_fields";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._password !== this._passwordConfirm) {
|
||||
this._errorMsg = "password_not_match";
|
||||
return;
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
this._errorMsg = "";
|
||||
|
||||
@@ -179,9 +126,9 @@ class OnboardingCreateUser extends LitElement {
|
||||
|
||||
const result = await onboardUserStep({
|
||||
client_id: clientId,
|
||||
name: this._name,
|
||||
username: this._username,
|
||||
password: this._password,
|
||||
name: String(this._newUser.name),
|
||||
username: String(this._newUser.username),
|
||||
password: String(this._newUser.password),
|
||||
language: this.language,
|
||||
});
|
||||
|
||||
@@ -199,13 +146,11 @@ class OnboardingCreateUser extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin: 32px 0 16px;
|
||||
mwc-button {
|
||||
margin: 32px 0 0;
|
||||
text-align: center;
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -3,19 +3,31 @@ import "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-dialog";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-picture-upload";
|
||||
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
|
||||
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
|
||||
|
||||
const cropOptions: CropOptions = {
|
||||
round: false,
|
||||
type: "image/jpeg",
|
||||
quality: 0.75,
|
||||
aspectRatio: 1.78,
|
||||
};
|
||||
|
||||
class DialogAreaDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _name!: string;
|
||||
|
||||
@state() private _picture!: string | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: AreaRegistryDetailDialogParams;
|
||||
@@ -28,6 +40,7 @@ class DialogAreaDetail extends LitElement {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._name = this._params.entry ? this._params.entry.name : "";
|
||||
this._picture = this._params.entry?.picture || null;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
@@ -47,12 +60,17 @@ class DialogAreaDetail extends LitElement {
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${entry
|
||||
? entry.name
|
||||
: this.hass.localize("ui.panel.config.areas.editor.default_name")}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
entry
|
||||
? entry.name
|
||||
: this.hass.localize("ui.panel.config.areas.editor.default_name")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${this._error
|
||||
? html` <ha-alert alert-type="error">${this._error}</ha-alert> `
|
||||
: ""}
|
||||
<div class="form">
|
||||
${entry
|
||||
? html`
|
||||
@@ -75,6 +93,13 @@ class DialogAreaDetail extends LitElement {
|
||||
)}
|
||||
.invalid=${nameInvalid}
|
||||
></paper-input>
|
||||
<ha-picture-upload
|
||||
.hass=${this.hass}
|
||||
.value=${this._picture}
|
||||
crop
|
||||
.cropOptions=${cropOptions}
|
||||
@change=${this._pictureChanged}
|
||||
></ha-picture-upload>
|
||||
</div>
|
||||
</div>
|
||||
${entry
|
||||
@@ -117,18 +142,24 @@ class DialogAreaDetail extends LitElement {
|
||||
this._name = ev.detail.value;
|
||||
}
|
||||
|
||||
private _pictureChanged(ev: PolymerChangedEvent<string | null>) {
|
||||
this._error = undefined;
|
||||
this._picture = (ev.target as HaPictureUpload).value;
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
const values: AreaRegistryEntryMutableParams = {
|
||||
name: this._name.trim(),
|
||||
picture: this._picture,
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry!(values);
|
||||
} else {
|
||||
await this._params!.createEntry!(values);
|
||||
}
|
||||
this._params = undefined;
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
err.message ||
|
||||
@@ -142,13 +173,11 @@ class DialogAreaDetail extends LitElement {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry!()) {
|
||||
this._params = undefined;
|
||||
this.closeDialog();
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
|
||||
navigate("/config/areas/dashboard");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -158,9 +187,6 @@ class DialogAreaDetail extends LitElement {
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,13 +1,17 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiCog } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { mdiImagePlus, mdiPencil } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
deleteAreaRegistryEntry,
|
||||
@@ -131,28 +135,62 @@ class HaConfigAreaPage extends LitElement {
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.tabs=${configSections.integrations}
|
||||
.tabs=${configSections.devices}
|
||||
.route=${this.route}
|
||||
>
|
||||
${this.narrow ? html` <span slot="header"> ${area.name} </span> ` : ""}
|
||||
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiCog}
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
.label=${this.hass.localize("ui.panel.config.areas.edit_settings")}
|
||||
></ha-icon-button>
|
||||
${this.narrow
|
||||
? html`<span slot="header"> ${area.name} </span>
|
||||
<ha-icon-button
|
||||
.path=${mdiPencil}
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.edit_settings"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: ""}
|
||||
|
||||
<div class="container">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<div class="fullwidth">
|
||||
<h1>${area.name}</h1>
|
||||
<h1>
|
||||
${area.name}
|
||||
<ha-icon-button
|
||||
.path=${mdiPencil}
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.edit_settings"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</h1>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="column">
|
||||
${area.picture
|
||||
? html`<div class="img-container">
|
||||
<img src=${area.picture} /><ha-icon-button
|
||||
.path=${mdiPencil}
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.edit_settings"
|
||||
)}
|
||||
class="img-edit-btn"
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: html`<mwc-button
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.add_picture"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiImagePlus} slot="icon"></ha-svg-icon>
|
||||
</mwc-button>`}
|
||||
<ha-card
|
||||
.header=${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
>${devices.length
|
||||
@@ -181,7 +219,8 @@ class HaConfigAreaPage extends LitElement {
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.linked_entities_caption"
|
||||
)}
|
||||
>${entities.length
|
||||
>
|
||||
${entities.length
|
||||
? entities.map(
|
||||
(entity) =>
|
||||
html`
|
||||
@@ -390,6 +429,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
|
||||
try {
|
||||
await deleteAreaRegistryEntry(this.hass!, entry!.area_id);
|
||||
afterNextRender(() => history.back());
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
return false;
|
||||
@@ -403,7 +443,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
haStyle,
|
||||
css`
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
font-family: var(--paper-font-headline_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-headline_-_-webkit-font-smoothing
|
||||
@@ -413,6 +453,13 @@ class HaConfigAreaPage extends LitElement {
|
||||
letter-spacing: var(--paper-font-headline_-_letter-spacing);
|
||||
line-height: var(--paper-font-headline_-_line-height);
|
||||
opacity: var(--dark-primary-opacity);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: var(--ha-card-border-radius, 4px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -458,6 +505,34 @@ class HaConfigAreaPage extends LitElement {
|
||||
paper-item.no-link {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
ha-card > a:first-child {
|
||||
display: block;
|
||||
}
|
||||
ha-card > *:first-child {
|
||||
margin-top: -16px;
|
||||
}
|
||||
.img-container {
|
||||
position: relative;
|
||||
}
|
||||
.img-edit-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
display: none;
|
||||
}
|
||||
.img-container:hover .img-edit-btn {
|
||||
display: block;
|
||||
}
|
||||
.img-edit-btn::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--card-background-color);
|
||||
opacity: 0.5;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,15 +1,8 @@
|
||||
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
@@ -21,7 +14,7 @@ import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
@@ -53,97 +46,51 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
entities: EntityRegistryEntry[]
|
||||
) =>
|
||||
areas.map((area) => {
|
||||
let noDevicesInArea = 0;
|
||||
let noServicesInArea = 0;
|
||||
let noEntitiesInArea = 0;
|
||||
|
||||
const devicesInArea = new Set();
|
||||
|
||||
for (const device of devices) {
|
||||
if (device.area_id === area.area_id) {
|
||||
devicesInArea.add(device.id);
|
||||
if (device.entry_type === "service") {
|
||||
noServicesInArea++;
|
||||
} else {
|
||||
noDevicesInArea++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let entitiesInArea = 0;
|
||||
|
||||
for (const entity of entities) {
|
||||
if (
|
||||
entity.area_id
|
||||
? entity.area_id === area.area_id
|
||||
: devicesInArea.has(entity.device_id)
|
||||
) {
|
||||
entitiesInArea++;
|
||||
noEntitiesInArea++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...area,
|
||||
devices: devicesInArea.size,
|
||||
entities: entitiesInArea,
|
||||
devices: noDevicesInArea,
|
||||
services: noServicesInArea,
|
||||
entities: noEntitiesInArea,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.area"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.area"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
},
|
||||
devices: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.devices"
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
},
|
||||
entities: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.entities"
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.integrations}
|
||||
.tabs=${configSections.devices}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._areas(this.areas, this.devices, this.entities)}
|
||||
@row-click=${this._handleRowClicked}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.no_areas"
|
||||
)}
|
||||
id="area_id"
|
||||
hasFab
|
||||
clickable
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
@@ -151,6 +98,58 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
.path=${mdiHelpCircle}
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
<div class="container">
|
||||
${this._areas(this.areas, this.devices, this.entities).map(
|
||||
(area) =>
|
||||
html`<a href=${`/config/areas/area/${area.area_id}`}
|
||||
><ha-card outlined>
|
||||
<div
|
||||
style=${styleMap({
|
||||
backgroundImage: area.picture
|
||||
? `url(${area.picture})`
|
||||
: undefined,
|
||||
})}
|
||||
class="picture ${!area.picture ? "placeholder" : ""}"
|
||||
></div>
|
||||
<h1 class="card-header">${area.name}</h1>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
${area.devices
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
"count",
|
||||
area.devices
|
||||
)}${area.services ? "," : ""}
|
||||
`
|
||||
: ""}
|
||||
${area.services
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
"count",
|
||||
area.services
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
${(area.devices || area.services) && area.entities
|
||||
? this.hass.localize("ui.common.and")
|
||||
: ""}
|
||||
${area.entities
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
"count",
|
||||
area.entities
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card></a
|
||||
>`
|
||||
)}
|
||||
</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize(
|
||||
@@ -161,7 +160,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -191,11 +190,6 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const areaId = ev.detail.id;
|
||||
navigate(`/config/areas/area/${areaId}`);
|
||||
}
|
||||
|
||||
private _openDialog(entry?: AreaRegistryEntry) {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
entry,
|
||||
@@ -210,6 +204,51 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 16px 16px;
|
||||
padding: 8px 16px 16px;
|
||||
margin: 0 auto 64px auto;
|
||||
max-width: 1000px;
|
||||
}
|
||||
.container > * {
|
||||
max-width: 500px;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
h1 {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.picture {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
.picture.placeholder::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
.card-content {
|
||||
min-height: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-areas-dashboard": HaConfigAreasDashboard;
|
||||
}
|
||||
}
|
||||
|
@@ -114,7 +114,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.backCallback=${this._backTapped}
|
||||
.tabs=${configSections.automation}
|
||||
.tabs=${configSections.automations}
|
||||
>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
|
@@ -225,7 +225,7 @@ class HaAutomationPicker extends LitElement {
|
||||
back-path="/config"
|
||||
id="entity_id"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automation}
|
||||
.tabs=${configSections.automations}
|
||||
.activeFilters=${this._activeFilters}
|
||||
.columns=${this._columns(this.narrow, this.hass.locale)}
|
||||
.data=${this._automations(this.automations, this._filteredAutomations)}
|
||||
|
@@ -112,7 +112,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automation}
|
||||
.tabs=${configSections.automations}
|
||||
>
|
||||
${this.narrow
|
||||
? html`<span slot="header">${title}</span>
|
||||
|
@@ -63,6 +63,13 @@ class DialogThingtalk extends LitElement {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeInitDialog() {
|
||||
if (this._placeholders) {
|
||||
return;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
@@ -82,7 +89,7 @@ class DialogThingtalk extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
@closed=${this.closeInitDialog}
|
||||
.heading=${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.header`
|
||||
)}
|
||||
|
@@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automation}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._processedBlueprints(this.blueprints)}
|
||||
id="entity_id"
|
||||
|
@@ -50,7 +50,7 @@ export class CloudLogin extends LitElement {
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<div class="content">
|
||||
<ha-config-section isWide=${this.isWide}>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">Home Assistant Cloud</span>
|
||||
<div slot="introduction">
|
||||
<p>
|
||||
|
@@ -1,88 +0,0 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "../ha-config-section";
|
||||
import "../ha-entity-config";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-form-customize";
|
||||
|
||||
class HaConfigCustomize extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide?: boolean;
|
||||
|
||||
@property() public narrow?: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@property() private _selectedEntityId = "";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.advanced}
|
||||
>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">
|
||||
${this.hass.localize("ui.panel.config.customize.picker.header")}
|
||||
</span>
|
||||
<span slot="introduction">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.customize.picker.introduction"
|
||||
)}
|
||||
<br />
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/configuration/customizing-devices/#customization-using-the-ui"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.customize.picker.documentation"
|
||||
)}
|
||||
</a>
|
||||
</span>
|
||||
<ha-entity-config
|
||||
.hass=${this.hass}
|
||||
.selectedEntityId=${this._selectedEntityId}
|
||||
>
|
||||
</ha-entity-config>
|
||||
</ha-config-section>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (!this.route.path.includes("/edit/")) {
|
||||
return;
|
||||
}
|
||||
const routeSegments = this.route.path.split("/edit/");
|
||||
this._selectedEntityId = routeSegments.length > 1 ? routeSegments[1] : "";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
customElements.define("ha-config-customize", HaConfigCustomize);
|
@@ -1,84 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import hassAttributeUtil from "../../../util/hass-attributes-util";
|
||||
import "../ha-form-style";
|
||||
import "./types/ha-customize-array";
|
||||
import "./types/ha-customize-boolean";
|
||||
import "./types/ha-customize-icon";
|
||||
import "./types/ha-customize-key-value";
|
||||
import "./types/ha-customize-string";
|
||||
|
||||
class HaCustomizeAttribute extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-form-style">
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.button {
|
||||
position: absolute;
|
||||
margin-top: -20px;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
<div id="wrapper" class="form-group"></div>
|
||||
<ha-icon-button class="button" on-click="tapButton">
|
||||
<ha-icon icon="[[getIcon(item.secondary)]]"></ha-icon>
|
||||
</ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
item: {
|
||||
type: Object,
|
||||
notify: true,
|
||||
observer: "itemObserver",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
tapButton() {
|
||||
if (this.item.secondary) {
|
||||
this.item = { ...this.item, secondary: false };
|
||||
} else {
|
||||
this.item = { ...this.item, closed: true };
|
||||
}
|
||||
}
|
||||
|
||||
getIcon(secondary) {
|
||||
return secondary ? "hass:pencil" : "hass:close";
|
||||
}
|
||||
|
||||
itemObserver(item) {
|
||||
const wrapper = this.$.wrapper;
|
||||
const tag = hassAttributeUtil.TYPE_TO_TAG[item.type].toUpperCase();
|
||||
let child;
|
||||
if (wrapper.lastChild && wrapper.lastChild.tagName === tag) {
|
||||
child = wrapper.lastChild;
|
||||
} else {
|
||||
if (wrapper.lastChild) {
|
||||
wrapper.removeChild(wrapper.lastChild);
|
||||
}
|
||||
// Creating an element with upper case works fine in Chrome, but in FF it doesn't immediately
|
||||
// become a defined Custom Element. Polymer does that in some later pass.
|
||||
this.$.child = child = document.createElement(tag.toLowerCase());
|
||||
child.className = "form-control";
|
||||
child.addEventListener("item-changed", () => {
|
||||
this.item = { ...child.item };
|
||||
});
|
||||
}
|
||||
child.setProperties({ item: this.item });
|
||||
if (child.parentNode === null) {
|
||||
wrapper.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("ha-customize-attribute", HaCustomizeAttribute);
|
@@ -1,34 +0,0 @@
|
||||
import { MutableData } from "@polymer/polymer/lib/mixins/mutable-data";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "./ha-customize-attribute";
|
||||
|
||||
class HaFormCustomizeAttributes extends MutableData(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<template is="dom-repeat" items="{{attributes}}" mutable-data="">
|
||||
<ha-customize-attribute item="{{item}}" hidden$="[[item.closed]]">
|
||||
</ha-customize-attribute>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
attributes: {
|
||||
type: Array,
|
||||
notify: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"ha-form-customize-attributes",
|
||||
HaFormCustomizeAttributes
|
||||
);
|
@@ -1,362 +0,0 @@
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import hassAttributeUtil from "../../../util/hass-attributes-util";
|
||||
import "../ha-form-style";
|
||||
import "./ha-form-customize-attributes";
|
||||
|
||||
export class HaFormCustomize extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style ha-form-style">
|
||||
.warning {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.attributes-text {
|
||||
padding-left: 20px;
|
||||
}
|
||||
</style>
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[computeShowWarning(localConfig, globalConfig)]]"
|
||||
>
|
||||
<div class="warning">
|
||||
[[localize('ui.panel.config.customize.warning.include_sentence')]]
|
||||
<a
|
||||
href="[[_computeDocumentationUrl(hass)]]"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>[[localize('ui.panel.config.customize.warning.include_link')]]</a
|
||||
>.<br />
|
||||
[[localize('ui.panel.config.customize.warning.not_applied')]]
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[hasLocalAttributes]]">
|
||||
<p class="attributes-text">
|
||||
[[localize('ui.panel.config.customize.attributes_customize')]]<br />
|
||||
</p>
|
||||
<ha-form-customize-attributes
|
||||
attributes="{{localAttributes}}"
|
||||
></ha-form-customize-attributes>
|
||||
</template>
|
||||
<template is="dom-if" if="[[hasGlobalAttributes]]">
|
||||
<p class="attributes-text">
|
||||
[[localize('ui.panel.config.customize.attributes_outside')]]<br />
|
||||
[[localize('ui.panel.config.customize.different_include')]]
|
||||
</p>
|
||||
<ha-form-customize-attributes
|
||||
attributes="{{globalAttributes}}"
|
||||
></ha-form-customize-attributes>
|
||||
</template>
|
||||
<template is="dom-if" if="[[hasExistingAttributes]]">
|
||||
<p class="attributes-text">
|
||||
[[localize('ui.panel.config.customize.attributes_set')]]<br />
|
||||
[[localize('ui.panel.config.customize.attributes_override')]]
|
||||
</p>
|
||||
<ha-form-customize-attributes
|
||||
attributes="{{existingAttributes}}"
|
||||
></ha-form-customize-attributes>
|
||||
</template>
|
||||
<template is="dom-if" if="[[hasNewAttributes]]">
|
||||
<p class="attributes-text">
|
||||
[[localize('ui.panel.config.customize.attributes_not_set')]]
|
||||
</p>
|
||||
<ha-form-customize-attributes
|
||||
attributes="{{newAttributes}}"
|
||||
></ha-form-customize-attributes>
|
||||
</template>
|
||||
<template is="dom-if" if="[[entity]]">
|
||||
<div class="form-group">
|
||||
<paper-dropdown-menu
|
||||
label="[[localize('ui.panel.config.customize.pick_attribute')]]"
|
||||
class="flex"
|
||||
dynamic-align=""
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
selected="{{selectedNewAttribute}}"
|
||||
>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[newAttributesOptions]]"
|
||||
as="option"
|
||||
>
|
||||
<paper-item>[[option]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
entity: Object,
|
||||
|
||||
localAttributes: {
|
||||
type: Array,
|
||||
computed: "computeLocalAttributes(localConfig)",
|
||||
},
|
||||
hasLocalAttributes: Boolean,
|
||||
|
||||
globalAttributes: {
|
||||
type: Array,
|
||||
computed: "computeGlobalAttributes(localConfig, globalConfig)",
|
||||
},
|
||||
hasGlobalAttributes: Boolean,
|
||||
|
||||
existingAttributes: {
|
||||
type: Array,
|
||||
computed:
|
||||
"computeExistingAttributes(localConfig, globalConfig, entity)",
|
||||
},
|
||||
hasExistingAttributes: Boolean,
|
||||
|
||||
newAttributes: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
hasNewAttributes: Boolean,
|
||||
|
||||
newAttributesOptions: Array,
|
||||
selectedNewAttribute: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: "selectedNewAttributeObserver",
|
||||
},
|
||||
|
||||
localConfig: Object,
|
||||
globalConfig: Object,
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
"attributesObserver(localAttributes.*, globalAttributes.*, existingAttributes.*, newAttributes.*)",
|
||||
];
|
||||
}
|
||||
|
||||
_initOpenObject(key, value, secondary, config) {
|
||||
return {
|
||||
attribute: key,
|
||||
value: value,
|
||||
closed: false,
|
||||
domain: computeStateDomain(this.entity),
|
||||
secondary: secondary,
|
||||
description: key,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
loadEntity(entity) {
|
||||
this.entity = entity;
|
||||
return this.hass
|
||||
.callApi("GET", "config/customize/config/" + entity.entity_id)
|
||||
.then((data) => {
|
||||
this.localConfig = data.local;
|
||||
this.globalConfig = data.global;
|
||||
this.newAttributes = [];
|
||||
});
|
||||
}
|
||||
|
||||
saveEntity() {
|
||||
const data = {};
|
||||
const attrs = this.localAttributes.concat(
|
||||
this.globalAttributes,
|
||||
this.existingAttributes,
|
||||
this.newAttributes
|
||||
);
|
||||
attrs.forEach((attr) => {
|
||||
if (
|
||||
attr.closed ||
|
||||
attr.secondary ||
|
||||
!attr.attribute ||
|
||||
attr.value === null ||
|
||||
attr.value === undefined
|
||||
)
|
||||
return;
|
||||
const value = attr.type === "json" ? JSON.parse(attr.value) : attr.value;
|
||||
if (value === null || value === undefined) return;
|
||||
data[attr.attribute] = value;
|
||||
});
|
||||
|
||||
const objectId = this.entity.entity_id;
|
||||
return this.hass.callApi(
|
||||
"POST",
|
||||
"config/customize/config/" + objectId,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
_computeSingleAttribute(key, value, secondary) {
|
||||
const config = hassAttributeUtil.LOGIC_STATE_ATTRIBUTES[key] || {
|
||||
type: hassAttributeUtil.UNKNOWN_TYPE,
|
||||
};
|
||||
return this._initOpenObject(
|
||||
key,
|
||||
config.type === "json" ? JSON.stringify(value) : value,
|
||||
secondary,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
_computeAttributes(config, keys, secondary) {
|
||||
return keys.map((key) =>
|
||||
this._computeSingleAttribute(key, config[key], secondary)
|
||||
);
|
||||
}
|
||||
|
||||
_computeDocumentationUrl(hass) {
|
||||
return documentationUrl(
|
||||
hass,
|
||||
"/docs/configuration/customizing-devices/#customization-using-the-ui"
|
||||
);
|
||||
}
|
||||
|
||||
computeLocalAttributes(localConfig) {
|
||||
if (!localConfig) return [];
|
||||
const localKeys = Object.keys(localConfig);
|
||||
const result = this._computeAttributes(localConfig, localKeys, false);
|
||||
return result;
|
||||
}
|
||||
|
||||
computeGlobalAttributes(localConfig, globalConfig) {
|
||||
if (!localConfig || !globalConfig) return [];
|
||||
const localKeys = Object.keys(localConfig);
|
||||
const globalKeys = Object.keys(globalConfig).filter(
|
||||
(key) => !localKeys.includes(key)
|
||||
);
|
||||
return this._computeAttributes(globalConfig, globalKeys, true);
|
||||
}
|
||||
|
||||
computeExistingAttributes(localConfig, globalConfig, entity) {
|
||||
if (!localConfig || !globalConfig || !entity) return [];
|
||||
const localKeys = Object.keys(localConfig);
|
||||
const globalKeys = Object.keys(globalConfig);
|
||||
const entityKeys = Object.keys(entity.attributes).filter(
|
||||
(key) => !localKeys.includes(key) && !globalKeys.includes(key)
|
||||
);
|
||||
return this._computeAttributes(entity.attributes, entityKeys, true);
|
||||
}
|
||||
|
||||
computeShowWarning(localConfig, globalConfig) {
|
||||
if (!localConfig || !globalConfig) return false;
|
||||
return Object.keys(localConfig).some(
|
||||
(key) =>
|
||||
JSON.stringify(globalConfig[key]) !== JSON.stringify(localConfig[key])
|
||||
);
|
||||
}
|
||||
|
||||
filterFromAttributes(attributes) {
|
||||
return (key) =>
|
||||
!attributes ||
|
||||
attributes.every((attr) => attr.attribute !== key || attr.closed);
|
||||
}
|
||||
|
||||
getNewAttributesOptions(
|
||||
localAttributes,
|
||||
globalAttributes,
|
||||
existingAttributes,
|
||||
newAttributes
|
||||
) {
|
||||
const knownKeys = Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES)
|
||||
.filter((key) => {
|
||||
const conf = hassAttributeUtil.LOGIC_STATE_ATTRIBUTES[key];
|
||||
return (
|
||||
conf &&
|
||||
(!conf.domains ||
|
||||
!this.entity ||
|
||||
conf.domains.includes(computeStateDomain(this.entity)))
|
||||
);
|
||||
})
|
||||
.filter(this.filterFromAttributes(localAttributes))
|
||||
.filter(this.filterFromAttributes(globalAttributes))
|
||||
.filter(this.filterFromAttributes(existingAttributes))
|
||||
.filter(this.filterFromAttributes(newAttributes));
|
||||
return knownKeys.sort().concat("Other");
|
||||
}
|
||||
|
||||
selectedNewAttributeObserver(selected) {
|
||||
if (selected < 0) return;
|
||||
const option = this.newAttributesOptions[selected];
|
||||
if (selected === this.newAttributesOptions.length - 1) {
|
||||
// The "Other" option.
|
||||
const attr = this._initOpenObject("", "", false /* secondary */, {
|
||||
type: hassAttributeUtil.ADD_TYPE,
|
||||
});
|
||||
this.push("newAttributes", attr);
|
||||
this.selectedNewAttribute = -1;
|
||||
return;
|
||||
}
|
||||
let result = this.localAttributes.findIndex(
|
||||
(attr) => attr.attribute === option
|
||||
);
|
||||
if (result >= 0) {
|
||||
this.set("localAttributes." + result + ".closed", false);
|
||||
this.selectedNewAttribute = -1;
|
||||
return;
|
||||
}
|
||||
result = this.globalAttributes.findIndex(
|
||||
(attr) => attr.attribute === option
|
||||
);
|
||||
if (result >= 0) {
|
||||
this.set("globalAttributes." + result + ".closed", false);
|
||||
this.selectedNewAttribute = -1;
|
||||
return;
|
||||
}
|
||||
result = this.existingAttributes.findIndex(
|
||||
(attr) => attr.attribute === option
|
||||
);
|
||||
if (result >= 0) {
|
||||
this.set("existingAttributes." + result + ".closed", false);
|
||||
this.selectedNewAttribute = -1;
|
||||
return;
|
||||
}
|
||||
result = this.newAttributes.findIndex((attr) => attr.attribute === option);
|
||||
if (result >= 0) {
|
||||
this.set("newAttributes." + result + ".closed", false);
|
||||
this.selectedNewAttribute = -1;
|
||||
return;
|
||||
}
|
||||
const attr = this._computeSingleAttribute(
|
||||
option,
|
||||
"",
|
||||
false /* secondary */
|
||||
);
|
||||
this.push("newAttributes", attr);
|
||||
this.selectedNewAttribute = -1;
|
||||
}
|
||||
|
||||
attributesObserver() {
|
||||
this.hasLocalAttributes =
|
||||
this.localAttributes && this.localAttributes.some((attr) => !attr.closed);
|
||||
this.hasGlobalAttributes =
|
||||
this.globalAttributes &&
|
||||
this.globalAttributes.some((attr) => !attr.closed);
|
||||
this.hasExistingAttributes =
|
||||
this.existingAttributes &&
|
||||
this.existingAttributes.some((attr) => !attr.closed);
|
||||
this.hasNewAttributes =
|
||||
this.newAttributes && this.newAttributes.some((attr) => !attr.closed);
|
||||
this.newAttributesOptions = this.getNewAttributesOptions(
|
||||
this.localAttributes,
|
||||
this.globalAttributes,
|
||||
this.existingAttributes,
|
||||
this.newAttributes
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-form-customize", HaFormCustomize);
|
@@ -1,63 +0,0 @@
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { EventsMixin } from "../../../../mixins/events-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaCustomizeArray extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
paper-dropdown-menu {
|
||||
margin: -9px 0;
|
||||
}
|
||||
</style>
|
||||
<paper-dropdown-menu
|
||||
label="[[item.description]]"
|
||||
disabled="[[item.secondary]]"
|
||||
selected-item-label="{{item.value}}"
|
||||
dynamic-align=""
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
selected="[[computeSelected(item)]]"
|
||||
>
|
||||
<template is="dom-repeat" items="[[getOptions(item)]]" as="option">
|
||||
<paper-item>[[option]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
item: {
|
||||
type: Object,
|
||||
notifies: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getOptions(item) {
|
||||
const domain = item.domain || "*";
|
||||
const options = item.options[domain] || item.options["*"];
|
||||
if (!options) {
|
||||
this.item.type = "string";
|
||||
this.fire("item-changed");
|
||||
return [];
|
||||
}
|
||||
return options.sort();
|
||||
}
|
||||
|
||||
computeSelected(item) {
|
||||
const options = this.getOptions(item);
|
||||
return options.indexOf(item.value);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-customize-array", HaCustomizeArray);
|
@@ -1,34 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../../components/ha-checkbox";
|
||||
import "../../../../components/ha-formfield";
|
||||
|
||||
class HaCustomizeBoolean extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<ha-formfield label="[[item.description]]">
|
||||
<ha-checkbox
|
||||
disabled="[[item.secondary]]"
|
||||
checked="[[item.value]]"
|
||||
on-change="checkedChanged"
|
||||
>
|
||||
</ha-checkbox
|
||||
></ha-formfield>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
item: {
|
||||
type: Object,
|
||||
notifies: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
checkedChanged(ev) {
|
||||
this.item.value = ev.target.checked;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-customize-boolean", HaCustomizeBoolean);
|
@@ -1,40 +0,0 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../../components/ha-icon";
|
||||
|
||||
class HaCustomizeIcon extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
@apply --layout-horizontal;
|
||||
}
|
||||
.icon-image {
|
||||
border: 1px solid grey;
|
||||
padding: 8px;
|
||||
margin-right: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
<ha-icon class="icon-image" icon="[[item.value]]"></ha-icon>
|
||||
<paper-input
|
||||
disabled="[[item.secondary]]"
|
||||
label="Icon"
|
||||
value="{{item.value}}"
|
||||
>
|
||||
</paper-input>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
item: {
|
||||
type: Object,
|
||||
notifies: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
customElements.define("ha-customize-icon", HaCustomizeIcon);
|
@@ -1,45 +0,0 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
class HaCustomizeKeyValue extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
@apply --layout-horizontal;
|
||||
}
|
||||
paper-input {
|
||||
@apply --layout-flex;
|
||||
}
|
||||
.key {
|
||||
padding-right: 20px;
|
||||
}
|
||||
</style>
|
||||
<paper-input
|
||||
disabled="[[item.secondary]]"
|
||||
class="key"
|
||||
label="Attribute name"
|
||||
value="{{item.attribute}}"
|
||||
>
|
||||
</paper-input>
|
||||
<paper-input
|
||||
disabled="[[item.secondary]]"
|
||||
label="Attribute value"
|
||||
value="{{item.value}}"
|
||||
>
|
||||
</paper-input>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
item: {
|
||||
type: Object,
|
||||
notifies: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
customElements.define("ha-customize-key-value", HaCustomizeKeyValue);
|
@@ -1,35 +0,0 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { formatAttributeName } from "../../../../util/hass-attributes-util";
|
||||
|
||||
class HaCustomizeString extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<paper-input
|
||||
disabled="[[item.secondary]]"
|
||||
label="[[getLabel(item)]]"
|
||||
value="{{item.value}}"
|
||||
>
|
||||
</paper-input>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
item: {
|
||||
type: Object,
|
||||
notifies: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getLabel(item) {
|
||||
return (
|
||||
formatAttributeName(item.description) +
|
||||
(item.type === "json" ? " (JSON formatted)" : "")
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-customize-string", HaCustomizeString);
|
@@ -1,3 +1,4 @@
|
||||
import "./ha-config-updates";
|
||||
import { mdiCloudLock } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
@@ -14,6 +15,7 @@ import { HomeAssistant } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-config-navigation";
|
||||
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
|
||||
|
||||
@customElement("ha-config-dashboard")
|
||||
class HaConfigDashboard extends LitElement {
|
||||
@@ -26,67 +28,11 @@ class HaConfigDashboard extends LitElement {
|
||||
|
||||
@property() public cloudStatus?: CloudStatus;
|
||||
|
||||
@property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
|
||||
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const content = html` <ha-config-section
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
>
|
||||
<div slot="header">${this.hass.localize("ui.panel.config.header")}</div>
|
||||
|
||||
<div slot="introduction">
|
||||
${this.hass.localize("ui.panel.config.introduction")}
|
||||
</div>
|
||||
|
||||
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
|
||||
? html`
|
||||
<ha-card>
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${[
|
||||
{
|
||||
component: "cloud",
|
||||
path: "/config/cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
info: this.cloudStatus,
|
||||
iconPath: mdiCloudLock,
|
||||
},
|
||||
]}
|
||||
></ha-config-navigation>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
${Object.values(configSections).map(
|
||||
(section) => html`
|
||||
<ha-card>
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${section}
|
||||
></ha-config-navigation>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
${!this.showAdvanced
|
||||
? html`
|
||||
<div class="promo-advanced">
|
||||
${this.hass.localize("ui.panel.config.advanced_mode.hint_enable")}
|
||||
<a href="/profile"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.advanced_mode.link_profile_page"
|
||||
)}</a
|
||||
>.
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-config-section>`;
|
||||
|
||||
if (!this.narrow && this.hass.dockedSidebar !== "always_hidden") {
|
||||
return content;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-app-layout>
|
||||
<app-header fixed slot="header">
|
||||
@@ -95,10 +41,72 @@ class HaConfigDashboard extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
<div main-title>${this.hass.localize("panel.config")}</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
${content}
|
||||
<ha-config-section
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
full-width
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio") &&
|
||||
this.supervisorUpdates === undefined
|
||||
? html``
|
||||
: html`${this.supervisorUpdates !== null
|
||||
? html`<ha-card>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.supervisorUpdates=${this.supervisorUpdates}
|
||||
></ha-config-updates>
|
||||
</ha-card>`
|
||||
: ""}
|
||||
<ha-card>
|
||||
${this.narrow && this.supervisorUpdates !== null
|
||||
? html`<div class="title">
|
||||
${this.hass.localize("panel.config")}
|
||||
</div>`
|
||||
: ""}
|
||||
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
|
||||
? html`
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${[
|
||||
{
|
||||
component: "cloud",
|
||||
path: "/config/cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
info: this.cloudStatus,
|
||||
iconPath: mdiCloudLock,
|
||||
iconColor: "#3B808E",
|
||||
},
|
||||
]}
|
||||
></ha-config-navigation>
|
||||
`
|
||||
: ""}
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${configSections.dashboard}
|
||||
></ha-config-navigation>
|
||||
</ha-card>
|
||||
${!this.showAdvanced
|
||||
? html`
|
||||
<div class="promo-advanced">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.advanced_mode.hint_enable"
|
||||
)}
|
||||
<a href="/profile"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.advanced_mode.link_profile_page"
|
||||
)}</a
|
||||
>.
|
||||
</div>
|
||||
`
|
||||
: ""}`}
|
||||
</ha-config-section>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
@@ -108,16 +116,16 @@ class HaConfigDashboard extends LitElement {
|
||||
haStyle,
|
||||
css`
|
||||
app-header {
|
||||
--app-header-background-color: var(--primary-background-color);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
--header-height: 55px;
|
||||
}
|
||||
ha-card:last-child {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
ha-config-section {
|
||||
margin-top: -12px;
|
||||
}
|
||||
:host([narrow]) ha-config-section {
|
||||
margin-top: -20px;
|
||||
margin: auto;
|
||||
margin-top: -32px;
|
||||
max-width: 600px;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
@@ -126,6 +134,11 @@ class HaConfigDashboard extends LitElement {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.promo-advanced {
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
@@ -134,6 +147,14 @@ class HaConfigDashboard extends LitElement {
|
||||
.promo-advanced a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
:host([narrow]) ha-card {
|
||||
background-color: var(--primary-background-color);
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
:host([narrow]) ha-config-section {
|
||||
margin-top: -42px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -24,10 +24,13 @@ class HaConfigNavigation extends LitElement {
|
||||
? html`
|
||||
<a href=${page.path} aria-role="option" tabindex="-1">
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon
|
||||
.path=${page.iconPath}
|
||||
<div
|
||||
class=${page.iconColor ? "icon-background" : ""}
|
||||
slot="item-icon"
|
||||
></ha-svg-icon>
|
||||
.style="background-color: ${page.iconColor || "undefined"}"
|
||||
>
|
||||
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
|
||||
</div>
|
||||
<paper-item-body two-line>
|
||||
${page.name ||
|
||||
this.hass.localize(
|
||||
@@ -54,7 +57,8 @@ class HaConfigNavigation extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div secondary>
|
||||
${this.hass.localize(
|
||||
${page.description ||
|
||||
this.hass.localize(
|
||||
`ui.panel.config.${page.component}.description`
|
||||
)}
|
||||
</div>
|
||||
@@ -81,6 +85,11 @@ class HaConfigNavigation extends LitElement {
|
||||
ha-svg-icon,
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
padding: 8px;
|
||||
}
|
||||
.iron-selected paper-item::before,
|
||||
a:not(.iron-selected):focus::before {
|
||||
@@ -102,6 +111,12 @@ class HaConfigNavigation extends LitElement {
|
||||
.iron-selected:focus paper-item::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
.icon-background {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.icon-background ha-svg-icon {
|
||||
color: #fff;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
142
src/panels/config/dashboard/ha-config-updates.ts
Normal file
142
src/panels/config/dashboard/ha-config-updates.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiPackageVariant } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-logo-svg";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
|
||||
import { buttonLinkStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
export const SUPERVISOR_UPDATE_NAMES = {
|
||||
core: "Home Assistant Core",
|
||||
os: "Home Assistant Operating System",
|
||||
supervisor: "Home Assistant Supervisor",
|
||||
};
|
||||
|
||||
@customElement("ha-config-updates")
|
||||
class HaConfigUpdates extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
|
||||
|
||||
@state() private _showAll = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.supervisorUpdates) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const updates =
|
||||
this._showAll || this.supervisorUpdates.length <= 3
|
||||
? this.supervisorUpdates
|
||||
: this.supervisorUpdates.slice(0, 2);
|
||||
|
||||
return html`
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: this.supervisorUpdates.length,
|
||||
})}
|
||||
</div>
|
||||
${updates.map(
|
||||
(update) => html`
|
||||
<paper-icon-item>
|
||||
<span slot="item-icon" class="icon">
|
||||
${update.update_type === "addon"
|
||||
? update.icon
|
||||
? html`<img src="/api/hassio${update.icon}" />`
|
||||
: html`<ha-svg-icon .path=${mdiPackageVariant}></ha-svg-icon>`
|
||||
: html`<ha-logo-svg></ha-logo-svg>`}
|
||||
</span>
|
||||
<paper-item-body two-line>
|
||||
${update.update_type === "addon"
|
||||
? update.name
|
||||
: SUPERVISOR_UPDATE_NAMES[update.update_type!]}
|
||||
<div secondary>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.version_available",
|
||||
{
|
||||
version_available: update.version_latest,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
<a href="/hassio${update.panel_path}">
|
||||
<mwc-button
|
||||
.label=${this.hass.localize("ui.panel.config.updates.show")}
|
||||
>
|
||||
</mwc-button>
|
||||
</a>
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
${!this._showAll && !this.narrow ? html`<div class="divider"></div>` : ""}
|
||||
${!this._showAll && this.supervisorUpdates.length >= 4
|
||||
? html`
|
||||
<button class="link show-all" @click=${this._showAllClicked}>
|
||||
${this.hass.localize("ui.panel.config.updates.show_all_updates")}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
private _showAllClicked() {
|
||||
this._showAll = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup[] {
|
||||
return [
|
||||
buttonLinkStyle,
|
||||
css`
|
||||
.title {
|
||||
font-size: 16px;
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
img,
|
||||
ha-svg-icon,
|
||||
ha-logo-svg {
|
||||
--mdc-icon-size: 32px;
|
||||
max-height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
ha-logo-svg {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
button.show-all {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
margin: 8px 16px;
|
||||
}
|
||||
.divider::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-updates": HaConfigUpdates;
|
||||
}
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-chip";
|
||||
import "../../../../components/ha-chip-set";
|
||||
import { showAutomationEditor } from "../../../../data/automation";
|
||||
import {
|
||||
@@ -10,6 +12,12 @@ import {
|
||||
import { showScriptEditor } from "../../../../data/script";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"entry-selected": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class HaDeviceAutomationCard<
|
||||
T extends DeviceAutomation
|
||||
> extends LitElement {
|
||||
@@ -55,29 +63,34 @@ export abstract class HaDeviceAutomationCard<
|
||||
return html`
|
||||
<h3>${this.hass.localize(this.headerKey)}</h3>
|
||||
<div class="content">
|
||||
<ha-chip-set
|
||||
@chip-clicked=${this._handleAutomationClicked}
|
||||
.items=${this.automations.map((automation) =>
|
||||
this._localizeDeviceAutomation(this.hass, automation)
|
||||
<ha-chip-set>
|
||||
${this.automations.map(
|
||||
(automation, idx) =>
|
||||
html`
|
||||
<ha-chip .index=${idx} @click=${this._handleAutomationClicked}>
|
||||
${this._localizeDeviceAutomation(this.hass, automation)}
|
||||
</ha-chip>
|
||||
`
|
||||
)}
|
||||
>
|
||||
</ha-chip-set>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAutomationClicked(ev: CustomEvent) {
|
||||
const automation = this.automations[ev.detail.index];
|
||||
const automation = this.automations[(ev.currentTarget as any).index];
|
||||
if (!automation) {
|
||||
return;
|
||||
}
|
||||
if (this.script) {
|
||||
showScriptEditor({ sequence: [automation as DeviceAction] });
|
||||
fireEvent(this, "entry-selected");
|
||||
return;
|
||||
}
|
||||
const data = {};
|
||||
data[this.type] = [automation];
|
||||
showAutomationEditor(data);
|
||||
fireEvent(this, "entry-selected");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -91,7 +91,7 @@ export class DialogDeviceAutomation extends LitElement {
|
||||
}.create`
|
||||
)}
|
||||
>
|
||||
<div @chip-clicked=${this.closeDialog}>
|
||||
<div @entry-selected=${this.closeDialog}>
|
||||
${this._triggers.length ||
|
||||
this._conditions.length ||
|
||||
this._actions.length
|
||||
|
@@ -82,9 +82,9 @@ export class HaDeviceCard extends LitElement {
|
||||
const device = devices.find((dev) => dev.id === deviceId);
|
||||
return device
|
||||
? computeDeviceName(device, this.hass)
|
||||
: `(${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device_unavailable"
|
||||
)})`;
|
||||
: `<${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.unknown_via_device"
|
||||
)}>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -218,7 +218,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.tabs=${configSections.integrations}
|
||||
.tabs=${configSections.devices}
|
||||
.route=${this.route}
|
||||
>
|
||||
${
|
||||
|
@@ -375,7 +375,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
.tabs=${configSections.integrations}
|
||||
.tabs=${configSections.devices}
|
||||
.route=${this.route}
|
||||
.activeFilters=${activeFilters}
|
||||
.numHidden=${this._numHiddenDevices}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
@@ -9,11 +10,10 @@ import {
|
||||
getEnergyPreferences,
|
||||
} from "../../../data/energy";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "../../../components/ha-alert";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./components/ha-energy-device-settings";
|
||||
import "./components/ha-energy-grid-settings";
|
||||
import "./components/ha-energy-solar-settings";
|
||||
@@ -68,14 +68,13 @@ class HaConfigEnergy extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.experiences}
|
||||
.header=${this.hass.localize("ui.panel.config.energy.caption")}
|
||||
>
|
||||
<ha-alert>
|
||||
${this.hass.localize("ui.panel.config.energy.new_device_info")}
|
||||
@@ -113,7 +112,7 @@ class HaConfigEnergy extends LitElement {
|
||||
@value-changed=${this._prefsChanged}
|
||||
></ha-energy-device-settings>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -165,21 +165,6 @@ export class DialogEntityEditor extends LitElement {
|
||||
>${this.hass.localize("ui.dialogs.entity_registry.faq")}</a
|
||||
>`
|
||||
)}
|
||||
${this.hass.userData?.showAdvanced
|
||||
? html`<br /><br />
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.info_customize",
|
||||
"customize_link",
|
||||
html`<a
|
||||
href=${"/config/customize/edit/" +
|
||||
this._params!.entity_id}
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.customize_link"
|
||||
)}</a
|
||||
>`
|
||||
)}`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
case "tab-related":
|
||||
|
@@ -376,9 +376,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
result.push({
|
||||
...entry,
|
||||
entity,
|
||||
name:
|
||||
computeEntityRegistryName(this.hass!, entry) ||
|
||||
this.hass.localize("state.default.unavailable"),
|
||||
name: computeEntityRegistryName(this.hass!, entry),
|
||||
unavailable,
|
||||
restored,
|
||||
area: area ? area.name : undefined,
|
||||
@@ -480,7 +478,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
? undefined
|
||||
: "/config"}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.integrations}
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user