mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-13 03:09:26 +00:00
Compare commits
163 Commits
20211020.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 | ||
![]() |
fcdceba09d | ||
![]() |
06d4ccf344 | ||
![]() |
a268040ae7 | ||
![]() |
67d79d618a | ||
![]() |
0e8a06e24d | ||
![]() |
d7732ee850 | ||
![]() |
729a928cfe | ||
![]() |
fe5a582a74 | ||
![]() |
c26a59d805 | ||
![]() |
ea331dbe0b | ||
![]() |
b97d6d7059 | ||
![]() |
9425b943dd | ||
![]() |
3fd0becfd4 | ||
![]() |
12ef191a0f | ||
![]() |
2bbb4acf3d | ||
![]() |
77d54df007 | ||
![]() |
1c35571ef0 | ||
![]() |
c8804160bf | ||
![]() |
0a6ffb6bc8 | ||
![]() |
6984f19aa0 | ||
![]() |
cb8de53d74 | ||
![]() |
93680b9764 | ||
![]() |
3cf9b745b5 | ||
![]() |
5851fe26ff | ||
![]() |
b188c4ec81 | ||
![]() |
4624c3d75b | ||
![]() |
7d196b4b95 | ||
![]() |
6347e44d94 | ||
![]() |
719d9386c5 | ||
![]() |
bb734be4bc | ||
![]() |
7cadaf1dc3 | ||
![]() |
c30453a86f | ||
![]() |
c2e3d0188e | ||
![]() |
aabb8ea16f | ||
![]() |
df572d59c5 | ||
![]() |
5ef7a37c20 | ||
![]() |
4b44e197ae | ||
![]() |
8b5b21ae69 | ||
![]() |
f5417fad6f | ||
![]() |
7fa6317f5c | ||
![]() |
74533cebc6 | ||
![]() |
10986db7c6 | ||
![]() |
67648baca7 | ||
![]() |
dc9182e9ab | ||
![]() |
4a7a81ffdb | ||
![]() |
09ef72647e | ||
![]() |
da38e6f986 | ||
![]() |
bd1a9f2cb0 | ||
![]() |
171eddd779 | ||
![]() |
7acc2f9e08 | ||
![]() |
27a6341137 | ||
![]() |
6c5e15e707 | ||
![]() |
06b1718ade | ||
![]() |
e50d2e16a7 | ||
![]() |
0b2404a0f2 | ||
![]() |
371804591d | ||
![]() |
54c64c15f3 | ||
![]() |
0e1124cd4f | ||
![]() |
70fd759e18 | ||
![]() |
8e383b2bec | ||
![]() |
63cd576d56 | ||
![]() |
32ac04ea78 | ||
![]() |
5d6bacb0bd | ||
![]() |
398d777681 | ||
![]() |
549a360d98 | ||
![]() |
1140e6026c | ||
![]() |
29a1167782 | ||
![]() |
d61a77f2d9 | ||
![]() |
b9bde1960b | ||
![]() |
a12c2eea5d | ||
![]() |
b5c717a559 | ||
![]() |
3adbc4cfaf | ||
![]() |
dd11fb1b99 | ||
![]() |
bf0d102c86 | ||
![]() |
dad2b92d2e | ||
![]() |
d027ec0018 | ||
![]() |
0c038398aa | ||
![]() |
5c3e0cc016 | ||
![]() |
9bcd26ce57 | ||
![]() |
3e8a6c418c | ||
![]() |
279f3e1183 | ||
![]() |
f77339ad85 | ||
![]() |
da73b316ff | ||
![]() |
82a49d2cbf | ||
![]() |
05711b4636 | ||
![]() |
2c2809573f | ||
![]() |
bbbeafcc92 | ||
![]() |
95c6adc739 | ||
![]() |
7c2e0aea92 | ||
![]() |
d05c76356f | ||
![]() |
cddf6ce1f4 | ||
![]() |
8abb212ae7 | ||
![]() |
0056d75127 | ||
![]() |
5be475ea17 | ||
![]() |
b157cf5294 | ||
![]() |
48c9c89e3d | ||
![]() |
80bbc9990a | ||
![]() |
736e117eca | ||
![]() |
5e52bd905d | ||
![]() |
31b69147f4 | ||
![]() |
bc5010a953 | ||
![]() |
49947f3337 | ||
![]() |
d3ce4af541 | ||
![]() |
d45f47d908 |
12
.yarn/patches/@material/mwc-icon-button/remove-icon.patch
Normal file
12
.yarn/patches/@material/mwc-icon-button/remove-icon.patch
Normal file
@@ -0,0 +1,12 @@
|
||||
diff --git a/mwc-icon-button-base.js b/mwc-icon-button-base.js
|
||||
index 45cdaab93ccc0a6daaaaabc01266dcdc32e46bfd..b3ea5b541597308d85f86ce6c23fd00785fda835 100644
|
||||
--- a/mwc-icon-button-base.js
|
||||
+++ b/mwc-icon-button-base.js
|
||||
@@ -63,7 +63,6 @@ export class IconButtonBase extends LitElement {
|
||||
@touchend="${this.handleRippleDeactivate}"
|
||||
@touchcancel="${this.handleRippleDeactivate}"
|
||||
>${this.renderRipple()}
|
||||
- <i class="material-icons">${this.icon}</i>
|
||||
<span
|
||||
><slot></slot
|
||||
></span>
|
@@ -165,6 +165,7 @@ module.exports.config = {
|
||||
cast({ isProdBuild, latestBuild }) {
|
||||
const entry = {
|
||||
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
|
||||
media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"),
|
||||
};
|
||||
|
||||
if (latestBuild) {
|
||||
|
@@ -154,6 +154,15 @@ gulp.task("gen-index-cast-dev", (done) => {
|
||||
contentReceiver
|
||||
);
|
||||
|
||||
const contentMedia = renderCastTemplate("media", {
|
||||
latestMediaJS: "/frontend_latest/media.js",
|
||||
es5MediaJS: "/frontend_es5/media.js",
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "media.html"),
|
||||
contentMedia
|
||||
);
|
||||
|
||||
const contentFAQ = renderCastTemplate("launcher-faq", {
|
||||
latestLauncherJS: "/frontend_latest/launcher.js",
|
||||
es5LauncherJS: "/frontend_es5/launcher.js",
|
||||
@@ -192,6 +201,15 @@ gulp.task("gen-index-cast-prod", (done) => {
|
||||
contentReceiver
|
||||
);
|
||||
|
||||
const contentMedia = renderCastTemplate("media", {
|
||||
latestMediaJS: latestManifest["media.js"],
|
||||
es5MediaJS: es5Manifest["media.js"],
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "media.html"),
|
||||
contentMedia
|
||||
);
|
||||
|
||||
const contentFAQ = renderCastTemplate("launcher-faq", {
|
||||
latestLauncherJS: latestManifest["launcher.js"],
|
||||
es5LauncherJS: es5Manifest["launcher.js"],
|
||||
|
@@ -26,6 +26,7 @@ const getMeta = () => {
|
||||
path: svg.match(/ d="([^"]+)"/)[1],
|
||||
name: icon.name,
|
||||
tags: icon.tags,
|
||||
aliases: icon.aliases,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -37,6 +38,7 @@ const addRemovedMeta = (meta) => {
|
||||
path: removeIcon.path,
|
||||
name: removeIcon.name,
|
||||
tags: [],
|
||||
aliases: [],
|
||||
}));
|
||||
const combinedMeta = [...meta, ...removedMeta];
|
||||
return combinedMeta.sort((a, b) => a.name.localeCompare(b.name));
|
||||
@@ -99,6 +101,7 @@ const findDifferentiator = (curString, prevString) => {
|
||||
|
||||
gulp.task("gen-icons-json", (done) => {
|
||||
const meta = getMeta();
|
||||
|
||||
const metaAndRemoved = addRemovedMeta(meta);
|
||||
const split = splitBySize(metaAndRemoved);
|
||||
|
||||
@@ -138,11 +141,17 @@ gulp.task("gen-icons-json", (done) => {
|
||||
JSON.stringify({ version: package.version, parts })
|
||||
);
|
||||
|
||||
const orderedMeta = orderMeta(meta);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(OUTPUT_DIR, "iconList.json"),
|
||||
JSON.stringify(orderedMeta.map((icon) => icon.name))
|
||||
JSON.stringify(
|
||||
orderMeta(meta).map((icon) => ({
|
||||
name: icon.name,
|
||||
keywords: [
|
||||
...icon.tags.map((t) => t.toLowerCase().replace(/\s\/\s/g, " ")),
|
||||
...icon.aliases,
|
||||
],
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
done();
|
||||
|
File diff suppressed because one or more lines are too long
46
cast/src/html/media.html.template
Normal file
46
cast/src/html/media.html.template
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
|
||||
<style>
|
||||
body {
|
||||
--logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
|
||||
--logo-repeat: no-repeat;
|
||||
--playback-logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
|
||||
--theme-hue: 200;
|
||||
--progress-color: #03a9f4;
|
||||
--splash-image: url('https://home-assistant.io/images/cast/splash.png');
|
||||
--splash-size: cover;
|
||||
--background-color: #41bdf5;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
|
||||
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
||||
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
|
||||
s.parentNode.insertBefore(g,s)}(document,'script'));
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<cast-media-player></cast-media-player>
|
||||
|
||||
<script>
|
||||
import("<%= latestMediaJS %>");
|
||||
window.latestJS = true;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
if (!window.latestJS) {
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
System.import("<%= es5MediaJS %>");
|
||||
};
|
||||
<% } else { %>
|
||||
_ls("<%= es5MediaJS %>");
|
||||
<% } %>
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
22
cast/src/media/entrypoint.ts
Normal file
22
cast/src/media/entrypoint.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const castContext = cast.framework.CastReceiverContext.getInstance();
|
||||
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
cast.framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = cast.framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
media.hlsVideoSegmentFormat =
|
||||
cast.framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
);
|
||||
|
||||
castContext.start();
|
@@ -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,6 +31,31 @@ const setTouchControlsVisibility = (visible: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
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://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 = 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 = () => {
|
||||
mediaPlayer.style.display = "none";
|
||||
lovelaceController.style.display = "initial";
|
||||
@@ -51,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);
|
||||
@@ -63,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
|
||||
@@ -103,6 +116,12 @@ const playerManager = castContext.getPlayerManager();
|
||||
playerManager.setMessageInterceptor(
|
||||
cast.framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png"
|
||||
) {
|
||||
return loadRequestData;
|
||||
}
|
||||
// We received a play media command, hide Lovelace and show media player
|
||||
showMediaPlayer();
|
||||
const media = loadRequestData.media;
|
||||
|
@@ -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
|
||||
@@ -107,6 +113,7 @@ export class HcMain extends HassElement {
|
||||
this._sendStatus();
|
||||
}
|
||||
});
|
||||
this.addEventListener("dialog-closed", this._dialogClosed);
|
||||
}
|
||||
|
||||
private _sendStatus(senderId?: string) {
|
||||
@@ -118,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;
|
||||
}
|
||||
|
||||
@@ -131,6 +138,30 @@ 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");
|
||||
};
|
||||
|
||||
private async _handleGetStatusMessage(msg: GetStatusMessage) {
|
||||
this._sendStatus(msg.senderId!);
|
||||
}
|
||||
@@ -149,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) {
|
||||
@@ -168,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 {
|
||||
@@ -194,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();
|
||||
@@ -210,8 +258,6 @@ export class HcMain extends HassElement {
|
||||
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
|
||||
}
|
||||
}
|
||||
this._showDemo = false;
|
||||
this._lovelacePath = msg.viewPath;
|
||||
|
||||
this._sendStatus();
|
||||
}
|
||||
@@ -232,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 |
91
gallery/src/demos/demo-automation-editor-action.ts
Normal file
91
gallery/src/demos/demo-automation-editor-action.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "../components/demo-black-white-row";
|
||||
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
|
||||
import "../../../src/panels/config/automation/action/ha-automation-action";
|
||||
import { HaChooseAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-choose";
|
||||
import { HaDelayAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-delay";
|
||||
import { HaDeviceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-device_id";
|
||||
import { HaEventAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-event";
|
||||
import { HaRepeatAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
|
||||
import { HaSceneAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-scene";
|
||||
import { HaServiceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-service";
|
||||
import { HaWaitForTriggerAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger";
|
||||
import { HaWaitAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
|
||||
import { Action } from "../../../src/data/script";
|
||||
import { HaConditionAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-condition";
|
||||
|
||||
const SCHEMAS: { name: string; actions: Action[] }[] = [
|
||||
{ name: "Event", actions: [HaEventAction.defaultConfig] },
|
||||
{ name: "Device", actions: [HaDeviceAction.defaultConfig] },
|
||||
{ name: "Service", actions: [HaServiceAction.defaultConfig] },
|
||||
{ name: "Condition", actions: [HaConditionAction.defaultConfig] },
|
||||
{ name: "Delay", actions: [HaDelayAction.defaultConfig] },
|
||||
{ name: "Scene", actions: [HaSceneAction.defaultConfig] },
|
||||
{ name: "Wait", actions: [HaWaitAction.defaultConfig] },
|
||||
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
|
||||
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] },
|
||||
{ name: "Choose", actions: [HaChooseAction.defaultConfig] },
|
||||
{ name: "Variables", actions: [{ variables: { hello: "1" } }] },
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-action")
|
||||
class DemoHaAutomationEditorAction extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
private data: any = SCHEMAS.map((info) => info.actions);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valueChanged = (ev) => {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
${SCHEMAS.map(
|
||||
(info, sampleIdx) => html`
|
||||
<demo-black-white-row
|
||||
.title=${info.name}
|
||||
.value=${this.data[sampleIdx]}
|
||||
>
|
||||
${["light", "dark"].map(
|
||||
(slot) =>
|
||||
html`
|
||||
<ha-automation-action
|
||||
slot=${slot}
|
||||
.hass=${this.hass}
|
||||
.actions=${this.data[sampleIdx]}
|
||||
.sampleIdx=${sampleIdx}
|
||||
@value-changed=${valueChanged}
|
||||
></ha-automation-action>
|
||||
`
|
||||
)}
|
||||
</demo-black-white-row>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-ha-automation-editor-action": DemoHaAutomationEditorAction;
|
||||
}
|
||||
}
|
127
gallery/src/demos/demo-automation-editor-condition.ts
Normal file
127
gallery/src/demos/demo-automation-editor-condition.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "../components/demo-black-white-row";
|
||||
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
|
||||
import type { Condition } from "../../../src/data/automation";
|
||||
import "../../../src/panels/config/automation/condition/ha-automation-condition";
|
||||
import { HaDeviceCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
|
||||
import { HaLogicalCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
|
||||
import HaNumericStateCondition from "../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
|
||||
import { HaStateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
|
||||
import { HaSunCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
|
||||
import { HaTemplateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-template";
|
||||
import { HaTimeCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
|
||||
import { HaTriggerCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
|
||||
import { HaZoneCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
|
||||
|
||||
const SCHEMAS: { name: string; conditions: Condition[] }[] = [
|
||||
{
|
||||
name: "State",
|
||||
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Numeric State",
|
||||
conditions: [
|
||||
{ condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Sun",
|
||||
conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Zone",
|
||||
conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Time",
|
||||
conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Template",
|
||||
conditions: [
|
||||
{ condition: "template", ...HaTemplateCondition.defaultConfig },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Device",
|
||||
conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "And",
|
||||
conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Or",
|
||||
conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Not",
|
||||
conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Trigger",
|
||||
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-condition")
|
||||
class DemoHaAutomationEditorCondition extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
private data: any = SCHEMAS.map((info) => info.conditions);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valueChanged = (ev) => {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
${SCHEMAS.map(
|
||||
(info, sampleIdx) => html`
|
||||
<demo-black-white-row
|
||||
.title=${info.name}
|
||||
.value=${this.data[sampleIdx]}
|
||||
>
|
||||
${["light", "dark"].map(
|
||||
(slot) =>
|
||||
html`
|
||||
<ha-automation-condition
|
||||
slot=${slot}
|
||||
.hass=${this.hass}
|
||||
.conditions=${this.data[sampleIdx]}
|
||||
.sampleIdx=${sampleIdx}
|
||||
@value-changed=${valueChanged}
|
||||
></ha-automation-condition>
|
||||
`
|
||||
)}
|
||||
</demo-black-white-row>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-ha-automation-editor-condition": DemoHaAutomationEditorCondition;
|
||||
}
|
||||
}
|
159
gallery/src/demos/demo-automation-editor-trigger.ts
Normal file
159
gallery/src/demos/demo-automation-editor-trigger.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "../components/demo-black-white-row";
|
||||
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
|
||||
import type { Trigger } from "../../../src/data/automation";
|
||||
import { HaGeolocationTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
|
||||
import { HaEventTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
|
||||
import { HaHassTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
|
||||
import { HaNumericStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
|
||||
import { HaSunTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-sun";
|
||||
import { HaTagTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-tag";
|
||||
import { HaTemplateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-template";
|
||||
import { HaTimeTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time";
|
||||
import { HaTimePatternTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern";
|
||||
import { HaWebhookTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-webhook";
|
||||
import { HaZoneTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-zone";
|
||||
import { HaDeviceTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-device";
|
||||
import { HaStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
|
||||
import { HaMQTTTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
||||
import "../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||
|
||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
{
|
||||
name: "State",
|
||||
triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "MQTT",
|
||||
triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "GeoLocation",
|
||||
triggers: [
|
||||
{ platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Home Assistant",
|
||||
triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Numeric State",
|
||||
triggers: [
|
||||
{ platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Sun",
|
||||
triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Time Pattern",
|
||||
triggers: [
|
||||
{ platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Webhook",
|
||||
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Zone",
|
||||
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Tag",
|
||||
triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Time",
|
||||
triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Template",
|
||||
triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Event",
|
||||
triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Device Trigger",
|
||||
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-trigger")
|
||||
class DemoHaAutomationEditorTrigger extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
private data: any = SCHEMAS.map((info) => info.triggers);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valueChanged = (ev) => {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
${SCHEMAS.map(
|
||||
(info, sampleIdx) => html`
|
||||
<demo-black-white-row
|
||||
.title=${info.name}
|
||||
.value=${this.data[sampleIdx]}
|
||||
>
|
||||
${["light", "dark"].map(
|
||||
(slot) =>
|
||||
html`
|
||||
<ha-automation-trigger
|
||||
slot=${slot}
|
||||
.hass=${this.hass}
|
||||
.triggers=${this.data[sampleIdx]}
|
||||
.sampleIdx=${sampleIdx}
|
||||
@value-changed=${valueChanged}
|
||||
></ha-automation-trigger>
|
||||
`
|
||||
)}
|
||||
</demo-black-white-row>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-ha-automation-editor-trigger": DemoHaAutomationEditorTrigger;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -222,6 +222,30 @@ const SCHEMAS: {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "OctoPrint",
|
||||
translations: {
|
||||
username: "Username",
|
||||
host: "Host",
|
||||
port: "Port Number",
|
||||
path: "Application Path",
|
||||
ssl: "Use SSL",
|
||||
},
|
||||
schema: [
|
||||
{ type: "string", name: "username", required: true, default: "" },
|
||||
{ type: "string", name: "host", required: true, default: "" },
|
||||
{
|
||||
type: "integer",
|
||||
valueMin: 1,
|
||||
valueMax: 65535,
|
||||
name: "port",
|
||||
optional: true,
|
||||
default: 80,
|
||||
},
|
||||
{ type: "string", name: "path", optional: true, default: "/" },
|
||||
{ type: "boolean", name: "ssl", optional: true, default: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-ha-form")
|
||||
|
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,
|
||||
@@ -11,6 +10,12 @@ import {
|
||||
mdiHomeAssistant,
|
||||
mdiKey,
|
||||
mdiNetwork,
|
||||
mdiNumeric1,
|
||||
mdiNumeric2,
|
||||
mdiNumeric3,
|
||||
mdiNumeric4,
|
||||
mdiNumeric5,
|
||||
mdiNumeric6,
|
||||
mdiPound,
|
||||
mdiShield,
|
||||
} from "@mdi/js";
|
||||
@@ -25,7 +30,7 @@ import "../../../../src/components/buttons/ha-call-api-button";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-label-badge";
|
||||
import "../../../../src/components/ha-chip";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
@@ -43,7 +48,6 @@ import {
|
||||
startHassioAddon,
|
||||
stopHassioAddon,
|
||||
uninstallHassioAddon,
|
||||
updateHassioAddon,
|
||||
validateHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import {
|
||||
@@ -58,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,
|
||||
@@ -73,10 +77,21 @@ const STAGE_ICON = {
|
||||
deprecated: mdiExclamationThick,
|
||||
};
|
||||
|
||||
const RATING_ICON = {
|
||||
1: mdiNumeric1,
|
||||
2: mdiNumeric2,
|
||||
3: mdiNumeric3,
|
||||
4: mdiNumeric4,
|
||||
5: mdiNumeric5,
|
||||
6: mdiNumeric6,
|
||||
};
|
||||
|
||||
@customElement("hassio-addon-info")
|
||||
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;
|
||||
@@ -113,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
|
||||
@@ -185,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>
|
||||
`
|
||||
: ""}
|
||||
@@ -246,6 +208,163 @@ class HassioAddonInfo extends LitElement {
|
||||
>`}
|
||||
</div>
|
||||
|
||||
<div class="capabilities">
|
||||
${this.addon.stage !== "stable"
|
||||
? html` <ha-chip
|
||||
hasIcon
|
||||
class=${classMap({
|
||||
yellow: this.addon.stage === "experimental",
|
||||
red: this.addon.stage === "deprecated",
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="stage"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${STAGE_ICON[this.addon.stage]}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
`addon.dashboard.capability.stages.${this.addon.stage}`
|
||||
)}
|
||||
</ha-chip>`
|
||||
: ""}
|
||||
|
||||
<ha-chip
|
||||
hasIcon
|
||||
class=${classMap({
|
||||
green: [5, 6].includes(Number(this.addon.rating)),
|
||||
yellow: [3, 4].includes(Number(this.addon.rating)),
|
||||
red: [1, 2].includes(Number(this.addon.rating)),
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="rating"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${RATING_ICON[this.addon.rating]}>
|
||||
</ha-svg-icon>
|
||||
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.rating"
|
||||
)}
|
||||
</ha-chip>
|
||||
${this.addon.host_network
|
||||
? html`
|
||||
<ha-chip
|
||||
hasIcon
|
||||
@click=${this._showMoreInfo}
|
||||
id="host_network"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiNetwork}> </ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host"
|
||||
)}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.full_access
|
||||
? html`
|
||||
<ha-chip
|
||||
hasIcon
|
||||
@click=${this._showMoreInfo}
|
||||
id="full_access"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiChip}></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.hardware"
|
||||
)}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.homeassistant_api
|
||||
? html`
|
||||
<ha-chip
|
||||
hasIcon
|
||||
@click=${this._showMoreInfo}
|
||||
id="homeassistant_api"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiHomeAssistant}
|
||||
></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.core"
|
||||
)}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
${this._computeHassioApi
|
||||
? html`
|
||||
<ha-chip hasIcon @click=${this._showMoreInfo} id="hassio_api">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiHomeAssistant}
|
||||
></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
`addon.dashboard.capability.role.${this.addon.hassio_role}`
|
||||
) || this.addon.hassio_role}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.docker_api
|
||||
? html`
|
||||
<ha-chip hasIcon @click=${this._showMoreInfo} id="docker_api">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDocker}></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.docker"
|
||||
)}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.host_pid
|
||||
? html`
|
||||
<ha-chip hasIcon @click=${this._showMoreInfo} id="host_pid">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPound}></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host_pid"
|
||||
)}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.apparmor !== "default"
|
||||
? html`
|
||||
<ha-chip
|
||||
hasIcon
|
||||
@click=${this._showMoreInfo}
|
||||
class=${this._computeApparmorClassName}
|
||||
id="apparmor"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiShield}></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.apparmor"
|
||||
)}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.auth_api
|
||||
? html`
|
||||
<ha-chip hasIcon @click=${this._showMoreInfo} id="auth_api">
|
||||
<ha-svg-icon slot="icon" .path=${mdiKey}></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.auth"
|
||||
)}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.ingress
|
||||
? html`
|
||||
<ha-chip hasIcon @click=${this._showMoreInfo} id="ingress">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiCursorDefaultClickOutline}
|
||||
></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.ingress"
|
||||
)}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
${this.supervisor.localize(
|
||||
@@ -266,172 +385,6 @@ class HassioAddonInfo extends LitElement {
|
||||
/>
|
||||
`
|
||||
: ""}
|
||||
<div class="security">
|
||||
${this.addon.stage !== "stable"
|
||||
? html` <ha-label-badge
|
||||
class=${classMap({
|
||||
yellow: this.addon.stage === "experimental",
|
||||
red: this.addon.stage === "deprecated",
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="stage"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.stage"
|
||||
)}
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${STAGE_ICON[this.addon.stage]}
|
||||
></ha-svg-icon>
|
||||
</ha-label-badge>`
|
||||
: ""}
|
||||
|
||||
<ha-label-badge
|
||||
class=${classMap({
|
||||
green: [5, 6].includes(Number(this.addon.rating)),
|
||||
yellow: [3, 4].includes(Number(this.addon.rating)),
|
||||
red: [1, 2].includes(Number(this.addon.rating)),
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="rating"
|
||||
label="rating"
|
||||
description=""
|
||||
>
|
||||
${this.addon.rating}
|
||||
</ha-label-badge>
|
||||
${this.addon.host_network
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@click=${this._showMoreInfo}
|
||||
id="host_network"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host"
|
||||
)}
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon .path=${mdiNetwork}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.full_access
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@click=${this._showMoreInfo}
|
||||
id="full_access"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.hardware"
|
||||
)}
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon .path=${mdiChip}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.homeassistant_api
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@click=${this._showMoreInfo}
|
||||
id="homeassistant_api"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.hass"
|
||||
)}
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`
|
||||
: ""}
|
||||
${this._computeHassioApi
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@click=${this._showMoreInfo}
|
||||
id="hassio_api"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.hassio"
|
||||
)}
|
||||
.description=${this.supervisor.localize(
|
||||
`addon.dashboard.capability.role.${this.addon.hassio_role}`
|
||||
) || this.addon.hassio_role}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.docker_api
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@click=${this._showMoreInfo}
|
||||
id="docker_api"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.docker"
|
||||
)}
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDocker}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.host_pid
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@click=${this._showMoreInfo}
|
||||
id="host_pid"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host_pid"
|
||||
)}
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.apparmor
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@click=${this._showMoreInfo}
|
||||
class=${this._computeApparmorClassName}
|
||||
id="apparmor"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.apparmor"
|
||||
)}
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon .path=${mdiShield}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.auth_api
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@click=${this._showMoreInfo}
|
||||
id="auth_api"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.auth"
|
||||
)}
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon .path=${mdiKey}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.ingress
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@click=${this._showMoreInfo}
|
||||
id="ingress"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.ingress"
|
||||
)}
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiCursorDefaultClickOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
${this.addon.version
|
||||
? html`
|
||||
<div
|
||||
@@ -893,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, {
|
||||
@@ -983,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;
|
||||
@@ -1175,34 +1093,31 @@ class HassioAddonInfo extends LitElement {
|
||||
.description a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-chip {
|
||||
text-transform: capitalize;
|
||||
--ha-chip-text-color: var(--text-primary-color);
|
||||
--ha-chip-background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.red {
|
||||
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
||||
--ha-chip-background-color: var(--label-badge-red, #df4c1e);
|
||||
}
|
||||
.blue {
|
||||
--ha-label-badge-color: var(--label-badge-blue, #039be5);
|
||||
--ha-chip-background-color: var(--label-badge-blue, #039be5);
|
||||
}
|
||||
.green {
|
||||
--ha-label-badge-color: var(--label-badge-green, #0da035);
|
||||
--ha-chip-background-color: var(--label-badge-green, #0da035);
|
||||
}
|
||||
.yellow {
|
||||
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
||||
--ha-chip-background-color: var(--label-badge-yellow, #f4b400);
|
||||
}
|
||||
.security {
|
||||
.capabilities {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
}
|
||||
.security h3 {
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.security ha-label-badge {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
--ha-label-badge-padding: 8px 0 0 0;
|
||||
}
|
||||
.changelog {
|
||||
display: contents;
|
||||
}
|
||||
@@ -1241,7 +1156,21 @@ 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;
|
||||
}
|
||||
.addon-options {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
@@ -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")}
|
||||
|
@@ -16,11 +16,9 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
@customElement("hassio-upload-backup")
|
||||
export class HassioUploadBackup extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@state() public value: string | null = null;
|
||||
|
||||
@@ -43,20 +41,6 @@ export class HassioUploadBackup extends LitElement {
|
||||
private async _uploadFile(ev) {
|
||||
const file = ev.detail.files[0];
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
showAlertDialog(this, {
|
||||
title: "Backup file is too big",
|
||||
text: html`The maximum allowed filesize is 1GB.<br />
|
||||
<a
|
||||
href="https://www.home-assistant.io/hassio/haos_common_tasks/#restoring-a-backup-on-a-new-install"
|
||||
target="_blank"
|
||||
>Have a look here on how to restore it.</a
|
||||
>`,
|
||||
confirmText: "ok",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!["application/x-tar"].includes(file.type)) {
|
||||
showAlertDialog(this, {
|
||||
title: "Unsupported file format",
|
||||
|
@@ -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,
|
||||
|
@@ -15,7 +15,7 @@ export class DialogHassioBackupUpload
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupUploadDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _params?: HassioBackupUploadDialogParams;
|
||||
|
||||
@@ -54,7 +54,7 @@ export class DialogHassioBackupUpload
|
||||
<ha-header-bar>
|
||||
<span slot="title"> Upload backup </span>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.close")}
|
||||
.label=${this.hass?.localize("common.close") || "close"}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
|
@@ -35,7 +35,7 @@ class HassioBackupDialog
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@@ -77,7 +77,7 @@ class HassioBackupDialog
|
||||
<ha-header-bar>
|
||||
<span slot="title">${this._backup.name}</span>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.close")}
|
||||
.label=${this.hass?.localize("common.close") || "close"}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
@@ -114,7 +114,7 @@ class HassioBackupDialog
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.menu")}
|
||||
.label=${this.hass!.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
@@ -192,25 +192,23 @@ class HassioBackupDialog
|
||||
}
|
||||
|
||||
if (!this._dialogParams?.onboarding) {
|
||||
this.hass
|
||||
.callApi(
|
||||
"POST",
|
||||
this.hass!.callApi(
|
||||
"POST",
|
||||
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/restore/partial`,
|
||||
backupDetails
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/restore/partial`,
|
||||
backupDetails
|
||||
).then(
|
||||
() => {
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||
@@ -244,24 +242,22 @@ class HassioBackupDialog
|
||||
}
|
||||
|
||||
if (!this._dialogParams?.onboarding) {
|
||||
this.hass
|
||||
.callApi(
|
||||
"POST",
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/restore/full`,
|
||||
backupDetails
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
this.hass!.callApi(
|
||||
"POST",
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/restore/full`,
|
||||
backupDetails
|
||||
).then(
|
||||
() => {
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
|
||||
@@ -283,36 +279,33 @@ class HassioBackupDialog
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass
|
||||
|
||||
.callApi(
|
||||
atLeastVersion(this.hass.config.version, 2021, 9) ? "DELETE" : "POST",
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass.config.version, 2021, 9)
|
||||
? `backups/${this._backup!.slug}`
|
||||
: `snapshots/${this._backup!.slug}/remove`
|
||||
}`
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
if (this._dialogParams!.onDelete) {
|
||||
this._dialogParams!.onDelete();
|
||||
}
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
this.hass!.callApi(
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9) ? "DELETE" : "POST",
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? `backups/${this._backup!.slug}`
|
||||
: `snapshots/${this._backup!.slug}/remove`
|
||||
}`
|
||||
).then(
|
||||
() => {
|
||||
if (this._dialogParams!.onDelete) {
|
||||
this._dialogParams!.onDelete();
|
||||
}
|
||||
);
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _downloadClicked() {
|
||||
let signedPath: { path: string };
|
||||
try {
|
||||
signedPath = await getSignedPath(
|
||||
this.hass,
|
||||
this.hass!,
|
||||
`/api/hassio/${
|
||||
atLeastVersion(this.hass.config.version, 2021, 9)
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/download`
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDelete } from "@mdi/js";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
addHassioDockerRegistry,
|
||||
@@ -19,22 +19,41 @@ import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { RegistriesDialogParams } from "./show-dialog-registries";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
type: "string",
|
||||
name: "registry",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "username",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "password",
|
||||
required: true,
|
||||
format: "password",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("dialog-hassio-registries")
|
||||
class HassioRegistriesDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) private _registries?: {
|
||||
@state() private _registries?: {
|
||||
registry: string;
|
||||
username: string;
|
||||
}[];
|
||||
|
||||
@state() private _registry?: string;
|
||||
|
||||
@state() private _username?: string;
|
||||
|
||||
@state() private _password?: string;
|
||||
@state() private _input: {
|
||||
registry?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
} = {};
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@@ -47,6 +66,7 @@ class HassioRegistriesDialog extends LitElement {
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._addingRegistry
|
||||
@@ -54,99 +74,77 @@ class HassioRegistriesDialog extends LitElement {
|
||||
: this.supervisor.localize("dialog.registries.title_manage")
|
||||
)}
|
||||
>
|
||||
<div class="form">
|
||||
${this._addingRegistry
|
||||
? html`
|
||||
<paper-input
|
||||
@value-changed=${this._inputChanged}
|
||||
class="flex-auto"
|
||||
name="registry"
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.registries.registry"
|
||||
)}
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
<paper-input
|
||||
@value-changed=${this._inputChanged}
|
||||
class="flex-auto"
|
||||
name="username"
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.registries.username"
|
||||
)}
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
<paper-input
|
||||
@value-changed=${this._inputChanged}
|
||||
class="flex-auto"
|
||||
name="password"
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.registries.password"
|
||||
)}
|
||||
type="password"
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
|
||||
${this._addingRegistry
|
||||
? html`
|
||||
<ha-form
|
||||
.data=${this._input}
|
||||
.schema=${SCHEMA}
|
||||
@value-changed=${this._valueChanged}
|
||||
.computeLabel=${this._computeLabel}
|
||||
></ha-form>
|
||||
<div class="action">
|
||||
<mwc-button
|
||||
?disabled=${Boolean(
|
||||
!this._registry || !this._username || !this._password
|
||||
!this._input.registry ||
|
||||
!this._input.username ||
|
||||
!this._input.password
|
||||
)}
|
||||
@click=${this._addNewRegistry}
|
||||
>
|
||||
${this.supervisor.localize("dialog.registries.add_registry")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`${this._registries?.length
|
||||
? this._registries.map(
|
||||
(entry) => html`
|
||||
<mwc-list-item class="option" hasMeta twoline>
|
||||
<span>${entry.registry}</span>
|
||||
<span slot="secondary"
|
||||
>${this.supervisor.localize(
|
||||
"dialog.registries.username"
|
||||
)}:
|
||||
${entry.username}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.entry=${entry}
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.registries.remove"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
slot="meta"
|
||||
@click=${this._removeRegistry}
|
||||
></ha-icon-button>
|
||||
</mwc-list-item>
|
||||
`
|
||||
)
|
||||
: html`
|
||||
<mwc-list-item>
|
||||
<span
|
||||
>${this.supervisor.localize(
|
||||
"dialog.registries.no_registries"
|
||||
)}</span
|
||||
>
|
||||
</mwc-list-item>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: html`${this._registries?.length
|
||||
? this._registries.map(
|
||||
(entry) => html`
|
||||
<ha-settings-row class="registry">
|
||||
<span slot="heading"> ${entry.registry} </span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.username"
|
||||
)}:
|
||||
${entry.username}
|
||||
</span>
|
||||
<ha-icon-button
|
||||
.entry=${entry}
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.registries.remove"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
@click=${this._removeRegistry}
|
||||
></ha-icon-button>
|
||||
</ha-settings-row>
|
||||
`
|
||||
)
|
||||
: html`
|
||||
<ha-alert>
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.no_registries"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
<div class="action">
|
||||
<mwc-button @click=${this._addRegistry}>
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.add_new_registry"
|
||||
)}
|
||||
</mwc-button> `}
|
||||
</div>
|
||||
</mwc-button>
|
||||
</div> `}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _inputChanged(ev: Event) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
this[`_${target.name}`] = target.value;
|
||||
private _computeLabel = (schema: HaFormSchema) =>
|
||||
this.supervisor.localize(`dialog.registries.${schema.name}`) || schema.name;
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._input = ev.detail.value;
|
||||
}
|
||||
|
||||
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
|
||||
this._opened = true;
|
||||
this._input = {};
|
||||
this.supervisor = dialogParams.supervisor;
|
||||
await this._loadRegistries();
|
||||
await this.updateComplete;
|
||||
@@ -155,6 +153,7 @@ class HassioRegistriesDialog extends LitElement {
|
||||
public closeDialog(): void {
|
||||
this._addingRegistry = false;
|
||||
this._opened = false;
|
||||
this._input = {};
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
@@ -179,15 +178,16 @@ class HassioRegistriesDialog extends LitElement {
|
||||
|
||||
private async _addNewRegistry(): Promise<void> {
|
||||
const data = {};
|
||||
data[this._registry!] = {
|
||||
username: this._username,
|
||||
password: this._password,
|
||||
data[this._input.registry!] = {
|
||||
username: this._input.username,
|
||||
password: this._input.password,
|
||||
};
|
||||
|
||||
try {
|
||||
await addHassioDockerRegistry(this.hass, data);
|
||||
await this._loadRegistries();
|
||||
this._addingRegistry = false;
|
||||
this._input = {};
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("dialog.registries.failed_to_add"),
|
||||
@@ -215,32 +215,20 @@ class HassioRegistriesDialog extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog.button-left {
|
||||
--justify-action-buttons: flex-start;
|
||||
}
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.option {
|
||||
.registry {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
mwc-button {
|
||||
margin-left: 8px;
|
||||
.action {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-icon-button {
|
||||
color: var(--error-color);
|
||||
margin: -10px;
|
||||
}
|
||||
mwc-list-item {
|
||||
cursor: default;
|
||||
}
|
||||
mwc-list-item span[slot="secondary"] {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -10px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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;
|
||||
};
|
||||
|
89
package.json
89
package.json
@@ -22,23 +22,23 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^5.0.2",
|
||||
"@codemirror/commands": "^0.19.2",
|
||||
"@codemirror/gutter": "^0.19.1",
|
||||
"@codemirror/highlight": "^0.19.2",
|
||||
"@codemirror/commands": "^0.19.5",
|
||||
"@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.0",
|
||||
"@codemirror/search": "^0.19.0",
|
||||
"@codemirror/state": "^0.19.1",
|
||||
"@codemirror/stream-parser": "^0.19.1",
|
||||
"@codemirror/text": "^0.19.2",
|
||||
"@codemirror/view": "^0.19.4",
|
||||
"@formatjs/intl-datetimeformat": "^4.2.4",
|
||||
"@formatjs/intl-getcanonicallocales": "^1.7.3",
|
||||
"@formatjs/intl-locale": "^2.4.38",
|
||||
"@formatjs/intl-numberformat": "^7.2.4",
|
||||
"@formatjs/intl-pluralrules": "^4.1.4",
|
||||
"@formatjs/intl-relativetimeformat": "^9.3.1",
|
||||
"@codemirror/rectangular-selection": "^0.19.1",
|
||||
"@codemirror/search": "^0.19.2",
|
||||
"@codemirror/state": "^0.19.4",
|
||||
"@codemirror/stream-parser": "^0.19.2",
|
||||
"@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",
|
||||
"@formatjs/intl-numberformat": "^7.2.5",
|
||||
"@formatjs/intl-pluralrules": "^4.1.5",
|
||||
"@formatjs/intl-relativetimeformat": "^9.3.2",
|
||||
"@formatjs/intl-utils": "^3.8.4",
|
||||
"@fullcalendar/common": "5.9.0",
|
||||
"@fullcalendar/core": "5.9.0",
|
||||
@@ -46,29 +46,29 @@
|
||||
"@fullcalendar/interaction": "5.9.0",
|
||||
"@fullcalendar/list": "5.9.0",
|
||||
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch",
|
||||
"@material/chips": "14.0.0-canary.353ca7e9f.0",
|
||||
"@material/data-table": "14.0.0-canary.353ca7e9f.0",
|
||||
"@material/mwc-button": "0.25.2",
|
||||
"@material/mwc-checkbox": "0.25.2",
|
||||
"@material/mwc-circular-progress": "0.25.2",
|
||||
"@material/mwc-dialog": "0.25.2",
|
||||
"@material/mwc-fab": "0.25.2",
|
||||
"@material/mwc-formfield": "0.25.2",
|
||||
"@material/mwc-icon-button": "0.25.2",
|
||||
"@material/mwc-linear-progress": "0.25.2",
|
||||
"@material/mwc-list": "0.25.2",
|
||||
"@material/mwc-menu": "0.25.2",
|
||||
"@material/mwc-radio": "0.25.2",
|
||||
"@material/mwc-ripple": "0.25.2",
|
||||
"@material/mwc-select": "0.25.2",
|
||||
"@material/mwc-slider": "0.25.2",
|
||||
"@material/mwc-switch": "0.25.2",
|
||||
"@material/mwc-tab": "0.25.2",
|
||||
"@material/mwc-tab-bar": "0.25.2",
|
||||
"@material/mwc-textfield": "0.25.2",
|
||||
"@material/top-app-bar": "14.0.0-canary.353ca7e9f.0",
|
||||
"@mdi/js": "6.3.95",
|
||||
"@mdi/svg": "6.3.95",
|
||||
"@material/chips": "14.0.0-canary.261f2db59.0",
|
||||
"@material/data-table": "14.0.0-canary.261f2db59.0",
|
||||
"@material/mwc-button": "0.25.3",
|
||||
"@material/mwc-checkbox": "0.25.3",
|
||||
"@material/mwc-circular-progress": "0.25.3",
|
||||
"@material/mwc-dialog": "0.25.3",
|
||||
"@material/mwc-fab": "0.25.3",
|
||||
"@material/mwc-formfield": "0.25.3",
|
||||
"@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch",
|
||||
"@material/mwc-linear-progress": "0.25.3",
|
||||
"@material/mwc-list": "0.25.3",
|
||||
"@material/mwc-menu": "0.25.3",
|
||||
"@material/mwc-radio": "0.25.3",
|
||||
"@material/mwc-ripple": "0.25.3",
|
||||
"@material/mwc-select": "0.25.3",
|
||||
"@material/mwc-slider": "0.25.3",
|
||||
"@material/mwc-switch": "0.25.3",
|
||||
"@material/mwc-tab": "0.25.3",
|
||||
"@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.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",
|
||||
@@ -78,7 +78,6 @@
|
||||
"@polymer/paper-input": "^3.2.1",
|
||||
"@polymer/paper-item": "^3.0.1",
|
||||
"@polymer/paper-listbox": "^3.0.1",
|
||||
"@polymer/paper-ripple": "^3.0.2",
|
||||
"@polymer/paper-slider": "^3.0.1",
|
||||
"@polymer/paper-styles": "^3.0.1",
|
||||
"@polymer/paper-tabs": "^3.1.0",
|
||||
@@ -109,7 +108,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"lit": "^2.0.0",
|
||||
"lit": "^2.0.2",
|
||||
"lit-vaadin-helpers": "^0.2.1",
|
||||
"marked": "^3.0.2",
|
||||
"memoize-one": "^5.2.1",
|
||||
@@ -181,7 +180,7 @@
|
||||
"eslint-import-resolver-webpack": "^0.13.1",
|
||||
"eslint-plugin-disable": "^2.0.1",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-lit": "^1.5.1",
|
||||
"eslint-plugin-lit": "^1.6.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-unused-imports": "^1.1.5",
|
||||
"eslint-plugin-wc": "^1.3.2",
|
||||
@@ -231,10 +230,10 @@
|
||||
"resolutions": {
|
||||
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"lit": "^2.0.0",
|
||||
"lit-html": "2.0.0",
|
||||
"lit-element": "3.0.0",
|
||||
"@lit/reactive-element": "1.0.0"
|
||||
"lit": "^2.0.2",
|
||||
"lit-html": "2.0.1",
|
||||
"lit-element": "3.0.1",
|
||||
"@lit/reactive-element": "1.0.1"
|
||||
},
|
||||
"main": "src/home-assistant.js",
|
||||
"husky": {
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20211020.0",
|
||||
version="20211123.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/frontend",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import "@material/mwc-button";
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -7,18 +8,20 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import "./ha-password-manager-polyfill";
|
||||
import { property, state } from "lit/decorators";
|
||||
import "../components/ha-form/ha-form";
|
||||
import "../components/ha-markdown";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-checkbox";
|
||||
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
|
||||
import "../components/ha-form/ha-form";
|
||||
import "../components/ha-formfield";
|
||||
import "../components/ha-markdown";
|
||||
import { AuthProvider } from "../data/auth";
|
||||
import {
|
||||
DataEntryFlowStep,
|
||||
DataEntryFlowStepForm,
|
||||
} from "../data/data_entry_flow";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
|
||||
import "./ha-password-manager-polyfill";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
@@ -41,6 +44,8 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _storeToken = false;
|
||||
|
||||
willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
@@ -201,12 +206,30 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
.computeError=${this._computeErrorCallback(step)}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-form>
|
||||
${this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
<ha-formfield
|
||||
class="store-token"
|
||||
.label=${this.localize("ui.panel.page-authorize.store_token")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this._storeToken}
|
||||
@change=${this._storeTokenChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
default:
|
||||
return html``;
|
||||
}
|
||||
}
|
||||
|
||||
private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) {
|
||||
this._storeToken = (e.currentTarget as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
private async _providerChanged(newProvider?: AuthProvider) {
|
||||
if (this._step && this._step.type === "form") {
|
||||
fetch(`/auth/login_flow/${this._step.flow_id}`, {
|
||||
@@ -274,6 +297,9 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
if (this.oauth2State) {
|
||||
url += `&state=${encodeURIComponent(this.oauth2State)}`;
|
||||
}
|
||||
if (this._storeToken) {
|
||||
url += `&storeToken=true`;
|
||||
}
|
||||
|
||||
document.location.assign(url);
|
||||
}
|
||||
@@ -357,6 +383,11 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
margin: 24px 0 8px;
|
||||
text-align: center;
|
||||
}
|
||||
/* Align with the rest of the form. */
|
||||
.store-token {
|
||||
margin-top: 10px;
|
||||
margin-left: -16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,11 @@ class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
|
||||
<p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p>
|
||||
${this.authProviders.map(
|
||||
(provider) => html`
|
||||
<paper-item .auth_provider=${provider} @click=${this._handlePick}>
|
||||
<paper-item
|
||||
role="button"
|
||||
.auth_provider=${provider}
|
||||
@click=${this._handlePick}
|
||||
>
|
||||
<paper-item-body>${provider.name}</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
|
@@ -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;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { AuthData } from "home-assistant-js-websocket";
|
||||
import { extractSearchParam } from "../url/search-params";
|
||||
|
||||
const storage = window.localStorage || {};
|
||||
|
||||
@@ -30,6 +31,11 @@ export function askWrite() {
|
||||
|
||||
export function saveTokens(tokens: AuthData | null) {
|
||||
tokenCache.tokens = tokens;
|
||||
|
||||
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
|
||||
tokenCache.writeEnabled = true;
|
||||
}
|
||||
|
||||
if (tokenCache.writeEnabled) {
|
||||
try {
|
||||
storage.hassTokens = JSON.stringify(tokens);
|
||||
@@ -45,7 +51,6 @@ export function enableWrite() {
|
||||
saveTokens(tokenCache.tokens);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadTokens() {
|
||||
if (tokenCache.tokens === undefined) {
|
||||
try {
|
||||
|
@@ -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;
|
||||
|
@@ -76,7 +76,6 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
configurator: mdiCog,
|
||||
conversation: mdiTextToSpeech,
|
||||
counter: mdiCounter,
|
||||
device_tracker: mdiAccount,
|
||||
fan: mdiFan,
|
||||
google_assistant: mdiGoogleAssistant,
|
||||
group: mdiGoogleCirclesCommunities,
|
||||
@@ -104,7 +103,6 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
siren: mdiBullhorn,
|
||||
simple_alarm: mdiBell,
|
||||
sun: mdiWhiteBalanceSunny,
|
||||
switch: mdiFlash,
|
||||
timer: mdiTimerOutline,
|
||||
updater: mdiCloudUpload,
|
||||
vacuum: mdiRobotVacuum,
|
||||
@@ -121,6 +119,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
current: mdiCurrentAc,
|
||||
date: mdiCalendar,
|
||||
energy: mdiLightningBolt,
|
||||
frequency: mdiSineWave,
|
||||
gas: mdiGasCylinder,
|
||||
humidity: mdiWaterPercent,
|
||||
illuminance: mdiBrightness5,
|
||||
@@ -145,6 +144,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
|
||||
/** Domains that have a state card. */
|
||||
export const DOMAINS_WITH_CARD = [
|
||||
"button",
|
||||
"climate",
|
||||
"cover",
|
||||
"configurator",
|
||||
|
@@ -1,2 +1,3 @@
|
||||
/** An empty image which can be set as src of an img element. */
|
||||
export default "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
export const emptyImageBase64 =
|
||||
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
|
@@ -1,43 +1,45 @@
|
||||
import {
|
||||
mdiBattery,
|
||||
mdiBatteryOutline,
|
||||
mdiBatteryCharging,
|
||||
mdiThermometer,
|
||||
mdiSnowflake,
|
||||
mdiServerNetworkOff,
|
||||
mdiServerNetwork,
|
||||
mdiDoorClosed,
|
||||
mdiDoorOpen,
|
||||
mdiGarage,
|
||||
mdiGarageOpen,
|
||||
mdiPowerPlugOff,
|
||||
mdiPowerPlug,
|
||||
mdiCheckCircle,
|
||||
mdiAlertCircle,
|
||||
mdiSmoke,
|
||||
mdiFire,
|
||||
mdiBattery,
|
||||
mdiBatteryCharging,
|
||||
mdiBatteryOutline,
|
||||
mdiBrightness5,
|
||||
mdiBrightness7,
|
||||
mdiCheckboxMarkedCircle,
|
||||
mdiCheckNetworkOutline,
|
||||
mdiCloseNetworkOutline,
|
||||
mdiCheckCircle,
|
||||
mdiCropPortrait,
|
||||
mdiDoorClosed,
|
||||
mdiDoorOpen,
|
||||
mdiFire,
|
||||
mdiGarage,
|
||||
mdiGarageOpen,
|
||||
mdiHome,
|
||||
mdiHomeOutline,
|
||||
mdiLock,
|
||||
mdiLockOpen,
|
||||
mdiWaterOff,
|
||||
mdiWater,
|
||||
mdiWalk,
|
||||
mdiRun,
|
||||
mdiHomeOutline,
|
||||
mdiHome,
|
||||
mdiSquare,
|
||||
mdiSquareOutline,
|
||||
mdiMusicNoteOff,
|
||||
mdiMusicNote,
|
||||
mdiMusicNoteOff,
|
||||
mdiPackage,
|
||||
mdiPackageUp,
|
||||
mdiCropPortrait,
|
||||
mdiPlay,
|
||||
mdiPowerPlug,
|
||||
mdiPowerPlugOff,
|
||||
mdiRadioboxBlank,
|
||||
mdiRun,
|
||||
mdiSmoke,
|
||||
mdiSnowflake,
|
||||
mdiSquare,
|
||||
mdiSquareOutline,
|
||||
mdiStop,
|
||||
mdiThermometer,
|
||||
mdiVibrate,
|
||||
mdiWalk,
|
||||
mdiWater,
|
||||
mdiWaterOff,
|
||||
mdiWindowClosed,
|
||||
mdiWindowOpen,
|
||||
mdiRadioboxBlank,
|
||||
mdiCheckboxMarkedCircle,
|
||||
} from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
@@ -53,7 +55,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
|
||||
case "cold":
|
||||
return is_off ? mdiThermometer : mdiSnowflake;
|
||||
case "connectivity":
|
||||
return is_off ? mdiServerNetworkOff : mdiServerNetwork;
|
||||
return is_off ? mdiCloseNetworkOutline : mdiCheckNetworkOutline;
|
||||
case "door":
|
||||
return is_off ? mdiDoorClosed : mdiDoorOpen;
|
||||
case "garage_door":
|
||||
@@ -85,6 +87,8 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
|
||||
return is_off ? mdiPowerPlugOff : mdiPowerPlug;
|
||||
case "presence":
|
||||
return is_off ? mdiHomeOutline : mdiHome;
|
||||
case "running":
|
||||
return is_off ? mdiStop : mdiPlay;
|
||||
case "sound":
|
||||
return is_off ? mdiMusicNoteOff : mdiMusicNote;
|
||||
case "update":
|
||||
|
@@ -4,7 +4,7 @@ import { FrontendLocaleData } from "../../data/translation";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { formatNumber } from "../number/format_number";
|
||||
import { formatNumber, isNumericState } from "../number/format_number";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
|
||||
@@ -20,7 +20,8 @@ export const computeStateDisplay = (
|
||||
return localize(`state.default.${compareState}`);
|
||||
}
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||
if (isNumericState(stateObj)) {
|
||||
if (stateObj.attributes.device_class === "monetary") {
|
||||
try {
|
||||
return formatNumber(compareState, locale, {
|
||||
@@ -31,15 +32,17 @@ export const computeStateDisplay = (
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
return `${formatNumber(compareState, locale)} ${
|
||||
return `${formatNumber(compareState, locale)}${
|
||||
stateObj.attributes.unit_of_measurement
|
||||
? " " + stateObj.attributes.unit_of_measurement
|
||||
: ""
|
||||
}`;
|
||||
}
|
||||
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
if (domain === "input_datetime") {
|
||||
if (state) {
|
||||
if (state !== undefined) {
|
||||
// If trying to display an explicit state, need to parse the explict state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
try {
|
||||
@@ -63,7 +66,7 @@ export const computeStateDisplay = (
|
||||
}
|
||||
}
|
||||
return state;
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
// Formatting methods may throw error if date parsing doesn't go well,
|
||||
// just return the state string in that case.
|
||||
return state;
|
||||
@@ -71,7 +74,17 @@ export const computeStateDisplay = (
|
||||
} else {
|
||||
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
|
||||
let date: Date;
|
||||
if (!stateObj.attributes.has_time) {
|
||||
if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
|
||||
date = new Date(
|
||||
stateObj.attributes.year,
|
||||
stateObj.attributes.month - 1,
|
||||
stateObj.attributes.day,
|
||||
stateObj.attributes.hour,
|
||||
stateObj.attributes.minute
|
||||
);
|
||||
return formatDateTime(date, locale);
|
||||
}
|
||||
if (stateObj.attributes.has_date) {
|
||||
date = new Date(
|
||||
stateObj.attributes.year,
|
||||
stateObj.attributes.month - 1,
|
||||
@@ -79,20 +92,12 @@ export const computeStateDisplay = (
|
||||
);
|
||||
return formatDate(date, locale);
|
||||
}
|
||||
if (!stateObj.attributes.has_date) {
|
||||
if (stateObj.attributes.has_time) {
|
||||
date = new Date();
|
||||
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
|
||||
return formatTime(date, locale);
|
||||
}
|
||||
|
||||
date = new Date(
|
||||
stateObj.attributes.year,
|
||||
stateObj.attributes.month - 1,
|
||||
stateObj.attributes.day,
|
||||
stateObj.attributes.hour,
|
||||
stateObj.attributes.minute
|
||||
);
|
||||
return formatDateTime(date, locale);
|
||||
return stateObj.state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +116,14 @@ export const computeStateDisplay = (
|
||||
return formatNumber(compareState, locale);
|
||||
}
|
||||
|
||||
// state of button is a timestamp
|
||||
if (
|
||||
domain === "button" ||
|
||||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
|
||||
) {
|
||||
return formatDateTime(new Date(compareState), locale);
|
||||
}
|
||||
|
||||
return (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
|
@@ -1,6 +1,13 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiAccountArrowRight,
|
||||
mdiAirHumidifierOff,
|
||||
mdiAirHumidifier,
|
||||
mdiFlash,
|
||||
mdiBluetooth,
|
||||
mdiBluetoothConnect,
|
||||
mdiLanConnect,
|
||||
mdiLanDisconnect,
|
||||
mdiLockOpen,
|
||||
mdiLockAlert,
|
||||
mdiLockClock,
|
||||
@@ -8,8 +15,12 @@ import {
|
||||
mdiCastConnected,
|
||||
mdiCast,
|
||||
mdiEmoticonDead,
|
||||
mdiPowerPlug,
|
||||
mdiPowerPlugOff,
|
||||
mdiSleep,
|
||||
mdiTimerSand,
|
||||
mdiToggleSwitch,
|
||||
mdiToggleSwitchOff,
|
||||
mdiZWave,
|
||||
mdiClock,
|
||||
mdiCalendar,
|
||||
@@ -44,6 +55,17 @@ export const domainIcon = (
|
||||
case "cover":
|
||||
return coverIcon(compareState, stateObj);
|
||||
|
||||
case "device_tracker":
|
||||
if (stateObj?.attributes.source_type === "router") {
|
||||
return compareState === "home" ? mdiLanConnect : mdiLanDisconnect;
|
||||
}
|
||||
if (
|
||||
["bluetooth", "bluetooth_le"].includes(stateObj?.attributes.source_type)
|
||||
) {
|
||||
return compareState === "home" ? mdiBluetoothConnect : mdiBluetooth;
|
||||
}
|
||||
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
|
||||
|
||||
case "humidifier":
|
||||
return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
|
||||
|
||||
@@ -63,6 +85,16 @@ export const domainIcon = (
|
||||
case "media_player":
|
||||
return compareState === "playing" ? mdiCastConnected : mdiCast;
|
||||
|
||||
case "switch":
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "outlet":
|
||||
return state === "on" ? mdiPowerPlug : mdiPowerPlugOff;
|
||||
case "switch":
|
||||
return state === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
|
||||
default:
|
||||
return mdiFlash;
|
||||
}
|
||||
|
||||
case "zwave":
|
||||
switch (compareState) {
|
||||
case "dead":
|
||||
|
24
src/common/entity/strip_prefix_from_entity_name.ts
Normal file
24
src/common/entity/strip_prefix_from_entity_name.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Strips a device name from an entity name.
|
||||
* @param entityName the entity name
|
||||
* @param lowerCasedPrefixWithSpaceSuffix the prefix to strip, lower cased with a space suffix
|
||||
* @returns
|
||||
*/
|
||||
export const stripPrefixFromEntityName = (
|
||||
entityName: string,
|
||||
lowerCasedPrefixWithSpaceSuffix: string
|
||||
) => {
|
||||
if (!entityName.toLowerCase().startsWith(lowerCasedPrefixWithSpaceSuffix)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length);
|
||||
|
||||
// If first word already has an upper case letter (e.g. from brand name)
|
||||
// leave as-is, otherwise capitalize the first word.
|
||||
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
|
||||
? newName
|
||||
: newName[0].toUpperCase() + newName.slice(1);
|
||||
};
|
||||
|
||||
const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;
|
@@ -1,6 +1,15 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
|
||||
import { round } from "./round";
|
||||
|
||||
/**
|
||||
* Returns true if the entity is considered numeric based on the attributes it has
|
||||
* @param stateObj The entity state object
|
||||
*/
|
||||
export const isNumericState = (stateObj: HassEntity): boolean =>
|
||||
!!stateObj.attributes.unit_of_measurement ||
|
||||
!!stateObj.attributes.state_class;
|
||||
|
||||
export const numberFormatToLocale = (
|
||||
localeOptions: FrontendLocaleData
|
||||
): string | string[] | undefined => {
|
||||
|
@@ -12,8 +12,8 @@ export const slugify = (value: string, delimiter = "_") => {
|
||||
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
||||
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and'
|
||||
.replace(/[^\w-]+/g, "") // Remove all non-word characters
|
||||
.replace(/-/, delimiter) // Replace - with delimiter
|
||||
.replace(new RegExp(`/${delimiter}${delimiter}+/`, "g"), delimiter) // Replace multiple delimiters with single delimiter
|
||||
.replace(new RegExp(`/^${delimiter}+/`), "") // Trim delimiter from start of text
|
||||
.replace(new RegExp(`/-+$/`), ""); // Trim delimiter from end of text
|
||||
.replace(/-/g, delimiter) // Replace - with delimiter
|
||||
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
|
||||
.replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text
|
||||
.replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text
|
||||
};
|
||||
|
@@ -32,6 +32,7 @@ if (__BUILD__ === "latest") {
|
||||
}
|
||||
if (shouldPolyfillDateTime()) {
|
||||
polyfills.push(import("@formatjs/intl-datetimeformat/polyfill"));
|
||||
polyfills.push(import("@formatjs/intl-datetimeformat/add-all-tz"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -24,7 +24,7 @@ const BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED = new Set([
|
||||
"plug",
|
||||
"power",
|
||||
"presence",
|
||||
"update",
|
||||
"running",
|
||||
]);
|
||||
|
||||
const STATIC_STATE_COLORS = new Set([
|
||||
|
@@ -70,7 +70,7 @@ export interface DataTableSortColumnData {
|
||||
|
||||
export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
title: TemplateResult | string;
|
||||
type?: "numeric" | "icon" | "icon-button";
|
||||
type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
|
||||
template?: <T>(data: any, row: T) => TemplateResult | string;
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
@@ -281,15 +281,13 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
const sorted = key === this._sortColumn;
|
||||
const classes = {
|
||||
"mdc-data-table__header-cell--numeric": Boolean(
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__header-cell--icon": Boolean(
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__header-cell--icon-button": Boolean(
|
||||
column.type === "icon-button"
|
||||
),
|
||||
"mdc-data-table__header-cell--numeric":
|
||||
column.type === "numeric",
|
||||
"mdc-data-table__header-cell--icon": column.type === "icon",
|
||||
"mdc-data-table__header-cell--icon-button":
|
||||
column.type === "icon-button",
|
||||
"mdc-data-table__header-cell--overflow-menu":
|
||||
column.type === "overflow-menu",
|
||||
sortable: Boolean(column.sortable),
|
||||
"not-sorted": Boolean(column.sortable && !sorted),
|
||||
grows: Boolean(column.grows),
|
||||
@@ -405,14 +403,14 @@ export class HaDataTable extends LitElement {
|
||||
<div
|
||||
role="cell"
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__cell--numeric":
|
||||
column.type === "numeric",
|
||||
"mdc-data-table__cell--icon":
|
||||
column.type === "icon",
|
||||
"mdc-data-table__cell--icon-button":
|
||||
Boolean(column.type === "icon-button"),
|
||||
column.type === "icon-button",
|
||||
"mdc-data-table__cell--overflow-menu":
|
||||
column.type === "overflow-menu",
|
||||
grows: Boolean(column.grows),
|
||||
forceLTR: Boolean(column.forceLTR),
|
||||
})}"
|
||||
@@ -680,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 {
|
||||
@@ -769,40 +767,65 @@ export class HaDataTable extends LitElement {
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--overflow-menu,
|
||||
.mdc-data-table__header-cell--overflow-menu,
|
||||
.mdc-data-table__header-cell--icon-button,
|
||||
.mdc-data-table__cell--icon-button {
|
||||
width: 56px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon-button,
|
||||
.mdc-data-table__cell--icon-button {
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--overflow-menu,
|
||||
.mdc-data-table__cell--icon-button {
|
||||
color: var(--secondary-text-color);
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon-button:first-child,
|
||||
.mdc-data-table__cell--icon-button:first-child {
|
||||
width: 64px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
:host([dir="rtl"])
|
||||
.mdc-data-table__header-cell--icon-button:first-child,
|
||||
:host([dir="rtl"]) .mdc-data-table__cell--icon-button:first-child {
|
||||
padding-left: auto;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon-button:first-child,
|
||||
.mdc-data-table__header-cell--icon-button:last-child,
|
||||
.mdc-data-table__cell--icon-button:last-child {
|
||||
width: 64px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
:host([dir="rtl"]) .mdc-data-table__header-cell--icon-button:last-child,
|
||||
:host([dir="rtl"]) .mdc-data-table__cell--icon-button:last-child {
|
||||
padding-right: auto;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--overflow-menu:first-child,
|
||||
.mdc-data-table__header-cell--overflow-menu:first-child,
|
||||
.mdc-data-table__header-cell--icon-button:first-child,
|
||||
.mdc-data-table__cell--icon-button:first-child {
|
||||
padding-left: 16px;
|
||||
}
|
||||
:host([dir="rtl"])
|
||||
.mdc-data-table__header-cell--overflow-menu:first-child,
|
||||
:host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:first-child,
|
||||
:host([dir="rtl"])
|
||||
.mdc-data-table__header-cell--overflow-menu:first-child,
|
||||
:host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:first-child {
|
||||
padding-left: 8px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--overflow-menu:last-child,
|
||||
.mdc-data-table__header-cell--overflow-menu:last-child,
|
||||
.mdc-data-table__header-cell--icon-button:last-child,
|
||||
.mdc-data-table__cell--icon-button:last-child {
|
||||
padding-right: 16px;
|
||||
}
|
||||
:host([dir="rtl"])
|
||||
.mdc-data-table__header-cell--overflow-menu:last-child,
|
||||
:host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:last-child,
|
||||
:host([dir="rtl"]) .mdc-data-table__header-cell--icon-button:last-child,
|
||||
:host([dir="rtl"]) .mdc-data-table__cell--icon-button:last-child {
|
||||
padding-right: 8px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
.mdc-data-table__cell--overflow-menu,
|
||||
.mdc-data-table__header-cell--overflow-menu {
|
||||
overflow: initial;
|
||||
}
|
||||
.mdc-data-table__cell--icon-button a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
@@ -14,7 +14,10 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { formatNumber } from "../../common/number/format_number";
|
||||
import {
|
||||
formatNumber,
|
||||
isNumericState,
|
||||
} from "../../common/number/format_number";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@@ -77,7 +80,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
const domain = computeStateDomain(entityState);
|
||||
|
||||
const showIcon = this.icon || this._computeShowIcon(domain, entityState);
|
||||
const image = showIcon
|
||||
const image = this.icon
|
||||
? ""
|
||||
: this.image
|
||||
? this.image
|
||||
@@ -145,7 +148,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
return entityState.state === UNKNOWN ||
|
||||
entityState.state === UNAVAILABLE
|
||||
? "-"
|
||||
: entityState.attributes.unit_of_measurement
|
||||
: isNumericState(entityState)
|
||||
? formatNumber(entityState.state, this.hass!.locale)
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
|
@@ -102,7 +102,12 @@ export class HaStatisticPicker extends LitElement {
|
||||
</style>
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
<paper-icon-item>
|
||||
<state-badge slot="item-icon" .stateObj=${item.state}></state-badge>
|
||||
${item.state
|
||||
? html`<state-badge
|
||||
slot="item-icon"
|
||||
.stateObj=${item.state}
|
||||
></state-badge>`
|
||||
: ""}
|
||||
<paper-item-body two-line="">
|
||||
${item.name}
|
||||
<span secondary
|
||||
@@ -153,7 +158,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
const entityState = this.hass.states[meta.statistic_id];
|
||||
if (!entityState) {
|
||||
if (!entitiesOnly) {
|
||||
output.push({ id: meta.statistic_id, name: meta.statistic_id });
|
||||
output.push({
|
||||
id: meta.statistic_id,
|
||||
name: meta.name || meta.statistic_id,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@@ -80,16 +80,14 @@ export class StateBadge extends LitElement {
|
||||
|
||||
this._showIcon = true;
|
||||
|
||||
if (stateObj) {
|
||||
if (stateObj && this.overrideImage === undefined) {
|
||||
// hide icon if we have entity picture
|
||||
if (
|
||||
((stateObj.attributes.entity_picture_local ||
|
||||
(stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture) &&
|
||||
!this.overrideIcon) ||
|
||||
this.overrideImage
|
||||
!this.overrideIcon
|
||||
) {
|
||||
let imageUrl =
|
||||
this.overrideImage ||
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
if (this.hass) {
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@@ -86,7 +86,7 @@ class HaCameraStream extends LitElement {
|
||||
}
|
||||
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS) {
|
||||
return this._url
|
||||
? html` <ha-hls-player
|
||||
? html`<ha-hls-player
|
||||
autoplay
|
||||
playsinline
|
||||
.allowExoPlayer=${this.allowExoPlayer}
|
||||
@@ -98,7 +98,7 @@ class HaCameraStream extends LitElement {
|
||||
: html``;
|
||||
}
|
||||
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC) {
|
||||
return html` <ha-web-rtc-player
|
||||
return html`<ha-web-rtc-player
|
||||
autoplay
|
||||
playsinline
|
||||
.muted=${this.muted}
|
||||
@@ -115,23 +115,18 @@ class HaCameraStream extends LitElement {
|
||||
// Fallback when unable to fetch stream url
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!isComponentLoaded(this.hass!, "stream") ||
|
||||
!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)
|
||||
) {
|
||||
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
|
||||
// Steaming is not supported by the camera so fallback to MJPEG stream
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC &&
|
||||
typeof RTCPeerConnection === "undefined"
|
||||
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC
|
||||
) {
|
||||
// Stream requires WebRTC but browser does not support, so fallback to
|
||||
// MJPEG stream.
|
||||
return true;
|
||||
// Browser support required for WebRTC
|
||||
return typeof RTCPeerConnection === "undefined";
|
||||
}
|
||||
// Render stream
|
||||
return false;
|
||||
// Server side stream component required for HLS
|
||||
return !isComponentLoaded(this.hass!, "stream");
|
||||
}
|
||||
|
||||
private async _getStreamUrl(): Promise<void> {
|
||||
|
@@ -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>
|
||||
|
@@ -38,6 +38,7 @@ export class HaDialog extends Dialog {
|
||||
.mdc-dialog {
|
||||
--mdc-dialog-scroll-divider-color: var(--divider-color);
|
||||
z-index: var(--dialog-z-index, 7);
|
||||
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
@@ -71,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;
|
||||
|
@@ -17,7 +17,7 @@ declare global {
|
||||
|
||||
@customElement("ha-file-upload")
|
||||
export class HaFileUpload extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@@ -88,7 +88,8 @@ export class HaFileUpload extends LitElement {
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.label=${this.hass?.localize("ui.common.close") ||
|
||||
"close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
`
|
||||
|
@@ -47,12 +47,19 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
const source = ev.target as TextField;
|
||||
const rawValue = source.value;
|
||||
const rawValue = source.value.replace(",", ".");
|
||||
|
||||
let value: number | undefined;
|
||||
|
||||
if (rawValue.endsWith(".")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawValue !== "") {
|
||||
value = parseFloat(rawValue);
|
||||
if (isNaN(value)) {
|
||||
value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect anything changed
|
||||
@@ -61,7 +68,6 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
const newRawValue = value === undefined ? "" : String(value);
|
||||
if (source.value !== newRawValue) {
|
||||
source.value = newRawValue;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@@ -36,7 +36,11 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if ("valueMin" in this.schema && "valueMax" in this.schema) {
|
||||
if (
|
||||
this.schema.valueMin !== undefined &&
|
||||
this.schema.valueMax !== undefined &&
|
||||
this.schema.valueMax - this.schema.valueMin < 256
|
||||
) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
@@ -96,10 +100,15 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
if (this.schema.optional) {
|
||||
return 0;
|
||||
return this.schema.valueMin || 0;
|
||||
}
|
||||
|
||||
return this.schema.description?.suggested_value || this.schema.default || 0;
|
||||
return (
|
||||
this.schema.description?.suggested_value ||
|
||||
this.schema.default ||
|
||||
this.schema.valueMin ||
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private _handleCheckboxChange(ev: Event) {
|
||||
|
@@ -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]) => {
|
||||
|
@@ -52,6 +52,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
return html`
|
||||
<mwc-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label}
|
||||
.value=${this.data}
|
||||
.disabled=${this.disabled}
|
||||
|
@@ -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 {
|
||||
|
@@ -229,6 +229,7 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
max-height: var(--video-max-height, calc(100vh - 97px));
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
119
src/components/ha-icon-overflow-menu.ts
Normal file
119
src/components/ha-icon-overflow-menu.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-button-menu";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
|
||||
export interface IconOverflowMenuItem {
|
||||
[key: string]: any;
|
||||
path: string;
|
||||
label: string;
|
||||
narrowOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
onClick: CallableFunction;
|
||||
}
|
||||
|
||||
@customElement("ha-icon-overflow-menu")
|
||||
export class HaIconOverflowMenu extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Array }) public items: IconOverflowMenuItem[] = [];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.narrow
|
||||
? html` <!-- Collapsed Representation for Small Screens -->
|
||||
<ha-button-menu
|
||||
@click=${this._handleIconOverflowMenuOpened}
|
||||
@closed=${this._handleIconOverflowMenuClosed}
|
||||
class="ha-icon-overflow-menu-overflow"
|
||||
corner="BOTTOM_START"
|
||||
absolute
|
||||
>
|
||||
<mwc-icon-button
|
||||
.title=${this.hass.localize("ui.common.menu")}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
|
||||
${this.items.map(
|
||||
(item) => html`
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${item.disabled}
|
||||
@click=${item.action}
|
||||
>
|
||||
<div slot="graphic">
|
||||
<ha-svg-icon .path=${item.path}></ha-svg-icon>
|
||||
</div>
|
||||
${item.label}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>`
|
||||
: html`
|
||||
<!-- Icon Representation for Big Screens -->
|
||||
|
||||
${this.items.map((item) =>
|
||||
item.narrowOnly
|
||||
? ""
|
||||
: html`<div>
|
||||
${item.tooltip
|
||||
? html`<paper-tooltip animation-delay="0" position="left">
|
||||
${item.tooltip}
|
||||
</paper-tooltip>`
|
||||
: ""}
|
||||
<mwc-icon-button
|
||||
@click=${item.action}
|
||||
.label=${item.label}
|
||||
.disabled=${item.disabled}
|
||||
>
|
||||
<ha-svg-icon .path=${item.path}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</div> `
|
||||
)}
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
protected _handleIconOverflowMenuOpened() {
|
||||
// If this component is used inside a data table, the z-index of the row
|
||||
// needs to be increased. Otherwise the ha-button-menu would be displayed
|
||||
// underneath the next row in the table.
|
||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||
if (row) {
|
||||
row.style.zIndex = "1";
|
||||
}
|
||||
}
|
||||
|
||||
protected _handleIconOverflowMenuClosed() {
|
||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||
if (row) {
|
||||
row.style.zIndex = "";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-icon-overflow-menu": HaIconOverflowMenu;
|
||||
}
|
||||
}
|
@@ -7,14 +7,19 @@ import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
|
||||
let mdiIconList: string[] = [];
|
||||
type IconItem = {
|
||||
icon: string;
|
||||
keywords: string[];
|
||||
};
|
||||
let iconItems: IconItem[] = [];
|
||||
|
||||
// eslint-disable-next-line lit/prefer-static-styles
|
||||
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
||||
const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<style>
|
||||
paper-icon-item {
|
||||
padding: 0;
|
||||
margin: -8px;
|
||||
@@ -37,8 +42,8 @@ const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
||||
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
<paper-icon-item>
|
||||
<ha-icon .icon=${item} slot="item-icon"></ha-icon>
|
||||
<paper-item-body>${item}</paper-item-body>
|
||||
<ha-icon .icon=${item.icon} slot="item-icon"></ha-icon>
|
||||
<paper-item-body>${item.icon}</paper-item-body>
|
||||
</paper-icon-item>`;
|
||||
|
||||
@customElement("ha-icon-picker")
|
||||
@@ -55,6 +60,8 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
|
||||
@@ -66,7 +73,7 @@ export class HaIconPicker extends LitElement {
|
||||
item-label-path="icon"
|
||||
.value=${this._value}
|
||||
allow-custom-value
|
||||
.filteredItems=${mdiIconList}
|
||||
.filteredItems=${iconItems}
|
||||
${comboBoxRenderer(rowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -81,6 +88,8 @@ export class HaIconPicker extends LitElement {
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
.errorMessage=${this.errorMessage}
|
||||
.invalid=${this.invalid}
|
||||
>
|
||||
${this._value || this.placeholder
|
||||
? html`
|
||||
@@ -105,10 +114,38 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
private async _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
if (this._opened && !mdiIconList.length) {
|
||||
if (this._opened && !iconItems.length) {
|
||||
const iconList = await import("../../build/mdi/iconList.json");
|
||||
mdiIconList = iconList.default.map((icon) => `mdi:${icon}`);
|
||||
(this.comboBox as any).filteredItems = mdiIconList;
|
||||
|
||||
iconItems = iconList.default.map((icon) => ({
|
||||
icon: `mdi:${icon.name}`,
|
||||
keywords: icon.keywords,
|
||||
}));
|
||||
|
||||
(this.comboBox as any).filteredItems = iconItems;
|
||||
|
||||
Object.keys(customIcons).forEach((iconSet) => {
|
||||
this._loadCustomIconItems(iconSet);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadCustomIconItems(iconsetPrefix: string) {
|
||||
try {
|
||||
const getIconList = customIcons[iconsetPrefix].getIconList;
|
||||
if (typeof getIconList !== "function") {
|
||||
return;
|
||||
}
|
||||
const iconList = await getIconList();
|
||||
const customIconItems = iconList.map((icon) => ({
|
||||
icon: `${iconsetPrefix}:${icon.name}`,
|
||||
keywords: icon.keywords ?? [],
|
||||
}));
|
||||
iconItems.push(...customIconItems);
|
||||
(this.comboBox as any).filteredItems = iconItems;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,16 +170,30 @@ export class HaIconPicker extends LitElement {
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
const characterCount = filterString.length;
|
||||
if (characterCount >= 2) {
|
||||
const filteredItems = mdiIconList.filter((icon) =>
|
||||
icon.includes(filterString)
|
||||
);
|
||||
const filteredItems: IconItem[] = [];
|
||||
const filteredItemsByKeywords: IconItem[] = [];
|
||||
|
||||
iconItems.forEach((item) => {
|
||||
if (item.icon.includes(filterString)) {
|
||||
filteredItems.push(item);
|
||||
return;
|
||||
}
|
||||
if (item.keywords.some((t) => t.includes(filterString))) {
|
||||
filteredItemsByKeywords.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
filteredItems.push(...filteredItemsByKeywords);
|
||||
|
||||
if (filteredItems.length > 0) {
|
||||
(this.comboBox as any).filteredItems = filteredItems;
|
||||
} else {
|
||||
(this.comboBox as any).filteredItems = [filterString];
|
||||
(this.comboBox as any).filteredItems = [
|
||||
{ icon: filterString, keywords: [] },
|
||||
];
|
||||
}
|
||||
} else {
|
||||
(this.comboBox as any).filteredItems = mdiIconList;
|
||||
(this.comboBox as any).filteredItems = iconItems;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { CustomIcon, customIconsets } from "../data/custom_iconsets";
|
||||
import { CustomIcon, customIcons } from "../data/custom_icons";
|
||||
import {
|
||||
checkCacheVersion,
|
||||
Chunks,
|
||||
@@ -29,335 +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",
|
||||
},
|
||||
application: {
|
||||
newName: "application-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"application-cog": {
|
||||
newName: "application-cog-outline",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"application-settings": {
|
||||
newName: "application-settings-outline",
|
||||
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-up": {
|
||||
newName: "boom-gate-arrow-up",
|
||||
removeIn: "2021.12",
|
||||
},
|
||||
"boom-gate-up-outline": {
|
||||
newName: "boom-gate-arrow-up-outline",
|
||||
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 = {};
|
||||
|
||||
@@ -376,7 +48,7 @@ export class HaIcon extends LitElement {
|
||||
|
||||
@state() private _path?: string;
|
||||
|
||||
@state() private _viewBox?;
|
||||
@state() private _viewBox?: string;
|
||||
|
||||
@state() private _legacy = false;
|
||||
|
||||
@@ -406,6 +78,7 @@ export class HaIcon extends LitElement {
|
||||
if (!this.icon) {
|
||||
return;
|
||||
}
|
||||
const requestedIcon = this.icon;
|
||||
const [iconPrefix, origIconName] = this.icon.split(":", 2);
|
||||
|
||||
let iconName = origIconName;
|
||||
@@ -415,10 +88,10 @@ export class HaIcon extends LitElement {
|
||||
}
|
||||
|
||||
if (!MDI_PREFIXES.includes(iconPrefix)) {
|
||||
if (iconPrefix in customIconsets) {
|
||||
const customIconset = customIconsets[iconPrefix];
|
||||
if (customIconset) {
|
||||
this._setCustomPath(customIconset(iconName));
|
||||
if (iconPrefix in customIcons) {
|
||||
const customIcon = customIcons[iconPrefix];
|
||||
if (customIcon && typeof customIcon.getIcon === "function") {
|
||||
this._setCustomPath(customIcon.getIcon(iconName), requestedIcon);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -461,14 +134,16 @@ export class HaIcon extends LitElement {
|
||||
}
|
||||
|
||||
if (databaseIcon) {
|
||||
this._path = databaseIcon;
|
||||
if (this.icon === requestedIcon) {
|
||||
this._path = databaseIcon;
|
||||
}
|
||||
cachedIcons[iconName] = databaseIcon;
|
||||
return;
|
||||
}
|
||||
const chunk = findIconChunk(iconName);
|
||||
|
||||
if (chunk in chunks) {
|
||||
this._setPath(chunks[chunk], iconName);
|
||||
this._setPath(chunks[chunk], iconName, requestedIcon);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -476,19 +151,31 @@ export class HaIcon extends LitElement {
|
||||
response.json()
|
||||
);
|
||||
chunks[chunk] = iconPromise;
|
||||
this._setPath(iconPromise, iconName);
|
||||
this._setPath(iconPromise, iconName, requestedIcon);
|
||||
debouncedWriteCache();
|
||||
}
|
||||
|
||||
private async _setCustomPath(promise: Promise<CustomIcon>) {
|
||||
private async _setCustomPath(
|
||||
promise: Promise<CustomIcon>,
|
||||
requestedIcon: string
|
||||
) {
|
||||
const icon = await promise;
|
||||
if (this.icon !== requestedIcon) {
|
||||
return;
|
||||
}
|
||||
this._path = icon.path;
|
||||
this._viewBox = icon.viewBox;
|
||||
}
|
||||
|
||||
private async _setPath(promise: Promise<Icons>, iconName: string) {
|
||||
private async _setPath(
|
||||
promise: Promise<Icons>,
|
||||
iconName: string,
|
||||
requestedIcon: string
|
||||
) {
|
||||
const iconPack = await promise;
|
||||
this._path = iconPack[iconName];
|
||||
if (this.icon === requestedIcon) {
|
||||
this._path = iconPack[iconName];
|
||||
}
|
||||
cachedIcons[iconName] = iconPack[iconName];
|
||||
}
|
||||
|
||||
|
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}
|
||||
|
@@ -502,6 +502,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean {
|
||||
if (entity.entity_category) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.includeDomains &&
|
||||
!this.includeDomains.includes(computeDomain(entity.entity_id))
|
||||
|
@@ -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,
|
||||
@@ -145,9 +148,15 @@ class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host,
|
||||
video {
|
||||
display: block;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
max-height: var(--video-max-height, calc(100vh - 97px));
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { Person } from "../../data/person";
|
||||
import { computeInitials } from "./ha-user-badge";
|
||||
import { computeUserInitials } from "../../data/user";
|
||||
|
||||
@customElement("ha-person-badge")
|
||||
class PersonBadge extends LitElement {
|
||||
@@ -22,7 +22,7 @@ class PersonBadge extends LitElement {
|
||||
class="picture"
|
||||
></div>`;
|
||||
}
|
||||
const initials = computeInitials(this.person.name);
|
||||
const initials = computeUserInitials(this.person.name);
|
||||
return html`<div
|
||||
class="initials ${classMap({ long: initials!.length > 2 })}"
|
||||
>
|
||||
|
@@ -10,25 +10,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { User } from "../../data/user";
|
||||
import { computeUserInitials, User } from "../../data/user";
|
||||
import { CurrentUser, HomeAssistant } from "../../types";
|
||||
|
||||
export const computeInitials = (name: string) => {
|
||||
if (!name) {
|
||||
return "?";
|
||||
}
|
||||
return (
|
||||
name
|
||||
.trim()
|
||||
// Split by space and take first 3 words
|
||||
.split(" ")
|
||||
.slice(0, 3)
|
||||
// Of each word, take first letter
|
||||
.map((s) => s.substr(0, 1))
|
||||
.join("")
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("ha-user-badge")
|
||||
class UserBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -75,7 +59,7 @@ class UserBadge extends LitElement {
|
||||
class="picture"
|
||||
></div>`;
|
||||
}
|
||||
const initials = computeInitials(this.user.name);
|
||||
const initials = computeUserInitials(this.user.name);
|
||||
return html`<div
|
||||
class="initials ${classMap({ long: initials!.length > 2 })}"
|
||||
>
|
||||
|
@@ -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 = (
|
||||
|
@@ -194,10 +194,10 @@ export interface NumericStateCondition extends BaseCondition {
|
||||
|
||||
export interface SunCondition extends BaseCondition {
|
||||
condition: "sun";
|
||||
after_offset: number;
|
||||
before_offset: number;
|
||||
after: "sunrise" | "sunset";
|
||||
before: "sunrise" | "sunset";
|
||||
after_offset?: number;
|
||||
before_offset?: number;
|
||||
after?: "sunrise" | "sunset";
|
||||
before?: "sunrise" | "sunset";
|
||||
}
|
||||
|
||||
export interface ZoneCondition extends BaseCondition {
|
||||
|
@@ -62,6 +62,8 @@ export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;
|
||||
|
||||
export interface SubscriptionInfo {
|
||||
human_description: string;
|
||||
provider: string;
|
||||
plan_renewal_date?: number;
|
||||
}
|
||||
|
||||
export interface CloudWebhook {
|
||||
@@ -76,6 +78,39 @@ export interface ThingTalkConversion {
|
||||
placeholders: PlaceholderContainer;
|
||||
}
|
||||
|
||||
export const cloudLogin = (
|
||||
hass: HomeAssistant,
|
||||
email: string,
|
||||
password: string
|
||||
) =>
|
||||
hass.callApi("POST", "cloud/login", {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
export const cloudLogout = (hass: HomeAssistant) =>
|
||||
hass.callApi("POST", "cloud/logout");
|
||||
|
||||
export const cloudForgotPassword = (hass: HomeAssistant, email: string) =>
|
||||
hass.callApi("POST", "cloud/forgot_password", {
|
||||
email,
|
||||
});
|
||||
|
||||
export const cloudRegister = (
|
||||
hass: HomeAssistant,
|
||||
email: string,
|
||||
password: string
|
||||
) =>
|
||||
hass.callApi("POST", "cloud/register", {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
export const cloudResendVerification = (hass: HomeAssistant, email: string) =>
|
||||
hass.callApi("POST", "cloud/resend_confirm", {
|
||||
email,
|
||||
});
|
||||
|
||||
export const fetchCloudStatus = (hass: HomeAssistant) =>
|
||||
hass.callWS<CloudStatus>({ type: "cloud/status" });
|
||||
|
||||
|
40
src/data/custom_icons.ts
Normal file
40
src/data/custom_icons.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { customIconsets } from "./custom_iconsets";
|
||||
|
||||
export interface CustomIcon {
|
||||
path: string;
|
||||
viewBox?: string;
|
||||
}
|
||||
|
||||
export interface CustomIconListItem {
|
||||
name: string;
|
||||
keywords?: string[];
|
||||
}
|
||||
|
||||
export interface CustomIconHelpers {
|
||||
getIcon: (name: string) => Promise<CustomIcon>;
|
||||
getIconList?: () => Promise<CustomIconListItem[]>;
|
||||
}
|
||||
|
||||
export interface CustomIconsWindow {
|
||||
customIcons?: {
|
||||
[key: string]: CustomIconHelpers;
|
||||
};
|
||||
}
|
||||
|
||||
const customIconsWindow = window as CustomIconsWindow;
|
||||
|
||||
if (!("customIcons" in customIconsWindow)) {
|
||||
customIconsWindow.customIcons = {};
|
||||
}
|
||||
|
||||
// Proxy for backward compatibility with icon sets
|
||||
export const customIcons = new Proxy(customIconsWindow.customIcons!, {
|
||||
get: (obj, prop: string) =>
|
||||
obj[prop] ??
|
||||
(customIconsets[prop]
|
||||
? {
|
||||
getIcon: customIconsets[prop],
|
||||
}
|
||||
: undefined),
|
||||
has: (obj, prop: string) => prop in obj || prop in customIconsets,
|
||||
});
|
@@ -1,9 +1,6 @@
|
||||
export interface CustomIcon {
|
||||
path: string;
|
||||
viewBox?: string;
|
||||
}
|
||||
import { CustomIcon } from "./custom_icons";
|
||||
|
||||
export interface CustomIconsetsWindow {
|
||||
interface CustomIconsetsWindow {
|
||||
customIconsets?: { [key: string]: (name: string) => Promise<CustomIcon> };
|
||||
}
|
||||
|
||||
|
@@ -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 = (
|
||||
|
@@ -79,7 +79,7 @@ export const fetchHassioBackups = async (
|
||||
};
|
||||
|
||||
export const fetchHassioBackupInfo = async (
|
||||
hass: HomeAssistant,
|
||||
hass: HomeAssistant | undefined,
|
||||
backup: string
|
||||
): Promise<HassioBackupDetail> => {
|
||||
if (hass) {
|
||||
@@ -202,7 +202,7 @@ export const createHassioPartialBackup = async (
|
||||
};
|
||||
|
||||
export const uploadBackup = async (
|
||||
hass: HomeAssistant,
|
||||
hass: HomeAssistant | undefined,
|
||||
file: File
|
||||
): Promise<HassioResponse<HassioBackup>> => {
|
||||
const fd = new FormData();
|
||||
|
@@ -74,6 +74,8 @@ export interface StatisticValue {
|
||||
export interface StatisticsMetaData {
|
||||
unit_of_measurement: string;
|
||||
statistic_id: string;
|
||||
source: string;
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export type StatisticsValidationResult =
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user