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

View File

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

View File

@ -1,24 +1,111 @@
import { HomeAssistant } from "../types";
export interface HomeAssistantSystemHealthInfo {
version: string;
dev: boolean;
hassio: boolean;
virtualenv: string;
python_version: string;
docker: boolean;
arch: string;
timezone: string;
os_name: string;
interface SystemCheckValueDateObject {
type: "date";
value: 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 {
[domain: string]: { [key: string]: string | number | boolean };
[domain: string]: {
manage_url?: string;
info: {
[key: string]: SystemCheckValue;
};
};
}
export const fetchSystemHealthInfo = (
hass: HomeAssistant
): Promise<SystemHealthInfo> =>
hass.callWS({
type: "system_health/info",
});
interface SystemHealthEventInitial {
type: "initial";
data: SystemHealthInfo;
}
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 { mdiContentCopy } from "@mdi/js";
import {
@ -15,10 +17,13 @@ import "@polymer/paper-tooltip/paper-tooltip";
import type { PaperTooltipElement } from "@polymer/paper-tooltip/paper-tooltip";
import { domainToName } from "../../../data/integration";
import {
fetchSystemHealthInfo,
subscribeSystemHealthInfo,
SystemHealthInfo,
SystemCheckValueObject,
} from "../../../data/system_health";
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) => {
if (a === "homeassistant") {
@ -60,19 +65,74 @@ class SystemHealthCard extends LitElement {
} else {
const domains = Object.keys(this._info).sort(sortKeys);
for (const domain of domains) {
const domainInfo = this._info[domain];
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`
<tr>
<td>${key}</td>
<td>${this._info[domain][key]}</td>
<td>
${this.hass.localize(
`ui.panel.config.info.system_health.checks.${domain}.${key}`
) || key}
</td>
<td>${value}</td>
</tr>
`);
}
if (domain !== "homeassistant") {
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`
@ -109,45 +169,63 @@ class SystemHealthCard extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._fetchInfo();
}
private async _fetchInfo() {
try {
if (!this.hass!.config.components.includes("system_health")) {
throw new Error();
}
this._info = await fetchSystemHealthInfo(this.hass!);
} catch (err) {
if (!this.hass!.config.components.includes("system_health")) {
this._info = {
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 {
const copyElement = this.shadowRoot?.querySelector(
".card-content"
) as HTMLElement;
let haContent: string | undefined;
const domainParts: string[] = [];
// Add temporary heading (fixed in EN since usually executed to provide support data)
const tempTitle = document.createElement("h3");
tempTitle.innerText = "System Health";
copyElement.insertBefore(tempTitle, copyElement.firstElementChild);
for (const domain of Object.keys(this._info!).sort(sortKeys)) {
const domainInfo = this._info![domain];
const parts = [`${domainToName(this.hass.localize, domain)}\n`];
const selection = window.getSelection()!;
selection.removeAllRanges();
const range = document.createRange();
range.selectNodeContents(copyElement);
selection.addRange(range);
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
document.execCommand("copy");
window.getSelection()!.removeAllRanges();
if (typeof domainInfo.info[key] === "object") {
const info = domainInfo.info[key] as SystemCheckValueObject;
// Remove temporary heading again
copyElement.removeChild(tempTitle);
if (info.type === "pending") {
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();
setTimeout(() => this._toolTip?.hide(), 3000);
@ -160,7 +238,7 @@ class SystemHealthCard extends LitElement {
}
td:first-child {
width: 33%;
width: 45%;
}
.loading-container {
@ -172,6 +250,19 @@ class SystemHealthCard extends LitElement {
.card-header {
justify-content: space-between;
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",
"integrations": "Integrations",
"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": {
"caption": "Logs",