Add HA Cloud login to onboarding (#24485)

* Add ha cloud login to onboarding

* Add view for no cloud backup available

* Add logout and forgot pw

* Improve styling

* Fix bug to open cloud backup after login

* Remove callback from catch in transform methods

* Remove unused variable

* Fix lint

* Add new onboarding restore design

* Fix lint

* Change back button style

* Update header styles

* Style onboarding left aligned

* Remove unused imports

* Fix imports

* Fix multi factor cloud auth

* Fix prettier

* Edit onboarding translations

* Revert gulp change

* Improve cloud login component

* Fix no-cloud-backup naming

* fix types

* Use cloud login function directly

* Fix eslint

* Hide restore picker when there is nothing to select

* Fix eslint
This commit is contained in:
Wendelin 2025-03-18 15:24:04 +01:00 committed by GitHub
parent 858b8b90d8
commit 4076e5655a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1702 additions and 969 deletions

View File

@ -14,6 +14,8 @@ export class HaProgressButton extends LitElement {
@property({ type: Boolean }) public raised = false; @property({ type: Boolean }) public raised = false;
@property({ type: Boolean }) public unelevated = false;
@state() private _result?: "success" | "error"; @state() private _result?: "success" | "error";
public render(): TemplateResult { public render(): TemplateResult {
@ -21,6 +23,7 @@ export class HaProgressButton extends LitElement {
return html` return html`
<mwc-button <mwc-button
?raised=${this.raised} ?raised=${this.raised}
.unelevated=${this.unelevated}
.disabled=${this.disabled || this.progress} .disabled=${this.disabled || this.progress}
class=${this._result || ""} class=${this._result || ""}
> >
@ -78,6 +81,7 @@ export class HaProgressButton extends LitElement {
pointer-events: none; pointer-events: none;
} }
mwc-button[unelevated].success,
mwc-button[raised].success { mwc-button[raised].success {
--mdc-theme-primary: var(--success-color); --mdc-theme-primary: var(--success-color);
--mdc-theme-on-primary: white; --mdc-theme-on-primary: white;
@ -91,6 +95,7 @@ export class HaProgressButton extends LitElement {
pointer-events: none; pointer-events: none;
} }
mwc-button[unelevated].error,
mwc-button[raised].error { mwc-button[raised].error {
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
--mdc-theme-on-primary: white; --mdc-theme-on-primary: white;

View File

@ -0,0 +1,50 @@
import { css, html, LitElement, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined";
import { customElement, property } from "lit/decorators";
@customElement("ha-divider")
export class HaMdDivider extends LitElement {
@property() public label?: string;
public render() {
return html`
<div
role=${ifDefined(this.label ? "separator" : undefined)}
aria-label=${ifDefined(this.label)}
>
<span class="line"></span>
${this.label
? html`
<span class="label">${this.label}</span>
<span class="line"></span>
`
: nothing}
</div>
`;
}
static styles = css`
:host {
width: var(--ha-divider-width, 100%);
}
div {
display: flex;
align-items: center;
justify-content: center;
}
.label {
padding: var(--ha-divider-label-padding, 0 16px);
}
.line {
flex: 1;
background-color: var(--divider-color);
height: var(--ha-divider-line-height, 1px);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-divider": HaMdDivider;
}
}

View File

@ -1,5 +1,6 @@
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { handleFetchPromise } from "../util/hass-call-api"; import { handleFetchPromise } from "../util/hass-call-api";
import type { CloudStatus } from "./cloud";
export interface InstallationType { export interface InstallationType {
installation_type: installation_type:
@ -36,6 +37,18 @@ export interface OnboardingStep {
done: boolean; done: boolean;
} }
interface CloudLoginBase {
email: string;
}
export interface CloudLoginPassword extends CloudLoginBase {
password: string;
}
export interface CloudLoginMFA extends CloudLoginBase {
code: string;
}
export const fetchOnboardingOverview = () => export const fetchOnboardingOverview = () =>
fetch(`${__HASS_URL__}/api/onboarding`, { credentials: "same-origin" }); fetch(`${__HASS_URL__}/api/onboarding`, { credentials: "same-origin" });
@ -91,3 +104,27 @@ export const fetchInstallationType = async (): Promise<InstallationType> => {
return response.json(); return response.json();
}; };
export const loginHaCloud = async (
params: CloudLoginPassword | CloudLoginMFA
) =>
handleFetchPromise(
fetch("/api/onboarding/cloud/login", {
method: "POST",
body: JSON.stringify(params),
})
);
export const fetchHaCloudStatus = async (): Promise<CloudStatus> =>
handleFetchPromise(fetch("/api/onboarding/cloud/status"));
export const signOutHaCloud = async () =>
handleFetchPromise(fetch("/api/onboarding/cloud/logout", { method: "POST" }));
export const forgotPasswordHaCloud = async (email: string) =>
handleFetchPromise(
fetch("/api/onboarding/cloud/forgot_password", {
method: "POST",
body: JSON.stringify({ email }),
})
);

View File

@ -21,6 +21,7 @@ export interface ConfirmationDialogParams extends BaseDialogBoxParams {
export interface PromptDialogParams extends BaseDialogBoxParams { export interface PromptDialogParams extends BaseDialogBoxParams {
inputLabel?: string; inputLabel?: string;
dismissText?: string;
inputType?: string; inputType?: string;
defaultValue?: string; defaultValue?: string;
placeholder?: string; placeholder?: string;

View File

@ -29,8 +29,9 @@
.header { .header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
margin-bottom: 32px; margin-bottom: 32px;
margin-left: 32px;
} }
.header img { .header img {

View File

@ -1,7 +1,6 @@
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { property, query, state } from "lit/decorators"; import { property, query, state } from "lit/decorators";
import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-button"; import "../components/ha-button";
import "../components/ha-toast"; import "../components/ha-toast";
import "../components/ha-icon-button"; import "../components/ha-icon-button";
@ -63,7 +62,6 @@ class NotificationManager extends LitElement {
return html` return html`
<ha-toast <ha-toast
leading leading
dir=${computeRTL(this.hass) ? "rtl" : "ltr"}
.labelText=${this._parameters.message} .labelText=${this._parameters.message}
.timeoutMs=${this._parameters.duration!} .timeoutMs=${this._parameters.duration!}
@MDCSnackbar:closed=${this._toastClosed} @MDCSnackbar:closed=${this._toastClosed}

View File

@ -41,7 +41,6 @@ import "./onboarding-analytics";
import "./onboarding-create-user"; import "./onboarding-create-user";
import "./onboarding-loading"; import "./onboarding-loading";
import "./onboarding-welcome"; import "./onboarding-welcome";
import "./onboarding-restore-backup";
import "./onboarding-welcome-links"; import "./onboarding-welcome-links";
import { makeDialogManager } from "../dialogs/make-dialog-manager"; import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
@ -50,7 +49,7 @@ import { mainWindow } from "../common/dom/get_main_window";
type OnboardingEvent = type OnboardingEvent =
| { | {
type: "init"; type: "init";
result: { restore: boolean }; result?: { restore: "upload" | "cloud" };
} }
| { | {
type: "user"; type: "user";
@ -98,7 +97,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@state() private _init = false; @state() private _init = false;
@state() private _restoring = false; @state() private _restoring?: "upload" | "cloud";
@state() private _supervisor?: boolean; @state() private _supervisor?: boolean;
@ -160,7 +159,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
return html`<onboarding-restore-backup return html`<onboarding-restore-backup
.localize=${this.localize} .localize=${this.localize}
.supervisor=${this._supervisor ?? false} .supervisor=${this._supervisor ?? false}
.language=${this.language} .mode=${this._restoring}
> >
</onboarding-restore-backup>`; </onboarding-restore-backup>`;
} }
@ -174,7 +173,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
const step = this._curStep()!; const step = this._curStep()!;
if (this._loading || !step) { if (this._loading || !step) {
return html`<onboarding-loading></onboarding-loading> `; return html`<onboarding-loading></onboarding-loading>`;
} }
if (step.step === "user") { if (step.step === "user") {
return html`<onboarding-create-user return html`<onboarding-create-user
@ -215,6 +214,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
this._fetchOnboardingSteps(); this._fetchOnboardingSteps();
import("./onboarding-integrations"); import("./onboarding-integrations");
import("./onboarding-core-config"); import("./onboarding-core-config");
import("./onboarding-restore-backup");
registerServiceWorker(this, false); registerServiceWorker(this, false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev)); this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
this.addEventListener("onboarding-progress", (ev) => this.addEventListener("onboarding-progress", (ev) =>
@ -230,7 +230,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("_page")) { if (changedProps.has("_page")) {
this._restoring = this._page === "restore_backup"; this._restoring =
this._page === "restore_backup"
? "upload"
: this._page === "restore_backup_cloud"
? "cloud"
: undefined;
if (this._page === null && this._steps && !this._steps[0].done) { if (this._page === null && this._steps && !this._steps[0].done) {
this._init = true; this._init = true;
} }
@ -345,12 +350,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
if (stepResult.type === "init") { if (stepResult.type === "init") {
this._init = false; this._init = false;
this._restoring = stepResult.result.restore; this._restoring = stepResult.result?.restore;
if (!this._restoring) { if (!this._restoring) {
this._progress = 0.25; this._progress = 0.25;
} else { } else {
navigate( navigate(
`${location.pathname}?${addSearchParam({ page: "restore_backup" })}` `${location.pathname}?${addSearchParam({ page: `restore_backup${this._restoring === "cloud" ? "_cloud" : ""}` })}`
); );
} }
} else if (stepResult.type === "user") { } else if (stepResult.type === "user") {
@ -489,6 +494,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
} }
static styles = css` static styles = css`
.card-content {
padding: 32px;
}
mwc-linear-progress { mwc-linear-progress {
position: fixed; position: fixed;
top: 0; top: 0;

View File

@ -1,7 +1,6 @@
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../components/ha-svg-icon";
import { brandsUrl } from "../util/brands-url"; import { brandsUrl } from "../util/brands-url";
@customElement("integration-badge") @customElement("integration-badge")

View File

@ -1,15 +1,9 @@
import type { TemplateResult } from "lit"; import type { TemplateResult } 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 "./restore-backup/onboarding-restore-backup-upload";
import "./restore-backup/onboarding-restore-backup-details";
import "./restore-backup/onboarding-restore-backup-restore"; import "./restore-backup/onboarding-restore-backup-restore";
import "./restore-backup/onboarding-restore-backup-status"; import "./restore-backup/onboarding-restore-backup-status";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-spinner";
import "../components/ha-alert";
import "./onboarding-loading"; import "./onboarding-loading";
import { removeSearchParam } from "../common/url/search-params"; import { removeSearchParam } from "../common/url/search-params";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
@ -19,9 +13,11 @@ import {
type BackupOnboardingConfig, type BackupOnboardingConfig,
type BackupOnboardingInfo, type BackupOnboardingInfo,
} from "../data/backup_onboarding"; } from "../data/backup_onboarding";
import type { BackupContentExtended, BackupData } from "../data/backup"; import { CLOUD_AGENT, type BackupContentExtended } from "../data/backup";
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
import { storage } from "../common/decorators/storage"; import { storage } from "../common/decorators/storage";
import { fetchHaCloudStatus, signOutHaCloud } from "../data/onboarding";
import type { CloudStatus } from "../data/cloud";
import { showToast } from "../util/toast";
const STATUS_INTERVAL_IN_MS = 5000; const STATUS_INTERVAL_IN_MS = 5000;
@ -29,27 +25,28 @@ const STATUS_INTERVAL_IN_MS = 5000;
class OnboardingRestoreBackup extends LitElement { class OnboardingRestoreBackup extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc; @property({ attribute: false }) public localize!: LocalizeFunc;
@property() public language!: string;
@property({ type: Boolean }) public supervisor = false; @property({ type: Boolean }) public supervisor = false;
@property() public mode!: "upload" | "cloud";
@state() private _view: @state() private _view:
| "loading" | "loading"
| "upload" | "upload"
| "select_data" | "cloud_login"
| "confirm_restore" | "empty_cloud"
| "restore"
| "status" = "loading"; | "status" = "loading";
@state() private _backup?: BackupContentExtended; @state() private _backup?: BackupContentExtended;
@state() private _backupInfo?: BackupOnboardingInfo; @state() private _backupInfo?: BackupOnboardingInfo;
@state() private _selectedData?: BackupData;
@state() private _error?: string; @state() private _error?: string;
@state() private _failed?: boolean; @state() private _failed?: boolean;
@state() private _cloudStatus?: CloudStatus;
@storage({ @storage({
key: "onboarding-restore-backup-backup-id", key: "onboarding-restore-backup-backup-id",
}) })
@ -62,90 +59,66 @@ class OnboardingRestoreBackup extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${ ${this._view === "loading"
this._view !== "status" || this._failed ? html`<onboarding-loading></onboarding-loading>`
? html`<ha-icon-button-arrow-prev : this._view === "upload"
.label=${this.localize("ui.panel.page-onboarding.restore.back")} ? html`
@click=${this._back} <onboarding-restore-backup-upload
></ha-icon-button-arrow-prev>` .supervisor=${this.supervisor}
: nothing .localize=${this.localize}
} @backup-uploaded=${this._backupUploaded}
</ha-icon-button> ></onboarding-restore-backup-upload>
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1> `
${ : this._view === "cloud_login"
this._error || (this._failed && this._view !== "status")
? html`<ha-alert
alert-type="error"
.title=${this._failed && this._view !== "status"
? this.localize("ui.panel.page-onboarding.restore.failed")
: ""}
>
${this._failed && this._view !== "status"
? this.localize(
`ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
)
: this._error}
</ha-alert>`
: nothing
}
${
this._view === "loading"
? html`<div class="loading">
<ha-spinner></ha-spinner>
</div>`
: this._view === "upload"
? html` ? html`
<onboarding-restore-backup-upload <onboarding-restore-backup-cloud-login
.supervisor=${this.supervisor}
.localize=${this.localize} .localize=${this.localize}
@backup-uploaded=${this._backupUploaded} @ha-refresh-cloud-status=${this._showCloudBackup}
></onboarding-restore-backup-upload> ></onboarding-restore-backup-cloud-login>
` `
: this._view === "select_data" : this._view === "empty_cloud"
? html`<onboarding-restore-backup-details ? html`
.localize=${this.localize} <onboarding-restore-backup-no-cloud-backup
.backup=${this._backup!} .localize=${this.localize}
@backup-restore=${this._restore} @sign-out=${this._signOut}
></onboarding-restore-backup-details>` ></onboarding-restore-backup-no-cloud-backup>
: this._view === "confirm_restore" `
: this._view === "restore"
? html`<onboarding-restore-backup-restore ? html`<onboarding-restore-backup-restore
.mode=${this.mode}
.localize=${this.localize} .localize=${this.localize}
.backup=${this._backup!} .backup=${this._backup!}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
.selectedData=${this._selectedData!} .error=${this._failed
? this.localize(
`ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
)
: this._error}
@restore-started=${this._restoreStarted} @restore-started=${this._restoreStarted}
@restore-backup-back=${this._back}
@sign-out=${this._signOut}
></onboarding-restore-backup-restore>` ></onboarding-restore-backup-restore>`
: nothing : nothing}
} ${this._view === "status" && this._backupInfo
${ ? html`<onboarding-restore-backup-status
this._view === "status" && this._backupInfo .localize=${this.localize}
? html`<onboarding-restore-backup-status .backupInfo=${this._backupInfo}
.localize=${this.localize} @restore-backup-back=${this._back}
.backupInfo=${this._backupInfo} ></onboarding-restore-backup-status>`
@show-backup-upload=${this._reupload} : nothing}
></onboarding-restore-backup-status>`
: nothing
}
${
["select_data", "confirm_restore"].includes(this._view) && this._backup
? html`<div class="backup-summary-wrapper">
<ha-backup-details-summary
translation-key-panel="page-onboarding.restore"
show-upload-another
.backup=${this._backup}
.localize=${this.localize}
@show-backup-upload=${this._reupload}
.isHassio=${this.supervisor}
></ha-backup-details-summary>
</div>`
: nothing
}
`; `;
} }
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this.mode === "cloud") {
import("./restore-backup/onboarding-restore-backup-cloud-login");
import("./restore-backup/onboarding-restore-backup-no-cloud-backup");
} else {
import("./restore-backup/onboarding-restore-backup-upload");
}
this._loadBackupInfo(); this._loadBackupInfo();
} }
@ -194,7 +167,25 @@ class OnboardingRestoreBackup extends LitElement {
last_non_idle_event: lastNonIdleEvent, last_non_idle_event: lastNonIdleEvent,
}; };
if (this._backupId) { if (this.mode === "cloud") {
try {
this._cloudStatus = await fetchHaCloudStatus();
} catch (err: any) {
this._error = err?.message || "Cannot get Home Assistant Cloud status";
}
if (this._cloudStatus?.logged_in && !this._backupId) {
this._backup = backups.find(({ agents }) =>
Object.keys(agents).includes(CLOUD_AGENT)
);
if (!this._backup) {
this._view = "empty_cloud";
return;
}
this._backupId = this._backup?.backup_id;
}
} else if (this._backupId) {
this._backup = backups.find( this._backup = backups.find(
({ backup_id }) => backup_id === this._backupId ({ backup_id }) => backup_id === this._backupId
); );
@ -219,31 +210,24 @@ class OnboardingRestoreBackup extends LitElement {
return; return;
} }
if ( if (this._backup) {
this._backup && this._view = "restore";
// after backup was uploaded
(lastNonIdleEvent?.manager_state === "receive_backup" ||
// when restore was confirmed but failed to start (for example, encryption key was wrong)
failedRestore)
) {
if (!this.supervisor && this._backup.homeassistant_included) {
this._selectedData = {
homeassistant_included: true,
folders: [],
addons: [],
homeassistant_version: this._backup.homeassistant_version,
database_included: this._backup.database_included,
};
// skip select data when supervisor is not available and backup includes HA
this._view = "confirm_restore";
} else {
this._view = "select_data";
}
return; return;
} }
// show upload as default // show default view
this._view = "upload"; if (this.mode === "upload") {
this._view = "upload";
} else if (this._cloudStatus?.logged_in) {
this._view = "empty_cloud";
} else {
this._view = "cloud_login";
}
}
private _showCloudBackup() {
this._view = "loading";
this._loadBackupInfo();
} }
private _scheduleLoadBackupInfo() { private _scheduleLoadBackupInfo() {
@ -264,45 +248,48 @@ class OnboardingRestoreBackup extends LitElement {
await this._loadBackupInfo(); await this._loadBackupInfo();
} }
private async _back() { private async _signOut() {
if (this._view === "upload" || (this._view === "status" && this._failed)) { this._view = "loading";
navigate(`${location.pathname}?${removeSearchParam("page")}`);
} else { showToast(this, {
const confirmed = await showConfirmationDialog(this, { id: "sign-out-ha-cloud",
title: this.localize( message: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.title" "ui.panel.page-onboarding.restore.ha-cloud.sign_out_progress"
), ),
text: this.localize( });
"ui.panel.page-onboarding.restore.cancel_restore.text" this._backupId = undefined;
), this._cloudStatus = undefined;
confirmText: this.localize( try {
"ui.panel.page-onboarding.restore.cancel_restore.yes" await signOutHaCloud();
), showToast(this, {
dismissText: this.localize( id: "sign-out-ha-cloud",
"ui.panel.page-onboarding.restore.cancel_restore.no" message: this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.sign_out_success"
),
});
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
showToast(this, {
id: "sign-out-ha-cloud",
message: this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.sign_out_error"
), ),
}); });
if (!confirmed) {
return;
}
navigate(`${location.pathname}?${removeSearchParam("page")}`);
} }
navigate(`${location.pathname}?${removeSearchParam("page")}`);
} }
private _restore(ev: CustomEvent) { private async _back() {
if (!this._backup || !ev.detail.selectedData) { this._view = "loading";
return;
}
this._selectedData = ev.detail.selectedData;
this._view = "confirm_restore";
}
private _reupload() {
this._backup = undefined; this._backup = undefined;
this._backupId = undefined; this._backupId = undefined;
this._view = "upload"; if (this.mode === "upload") {
this._view = "upload";
} else {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
} }
static styles = [ static styles = [
@ -313,21 +300,8 @@ class OnboardingRestoreBackup extends LitElement {
flex-direction: column; flex-direction: column;
position: relative; position: relative;
} }
ha-icon-button-arrow-prev { .logout {
position: absolute; white-space: nowrap;
top: 12px;
}
ha-card {
width: 100%;
}
.loading {
display: flex;
justify-content: center;
padding: 32px;
}
.backup-summary-wrapper {
margin-top: 24px;
padding: 0 20px;
} }
`, `,
]; ];

View File

@ -6,6 +6,10 @@ import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles"; import { onBoardingStyles } from "./styles";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import "../components/ha-button"; import "../components/ha-button";
import "../components/ha-divider";
import "../components/ha-md-list";
import "../components/ha-md-list-item";
import "../components/ha-icon-button-next";
@customElement("onboarding-welcome") @customElement("onboarding-welcome")
class OnboardingWelcome extends LitElement { class OnboardingWelcome extends LitElement {
@ -22,23 +26,52 @@ class OnboardingWelcome extends LitElement {
${this.localize("ui.panel.page-onboarding.welcome.start")} ${this.localize("ui.panel.page-onboarding.welcome.start")}
</ha-button> </ha-button>
<ha-button @click=${this._restoreBackup}> <ha-divider
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")} .label=${this.localize("ui.panel.page-onboarding.welcome.or_restore")}
</ha-button> ></ha-divider>
<ha-md-list>
<ha-md-list-item type="button" @click=${this._restoreBackupUpload}>
<div slot="headline">
${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
</div>
<div slot="supporting-text">
${this.localize(
"ui.panel.page-onboarding.restore.options.upload_description"
)}
</div>
<ha-icon-button-next slot="end"></ha-icon-button-next>
</ha-md-list-item>
<ha-md-list-item type="button" @click=${this._restoreBackupCloud}>
<div slot="headline">Home Assistant Cloud</div>
<div slot="supporting-text">
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.description"
)}
</div>
<ha-icon-button-next slot="end"></ha-icon-button-next>
</ha-md-list-item>
</ha-md-list>
`; `;
} }
private _start(): void { private _start(): void {
fireEvent(this, "onboarding-step", { fireEvent(this, "onboarding-step", {
type: "init", type: "init",
result: { restore: false },
}); });
} }
private _restoreBackup(): void { private _restoreBackupUpload(): void {
fireEvent(this, "onboarding-step", { fireEvent(this, "onboarding-step", {
type: "init", type: "init",
result: { restore: true }, result: { restore: "upload" },
});
}
private _restoreBackupCloud(): void {
fireEvent(this, "onboarding-step", {
type: "init",
result: { restore: "cloud" },
}); });
} }
@ -49,13 +82,31 @@ class OnboardingWelcome extends LitElement {
:host { :host {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: flex-start;
}
h1 {
margin-top: 16px;
margin-bottom: 8px;
}
p {
margin: 0;
} }
.start { .start {
--button-height: 48px; --button-height: 48px;
--mdc-typography-button-font-size: 1rem; --mdc-typography-button-font-size: 1rem;
--mdc-button-horizontal-padding: 24px; --mdc-button-horizontal-padding: 24px;
margin: 16px 0; margin: 32px 0;
}
ha-divider {
--ha-divider-width: calc(100% + 64px);
margin-left: -32px;
margin-right: -32px;
}
ha-md-list {
width: calc(100% + 32px);
margin-left: -16px;
margin-right: -16px;
padding-bottom: 0;
} }
`, `,
]; ];

View File

@ -0,0 +1,148 @@
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import "../../panels/config/cloud/login/cloud-login";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-spinner";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { BackupContentExtended } from "../../data/backup";
import { navigate } from "../../common/navigate";
import { removeSearchParam } from "../../common/url/search-params";
import { onBoardingStyles } from "../styles";
import type { CloudLogin } from "../../panels/config/cloud/login/cloud-login";
import type { CloudForgotPasswordCard } from "../../panels/config/cloud/forgot-password/cloud-forgot-password-card";
@customElement("onboarding-restore-backup-cloud-login")
class OnboardingRestoreBackupCloudLogin extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: false }) public backup!: BackupContentExtended;
@state() private _email?: string;
@state() private _view: "login" | "forgot-password" | "loading" = "login";
@state() private _showResetPasswordDone = false;
@query("cloud-login") private _cloudLoginElement?: CloudLogin;
@query("cloud-forgot-password-card")
private _forgotPasswordElement?: CloudForgotPasswordCard;
render() {
return html`
<ha-icon-button-arrow-prev
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
@click=${this._back}
></ha-icon-button-arrow-prev>
<h1>Home Assistant Cloud</h1>
<p>
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.sign_in_description"
)}
</p>
${this._showResetPasswordDone ? this._renderResetPasswordDone() : nothing}
${this._view === "login"
? html`<cloud-login
card-less
.email=${this._email}
.localize=${this.localize}
translation-key-panel="page-onboarding.restore.ha-cloud"
@cloud-forgot-password=${this._showForgotPassword}
></cloud-login>`
: this._view === "loading"
? html`<div class="loading">
<ha-spinner size="large"></ha-spinner>
</div>`
: html`<cloud-forgot-password-card
card-less
.email=${this._email}
.localize=${this.localize}
translation-key-panel="page-onboarding.restore.ha-cloud.forgot_password"
@cloud-email-changed=${this._emailChanged}
@cloud-done=${this._showPasswordResetDone}
></cloud-forgot-password-card>`}
`;
}
private _back() {
if (this._view === "forgot-password") {
this._view = "login";
return;
}
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
private _renderResetPasswordDone() {
return html`<ha-alert
dismissable
@alert-dismissed-clicked=${this._dismissResetPasswordDoneInfo}
>
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.forgot_password.check_your_email"
)}
</ha-alert>`;
}
private async _showForgotPassword() {
this._view = "loading";
if (this._cloudLoginElement) {
this._email = this._cloudLoginElement.emailField.value;
}
await import(
"../../panels/config/cloud/forgot-password/cloud-forgot-password-card"
);
this._view = "forgot-password";
}
private _emailChanged() {
if (this._forgotPasswordElement) {
this._email = this._forgotPasswordElement?.emailField.value;
}
}
private _showPasswordResetDone() {
this._view = "login";
this._showResetPasswordDone = true;
}
private _dismissResetPasswordDoneInfo() {
this._showResetPasswordDone = false;
}
static get styles(): CSSResultGroup {
return [
onBoardingStyles,
css`
h1,
p {
text-align: left;
}
h2 {
font-size: 24px;
display: flex;
align-items: center;
gap: 16px;
}
h2 img {
width: 48px;
}
.loading {
display: flex;
justify-content: center;
}
ha-alert {
margin-bottom: 8px;
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-restore-backup-cloud-login": OnboardingRestoreBackupCloudLogin;
}
}

View File

@ -1,56 +0,0 @@
import { css, html, LitElement, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../panels/config/backup/components/ha-backup-details-restore";
import "../../panels/config/backup/components/ha-backup-details-summary";
import { haStyle } from "../../resources/styles";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { BackupContentExtended } from "../../data/backup";
@customElement("onboarding-restore-backup-details")
class OnboardingRestoreBackupDetails extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: false }) public backup!: BackupContentExtended;
render() {
return html`
${this.backup.homeassistant_included
? html`<ha-backup-details-restore
.backup=${this.backup}
.localize=${this.localize}
translation-key-panel="page-onboarding.restore"
ha-required
></ha-backup-details-restore>`
: html`
<ha-alert alert-type="error">
${this.localize(
"ui.panel.page-onboarding.restore.details.home_assistant_missing"
)}
</ha-alert>
`}
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
padding: 28px 20px 0;
}
ha-backup-details-restore {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-restore-backup-details": OnboardingRestoreBackupDetails;
}
}

View File

@ -0,0 +1,88 @@
import { LitElement, html, css, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-button";
import "../../components/ha-icon-button-arrow-prev";
import { fireEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
import { removeSearchParam } from "../../common/url/search-params";
import { onBoardingStyles } from "../styles";
@customElement("onboarding-restore-backup-no-cloud-backup")
class OnboardingRestoreBackupNoCloudBackup extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
render() {
return html`
<ha-icon-button-arrow-prev
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
@click=${this._back}
></ha-icon-button-arrow-prev>
<h1>
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.no_cloud_backup"
)}
</h1>
<div class="description">
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.no_cloud_backup_description"
)}
</div>
<div class="actions">
<ha-button @click=${this._signOut}>
${this.localize("ui.panel.page-onboarding.restore.ha-cloud.sign_out")}
</ha-button>
<a
href="https://www.nabucasa.com/config/backups/"
target="_blank"
rel="noreferrer noopener"
>
<ha-button>
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.learn_more"
)}
</ha-button>
</a>
</div>
`;
}
private _back() {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
private _signOut() {
fireEvent(this, "sign-out");
}
static get styles(): CSSResultGroup {
return [
onBoardingStyles,
css`
h1,
p {
text-align: left;
}
.description {
font-size: 1rem;
line-height: 1.5rem;
margin-top: 24px;
margin-bottom: 32px;
}
.actions {
display: flex;
justify-content: space-between;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-restore-backup-no-cloud-backup": OnboardingRestoreBackupNoCloudBackup;
}
interface HASSDomEvents {
"sign-out": undefined;
}
}

View File

@ -1,20 +1,25 @@
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit"; import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import "../../components/ha-card"; import "../../components/ha-button";
import "../../components/ha-alert"; import "../../components/ha-alert";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/buttons/ha-progress-button"; import "../../components/buttons/ha-progress-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-password-field"; import "../../components/ha-password-field";
import { haStyle } from "../../resources/styles"; import "../../panels/config/backup/components/ha-backup-data-picker";
import "../../panels/config/backup/components/ha-backup-formfield-label";
import type { LocalizeFunc } from "../../common/translations/localize"; import type { LocalizeFunc } from "../../common/translations/localize";
import { import {
CORE_LOCAL_AGENT, getPreferredAgentForDownload,
HASSIO_LOCAL_AGENT,
type BackupContentExtended, type BackupContentExtended,
type BackupData, type BackupData,
} from "../../data/backup"; } from "../../data/backup";
import { restoreOnboardingBackup } from "../../data/backup_onboarding"; import { restoreOnboardingBackup } from "../../data/backup_onboarding";
import type { HaProgressButton } from "../../components/buttons/ha-progress-button"; import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { onBoardingStyles } from "../styles";
import { formatDateTimeWithBrowserDefaults } from "../../common/datetime/format_date_time";
@customElement("onboarding-restore-backup-restore") @customElement("onboarding-restore-backup-restore")
class OnboardingRestoreBackupRestore extends LitElement { class OnboardingRestoreBackupRestore extends LitElement {
@ -22,11 +27,12 @@ class OnboardingRestoreBackupRestore extends LitElement {
@property({ attribute: false }) public backup!: BackupContentExtended; @property({ attribute: false }) public backup!: BackupContentExtended;
@property({ attribute: false })
public selectedData!: BackupData;
@property({ type: Boolean }) public supervisor = false; @property({ type: Boolean }) public supervisor = false;
@property() public error?: string;
@property() public mode!: "upload" | "cloud";
@state() private _encryptionKey = ""; @state() private _encryptionKey = "";
@state() private _encryptionKeyWrong = false; @state() private _encryptionKeyWrong = false;
@ -35,94 +41,237 @@ class OnboardingRestoreBackupRestore extends LitElement {
@state() private _loading = false; @state() private _loading = false;
@state() private _selectedData?: BackupData;
@query("ha-progress-button")
private _progressButtonElement!: HaProgressButton;
render() { render() {
const agentId = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT; const agentId = getPreferredAgentForDownload(
Object.keys(this.backup.agents)
);
const backupProtected = this.backup.agents[agentId].protected; const backupProtected = this.backup.agents[agentId].protected;
const formattedDate = formatDateTimeWithBrowserDefaults(
new Date(this.backup.date)
);
const onlyHomeAssistantBackup =
this.backup.addons.length === 0 && this.backup.folders.length === 0;
return html` return html`
${this.backup.homeassistant_included && <ha-icon-button-arrow-prev
!this.supervisor && .label=${this.localize("ui.panel.page-onboarding.restore.back")}
(this.backup.addons.length > 0 || this.backup.folders.length > 0) @click=${this._back}
? html`<ha-alert alert-type="warning" class="supervisor-warning"> ></ha-icon-button-arrow-prev>
${this.localize( <h1>
"ui.panel.page-onboarding.restore.details.addons_unsupported" ${this.localize(
)} "ui.panel.page-onboarding.restore.details.restore.title"
</ha-alert>` )}
: nothing} </h1>
<ha-card
.header=${this.localize("ui.panel.page-onboarding.restore.restore")} ${this.backup.homeassistant_included
> ? html`<div class="description">
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
: nothing}
<p>
${this.localize( ${this.localize(
"ui.panel.page-onboarding.restore.confirm_restore_full_backup_text" "ui.panel.page-onboarding.restore.confirm_restore_full_backup_text"
)} )}
</p> </div>`
${backupProtected : html`
? html`<p> <ha-alert alert-type="error">
${this.localize( ${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.title" "ui.panel.page-onboarding.restore.details.home_assistant_missing"
)} )}
</p> </ha-alert>
${this._encryptionKeyWrong `}
? html` ${this.error
<ha-alert alert-type="error"> ? html`<ha-alert
${this.localize( alert-type="error"
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key" .title=${this.localize("ui.panel.page-onboarding.restore.failed")}
)}
</ha-alert>
`
: nothing}
<ha-password-field
.disabled=${this._loading}
@input=${this._encryptionKeyChanged}
.label=${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.input_label"
)}
.value=${this._encryptionKey}
></ha-password-field>`
: nothing}
</div>
<div class="card-actions">
<ha-progress-button
.progress=${this._loading}
.disabled=${this._loading ||
(backupProtected && this._encryptionKey === "")}
@click=${this._startRestore}
destructive
> >
${this.error}
</ha-alert>`
: nothing}
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.localize( ${this.localize(
"ui.panel.page-onboarding.restore.details.restore.action" "ui.panel.page-onboarding.restore.details.summary.created"
)} )}
</ha-progress-button> </span>
</div> <span slot="supporting-text">${formattedDate}</span>
</ha-card> </ha-md-list-item>
${onlyHomeAssistantBackup
? html`<ha-md-list-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.content"
)}
</span>
<ha-backup-formfield-label
slot="supporting-text"
.version=${this.backup.homeassistant_version}
.label=${this.localize(
`ui.panel.page-onboarding.restore.data_picker.${this.backup.database_included ? "settings_and_history" : "settings"}`
)}
></ha-backup-formfield-label>
</ha-md-list-item>`
: nothing}
</ha-md-list>
${!onlyHomeAssistantBackup
? html`<h2>
${this.localize("ui.panel.page-onboarding.restore.select_type")}
</h2>`
: nothing}
${this.backup.homeassistant_included &&
!this.supervisor &&
this.backup.addons.length > 0
? html`<ha-alert class="supervisor-warning">
${this.localize(
"ui.panel.page-onboarding.restore.details.addons_unsupported"
)}
<a
slot="action"
href="https://www.home-assistant.io/installation/#advanced-installation-methods"
target="_blank"
rel="noreferrer noopener"
>
<ha-button
>${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.learn_more"
)}</ha-button
>
</a>
</ha-alert>`
: nothing}
${!onlyHomeAssistantBackup
? html`<ha-backup-data-picker
translation-key-panel="page-onboarding.restore"
.localize=${this.localize}
.data=${this.backup}
.value=${this._selectedData}
@value-changed=${this._selectedBackupChanged}
.requiredItems=${["config"]}
.addonsDisabled=${!this.supervisor}
></ha-backup-data-picker>`
: nothing}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
: nothing}
${backupProtected
? html`<div class="encryption">
<h2>
${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.label"
)}
</h2>
<span>
${this.localize(
`ui.panel.page-onboarding.restore.details.restore.encryption.description${this.mode === "cloud" ? "_cloud" : ""}`
)}
</span>
<ha-password-field
.disabled=${this._loading}
@input=${this._encryptionKeyChanged}
.label=${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.input_label"
)}
.value=${this._encryptionKey}
@keydown=${this._keyDown}
.errorMessage=${this._encryptionKeyWrong
? this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
)
: ""}
.invalid=${this._encryptionKeyWrong}
></ha-password-field>
</div>`
: nothing}
<div class="actions${this.mode === "cloud" ? " cloud" : ""}">
${this.mode === "cloud"
? html`<ha-button @click=${this._signOut}>
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.sign_out"
)}
</ha-button>`
: nothing}
<ha-progress-button
unelevated
.progress=${this._loading}
.disabled=${this._loading ||
(backupProtected && this._encryptionKey === "") ||
!this.backup.homeassistant_included}
@click=${this._startRestore}
>
${this.localize(
"ui.panel.page-onboarding.restore.details.restore.action"
)}
</ha-progress-button>
</div>
`; `;
} }
protected willUpdate() {
if (!this.hasUpdated) {
this._selectedData = {
homeassistant_included: true,
folders: [],
addons: [],
homeassistant_version: this.backup.homeassistant_version,
database_included: this.backup.database_included,
};
}
}
private _keyDown(ev: KeyboardEvent) {
if (ev.key === "Enter" && this._encryptionKey !== "") {
this._progressButtonElement.click();
}
}
private _signOut() {
fireEvent(this, "sign-out");
}
private _selectedBackupChanged(ev: CustomEvent) {
ev.stopPropagation();
this._selectedData = ev.detail.value;
}
private _encryptionKeyChanged(ev): void { private _encryptionKeyChanged(ev): void {
this._encryptionKey = ev.target.value; this._encryptionKey = ev.target.value;
} }
private async _startRestore(ev: CustomEvent): Promise<void> { private async _startRestore(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as HaProgressButton; const agentId = Object.keys(this.backup.agents)[0];
const backupProtected = this.backup.agents[agentId].protected;
if (
this._loading ||
(backupProtected && this._encryptionKey === "") ||
!this.backup.homeassistant_included ||
!this._selectedData
) {
return;
}
this._loading = true; this._loading = true;
const button = ev.currentTarget as HaProgressButton;
this._error = undefined; this._error = undefined;
this._encryptionKeyWrong = false; this._encryptionKeyWrong = false;
const backupAgent = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT; const backupAgent = Object.keys(this.backup.agents)[0];
try { try {
await restoreOnboardingBackup({ await restoreOnboardingBackup({
agent_id: backupAgent, agent_id: backupAgent,
backup_id: this.backup.backup_id, backup_id: this.backup.backup_id,
password: this._encryptionKey || undefined, password: this._encryptionKey || undefined,
restore_addons: this.selectedData.addons.map((addon) => addon.slug), restore_addons: this._selectedData.addons.map((addon) => addon.slug),
restore_database: this.selectedData.database_included, restore_database: this._selectedData.database_included,
restore_folders: this.selectedData.folders, restore_folders: this._selectedData.folders,
}); });
button.actionSuccess(); button.actionSuccess();
fireEvent(this, "restore-started"); fireEvent(this, "restore-started");
@ -145,21 +294,81 @@ class OnboardingRestoreBackupRestore extends LitElement {
} }
} }
private _back() {
fireEvent(this, "restore-backup-back");
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, onBoardingStyles,
css` css`
:host { h1,
padding: 28px 20px 0; p {
text-align: left;
} }
.card-actions { .description {
font-size: 1rem;
line-height: 1.5rem;
margin-top: 24px;
margin-bottom: 16px;
}
ha-alert {
display: block;
margin-top: 16px;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 64px;
--md-list-item-supporting-text-size: 1rem;
--md-list-item-label-text-size: 0.875rem;
--md-list-item-label-text-color: var(--secondary-text-color);
--md-list-item-supporting-text-color: var(--primary-text-color);
}
ha-md-list-item [slot="supporting-text"] {
display: flex; display: flex;
justify-content: flex-end; align-items: center;
flex-direction: row;
gap: 8px;
line-height: normal;
}
h2 {
font-size: 22px;
margin-top: 24px;
margin-bottom: 8px;
font-style: normal;
font-weight: 400;
} }
.supervisor-warning { .supervisor-warning {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
ha-backup-data-picker {
display: block;
margin-bottom: 32px;
}
.encryption {
margin-bottom: 32px;
}
.encryption ha-password-field {
margin-top: 24px;
}
.actions {
display: flex;
justify-content: flex-end;
}
.actions.cloud {
justify-content: space-between;
}
a ha-button {
--mdc-theme-primary: var(--primary-color);
white-space: nowrap;
}
`, `,
]; ];
} }
@ -171,5 +380,6 @@ declare global {
} }
interface HASSDomEvents { interface HASSDomEvents {
"restore-started"; "restore-started";
"restore-backup-back";
} }
} }

View File

@ -1,15 +1,12 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit"; import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-spinner";
import "../../components/ha-alert"; import "../../components/ha-alert";
import "../../components/ha-button"; import "../../components/ha-button";
import { haStyle } from "../../resources/styles";
import type { LocalizeFunc } from "../../common/translations/localize"; import type { LocalizeFunc } from "../../common/translations/localize";
import type { BackupOnboardingInfo } from "../../data/backup_onboarding"; import type { BackupOnboardingInfo } from "../../data/backup_onboarding";
import { onBoardingStyles } from "../styles";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
import { removeSearchParam } from "../../common/url/search-params";
@customElement("onboarding-restore-backup-status") @customElement("onboarding-restore-backup-status")
class OnboardingRestoreBackupStatus extends LitElement { class OnboardingRestoreBackupStatus extends LitElement {
@ -20,73 +17,64 @@ class OnboardingRestoreBackupStatus extends LitElement {
render() { render() {
return html` return html`
<ha-card <h1>
.header=${this.localize( ${this.localize(
`ui.panel.page-onboarding.restore.${this.backupInfo.state === "restore_backup" ? "in_progress" : "failed"}` `ui.panel.page-onboarding.restore.${this.backupInfo.state === "restore_backup" ? "in_progress" : "failed"}`
)} )}
> </h1>
<div class="card-content"> ${this.backupInfo.state === "restore_backup"
${this.backupInfo.state === "restore_backup" ? html` <p>
? html` ${this.localize(
<div class="loading"> `ui.panel.page-onboarding.restore.in_progress_description`
<ha-spinner></ha-spinner> )}
</div> </p>`
<p> : nothing}
${this.localize( <div class="card-content">
"ui.panel.page-onboarding.restore.in_progress_description" ${this.backupInfo.state === "restore_backup"
)} ? html`
</p> <div class="loading">
` <mwc-linear-progress indeterminate></mwc-linear-progress>
: html` </div>
<ha-alert alert-type="error"> `
${this.localize( : html`
"ui.panel.page-onboarding.restore.failed_status_description" <ha-alert alert-type="error">
)}
</ha-alert>
${this.backupInfo.last_non_idle_event?.reason
? html`
<div class="failed">
<h4>Error:</h4>
${this.backupInfo.last_non_idle_event?.reason}
</div>
`
: nothing}
`}
</div>
${this.backupInfo.state !== "restore_backup"
? html`<div class="card-actions">
<ha-button @click=${this._uploadAnother} destructive>
${this.localize( ${this.localize(
`ui.panel.page-onboarding.restore.details.summary.upload_another` "ui.panel.page-onboarding.restore.failed_status_description"
)} )}
</ha-button> </ha-alert>
<ha-button @click=${this._home} destructive> ${this.backupInfo.last_non_idle_event?.reason
${this.localize( ? html`
`ui.panel.page-onboarding.restore.details.summary.home` <div class="failed">
)} <h4>Error:</h4>
</ha-button> ${this.backupInfo.last_non_idle_event?.reason}
</div>` </div>
: nothing} `
</ha-card> : nothing}
`}
</div>
${this.backupInfo.state !== "restore_backup"
? html`<div class="actions">
<ha-button @click=${this._back}>
${this.localize("ui.panel.page-onboarding.restore.back")}
</ha-button>
</div>`
: nothing}
`; `;
} }
private _uploadAnother() { private _back() {
fireEvent(this, "show-backup-upload"); fireEvent(this, "restore-backup-back");
}
private _home() {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, onBoardingStyles,
css` css`
:host { h1,
padding: 28px 20px 0; p {
text-align: left;
} }
.card-actions { .actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
@ -104,6 +92,9 @@ class OnboardingRestoreBackupStatus extends LitElement {
padding: 16px 0; padding: 16px 0;
font-size: 16px; font-size: 16px;
} }
mwc-linear-progress {
width: 100%;
}
`, `,
]; ];
} }

View File

@ -1,10 +1,9 @@
import { mdiFolderUpload } from "@mdi/js"; import { mdiFolderUpload } from "@mdi/js";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit"; import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-file-upload"; import "../../components/ha-file-upload";
import "../../components/ha-alert"; import "../../components/ha-alert";
import { haStyle } from "../../resources/styles"; import "../../components/ha-icon-button-arrow-prev";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event"; import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { import {
@ -14,6 +13,9 @@ import {
} from "../../data/backup"; } from "../../data/backup";
import type { LocalizeFunc } from "../../common/translations/localize"; import type { LocalizeFunc } from "../../common/translations/localize";
import { uploadOnboardingBackup } from "../../data/backup_onboarding"; import { uploadOnboardingBackup } from "../../data/backup_onboarding";
import { onBoardingStyles } from "../styles";
import { navigate } from "../../common/navigate";
import { removeSearchParam } from "../../common/url/search-params";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -32,39 +34,41 @@ class OnboardingRestoreBackupUpload extends LitElement {
render() { render() {
return html` return html`
<ha-card <ha-icon-button-arrow-prev
.header=${this.localize( .label=${this.localize("ui.panel.page-onboarding.restore.back")}
"ui.panel.page-onboarding.restore.upload_backup" @click=${this._back}
></ha-icon-button-arrow-prev>
<h1>
${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
</h1>
<p>
${this.localize(
"ui.panel.page-onboarding.restore.upload_backup_subtitle"
)} )}
> </p>
<div class="card-content"> ${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-file-upload
<ha-file-upload .uploading=${this._uploading}
.uploading=${this._uploading} .icon=${mdiFolderUpload}
.icon=${mdiFolderUpload} accept=${SUPPORTED_UPLOAD_FORMAT}
accept=${SUPPORTED_UPLOAD_FORMAT} .localize=${this.localize}
.localize=${this.localize} .label=${this.localize(
.label=${this.localize( "ui.panel.page-onboarding.restore.upload_input_label"
"ui.panel.page-onboarding.restore.upload_input_label" )}
)} .secondary=${this.localize(
.secondary=${this.localize( "ui.panel.page-onboarding.restore.upload_secondary"
"ui.panel.page-onboarding.restore.upload_secondary" )}
)} .supports=${this.localize(
.supports=${this.localize( "ui.panel.page-onboarding.restore.upload_supports_tar"
"ui.panel.page-onboarding.restore.upload_supports_tar" )}
)} .deleteLabel=${this.localize("ui.panel.page-onboarding.restore.delete")}
.deleteLabel=${this.localize( .uploadingLabel=${this.localize(
"ui.panel.page-onboarding.restore.delete" "ui.panel.page-onboarding.restore.uploading"
)} )}
.uploadingLabel=${this.localize( @file-picked=${this._filePicked}
"ui.panel.page-onboarding.restore.uploading" ></ha-file-upload>
)}
@file-picked=${this._filePicked}
></ha-file-upload>
</div>
</ha-card>
`; `;
} }
@ -98,17 +102,20 @@ class OnboardingRestoreBackupUpload extends LitElement {
typeof err.body === "string" typeof err.body === "string"
? err.body ? err.body
: err.body?.message || err.message || "Unknown error occurred"; : err.body?.message || err.message || "Unknown error occurred";
} finally {
this._uploading = false;
} }
} }
private _back() {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, onBoardingStyles,
css` css`
:host { h1,
width: 100%; p {
text-align: left;
} }
.card-actions { .card-actions {
display: flex; display: flex;

View File

@ -1,16 +1,24 @@
import { css } from "lit"; import { css } from "lit";
export const onBoardingStyles = css` export const onBoardingStyles = css`
.card-content {
padding: 32px;
}
h1 { h1 {
text-align: center;
font-weight: 400; font-weight: 400;
font-size: 28px; font-size: 28px;
line-height: 36px; line-height: 36px;
margin-bottom: 8px;
}
ha-icon-button-arrow-prev {
margin-left: -12px;
display: block;
} }
p { p {
font-size: 1rem; font-size: 1rem;
line-height: 1.5rem; line-height: 1.5rem;
text-align: center; margin-top: 0;
margin-bottom: 32px;
} }
.footer { .footer {
margin-top: 16px; margin-top: 16px;

View File

@ -7,7 +7,6 @@ import { stringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-checkbox"; import "../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../components/ha-checkbox"; import type { HaCheckbox } from "../../../../components/ha-checkbox";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "./ha-backup-formfield-label"; import "./ha-backup-formfield-label";
@ -30,6 +29,8 @@ export class HaBackupAddonsPicker extends LitElement {
@property({ attribute: "hide-version", type: Boolean }) @property({ attribute: "hide-version", type: Boolean })
public hideVersion = false; public hideVersion = false;
@property({ type: Boolean }) public disabled = false;
private _addons = memoizeOne((addons: BackupAddonItem[]) => private _addons = memoizeOne((addons: BackupAddonItem[]) =>
addons.sort((a, b) => addons.sort((a, b) =>
stringCompare(a.name, b.name, this.hass?.locale?.language) stringCompare(a.name, b.name, this.hass?.locale?.language)
@ -56,6 +57,7 @@ export class HaBackupAddonsPicker extends LitElement {
.id=${item.slug} .id=${item.slug}
.checked=${this.value?.includes(item.slug) || false} .checked=${this.value?.includes(item.slug) || false}
@change=${this._checkboxChanged} @change=${this._checkboxChanged}
.disabled=${this.disabled}
></ha-checkbox> ></ha-checkbox>
</ha-formfield> </ha-formfield>
` `

View File

@ -17,7 +17,6 @@ import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-checkbox"; import "../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../components/ha-checkbox"; import type { HaCheckbox } from "../../../../components/ha-checkbox";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
import "../../../../components/ha-svg-icon";
import type { BackupData } from "../../../../data/backup"; import type { BackupData } from "../../../../data/backup";
import { fetchHassioAddonsInfo } from "../../../../data/hassio/addon"; import { fetchHassioAddonsInfo } from "../../../../data/hassio/addon";
import { mdiHomeAssistant } from "../../../../resources/home-assistant-logo-svg"; import { mdiHomeAssistant } from "../../../../resources/home-assistant-logo-svg";
@ -62,6 +61,8 @@ export class HaBackupDataPicker extends LitElement {
| "page-onboarding.restore" | "page-onboarding.restore"
| "config.backup" = "config.backup"; | "config.backup" = "config.backup";
@property({ type: Boolean, attribute: false }) public addonsDisabled = false;
@state() public _addonIcons: Record<string, boolean> = {}; @state() public _addonIcons: Record<string, boolean> = {};
protected firstUpdated(changedProps: PropertyValues): void { protected firstUpdated(changedProps: PropertyValues): void {
@ -304,6 +305,7 @@ export class HaBackupDataPicker extends LitElement {
.indeterminate=${selectedItems.addons.length > 0 && .indeterminate=${selectedItems.addons.length > 0 &&
selectedItems.addons.length < addonsItems.length} selectedItems.addons.length < addonsItems.length}
@change=${this._sectionChanged} @change=${this._sectionChanged}
.disabled=${this.addonsDisabled}
></ha-checkbox> ></ha-checkbox>
</ha-formfield> </ha-formfield>
<ha-backup-addons-picker <ha-backup-addons-picker
@ -311,6 +313,7 @@ export class HaBackupDataPicker extends LitElement {
.value=${selectedItems.addons} .value=${selectedItems.addons}
@value-changed=${this._addonsChanged} @value-changed=${this._addonsChanged}
.addons=${addonsItems} .addons=${addonsItems}
.disabled=${this.addonsDisabled}
> >
</ha-backup-addons-picker> </ha-backup-addons-picker>
</div> </div>

View File

@ -1,77 +1,54 @@
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item"; import "../../../../components/ha-md-list-item";
import "../../../../components/ha-button";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { LocalizeFunc } from "../../../../common/translations/localize"; import { formatDateTime } from "../../../../common/datetime/format_date_time";
import {
formatDateTime,
formatDateTimeWithBrowserDefaults,
} from "../../../../common/datetime/format_date_time";
import { import {
computeBackupSize, computeBackupSize,
computeBackupType, computeBackupType,
type BackupContentExtended, type BackupContentExtended,
} from "../../../../data/backup"; } from "../../../../data/backup";
import { fireEvent } from "../../../../common/dom/fire_event";
import { bytesToString } from "../../../../util/bytes-to-string"; import { bytesToString } from "../../../../util/bytes-to-string";
declare global {
interface HASSDomEvents {
"show-backup-upload": undefined;
}
}
@customElement("ha-backup-details-summary") @customElement("ha-backup-details-summary")
class HaBackupDetailsSummary extends LitElement { class HaBackupDetailsSummary extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ type: Object }) public backup!: BackupContentExtended; @property({ type: Object }) public backup!: BackupContentExtended;
@property({ type: Boolean, attribute: "hassio" }) public isHassio = false; @property({ type: Boolean, attribute: "hassio" }) public isHassio = false;
@property({ attribute: "translation-key-panel" }) public translationKeyPanel:
| "page-onboarding.restore"
| "config.backup" = "config.backup";
@property({ type: Boolean, attribute: "show-upload-another" })
public showUploadAnother = false;
render() { render() {
const backupDate = new Date(this.backup.date); const backupDate = new Date(this.backup.date);
const formattedDate = this.hass const formattedDate = formatDateTime(
? formatDateTime(backupDate, this.hass.locale, this.hass.config) backupDate,
: formatDateTimeWithBrowserDefaults(backupDate); this.hass.locale,
this.hass.config
);
return html` return html`
<ha-card> <ha-card>
<div class="card-header"> <div class="card-header">
${this.localize( ${this.hass.localize("ui.panel.config.backup.details.summary.title")}
`ui.panel.${this.translationKeyPanel}.details.summary.title`
)}
</div> </div>
<div class="card-content"> <div class="card-content">
<ha-md-list class="summary"> <ha-md-list class="summary">
${this.translationKeyPanel === "config.backup"
? html`<ha-md-list-item>
<span slot="headline">
${this.localize("ui.panel.config.backup.backup_type")}
</span>
<span slot="supporting-text">
${this.localize(
`ui.panel.config.backup.type.${computeBackupType(this.backup, this.isHassio)}`
)}
</span>
</ha-md-list-item>`
: nothing}
<ha-md-list-item> <ha-md-list-item>
<span slot="headline"> <span slot="headline">
${this.localize( ${this.hass.localize("ui.panel.config.backup.backup_type")}
`ui.panel.${this.translationKeyPanel}.details.summary.size` </span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.type.${computeBackupType(this.backup, this.isHassio)}`
)}
</span>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.details.summary.size"
)} )}
</span> </span>
<span slot="supporting-text"> <span slot="supporting-text">
@ -80,31 +57,18 @@ class HaBackupDetailsSummary extends LitElement {
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<span slot="headline"> <span slot="headline">
${this.localize( ${this.hass.localize(
`ui.panel.${this.translationKeyPanel}.details.summary.created` "ui.panel.config.backup.details.summary.created"
)} )}
</span> </span>
<span slot="supporting-text"> ${formattedDate} </span> <span slot="supporting-text">${formattedDate}</span>
</ha-md-list-item> </ha-md-list-item>
</ha-md-list> </ha-md-list>
</div> </div>
${this.showUploadAnother
? html`<div class="card-actions">
<ha-button @click=${this._uploadAnother} destructive>
${this.localize(
`ui.panel.page-onboarding.restore.details.summary.upload_another`
)}
</ha-button>
</div>`
: nothing}
</ha-card> </ha-card>
`; `;
} }
private _uploadAnother() {
fireEvent(this, "show-backup-upload");
}
static styles = css` static styles = css`
:host { :host {
max-width: 690px; max-width: 690px;

View File

@ -26,7 +26,7 @@ class DialogCloudAlreadyConnected extends LitElement {
} }
public closeDialog() { public closeDialog() {
this._params?.closeDialog(); this._params?.closeDialog?.();
this._params = undefined; this._params = undefined;
this._obfuscateIp = true; this._obfuscateIp = true;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
@ -148,7 +148,7 @@ class DialogCloudAlreadyConnected extends LitElement {
} }
private _logInHere() { private _logInHere() {
this._params?.logInHereAction(); this._params?.logInHereAction?.();
this.closeDialog(); this.closeDialog();
} }

View File

@ -7,17 +7,31 @@ export interface CloudAlreadyConnectedParams {
name?: string; name?: string;
version?: string; version?: string;
}; };
logInHereAction: () => void; logInHereAction?: () => void;
closeDialog: () => void; closeDialog?: () => void;
} }
export const showCloudAlreadyConnectedDialog = ( export const showCloudAlreadyConnectedDialog = (
element: HTMLElement, element: HTMLElement,
webhookDialogParams: CloudAlreadyConnectedParams webhookDialogParams: CloudAlreadyConnectedParams
): void => { ) =>
fireEvent(element, "show-dialog", { new Promise((resolve) => {
dialogTag: "dialog-cloud-already-connected", const originalClose = webhookDialogParams.closeDialog;
dialogImport: () => import("./dialog-cloud-already-connected"), const originalLogInHereAction = webhookDialogParams.logInHereAction;
dialogParams: webhookDialogParams,
fireEvent(element, "show-dialog", {
dialogTag: "dialog-cloud-already-connected",
dialogImport: () => import("./dialog-cloud-already-connected"),
dialogParams: {
...webhookDialogParams,
closeDialog: () => {
originalClose?.();
resolve(false);
},
logInHereAction: () => {
originalLogInHereAction?.();
resolve(true);
},
},
});
}); });
};

View File

@ -0,0 +1,170 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import type { HaTextField } from "../../../../components/ha-textfield";
import "../../../../components/ha-textfield";
import { haStyle } from "../../../../resources/styles";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { cloudForgotPassword } from "../../../../data/cloud";
import { forgotPasswordHaCloud } from "../../../../data/onboarding";
import type { HomeAssistant } from "../../../../types";
@customElement("cloud-forgot-password-card")
export class CloudForgotPasswordCard extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: "translation-key-panel" }) public translationKeyPanel:
| "page-onboarding.restore.ha-cloud.forgot_password"
| "config.cloud.forgot_password" = "config.cloud.forgot_password";
@property() public email?: string;
@property({ type: Boolean, attribute: "card-less" }) public cardLess = false;
@state() private _inProgress = false;
@state() private _error?: string;
@query("#email", true) public emailField!: HaTextField;
protected render(): TemplateResult {
if (this.cardLess) {
return this._renderContent();
}
return html`
<ha-card
outlined
.header=${this.localize(
`ui.panel.${this.translationKeyPanel}.subtitle`
)}
>
${this._renderContent()}
</ha-card>
`;
}
private _renderContent() {
return html`
<div class="card-content">
<p>
${this.localize(`ui.panel.${this.translationKeyPanel}.instructions`)}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-textfield
autofocus
id="email"
label=${this.localize(`ui.panel.${this.translationKeyPanel}.email`)}
.value=${this.email ?? ""}
type="email"
required
.disabled=${this._inProgress}
@keydown=${this._keyDown}
.validationMessage=${this.localize(
`ui.panel.${this.translationKeyPanel}.email_error_msg`
)}
></ha-textfield>
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._handleEmailPasswordReset}
.progress=${this._inProgress}
>
${this.localize(
`ui.panel.${this.translationKeyPanel}.send_reset_email`
)}
</ha-progress-button>
</div>
`;
}
private _keyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._handleEmailPasswordReset();
}
}
private _resetPassword = async (email: string) => {
this._inProgress = true;
try {
if (this.hass) {
await cloudForgotPassword(this.hass, email);
} else {
// for onboarding
await forgotPasswordHaCloud(email);
}
fireEvent(this, "cloud-email-changed", { value: email });
this._inProgress = false;
fireEvent(this, "cloud-done", {
flashMessage: this.localize(
`ui.panel.${this.translationKeyPanel}.check_your_email`
),
});
} catch (err: any) {
this._inProgress = false;
const errCode = err && err.body && err.body.code;
if (errCode === "usernotfound" && email !== email.toLowerCase()) {
await this._resetPassword(email.toLowerCase());
} else {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
}
};
private async _handleEmailPasswordReset() {
const emailField = this.emailField;
const email = emailField.value;
if (!emailField.reportValidity()) {
emailField.focus();
return;
}
this._inProgress = true;
this._resetPassword(email);
}
static get styles() {
return [
haStyle,
css`
ha-card {
max-width: 600px;
margin: 0 auto;
margin-top: 24px;
}
h1 {
margin: 0;
}
ha-textfield {
width: 100%;
}
.card-actions {
display: flex;
justify-content: flex-end;
align-items: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"cloud-forgot-password-card": CloudForgotPasswordCard;
}
}

View File

@ -1,13 +1,7 @@
import type { TemplateResult } from "lit"; import type { 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, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import "./cloud-forgot-password-card";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import type { HaTextField } from "../../../../components/ha-textfield";
import "../../../../components/ha-textfield";
import { cloudForgotPassword } from "../../../../data/cloud";
import "../../../../layouts/hass-subpage"; import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
@ -22,10 +16,6 @@ export class CloudForgotPassword extends LitElement {
@state() public _requestInProgress = false; @state() public _requestInProgress = false;
@state() private _error?: string;
@query("#email", true) private _emailField!: HaTextField;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<hass-subpage <hass-subpage
@ -36,98 +26,16 @@ export class CloudForgotPassword extends LitElement {
)} )}
> >
<div class="content"> <div class="content">
<ha-card <cloud-forgot-password-card
outlined .hass=${this.hass}
.header=${this.hass.localize( .localize=${this.hass.localize}
"ui.panel.config.cloud.forgot_password.subtitle" .email=${this.email}
)} ></cloud-forgot-password-card>
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.cloud.forgot_password.instructions"
)}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-textfield
autofocus
id="email"
label=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.email"
)}
.value=${this.email}
type="email"
required
@keydown=${this._keyDown}
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.email_error_msg"
)}
></ha-textfield>
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._handleEmailPasswordReset}
.progress=${this._requestInProgress}
>
${this.hass.localize(
"ui.panel.config.cloud.forgot_password.send_reset_email"
)}
</ha-progress-button>
</div>
</ha-card>
</div> </div>
</hass-subpage> </hass-subpage>
`; `;
} }
private _keyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._handleEmailPasswordReset();
}
}
private async _handleEmailPasswordReset() {
const emailField = this._emailField;
const email = emailField.value;
if (!emailField.reportValidity()) {
emailField.focus();
return;
}
this._requestInProgress = true;
const doResetPassword = async (username: string) => {
try {
await cloudForgotPassword(this.hass, username);
// @ts-ignore
fireEvent(this, "email-changed", { value: username });
this._requestInProgress = false;
// @ts-ignore
fireEvent(this, "cloud-done", {
flashMessage: this.hass.localize(
"ui.panel.config.cloud.forgot_password.check_your_email"
),
});
} catch (err: any) {
this._requestInProgress = false;
const errCode = err && err.body && err.body.code;
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doResetPassword(username.toLowerCase());
} else {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
}
};
await doResetPassword(email);
}
static get styles() { static get styles() {
return [ return [
haStyle, haStyle,
@ -135,25 +43,6 @@ export class CloudForgotPassword extends LitElement {
.content { .content {
padding-bottom: 24px; padding-bottom: 24px;
} }
ha-card {
max-width: 600px;
margin: 0 auto;
margin-top: 24px;
}
h1 {
margin: 0;
}
ha-textfield {
width: 100%;
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-actions a {
color: var(--primary-text-color);
}
`, `,
]; ];
} }

View File

@ -5,7 +5,7 @@ import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page"; import { HassRouterPage } from "../../../layouts/hass-router-page";
import type { ValueChangedEvent, HomeAssistant, Route } from "../../../types"; import type { ValueChangedEvent, HomeAssistant, Route } from "../../../types";
import "./account/cloud-account"; import "./account/cloud-account";
import "./login/cloud-login"; import "./login/cloud-login-panel";
const LOGGED_IN_URLS = ["account", "google-assistant", "alexa"]; const LOGGED_IN_URLS = ["account", "google-assistant", "alexa"];
const NOT_LOGGED_IN_URLS = ["login", "register", "forgot-password"]; const NOT_LOGGED_IN_URLS = ["login", "register", "forgot-password"];
@ -39,7 +39,7 @@ class HaConfigCloud extends HassRouterPage {
}, },
routes: { routes: {
login: { login: {
tag: "cloud-login", tag: "cloud-login-panel",
}, },
register: { register: {
tag: "cloud-register", tag: "cloud-register",
@ -90,7 +90,7 @@ class HaConfigCloud extends HassRouterPage {
protected createElement(tag: string) { protected createElement(tag: string) {
const el = super.createElement(tag); const el = super.createElement(tag);
el.addEventListener("email-changed", (ev) => { el.addEventListener("cloud-email-changed", (ev) => {
this._loginEmail = (ev as ValueChangedEvent<string>).detail.value; this._loginEmail = (ev as ValueChangedEvent<string>).detail.value;
}); });
el.addEventListener("flash-message-changed", (ev) => { el.addEventListener("flash-message-changed", (ev) => {

View File

@ -0,0 +1,246 @@
import "@material/mwc-list/mwc-list";
import { mdiDeleteForever, mdiDotsVertical, mdiDownload } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { navigate } from "../../../../common/navigate";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-list-item";
import "../../../../components/ha-button-menu";
import { removeCloudData } from "../../../../data/cloud";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import "./cloud-login";
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
import type { CloudLogin } from "./cloud-login";
@customElement("cloud-login-panel")
export class CloudLoginPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property() public email?: string;
@property({ attribute: false }) public flashMessage?: string;
@query("cloud-login") private _cloudLoginElement!: CloudLogin;
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
header="Home Assistant Cloud"
>
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.cloud.account.reset_cloud_data"
)}
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.cloud.account.download_support_package"
)}
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<div class="content">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span>
<div slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.cloud.login.introduction"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.cloud.login.introduction2"
)}
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
>
Nabu&nbsp;Casa,&nbsp;Inc</a
>${this.hass.localize(
"ui.panel.config.cloud.login.introduction2a"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.cloud.login.introduction3"
)}
</p>
<p>
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.cloud.login.learn_more_link"
)}
</a>
</p>
</div>
${this.flashMessage
? html`<ha-alert
dismissable
@alert-dismissed-clicked=${this._dismissFlash}
>
${this.flashMessage}
</ha-alert>`
: ""}
<cloud-login
.hass=${this.hass}
.email=${this.email}
.localize=${this.hass.localize}
@cloud-forgot-password=${this._handleForgotPassword}
check-connection
></cloud-login>
<ha-card outlined>
<mwc-list>
<ha-list-item @click=${this._handleRegister} twoline hasMeta>
${this.hass.localize(
"ui.panel.config.cloud.login.start_trial"
)}
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.cloud.login.trial_info"
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</mwc-list>
</ha-card>
</ha-config-section>
</div>
</hass-subpage>
`;
}
private _handleForgotPassword() {
this._dismissFlash();
fireEvent(this, "cloud-email-changed", {
value: this._cloudLoginElement.emailField.value,
});
navigate("/config/cloud/forgot-password");
}
private _handleRegister() {
this._dismissFlash();
fireEvent(this, "cloud-email-changed", {
value: this._cloudLoginElement.emailField.value,
});
navigate("/config/cloud/register");
}
private _dismissFlash() {
fireEvent(this, "flash-message-changed", { value: "" });
}
private _handleMenuAction(ev) {
switch (ev.detail.index) {
case 0:
this._deleteCloudData();
break;
case 1:
this._downloadSupportPackage();
}
}
private async _deleteCloudData() {
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.account.reset_data_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.cloud.account.reset_data_confirm_text"
),
confirmText: this.hass.localize("ui.panel.config.cloud.account.reset"),
destructive: true,
});
if (!confirm) {
return;
}
try {
await removeCloudData(this.hass);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.account.reset_data_failed"
),
text: err?.message,
});
return;
} finally {
fireEvent(this, "ha-refresh-cloud-status");
}
}
private async _downloadSupportPackage() {
showSupportPackageDialog(this);
}
static get styles() {
return [
haStyle,
css`
.content {
padding-bottom: 24px;
}
[slot="introduction"] {
margin: -1em 0;
}
[slot="introduction"] a {
color: var(--primary-color);
}
ha-card {
overflow: hidden;
}
ha-card .card-header {
margin-bottom: -8px;
}
h1 {
margin: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"cloud-login-panel": CloudLoginPanel;
}
interface HASSDomEvents {
"cloud-email-changed": { value: string };
"flash-message-changed": { value: string };
}
}

View File

@ -1,217 +1,126 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list";
import { mdiDeleteForever, mdiDotsVertical, mdiDownload } from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { navigate } from "../../../../common/navigate";
import "../../../../components/buttons/ha-progress-button"; import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-icon-next"; import "../../../../components/ha-button";
import "../../../../components/ha-list-item";
import "../../../../components/ha-password-field"; import "../../../../components/ha-password-field";
import "../../../../components/ha-button-menu";
import type { HaPasswordField } from "../../../../components/ha-password-field"; import type { HaPasswordField } from "../../../../components/ha-password-field";
import "../../../../components/ha-textfield"; import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield"; import type { HaTextField } from "../../../../components/ha-textfield";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline"; import { haStyle } from "../../../../resources/styles";
import { cloudLogin, removeCloudData } from "../../../../data/cloud"; import type { LocalizeFunc } from "../../../../common/translations/localize";
import { cloudLogin } from "../../../../data/cloud";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../lovelace/custom-card-helpers";
import "../../../../layouts/hass-subpage"; import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected"; import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected";
import type { HomeAssistant } from "../../../../types";
import { loginHaCloud } from "../../../../data/onboarding";
@customElement("cloud-login") @customElement("cloud-login")
export class CloudLogin extends LitElement { export class CloudLogin extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false; @property({ type: Boolean, attribute: "check-connection" })
public checkConnection = false;
@property({ type: Boolean }) public narrow = false;
@property() public email?: string; @property() public email?: string;
@property({ attribute: false }) public flashMessage?: string; @property({ attribute: false }) public localize!: LocalizeFunc;
@state() private _password?: string; @property({ attribute: "translation-key-panel" }) public translationKeyPanel:
| "page-onboarding.restore.ha-cloud"
| "config.cloud" = "config.cloud";
@state() private _requestInProgress = false; @property({ type: Boolean, attribute: "card-less" }) public cardLess = false;
@state() private _error?: string; @query("#email", true) public emailField!: HaTextField;
@state() private _checkConnection = true;
@query("#email", true) private _emailField!: HaTextField;
@query("#password", true) private _passwordField!: HaPasswordField; @query("#password", true) private _passwordField!: HaPasswordField;
@state() private _error?: string;
@state() private _inProgress = false;
protected render(): TemplateResult { protected render(): TemplateResult {
if (this.cardLess) {
return this._renderLoginForm();
}
return html` return html`
<hass-subpage <ha-card
.hass=${this.hass} outlined
.narrow=${this.narrow} .header=${this.localize(
header="Home Assistant Cloud" `ui.panel.${this.translationKeyPanel}.login.sign_in`
)}
> >
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}> ${this._renderLoginForm()}
<ha-icon-button </ha-card>
slot="trigger" `;
.label=${this.hass.localize("ui.common.menu")} }
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon"> private _renderLoginForm() {
${this.hass.localize( return html`
"ui.panel.config.cloud.account.reset_cloud_data" <div class="card-content login-form">
)} ${this._error
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon> ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
</ha-list-item> : nothing}
<ha-list-item graphic="icon"> <ha-textfield
${this.hass.localize( .label=${this.localize(
"ui.panel.config.cloud.account.download_support_package" `ui.panel.${this.translationKeyPanel}.login.email`
)} )}
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon> id="email"
</ha-list-item> name="username"
</ha-button-menu> type="email"
<div class="content"> autocomplete="username"
<ha-config-section .isWide=${this.isWide}> required
<span slot="header">Home Assistant Cloud</span> .value=${this.email ?? ""}
<div slot="introduction"> @keydown=${this._keyDown}
<p> .disabled=${this._inProgress}
${this.hass.localize( .validationMessage=${this.localize(
"ui.panel.config.cloud.login.introduction" `ui.panel.${this.translationKeyPanel}.login.email_error_msg`
)} )}
</p> ></ha-textfield>
<p> <ha-password-field
${this.hass.localize( id="password"
"ui.panel.config.cloud.login.introduction2" name="password"
)} .label=${this.localize(
<a `ui.panel.${this.translationKeyPanel}.login.password`
href="https://www.nabucasa.com" )}
target="_blank" autocomplete="current-password"
rel="noreferrer" required
> minlength="8"
Nabu&nbsp;Casa,&nbsp;Inc</a @keydown=${this._keyDown}
>${this.hass.localize( .disabled=${this._inProgress}
"ui.panel.config.cloud.login.introduction2a" .validationMessage=${this.localize(
)} `ui.panel.${this.translationKeyPanel}.login.password_error_msg`
</p> )}
<p> ></ha-password-field>
${this.hass.localize( </div>
"ui.panel.config.cloud.login.introduction3" <div class="card-actions">
)} <ha-button
</p> .disabled=${this._inProgress}
<p> @click=${this._handleForgotPassword}
<a >
href="https://www.nabucasa.com" ${this.localize(
target="_blank" `ui.panel.${this.translationKeyPanel}.login.forgot_password`
rel="noreferrer" )}
> </ha-button>
${this.hass.localize( <ha-progress-button
"ui.panel.config.cloud.login.learn_more_link" unelevated
)} @click=${this._handleLogin}
</a> .progress=${this._inProgress}
</p> >${this.localize(
</div> `ui.panel.${this.translationKeyPanel}.login.sign_in`
)}</ha-progress-button
${this.flashMessage >
? html`<ha-alert </div>
dismissable
@alert-dismissed-clicked=${this._dismissFlash}
>
${this.flashMessage}
</ha-alert>`
: ""}
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.cloud.login.sign_in"
)}
>
<div class="card-content login-form">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.cloud.login.email"
)}
id="email"
name="username"
type="email"
autocomplete="username"
required
.value=${this.email}
@keydown=${this._keyDown}
.disabled=${this._requestInProgress}
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.login.email_error_msg"
)}
></ha-textfield>
<ha-password-field
id="password"
name="password"
.label=${this.hass.localize(
"ui.panel.config.cloud.login.password"
)}
.value=${this._password || ""}
autocomplete="current-password"
required
minlength="8"
@keydown=${this._keyDown}
.disabled=${this._requestInProgress}
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.login.password_error_msg"
)}
></ha-password-field>
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._handleLogin}
.progress=${this._requestInProgress}
>${this.hass.localize(
"ui.panel.config.cloud.login.sign_in"
)}</ha-progress-button
>
<button
class="link pwd-forgot-link"
.disabled=${this._requestInProgress}
@click=${this._handleForgotPassword}
>
${this.hass.localize(
"ui.panel.config.cloud.login.forgot_password"
)}
</button>
</div>
</ha-card>
<ha-card outlined>
<mwc-list>
<ha-list-item @click=${this._handleRegister} twoline hasMeta>
${this.hass.localize(
"ui.panel.config.cloud.login.start_trial"
)}
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.cloud.login.trial_info"
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</mwc-list>
</ha-card>
</ha-config-section>
</div>
</hass-subpage>
`; `;
} }
@ -221,36 +130,100 @@ export class CloudLogin extends LitElement {
} }
} }
private async _handleLogin() { private _handleCloudLoginError = async (
const emailField = this._emailField; err: any,
const passwordField = this._passwordField; email: string,
password: string,
checkConnection: boolean
): Promise<"cancel" | "password-change" | string | undefined> => {
const errCode = err && err.body && err.body.code;
if (errCode === "mfarequired") {
const totpCode = await showPromptDialog(this, {
title: this.localize(
`ui.panel.${this.translationKeyPanel}.login.totp_code_prompt_title`
),
inputLabel: this.localize(
`ui.panel.${this.translationKeyPanel}.login.totp_code`
),
inputType: "text",
defaultValue: "",
confirmText: this.localize(
`ui.panel.${this.translationKeyPanel}.login.submit`
),
dismissText: this.localize(
`ui.panel.${this.translationKeyPanel}.login.cancel`
),
});
if (totpCode !== null && totpCode !== "") {
this._login(email, password, checkConnection, totpCode);
return undefined;
}
}
if (errCode === "alreadyconnectederror") {
const logInHere = await showCloudAlreadyConnectedDialog(this, {
details: JSON.parse(err.body.message),
});
if (logInHere) {
this._login(email, password, false);
}
const email = emailField.value; return logInHere ? undefined : "cancel";
const password = passwordField.value; }
if (errCode === "PasswordChangeRequired") {
if (!emailField.reportValidity()) { showAlertDialog(this, {
passwordField.reportValidity(); title: this.localize(
emailField.focus(); `ui.panel.${this.translationKeyPanel}.login.alert_password_change_required`
return; ),
});
return "password-change";
}
if (errCode === "usernotfound" && email !== email.toLowerCase()) {
this._login(email.toLowerCase(), password, checkConnection);
return undefined;
} }
if (!passwordField.reportValidity()) { switch (errCode) {
passwordField.focus(); case "UserNotConfirmed":
return; return this.localize(
`ui.panel.${this.translationKeyPanel}.login.alert_email_confirm_necessary`
);
case "mfarequired":
return this.localize(
`ui.panel.${this.translationKeyPanel}.login.alert_mfa_code_required`
);
case "mfaexpiredornotstarted":
return this.localize(
`ui.panel.${this.translationKeyPanel}.login.alert_mfa_expired_or_not_started`
);
case "invalidtotpcode":
return this.localize(
`ui.panel.${this.translationKeyPanel}.login.alert_totp_code_invalid`
);
default:
return err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
};
private _login = async (
email: string,
password: string,
checkConnection: boolean,
code?: string
): Promise<undefined> => {
if (!password && !code) {
throw new Error("Password or code required");
} }
this._requestInProgress = true; try {
if (this.hass) {
const doLogin = async (username: string, code?: string) => {
try {
const result = await cloudLogin({ const result = await cloudLogin({
hass: this.hass, hass: this.hass,
email: username, email,
...(code ? { code } : { password }), ...(code ? { code } : { password }),
check_connection: this._checkConnection, check_connection: checkConnection,
}); });
this.email = "";
this._password = "";
if (result.cloud_pipeline) { if (result.cloud_pipeline) {
if ( if (
await showConfirmationDialog(this, { await showConfirmationDialog(this, {
@ -265,180 +238,75 @@ export class CloudLogin extends LitElement {
setAssistPipelinePreferred(this.hass, result.cloud_pipeline); setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
} }
} }
fireEvent(this, "ha-refresh-cloud-status"); } else {
} catch (err: any) { // for onboarding
const errCode = err && err.body && err.body.code; await loginHaCloud({
if (errCode === "mfarequired") { email,
const totpCode = await showPromptDialog(this, { ...(code ? { code } : { password: password! }),
title: this.hass.localize( });
"ui.panel.config.cloud.login.totp_code_prompt_title"
),
inputLabel: this.hass.localize(
"ui.panel.config.cloud.login.totp_code"
),
inputType: "text",
defaultValue: "",
confirmText: this.hass.localize(
"ui.panel.config.cloud.login.submit"
),
});
if (totpCode !== null && totpCode !== "") {
await doLogin(username, totpCode);
return;
}
}
if (errCode === "alreadyconnectederror") {
showCloudAlreadyConnectedDialog(this, {
details: JSON.parse(err.body.message),
logInHereAction: () => {
this._checkConnection = false;
doLogin(username);
},
closeDialog: () => {
this._requestInProgress = false;
this.email = "";
this._password = "";
},
});
return;
}
if (errCode === "PasswordChangeRequired") {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.login.alert_password_change_required"
),
});
navigate("/config/cloud/forgot-password");
return;
}
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doLogin(username.toLowerCase());
return;
}
this._password = "";
this._requestInProgress = false;
switch (errCode) {
case "UserNotConfirmed":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
);
break;
case "mfarequired":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_mfa_code_required"
);
break;
case "mfaexpiredornotstarted":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_mfa_expired_or_not_started"
);
break;
case "invalidtotpcode":
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_totp_code_invalid"
);
break;
default:
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
break;
}
emailField.focus();
} }
}; this.email = "";
fireEvent(this, "ha-refresh-cloud-status");
} catch (err: any) {
const error = await this._handleCloudLoginError(
err,
email,
password,
checkConnection
);
await doLogin(email); if (error === "cancel") {
} this._inProgress = false;
this.email = "";
this._passwordField.value = "";
return;
}
if (error === "password-change") {
this._handleForgotPassword();
return;
}
private _handleRegister() { this._inProgress = false;
this._dismissFlash(); this._error = error;
// @ts-ignore }
fireEvent(this, "email-changed", { value: this._emailField.value }); };
navigate("/config/cloud/register");
private async _handleLogin() {
if (!this._inProgress) {
if (!this.emailField.reportValidity()) {
this.emailField.focus();
return;
}
if (!this._passwordField.reportValidity()) {
this._passwordField.focus();
return;
}
this._inProgress = true;
this._login(
this.emailField.value,
this._passwordField.value,
this.checkConnection
);
}
} }
private _handleForgotPassword() { private _handleForgotPassword() {
this._dismissFlash(); fireEvent(this, "cloud-forgot-password");
// @ts-ignore
fireEvent(this, "email-changed", { value: this._emailField.value });
navigate("/config/cloud/forgot-password");
}
private _dismissFlash() {
// @ts-ignore
fireEvent(this, "flash-message-changed", { value: "" });
}
private _handleMenuAction(ev) {
switch (ev.detail.index) {
case 0:
this._deleteCloudData();
break;
case 1:
this._downloadSupportPackage();
}
}
private async _deleteCloudData() {
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.account.reset_data_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.cloud.account.reset_data_confirm_text"
),
confirmText: this.hass.localize("ui.panel.config.cloud.account.reset"),
destructive: true,
});
if (!confirm) {
return;
}
try {
await removeCloudData(this.hass);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.account.reset_data_failed"
),
text: err?.message,
});
return;
} finally {
fireEvent(this, "ha-refresh-cloud-status");
}
}
private async _downloadSupportPackage() {
showSupportPackageDialog(this);
} }
static get styles() { static get styles() {
return [ return [
haStyle, haStyle,
css` css`
.content {
padding-bottom: 24px;
}
[slot="introduction"] {
margin: -1em 0;
}
[slot="introduction"] a {
color: var(--primary-color);
}
ha-card { ha-card {
overflow: hidden; overflow: hidden;
} }
ha-card .card-header { ha-card .card-header {
margin-bottom: -8px; margin-bottom: -8px;
} }
h1 {
margin: 0;
}
.card-actions { .card-actions {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -457,4 +325,14 @@ declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"cloud-login": CloudLogin; "cloud-login": CloudLogin;
} }
interface HASSDomEvents {
"cloud-login": {
email: string;
password: string;
};
"cloud-forgot-password": {
email: string;
};
}
} }

View File

@ -141,7 +141,7 @@ export class CloudRegister extends LitElement {
type="email" type="email"
autocomplete="email" autocomplete="email"
required required
.value=${this.email} .value=${this.email ?? ""}
@keydown=${this._keyDown} @keydown=${this._keyDown}
validationMessage=${this.hass.localize( validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.email_error_msg" "ui.panel.config.cloud.register.email_error_msg"
@ -260,9 +260,7 @@ export class CloudRegister extends LitElement {
private _verificationEmailSent(email: string) { private _verificationEmailSent(email: string) {
this._requestInProgress = false; this._requestInProgress = false;
this._password = ""; this._password = "";
// @ts-ignore fireEvent(this, "cloud-email-changed", { value: email });
fireEvent(this, "email-changed", { value: email });
// @ts-ignore
fireEvent(this, "cloud-done", { fireEvent(this, "cloud-done", {
flashMessage: this.hass.localize( flashMessage: this.hass.localize(
"ui.panel.config.cloud.register.account_created" "ui.panel.config.cloud.register.account_created"
@ -304,4 +302,8 @@ declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"cloud-register": CloudRegister; "cloud-register": CloudRegister;
} }
interface HASSDomEvents {
"cloud-done": { flashMessage: string };
}
} }

View File

@ -4559,6 +4559,7 @@
"password_error_msg": "Passwords are at least 8 characters", "password_error_msg": "Passwords are at least 8 characters",
"totp_code_prompt_title": "Two-factor authentication", "totp_code_prompt_title": "Two-factor authentication",
"totp_code": "TOTP code", "totp_code": "TOTP code",
"cancel": "Cancel",
"submit": "Submit", "submit": "Submit",
"forgot_password": "Forgot password?", "forgot_password": "Forgot password?",
"start_trial": "Start your free 1 month trial", "start_trial": "Start your free 1 month trial",
@ -8219,7 +8220,7 @@
"welcome": { "welcome": {
"header": "Welcome!", "header": "Welcome!",
"start": "Create my smart home", "start": "Create my smart home",
"restore_backup": "Restore from backup", "or_restore": "Or restore",
"vision": "Read our vision", "vision": "Read our vision",
"community": "Join our community", "community": "Join our community",
"download_app": "Download our app", "download_app": "Download our app",
@ -8252,7 +8253,7 @@
}, },
"core-config": { "core-config": {
"location_header": "Home location", "location_header": "Home location",
"intro_location": "Let's set up the location of your home so that you can display information such as the local weather and use sun-based or presence-based automations.", "intro_location": "This data is stored on your Home Assistant system, though some cloud-based integrations may use this data to function.",
"location_address": "Powered by {openstreetmap} ({osm_privacy_policy}).", "location_address": "Powered by {openstreetmap} ({osm_privacy_policy}).",
"osm_privacy_policy": "Privacy policy", "osm_privacy_policy": "Privacy policy",
"title_location_detect": "Do you want us to detect your location?", "title_location_detect": "Do you want us to detect your location?",
@ -8300,6 +8301,7 @@
"restore": { "restore": {
"header": "Restore a backup", "header": "Restore a backup",
"upload_backup": "[%key:ui::panel::config::backup::dialogs::upload::title%]", "upload_backup": "[%key:ui::panel::config::backup::dialogs::upload::title%]",
"upload_backup_subtitle": "Upload a backup file from your device",
"unsupported": { "unsupported": {
"title": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::title%]", "title": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::title%]",
"text": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::text%]" "text": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::text%]"
@ -8312,19 +8314,18 @@
"uploading": "[%key:ui::components::file-upload::uploading%]", "uploading": "[%key:ui::components::file-upload::uploading%]",
"details": { "details": {
"home_assistant_missing": "This backup does not include your Home Assistant configuration, you cannot use it to restore your instance.", "home_assistant_missing": "This backup does not include your Home Assistant configuration, you cannot use it to restore your instance.",
"addons_unsupported": "This backup includes add-ons and folders, which are not supported in this installation method of Home Assistant. You can still restore Home Assistant, but the unsupported files will not be restored.", "addons_unsupported": "Your installation method doesnt support add-ons. If you want to restore these, you have to install Home Assistant Operating System",
"summary": { "summary": {
"title": "[%key:ui::panel::config::backup::details::summary::title%]",
"size": "[%key:ui::panel::config::backup::details::summary::size%]",
"created": "[%key:ui::panel::config::backup::details::summary::created%]", "created": "[%key:ui::panel::config::backup::details::summary::created%]",
"upload_another": "Upload another", "content": "Content"
"home": "Home"
}, },
"restore": { "restore": {
"title": "[%key:ui::panel::config::backup::details::restore::title%]", "title": "[%key:ui::panel::config::backup::dialogs::restore::title%]",
"action": "[%key:ui::panel::config::backup::details::restore::action%]", "action": "Restore backup",
"encryption": { "encryption": {
"title": "[%key:ui::panel::config::backup::dialogs::restore::encryption::different_key%]", "label": "Encryption",
"description": "[%key:ui::panel::config::backup::dialogs::restore::encryption::different_key%]",
"description_cloud": "Home Assistant Cloud is the privacy-focused cloud. We dont store your encryption key. Enter the encryption key you saved or use the emergency kit you have downloaded.",
"incorrect_key": "[%key:ui::panel::config::backup::dialogs::restore::encryption::incorrect_key%]", "incorrect_key": "[%key:ui::panel::config::backup::dialogs::restore::encryption::incorrect_key%]",
"input_label": "[%key:ui::panel::config::backup::dialogs::restore::encryption::input_label%]" "input_label": "[%key:ui::panel::config::backup::dialogs::restore::encryption::input_label%]"
} }
@ -8362,7 +8363,7 @@
"confirm_restore_partial_backup_title": "[%key:supervisor::backup::confirm_restore_partial_backup_title%]", "confirm_restore_partial_backup_title": "[%key:supervisor::backup::confirm_restore_partial_backup_title%]",
"confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again and you see the login screen. If it fails it will bring you back to the onboarding.", "confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again and you see the login screen. If it fails it will bring you back to the onboarding.",
"confirm_restore_full_backup_title": "[%key:supervisor::backup::confirm_restore_full_backup_title%]", "confirm_restore_full_backup_title": "[%key:supervisor::backup::confirm_restore_full_backup_title%]",
"confirm_restore_full_backup_text": "Your entire system will be wiped and the backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again and you see the login screen. If it fails it will bring you back to the onboarding.", "confirm_restore_full_backup_text": "Depending on the size of the backup, this can take up to 45 minutes. Home Assistant will restart and you will see the login screen when its restored.",
"restore": "[%key:supervisor::backup::restore%]", "restore": "[%key:supervisor::backup::restore%]",
"close": "[%key:ui::common::close%]", "close": "[%key:ui::common::close%]",
"cancel": "[%key:ui::common::cancel%]", "cancel": "[%key:ui::common::cancel%]",
@ -8376,6 +8377,50 @@
"text": "Are you sure you want to cancel the restore process and return to the onboarding?", "text": "Are you sure you want to cancel the restore process and return to the onboarding?",
"yes": "[%key:ui::common::yes%]", "yes": "[%key:ui::common::yes%]",
"no": "[%key:ui::common::no%]" "no": "[%key:ui::common::no%]"
},
"ha-cloud": {
"description": "Restore from your Home Assistant Cloud backup.",
"no_cloud_backup": "No backup available",
"no_cloud_backup_description": "This Home Assistant Cloud account doesnt have a backup stored. You can learn more how Cloud backups work at the Nabu Casa website.",
"sign_out": "[%key:ui::panel::config::cloud::account::sign_out%]",
"sign_out_progress": "Home Assistant cloud signing out...",
"sign_out_success": "Home Assistant cloud signed out",
"sign_out_error": "Failed to sign out from Home Assistant cloud",
"learn_more": "Learn more",
"sign_in_description": "Sign in to your Nabu Casa account and restore your Home Assistant Cloud backup.",
"login": {
"title": "[%key:ui::panel::config::cloud::login::title%]",
"sign_in": "[%key:ui::panel::config::cloud::login::sign_in%]",
"email": "[%key:ui::panel::config::cloud::login::email%]",
"email_error_msg": "[%key:ui::panel::config::cloud::login::email_error_msg%]",
"password": "[%key:ui::panel::config::cloud::login::password%]",
"password_error_msg": "[%key:ui::panel::config::cloud::login::password_error_msg%]",
"totp_code_prompt_title": "[%key:ui::panel::config::cloud::login::totp_code_prompt_title%]",
"totp_code": "[%key:ui::panel::config::cloud::login::totp_code%]",
"cancel": "[%key:ui::panel::config::cloud::login::cancel%]",
"submit": "[%key:ui::panel::config::cloud::login::submit%]",
"forgot_password": "[%key:ui::panel::config::cloud::login::forgot_password%]",
"start_trial": "[%key:ui::panel::config::cloud::login::start_trial%]",
"trial_info": "[%key:ui::panel::config::cloud::login::trial_info%]",
"alert_password_change_required": "[%key:ui::panel::config::cloud::login::alert_password_change_required%]",
"alert_email_confirm_necessary": "[%key:ui::panel::config::cloud::login::alert_email_confirm_necessary%]",
"alert_mfa_code_required": "[%key:ui::panel::config::cloud::login::alert_mfa_code_required%]",
"alert_mfa_expired_or_not_started": "[%key:ui::panel::config::cloud::login::alert_mfa_expired_or_not_started%]",
"alert_totp_code_invalid": "[%key:ui::panel::config::cloud::login::alert_totp_code_invalid%]"
},
"forgot_password": {
"title": "[%key:ui::panel::config::cloud::forgot_password::title%]",
"subtitle": "[%key:ui::panel::config::cloud::forgot_password::subtitle%]",
"instructions": "[%key:ui::panel::config::cloud::forgot_password::instructions%]",
"email": "[%key:ui::panel::config::cloud::forgot_password::email%]",
"email_error_msg": "[%key:ui::panel::config::cloud::forgot_password::email_error_msg%]",
"send_reset_email": "[%key:ui::panel::config::cloud::forgot_password::send_reset_email%]",
"check_your_email": "[%key:ui::panel::config::cloud::forgot_password::check_your_email%]"
}
},
"options": {
"title": "How to restore?",
"upload_description": "Upload and restore a Home Assistant backup to this system"
} }
} }
}, },