From 387392713c9365cc34a20f40a4be95dd3f602936 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 7 Nov 2024 21:32:28 +0100 Subject: [PATCH] move download logs button, switch between raw and normal logs (#22721) --- .../addon-view/log/hassio-addon-log-tab.ts | 1 - src/panels/config/logs/error-log-card.ts | 237 +++++++++--------- src/panels/config/logs/ha-config-logs.ts | 35 +-- src/panels/config/logs/system-log-card.ts | 64 ++++- .../lovelace/cards/hui-recovery-mode-card.ts | 2 +- src/translations/en.json | 7 +- 6 files changed, 200 insertions(+), 146 deletions(-) diff --git a/hassio/src/addon-view/log/hassio-addon-log-tab.ts b/hassio/src/addon-view/log/hassio-addon-log-tab.ts index a40454fe83..34ce4f4626 100644 --- a/hassio/src/addon-view/log/hassio-addon-log-tab.ts +++ b/hassio/src/addon-view/log/hassio-addon-log-tab.ts @@ -47,7 +47,6 @@ class HassioAddonLogDashboard extends LitElement { .localizeFunc=${this.supervisor.localize} .header=${this.addon.name} .provider=${this.addon.slug} - show .filter=${this._filter} > diff --git a/src/panels/config/logs/error-log-card.ts b/src/panels/config/logs/error-log-card.ts index e9e0f7dda9..f9fbe31410 100644 --- a/src/panels/config/logs/error-log-card.ts +++ b/src/panels/config/logs/error-log-card.ts @@ -11,6 +11,7 @@ import { mdiRefresh, mdiWrap, mdiWrapDisabled, + mdiFolderTextOutline, } from "@mdi/js"; import { css, @@ -58,7 +59,7 @@ import { downloadFileSupported, fileDownload, } from "../../../util/file_download"; -import type { HASSDomEvent } from "../../../common/dom/fire_event"; +import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event"; import type { ConnectionStatus } from "../../../data/connection-status"; import { atLeastVersion } from "../../../common/config/version"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; @@ -79,9 +80,10 @@ class ErrorLogCard extends LitElement { @property() public header?: string; - @property() public provider!: string; + @property() public provider?: string; - @property({ type: Boolean, attribute: true }) public show = false; + @property({ attribute: "allow-switch", type: Boolean }) public allowSwitch = + false; @query(".error-log") private _logElement?: HTMLElement; @@ -130,26 +132,32 @@ class ErrorLogCard extends LitElement { @state() private _wrapLines = true; - @state() private _downloadSupported; + @state() private _downloadSupported?: boolean; - @state() private _logsFileLink; + @state() private _logsFileLink?: string; protected render(): TemplateResult { + const streaming = + this._streamSupported && + this.provider && + isComponentLoaded(this.hass, "hassio") && + this._loadingState !== "loading"; + + const hasBoots = this._streamSupported && Array.isArray(this._boots); + const localize = this.localizeFunc || this.hass.localize; return html`
${this._error ? html`${this._error}` : nothing} - +

${this.header || localize("ui.panel.config.logs.show_full_logs")}

- ${this._streamSupported && - Array.isArray(this._boots) && - this._showBootsSelect + ${hasBoots && this._showBootsSelect ? html` - ${this._boots.map( + ${this._boots!.map( (boot) => html` - ${!this._streamSupported || this._error + ${!streaming || this._error ? html`` : nothing} - ${this._streamSupported && Array.isArray(this._boots) + ${(this.allowSwitch && this.provider === "core") || hasBoots ? html` - - - ${localize( - `ui.panel.config.logs.${this._showBootsSelect ? "hide" : "show"}_haos_boots` - )} - + ${this.allowSwitch && this.provider === "core" + ? html` + + ${this.hass.localize( + "ui.panel.config.logs.show_condensed_logs" + )} + ` + : nothing} + ${hasBoots + ? html` + + ${localize( + `ui.panel.config.logs.${this._showBootsSelect ? "hide" : "show"}_haos_boots` + )} + ` + : nothing} ` : nothing} @@ -305,48 +326,34 @@ class ErrorLogCard extends LitElement { slot="trailingIcon" > - ${this._streamSupported && - this._loadingState !== "loading" && - this._boot === 0 && - !this._error + ${streaming && this._boot === 0 && !this._error ? html`
Live
` : nothing} - ${this.show === false - ? html` - ${this._downloadSupported - ? html` - - - ${localize("ui.panel.config.logs.download_logs")} - - ` - : nothing} - - ${localize("ui.panel.config.logs.load_logs")} - - ` - : nothing}
`; } - public connectedCallback() { - super.connectedCallback(); + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (changedProps.has("provider")) { + this._boot = 0; + this._loadLogs(); + } + if (this.hasUpdated) { + return; + } + this._streamSupported = atLeastVersion(this.hass.config.version, 2024, 11); + this._downloadSupported = downloadFileSupported(this.hass); + // just needs to be loaded once, because only the host endpoints provide boots information + this._loadBoots(); - if (this._streamSupported === undefined) { - this._streamSupported = atLeastVersion( - this.hass.config.version, - 2024, - 11 - ); - } - if (this._downloadSupported === undefined && this.hass) { - this._downloadSupported = downloadFileSupported(this.hass); - } + window.addEventListener("connection-status", this._handleConnectionStatus); + + this.hass.loadFragmentTranslation("config"); } protected firstUpdated(changedProps: PropertyValues) { @@ -356,28 +363,11 @@ class ErrorLogCard extends LitElement { this._scrolledToTopController.callback = this._handleTopScroll; this._scrolledToTopController.observe(this._scrollTopMarkerElement!); - - window.addEventListener("connection-status", this._handleConnectionStatus); - - if (this.hass?.config.recovery_mode || this.show) { - this.hass.loadFragmentTranslation("config"); - } - - // just needs to be loaded once, because only the host endpoints provide boots information - this._loadBoots(); } protected updated(changedProps) { super.updated(changedProps); - if ( - (changedProps.has("show") && this.show) || - (changedProps.has("provider") && this.show) - ) { - this._boot = 0; - this._loadLogs(); - } - if (this._newLogsIndicator && this._scrolledToBottomController.value) { this._newLogsIndicator = false; } @@ -411,7 +401,7 @@ class ErrorLogCard extends LitElement { } private async _downloadLogs(): Promise { - if (this._streamSupported) { + if (this._streamSupported && this.provider) { showDownloadLogsDialog(this, { header: this.header, provider: this.provider, @@ -433,10 +423,6 @@ class ErrorLogCard extends LitElement { } } - private _showLogs(): void { - this.show = true; - } - private async _loadLogs(): Promise { this._error = undefined; this._loadingState = "loading"; @@ -448,15 +434,16 @@ class ErrorLogCard extends LitElement { try { if (this._logStreamAborter) { this._logStreamAborter.abort(); + this._logStreamAborter = undefined; } - this._logStreamAborter = new AbortController(); - if ( this._streamSupported && isComponentLoaded(this.hass, "hassio") && this.provider ) { + this._logStreamAborter = new AbortController(); + // check if there are any logs at all const testResponse = await fetchHassioLogs( this.hass, @@ -599,60 +586,62 @@ class ErrorLogCard extends LitElement { if (ev.detail === "disconnected" && this._logStreamAborter) { this._logStreamAborter.abort(); } - if (ev.detail === "connected" && this.show) { + if (ev.detail === "connected") { this._loadLogs(); } }; private async _loadMoreLogs() { if ( - this._firstCursor && - this._loadingPrevState !== "loading" && - this._loadingState === "loaded" && - this._logElement + !this._firstCursor || + this._loadingPrevState === "loading" || + this._loadingState !== "loaded" || + !this._logElement || + !this.provider ) { - const scrolledToBottom = this._scrolledToBottomController.value; - const scrollPositionFromBottom = - this._logElement.scrollHeight - this._logElement.scrollTop; - this._loadingPrevState = "loading"; - const response = await fetchHassioLogs( - this.hass, - this.provider, - `entries=${this._firstCursor}:-100:100`, - this._boot - ); + return; + } + const scrolledToBottom = this._scrolledToBottomController.value; + const scrollPositionFromBottom = + this._logElement.scrollHeight - this._logElement.scrollTop; + this._loadingPrevState = "loading"; + const response = await fetchHassioLogs( + this.hass, + this.provider, + `entries=${this._firstCursor}:-100:100`, + this._boot + ); - if (response.headers.has("X-First-Cursor")) { - if (this._firstCursor === response.headers.get("X-First-Cursor")!) { - this._loadingPrevState = "end"; - return; - } - this._firstCursor = response.headers.get("X-First-Cursor")!; - } - - const body = await response.text(); - - if (body) { - const lines = body - .split("\n") - .filter((line) => line.trim() !== "") - .reverse(); - - this._ansiToHtmlElement?.parseLinesToColoredPre(lines, true); - this._numberOfLines! += lines.length; - this._loadingPrevState = "loaded"; - } else { + if (response.headers.has("X-First-Cursor")) { + if (this._firstCursor === response.headers.get("X-First-Cursor")!) { this._loadingPrevState = "end"; + return; } + this._firstCursor = response.headers.get("X-First-Cursor")!; + } - if (scrolledToBottom) { - this._scrollToBottom(); - } else if (this._loadingPrevState !== "end" && this._logElement) { - window.requestAnimationFrame(() => { - this._logElement!.scrollTop = - this._logElement!.scrollHeight - scrollPositionFromBottom; - }); - } + const body = await response.text(); + + if (body) { + const lines = body + .split("\n") + .filter((line) => line.trim() !== "") + .reverse(); + + this._ansiToHtmlElement?.parseLinesToColoredPre(lines, true); + this._numberOfLines! += lines.length; + this._loadingPrevState = "loaded"; + } else { + this._loadingPrevState = "end"; + } + + if (scrolledToBottom) { + this._scrollToBottom(); + } else if (this._loadingPrevState !== "end" && this._logElement) { + window.requestAnimationFrame(() => { + this._logElement!.scrollTop = + this._logElement!.scrollHeight - scrollPositionFromBottom; + }); } } @@ -694,7 +683,15 @@ class ErrorLogCard extends LitElement { } private _handleOverflowAction(ev: CustomEvent) { - switch (ev.detail.index) { + let index = ev.detail.index; + if (this.provider === "core") { + index--; + } + switch (index) { + case -1: + // @ts-ignore + fireEvent(this, "switch-log-view"); + break; case 0: this._showBootsSelect = !this._showBootsSelect; break; diff --git a/src/panels/config/logs/ha-config-logs.ts b/src/panels/config/logs/ha-config-logs.ts index 4b15706054..3d6ae423c7 100644 --- a/src/panels/config/logs/ha-config-logs.ts +++ b/src/panels/config/logs/ha-config-logs.ts @@ -3,20 +3,20 @@ import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { navigate } from "../../../common/navigate"; import { extractSearchParam } from "../../../common/url/search-params"; -import "../../../components/ha-button-menu"; import "../../../components/ha-button"; +import "../../../components/ha-button-menu"; import "../../../components/search-input"; import type { LogProvider } from "../../../data/error_log"; import { fetchHassioAddonsInfo } from "../../../data/hassio/addon"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import "./error-log-card"; import "./system-log-card"; import type { SystemLogCard } from "./system-log-card"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import { navigate } from "../../../common/navigate"; const logProviders: LogProvider[] = [ { @@ -57,6 +57,8 @@ export class HaConfigLogs extends LitElement { @state() private _filter = extractSearchParam("filter") || ""; + @state() private _detail = false; + @query("system-log-card") private systemLog?: SystemLogCard; @state() private _selectedLogProvider = "core"; @@ -141,7 +143,7 @@ export class HaConfigLogs extends LitElement { : ""} ${search}
- ${this._selectedLogProvider === "core" + ${this._selectedLogProvider === "core" && !this._detail ? html` p.key === this._selectedLogProvider )!.name} .filter=${this._filter} + @switch-log-view=${this._showDetail} > ` - : ""} - p.key === this._selectedLogProvider - )!.name} - .filter=${this._filter} - .provider=${this._selectedLogProvider} - .show=${this._selectedLogProvider !== "core"} - > + : html` p.key === this._selectedLogProvider + )!.name} + .filter=${this._filter} + .provider=${this._selectedLogProvider} + @switch-log-view=${this._showDetail} + allow-switch + >`}
`; } + private _showDetail() { + this._detail = !this._detail; + } + private _selectProvider(ev) { this._selectedLogProvider = (ev.currentTarget as any).provider; this._filter = ""; diff --git a/src/panels/config/logs/system-log-card.ts b/src/panels/config/logs/system-log-card.ts index 1e1a9d12c1..60ef7284d0 100644 --- a/src/panels/config/logs/system-log-card.ts +++ b/src/panels/config/logs/system-log-card.ts @@ -1,16 +1,20 @@ -import { mdiRefresh } from "@mdi/js"; import "@material/mwc-list/mwc-list"; +import { mdiDotsVertical, mdiDownload, mdiRefresh, mdiText } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../common/dom/fire_event"; import type { LocalizeFunc } from "../../../common/translations/localize"; import "../../../components/buttons/ha-call-service-button"; import "../../../components/buttons/ha-progress-button"; +import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-circular-progress"; import "../../../components/ha-icon-button"; import "../../../components/ha-list-item"; +import { getSignedPath } from "../../../data/auth"; +import { getErrorLogDownloadUrl } from "../../../data/error_log"; import { domainToName } from "../../../data/integration"; import type { LoggedError } from "../../../data/system_log"; import { @@ -19,6 +23,7 @@ import { isCustomIntegrationError, } from "../../../data/system_log"; import type { HomeAssistant } from "../../../types"; +import { fileDownload } from "../../../util/file_download"; import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail"; import { formatSystemLogTime } from "./util"; @@ -104,11 +109,34 @@ export class SystemLogCard extends LitElement { : html`

${this.header || "Logs"}

- +
+ + + + + + + + + ${this.hass.localize( + "ui.panel.config.logs.show_full_logs" + )} + + +
${this._items.length === 0 ? html` @@ -195,6 +223,19 @@ export class SystemLogCard extends LitElement { } } + private _handleOverflowAction() { + // @ts-ignore + fireEvent(this, "switch-log-view"); + } + + private async _downloadLogs() { + const timeString = new Date().toISOString().replace(/:/g, "-"); + const downloadUrl = getErrorLogDownloadUrl; + const logFileName = `home-assistant_${timeString}.log`; + const signedUrl = await getSignedPath(this.hass, downloadUrl); + fileDownload(signedUrl.path, logFileName); + } + private _openLog(ev: Event): void { const item = (ev.currentTarget as any).logItem; showSystemLogDetailDialog(this, { item }); @@ -203,7 +244,7 @@ export class SystemLogCard extends LitElement { static get styles(): CSSResultGroup { return css` ha-card { - padding-top: 16px; + padding-top: 8px; } .header { @@ -212,6 +253,11 @@ export class SystemLogCard extends LitElement { padding: 0 16px; } + .header-buttons { + display: flex; + align-items: center; + } + .card-header { color: var(--ha-card-header-color, var(--primary-text-color)); font-family: var(--ha-card-header-font-family, inherit); @@ -243,6 +289,10 @@ export class SystemLogCard extends LitElement { color: var(--warning-color); } + .card-content { + border-top: 1px solid var(--divider-color); + } + .card-actions, .empty-content { direction: var(--direction); diff --git a/src/panels/lovelace/cards/hui-recovery-mode-card.ts b/src/panels/lovelace/cards/hui-recovery-mode-card.ts index 10102b6dd7..37d979dc19 100644 --- a/src/panels/lovelace/cards/hui-recovery-mode-card.ts +++ b/src/panels/lovelace/cards/hui-recovery-mode-card.ts @@ -31,7 +31,7 @@ export class HuiRecoveryModeCard extends LitElement implements LovelaceCard { "ui.panel.lovelace.cards.recovery-mode.description" )}
- +
`; } diff --git a/src/translations/en.json b/src/translations/en.json index 2762e21788..47865affb5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2470,9 +2470,9 @@ "search": "Search logs", "failed_get_logs": "Failed to get {provider} logs, {error}", "no_issues_search": "No issues found for search term ''{term}''", - "load_logs": "Load full logs", + "load_logs": "Load logs", "nr_of_lines": "Number of lines", - "loading_log": "Loading full log…", + "loading_log": "Loading log…", "no_errors": "No errors have been reported", "no_issues": "There are no new issues!", "clear": "Clear", @@ -2489,7 +2489,8 @@ }, "custom_integration": "custom integration", "error_from_custom_integration": "This error originated from a custom integration.", - "show_full_logs": "Show full logs", + "show_full_logs": "Show raw logs", + "show_condensed_logs": "Show condensed logs", "select_number_of_lines": "Select number of lines to download", "lines": "Lines", "download_logs": "Download logs",