Move updates (#10626)

This commit is contained in:
Joakim Sørensen 2021-11-17 19:21:27 +01:00 committed by GitHub
parent 481da19c74
commit e9f0967578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 829 additions and 626 deletions

View File

@ -1,3 +1,4 @@
import "../../../src/components/ha-logo-svg";
import { html, css, LitElement, TemplateResult } from "lit"; import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "../../../src/components/ha-alert"; import "../../../src/components/ha-alert";
@ -10,6 +11,8 @@ const alerts: {
dismissable?: boolean; dismissable?: boolean;
action?: string; action?: string;
rtl?: boolean; rtl?: boolean;
iconSlot?: TemplateResult;
actionSlot?: TemplateResult;
}[] = [ }[] = [
{ {
title: "Test info alert", title: "Test info alert",
@ -81,6 +84,18 @@ const alerts: {
type: "warning", type: "warning",
action: "save", action: "save",
}, },
{
title: "Slotted icon",
description: "Alert with slotted icon",
type: "warning",
iconSlot: html`<ha-logo-svg slot="icon"></ha-logo-svg>`,
},
{
title: "Slotted action",
description: "Alert with slotted action",
type: "info",
actionSlot: html`<mwc-button slot="action" label="action"></mwc-button>`,
},
{ {
description: "Dismissable information (RTL)", description: "Dismissable information (RTL)",
type: "info", type: "info",
@ -117,7 +132,7 @@ export class DemoHaAlert extends LitElement {
.actionText=${alert.action || ""} .actionText=${alert.action || ""}
.rtl=${alert.rtl || false} .rtl=${alert.rtl || false}
> >
${alert.description} ${alert.iconSlot} ${alert.description} ${alert.actionSlot}
</ha-alert> </ha-alert>
` `
)} )}
@ -145,6 +160,15 @@ export class DemoHaAlert extends LitElement {
span { span {
margin-right: 16px; margin-right: 16px;
} }
ha-logo-svg {
width: 28px;
height: 28px;
padding-right: 8px;
place-self: center;
}
mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
`; `;
} }
} }

View File

@ -1,6 +1,5 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { import {
mdiArrowUpBoldCircle,
mdiCheckCircle, mdiCheckCircle,
mdiChip, mdiChip,
mdiCircle, mdiCircle,
@ -49,7 +48,6 @@ import {
startHassioAddon, startHassioAddon,
stopHassioAddon, stopHassioAddon,
uninstallHassioAddon, uninstallHassioAddon,
updateHassioAddon,
validateHassioAddonOption, validateHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { import {
@ -69,9 +67,8 @@ import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content"; import "../../components/hassio-card-content";
import "../../components/supervisor-metric"; import "../../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown"; import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import { addonArchIsSupported } from "../../util/addon"; import { addonArchIsSupported, extractChangelog } from "../../util/addon";
const STAGE_ICON = { const STAGE_ICON = {
stable: mdiCheckCircle, stable: mdiCheckCircle,
@ -128,69 +125,23 @@ class HassioAddonInfo extends LitElement {
return html` return html`
${this.addon.update_available ${this.addon.update_available
? html` ? html`
<ha-card <ha-alert
.header="${this.supervisor.localize( .title=${this.supervisor.localize("common.update_available", {
"common.update_available", count: 1,
"count", })}
1
)}🎉"
> >
<div class="card-content"> ${this.supervisor.localize(
<hassio-card-content "addon.dashboard.new_update_available",
.hass=${this.hass} { name: this.addon.name, version: this.addon.version_latest }
.title=${this.supervisor.localize( )}
"addon.dashboard.new_update_available", <a
"name", href="/hassio/update-available/${this.addon.slug}"
this.addon.name, slot="action"
"version", >
this.addon.version_latest <mwc-button .label=${this.supervisor.localize("common.review")}>
)}
.description=${this.supervisor.localize(
"common.running_version",
"version",
this.addon.version
)}
icon=${mdiArrowUpBoldCircle}
iconClass="update"
></hassio-card-content>
${!this.addon.available && addonStoreInfo
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
</ha-alert>
`
: html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch",
"core_version_installed",
this.supervisor.core.version,
"core_version_needed",
addonStoreInfo.homeassistant
)}
</ha-alert>
`
: ""}
</div>
<div class="card-actions">
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
${this.supervisor.localize("addon.dashboard.changelog")}
</mwc-button>
`
: html`<span></span>`}
<mwc-button @click=${this._updateClicked}>
${this.supervisor.localize("common.update")}
</mwc-button> </mwc-button>
</div> </a>
</ha-card> </ha-alert>
` `
: ""} : ""}
${!this.addon.protected ${!this.addon.protected
@ -899,22 +850,14 @@ class HassioAddonInfo extends LitElement {
private async _openChangelog(): Promise<void> { private async _openChangelog(): Promise<void> {
try { try {
let content = await fetchHassioAddonChangelog(this.hass, this.addon.slug); const content = await fetchHassioAddonChangelog(
if ( this.hass,
content.includes(`# ${this.addon.version}`) && this.addon.slug
content.includes(`# ${this.addon.version_latest}`) );
) {
const newcontent = content.split(`# ${this.addon.version}`)[0];
if (newcontent.includes(`# ${this.addon.version_latest}`)) {
// Only change the content if the new version still exist
// if the changelog does not have the newests version on top
// this will not be true, and we don't modify the content
content = newcontent;
}
}
showHassioMarkdownDialog(this, { showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"), title: this.supervisor.localize("addon.dashboard.changelog"),
content, content: extractChangelog(this.addon, content),
}); });
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
@ -989,33 +932,6 @@ class HassioAddonInfo extends LitElement {
button.progress = false; button.progress = false;
} }
private async _updateClicked(): Promise<void> {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: this.addon.name,
version: this.addon.version_latest,
backupParams: {
name: `addon_${this.addon.slug}_${this.addon.version}`,
addons: [this.addon.slug],
homeassistant: false,
},
updateHandler: async () => this._updateAddon(),
});
}
private async _updateAddon(): Promise<void> {
await updateHassioAddon(this.hass, this.addon.slug);
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
const eventdata = {
success: true,
response: undefined,
path: "update",
};
fireEvent(this, "hass-api-called", eventdata);
}
private async _startClicked(ev: CustomEvent): Promise<void> { private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any; const button = ev.currentTarget as any;
button.progress = true; button.progress = true;
@ -1244,6 +1160,13 @@ class HassioAddonInfo extends LitElement {
align-self: end; align-self: end;
} }
ha-alert mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
a {
text-decoration: none;
}
@media (max-width: 720px) { @media (max-width: 720px) {
ha-chip { ha-chip {
line-height: 36px; line-height: 36px;

View File

@ -158,7 +158,7 @@ export class HassioBackups extends LitElement {
} }
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.tabs=${supervisorTabs} .tabs=${supervisorTabs(this.hass)}
.hass=${this.hass} .hass=${this.hass}
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.searchLabel=${this.supervisor.localize("search")} .searchLabel=${this.supervisor.localize("search")}

View File

@ -20,7 +20,9 @@ class HassioAddons extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="content"> <div class="content">
<h1>${this.supervisor.localize("dashboard.addons")}</h1> ${!atLeastVersion(this.hass.config.version, 2021, 12)
? html` <h1>${this.supervisor.localize("dashboard.addons")}</h1> `
: ""}
<div class="card-group"> <div class="card-group">
${!this.supervisor.supervisor.addons?.length ${!this.supervisor.supervisor.addons?.length
? html` ? html`

View File

@ -1,6 +1,7 @@
import { mdiStorePlus } from "@mdi/js"; import { mdiStorePlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/ha-fab"; import "../../../src/components/ha-fab";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
@ -27,7 +28,7 @@ class HassioDashboard extends LitElement {
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs} .tabs=${supervisorTabs(this.hass)}
main-page main-page
supervisor supervisor
hasFab hasFab
@ -36,10 +37,14 @@ class HassioDashboard extends LitElement {
${this.supervisor.localize("panel.dashboard")} ${this.supervisor.localize("panel.dashboard")}
</span> </span>
<div class="content"> <div class="content">
<hassio-update ${!atLeastVersion(this.hass.config.version, 2021, 12)
.hass=${this.hass} ? html`
.supervisor=${this.supervisor} <hassio-update
></hassio-update> .hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-update>
`
: ""}
<hassio-addons <hassio-addons
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}

View File

@ -3,34 +3,18 @@ import { mdiHomeAssistant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host"; import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import { import {
HassioHomeAssistantInfo, HassioHomeAssistantInfo,
HassioSupervisorInfo, HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
Supervisor,
supervisorApiWsRequest,
} from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string => const computeVersion = (key: string, version: string): string =>
@ -73,26 +57,18 @@ export class HassioUpdate extends LitElement {
${this._renderUpdateCard( ${this._renderUpdateCard(
"Home Assistant Core", "Home Assistant Core",
"core", "core",
this.supervisor.core, this.supervisor.core
"hassio/homeassistant/update",
`https://${
this.supervisor.core.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`
)} )}
${this._renderUpdateCard( ${this._renderUpdateCard(
"Supervisor", "Supervisor",
"supervisor", "supervisor",
this.supervisor.supervisor, this.supervisor.supervisor
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
)} )}
${this.supervisor.host.features.includes("haos") ${this.supervisor.host.features.includes("haos")
? this._renderUpdateCard( ? this._renderUpdateCard(
"Operating System", "Operating System",
"os", "os",
this.supervisor.os, this.supervisor.os
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
) )
: ""} : ""}
</div> </div>
@ -103,9 +79,7 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard( private _renderUpdateCard(
name: string, name: string,
key: string, key: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo, object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo
apiPath: string,
releaseNotesUrl: string
): TemplateResult { ): TemplateResult {
if (!object.update_available) { if (!object.update_available) {
return html``; return html``;
@ -136,96 +110,15 @@ export class HassioUpdate extends LitElement {
</ha-settings-row> </ha-settings-row>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href=${releaseNotesUrl} target="_blank" rel="noreferrer"> <a href="/hassio/update-available/${key}">
<mwc-button> <mwc-button .label=${this.supervisor.localize("common.review")}>
${this.supervisor.localize("common.release_notes")}
</mwc-button> </mwc-button>
</a> </a>
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.key=${key}
.version=${object.version_latest}
@click=${this._confirmUpdate}
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
if (item.key === "core") {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
backupParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => this._updateCore(),
});
return;
}
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
item.name
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
item.name,
"version",
computeVersion(item.key, item.version)
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
item.progress = false;
return;
}
try {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
await supervisorApiWsRequest(this.hass.connection, {
method: "post",
endpoint: item.apiPath.replace("hassio", ""),
timeout: null,
});
} else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
}
fireEvent(this, "supervisor-collection-refresh", {
collection: item.key,
});
} catch (err: any) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("common.error.update_failed"),
text: extractApiErrorMessage(err),
});
}
}
item.progress = false;
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -1,203 +0,0 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-switch";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import { createHassioPartialBackup } from "../../../../src/data/hassio/backup";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
@customElement("dialog-supervisor-update")
class DialogSupervisorUpdate extends LitElement {
public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _createBackup = true;
@state() private _action: "backup" | "update" | null = null;
@state() private _error?: string;
@state()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
public async showDialog(
params: SupervisorDialogSupervisorUpdateParams
): Promise<void> {
this._opened = true;
this._dialogParams = params;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createBackup = true;
this._error = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
${this._dialogParams.supervisor.localize(
"confirm.update.title",
"name",
this._dialogParams.name
)}
</h2>
</slot>
<div>
${this._dialogParams.supervisor.localize(
"confirm.update.text",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)}
</div>
<ha-settings-row>
<span slot="heading">
${this._dialogParams.supervisor.localize(
"dialog.update.backup"
)}
</span>
<span slot="description">
${this._dialogParams.supervisor.localize(
"dialog.update.create_backup",
"name",
this._dialogParams.name
)}
</span>
<ha-switch
.checked=${this._createBackup}
haptic
@click=${this._toggleBackup}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this._dialogParams.supervisor.localize("common.cancel")}
</mwc-button>
<mwc-button
.disabled=${this._error !== undefined}
@click=${this._update}
slot="primaryAction"
>
${this._dialogParams.supervisor.localize("common.update")}
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this._dialogParams.supervisor.localize(
"dialog.update.updating",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)
: this._dialogParams.supervisor.localize(
"dialog.update.creating_backup",
"name",
this._dialogParams.name
)}
</p>`}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
</ha-dialog>
`;
}
private _toggleBackup() {
this._createBackup = !this._createBackup;
}
private async _update() {
if (this._createBackup) {
this._action = "backup";
try {
await createHassioPartialBackup(
this.hass,
this._dialogParams!.backupParams
);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await this._dialogParams!.updateHandler!();
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
}
return;
}
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
.form {
color: var(--primary-text-color);
}
ha-settings-row {
margin-top: 32px;
padding: 0;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-update": DialogSupervisorUpdate;
}
}

View File

@ -1,21 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorUpdateParams {
supervisor: Supervisor;
name: string;
version: string;
backupParams: any;
updateHandler: () => Promise<void>;
}
export const showDialogSupervisorUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-update",
dialogImport: () => import("./dialog-supervisor-update"),
dialogParams,
});
};

View File

@ -34,6 +34,9 @@ const REDIRECTS: Redirects = {
supervisor_store: { supervisor_store: {
redirect: "/hassio/store", redirect: "/hassio/store",
}, },
supervisor_addons: {
redirect: "/hassio/dashboard",
},
supervisor_addon: { supervisor_addon: {
redirect: "/hassio/addon", redirect: "/hassio/addon",
params: { params: {

View File

@ -35,6 +35,10 @@ class HassioRouter extends HassRouterPage {
backups: "dashboard", backups: "dashboard",
store: "dashboard", store: "dashboard",
system: "dashboard", system: "dashboard",
"update-available": {
tag: "update-available-dashboard",
load: () => import("./update-available/update-available-dashboard"),
},
addon: { addon: {
tag: "hassio-addon-dashboard", tag: "hassio-addon-dashboard",
load: () => import("./addon-view/hassio-addon-dashboard"), load: () => import("./addon-view/hassio-addon-dashboard"),

View File

@ -1,11 +1,22 @@
import { mdiBackupRestore, mdiCogs, mdiViewDashboard } from "@mdi/js"; import {
mdiBackupRestore,
mdiCogs,
mdiPuzzle,
mdiViewDashboard,
} from "@mdi/js";
import { atLeastVersion } from "../../src/common/config/version";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage"; import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../src/types";
export const supervisorTabs: PageNavigation[] = [ export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [
{ {
translationKey: "panel.dashboard", translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`, path: `/hassio/dashboard`,
iconPath: mdiViewDashboard, iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
}, },
{ {
translationKey: "panel.backups", translationKey: "panel.backups",

View File

@ -2,7 +2,7 @@ import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@ -12,7 +12,7 @@ import {
fetchHassioStats, fetchHassioStats,
HassioStats, HassioStats,
} from "../../../src/data/hassio/common"; } from "../../../src/data/hassio/common";
import { restartCore, updateCore } from "../../../src/data/supervisor/core"; import { restartCore } from "../../../src/data/supervisor/core";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
@ -22,7 +22,6 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string"; import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric"; import "../components/supervisor-metric";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info") @customElement("hassio-core-info")
@ -67,14 +66,15 @@ class HassioCoreInfo extends LitElement {
<span slot="description"> <span slot="description">
core-${this.supervisor.core.version_latest} core-${this.supervisor.core.version_latest}
</span> </span>
${this.supervisor.core.update_available ${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.core.update_available
? html` ? html`
<ha-progress-button <a href="/hassio/update-available/core">
.title=${this.supervisor.localize("common.update")} <mwc-button
@click=${this._coreUpdate} .label=${this.supervisor.localize("common.review")}
> >
${this.supervisor.localize("common.update")} </mwc-button>
</ha-progress-button> </a>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@ -160,27 +160,6 @@ class HassioCoreInfo extends LitElement {
} }
} }
private async _coreUpdate(): Promise<void> {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
backupParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => this._updateCore(),
});
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@ -239,6 +218,9 @@ class HassioCoreInfo extends LitElement {
mwc-list-item ha-svg-icon { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
a {
text-decoration: none;
}
`, `,
]; ];
} }

View File

@ -21,7 +21,6 @@ import {
configSyncOS, configSyncOS,
rebootHost, rebootHost,
shutdownHost, shutdownHost,
updateOS,
} from "../../../src/data/hassio/host"; } from "../../../src/data/hassio/host";
import { import {
fetchNetworkInfo, fetchNetworkInfo,
@ -106,11 +105,15 @@ class HassioHostInfo extends LitElement {
<span slot="description"> <span slot="description">
${this.supervisor.host.operating_system} ${this.supervisor.host.operating_system}
</span> </span>
${this.supervisor.os.update_available ${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.os.update_available
? html` ? html`
<ha-progress-button @click=${this._osUpdate}> <a href="/hassio/update-available/os">
${this.supervisor.localize("commmon.update")} <mwc-button
</ha-progress-button> .label=${this.supervisor.localize("common.review")}
>
</mwc-button>
</a>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@ -333,50 +336,6 @@ class HassioHostInfo extends LitElement {
button.progress = false; button.progress = false;
} }
private async _osUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Home Assistant Operating System"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Home Assistant Operating System",
"version",
this.supervisor.os.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateOS(this.hass);
fireEvent(this, "supervisor-collection-refresh", { collection: "os" });
} catch (err: any) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Home Assistant Operating System"
),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> { private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, { showNetworkDialog(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
@ -494,6 +453,9 @@ class HassioHostInfo extends LitElement {
mwc-list-item ha-svg-icon { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
a {
text-decoration: none;
}
`, `,
]; ];
} }

View File

@ -17,7 +17,6 @@ import {
restartSupervisor, restartSupervisor,
setSupervisorOption, setSupervisorOption,
SupervisorOptions, SupervisorOptions,
updateSupervisor,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
@ -77,16 +76,15 @@ class HassioSupervisorInfo extends LitElement {
<span slot="description"> <span slot="description">
supervisor-${this.supervisor.supervisor.version_latest} supervisor-${this.supervisor.supervisor.version_latest}
</span> </span>
${this.supervisor.supervisor.update_available ${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.supervisor.update_available
? html` ? html`
<ha-progress-button <a href="/hassio/update-available/supervisor">
.title=${this.supervisor.localize( <mwc-button
"system.supervisor.update_supervisor" .label=${this.supervisor.localize("common.review")}
)} >
@click=${this._supervisorUpdate} </mwc-button>
> </a>
${this.supervisor.localize("common.update")}
</ha-progress-button>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@ -337,51 +335,6 @@ class HassioSupervisorInfo extends LitElement {
} }
} }
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Supervisor",
"version",
this.supervisor.supervisor.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateSupervisor(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Supervisor"
),
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
private async _diagnosticsInformationDialog(): Promise<void> { private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, { await showAlertDialog(this, {
title: this.supervisor.localize( title: this.supervisor.localize(
@ -513,6 +466,9 @@ class HassioSupervisorInfo extends LitElement {
white-space: normal; white-space: normal;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
a {
text-decoration: none;
}
`, `,
]; ];
} }

View File

@ -28,7 +28,7 @@ class HassioSystem extends LitElement {
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs} .tabs=${supervisorTabs(this.hass)}
main-page main-page
supervisor supervisor
> >

View File

@ -0,0 +1,391 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-expansion-panel";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { updateOS } from "../../../src/data/hassio/host";
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
const changelogUrl = (
hass: HomeAssistant,
entry: string,
version: string
): string | undefined => {
if (entry === "core") {
return version?.includes("dev")
? "https://github.com/home-assistant/core/commits/dev"
: documentationUrl(hass, "/latest-release-notes/");
}
if (entry === "os") {
return version?.includes("dev")
? "https://github.com/home-assistant/operating-system/commits/dev"
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
}
if (entry === "supervisor") {
return version?.includes("dev")
? "https://github.com/home-assistant/supervisor/commits/main"
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
}
return undefined;
};
class UpdateAvailableDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@state() private _updateEntry?: string;
@state() private _changelogContent?: string;
@state() private _addonInfo?: HassioAddonDetails;
@state() private _createBackup = true;
@state() private _action: "backup" | "update" | null = null;
@state() private _error?: string;
private _isAddon = false;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
if (!this._updateEntry) {
return html``;
}
const name =
// @ts-ignore
this._addonInfo?.name || SUPERVISOR_UPDATE_NAMES[this._updateEntry];
const changelog = !this._isAddon
? changelogUrl(
this.hass,
this._updateEntry,
this.supervisor[this._updateEntry]?.version
)
: undefined;
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
>
<ha-card
.header=${this.supervisor.localize("update_available.update_name", {
name,
})}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._action === null
? html`
${this._changelogContent
? html`
<ha-expansion-panel header="Changelog" outlined>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-expansion-panel>
`
: ""}
<div class="versions">
<p>
${this.supervisor.localize(
"update_available.description",
{
name,
version:
this._addonInfo?.version ||
this.supervisor[this._updateEntry]?.version,
newest_version:
this._addonInfo?.version_latest ||
this.supervisor[this._updateEntry]?.version_latest,
}
)}
</p>
${this._updateEntry === "core"
? html`
<i>
${this.supervisor.localize(
"update_available.core_note",
{
version:
this._addonInfo?.version ||
this.supervisor[this._updateEntry]?.version,
}
)}
</i>
`
: ""}
</div>
${!["os", "supervisor"].includes(this._updateEntry)
? html`
<ha-settings-row>
<ha-checkbox
slot="prefix"
.checked=${this._createBackup}
@click=${this._toggleBackup}
>
</ha-checkbox>
<span slot="heading">
${this.supervisor.localize(
"update_available.create_backup"
)}
</span>
</ha-settings-row>
`
: ""}
`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this.supervisor.localize("update_available.updating", {
name,
version:
this._addonInfo?.version_latest ||
this.supervisor[this._updateEntry]?.version_latest,
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name }
)}
</p>`}
</div>
${this._action === null
? html`
<div class="card-actions">
${changelog
? html`<a
.href=${changelog}
target="_blank"
rel="noreferrer"
>
<mwc-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</mwc-button>
</a>`
: ""}
<span></span>
<ha-progress-button
.disabled=${this._error !== undefined}
@click=${this._update}
raised
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: ""}
</ha-card>
</hass-subpage>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._updateEntry = this.route.path.substring(1, this.route.path.length);
this._isAddon = !["core", "os", "supervisor"].includes(this._updateEntry);
if (this._isAddon) {
this._loadAddonData();
}
}
private async _loadAddonData() {
try {
this._addonInfo = await fetchHassioAddonInfo(
this.hass,
this._updateEntry!
);
} catch (err) {
showAlertDialog(this, {
title: this._updateEntry,
text: extractApiErrorMessage(err),
confirm: () => history.back(),
});
return;
}
const addonStoreInfo =
!this._addonInfo.detached && !this._addonInfo.available
? this._addonStoreInfo(
this._addonInfo.slug,
this.supervisor.store.addons
)
: undefined;
if (this._addonInfo.changelog) {
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this._updateEntry!
);
this._changelogContent = extractChangelog(this._addonInfo, content);
} catch (err) {
this._error = extractApiErrorMessage(err);
return;
}
}
if (!this._addonInfo.available && addonStoreInfo) {
if (
!addonArchIsSupported(
this.supervisor.info.supported_arch,
this._addonInfo.arch
)
) {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch"
);
} else {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo.homeassistant,
}
);
}
}
}
private _toggleBackup() {
this._createBackup = !this._createBackup;
}
private async _update() {
if (this._createBackup) {
let backupArgs: HassioPartialBackupCreateParams;
if (this._isAddon) {
backupArgs = {
name: `addon_${this._updateEntry}_${this._addonInfo?.version}`,
addons: [this._updateEntry!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateEntry}_${this._addonInfo?.version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
if (this._isAddon) {
await updateHassioAddon(this.hass, this._updateEntry!);
} else if (this._updateEntry === "core") {
await updateCore(this.hass);
} else if (this._updateEntry === "os") {
await updateOS(this.hass);
} else if (this._updateEntry === "supervisor") {
await updateSupervisor(this.hass);
}
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
history.back();
}
static get styles(): CSSResultGroup {
return css`
hass-subpage {
--app-header-background-color: background-color: var(--primary-background-color);
}
ha-card {
margin: auto;
margin-top: 16px;
max-width: 600px;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`;
}
}
customElements.define("update-available-dashboard", UpdateAvailableDashboard);

View File

@ -1,7 +1,30 @@
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { HassioAddonDetails } from "../../../src/data/hassio/addon";
import { SupervisorArch } from "../../../src/data/supervisor/supervisor"; import { SupervisorArch } from "../../../src/data/supervisor/supervisor";
export const addonArchIsSupported = memoizeOne( export const addonArchIsSupported = memoizeOne(
(supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) => (supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) =>
addon_archs.some((arch) => supported_archs.includes(arch)) addon_archs.some((arch) => supported_archs.includes(arch))
); );
export const extractChangelog = (
addon: HassioAddonDetails,
content: string
): string => {
if (content.startsWith("# Changelog")) {
content = content.substr(12, content.length);
}
if (
content.includes(`# ${addon.version}`) &&
content.includes(`# ${addon.version_latest}`)
) {
const newcontent = content.split(`# ${addon.version}`)[0];
if (newcontent.includes(`# ${addon.version_latest}`)) {
// Only change the content if the new version still exist
// if the changelog does not have the newests version on top
// this will not be true, and we don't modify the content
content = newcontent;
}
}
return content;
};

View File

@ -51,27 +51,31 @@ class HaAlert extends LitElement {
[this.alertType]: true, [this.alertType]: true,
})}" })}"
> >
<div class="icon ${this.title ? "" : "no-title"}"> <slot name="icon">
<ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon> <div class="icon ${this.title ? "" : "no-title"}">
</div> <ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon>
</div>
</slot>
<div class="content"> <div class="content">
<div class="main-content"> <div class="main-content">
${this.title ? html`<div class="title">${this.title}</div>` : ""} ${this.title ? html`<div class="title">${this.title}</div>` : ""}
<slot></slot> <slot></slot>
</div> </div>
<div class="action"> <div class="action">
${this.actionText <slot name="action">
? html`<mwc-button ${this.actionText
@click=${this._action_clicked} ? html`<mwc-button
.label=${this.actionText} @click=${this._action_clicked}
></mwc-button>` .label=${this.actionText}
: this.dismissable ></mwc-button>`
? html`<ha-icon-button : this.dismissable
@click=${this._dismiss_clicked} ? html`<ha-icon-button
label="Dismiss alert" @click=${this._dismiss_clicked}
.path=${mdiClose} label="Dismiss alert"
></ha-icon-button>` .path=${mdiClose}
: ""} ></ha-icon-button>`
: ""}
</slot>
</div> </div>
</div> </div>
</div> </div>
@ -107,14 +111,14 @@ class HaAlert extends LitElement {
content: ""; content: "";
border-radius: 4px; border-radius: 4px;
} }
.icon { slot > .icon {
margin-right: 8px; margin-right: 8px;
width: 24px; width: 24px;
} }
.icon.no-title { .icon.no-title {
align-self: center; align-self: center;
} }
.issue-type.rtl > .icon { .issue-type.rtl > slot > .icon {
margin-right: 0px; margin-right: 0px;
margin-left: 8px; margin-left: 8px;
width: 24px; width: 24px;
@ -142,28 +146,28 @@ class HaAlert extends LitElement {
ha-icon-button { ha-icon-button {
--mdc-icon-button-size: 36px; --mdc-icon-button-size: 36px;
} }
.issue-type.info > .icon { .issue-type.info > slot > .icon {
color: var(--info-color); color: var(--info-color);
} }
.issue-type.info::before { .issue-type.info::before {
background-color: var(--info-color); background-color: var(--info-color);
} }
.issue-type.warning > .icon { .issue-type.warning > slot > .icon {
color: var(--warning-color); color: var(--warning-color);
} }
.issue-type.warning::before { .issue-type.warning::before {
background-color: var(--warning-color); background-color: var(--warning-color);
} }
.issue-type.error > .icon { .issue-type.error > slot > .icon {
color: var(--error-color); color: var(--error-color);
} }
.issue-type.error::before { .issue-type.error::before {
background-color: var(--error-color); background-color: var(--error-color);
} }
.issue-type.success > .icon { .issue-type.success > slot > .icon {
color: var(--success-color); color: var(--success-color);
} }
.issue-type.success::before { .issue-type.success::before {

View File

@ -0,0 +1,51 @@
import { css, CSSResultGroup, LitElement, svg, SVGTemplateResult } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-logo-svg")
export class HaLogoSvg extends LitElement {
protected render(): SVGTemplateResult {
return svg`
<svg version="1.1" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="a" d="m44.041 343.22v-133.64h-34.753a9.333 9.333 0 0 1-6.655-15.876l187.01-190.21c4.517-4.594 11.903-4.657 16.498-0.14l0.12 0.12 97.601 98.794v-18.297a7.778 7.778 0 0 1 7.778-7.778h32.41a7.778 7.778 0 0 1 7.779 7.778v67.138l41.568 42.618a9.333 9.333 0 0 1-6.682 15.85h-34.886v133.64a7.778 7.778 0 0 1-7.778 7.778h-292.23a7.778 7.778 0 0 1-7.778-7.778zm206.39-163.26a15.029 15.029 0 0 0 1.46-6.486c0-8.308-6.71-15.043-14.989-15.043-8.278 0-14.989 6.735-14.989 15.043s6.711 15.044 14.99 15.044c2.314 0 4.505-0.527 6.462-1.467l21.518 21.596v20.918l-26.981 27.078v-19.84a15.046 15.046 0 0 0 9.993-14.187c0-8.308-6.711-15.044-14.99-15.044-8.278 0-14.99 6.736-14.99 15.044 0 6.55 4.172 12.122 9.994 14.187v29.868l-24.983 25.073v-147.28l20.519-20.592a14.886 14.886 0 0 0 6.462 1.466c8.279 0 14.99-6.735 14.99-15.044 0-8.308-6.711-15.043-14.99-15.043-8.278 0-14.989 6.735-14.989 15.043 0 2.323 0.524 4.522 1.46 6.486l-18.448 18.515-18.449-18.515a15.029 15.029 0 0 0 1.46-6.486c0-8.308-6.71-15.043-14.989-15.043-8.278 0-14.989 6.735-14.989 15.043 0 8.309 6.711 15.044 14.99 15.044 2.314 0 4.505-0.527 6.462-1.466l20.518 20.592v105.16l-35.974-36.104v-28.865a15.046 15.046 0 0 0 9.993-14.187c0-8.309-6.711-15.044-14.99-15.044-8.278 0-14.99 6.735-14.99 15.044 0 6.55 4.172 12.122 9.994 14.187v18.837l-27.98-28.081v-27.863a15.046 15.046 0 0 0 9.993-14.187c0-8.308-6.711-15.044-14.99-15.044-8.278 0-14.99 6.736-14.99 15.044 0 6.55 4.172 12.122 9.994 14.187v32.017l30.907 31.018h-17.77c-2.058-5.843-7.61-10.029-14.137-10.029-8.278 0-14.99 6.735-14.99 15.043 0 8.309 6.712 15.044 14.99 15.044 6.527 0 12.08-4.186 14.137-10.03h27.763l43.04 43.196v75.074l-22.983-23.066v-28.866a15.046 15.046 0 0 0 9.993-14.187c0-8.308-6.711-15.043-14.99-15.043-8.278 0-14.99 6.735-14.99 15.043 0 6.55 4.172 12.122 9.994 14.187v18.837l-33.439-33.558a15.029 15.029 0 0 0 1.461-6.486c0-8.308-6.71-15.043-14.99-15.043-8.278 0-14.989 6.735-14.989 15.043s6.711 15.043 14.99 15.043c2.314 0 4.506-0.526 6.462-1.466l33.439 33.559h-17.77c-2.058-5.843-7.61-10.03-14.137-10.03-8.278 0-14.99 6.736-14.99 15.044s6.712 15.043 14.99 15.043c6.527 0 12.079-4.186 14.137-10.029h27.763l27.98 28.081h14.132l28.98-29.083h26.763c2.058 5.842 7.61 10.028 14.137 10.028 8.278 0 14.99-6.735 14.99-15.043s-6.712-15.043-14.99-15.043c-6.527 0-12.079 4.186-14.137 10.029h-30.902l-26.91 27.006v-32.951l32.049-32.164h51.746c2.058 5.843 7.61 10.029 14.136 10.029 8.279 0 14.99-6.735 14.99-15.043 0-8.309-6.711-15.044-14.99-15.044-6.526 0-12.078 4.186-14.136 10.03h-41.755l29.908-30.016v-25.072l21.517-21.596a14.886 14.886 0 0 0 6.463 1.467c8.278 0 14.99-6.736 14.99-15.044s-6.712-15.043-14.99-15.043-14.99 6.735-14.99 15.043c0 2.323 0.525 4.522 1.461 6.486l-14.451 14.504v-45.917a15.046 15.046 0 0 0 9.993-14.187c0-8.309-6.711-15.044-14.99-15.044-8.278 0-14.99 6.735-14.99 15.044 0 6.55 4.172 12.122 9.994 14.187v45.915l-14.452-14.504zm-129.45 143.95c-3.311 0-5.996-2.694-5.996-6.017s2.685-6.017 5.996-6.017c3.312 0 5.996 2.694 5.996 6.017s-2.684 6.017-5.996 6.017zm43.97-45.13c-3.312 0-5.997-2.694-5.997-6.017s2.685-6.017 5.996-6.017c3.312 0 5.996 2.694 5.996 6.017s-2.684 6.017-5.996 6.017zm-51.964-7.02c-3.312 0-5.996-2.694-5.996-6.017s2.684-6.017 5.996-6.017c3.311 0 5.996 2.694 5.996 6.017s-2.685 6.017-5.996 6.017zm-4.997-50.144c-3.311 0-5.995-2.694-5.995-6.018 0-3.323 2.684-6.017 5.995-6.017 3.312 0 5.996 2.694 5.996 6.017 0 3.324-2.684 6.018-5.996 6.018zm124.91 7.02c-3.311 0-5.995-2.694-5.995-6.017 0-3.324 2.684-6.018 5.995-6.018 3.312 0 5.996 2.694 5.996 6.018 0 3.323-2.684 6.017-5.996 6.017zm67.952 46.133c-3.31 0-5.995-2.694-5.995-6.017 0-3.324 2.684-6.018 5.995-6.018 3.312 0 5.996 2.694 5.996 6.018 0 3.323-2.684 6.017-5.996 6.017zm-25.981 48.138c-3.312 0-5.996-2.694-5.996-6.017s2.684-6.017 5.996-6.017c3.311 0 5.996 2.694 5.996 6.017s-2.685 6.017-5.996 6.017zm27.98-143.41c-3.311 0-5.996-2.695-5.996-6.018s2.685-6.017 5.996-6.017 5.996 2.694 5.996 6.017-2.685 6.018-5.996 6.018zm-32.977-39.113c-3.311 0-5.996-2.694-5.996-6.017 0-3.324 2.685-6.018 5.996-6.018 3.312 0 5.996 2.694 5.996 6.018 0 3.323-2.684 6.017-5.996 6.017zm-39.972-24.07c-3.311 0-5.995-2.693-5.995-6.017 0-3.323 2.684-6.017 5.995-6.017 3.312 0 5.996 2.694 5.996 6.017 0 3.324-2.684 6.018-5.996 6.018zm-63.955 0c-3.31 0-5.995-2.693-5.995-6.017 0-3.323 2.684-6.017 5.995-6.017 3.312 0 5.996 2.694 5.996 6.017 0 3.324-2.684 6.018-5.996 6.018zm-51.963 23.067c-3.311 0-5.996-2.694-5.996-6.017 0-3.324 2.685-6.018 5.996-6.018s5.996 2.694 5.996 6.018c0 3.323-2.685 6.017-5.996 6.017zm37.973 37.107c-3.311 0-5.995-2.694-5.995-6.017 0-3.324 2.684-6.018 5.995-6.018 3.312 0 5.996 2.694 5.996 6.018 0 3.323-2.684 6.017-5.996 6.017zm84.94 3.009c-3.31 0-5.995-2.695-5.995-6.018s2.684-6.017 5.995-6.017c3.312 0 5.996 2.694 5.996 6.017s-2.684 6.018-5.996 6.018z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<rect width="500" height="500" ry="40.911" fill="#41bdf5" fill-rule="evenodd" stroke-width="5.8497"/>
<g transform="translate(52 70)">
<mask id="b" fill="#fff">
<use xlink:href="#a"/>
</mask>
<g fill="#FFF" mask="url(#b)">
<path d="M0 0h396v351H0z"/>
</g>
</g>
</g>
</svg>`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: var(--ha-icon-display, inline-flex);
align-items: center;
justify-content: center;
position: relative;
vertical-align: middle;
fill: currentcolor;
width: var(--mdc-icon-size, 24px);
height: var(--mdc-icon-size, 24px);
}
svg {
width: 100%;
height: 100%;
pointer-events: none;
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-logo-svg": HaLogoSvg;
}
}

View File

@ -51,7 +51,7 @@ import {
} from "../external_app/external_config"; } from "../external_app/external_config";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-icon"; import "./ha-icon";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-menu-button"; import "./ha-menu-button";
@ -189,6 +189,8 @@ class HaSidebar extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow!: boolean; @property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property() public route!: Route;
@property({ type: Boolean }) public alwaysExpand = false; @property({ type: Boolean }) public alwaysExpand = false;
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public editMode = false;
@ -351,12 +353,19 @@ class HaSidebar extends LitElement {
this._hiddenPanels this._hiddenPanels
); );
// Show the update-available as beeing part of configuration
const selectedPanel = this.route.path?.startsWith(
"/hassio/update-available"
)
? "config"
: this.hass.panelUrl;
// prettier-ignore // prettier-ignore
return html` return html`
<paper-listbox <paper-listbox
attr-for-selected="data-panel" attr-for-selected="data-panel"
class="ha-scrollbar" class="ha-scrollbar"
.selected=${this.hass.panelUrl} .selected=${selectedPanel}
@focusin=${this._listboxFocusIn} @focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut} @focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll} @scroll=${this._listboxScroll}

View File

@ -70,6 +70,42 @@ export interface Supervisor {
localize: LocalizeFunc; localize: LocalizeFunc;
} }
interface SupervisorBaseAvailableUpdates {
panel_path?: string;
update_type?: string;
version_latest?: string;
}
interface SupervisorAddonAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "addon";
icon?: string;
name?: string;
}
interface SupervisorCoreAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "core";
}
interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates {
update_type?: "os";
}
interface SupervisorSupervisorAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "supervisor";
}
export type SupervisorAvailableUpdates =
| SupervisorAddonAvailableUpdates
| SupervisorCoreAvailableUpdates
| SupervisorOsAvailableUpdates
| SupervisorSupervisorAvailableUpdates;
export interface SupervisorAvailableUpdatesResponse {
available_updates: SupervisorAvailableUpdates[];
}
export const supervisorApiWsRequest = <T>( export const supervisorApiWsRequest = <T>(
conn: Connection, conn: Connection,
request: supervisorApiRequest request: supervisorApiRequest
@ -139,3 +175,14 @@ export const subscribeSupervisorEvents = (
getSupervisorEventCollection(hass.connection, key, endpoint).subscribe( getSupervisorEventCollection(hass.connection, key, endpoint).subscribe(
onChange onChange
); );
export const fetchSupervisorAvailableUpdates = async (
hass: HomeAssistant
): Promise<SupervisorAvailableUpdates[]> =>
(
await hass.callWS<SupervisorAvailableUpdatesResponse>({
type: "supervisor/api",
endpoint: "/supervisor/available_updates",
method: "get",
})
).available_updates;

View File

@ -88,6 +88,7 @@ class HomeAssistantMain extends LitElement {
<ha-sidebar <ha-sidebar
.hass=${hass} .hass=${hass}
.narrow=${sidebarNarrow} .narrow=${sidebarNarrow}
.route=${this.route}
.editMode=${this._sidebarEditMode} .editMode=${this._sidebarEditMode}
.alwaysExpand=${sidebarNarrow || .alwaysExpand=${sidebarNarrow ||
this.hass.dockedSidebar === "docked"} this.hass.dockedSidebar === "docked"}

View File

@ -1,3 +1,4 @@
import "./ha-config-updates";
import { mdiCloudLock } from "@mdi/js"; import { mdiCloudLock } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
@ -35,10 +36,16 @@ class HaConfigDashboard extends LitElement {
> >
<div slot="header">${this.hass.localize("ui.panel.config.header")}</div> <div slot="header">${this.hass.localize("ui.panel.config.header")}</div>
<div slot="introduction"> <div class="intro" slot="introduction">
${this.hass.localize("ui.panel.config.introduction")} ${this.hass.localize("ui.panel.config.introduction")}
</div> </div>
${isComponentLoaded(this.hass, "hassio")
? html`<ha-config-updates
.hass=${this.hass}
slot="introduction"
></ha-config-updates>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud") ${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html` ? html`
<ha-card> <ha-card>
@ -134,6 +141,9 @@ class HaConfigDashboard extends LitElement {
.promo-advanced a { .promo-advanced a {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.intro {
margin-bottom: 24px;
}
`, `,
]; ];
} }

View File

@ -0,0 +1,120 @@
import "@material/mwc-button/mwc-button";
import { mdiPackageVariant } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-alert";
import "../../../components/ha-logo-svg";
import "../../../components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
fetchSupervisorAvailableUpdates,
SupervisorAvailableUpdates,
} from "../../../data/supervisor/supervisor";
import { HomeAssistant } from "../../../types";
export const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
@customElement("ha-config-updates")
class HaConfigUpdates extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _supervisorUpdates?: SupervisorAvailableUpdates[];
@state() private _error?: string;
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._loadSupervisorUpdates();
}
protected render(): TemplateResult {
return html`
${this._error
? html`<ha-alert
.title=${this.hass.localize(
"ui.panel.config.updates.unable_to_fetch"
)}
alert-type="error"
>
${this._error}
</ha-alert>`
: ""}
${this._supervisorUpdates?.map(
(update) => html`
<ha-alert
.title=${update.update_type === "addon"
? update.name
: SUPERVISOR_UPDATE_NAMES[update.update_type!]}
>
<span slot="icon" class="icon">
${update.update_type === "addon"
? update.icon
? html`<img src="/api/hassio${update.icon}" />`
: html`<ha-svg-icon .path=${mdiPackageVariant}></ha-svg-icon>`
: html`<ha-logo-svg></ha-logo-svg>`}
</span>
${this.hass.localize("ui.panel.config.updates.version_available", {
version_available: update.version_latest,
})}
<a href="/hassio${update.panel_path}" slot="action">
<mwc-button
.label=${this.hass.localize("ui.panel.config.updates.review")}
>
</mwc-button>
</a>
</ha-alert>
`
)}
`;
}
private async _loadSupervisorUpdates(): Promise<void> {
try {
this._supervisorUpdates = await fetchSupervisorAvailableUpdates(
this.hass
);
} catch (err) {
this._error = extractApiErrorMessage(err);
}
}
static get styles(): CSSResultGroup {
return css`
a {
text-decoration: none;
color: var(--primary-text-color);
}
.icon {
place-self: center;
}
img,
ha-svg-icon,
ha-logo-svg {
width: var(--mdc-icon-size, 32px);
height: var(--mdc-icon-size, 32px);
padding-right: 12px;
display: block;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-updates": HaConfigUpdates;
}
}

View File

@ -926,6 +926,11 @@
}, },
"learn_more": "Learn more" "learn_more": "Learn more"
}, },
"updates": {
"unable_to_fetch": "Unable to fetch available updates",
"version_available": "Version {version_available} is available",
"review": "review"
},
"areas": { "areas": {
"caption": "Areas", "caption": "Areas",
"description": "Group devices and entities into areas", "description": "Group devices and entities into areas",
@ -4178,6 +4183,7 @@
"save": "[%key:ui::common::save%]", "save": "[%key:ui::common::save%]",
"close": "[%key:ui::common::close%]", "close": "[%key:ui::common::close%]",
"menu": "[%key:ui::common::menu%]", "menu": "[%key:ui::common::menu%]",
"review": "[%key:ui::panel::config::updates::review%]",
"show_more": "Show more information about this", "show_more": "Show more information about this",
"update_available": "{count, plural,\n one {Update}\n other {{count} updates}\n} pending", "update_available": "{count, plural,\n one {Update}\n other {{count} updates}\n} pending",
"update": "Update", "update": "Update",
@ -4187,11 +4193,16 @@
"update_failed": "Update failed" "update_failed": "Update failed"
} }
}, },
"update_available": {
"update_name": "Update {name}",
"open_release_notes": "Open release notes",
"create_backup": "Create backup before updating",
"description": "There is an update available for the {name}. You have {version} installed. Click update to update to version {newest_version}",
"core_note": "The supervisor will roll back to version {version} if your instance does not come up after the update.",
"updating": "Updating {name} to version {version}",
"creating_backup": "Creating backup of {name}"
},
"confirm": { "confirm": {
"update": {
"title": "Update {name}",
"text": "Are you sure you want to update {name} to version {version}?"
},
"restart": { "restart": {
"title": "[%key:supervisor::common::restart_name%]", "title": "[%key:supervisor::common::restart_name%]",
"text": "Are you sure you want to restart {name}?" "text": "Are you sure you want to restart {name}?"
@ -4215,6 +4226,7 @@
"repositories": "Repositories" "repositories": "Repositories"
}, },
"panel": { "panel": {
"addons": "Add-ons",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"backups": "Backups", "backups": "Backups",
"store": "Add-on Store", "store": "Add-on Store",
@ -4382,12 +4394,6 @@
"confirm_text": "Restart add-on", "confirm_text": "Restart add-on",
"text": "Do you want to restart the add-on with your changes?" "text": "Do you want to restart the add-on with your changes?"
}, },
"update": {
"backup": "Backup",
"create_backup": "Create a backup of {name} before updating",
"updating": "Updating {name} to version {version}",
"creating_backup": "Creating backup of {name}"
},
"hardware": { "hardware": {
"title": "Hardware", "title": "Hardware",
"search": "Search hardware", "search": "Search hardware",