diff --git a/src/external_app/external_messaging.ts b/src/external_app/external_messaging.ts index a870b72be2ff..292746bdee17 100644 --- a/src/external_app/external_messaging.ts +++ b/src/external_app/external_messaging.ts @@ -264,6 +264,7 @@ export interface ExternalConfig { hasAssist: boolean; hasBarCodeScanner: number; canSetupImprov: boolean; + downloadFileSupported: boolean; } export class ExternalMessaging { diff --git a/src/panels/config/logs/dialog-download-logs.ts b/src/panels/config/logs/dialog-download-logs.ts index 0a7e9d965f40..801ef1db2262 100644 --- a/src/panels/config/logs/dialog-download-logs.ts +++ b/src/panels/config/logs/dialog-download-logs.ts @@ -17,19 +17,21 @@ import type { HomeAssistant } from "../../../types"; import { fileDownload } from "../../../util/file_download"; import type { DownloadLogsDialogParams } from "./show-dialog-download-logs"; +const DEFAULT_LINE_COUNT = 500; + @customElement("dialog-download-logs") class DownloadLogsDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _dialogParams?: DownloadLogsDialogParams; - @state() private _lineCount = 100; + @state() private _lineCount = DEFAULT_LINE_COUNT; @query("ha-md-dialog") private _dialogElement!: HaMdDialog; public showDialog(dialogParams: DownloadLogsDialogParams) { this._dialogParams = dialogParams; - this._lineCount = this._dialogParams?.defaultLineCount ?? 100; + this._lineCount = this._dialogParams?.defaultLineCount || 500; } public closeDialog() { @@ -38,7 +40,7 @@ class DownloadLogsDialog extends LitElement { private _dialogClosed() { this._dialogParams = undefined; - this._lineCount = 100; + this._lineCount = DEFAULT_LINE_COUNT; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -48,7 +50,7 @@ class DownloadLogsDialog extends LitElement { } const numberOfLinesOptions = [100, 500, 1000, 5000, 10000]; - if (!numberOfLinesOptions.includes(this._lineCount)) { + if (!numberOfLinesOptions.includes(this._lineCount) && this._lineCount) { numberOfLinesOptions.push(this._lineCount); numberOfLinesOptions.sort((a, b) => a - b); } @@ -63,7 +65,7 @@ class DownloadLogsDialog extends LitElement { .path=${mdiClose} > - ${this.hass.localize("ui.panel.config.logs.download_full_log")} + ${this.hass.localize("ui.panel.config.logs.download_logs")} ${this._dialogParams.header}${this._dialogParams.boot === 0 @@ -95,7 +97,7 @@ class DownloadLogsDialog extends LitElement { ${this.hass.localize("ui.common.cancel")} - + ${this.hass.localize("ui.common.download")} @@ -103,7 +105,7 @@ class DownloadLogsDialog extends LitElement { `; } - private async _dowloadLogs() { + private async _downloadLogs() { const provider = this._dialogParams!.provider; const boot = this._dialogParams!.boot; diff --git a/src/panels/config/logs/error-log-card.ts b/src/panels/config/logs/error-log-card.ts index 259a87ca7d12..d7d2bc430ec1 100644 --- a/src/panels/config/logs/error-log-card.ts +++ b/src/panels/config/logs/error-log-card.ts @@ -1,6 +1,7 @@ import "@material/mwc-list/mwc-list-item"; import { mdiArrowCollapseDown, + mdiCircle, mdiDownload, mdiMenuDown, mdiRefresh, @@ -40,10 +41,14 @@ import { fetchHassioBoots, fetchHassioLogs, fetchHassioLogsFollow, + getHassioLogDownloadLinesUrl, getHassioLogDownloadUrl, } from "../../../data/hassio/supervisor"; import type { HomeAssistant } from "../../../types"; -import { fileDownload } from "../../../util/file_download"; +import { + downloadFileSupported, + fileDownload, +} from "../../../util/file_download"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { ConnectionStatus } from "../../../data/connection-status"; import { atLeastVersion } from "../../../common/config/version"; @@ -109,6 +114,10 @@ class ErrorLogCard extends LitElement { @state() private _boots?: number[]; + @state() private _downloadSupported; + + @state() private _logsFileLink; + protected render(): TemplateResult { return html`
@@ -176,13 +185,32 @@ class ErrorLogCard extends LitElement { ` : nothing} - + ${this._downloadSupported + ? html` + + ` + : this._logsFileLink + ? html` + + + + ` + : nothing} ${!this._streamSupported || this._error ? html` + ${this._streamSupported && + this._loadingState !== "loading" && + !this._error + ? html`
+ + Live +
` + : nothing} ${this.show === false ? html` - - - ${this.hass.localize("ui.panel.config.logs.download_full_log")} - + ${this._downloadSupported + ? html` + + + ${this.hass.localize( + "ui.panel.config.logs.download_logs" + )} + + ` + : nothing} ${this.hass.localize("ui.panel.config.logs.load_logs")} @@ -268,6 +310,9 @@ class ErrorLogCard extends LitElement { 11 ); } + if (this._downloadSupported === undefined && this.hass) { + this._downloadSupported = downloadFileSupported(this.hass); + } } protected firstUpdated(changedProps: PropertyValues) { @@ -331,7 +376,7 @@ class ErrorLogCard extends LitElement { ); } - private async _downloadFullLog(): Promise { + private async _downloadLogs(): Promise { if (this._streamSupported) { showDownloadLogsDialog(this, { header: this.header, @@ -378,6 +423,18 @@ class ErrorLogCard extends LitElement { isComponentLoaded(this.hass, "hassio") && this.provider ) { + // check if there are any logs at all + const testResponse = await fetchHassioLogs( + this.hass, + this.provider, + `entries=:-1:`, + this._boot + ); + const testLogs = await testResponse.text(); + if (!testLogs.trim()) { + this._loadingState = "empty"; + } + const response = await fetchHassioLogsFollow( this.hass, this.provider, @@ -438,6 +495,17 @@ class ErrorLogCard extends LitElement { } else { this._newLogsIndicator = true; } + + if (!this._downloadSupported) { + const downloadUrl = getHassioLogDownloadLinesUrl( + this.provider, + this._numberOfLines, + this._boot + ); + getSignedPath(this.hass, downloadUrl).then((signedUrl) => { + this._logsFileLink = signedUrl.path; + }); + } } } } else { @@ -597,6 +665,9 @@ class ErrorLogCard extends LitElement { } static styles: CSSResultGroup = css` + :host { + direction: var(--direction); + } .error-log-intro { text-align: center; margin: 16px; @@ -646,7 +717,7 @@ class ErrorLogCard extends LitElement { position: relative; font-family: var(--code-font-family, monospace); clear: both; - text-align: left; + text-align: start; padding-top: 12px; padding-bottom: 12px; overflow-y: scroll; @@ -713,6 +784,36 @@ class ErrorLogCard extends LitElement { --ha-assist-chip-container-shape: 10px; --md-assist-chip-trailing-space: 8px; } + + @keyframes breathe { + from { + opacity: 0.8; + } + to { + opacity: 0; + } + } + + .live-indicator { + position: absolute; + bottom: 0; + inset-inline-end: 16px; + border-top-right-radius: 8px; + border-top-left-radius: 8px; + background-color: var(--primary-color); + color: var(--text-primary-color); + padding: 4px 8px; + opacity: 0.8; + } + .live-indicator ha-svg-icon { + animation: breathe 1s cubic-bezier(0.5, 0, 1, 1) infinite alternate; + height: 14px; + width: 14px; + } + + .download-link { + color: var(--text-color); + } `; } diff --git a/src/translations/en.json b/src/translations/en.json index 0c3cd4698d8f..248a77bebc74 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2492,7 +2492,7 @@ "show_full_logs": "Show full logs", "select_number_of_lines": "Select number of lines to download", "lines": "Lines", - "download_full_log": "Download full log", + "download_logs": "Download logs", "scroll_down_button": "New logs - Click to scroll", "provider_not_found": "Log provider not found", "provider_not_available": "Logs for ''{provider}'' are not available on your system.", diff --git a/src/util/file_download.ts b/src/util/file_download.ts index f163b146a55c..dc9ba6b54ad7 100644 --- a/src/util/file_download.ts +++ b/src/util/file_download.ts @@ -1,3 +1,6 @@ +import type { HomeAssistant } from "../types"; +import { isIosApp } from "./is_ios"; + export const fileDownload = (href: string, filename = ""): void => { const a = document.createElement("a"); a.target = "_blank"; @@ -8,3 +11,6 @@ export const fileDownload = (href: string, filename = ""): void => { a.dispatchEvent(new MouseEvent("click")); document.body.removeChild(a); }; + +export const downloadFileSupported = (hass: HomeAssistant): boolean => + !isIosApp(hass) || !!hass.auth.external?.config.downloadFileSupported; diff --git a/src/util/is_ios.ts b/src/util/is_ios.ts new file mode 100644 index 000000000000..b7f72ed2f562 --- /dev/null +++ b/src/util/is_ios.ts @@ -0,0 +1,5 @@ +import type { HomeAssistant } from "../types"; +import { isSafari } from "./is_safari"; + +export const isIosApp = (hass: HomeAssistant): boolean => + isSafari && !!hass.auth.external;