295 lines
8.0 KiB
TypeScript
295 lines
8.0 KiB
TypeScript
import "@material/mwc-button";
|
|
import "@material/mwc-list/mwc-list-item";
|
|
import { mdiRefresh, mdiDownload } from "@mdi/js";
|
|
import {
|
|
css,
|
|
CSSResultGroup,
|
|
html,
|
|
LitElement,
|
|
PropertyValues,
|
|
TemplateResult,
|
|
} from "lit";
|
|
import { customElement, property, state } from "lit/decorators";
|
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
|
import "../../../components/ha-alert";
|
|
import "../../../components/ha-ansi-to-html";
|
|
import "../../../components/ha-card";
|
|
import "../../../components/ha-icon-button";
|
|
import "../../../components/ha-select";
|
|
import "../../../components/ha-svg-icon";
|
|
|
|
import { getSignedPath } from "../../../data/auth";
|
|
|
|
import { fetchErrorLog, getErrorLogDownloadUrl } from "../../../data/error_log";
|
|
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
|
import {
|
|
fetchHassioLogs,
|
|
getHassioLogDownloadUrl,
|
|
} from "../../../data/hassio/supervisor";
|
|
import { HomeAssistant } from "../../../types";
|
|
import { debounce } from "../../../common/util/debounce";
|
|
import { fileDownload } from "../../../util/file_download";
|
|
|
|
@customElement("error-log-card")
|
|
class ErrorLogCard extends LitElement {
|
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
|
|
@property() public filter = "";
|
|
|
|
@property() public header?: string;
|
|
|
|
@property() public provider!: string;
|
|
|
|
@property({ type: Boolean, attribute: true }) public show = false;
|
|
|
|
@state() private _isLogLoaded = false;
|
|
|
|
@state() private _logHTML?: TemplateResult[] | TemplateResult | string;
|
|
|
|
@state() private _error?: string;
|
|
|
|
protected render(): TemplateResult {
|
|
return html`
|
|
<div class="error-log-intro">
|
|
${this._error
|
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
|
: ""}
|
|
${this._logHTML
|
|
? html`
|
|
<ha-card outlined>
|
|
<div class="header">
|
|
<h1 class="card-header">
|
|
${this.header ||
|
|
this.hass.localize("ui.panel.config.logs.show_full_logs")}
|
|
</h1>
|
|
<div>
|
|
<ha-icon-button
|
|
.path=${mdiRefresh}
|
|
@click=${this._refresh}
|
|
.label=${this.hass.localize("ui.common.refresh")}
|
|
></ha-icon-button>
|
|
<ha-icon-button
|
|
.path=${mdiDownload}
|
|
@click=${this._downloadFullLog}
|
|
.label=${this.hass.localize(
|
|
"ui.panel.config.logs.download_full_log"
|
|
)}
|
|
></ha-icon-button>
|
|
</div>
|
|
</div>
|
|
<div class="card-content error-log">${this._logHTML}</div>
|
|
</ha-card>
|
|
`
|
|
: ""}
|
|
${!this._logHTML
|
|
? html`
|
|
<mwc-button outlined @click=${this._downloadFullLog}>
|
|
<ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
|
|
${this.hass.localize("ui.panel.config.logs.download_full_log")}
|
|
</mwc-button>
|
|
<mwc-button raised @click=${this._refreshLogs}>
|
|
${this.hass.localize("ui.panel.config.logs.load_logs")}
|
|
</mwc-button>
|
|
`
|
|
: ""}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private _debounceSearch = debounce(
|
|
() => (this._isLogLoaded ? this._refreshLogs() : this._debounceSearch()),
|
|
150,
|
|
false
|
|
);
|
|
|
|
protected firstUpdated(changedProps: PropertyValues) {
|
|
super.firstUpdated(changedProps);
|
|
|
|
if (this.hass?.config.recovery_mode || this.show) {
|
|
this.hass.loadFragmentTranslation("config");
|
|
this._refreshLogs();
|
|
}
|
|
}
|
|
|
|
protected updated(changedProps) {
|
|
super.updated(changedProps);
|
|
|
|
if (changedProps.has("provider")) {
|
|
this._logHTML = undefined;
|
|
}
|
|
|
|
if (
|
|
(changedProps.has("show") && this.show) ||
|
|
(changedProps.has("provider") && this.show)
|
|
) {
|
|
this._refreshLogs();
|
|
return;
|
|
}
|
|
|
|
if (changedProps.has("filter")) {
|
|
this._debounceSearch();
|
|
}
|
|
}
|
|
|
|
private async _refresh(ev: CustomEvent): Promise<void> {
|
|
const button = ev.currentTarget as any;
|
|
button.progress = true;
|
|
|
|
await this._refreshLogs();
|
|
button.progress = false;
|
|
}
|
|
|
|
private async _downloadFullLog(): Promise<void> {
|
|
const timeString = new Date().toISOString().replace(/:/g, "-");
|
|
const downloadUrl =
|
|
this.provider !== "core"
|
|
? getHassioLogDownloadUrl(this.provider)
|
|
: getErrorLogDownloadUrl;
|
|
const logFileName =
|
|
this.provider !== "core"
|
|
? `${this.provider}_${timeString}.log`
|
|
: `home-assistant_${timeString}.log`;
|
|
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
|
fileDownload(signedUrl.path, logFileName);
|
|
}
|
|
|
|
private async _refreshLogs(): Promise<void> {
|
|
this._logHTML = this.hass.localize("ui.panel.config.logs.loading_log");
|
|
let log: string;
|
|
|
|
if (this.provider !== "core" && isComponentLoaded(this.hass, "hassio")) {
|
|
try {
|
|
log = await fetchHassioLogs(this.hass, this.provider);
|
|
if (this.filter) {
|
|
log = log
|
|
.split("\n")
|
|
.filter((entry) =>
|
|
entry.toLowerCase().includes(this.filter.toLowerCase())
|
|
)
|
|
.join("\n");
|
|
}
|
|
if (!log) {
|
|
this._logHTML = this.hass.localize("ui.panel.config.logs.no_errors");
|
|
return;
|
|
}
|
|
this._logHTML = html`<ha-ansi-to-html .content=${log}>
|
|
</ha-ansi-to-html>`;
|
|
this._isLogLoaded = true;
|
|
return;
|
|
} catch (err: any) {
|
|
this._error = this.hass.localize(
|
|
"ui.panel.config.logs.failed_get_logs",
|
|
{ provider: this.provider, error: extractApiErrorMessage(err) }
|
|
);
|
|
return;
|
|
}
|
|
} else {
|
|
log = await fetchErrorLog(this.hass!);
|
|
}
|
|
|
|
this._isLogLoaded = true;
|
|
|
|
const split = log && log.split("\n");
|
|
|
|
this._logHTML = split
|
|
? (this.filter
|
|
? split.filter((entry) => {
|
|
if (this.filter) {
|
|
return entry.toLowerCase().includes(this.filter.toLowerCase());
|
|
}
|
|
return entry;
|
|
})
|
|
: split
|
|
).map((entry) => {
|
|
if (entry.includes("INFO"))
|
|
return html`<div class="info">${entry}</div>`;
|
|
|
|
if (entry.includes("WARNING"))
|
|
return html`<div class="warning">${entry}</div>`;
|
|
|
|
if (
|
|
entry.includes("ERROR") ||
|
|
entry.includes("FATAL") ||
|
|
entry.includes("CRITICAL")
|
|
)
|
|
return html`<div class="error">${entry}</div>`;
|
|
|
|
return html`<div>${entry}</div>`;
|
|
})
|
|
: this.hass.localize("ui.panel.config.logs.no_errors");
|
|
}
|
|
|
|
static styles: CSSResultGroup = css`
|
|
.error-log-intro {
|
|
text-align: center;
|
|
margin: 16px;
|
|
}
|
|
|
|
ha-card {
|
|
padding-top: 16px;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 0 16px;
|
|
}
|
|
|
|
.card-header {
|
|
color: var(--ha-card-header-color, --primary-text-color);
|
|
font-family: var(--ha-card-header-font-family, inherit);
|
|
font-size: var(--ha-card-header-font-size, 24px);
|
|
letter-spacing: -0.012em;
|
|
line-height: 48px;
|
|
display: block;
|
|
margin-block-start: 0px;
|
|
margin-block-end: 0px;
|
|
font-weight: normal;
|
|
}
|
|
|
|
ha-select {
|
|
display: block;
|
|
max-width: 500px;
|
|
width: 100%;
|
|
}
|
|
|
|
ha-icon-button {
|
|
float: right;
|
|
}
|
|
|
|
.error-log {
|
|
font-family: var(--code-font-family, monospace);
|
|
clear: both;
|
|
text-align: left;
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.error-log > div {
|
|
overflow: auto;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.error-log > div:hover {
|
|
background-color: var(--secondary-background-color);
|
|
}
|
|
|
|
.error {
|
|
color: var(--error-color);
|
|
}
|
|
|
|
.warning {
|
|
color: var(--warning-color);
|
|
}
|
|
|
|
mwc-button {
|
|
direction: var(--direction);
|
|
}
|
|
`;
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"error-log-card": ErrorLogCard;
|
|
}
|
|
}
|