Support system health streaming (#7593)

This commit is contained in:
Paulus Schoutsen 2020-11-09 16:11:01 +01:00 committed by GitHub
parent c162e84383
commit 161561c48a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 275 additions and 52 deletions

View File

@ -0,0 +1,8 @@
export const copyToClipboard = (str) => {
const el = document.createElement("textarea");
el.value = str;
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
};

View File

@ -11,7 +11,7 @@ export class HaCircularProgress extends CircularProgress {
public alt = "Loading"; public alt = "Loading";
@property() @property()
public size: "small" | "medium" | "large" = "medium"; public size: "tiny" | "small" | "medium" | "large" = "medium";
// @ts-ignore // @ts-ignore
public set density(_) { public set density(_) {
@ -20,6 +20,8 @@ export class HaCircularProgress extends CircularProgress {
public get density() { public get density() {
switch (this.size) { switch (this.size) {
case "tiny":
return -8;
case "small": case "small":
return -5; return -5;
case "medium": case "medium":

View File

@ -47,7 +47,6 @@ class HaMarkdownElement extends UpdatingElement {
node.host !== document.location.host node.host !== document.location.host
) { ) {
node.target = "_blank"; node.target = "_blank";
node.rel = "noreferrer";
// protect referrer on external links and deny window.opener access for security reasons // protect referrer on external links and deny window.opener access for security reasons
// (see https://mathiasbynens.github.io/rel-noopener/) // (see https://mathiasbynens.github.io/rel-noopener/)

View File

@ -1,24 +1,111 @@
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
export interface HomeAssistantSystemHealthInfo { interface SystemCheckValueDateObject {
version: string; type: "date";
dev: boolean; value: string;
hassio: boolean;
virtualenv: string;
python_version: string;
docker: boolean;
arch: string;
timezone: string;
os_name: string;
} }
interface SystemCheckValueErrorObject {
type: "failed";
error: string;
more_info?: string;
}
interface SystemCheckValuePendingObject {
type: "pending";
}
export type SystemCheckValueObject =
| SystemCheckValueDateObject
| SystemCheckValueErrorObject
| SystemCheckValuePendingObject;
export type SystemCheckValue =
| string
| number
| boolean
| SystemCheckValueObject;
export interface SystemHealthInfo { export interface SystemHealthInfo {
[domain: string]: { [key: string]: string | number | boolean }; [domain: string]: {
manage_url?: string;
info: {
[key: string]: SystemCheckValue;
};
};
} }
export const fetchSystemHealthInfo = ( interface SystemHealthEventInitial {
hass: HomeAssistant type: "initial";
): Promise<SystemHealthInfo> => data: SystemHealthInfo;
hass.callWS({ }
type: "system_health/info", interface SystemHealthEventUpdateSuccess {
}); type: "update";
success: true;
domain: string;
key: string;
data: SystemCheckValue;
}
interface SystemHealthEventUpdateError {
type: "update";
success: false;
domain: string;
key: string;
error: {
msg: string;
};
}
interface SystemHealthEventFinish {
type: "finish";
}
type SystemHealthEvent =
| SystemHealthEventInitial
| SystemHealthEventUpdateSuccess
| SystemHealthEventUpdateError
| SystemHealthEventFinish;
export const subscribeSystemHealthInfo = (
hass: HomeAssistant,
callback: (info: SystemHealthInfo) => void
) => {
let data = {};
const unsubProm = hass.connection.subscribeMessage<SystemHealthEvent>(
(updateEvent) => {
if (updateEvent.type === "initial") {
data = updateEvent.data;
callback(data);
return;
}
if (updateEvent.type === "finish") {
unsubProm.then((unsub) => unsub());
return;
}
data = {
...data,
[updateEvent.domain]: {
...data[updateEvent.domain],
info: {
...data[updateEvent.domain].info,
[updateEvent.key]: updateEvent.success
? updateEvent.data
: {
error: true,
value: updateEvent.error.msg,
},
},
},
};
callback(data);
},
{
type: "system_health/info",
}
);
return unsubProm;
};

View File

@ -1,3 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import { mdiContentCopy } from "@mdi/js"; import { mdiContentCopy } from "@mdi/js";
import { import {
@ -15,10 +17,13 @@ import "@polymer/paper-tooltip/paper-tooltip";
import type { PaperTooltipElement } from "@polymer/paper-tooltip/paper-tooltip"; import type { PaperTooltipElement } from "@polymer/paper-tooltip/paper-tooltip";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import { import {
fetchSystemHealthInfo, subscribeSystemHealthInfo,
SystemHealthInfo, SystemHealthInfo,
SystemCheckValueObject,
} from "../../../data/system_health"; } from "../../../data/system_health";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
const sortKeys = (a: string, b: string) => { const sortKeys = (a: string, b: string) => {
if (a === "homeassistant") { if (a === "homeassistant") {
@ -60,19 +65,74 @@ class SystemHealthCard extends LitElement {
} else { } else {
const domains = Object.keys(this._info).sort(sortKeys); const domains = Object.keys(this._info).sort(sortKeys);
for (const domain of domains) { for (const domain of domains) {
const domainInfo = this._info[domain];
const keys: TemplateResult[] = []; const keys: TemplateResult[] = [];
for (const key of Object.keys(this._info[domain]).sort()) { for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (typeof domainInfo.info[key] === "object") {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
value = html`
<ha-circular-progress active size="tiny"></ha-circular-progress>
`;
} else if (info.type === "failed") {
value = html`
<span class="error">${info.error}</span>${!info.more_info
? ""
: html`
<a
href=${info.more_info}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.info.system_health.more_info"
)}
</a>
`}
`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.language);
}
} else {
value = domainInfo.info[key];
}
keys.push(html` keys.push(html`
<tr> <tr>
<td>${key}</td> <td>
<td>${this._info[domain][key]}</td> ${this.hass.localize(
`ui.panel.config.info.system_health.checks.${domain}.${key}`
) || key}
</td>
<td>${value}</td>
</tr> </tr>
`); `);
} }
if (domain !== "homeassistant") { if (domain !== "homeassistant") {
sections.push( sections.push(
html`<h3>${domainToName(this.hass.localize, domain)}</h3>` html`
<div class="card-header">
<h3>
${domainToName(this.hass.localize, domain)}
</h3>
${!domainInfo.manage_url
? ""
: html`
<a class="manage" href=${domainInfo.manage_url}>
<mwc-button>
${this.hass.localize(
"ui.panel.config.info.system_health.manage"
)}
</mwc-button>
</a>
`}
</div>
`
); );
} }
sections.push(html` sections.push(html`
@ -109,45 +169,63 @@ class SystemHealthCard extends LitElement {
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._fetchInfo();
}
private async _fetchInfo() { if (!this.hass!.config.components.includes("system_health")) {
try {
if (!this.hass!.config.components.includes("system_health")) {
throw new Error();
}
this._info = await fetchSystemHealthInfo(this.hass!);
} catch (err) {
this._info = { this._info = {
system_health: { system_health: {
error: this.hass.localize("ui.panel.config.info.system_health_error"), info: {
error: this.hass.localize(
"ui.panel.config.info.system_health_error"
),
},
}, },
}; };
return;
} }
subscribeSystemHealthInfo(this.hass!, (info) => {
this._info = info;
});
} }
private _copyInfo(): void { private _copyInfo(): void {
const copyElement = this.shadowRoot?.querySelector( let haContent: string | undefined;
".card-content" const domainParts: string[] = [];
) as HTMLElement;
// Add temporary heading (fixed in EN since usually executed to provide support data) for (const domain of Object.keys(this._info!).sort(sortKeys)) {
const tempTitle = document.createElement("h3"); const domainInfo = this._info![domain];
tempTitle.innerText = "System Health"; const parts = [`${domainToName(this.hass.localize, domain)}\n`];
copyElement.insertBefore(tempTitle, copyElement.firstElementChild);
const selection = window.getSelection()!; for (const key of Object.keys(domainInfo.info)) {
selection.removeAllRanges(); let value: unknown;
const range = document.createRange();
range.selectNodeContents(copyElement);
selection.addRange(range);
document.execCommand("copy"); if (typeof domainInfo.info[key] === "object") {
window.getSelection()!.removeAllRanges(); const info = domainInfo.info[key] as SystemCheckValueObject;
// Remove temporary heading again if (info.type === "pending") {
copyElement.removeChild(tempTitle); value = "pending";
} else if (info.type === "failed") {
value = `failed to load: ${info.error}`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.language);
}
} else {
value = domainInfo.info[key];
}
parts.push(`${key}: ${value}`);
}
if (domain === "homeassistant") {
haContent = parts.join("\n");
} else {
domainParts.push(parts.join("\n"));
}
}
copyToClipboard(
`System Health\n\n${haContent}\n\n${domainParts.join("\n\n")}`
);
this._toolTip!.show(); this._toolTip!.show();
setTimeout(() => this._toolTip?.hide(), 3000); setTimeout(() => this._toolTip?.hide(), 3000);
@ -160,7 +238,7 @@ class SystemHealthCard extends LitElement {
} }
td:first-child { td:first-child {
width: 33%; width: 45%;
} }
.loading-container { .loading-container {
@ -172,6 +250,19 @@ class SystemHealthCard extends LitElement {
.card-header { .card-header {
justify-content: space-between; justify-content: space-between;
display: flex; display: flex;
align-items: center;
}
.error {
color: var(--error-color);
}
a {
color: var(--primary-color);
}
a.manage {
text-decoration: none;
} }
`; `;
} }

View File

@ -873,7 +873,43 @@
"system_health_error": "System Health component is not loaded. Add 'system_health:' to configuration.yaml", "system_health_error": "System Health component is not loaded. Add 'system_health:' to configuration.yaml",
"integrations": "Integrations", "integrations": "Integrations",
"documentation": "Documentation", "documentation": "Documentation",
"issues": "Issues" "issues": "Issues",
"system_health": {
"manage": "Manage",
"more_info": "more info",
"checks": {
"homeassistant": {
"arch": "CPU Architecture",
"dev": "Development",
"docker": "Docker",
"hassio": "HassOS",
"installation_type": "Installation Type",
"os_name": "Operating System Name",
"os_version": "Operating System Version",
"python_version": "Python Version",
"timezone": "Timezone",
"version": "Version",
"virtualenv": "Virtual Environment"
},
"cloud": {
"can_reach_cert_server": "Reach Certificate Server",
"can_reach_cloud": "Reach Home Assistant Cloud",
"can_reach_cloud_auth": "Reach Authentication Server",
"relayer_connected": "Relayer Connected",
"remote_connected": "Remote Connected",
"remote_enabled": "Remote Enabled",
"alexa_enabled": "Alexa Enabled",
"google_enabled": "Google Enabled",
"logged_in": "Logged In",
"subscription_expiration": "Subscription Expiration"
},
"lovelace": {
"dashboards": "Dashboards",
"mode": "Mode",
"resources": "Resources"
}
}
}
}, },
"logs": { "logs": {
"caption": "Logs", "caption": "Logs",