mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-15 07:25:54 +00:00
Migrate profile dialogs to wa, refactor LL access token dialog (#29440)
* Migrate profile dialog(s) to wa * Make sure code is entered before submit is allowed * Refactor dialog * Remove unused params * Pass existing names and validate name is not already used * Reduce cleanup on show * Make QR image larger * max width * Fix * Remove extra event fire * Make params required * cleanup * Cleanup * Fix * Fix
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../components/ha-form/types";
|
||||
import "../../components/ha-markdown";
|
||||
import "../../components/ha-spinner";
|
||||
import { autocompleteLoginFields } from "../../data/auth";
|
||||
@@ -28,7 +31,7 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _stepData: any = {};
|
||||
|
||||
@@ -39,7 +42,7 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
public showDialog({ continueFlowId, mfaModuleId, dialogClosedCallback }) {
|
||||
this._instance = instance++;
|
||||
this._dialogClosedCallback = dialogClosedCallback;
|
||||
this._opened = true;
|
||||
this._open = true;
|
||||
|
||||
const fetchStep = continueFlowId
|
||||
? this.hass.callWS({
|
||||
@@ -61,22 +64,29 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
// Closed dialog by clicking on the overlay
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
if (this._step) {
|
||||
this._flowDone();
|
||||
return;
|
||||
}
|
||||
this._opened = false;
|
||||
|
||||
this._resetDialogState();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
if (this._instance === undefined) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
.heading=${this._computeStepTitle()}
|
||||
@closed=${this.closeDialog}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
header-title=${this._computeStepTitle()}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._errorMessage
|
||||
@@ -115,6 +125,7 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
)}
|
||||
></ha-markdown>
|
||||
<ha-form
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.data=${this._stepData}
|
||||
.schema=${autocompleteLoginFields(
|
||||
@@ -127,31 +138,33 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
></ha-form>`
|
||||
: ""}`}
|
||||
</div>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
appearance=${["abort", "create_entry"].includes(
|
||||
this._step?.type || ""
|
||||
)
|
||||
? "accent"
|
||||
: "plain"}
|
||||
>${this.hass.localize(
|
||||
["abort", "create_entry"].includes(this._step?.type || "")
|
||||
? "ui.panel.profile.mfa_setup.close"
|
||||
: "ui.common.cancel"
|
||||
)}</ha-button
|
||||
>
|
||||
${this._step?.type === "form"
|
||||
? html`<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._loading}
|
||||
@click=${this._submitStep}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.mfa_setup.submit"
|
||||
)}</ha-button
|
||||
>`
|
||||
: nothing}
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot=${this._step?.type === "form"
|
||||
? "secondaryAction"
|
||||
: "primaryAction"}
|
||||
appearance=${ifDefined(
|
||||
this._step?.type === "form" ? "plain" : undefined
|
||||
)}
|
||||
@click=${this.closeDialog}
|
||||
>${this.hass.localize(
|
||||
["abort", "create_entry"].includes(this._step?.type || "")
|
||||
? "ui.panel.profile.mfa_setup.close"
|
||||
: "ui.common.cancel"
|
||||
)}</ha-button
|
||||
>
|
||||
${this._step?.type === "form"
|
||||
? html`<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._isSubmitDisabled()}
|
||||
@click=${this._submitStep}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.mfa_setup.submit"
|
||||
)}</ha-button
|
||||
>`
|
||||
: nothing}
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -162,9 +175,6 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
ha-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-svg-background-color: white;
|
||||
--markdown-svg-color: black;
|
||||
@@ -177,9 +187,17 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
ha-markdown-element p {
|
||||
text-align: center;
|
||||
}
|
||||
ha-markdown-element svg {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-markdown-element code {
|
||||
background-color: transparent;
|
||||
}
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
ha-markdown-element > *:last-child {
|
||||
margin-bottom: revert;
|
||||
}
|
||||
@@ -206,6 +224,10 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
}
|
||||
|
||||
private _submitStep() {
|
||||
if (this._isSubmitDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
this._errorMessage = undefined;
|
||||
|
||||
@@ -234,6 +256,62 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _isSubmitDisabled() {
|
||||
return this._loading || this._hasMissingRequiredFields();
|
||||
}
|
||||
|
||||
private _hasMissingRequiredFields(
|
||||
schema: readonly HaFormSchema[] = this._step?.type === "form"
|
||||
? this._step.data_schema
|
||||
: []
|
||||
): boolean {
|
||||
for (const field of schema) {
|
||||
if ("schema" in field) {
|
||||
if (this._hasMissingRequiredFields(field.schema)) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!field.required) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
field.default !== undefined ||
|
||||
field.description?.suggested_value !== undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this._isEmptyValue(this._stepData[field.name])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _isEmptyValue(value: unknown): boolean {
|
||||
if (value === undefined || value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value.trim() === "";
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0;
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
return Object.keys(value as Record<string, unknown>).length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _processStep(step) {
|
||||
if (!step.errors) step.errors = {};
|
||||
this._step = step;
|
||||
@@ -251,12 +329,15 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
this._dialogClosedCallback!({
|
||||
flowFinished,
|
||||
});
|
||||
this._resetDialogState();
|
||||
}
|
||||
|
||||
private _resetDialogState() {
|
||||
this._errorMessage = undefined;
|
||||
this._step = undefined;
|
||||
this._stepData = {};
|
||||
this._dialogClosedCallback = undefined;
|
||||
this.closeDialog();
|
||||
this._instance = undefined;
|
||||
}
|
||||
|
||||
private _computeStepTitle() {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import { mdiContentCopy, mdiQrcode } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import { copyToClipboard } from "../../common/util/copy-clipboard";
|
||||
import { withViewTransition } from "../../common/util/view-transition";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-icon-button";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LongLivedAccessTokenDialogParams } from "./show-long-lived-access-token-dialog";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import { copyToClipboard } from "../../common/util/copy-clipboard";
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
const QR_LOGO_URL = "/static/icons/favicon-192x192.png";
|
||||
@@ -20,81 +21,221 @@ const QR_LOGO_URL = "/static/icons/favicon-192x192.png";
|
||||
export class HaLongLivedAccessTokenDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LongLivedAccessTokenDialogParams;
|
||||
|
||||
@state() private _qrCode?: TemplateResult;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _renderDialog = false;
|
||||
|
||||
@state() private _name = "";
|
||||
|
||||
@state() private _token?: string;
|
||||
|
||||
private _createdCallback!: () => void;
|
||||
|
||||
private _existingNames = new Set<string>();
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _errorMessage?: string;
|
||||
|
||||
public showDialog(params: LongLivedAccessTokenDialogParams): void {
|
||||
this._params = params;
|
||||
this._createdCallback = params.createdCallback;
|
||||
this._existingNames = new Set(
|
||||
params.existingNames.map((name) => this._normalizeName(name))
|
||||
);
|
||||
this._renderDialog = true;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._open = false;
|
||||
this._renderDialog = false;
|
||||
this._name = "";
|
||||
this._token = undefined;
|
||||
this._existingNames = new Set();
|
||||
this._errorMessage = undefined;
|
||||
this._loading = false;
|
||||
this._qrCode = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._params.token) {
|
||||
if (!this._renderDialog) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
.heading=${createCloseHeading(this.hass, this._params.name)}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div>
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
.value=${this._params.token}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.prompt_copy_token"
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._token
|
||||
? this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.created_title",
|
||||
{ name: this._name }
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.create"
|
||||
)}
|
||||
type="text"
|
||||
iconTrailing
|
||||
readOnly
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._copyToken}
|
||||
slot="trailingIcon"
|
||||
.path=${mdiContentCopy}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
<div id="qr">
|
||||
${this._qrCode
|
||||
? this._qrCode
|
||||
: html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._generateQR}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.generate_qr_code"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="content">
|
||||
${this._errorMessage
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._errorMessage}</ha-alert
|
||||
>`
|
||||
: nothing}
|
||||
${this._token
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.prompt_copy_token"
|
||||
)}
|
||||
</p>
|
||||
<div class="token-row">
|
||||
<ha-textfield
|
||||
autofocus
|
||||
.value=${this._token}
|
||||
type="text"
|
||||
readOnly
|
||||
></ha-textfield>
|
||||
<ha-button appearance="plain" @click=${this._copyToken}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentCopy}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.copy")}
|
||||
</ha-button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr">
|
||||
${this._qrCode
|
||||
? this._qrCode
|
||||
: html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._generateQR}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiQrcode}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.generate_qr_code"
|
||||
)}
|
||||
</ha-button>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-textfield
|
||||
autofocus
|
||||
.value=${this._name}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.name"
|
||||
)}
|
||||
.invalid=${this._hasDuplicateName()}
|
||||
.errorMessage=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.name_exists"
|
||||
)}
|
||||
required
|
||||
@input=${this._nameChanged}
|
||||
></ha-textfield>
|
||||
`}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
${this._token
|
||||
? nothing
|
||||
: html`<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>`}
|
||||
${!this._token
|
||||
? html`<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._isCreateDisabled()}
|
||||
@click=${this._createToken}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.create"
|
||||
)}
|
||||
</ha-button>`
|
||||
: html`<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>`}
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _copyToken(ev): Promise<void> {
|
||||
const textField = ev.target.parentElement as HaTextField;
|
||||
await copyToClipboard(textField.value);
|
||||
private _nameChanged(ev: Event) {
|
||||
this._name = (ev.currentTarget as HTMLInputElement).value;
|
||||
this._errorMessage = undefined;
|
||||
}
|
||||
|
||||
private _isCreateDisabled() {
|
||||
return this._loading || !this._name.trim() || this._hasDuplicateName();
|
||||
}
|
||||
|
||||
private async _createToken(): Promise<void> {
|
||||
if (this._isCreateDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = this._name.trim();
|
||||
|
||||
this._loading = true;
|
||||
this._errorMessage = undefined;
|
||||
|
||||
try {
|
||||
this._token = await this.hass.callWS<string>({
|
||||
type: "auth/long_lived_access_token",
|
||||
lifespan: 3650,
|
||||
client_name: name,
|
||||
});
|
||||
this._name = name;
|
||||
this._createdCallback();
|
||||
} catch (err: unknown) {
|
||||
this._errorMessage = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _copyToken(): Promise<void> {
|
||||
if (!this._token) {
|
||||
return;
|
||||
}
|
||||
|
||||
await copyToClipboard(this._token);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
private _normalizeName(name: string): string {
|
||||
return name.trim().toLowerCase();
|
||||
}
|
||||
|
||||
private _hasDuplicateName(): boolean {
|
||||
return this._existingNames.has(this._normalizeName(this._name));
|
||||
}
|
||||
|
||||
private async _generateQR() {
|
||||
if (!this._token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const qrcode = await import("qrcode");
|
||||
const canvas = await qrcode.toCanvas(this._params!.token, {
|
||||
width: 180,
|
||||
const canvas = await qrcode.toCanvas(this._token, {
|
||||
width: 512,
|
||||
errorCorrectionLevel: "Q",
|
||||
});
|
||||
const context = canvas.getContext("2d");
|
||||
@@ -112,35 +253,46 @@ export class HaLongLivedAccessTokenDialog extends LitElement {
|
||||
canvas.height / 3
|
||||
);
|
||||
|
||||
this._qrCode = html`<img
|
||||
alt=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.qr_code_image",
|
||||
{ name: this._params!.name }
|
||||
)}
|
||||
src=${canvas.toDataURL()}
|
||||
></img>`;
|
||||
await withViewTransition(() => {
|
||||
this._qrCode = html`<img
|
||||
alt=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.qr_code_image",
|
||||
{ name: this._name }
|
||||
)}
|
||||
src=${canvas.toDataURL()}
|
||||
></img>`;
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
#qr {
|
||||
text-align: center;
|
||||
}
|
||||
#qr img {
|
||||
max-width: 90%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.token-row {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: center;
|
||||
}
|
||||
.token-row ha-textfield {
|
||||
flex: 1;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
--textfield-icon-trailing-padding: 0;
|
||||
}
|
||||
ha-textfield > ha-icon-button {
|
||||
position: relative;
|
||||
right: -8px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: -8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -13,7 +13,6 @@ import type { RefreshToken } from "../../data/refresh_token";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -26,14 +25,14 @@ class HaLongLivedTokens extends LitElement {
|
||||
@property({ attribute: false }) public refreshTokens?: RefreshToken[];
|
||||
|
||||
private _accessTokens = memoizeOne(
|
||||
(refreshTokens: RefreshToken[]): RefreshToken[] =>
|
||||
refreshTokens
|
||||
?.filter((token) => token.type === "long_lived_access_token")
|
||||
(refreshTokens?: RefreshToken[]): RefreshToken[] =>
|
||||
(refreshTokens ?? [])
|
||||
.filter((token) => token.type === "long_lived_access_token")
|
||||
.reverse()
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const accessTokens = this._accessTokens(this.refreshTokens!);
|
||||
const accessTokens = this._accessTokens(this.refreshTokens);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
@@ -55,13 +54,13 @@ class HaLongLivedTokens extends LitElement {
|
||||
"ui.panel.profile.long_lived_access_tokens.learn_auth_requests"
|
||||
)}
|
||||
</a>
|
||||
${!accessTokens?.length
|
||||
${!accessTokens.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.empty_state"
|
||||
)}
|
||||
</p>`
|
||||
: accessTokens!.map(
|
||||
: accessTokens.map(
|
||||
(token) =>
|
||||
html`<ha-settings-row two-line>
|
||||
<span slot="heading">${token.client_name}</span>
|
||||
@@ -98,38 +97,15 @@ class HaLongLivedTokens extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _createToken(): Promise<void> {
|
||||
const name = await showPromptDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.prompt_name"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.name"
|
||||
),
|
||||
private _createToken(): void {
|
||||
const accessTokens = this._accessTokens(this.refreshTokens);
|
||||
|
||||
showLongLivedAccessTokenDialog(this, {
|
||||
createdCallback: () => fireEvent(this, "hass-refresh-tokens"),
|
||||
existingNames: accessTokens
|
||||
.map((token) => token.client_name)
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
});
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await this.hass.callWS<string>({
|
||||
type: "auth/long_lived_access_token",
|
||||
lifespan: 3650,
|
||||
client_name: name,
|
||||
});
|
||||
|
||||
showLongLivedAccessTokenDialog(this, { token, name });
|
||||
|
||||
fireEvent(this, "hass-refresh-tokens");
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.create_failed"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteToken(ev: Event): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface LongLivedAccessTokenDialogParams {
|
||||
token: string;
|
||||
name: string;
|
||||
createdCallback: () => void;
|
||||
existingNames: string[];
|
||||
}
|
||||
|
||||
export const showLongLivedAccessTokenDialog = (
|
||||
|
||||
@@ -9727,8 +9727,10 @@
|
||||
"confirm_delete_text": "Are you sure you want to delete the long-lived access token for {name}?",
|
||||
"delete_failed": "Failed to delete the access token.",
|
||||
"create": "Create token",
|
||||
"created_title": "Token created: {name}",
|
||||
"create_failed": "Failed to create the access token.",
|
||||
"name": "Name",
|
||||
"name_exists": "A token with this name already exists.",
|
||||
"prompt_name": "Give the token a name",
|
||||
"prompt_copy_token": "Copy your access token. It will not be shown again.",
|
||||
"empty_state": "You have no long-lived access tokens yet.",
|
||||
|
||||
Reference in New Issue
Block a user