From c7796e95576fe6404a8bd6357ca9e08ba52d74fe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Feb 2019 11:58:07 -0800 Subject: [PATCH] Allow picking users (#2768) * Allow picking users * Update ha-user-badge.ts --- src/components/ha-sidebar.ts | 48 +------- src/components/user/ha-user-badge.ts | 77 +++++++++++++ src/components/user/ha-user-picker.ts | 104 ++++++++++++++++++ src/data/auth.ts | 21 ++++ src/data/ws-user.ts | 11 +- src/layouts/hass-loading-screen.ts | 2 +- src/layouts/hass-subpage.ts | 2 +- .../dialog-area-registry-detail.ts | 2 +- .../config/automation/ha-automation-editor.ts | 2 +- .../cloud/cloud-webhook-manage-dialog.ts | 2 +- .../dialog-entity-registry-detail.ts | 2 +- .../config/person/dialog-person-detail.ts | 40 +++++-- src/panels/config/person/ha-config-person.ts | 30 ++++- .../person/show-dialog-person-detail.ts | 2 + ...ser-picker.js => ha-config-user-picker.js} | 2 +- src/panels/config/users/ha-config-users.js | 12 +- src/panels/config/zha/ha-config-zha.ts | 2 +- .../config/zha/zha-cluster-attributes.ts | 2 +- src/panels/config/zha/zha-cluster-commands.ts | 2 +- src/panels/config/zha/zha-clusters.ts | 2 +- src/panels/config/zha/zha-device-card.ts | 2 +- src/panels/config/zha/zha-network.ts | 2 +- src/panels/config/zha/zha-node.ts | 2 +- .../dev-info/dialog-system-log-detail.ts | 2 +- src/panels/dev-info/ha-panel-dev-info.ts | 2 +- .../card-editor/hui-dialog-pick-card.ts | 2 +- .../editor/card-editor/hui-edit-card.ts | 2 +- .../lovelace/editor/hui-dialog-save-config.ts | 2 +- .../hui-dialog-edit-lovelace.ts | 2 +- .../editor/view-editor/hui-edit-view.ts | 2 +- src/panels/lovelace/hui-editor.ts | 2 +- src/panels/lovelace/hui-root.ts | 2 +- src/resources/ha-style.ts | 89 +-------------- src/resources/styles.ts | 88 +++++++++++++++ src/types.ts | 4 +- 35 files changed, 393 insertions(+), 179 deletions(-) create mode 100644 src/components/user/ha-user-badge.ts create mode 100644 src/components/user/ha-user-picker.ts rename src/panels/config/users/{ha-user-picker.js => ha-config-user-picker.js} (98%) create mode 100644 src/resources/styles.ts diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 4cc5aaddf9..82cf0585ab 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -6,7 +6,6 @@ import { PropertyValues, property, } from "lit-element"; -import { classMap } from "lit-html/directives/class-map"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-item/paper-icon-item"; @@ -14,27 +13,12 @@ import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import "./ha-icon"; +import "../components/user/ha-user-badge"; import isComponentLoaded from "../common/config/is_component_loaded"; import { HomeAssistant, Panel } from "../types"; import { fireEvent } from "../common/dom/fire_event"; import { DEFAULT_PANEL } from "../common/const"; -const computeInitials = (name: string) => { - if (!name) { - return "user"; - } - return ( - name - .trim() - // Split by space and take first 3 words - .split(" ") - .slice(0, 3) - // Of each word, take first letter - .map((s) => s.substr(0, 1)) - .join("") - ); -}; - const computeUrl = (urlPath) => `/${urlPath}`; const computePanels = (hass: HomeAssistant) => { @@ -93,22 +77,13 @@ class HaSidebar extends LitElement { return html``; } - const initials = hass.user ? computeInitials(hass.user.name) : ""; - return html`
Home Assistant
${hass.user ? html` - 2, - })}" - > - - ${initials} + + ` : ""} @@ -344,23 +319,6 @@ class HaSidebar extends LitElement { .dev-tools a { color: var(--sidebar-icon-color); } - - .profile-badge { - /* for ripple */ - position: relative; - box-sizing: border-box; - width: 40px; - line-height: 40px; - border-radius: 50%; - text-align: center; - background-color: var(--light-primary-color); - text-decoration: none; - color: var(--primary-text-color); - } - - .profile-badge.long { - font-size: 80%; - } `; } } diff --git a/src/components/user/ha-user-badge.ts b/src/components/user/ha-user-badge.ts new file mode 100644 index 0000000000..a4bb8a5a31 --- /dev/null +++ b/src/components/user/ha-user-badge.ts @@ -0,0 +1,77 @@ +import { + LitElement, + TemplateResult, + css, + CSSResult, + html, + property, + customElement, +} from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; +import { User } from "../../data/auth"; +import { CurrentUser } from "../../types"; + +const computeInitials = (name: string) => { + if (!name) { + return "user"; + } + return ( + name + .trim() + // Split by space and take first 3 words + .split(" ") + .slice(0, 3) + // Of each word, take first letter + .map((s) => s.substr(0, 1)) + .join("") + ); +}; + +@customElement("ha-user-badge") +class StateBadge extends LitElement { + @property() public user?: User | CurrentUser; + + protected render(): TemplateResult | void { + const user = this.user; + + const initials = user ? computeInitials(user.name) : "?"; + + return html` +
2, + })}" + > + ${initials} +
+ `; + } + + static get styles(): CSSResult { + return css` + .profile-badge { + display: inline-block; + box-sizing: border-box; + width: 40px; + line-height: 40px; + border-radius: 50%; + text-align: center; + background-color: var(--light-primary-color); + text-decoration: none; + color: var(--primary-text-color); + overflow: hidden; + } + + .profile-badge.long { + font-size: 80%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-user-badge": StateBadge; + } +} diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts new file mode 100644 index 0000000000..9fcc6db530 --- /dev/null +++ b/src/components/user/ha-user-picker.ts @@ -0,0 +1,104 @@ +import "@polymer/paper-icon-button/paper-icon-button"; +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-icon-item"; +import "@polymer/paper-item/paper-item-body"; +import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; +import "@polymer/paper-listbox/paper-listbox"; +import memoizeOne from "memoize-one"; +import { + LitElement, + TemplateResult, + html, + css, + CSSResult, + property, +} from "lit-element"; +import { HomeAssistant } from "../../types"; +import { fireEvent } from "../../common/dom/fire_event"; +import { User, fetchUsers } from "../../data/auth"; +import compare from "../../common/string/compare"; + +class HaEntityPicker extends LitElement { + public hass?: HomeAssistant; + @property() public label?: string; + @property() public value?: string; + @property() public users?: User[]; + + private _sortedUsers = memoizeOne((users?: User[]) => { + if (!users || users.length === 1) { + return users || []; + } + const sorted = [...users]; + sorted.sort((a, b) => compare(a.name, b.name)); + return sorted; + }); + + protected render(): TemplateResult | void { + return html` + + + + No user + + ${this._sortedUsers(this.users).map( + (user) => html` + + + ${user.name} + + ` + )} + + + `; + } + + private get _value() { + return this.value || ""; + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + if (this.users === undefined) { + fetchUsers(this.hass!).then((users) => { + this.users = users; + }); + } + } + + private _userChanged(ev) { + const newValue = ev.detail.item.dataset.userId; + + if (newValue !== this._value) { + this.value = ev.detail.value; + setTimeout(() => { + fireEvent(this, "value-changed", { value: newValue }); + fireEvent(this, "change"); + }, 0); + } + } + + static get styles(): CSSResult { + return css` + :host { + display: inline-block; + } + paper-dropdown-menu-light { + display: block; + } + paper-listbox { + min-width: 200px; + } + paper-icon-item { + cursor: pointer; + } + `; + } +} + +customElements.define("ha-user-picker", HaEntityPicker); diff --git a/src/data/auth.ts b/src/data/auth.ts index 52c3af9099..4ec1e57146 100644 --- a/src/data/auth.ts +++ b/src/data/auth.ts @@ -1,5 +1,26 @@ +import { HomeAssistant } from "../types"; + export interface AuthProvider { name: string; id: string; type: string; } + +interface Credential { + type: string; +} + +export interface User { + id: string; + name: string; + is_owner: boolean; + is_active: boolean; + system_generated: boolean; + group_ids: string[]; + credentials: Credential[]; +} + +export const fetchUsers = async (hass: HomeAssistant) => + hass.callWS({ + type: "config/auth/list", + }); diff --git a/src/data/ws-user.ts b/src/data/ws-user.ts index 66be784eb8..2e171b8dab 100644 --- a/src/data/ws-user.ts +++ b/src/data/ws-user.ts @@ -3,12 +3,17 @@ import { Connection, getCollection, } from "home-assistant-js-websocket"; -import { User } from "../types"; +import { CurrentUser } from "../types"; export const userCollection = (conn: Connection) => - getCollection(conn, "_usr", () => getUser(conn) as Promise, undefined); + getCollection( + conn, + "_usr", + () => getUser(conn) as Promise, + undefined + ); export const subscribeUser = ( conn: Connection, - onChange: (user: User) => void + onChange: (user: CurrentUser) => void ) => userCollection(conn).subscribe(onChange); diff --git a/src/layouts/hass-loading-screen.ts b/src/layouts/hass-loading-screen.ts index 6c96a88026..f67d376e1f 100644 --- a/src/layouts/hass-loading-screen.ts +++ b/src/layouts/hass-loading-screen.ts @@ -10,7 +10,7 @@ import { customElement, } from "lit-element"; import "../components/ha-menu-button"; -import { haStyle } from "../resources/ha-style"; +import { haStyle } from "../resources/styles"; @customElement("hass-loading-screen") class HassLoadingScreen extends LitElement { diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts index 25b4b5a794..8895df0e74 100644 --- a/src/layouts/hass-subpage.ts +++ b/src/layouts/hass-subpage.ts @@ -10,7 +10,7 @@ import { customElement, CSSResult, } from "lit-element"; -import { haStyle } from "../resources/ha-style"; +import { haStyle } from "../resources/styles"; @customElement("hass-subpage") class HassSubpage extends LitElement { diff --git a/src/panels/config/area_registry/dialog-area-registry-detail.ts b/src/panels/config/area_registry/dialog-area-registry-detail.ts index 3ccc56eec5..4fd01bf688 100644 --- a/src/panels/config/area_registry/dialog-area-registry-detail.ts +++ b/src/panels/config/area_registry/dialog-area-registry-detail.ts @@ -12,7 +12,7 @@ import "@polymer/paper-input/paper-input"; import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail"; import { PolymerChangedEvent } from "../../../polymer-types"; -import { haStyleDialog } from "../../../resources/ha-style"; +import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { AreaRegistryEntryMutableParams } from "../../../data/area_registry"; diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index b2f0e237dd..65abc4bcda 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -21,7 +21,7 @@ import Automation from "../js/automation"; import unmountPreact from "../../../common/preact/unmount"; import computeStateName from "../../../common/entity/compute_state_name"; -import { haStyle } from "../../../resources/ha-style"; +import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { AutomationEntity, AutomationConfig } from "../../../data/automation"; import { navigate } from "../../../common/navigate"; diff --git a/src/panels/config/cloud/cloud-webhook-manage-dialog.ts b/src/panels/config/cloud/cloud-webhook-manage-dialog.ts index a012de9b4f..f484e4f84c 100644 --- a/src/panels/config/cloud/cloud-webhook-manage-dialog.ts +++ b/src/panels/config/cloud/cloud-webhook-manage-dialog.ts @@ -18,7 +18,7 @@ import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { HomeAssistant } from "../../../types"; import { WebhookDialogParams } from "./types"; -import { haStyle } from "../../../resources/ha-style"; +import { haStyle } from "../../../resources/styles"; const inputLabel = "Public URL – Click to copy to clipboard"; diff --git a/src/panels/config/entity_registry/dialog-entity-registry-detail.ts b/src/panels/config/entity_registry/dialog-entity-registry-detail.ts index d8f7a9be34..3ddebbcefa 100644 --- a/src/panels/config/entity_registry/dialog-entity-registry-detail.ts +++ b/src/panels/config/entity_registry/dialog-entity-registry-detail.ts @@ -12,7 +12,7 @@ import "@polymer/paper-input/paper-input"; import { EntityRegistryDetailDialogParams } from "./show-dialog-entity-registry-detail"; import { PolymerChangedEvent } from "../../../polymer-types"; -import { haStyleDialog } from "../../../resources/ha-style"; +import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import computeDomain from "../../../common/entity/compute_domain"; import { HassEntity } from "home-assistant-js-websocket"; diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index d44eeb8436..ce9815ad76 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -9,29 +9,37 @@ import { import "@polymer/paper-dialog/paper-dialog"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-input/paper-input"; +import "@material/mwc-button"; import "../../../components/entity/ha-entities-picker"; +import "../../../components/user/ha-user-picker"; import { PersonDetailDialogParams } from "./show-dialog-person-detail"; import { PolymerChangedEvent } from "../../../polymer-types"; -import { haStyleDialog } from "../../../resources/ha-style"; +import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { PersonMutableParams } from "../../../data/person"; class DialogPersonDetail extends LitElement { @property() public hass!: HomeAssistant; @property() private _name!: string; + @property() private _userId?: string; @property() private _deviceTrackers!: string[]; @property() private _error?: string; @property() private _params?: PersonDetailDialogParams; - @property() private _submitting?: boolean; + @property() private _submitting: boolean = false; public async showDialog(params: PersonDetailDialogParams): Promise { this._params = params; this._error = undefined; - this._name = this._params.entry ? this._params.entry.name : ""; - this._deviceTrackers = this._params.entry - ? this._params.entry.device_trackers || [] - : []; + if (this._params.entry) { + this._name = this._params.entry.name || ""; + this._userId = this._params.entry.user_id || undefined; + this._deviceTrackers = this._params.entry.device_trackers || []; + } else { + this._name = ""; + this._userId = undefined; + this._deviceTrackers = []; + } await this.updateComplete; } @@ -61,6 +69,13 @@ class DialogPersonDetail extends LitElement { error-message="Name is required" .invalid=${nameInvalid} > +

${this.hass.localize( "ui.panel.config.person.detail.device_tracker_intro" @@ -108,6 +123,11 @@ class DialogPersonDetail extends LitElement { this._name = ev.detail.value; } + private _userChanged(ev: PolymerChangedEvent) { + this._error = undefined; + this._userId = ev.detail.value; + } + private _deviceTrackersChanged(ev: PolymerChangedEvent) { this._error = undefined; this._deviceTrackers = ev.detail.value; @@ -119,8 +139,7 @@ class DialogPersonDetail extends LitElement { const values: PersonMutableParams = { name: this._name.trim(), device_trackers: this._deviceTrackers, - // Temp, we will add this in a future PR. - user_id: null, + user_id: this._userId || null, }; if (this._params!.entry) { await this._params!.updateEntry(values); @@ -129,7 +148,7 @@ class DialogPersonDetail extends LitElement { } this._params = undefined; } catch (err) { - this._error = err; + this._error = err ? err.message : "Unknown error"; } finally { this._submitting = false; } @@ -162,6 +181,9 @@ class DialogPersonDetail extends LitElement { .form { padding-bottom: 24px; } + ha-user-picker { + margin-top: 16px; + } mwc-button.warning { margin-right: auto; } diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts index 06367c1e62..25071c52b7 100644 --- a/src/panels/config/person/ha-config-person.ts +++ b/src/panels/config/person/ha-config-person.ts @@ -27,12 +27,14 @@ import { showPersonDetailDialog, loadPersonDetailDialog, } from "./show-dialog-person-detail"; +import { User, fetchUsers } from "../../../data/auth"; class HaConfigPerson extends LitElement { public hass?: HomeAssistant; public isWide?: boolean; private _storageItems?: Person[]; private _configItems?: Person[]; + private _usersLoad?: Promise; static get properties(): PropertyDeclarations { return { @@ -62,7 +64,7 @@ class HaConfigPerson extends LitElement { ${this._configItems.length > 0 ? html`

- Note: people configured via configuration.yaml cannot be + Note: persons configured via configuration.yaml cannot be edited via the UI.

` @@ -81,7 +83,7 @@ class HaConfigPerson extends LitElement { ${this._storageItems.length === 0 ? html`
- Looks like you have no people yet! + Looks like you have not created any persons yet. CREATE PERSON @@ -91,7 +93,7 @@ class HaConfigPerson extends LitElement { ${this._configItems.length > 0 ? html` - + ${this._configItems.map((entry) => { return html` @@ -123,6 +125,7 @@ class HaConfigPerson extends LitElement { } private async _fetchData() { + this._usersLoad = fetchUsers(this.hass!); const personData = await fetchPersons(this.hass!); this._storageItems = personData.storage.sort((ent1, ent2) => @@ -142,9 +145,27 @@ class HaConfigPerson extends LitElement { this._openDialog(entry); } - private _openDialog(entry?: Person) { + private _allowedUsers(users: User[], currentPerson?: Person) { + const used = new Set(); + for (const coll of [this._configItems, this._storageItems]) { + for (const pers of coll!) { + if (pers.user_id) { + used.add(pers.user_id); + } + } + } + const currentUserId = currentPerson ? currentPerson.user_id : undefined; + return users.filter( + (user) => user.id === currentUserId || !used.has(user.id) + ); + } + + private async _openDialog(entry?: Person) { + const users = await this._usersLoad!; + showPersonDetailDialog(this, { entry, + users: this._allowedUsers(users, entry), createEntry: async (values) => { const created = await createPerson(this.hass!, values); this._storageItems = this._storageItems!.concat(created).sort( @@ -191,6 +212,7 @@ All devices in this area will become unassigned.`) } .empty { text-align: center; + padding: 8px; } paper-item { padding-top: 4px; diff --git a/src/panels/config/person/show-dialog-person-detail.ts b/src/panels/config/person/show-dialog-person-detail.ts index 1a15a00601..96aead9702 100644 --- a/src/panels/config/person/show-dialog-person-detail.ts +++ b/src/panels/config/person/show-dialog-person-detail.ts @@ -1,8 +1,10 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { Person, PersonMutableParams } from "../../../data/person"; +import { User } from "../../../data/auth"; export interface PersonDetailDialogParams { entry?: Person; + users: User[]; createEntry: (values: PersonMutableParams) => Promise; updateEntry: (updates: Partial) => Promise; removeEntry: () => Promise; diff --git a/src/panels/config/users/ha-user-picker.js b/src/panels/config/users/ha-config-user-picker.js similarity index 98% rename from src/panels/config/users/ha-user-picker.js rename to src/panels/config/users/ha-config-user-picker.js index 1a7185264a..9d10f13487 100644 --- a/src/panels/config/users/ha-user-picker.js +++ b/src/panels/config/users/ha-config-user-picker.js @@ -115,4 +115,4 @@ class HaUserPicker extends EventsMixin( } } -customElements.define("ha-user-picker", HaUserPicker); +customElements.define("ha-config-user-picker", HaUserPicker); diff --git a/src/panels/config/users/ha-config-users.js b/src/panels/config/users/ha-config-users.js index 7d83ee515d..16cfe6d5b9 100644 --- a/src/panels/config/users/ha-config-users.js +++ b/src/panels/config/users/ha-config-users.js @@ -6,9 +6,10 @@ import { PolymerElement } from "@polymer/polymer/polymer-element"; import NavigateMixin from "../../../mixins/navigate-mixin"; -import "./ha-user-picker"; +import "./ha-config-user-picker"; import "./ha-user-editor"; import { fireEvent } from "../../../common/dom/fire_event"; +import { fetchUsers } from "../../../data/auth"; /* * @appliesMixin NavigateMixin @@ -23,7 +24,10 @@ class HaConfigUsers extends NavigateMixin(PolymerElement) { >