diff --git a/src/data/refresh_token.ts b/src/data/refresh_token.ts new file mode 100644 index 0000000000..bc2752bdf0 --- /dev/null +++ b/src/data/refresh_token.ts @@ -0,0 +1,17 @@ +declare global { + interface HASSDomEvents { + "hass-refresh-tokens": undefined; + } +} + +export interface RefreshToken { + client_icon?: string; + client_id: string; + client_name?: string; + created_at: string; + id: string; + is_current: boolean; + last_used_at?: string; + last_used_ip?: string; + type: "normal" | "long_lived_access_token"; +} diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index 7d63d3dd53..5df7cb4f1e 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -57,7 +57,8 @@ class DialogBox extends LitElement { open ?scrimClickAction=${this._params.prompt} ?escapeKeyAction=${this._params.prompt} - @closed=${this._dismiss} + @closed=${this._dialogClosed} + defaultAction="ignore" .heading=${this._params.title ? this._params.title : this._params.confirmation && @@ -78,10 +79,10 @@ class DialogBox extends LitElement { ${this._params.prompt ? html` `} - + ${this._params.confirmText ? this._params.confirmText : this.hass.localize("ui.dialogs.generic.ok")} @@ -133,7 +138,17 @@ class DialogBox extends LitElement { this._close(); } + private _dialogClosed(ev) { + if (ev.detail.action === "ignore") { + return; + } + this.closeDialog(); + } + private _close(): void { + if (!this._params) { + return; + } this._params = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } diff --git a/src/panels/profile/ha-long-lived-access-tokens-card.js b/src/panels/profile/ha-long-lived-access-tokens-card.js deleted file mode 100644 index 5943164610..0000000000 --- a/src/panels/profile/ha-long-lived-access-tokens-card.js +++ /dev/null @@ -1,171 +0,0 @@ -import "@material/mwc-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { formatDateTime } from "../../common/datetime/format_date_time"; -import "../../components/ha-card"; -import "../../components/ha-icon-button"; -import { - showAlertDialog, - showPromptDialog, - showConfirmationDialog, -} from "../../dialogs/generic/show-dialog-box"; -import { EventsMixin } from "../../mixins/events-mixin"; -import LocalizeMixin from "../../mixins/localize-mixin"; -import "../../styles/polymer-ha-style"; -import "../../components/ha-settings-row"; - -/* - * @appliesMixin EventsMixin - * @appliesMixin LocalizeMixin - */ -class HaLongLivedTokens extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - -
-

- [[localize('ui.panel.profile.long_lived_access_tokens.description')]] - - [[localize('ui.panel.profile.long_lived_access_tokens.learn_auth_requests')]] - -

- -
- -
- - [[localize('ui.panel.profile.long_lived_access_tokens.create')]] - -
-
- `; - } - - static get properties() { - return { - hass: Object, - refreshTokens: Array, - _tokens: { - type: Array, - computed: "_computeTokens(refreshTokens)", - }, - }; - } - - _computeTokens(refreshTokens) { - return refreshTokens - .filter((tkn) => tkn.type === "long_lived_access_token") - .reverse(); - } - - _formatTitle(name) { - return this.localize( - "ui.panel.profile.long_lived_access_tokens.token_title", - "name", - name - ); - } - - _formatCreatedAt(created) { - return this.localize( - "ui.panel.profile.long_lived_access_tokens.created_at", - "date", - formatDateTime(new Date(created), this.hass.language) - ); - } - - async _handleCreate() { - const name = await showPromptDialog(this, { - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.prompt_name" - ), - }); - if (!name) return; - try { - const token = await this.hass.callWS({ - type: "auth/long_lived_access_token", - lifespan: 3650, - client_name: name, - }); - await showPromptDialog(this, { - title: name, - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.prompt_copy_token" - ), - defaultValue: token, - }); - this.fire("hass-refresh-tokens"); - } catch (err) { - // eslint-disable-next-line - console.error(err); - showAlertDialog(this, { - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.create_failed" - ), - }); - } - } - - async _handleDelete(ev) { - const token = ev.model.item; - if ( - !(await showConfirmationDialog(this, { - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.confirm_delete", - "name", - token.client_name - ), - })) - ) { - return; - } - try { - await this.hass.callWS({ - type: "auth/delete_refresh_token", - refresh_token_id: token.id, - }); - this.fire("hass-refresh-tokens"); - } catch (err) { - // eslint-disable-next-line - console.error(err); - showAlertDialog(this, { - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.delete_failed" - ), - }); - } - } -} - -customElements.define("ha-long-lived-access-tokens-card", HaLongLivedTokens); diff --git a/src/panels/profile/ha-long-lived-access-tokens-card.ts b/src/panels/profile/ha-long-lived-access-tokens-card.ts new file mode 100644 index 0000000000..8c8eb36d96 --- /dev/null +++ b/src/panels/profile/ha-long-lived-access-tokens-card.ts @@ -0,0 +1,193 @@ +import "@material/mwc-button"; +import { + css, + CSSResultArray, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import relativeTime from "../../common/datetime/relative_time"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-card"; +import "../../components/ha-icon-button"; +import "../../components/ha-settings-row"; +import { RefreshToken } from "../../data/refresh_token"; +import { + showAlertDialog, + showConfirmationDialog, + showPromptDialog, +} from "../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../resources/styles"; +import "../../styles/polymer-ha-style"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-long-lived-access-tokens-card") +class HaLongLivedTokens extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public refreshTokens?: RefreshToken[]; + + private _accessTokens = memoizeOne( + (refreshTokens: RefreshToken[]): RefreshToken[] => + refreshTokens + ?.filter((token) => token.type === "long_lived_access_token") + .reverse() + ); + + protected render(): TemplateResult { + const accessTokens = this._accessTokens(this.refreshTokens!); + + return html` + +
+ ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.description" + )} + + + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.learn_auth_requests" + )} + + ${!accessTokens?.length + ? html`

+ ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.empty_state" + )} +

` + : accessTokens!.map( + (token) => html` + ${token.client_name} +
+ ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.created", + "date", + relativeTime( + new Date(token.created_at), + this.hass.localize + ) + )} +
+ +
` + )} +
+ +
+ + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.create" + )} + +
+
+ `; + } + + private async _createToken(): Promise { + 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" + ), + }); + + if (!name) { + return; + } + + try { + const token = await this.hass.callWS({ + type: "auth/long_lived_access_token", + lifespan: 3650, + client_name: name, + }); + + showPromptDialog(this, { + title: name, + text: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.prompt_copy_token" + ), + defaultValue: token, + }); + + fireEvent(this, "hass-refresh-tokens"); + } catch (err) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.create_failed" + ), + text: err.message, + }); + } + } + + private async _deleteToken(ev: Event): Promise { + const token = (ev.currentTarget as any).token; + if ( + !(await showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.confirm_delete", + "name", + token.client_name + ), + })) + ) { + return; + } + try { + await this.hass.callWS({ + type: "auth/delete_refresh_token", + refresh_token_id: token.id, + }); + fireEvent(this, "hass-refresh-tokens"); + } catch (err) { + await showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.delete_failed" + ), + text: err.message, + }); + } + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + ha-settings-row { + padding: 0; + } + a { + color: var(--primary-color); + } + ha-icon-button { + color: var(--primary-text-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-long-lived-access-tokens-card": HaLongLivedTokens; + } +} diff --git a/src/panels/profile/ha-panel-profile.ts b/src/panels/profile/ha-panel-profile.ts index 3c429e2e31..b4aa386092 100644 --- a/src/panels/profile/ha-panel-profile.ts +++ b/src/panels/profile/ha-panel-profile.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "../../layouts/ha-app-layout"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/paper-item/paper-item"; @@ -9,9 +8,9 @@ import { css, CSSResultArray, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import { fireEvent } from "../../common/dom/fire_event"; @@ -22,7 +21,9 @@ import { CoreFrontendUserData, getOptimisticFrontendUserDataCollection, } from "../../data/frontend"; +import { RefreshToken } from "../../data/refresh_token"; import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; +import "../../layouts/ha-app-layout"; import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import "./ha-advanced-mode-row"; @@ -35,15 +36,15 @@ import "./ha-pick-language-row"; import "./ha-pick-theme-row"; import "./ha-push-notifications-row"; import "./ha-refresh-tokens-card"; -import "./ha-set-vibrate-row"; import "./ha-set-suspend-row"; +import "./ha-set-vibrate-row"; class HaPanelProfile extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public narrow!: boolean; + @property({ type: Boolean }) public narrow!: boolean; - @internalProperty() private _refreshTokens?: unknown[]; + @internalProperty() private _refreshTokens?: RefreshToken[]; @internalProperty() private _coreUserData?: CoreFrontendUserData | null; diff --git a/src/translations/en.json b/src/translations/en.json index 24f05139cc..29cc400560 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2512,14 +2512,13 @@ "header": "Long-Lived Access Tokens", "description": "Create long-lived access tokens to allow your scripts to interact with your Home Assistant instance. Each token will be valid for 10 years from creation. The following long-lived access tokens are currently active.", "learn_auth_requests": "Learn how to make authenticated requests.", - "created_at": "Created at {date}", - "last_used": "Last used at {date} from {location}", - "not_used": "Has never been used", + "created": "Created {date}", "confirm_delete": "Are you sure you want to delete the access token for {name}?", "delete_failed": "Failed to delete the access token.", "create": "Create Token", "create_failed": "Failed to create the access token.", - "prompt_name": "Name?", + "name": "Name", + "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." }