Add select prev boots in error-log-card (#22528)

* Add select prev boots in error-log-card

* Fix boot selector style in error-log-card
This commit is contained in:
Wendelin 2024-10-28 16:41:21 +01:00 committed by GitHub
parent 3180747a0a
commit dc0cab9307
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 145 additions and 11 deletions

View File

@ -65,6 +65,10 @@ export type HassioInfo = {
timezone: string; timezone: string;
}; };
export type HassioBoots = {
boots: Record<number, string>;
};
export type HassioPanelInfo = PanelInfo< export type HassioPanelInfo = PanelInfo<
| undefined | undefined
| { | {
@ -177,14 +181,18 @@ export const fetchHassioInfo = async (
); );
}; };
export const fetchHassioBoots = async (hass: HomeAssistant) =>
hass.callApi<HassioResponse<HassioBoots>>("GET", `hassio/host/logs/boots`);
export const fetchHassioLogs = async ( export const fetchHassioLogs = async (
hass: HomeAssistant, hass: HomeAssistant,
provider: string, provider: string,
range?: string range?: string,
boot = 0
) => ) =>
hass.callApiRaw( hass.callApiRaw(
"GET", "GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`, `hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}`,
undefined, undefined,
range range
? { ? {
@ -197,11 +205,12 @@ export const fetchHassioLogsFollow = async (
hass: HomeAssistant, hass: HomeAssistant,
provider: string, provider: string,
signal: AbortSignal, signal: AbortSignal,
lines = 100 lines = 100,
boot = 0
) => ) =>
hass.callApiRaw( hass.callApiRaw(
"GET", "GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/follow?lines=${lines}`, `hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}/follow?lines=${lines}`,
undefined, undefined,
undefined, undefined,
signal signal
@ -212,10 +221,14 @@ export const getHassioLogDownloadUrl = (provider: string) =>
provider.includes("_") ? `addons/${provider}` : provider provider.includes("_") ? `addons/${provider}` : provider
}/logs`; }/logs`;
export const getHassioLogDownloadLinesUrl = (provider: string, lines: number) => export const getHassioLogDownloadLinesUrl = (
provider: string,
lines: number,
boot = 0
) =>
`/api/hassio/${ `/api/hassio/${
provider.includes("_") ? `addons/${provider}` : provider provider.includes("_") ? `addons/${provider}` : provider
}/logs?lines=${lines}`; }/logs/boots/${boot}?lines=${lines}`;
export const setSupervisorOption = async ( export const setSupervisorOption = async (
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -65,7 +65,11 @@ class DownloadLogsDialog extends LitElement {
<span slot="title" id="dialog-light-color-favorite-title"> <span slot="title" id="dialog-light-color-favorite-title">
${this.hass.localize("ui.panel.config.logs.download_full_log")} ${this.hass.localize("ui.panel.config.logs.download_full_log")}
</span> </span>
<span slot="subtitle"> ${this._dialogParams.header} </span> <span slot="subtitle">
${this._dialogParams.header}${this._dialogParams.boot === 0
? ""
: `${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`}
</span>
</ha-dialog-header> </ha-dialog-header>
<div slot="content" class="content"> <div slot="content" class="content">
<div> <div>
@ -104,9 +108,14 @@ class DownloadLogsDialog extends LitElement {
private async _dowloadLogs() { private async _dowloadLogs() {
const provider = this._dialogParams!.provider; const provider = this._dialogParams!.provider;
const boot = this._dialogParams!.boot;
const timeString = new Date().toISOString().replace(/:/g, "-"); const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl = getHassioLogDownloadLinesUrl(provider, this._lineCount); const downloadUrl = getHassioLogDownloadLinesUrl(
provider,
this._lineCount,
boot
);
const logFileName = const logFileName =
provider !== "core" provider !== "core"
? `${provider}_${timeString}.log` ? `${provider}_${timeString}.log`

View File

@ -1,5 +1,10 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiArrowCollapseDown, mdiDownload, mdiRefresh } from "@mdi/js"; import {
mdiArrowCollapseDown,
mdiDownload,
mdiMenuDown,
mdiRefresh,
} from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -22,12 +27,17 @@ import "../../../components/ha-button";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-md-divider";
import { getSignedPath } from "../../../data/auth"; import { getSignedPath } from "../../../data/auth";
import { fetchErrorLog, getErrorLogDownloadUrl } from "../../../data/error_log"; import { fetchErrorLog, getErrorLogDownloadUrl } from "../../../data/error_log";
import { extractApiErrorMessage } from "../../../data/hassio/common"; import { extractApiErrorMessage } from "../../../data/hassio/common";
import { import {
fetchHassioBoots,
fetchHassioLogs, fetchHassioLogs,
fetchHassioLogsFollow, fetchHassioLogsFollow,
getHassioLogDownloadUrl, getHassioLogDownloadUrl,
@ -40,6 +50,7 @@ import { atLeastVersion } from "../../../common/config/version";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { debounce } from "../../../common/util/debounce"; import { debounce } from "../../../common/util/debounce";
import { showDownloadLogsDialog } from "./show-dialog-download-logs"; import { showDownloadLogsDialog } from "./show-dialog-download-logs";
import type { HaMenu } from "../../../components/ha-menu";
const NUMBER_OF_LINES = 100; const NUMBER_OF_LINES = 100;
@ -64,6 +75,8 @@ class ErrorLogCard extends LitElement {
@query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml; @query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml;
@query("#boots-menu") private _bootsMenu?: HaMenu;
@state() private _firstCursor?: string; @state() private _firstCursor?: string;
@state() private _scrolledToBottomController = @state() private _scrolledToBottomController =
@ -92,6 +105,10 @@ class ErrorLogCard extends LitElement {
@state() private _numberOfLines?: number; @state() private _numberOfLines?: number;
@state() private _boot = 0;
@state() private _boots?: number[];
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="error-log-intro"> <div class="error-log-intro">
@ -105,6 +122,60 @@ class ErrorLogCard extends LitElement {
this.hass.localize("ui.panel.config.logs.show_full_logs")} this.hass.localize("ui.panel.config.logs.show_full_logs")}
</h1> </h1>
<div class="action-buttons"> <div class="action-buttons">
${this._streamSupported && Array.isArray(this._boots)
? html`
<ha-assist-chip
.label=${this._boot === 0
? this.hass.localize("ui.panel.config.logs.current")
: this._boot === -1
? this.hass.localize("ui.panel.config.logs.previous")
: this.hass.localize(
"ui.panel.config.logs.startups_ago",
{ boot: this._boot * -1 }
)}
id="boots-anchor"
@click=${this._toggleBootsMenu}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
<ha-menu
anchor="boots-anchor"
id="boots-menu"
positioning="fixed"
>
${this._boots.map(
(boot) => html`
<ha-md-menu-item
.value=${boot}
@click=${this._setBoot}
.selected=${boot === this._boot}
>
${boot === 0
? this.hass.localize(
"ui.panel.config.logs.current"
)
: boot === -1
? this.hass.localize(
"ui.panel.config.logs.previous"
)
: this.hass.localize(
"ui.panel.config.logs.startups_ago",
{ boot: boot * -1 }
)}
</ha-md-menu-item>
${boot === 0
? html`<ha-md-divider
role="separator"
></ha-md-divider>`
: nothing}
`
)}
</ha-menu>
`
: nothing}
<ha-icon-button <ha-icon-button
.path=${mdiDownload} .path=${mdiDownload}
@click=${this._downloadFullLog} @click=${this._downloadFullLog}
@ -212,6 +283,9 @@ class ErrorLogCard extends LitElement {
if (this.hass?.config.recovery_mode || this.show) { if (this.hass?.config.recovery_mode || this.show) {
this.hass.loadFragmentTranslation("config"); 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) {
@ -221,6 +295,7 @@ class ErrorLogCard extends LitElement {
(changedProps.has("show") && this.show) || (changedProps.has("show") && this.show) ||
(changedProps.has("provider") && this.show) (changedProps.has("provider") && this.show)
) { ) {
this._boot = 0;
this._loadLogs(); this._loadLogs();
} }
@ -262,6 +337,7 @@ class ErrorLogCard extends LitElement {
header: this.header, header: this.header,
provider: this.provider, provider: this.provider,
defaultLineCount: this._numberOfLines, defaultLineCount: this._numberOfLines,
boot: this._boot,
}); });
} else { } else {
const timeString = new Date().toISOString().replace(/:/g, "-"); const timeString = new Date().toISOString().replace(/:/g, "-");
@ -306,7 +382,8 @@ class ErrorLogCard extends LitElement {
this.hass, this.hass,
this.provider, this.provider,
this._logStreamAborter.signal, this._logStreamAborter.signal,
NUMBER_OF_LINES NUMBER_OF_LINES,
this._boot
); );
if (response.headers.has("X-First-Cursor")) { if (response.headers.has("X-First-Cursor")) {
@ -437,7 +514,8 @@ class ErrorLogCard extends LitElement {
const response = await fetchHassioLogs( const response = await fetchHassioLogs(
this.hass, this.hass,
this.provider, this.provider,
`entries=${this._firstCursor}:-100:100` `entries=${this._firstCursor}:-100:100`,
this._boot
); );
if (response.headers.has("X-First-Cursor")) { if (response.headers.has("X-First-Cursor")) {
@ -488,6 +566,31 @@ class ErrorLogCard extends LitElement {
return isVisible; return isVisible;
}; };
private async _loadBoots() {
if (this._streamSupported && isComponentLoaded(this.hass, "hassio")) {
try {
const { data } = await fetchHassioBoots(this.hass);
this._boots = Object.keys(data.boots)
.map(Number)
.sort((a, b) => b - a);
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
}
}
}
private _toggleBootsMenu() {
if (this._bootsMenu) {
this._bootsMenu.open = !this._bootsMenu.open;
}
}
private _setBoot(ev: any) {
this._boot = ev.target.value;
this._loadLogs();
}
static styles: CSSResultGroup = css` static styles: CSSResultGroup = css`
.error-log-intro { .error-log-intro {
text-align: center; text-align: center;
@ -600,6 +703,11 @@ class ErrorLogCard extends LitElement {
justify-content: center; justify-content: center;
padding: 16px; padding: 16px;
} }
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
--md-assist-chip-trailing-space: 8px;
}
`; `;
} }

View File

@ -4,6 +4,7 @@ export interface DownloadLogsDialogParams {
header?: string; header?: string;
provider: string; provider: string;
defaultLineCount?: number; defaultLineCount?: number;
boot: number;
} }
export const showDownloadLogsDialog = ( export const showDownloadLogsDialog = (

View File

@ -2490,6 +2490,9 @@
"scroll_down_button": "New logs - Click to scroll", "scroll_down_button": "New logs - Click to scroll",
"provider_not_found": "Log provider not found", "provider_not_found": "Log provider not found",
"provider_not_available": "Logs for ''{provider}'' are not available on your system.", "provider_not_available": "Logs for ''{provider}'' are not available on your system.",
"current": "Current",
"previous": "Previous",
"startups_ago": "{boot} startups ago",
"detail": { "detail": {
"logger": "Logger", "logger": "Logger",
"source": "Source", "source": "Source",