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 unelevated = false;
@state() private _result?: "success" | "error";
public render(): TemplateResult {
@ -21,6 +23,7 @@ export class HaProgressButton extends LitElement {
return html`
<mwc-button
?raised=${this.raised}
.unelevated=${this.unelevated}
.disabled=${this.disabled || this.progress}
class=${this._result || ""}
>
@ -78,6 +81,7 @@ export class HaProgressButton extends LitElement {
pointer-events: none;
}
mwc-button[unelevated].success,
mwc-button[raised].success {
--mdc-theme-primary: var(--success-color);
--mdc-theme-on-primary: white;
@ -91,6 +95,7 @@ export class HaProgressButton extends LitElement {
pointer-events: none;
}
mwc-button[unelevated].error,
mwc-button[raised].error {
--mdc-theme-primary: var(--error-color);
--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 { handleFetchPromise } from "../util/hass-call-api";
import type { CloudStatus } from "./cloud";
export interface InstallationType {
installation_type:
@ -36,6 +37,18 @@ export interface OnboardingStep {
done: boolean;
}
interface CloudLoginBase {
email: string;
}
export interface CloudLoginPassword extends CloudLoginBase {
password: string;
}
export interface CloudLoginMFA extends CloudLoginBase {
code: string;
}
export const fetchOnboardingOverview = () =>
fetch(`${__HASS_URL__}/api/onboarding`, { credentials: "same-origin" });
@ -91,3 +104,27 @@ export const fetchInstallationType = async (): Promise<InstallationType> => {
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 {
inputLabel?: string;
dismissText?: string;
inputType?: string;
defaultValue?: string;
placeholder?: string;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,9 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
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-status";
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 { removeSearchParam } from "../common/url/search-params";
import { navigate } from "../common/navigate";
@ -19,9 +13,11 @@ import {
type BackupOnboardingConfig,
type BackupOnboardingInfo,
} from "../data/backup_onboarding";
import type { BackupContentExtended, BackupData } from "../data/backup";
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
import { CLOUD_AGENT, type BackupContentExtended } from "../data/backup";
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;
@ -29,27 +25,28 @@ const STATUS_INTERVAL_IN_MS = 5000;
class OnboardingRestoreBackup extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public language!: string;
@property({ type: Boolean }) public supervisor = false;
@property() public mode!: "upload" | "cloud";
@state() private _view:
| "loading"
| "upload"
| "select_data"
| "confirm_restore"
| "cloud_login"
| "empty_cloud"
| "restore"
| "status" = "loading";
@state() private _backup?: BackupContentExtended;
@state() private _backupInfo?: BackupOnboardingInfo;
@state() private _selectedData?: BackupData;
@state() private _error?: string;
@state() private _failed?: boolean;
@state() private _cloudStatus?: CloudStatus;
@storage({
key: "onboarding-restore-backup-backup-id",
})
@ -62,37 +59,8 @@ class OnboardingRestoreBackup extends LitElement {
protected render(): TemplateResult {
return html`
${
this._view !== "status" || this._failed
? html`<ha-icon-button-arrow-prev
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
@click=${this._back}
></ha-icon-button-arrow-prev>`
: nothing
}
</ha-icon-button>
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
${
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 === "loading"
? html`<onboarding-loading></onboarding-loading>`
: this._view === "upload"
? html`
<onboarding-restore-backup-upload
@ -101,51 +69,56 @@ class OnboardingRestoreBackup extends LitElement {
@backup-uploaded=${this._backupUploaded}
></onboarding-restore-backup-upload>
`
: this._view === "select_data"
? html`<onboarding-restore-backup-details
: this._view === "cloud_login"
? html`
<onboarding-restore-backup-cloud-login
.localize=${this.localize}
.backup=${this._backup!}
@backup-restore=${this._restore}
></onboarding-restore-backup-details>`
: this._view === "confirm_restore"
@ha-refresh-cloud-status=${this._showCloudBackup}
></onboarding-restore-backup-cloud-login>
`
: this._view === "empty_cloud"
? html`
<onboarding-restore-backup-no-cloud-backup
.localize=${this.localize}
@sign-out=${this._signOut}
></onboarding-restore-backup-no-cloud-backup>
`
: this._view === "restore"
? html`<onboarding-restore-backup-restore
.mode=${this.mode}
.localize=${this.localize}
.backup=${this._backup!}
.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-backup-back=${this._back}
@sign-out=${this._signOut}
></onboarding-restore-backup-restore>`
: nothing
}
${
this._view === "status" && this._backupInfo
: nothing}
${this._view === "status" && this._backupInfo
? html`<onboarding-restore-backup-status
.localize=${this.localize}
.backupInfo=${this._backupInfo}
@show-backup-upload=${this._reupload}
@restore-backup-back=${this._back}
></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
}
: nothing}
`;
}
protected 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();
}
@ -194,7 +167,25 @@ class OnboardingRestoreBackup extends LitElement {
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(
({ backup_id }) => backup_id === this._backupId
);
@ -219,31 +210,24 @@ class OnboardingRestoreBackup extends LitElement {
return;
}
if (
this._backup &&
// 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";
}
if (this._backup) {
this._view = "restore";
return;
}
// show upload as default
// show default view
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() {
@ -264,45 +248,48 @@ class OnboardingRestoreBackup extends LitElement {
await this._loadBackupInfo();
}
private async _back() {
if (this._view === "upload" || (this._view === "status" && this._failed)) {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
} else {
const confirmed = await showConfirmationDialog(this, {
title: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.title"
),
text: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.text"
),
confirmText: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.yes"
),
dismissText: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.no"
private async _signOut() {
this._view = "loading";
showToast(this, {
id: "sign-out-ha-cloud",
message: this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.sign_out_progress"
),
});
this._backupId = undefined;
this._cloudStatus = undefined;
try {
await signOutHaCloud();
showToast(this, {
id: "sign-out-ha-cloud",
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")}`);
}
}
private _restore(ev: CustomEvent) {
if (!this._backup || !ev.detail.selectedData) {
return;
}
this._selectedData = ev.detail.selectedData;
this._view = "confirm_restore";
}
private _reupload() {
private async _back() {
this._view = "loading";
this._backup = undefined;
this._backupId = undefined;
if (this.mode === "upload") {
this._view = "upload";
} else {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
}
static styles = [
@ -313,21 +300,8 @@ class OnboardingRestoreBackup extends LitElement {
flex-direction: column;
position: relative;
}
ha-icon-button-arrow-prev {
position: absolute;
top: 12px;
}
ha-card {
width: 100%;
}
.loading {
display: flex;
justify-content: center;
padding: 32px;
}
.backup-summary-wrapper {
margin-top: 24px;
padding: 0 20px;
.logout {
white-space: nowrap;
}
`,
];

View File

@ -6,6 +6,10 @@ import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles";
import { fireEvent } from "../common/dom/fire_event";
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")
class OnboardingWelcome extends LitElement {
@ -22,23 +26,52 @@ class OnboardingWelcome extends LitElement {
${this.localize("ui.panel.page-onboarding.welcome.start")}
</ha-button>
<ha-button @click=${this._restoreBackup}>
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
</ha-button>
<ha-divider
.label=${this.localize("ui.panel.page-onboarding.welcome.or_restore")}
></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 {
fireEvent(this, "onboarding-step", {
type: "init",
result: { restore: false },
});
}
private _restoreBackup(): void {
private _restoreBackupUpload(): void {
fireEvent(this, "onboarding-step", {
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 {
display: flex;
flex-direction: column;
align-items: center;
align-items: flex-start;
}
h1 {
margin-top: 16px;
margin-bottom: 8px;
}
p {
margin: 0;
}
.start {
--button-height: 48px;
--mdc-typography-button-font-size: 1rem;
--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 { customElement, property, state } from "lit/decorators";
import "../../components/ha-card";
import { customElement, property, state, query } from "lit/decorators";
import "../../components/ha-button";
import "../../components/ha-alert";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/buttons/ha-progress-button";
import "../../components/ha-icon-button-arrow-prev";
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 {
CORE_LOCAL_AGENT,
HASSIO_LOCAL_AGENT,
getPreferredAgentForDownload,
type BackupContentExtended,
type BackupData,
} from "../../data/backup";
import { restoreOnboardingBackup } from "../../data/backup_onboarding";
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
import { fireEvent } from "../../common/dom/fire_event";
import { onBoardingStyles } from "../styles";
import { formatDateTimeWithBrowserDefaults } from "../../common/datetime/format_date_time";
@customElement("onboarding-restore-backup-restore")
class OnboardingRestoreBackupRestore extends LitElement {
@ -22,11 +27,12 @@ class OnboardingRestoreBackupRestore extends LitElement {
@property({ attribute: false }) public backup!: BackupContentExtended;
@property({ attribute: false })
public selectedData!: BackupData;
@property({ type: Boolean }) public supervisor = false;
@property() public error?: string;
@property() public mode!: "upload" | "cloud";
@state() private _encryptionKey = "";
@state() private _encryptionKeyWrong = false;
@ -35,47 +41,136 @@ class OnboardingRestoreBackupRestore extends LitElement {
@state() private _loading = false;
@state() private _selectedData?: BackupData;
@query("ha-progress-button")
private _progressButtonElement!: HaProgressButton;
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 formattedDate = formatDateTimeWithBrowserDefaults(
new Date(this.backup.date)
);
const onlyHomeAssistantBackup =
this.backup.addons.length === 0 && this.backup.folders.length === 0;
return html`
${this.backup.homeassistant_included &&
!this.supervisor &&
(this.backup.addons.length > 0 || this.backup.folders.length > 0)
? html`<ha-alert alert-type="warning" class="supervisor-warning">
<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.details.addons_unsupported"
"ui.panel.page-onboarding.restore.details.restore.title"
)}
</ha-alert>`
: nothing}
<ha-card
.header=${this.localize("ui.panel.page-onboarding.restore.restore")}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
: nothing}
<p>
</h1>
${this.backup.homeassistant_included
? html`<div class="description">
${this.localize(
"ui.panel.page-onboarding.restore.confirm_restore_full_backup_text"
)}
</p>
${backupProtected
? html`<p>
${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.title"
)}
</p>
${this._encryptionKeyWrong
? html`
</div>`
: html`
<ha-alert alert-type="error">
${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
"ui.panel.page-onboarding.restore.details.home_assistant_missing"
)}
</ha-alert>
`
`}
${this.error
? html`<ha-alert
alert-type="error"
.title=${this.localize("ui.panel.page-onboarding.restore.failed")}
>
${this.error}
</ha-alert>`
: nothing}
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.created"
)}
</span>
<span slot="supporting-text">${formattedDate}</span>
</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}
@ -83,46 +178,100 @@ class OnboardingRestoreBackupRestore extends LitElement {
"ui.panel.page-onboarding.restore.details.restore.encryption.input_label"
)}
.value=${this._encryptionKey}
></ha-password-field>`
@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}
</div>
<div class="card-actions">
<ha-progress-button
unelevated
.progress=${this._loading}
.disabled=${this._loading ||
(backupProtected && this._encryptionKey === "")}
(backupProtected && this._encryptionKey === "") ||
!this.backup.homeassistant_included}
@click=${this._startRestore}
destructive
>
${this.localize(
"ui.panel.page-onboarding.restore.details.restore.action"
)}
</ha-progress-button>
</div>
</ha-card>
`;
}
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 {
this._encryptionKey = ev.target.value;
}
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;
const button = ev.currentTarget as HaProgressButton;
this._error = undefined;
this._encryptionKeyWrong = false;
const backupAgent = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT;
const backupAgent = Object.keys(this.backup.agents)[0];
try {
await restoreOnboardingBackup({
agent_id: backupAgent,
backup_id: this.backup.backup_id,
password: this._encryptionKey || undefined,
restore_addons: this.selectedData.addons.map((addon) => addon.slug),
restore_database: this.selectedData.database_included,
restore_folders: this.selectedData.folders,
restore_addons: this._selectedData.addons.map((addon) => addon.slug),
restore_database: this._selectedData.database_included,
restore_folders: this._selectedData.folders,
});
button.actionSuccess();
fireEvent(this, "restore-started");
@ -145,21 +294,81 @@ class OnboardingRestoreBackupRestore extends LitElement {
}
}
private _back() {
fireEvent(this, "restore-backup-back");
}
static get styles(): CSSResultGroup {
return [
haStyle,
onBoardingStyles,
css`
:host {
padding: 28px 20px 0;
h1,
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;
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 {
display: block;
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 {
"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 { customElement, property } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-spinner";
import "../../components/ha-alert";
import "../../components/ha-button";
import { haStyle } from "../../resources/styles";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { BackupOnboardingInfo } from "../../data/backup_onboarding";
import { onBoardingStyles } from "../styles";
import { fireEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
import { removeSearchParam } from "../../common/url/search-params";
@customElement("onboarding-restore-backup-status")
class OnboardingRestoreBackupStatus extends LitElement {
@ -20,22 +17,24 @@ class OnboardingRestoreBackupStatus extends LitElement {
render() {
return html`
<ha-card
.header=${this.localize(
<h1>
${this.localize(
`ui.panel.page-onboarding.restore.${this.backupInfo.state === "restore_backup" ? "in_progress" : "failed"}`
)}
>
</h1>
${this.backupInfo.state === "restore_backup"
? html` <p>
${this.localize(
`ui.panel.page-onboarding.restore.in_progress_description`
)}
</p>`
: nothing}
<div class="card-content">
${this.backupInfo.state === "restore_backup"
? html`
<div class="loading">
<ha-spinner></ha-spinner>
<mwc-linear-progress indeterminate></mwc-linear-progress>
</div>
<p>
${this.localize(
"ui.panel.page-onboarding.restore.in_progress_description"
)}
</p>
`
: html`
<ha-alert alert-type="error">
@ -54,39 +53,28 @@ class OnboardingRestoreBackupStatus extends LitElement {
`}
</div>
${this.backupInfo.state !== "restore_backup"
? html`<div class="card-actions">
<ha-button @click=${this._uploadAnother} destructive>
${this.localize(
`ui.panel.page-onboarding.restore.details.summary.upload_another`
)}
</ha-button>
<ha-button @click=${this._home} destructive>
${this.localize(
`ui.panel.page-onboarding.restore.details.summary.home`
)}
? html`<div class="actions">
<ha-button @click=${this._back}>
${this.localize("ui.panel.page-onboarding.restore.back")}
</ha-button>
</div>`
: nothing}
</ha-card>
`;
}
private _uploadAnother() {
fireEvent(this, "show-backup-upload");
}
private _home() {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
private _back() {
fireEvent(this, "restore-backup-back");
}
static get styles(): CSSResultGroup {
return [
haStyle,
onBoardingStyles,
css`
:host {
padding: 28px 20px 0;
h1,
p {
text-align: left;
}
.card-actions {
.actions {
display: flex;
justify-content: flex-end;
}
@ -104,6 +92,9 @@ class OnboardingRestoreBackupStatus extends LitElement {
padding: 16px 0;
font-size: 16px;
}
mwc-linear-progress {
width: 100%;
}
`,
];
}

View File

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

View File

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

View File

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

View File

@ -17,7 +17,6 @@ import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../components/ha-checkbox";
import "../../../../components/ha-formfield";
import "../../../../components/ha-svg-icon";
import type { BackupData } from "../../../../data/backup";
import { fetchHassioAddonsInfo } from "../../../../data/hassio/addon";
import { mdiHomeAssistant } from "../../../../resources/home-assistant-logo-svg";
@ -62,6 +61,8 @@ export class HaBackupDataPicker extends LitElement {
| "page-onboarding.restore"
| "config.backup" = "config.backup";
@property({ type: Boolean, attribute: false }) public addonsDisabled = false;
@state() public _addonIcons: Record<string, boolean> = {};
protected firstUpdated(changedProps: PropertyValues): void {
@ -304,6 +305,7 @@ export class HaBackupDataPicker extends LitElement {
.indeterminate=${selectedItems.addons.length > 0 &&
selectedItems.addons.length < addonsItems.length}
@change=${this._sectionChanged}
.disabled=${this.addonsDisabled}
></ha-checkbox>
</ha-formfield>
<ha-backup-addons-picker
@ -311,6 +313,7 @@ export class HaBackupDataPicker extends LitElement {
.value=${selectedItems.addons}
@value-changed=${this._addonsChanged}
.addons=${addonsItems}
.disabled=${this.addonsDisabled}
>
</ha-backup-addons-picker>
</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 "../../../../components/ha-card";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-button";
import type { HomeAssistant } from "../../../../types";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import {
formatDateTime,
formatDateTimeWithBrowserDefaults,
} from "../../../../common/datetime/format_date_time";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import {
computeBackupSize,
computeBackupType,
type BackupContentExtended,
} from "../../../../data/backup";
import { fireEvent } from "../../../../common/dom/fire_event";
import { bytesToString } from "../../../../util/bytes-to-string";
declare global {
interface HASSDomEvents {
"show-backup-upload": undefined;
}
}
@customElement("ha-backup-details-summary")
class HaBackupDetailsSummary extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Object }) public backup!: BackupContentExtended;
@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() {
const backupDate = new Date(this.backup.date);
const formattedDate = this.hass
? formatDateTime(backupDate, this.hass.locale, this.hass.config)
: formatDateTimeWithBrowserDefaults(backupDate);
const formattedDate = formatDateTime(
backupDate,
this.hass.locale,
this.hass.config
);
return html`
<ha-card>
<div class="card-header">
${this.localize(
`ui.panel.${this.translationKeyPanel}.details.summary.title`
)}
${this.hass.localize("ui.panel.config.backup.details.summary.title")}
</div>
<div class="card-content">
<ha-md-list class="summary">
${this.translationKeyPanel === "config.backup"
? html`<ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.localize("ui.panel.config.backup.backup_type")}
${this.hass.localize("ui.panel.config.backup.backup_type")}
</span>
<span slot="supporting-text">
${this.localize(
${this.hass.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">
${this.localize(
`ui.panel.${this.translationKeyPanel}.details.summary.size`
${this.hass.localize(
"ui.panel.config.backup.details.summary.size"
)}
</span>
<span slot="supporting-text">
@ -80,31 +57,18 @@ class HaBackupDetailsSummary extends LitElement {
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.localize(
`ui.panel.${this.translationKeyPanel}.details.summary.created`
${this.hass.localize(
"ui.panel.config.backup.details.summary.created"
)}
</span>
<span slot="supporting-text">${formattedDate}</span>
</ha-md-list-item>
</ha-md-list>
</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>
`;
}
private _uploadAnother() {
fireEvent(this, "show-backup-upload");
}
static styles = css`
:host {
max-width: 690px;

View File

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

View File

@ -7,17 +7,31 @@ export interface CloudAlreadyConnectedParams {
name?: string;
version?: string;
};
logInHereAction: () => void;
closeDialog: () => void;
logInHereAction?: () => void;
closeDialog?: () => void;
}
export const showCloudAlreadyConnectedDialog = (
element: HTMLElement,
webhookDialogParams: CloudAlreadyConnectedParams
): void => {
) =>
new Promise((resolve) => {
const originalClose = webhookDialogParams.closeDialog;
const originalLogInHereAction = webhookDialogParams.logInHereAction;
fireEvent(element, "show-dialog", {
dialogTag: "dialog-cloud-already-connected",
dialogImport: () => import("./dialog-cloud-already-connected"),
dialogParams: webhookDialogParams,
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 { css, html, LitElement } 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 { cloudForgotPassword } from "../../../../data/cloud";
import { customElement, property, state } from "lit/decorators";
import "./cloud-forgot-password-card";
import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@ -22,10 +16,6 @@ export class CloudForgotPassword extends LitElement {
@state() public _requestInProgress = false;
@state() private _error?: string;
@query("#email", true) private _emailField!: HaTextField;
protected render(): TemplateResult {
return html`
<hass-subpage
@ -36,98 +26,16 @@ export class CloudForgotPassword extends LitElement {
)}
>
<div class="content">
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.subtitle"
)}
>
<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>
<cloud-forgot-password-card
.hass=${this.hass}
.localize=${this.hass.localize}
.email=${this.email}
></cloud-forgot-password-card>
</div>
</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() {
return [
haStyle,
@ -135,25 +43,6 @@ export class CloudForgotPassword extends LitElement {
.content {
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 type { ValueChangedEvent, HomeAssistant, Route } from "../../../types";
import "./account/cloud-account";
import "./login/cloud-login";
import "./login/cloud-login-panel";
const LOGGED_IN_URLS = ["account", "google-assistant", "alexa"];
const NOT_LOGGED_IN_URLS = ["login", "register", "forgot-password"];
@ -39,7 +39,7 @@ class HaConfigCloud extends HassRouterPage {
},
routes: {
login: {
tag: "cloud-login",
tag: "cloud-login-panel",
},
register: {
tag: "cloud-register",
@ -90,7 +90,7 @@ class HaConfigCloud extends HassRouterPage {
protected createElement(tag: string) {
const el = super.createElement(tag);
el.addEventListener("email-changed", (ev) => {
el.addEventListener("cloud-email-changed", (ev) => {
this._loginEmail = (ev as ValueChangedEvent<string>).detail.value;
});
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 { css, html, LitElement } 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 { navigate } from "../../../../common/navigate";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-list-item";
import "../../../../components/ha-button";
import "../../../../components/ha-password-field";
import "../../../../components/ha-button-menu";
import type { HaPasswordField } from "../../../../components/ha-password-field";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
import { cloudLogin, removeCloudData } from "../../../../data/cloud";
import { haStyle } from "../../../../resources/styles";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { cloudLogin } from "../../../../data/cloud";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} 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 { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
} from "../../../lovelace/custom-card-helpers";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
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")
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 }) public narrow = false;
@property({ type: Boolean, attribute: "check-connection" })
public checkConnection = false;
@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;
@state() private _checkConnection = true;
@query("#email", true) private _emailField!: HaTextField;
@query("#email", true) public emailField!: HaTextField;
@query("#password", true) private _passwordField!: HaPasswordField;
@state() private _error?: string;
@state() private _inProgress = false;
protected render(): TemplateResult {
if (this.cardLess) {
return this._renderLoginForm();
}
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>`
: ""}
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.cloud.login.sign_in"
.header=${this.localize(
`ui.panel.${this.translationKeyPanel}.login.sign_in`
)}
>
${this._renderLoginForm()}
</ha-card>
`;
}
private _renderLoginForm() {
return html`
<div class="card-content login-form">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
: nothing}
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.cloud.login.email"
.label=${this.localize(
`ui.panel.${this.translationKeyPanel}.login.email`
)}
id="email"
name="username"
type="email"
autocomplete="username"
required
.value=${this.email}
.value=${this.email ?? ""}
@keydown=${this._keyDown}
.disabled=${this._requestInProgress}
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.login.email_error_msg"
.disabled=${this._inProgress}
.validationMessage=${this.localize(
`ui.panel.${this.translationKeyPanel}.login.email_error_msg`
)}
></ha-textfield>
<ha-password-field
id="password"
name="password"
.label=${this.hass.localize(
"ui.panel.config.cloud.login.password"
.label=${this.localize(
`ui.panel.${this.translationKeyPanel}.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"
.disabled=${this._inProgress}
.validationMessage=${this.localize(
`ui.panel.${this.translationKeyPanel}.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}
<ha-button
.disabled=${this._inProgress}
@click=${this._handleForgotPassword}
>
${this.hass.localize(
"ui.panel.config.cloud.login.forgot_password"
${this.localize(
`ui.panel.${this.translationKeyPanel}.login.forgot_password`
)}
</button>
</ha-button>
<ha-progress-button
unelevated
@click=${this._handleLogin}
.progress=${this._inProgress}
>${this.localize(
`ui.panel.${this.translationKeyPanel}.login.sign_in`
)}</ha-progress-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() {
const emailField = this._emailField;
const passwordField = this._passwordField;
const email = emailField.value;
const password = passwordField.value;
if (!emailField.reportValidity()) {
passwordField.reportValidity();
emailField.focus();
return;
private _handleCloudLoginError = async (
err: any,
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);
}
if (!passwordField.reportValidity()) {
passwordField.focus();
return;
return logInHere ? undefined : "cancel";
}
if (errCode === "PasswordChangeRequired") {
showAlertDialog(this, {
title: this.localize(
`ui.panel.${this.translationKeyPanel}.login.alert_password_change_required`
),
});
return "password-change";
}
if (errCode === "usernotfound" && email !== email.toLowerCase()) {
this._login(email.toLowerCase(), password, checkConnection);
return undefined;
}
this._requestInProgress = true;
switch (errCode) {
case "UserNotConfirmed":
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");
}
const doLogin = async (username: string, code?: string) => {
try {
if (this.hass) {
const result = await cloudLogin({
hass: this.hass,
email: username,
email,
...(code ? { code } : { password }),
check_connection: this._checkConnection,
check_connection: checkConnection,
});
this.email = "";
this._password = "";
if (result.cloud_pipeline) {
if (
await showConfirmationDialog(this, {
@ -265,180 +238,75 @@ export class CloudLogin extends LitElement {
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
}
}
} else {
// for onboarding
await loginHaCloud({
email,
...(code ? { code } : { password: password! }),
});
}
this.email = "";
fireEvent(this, "ha-refresh-cloud-status");
} catch (err: any) {
const errCode = err && err.body && err.body.code;
if (errCode === "mfarequired") {
const totpCode = await showPromptDialog(this, {
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;
const error = await this._handleCloudLoginError(
err,
email,
password,
checkConnection
);
if (error === "cancel") {
this._inProgress = false;
this.email = "";
this._password = "";
},
});
this._passwordField.value = "";
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());
if (error === "password-change") {
this._handleForgotPassword();
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._inProgress = false;
this._error = error;
}
};
await doLogin(email);
private async _handleLogin() {
if (!this._inProgress) {
if (!this.emailField.reportValidity()) {
this.emailField.focus();
return;
}
private _handleRegister() {
this._dismissFlash();
// @ts-ignore
fireEvent(this, "email-changed", { value: this._emailField.value });
navigate("/config/cloud/register");
if (!this._passwordField.reportValidity()) {
this._passwordField.focus();
return;
}
this._inProgress = true;
this._login(
this.emailField.value,
this._passwordField.value,
this.checkConnection
);
}
}
private _handleForgotPassword() {
this._dismissFlash();
// @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);
fireEvent(this, "cloud-forgot-password");
}
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;
}
.card-actions {
display: flex;
justify-content: space-between;
@ -457,4 +325,14 @@ declare global {
interface HTMLElementTagNameMap {
"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"
autocomplete="email"
required
.value=${this.email}
.value=${this.email ?? ""}
@keydown=${this._keyDown}
validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.email_error_msg"
@ -260,9 +260,7 @@ export class CloudRegister extends LitElement {
private _verificationEmailSent(email: string) {
this._requestInProgress = false;
this._password = "";
// @ts-ignore
fireEvent(this, "email-changed", { value: email });
// @ts-ignore
fireEvent(this, "cloud-email-changed", { value: email });
fireEvent(this, "cloud-done", {
flashMessage: this.hass.localize(
"ui.panel.config.cloud.register.account_created"
@ -304,4 +302,8 @@ declare global {
interface HTMLElementTagNameMap {
"cloud-register": CloudRegister;
}
interface HASSDomEvents {
"cloud-done": { flashMessage: string };
}
}

View File

@ -4559,6 +4559,7 @@
"password_error_msg": "Passwords are at least 8 characters",
"totp_code_prompt_title": "Two-factor authentication",
"totp_code": "TOTP code",
"cancel": "Cancel",
"submit": "Submit",
"forgot_password": "Forgot password?",
"start_trial": "Start your free 1 month trial",
@ -8219,7 +8220,7 @@
"welcome": {
"header": "Welcome!",
"start": "Create my smart home",
"restore_backup": "Restore from backup",
"or_restore": "Or restore",
"vision": "Read our vision",
"community": "Join our community",
"download_app": "Download our app",
@ -8252,7 +8253,7 @@
},
"core-config": {
"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}).",
"osm_privacy_policy": "Privacy policy",
"title_location_detect": "Do you want us to detect your location?",
@ -8300,6 +8301,7 @@
"restore": {
"header": "Restore a backup",
"upload_backup": "[%key:ui::panel::config::backup::dialogs::upload::title%]",
"upload_backup_subtitle": "Upload a backup file from your device",
"unsupported": {
"title": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::title%]",
"text": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::text%]"
@ -8312,19 +8314,18 @@
"uploading": "[%key:ui::components::file-upload::uploading%]",
"details": {
"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": {
"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%]",
"upload_another": "Upload another",
"home": "Home"
"content": "Content"
},
"restore": {
"title": "[%key:ui::panel::config::backup::details::restore::title%]",
"action": "[%key:ui::panel::config::backup::details::restore::action%]",
"title": "[%key:ui::panel::config::backup::dialogs::restore::title%]",
"action": "Restore backup",
"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%]",
"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_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_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%]",
"close": "[%key:ui::common::close%]",
"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?",
"yes": "[%key:ui::common::yes%]",
"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"
}
}
},