move download logs button, switch between raw and normal logs (#22721)

This commit is contained in:
Bram Kragten 2024-11-07 21:32:28 +01:00 committed by GitHub
parent 4c898a2a5a
commit 9f55ef811d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 200 additions and 146 deletions

View File

@ -47,7 +47,6 @@ class HassioAddonLogDashboard extends LitElement {
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.header=${this.addon.name} .header=${this.addon.name}
.provider=${this.addon.slug} .provider=${this.addon.slug}
show
.filter=${this._filter} .filter=${this._filter}
> >
</error-log-card> </error-log-card>

View File

@ -11,6 +11,7 @@ import {
mdiRefresh, mdiRefresh,
mdiWrap, mdiWrap,
mdiWrapDisabled, mdiWrapDisabled,
mdiFolderTextOutline,
} from "@mdi/js"; } from "@mdi/js";
import { import {
css, css,
@ -58,7 +59,7 @@ import {
downloadFileSupported, downloadFileSupported,
fileDownload, fileDownload,
} from "../../../util/file_download"; } 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 type { ConnectionStatus } from "../../../data/connection-status";
import { atLeastVersion } from "../../../common/config/version"; import { atLeastVersion } from "../../../common/config/version";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@ -79,9 +80,10 @@ class ErrorLogCard extends LitElement {
@property() public header?: string; @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; @query(".error-log") private _logElement?: HTMLElement;
@ -130,26 +132,32 @@ class ErrorLogCard extends LitElement {
@state() private _wrapLines = true; @state() private _wrapLines = true;
@state() private _downloadSupported; @state() private _downloadSupported?: boolean;
@state() private _logsFileLink; @state() private _logsFileLink?: string;
protected render(): TemplateResult { 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; const localize = this.localizeFunc || this.hass.localize;
return html` return html`
<div class="error-log-intro"> <div class="error-log-intro">
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing} : nothing}
<ha-card outlined class=${classMap({ hidden: this.show === false })}> <ha-card outlined>
<div class="header"> <div class="header">
<h1 class="card-header"> <h1 class="card-header">
${this.header || localize("ui.panel.config.logs.show_full_logs")} ${this.header || localize("ui.panel.config.logs.show_full_logs")}
</h1> </h1>
<div class="action-buttons"> <div class="action-buttons">
${this._streamSupported && ${hasBoots && this._showBootsSelect
Array.isArray(this._boots) &&
this._showBootsSelect
? html` ? html`
<ha-assist-chip <ha-assist-chip
.title=${localize( .title=${localize(
@ -175,7 +183,7 @@ class ErrorLogCard extends LitElement {
id="boots-menu" id="boots-menu"
positioning="fixed" positioning="fixed"
> >
${this._boots.map( ${this._boots!.map(
(boot) => html` (boot) => html`
<ha-md-menu-item <ha-md-menu-item
.value=${boot} .value=${boot}
@ -232,19 +240,31 @@ class ErrorLogCard extends LitElement {
`ui.panel.config.logs.${this._wrapLines ? "full_width" : "wrap_lines"}` `ui.panel.config.logs.${this._wrapLines ? "full_width" : "wrap_lines"}`
)} )}
></ha-icon-button> ></ha-icon-button>
${!this._streamSupported || this._error ${!streaming || this._error
? html`<ha-icon-button ? html`<ha-icon-button
.path=${mdiRefresh} .path=${mdiRefresh}
@click=${this._loadLogs} @click=${this._loadLogs}
.label=${localize("ui.common.refresh")} .label=${localize("ui.common.refresh")}
></ha-icon-button>` ></ha-icon-button>`
: nothing} : nothing}
${this._streamSupported && Array.isArray(this._boots) ${(this.allowSwitch && this.provider === "core") || hasBoots
? html` ? html`
<ha-button-menu @action=${this._handleOverflowAction}> <ha-button-menu @action=${this._handleOverflowAction}>
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}> <ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
</ha-icon-button> </ha-icon-button>
<ha-list-item graphic="icon"> ${this.allowSwitch && this.provider === "core"
? html`<ha-list-item graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiFolderTextOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.logs.show_condensed_logs"
)}
</ha-list-item>`
: nothing}
${hasBoots
? html`<ha-list-item graphic="icon">
<ha-svg-icon <ha-svg-icon
slot="graphic" slot="graphic"
.path=${mdiFormatListNumbered} .path=${mdiFormatListNumbered}
@ -252,7 +272,8 @@ class ErrorLogCard extends LitElement {
${localize( ${localize(
`ui.panel.config.logs.${this._showBootsSelect ? "hide" : "show"}_haos_boots` `ui.panel.config.logs.${this._showBootsSelect ? "hide" : "show"}_haos_boots`
)} )}
</ha-list-item> </ha-list-item>`
: nothing}
</ha-button-menu> </ha-button-menu>
` `
: nothing} : nothing}
@ -305,48 +326,34 @@ class ErrorLogCard extends LitElement {
slot="trailingIcon" slot="trailingIcon"
></ha-svg-icon> ></ha-svg-icon>
</ha-button> </ha-button>
${this._streamSupported && ${streaming && this._boot === 0 && !this._error
this._loadingState !== "loading" &&
this._boot === 0 &&
!this._error
? html`<div class="live-indicator"> ? html`<div class="live-indicator">
<ha-svg-icon path=${mdiCircle}></ha-svg-icon> <ha-svg-icon path=${mdiCircle}></ha-svg-icon>
Live Live
</div>` </div>`
: nothing} : nothing}
</ha-card> </ha-card>
${this.show === false
? html`
${this._downloadSupported
? html`
<ha-button outlined @click=${this._downloadLogs}>
<ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
${localize("ui.panel.config.logs.download_logs")}
</ha-button>
`
: nothing}
<mwc-button raised @click=${this._showLogs}>
${localize("ui.panel.config.logs.load_logs")}
</mwc-button>
`
: nothing}
</div> </div>
`; `;
} }
public connectedCallback() { protected willUpdate(changedProps: PropertyValues) {
super.connectedCallback(); super.willUpdate(changedProps);
if (changedProps.has("provider")) {
if (this._streamSupported === undefined) { this._boot = 0;
this._streamSupported = atLeastVersion( this._loadLogs();
this.hass.config.version,
2024,
11
);
} }
if (this._downloadSupported === undefined && this.hass) { if (this.hasUpdated) {
return;
}
this._streamSupported = atLeastVersion(this.hass.config.version, 2024, 11);
this._downloadSupported = downloadFileSupported(this.hass); this._downloadSupported = downloadFileSupported(this.hass);
} // just needs to be loaded once, because only the host endpoints provide boots information
this._loadBoots();
window.addEventListener("connection-status", this._handleConnectionStatus);
this.hass.loadFragmentTranslation("config");
} }
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
@ -356,28 +363,11 @@ class ErrorLogCard extends LitElement {
this._scrolledToTopController.callback = this._handleTopScroll; this._scrolledToTopController.callback = this._handleTopScroll;
this._scrolledToTopController.observe(this._scrollTopMarkerElement!); 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) { protected updated(changedProps) {
super.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) { if (this._newLogsIndicator && this._scrolledToBottomController.value) {
this._newLogsIndicator = false; this._newLogsIndicator = false;
} }
@ -411,7 +401,7 @@ class ErrorLogCard extends LitElement {
} }
private async _downloadLogs(): Promise<void> { private async _downloadLogs(): Promise<void> {
if (this._streamSupported) { if (this._streamSupported && this.provider) {
showDownloadLogsDialog(this, { showDownloadLogsDialog(this, {
header: this.header, header: this.header,
provider: this.provider, provider: this.provider,
@ -433,10 +423,6 @@ class ErrorLogCard extends LitElement {
} }
} }
private _showLogs(): void {
this.show = true;
}
private async _loadLogs(): Promise<void> { private async _loadLogs(): Promise<void> {
this._error = undefined; this._error = undefined;
this._loadingState = "loading"; this._loadingState = "loading";
@ -448,15 +434,16 @@ class ErrorLogCard extends LitElement {
try { try {
if (this._logStreamAborter) { if (this._logStreamAborter) {
this._logStreamAborter.abort(); this._logStreamAborter.abort();
this._logStreamAborter = undefined;
} }
this._logStreamAborter = new AbortController();
if ( if (
this._streamSupported && this._streamSupported &&
isComponentLoaded(this.hass, "hassio") && isComponentLoaded(this.hass, "hassio") &&
this.provider this.provider
) { ) {
this._logStreamAborter = new AbortController();
// check if there are any logs at all // check if there are any logs at all
const testResponse = await fetchHassioLogs( const testResponse = await fetchHassioLogs(
this.hass, this.hass,
@ -599,18 +586,21 @@ class ErrorLogCard extends LitElement {
if (ev.detail === "disconnected" && this._logStreamAborter) { if (ev.detail === "disconnected" && this._logStreamAborter) {
this._logStreamAborter.abort(); this._logStreamAborter.abort();
} }
if (ev.detail === "connected" && this.show) { if (ev.detail === "connected") {
this._loadLogs(); this._loadLogs();
} }
}; };
private async _loadMoreLogs() { private async _loadMoreLogs() {
if ( if (
this._firstCursor && !this._firstCursor ||
this._loadingPrevState !== "loading" && this._loadingPrevState === "loading" ||
this._loadingState === "loaded" && this._loadingState !== "loaded" ||
this._logElement !this._logElement ||
!this.provider
) { ) {
return;
}
const scrolledToBottom = this._scrolledToBottomController.value; const scrolledToBottom = this._scrolledToBottomController.value;
const scrollPositionFromBottom = const scrollPositionFromBottom =
this._logElement.scrollHeight - this._logElement.scrollTop; this._logElement.scrollHeight - this._logElement.scrollTop;
@ -654,7 +644,6 @@ class ErrorLogCard extends LitElement {
}); });
} }
} }
}
private _handleTopScroll = (entries) => { private _handleTopScroll = (entries) => {
const isVisible = entries[0].isIntersecting; const isVisible = entries[0].isIntersecting;
@ -694,7 +683,15 @@ class ErrorLogCard extends LitElement {
} }
private _handleOverflowAction(ev: CustomEvent<ActionDetail>) { private _handleOverflowAction(ev: CustomEvent<ActionDetail>) {
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: case 0:
this._showBootsSelect = !this._showBootsSelect; this._showBootsSelect = !this._showBootsSelect;
break; break;

View File

@ -3,20 +3,20 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { navigate } from "../../../common/navigate";
import { extractSearchParam } from "../../../common/url/search-params"; import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-button-menu";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/search-input"; import "../../../components/search-input";
import type { LogProvider } from "../../../data/error_log"; import type { LogProvider } from "../../../data/error_log";
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon"; import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types"; import type { HomeAssistant, Route } from "../../../types";
import "./error-log-card"; import "./error-log-card";
import "./system-log-card"; import "./system-log-card";
import type { SystemLogCard } from "./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[] = [ const logProviders: LogProvider[] = [
{ {
@ -57,6 +57,8 @@ export class HaConfigLogs extends LitElement {
@state() private _filter = extractSearchParam("filter") || ""; @state() private _filter = extractSearchParam("filter") || "";
@state() private _detail = false;
@query("system-log-card") private systemLog?: SystemLogCard; @query("system-log-card") private systemLog?: SystemLogCard;
@state() private _selectedLogProvider = "core"; @state() private _selectedLogProvider = "core";
@ -141,7 +143,7 @@ export class HaConfigLogs extends LitElement {
: ""} : ""}
${search} ${search}
<div class="content"> <div class="content">
${this._selectedLogProvider === "core" ${this._selectedLogProvider === "core" && !this._detail
? html` ? html`
<system-log-card <system-log-card
.hass=${this.hass} .hass=${this.hass}
@ -149,23 +151,28 @@ export class HaConfigLogs extends LitElement {
(p) => p.key === this._selectedLogProvider (p) => p.key === this._selectedLogProvider
)!.name} )!.name}
.filter=${this._filter} .filter=${this._filter}
@switch-log-view=${this._showDetail}
></system-log-card> ></system-log-card>
` `
: ""} : html`<error-log-card
<error-log-card
.hass=${this.hass} .hass=${this.hass}
.header=${this._logProviders.find( .header=${this._logProviders.find(
(p) => p.key === this._selectedLogProvider (p) => p.key === this._selectedLogProvider
)!.name} )!.name}
.filter=${this._filter} .filter=${this._filter}
.provider=${this._selectedLogProvider} .provider=${this._selectedLogProvider}
.show=${this._selectedLogProvider !== "core"} @switch-log-view=${this._showDetail}
></error-log-card> allow-switch
></error-log-card>`}
</div> </div>
</hass-subpage> </hass-subpage>
`; `;
} }
private _showDetail() {
this._detail = !this._detail;
}
private _selectProvider(ev) { private _selectProvider(ev) {
this._selectedLogProvider = (ev.currentTarget as any).provider; this._selectedLogProvider = (ev.currentTarget as any).provider;
this._filter = ""; this._filter = "";

View File

@ -1,16 +1,20 @@
import { mdiRefresh } from "@mdi/js";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import { mdiDotsVertical, mdiDownload, mdiRefresh, mdiText } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize"; import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/buttons/ha-call-service-button"; import "../../../components/buttons/ha-call-service-button";
import "../../../components/buttons/ha-progress-button"; import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
import { getSignedPath } from "../../../data/auth";
import { getErrorLogDownloadUrl } from "../../../data/error_log";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import type { LoggedError } from "../../../data/system_log"; import type { LoggedError } from "../../../data/system_log";
import { import {
@ -19,6 +23,7 @@ import {
isCustomIntegrationError, isCustomIntegrationError,
} from "../../../data/system_log"; } from "../../../data/system_log";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail"; import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
import { formatSystemLogTime } from "./util"; import { formatSystemLogTime } from "./util";
@ -104,11 +109,34 @@ export class SystemLogCard extends LitElement {
: html` : html`
<div class="header"> <div class="header">
<h1 class="card-header">${this.header || "Logs"}</h1> <h1 class="card-header">${this.header || "Logs"}</h1>
<div class="header-buttons">
<ha-icon-button
.path=${mdiDownload}
@click=${this._downloadLogs}
.label=${this.hass.localize(
"ui.panel.config.logs.download_logs"
)}
></ha-icon-button>
<ha-icon-button <ha-icon-button
.path=${mdiRefresh} .path=${mdiRefresh}
@click=${this.fetchData} @click=${this.fetchData}
.label=${this.hass.localize("ui.common.refresh")} .label=${this.hass.localize("ui.common.refresh")}
></ha-icon-button> ></ha-icon-button>
<ha-button-menu @action=${this._handleOverflowAction}>
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
</ha-icon-button>
<ha-list-item graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiText}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.logs.show_full_logs"
)}
</ha-list-item>
</ha-button-menu>
</div>
</div> </div>
${this._items.length === 0 ${this._items.length === 0
? html` ? 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 { private _openLog(ev: Event): void {
const item = (ev.currentTarget as any).logItem; const item = (ev.currentTarget as any).logItem;
showSystemLogDetailDialog(this, { item }); showSystemLogDetailDialog(this, { item });
@ -203,7 +244,7 @@ export class SystemLogCard extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-card { ha-card {
padding-top: 16px; padding-top: 8px;
} }
.header { .header {
@ -212,6 +253,11 @@ export class SystemLogCard extends LitElement {
padding: 0 16px; padding: 0 16px;
} }
.header-buttons {
display: flex;
align-items: center;
}
.card-header { .card-header {
color: var(--ha-card-header-color, var(--primary-text-color)); color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit); font-family: var(--ha-card-header-font-family, inherit);
@ -243,6 +289,10 @@ export class SystemLogCard extends LitElement {
color: var(--warning-color); color: var(--warning-color);
} }
.card-content {
border-top: 1px solid var(--divider-color);
}
.card-actions, .card-actions,
.empty-content { .empty-content {
direction: var(--direction); direction: var(--direction);

View File

@ -31,7 +31,7 @@ export class HuiRecoveryModeCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.recovery-mode.description" "ui.panel.lovelace.cards.recovery-mode.description"
)} )}
</div> </div>
<error-log-card .hass=${this.hass}></error-log-card> <error-log-card .hass=${this.hass} provider="core"></error-log-card>
</ha-card> </ha-card>
`; `;
} }

View File

@ -2470,9 +2470,9 @@
"search": "Search logs", "search": "Search logs",
"failed_get_logs": "Failed to get {provider} logs, {error}", "failed_get_logs": "Failed to get {provider} logs, {error}",
"no_issues_search": "No issues found for search term ''{term}''", "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", "nr_of_lines": "Number of lines",
"loading_log": "Loading full log…", "loading_log": "Loading log…",
"no_errors": "No errors have been reported", "no_errors": "No errors have been reported",
"no_issues": "There are no new issues!", "no_issues": "There are no new issues!",
"clear": "Clear", "clear": "Clear",
@ -2489,7 +2489,8 @@
}, },
"custom_integration": "custom integration", "custom_integration": "custom integration",
"error_from_custom_integration": "This error originated from a 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", "select_number_of_lines": "Select number of lines to download",
"lines": "Lines", "lines": "Lines",
"download_logs": "Download logs", "download_logs": "Download logs",