mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-24 21:37:21 +00:00
Support system health streaming (#7593)
This commit is contained in:
parent
c162e84383
commit
161561c48a
8
src/common/util/copy-clipboard.ts
Normal file
8
src/common/util/copy-clipboard.ts
Normal 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);
|
||||
};
|
@ -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":
|
||||
|
@ -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/)
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user