diff --git a/src/entrypoints/app.js b/src/entrypoints/app.js index 3aaf3fac62..8f64cd4a46 100644 --- a/src/entrypoints/app.js +++ b/src/entrypoints/app.js @@ -173,6 +173,8 @@ class HomeAssistant extends LocalizeMixin(PolymerElement) { moreInfoEntityId: null, callService: null, callApi: null, + sendWS: null, + callWS: null, }); return; } @@ -243,6 +245,29 @@ class HomeAssistant extends LocalizeMixin(PolymerElement) { return await hassCallApi(host, auth, method, path, parameters); } }, + // For messages that do not get a response + sendWS: (msg) => { + // eslint-disable-next-line + if (__DEV__) console.log('Sending', msg); + conn.sendMessage(msg); + }, + // For messages that expect a response + callWS: (msg) => { + /* eslint-disable no-console */ + if (__DEV__) console.log('Sending', msg); + + const resp = conn.sendMessagePromise(msg); + + if (__DEV__) { + resp.then( + result => console.log('Received', result), + err => console.log('Error', err), + ); + } + // In the future we'll do this as a breaking change + // inside home-assistant-js-websocket + return resp.then(result => result.result); + }, }, this.$.storage.getStoredState()); var reconnected = () => { diff --git a/src/panels/config/dashboard/ha-config-dashboard.js b/src/panels/config/dashboard/ha-config-dashboard.js index 2919d63b5c..e426b89677 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.js +++ b/src/panels/config/dashboard/ha-config-dashboard.js @@ -9,6 +9,7 @@ import '../../../components/ha-menu-button.js'; import '../ha-config-section.js'; import './ha-config-cloud-menu.js'; import './ha-config-entries-menu.js'; +import './ha-config-users-menu.js'; import './ha-config-navigation.js'; import isComponentLoaded from '../../../common/config/is_component_loaded.js'; @@ -39,14 +40,18 @@ class HaConfigDashboard extends LocalizeMixin(PolymerElement) { [[localize('ui.panel.config.header')]] [[localize('ui.panel.config.introduction')]] - + - + + + + + diff --git a/src/panels/config/dashboard/ha-config-users-menu.js b/src/panels/config/dashboard/ha-config-users-menu.js new file mode 100644 index 0000000000..759bdf2470 --- /dev/null +++ b/src/panels/config/dashboard/ha-config-users-menu.js @@ -0,0 +1,47 @@ +import '@polymer/iron-icon/iron-icon.js'; +import '@polymer/paper-card/paper-card.js'; +import '@polymer/paper-item/paper-item-body.js'; +import '@polymer/paper-item/paper-item.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import LocalizeMixin from '../../../mixins/localize-mixin.js'; + +/* + * @appliesMixin LocalizeMixin + */ +class HaConfigUsersMenu extends LocalizeMixin(PolymerElement) { + static get template() { + return html` + + + + + + [[localize('ui.panel.config.users.caption')]] + + [[localize('ui.panel.config.users.description')]] + + + + + + +`; + } + + static get properties() { + return { + hass: Object, + }; + } +} + +customElements.define('ha-config-users-menu', HaConfigUsersMenu); diff --git a/src/panels/config/ha-panel-config.js b/src/panels/config/ha-panel-config.js index 9aee750918..43e598d873 100644 --- a/src/panels/config/ha-panel-config.js +++ b/src/panels/config/ha-panel-config.js @@ -12,6 +12,7 @@ import './core/ha-config-core.js'; import './customize/ha-config-customize.js'; import './dashboard/ha-config-dashboard.js'; import './script/ha-config-script.js'; +import './users/ha-config-users.js'; import './zwave/ha-config-zwave.js'; import isComponentLoaded from '../../common/config/is_component_loaded.js'; @@ -104,6 +105,14 @@ class HaPanelConfig extends NavigateMixin(PolymerElement) { is-wide='[[isWide]]' > + + + + `; } diff --git a/src/panels/config/users/ha-config-users.js b/src/panels/config/users/ha-config-users.js new file mode 100644 index 0000000000..68c39953db --- /dev/null +++ b/src/panels/config/users/ha-config-users.js @@ -0,0 +1,100 @@ +import '@polymer/app-route/app-route.js'; +import { timeOut } from '@polymer/polymer/lib/utils/async.js'; +import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import NavigateMixin from '../../../mixins/navigate-mixin.js'; + +import './ha-user-picker.js'; +import './ha-user-editor.js'; + +/* + * @appliesMixin NavigateMixin + */ +class HaConfigUsers extends NavigateMixin(PolymerElement) { + static get template() { + return html` + + + + + + + + +`; + } + + static get properties() { + return { + hass: Object, + route: { + type: Object, + observer: '_checkRoute', + }, + _routeData: Object, + _user: { + type: Object, + value: null, + }, + _users: { + type: Array, + value: null, + }, + }; + } + + ready() { + super.ready(); + this._loadData(); + this.addEventListener('reload-users', () => this._loadData()); + } + + _handlePickUser(ev) { + this._user = ev.detail.user; + } + + _checkRoute(route) { + if (!route || route.path.substr(0, 6) !== '/users') return; + + // prevent list gettung under toolbar + this.fire('iron-resize'); + + this._debouncer = Debouncer.debounce( + this._debouncer, + timeOut.after(0), + () => { + if (route.path === '/users') { + this.navigate('/config/users/picker', true); + } + } + ); + } + + _computeUser(users, userId) { + return users && users.filter(u => u.id === userId)[0]; + } + + _equals(a, b) { + return a === b; + } + + async _loadData() { + this._users = await this.hass.callWS({ + type: 'config/auth/list', + }); + } +} + +customElements.define('ha-config-users', HaConfigUsers); diff --git a/src/panels/config/users/ha-dialog-add-user.js b/src/panels/config/users/ha-dialog-add-user.js new file mode 100644 index 0000000000..b460ffacd3 --- /dev/null +++ b/src/panels/config/users/ha-dialog-add-user.js @@ -0,0 +1,169 @@ +import '@polymer/paper-button/paper-button.js'; +import '@polymer/paper-dialog/paper-dialog.js'; +import '@polymer/paper-spinner/paper-spinner.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import '../../../resources/ha-style.js'; + +import LocalizeMixin from '../../../mixins/localize-mixin.js'; + +/* + * @appliesMixin LocalizeMixin + */ +class HaDialogAddUser extends LocalizeMixin(PolymerElement) { + static get template() { + return html` + + + Add user + + + [[_errorMsg]] + + + + + + + + + + Create + + + +`; + } + + static get properties() { + return { + _hass: Object, + _dialogClosedCallback: Function, + + _loading: { + type: Boolean, + value: false, + }, + + // Error message when can't talk to server etc + _errorMsg: String, + + _opened: { + type: Boolean, + value: false, + }, + + _username: String, + _password: String, + }; + } + + ready() { + super.ready(); + this.addEventListener('keypress', (ev) => { + if (ev.keyCode === 13) { + this._createUser(); + } + }); + } + + showDialog({ hass, dialogClosedCallback }) { + this.hass = hass; + this._dialogClosedCallback = dialogClosedCallback; + this._loading = false; + this._opened = true; + } + + async _createUser() { + if (!this._username || !this._password) return; + + this._loading = true; + this._errorMsg = null; + + let userId; + + try { + const userResponse = await this.hass.callWS({ + type: 'config/auth/create', + name: this._username, + }); + userId = userResponse.user.id; + } catch (err) { + this._loading = false; + this._errorMsg = err.code; + return; + } + + try { + await this.hass.callWS({ + type: 'config/auth_provider/homeassistant/create', + user_id: userId, + username: this._username, + password: this._password, + }); + } catch (err) { + this._loading = false; + this._errorMsg = err.code; + await this.hass.callWS({ + type: 'config/auth/delete', + user_id: userId, + }); + return; + } + + this._dialogDone(userId); + } + + _dialogDone(userId) { + this._dialogClosedCallback({ userId }); + + this.setProperties({ + _errorMsg: null, + _username: '', + _password: '', + _dialogClosedCallback: null, + _opened: false, + }); + } + + _equals(a, b) { + return a === b; + } + + _openedChanged(ev) { + // Closed dialog by clicking on the overlay + // Check against dialogClosedCallback to make sure we didn't change + // programmatically + if (this._dialogClosedCallback && !ev.detail.value) { + this._dialogDone(); + } + } +} + +customElements.define('ha-dialog-add-user', HaDialogAddUser); diff --git a/src/panels/config/users/ha-user-editor.js b/src/panels/config/users/ha-user-editor.js new file mode 100644 index 0000000000..8c1f492829 --- /dev/null +++ b/src/panels/config/users/ha-user-editor.js @@ -0,0 +1,99 @@ +import '@polymer/paper-button/paper-button.js'; +import '@polymer/paper-card/paper-card.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import '../../../layouts/hass-subpage.js'; +import LocalizeMixin from '../../../mixins/localize-mixin.js'; +import NavigateMixin from '../../../mixins/navigate-mixin.js'; +import EventsMixin from '../../../mixins/events-mixin.js'; + + +/* + * @appliesMixin LocalizeMixin + * @appliesMixin NavigateMixin + * @appliesMixin EventsMixin + */ +class HaUserEditor extends EventsMixin(NavigateMixin(LocalizeMixin(PolymerElement))) { + static get template() { + return html` + + + + + + + ID + [[user.id]] + + + Owner + [[user.is_owner]] + + + Active + [[user.is_active]] + + + System generated + [[user.system_generated]] + + + + + + + [[localize('ui.panel.config.users.editor.delete_user')]] + + + + +`; + } + + static get properties() { + return { + hass: Object, + user: Object, + }; + } + + _computeName(user) { + return user && (user.name || 'Unnamed user'); + } + + async _deleteUser(ev) { + if (!confirm(`Are you sure you want to delete ${this._computeName(this.user)}`)) { + ev.target.blur(); + return; + } + try { + await this.hass.callWS({ + type: 'config/auth/delete', + user_id: this.user.id, + }); + } catch (err) { + alert(err.code); + return; + } + this.fire('reload-users'); + this.navigate('/config/users'); + } +} + +customElements.define('ha-user-editor', HaUserEditor); diff --git a/src/panels/config/users/ha-user-picker.js b/src/panels/config/users/ha-user-picker.js new file mode 100644 index 0000000000..88233b85c1 --- /dev/null +++ b/src/panels/config/users/ha-user-picker.js @@ -0,0 +1,110 @@ +import '@polymer/paper-fab/paper-fab.js'; +import '@polymer/paper-item/paper-item.js'; +import '@polymer/paper-card/paper-card.js'; +import '@polymer/paper-item/paper-item-body.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import '../../../layouts/hass-subpage.js'; + +import LocalizeMixin from '../../../mixins/localize-mixin.js'; +import NavigateMixin from '../../../mixins/navigate-mixin.js'; +import EventsMixin from '../../../mixins/events-mixin.js'; + +let registeredDialog = false; + +/* + * @appliesMixin LocalizeMixin + * @appliesMixin NavigateMixin + * @appliesMixin EventsMixin + */ +class HaUserPicker extends EventsMixin(NavigateMixin(LocalizeMixin(PolymerElement))) { + static get template() { + return html` + + + + + + + + + [[_withDefault(user.name, 'Unnamed User')]] + [[user.id]] + + + + + + + + + +`; + } + + static get properties() { + return { + hass: Object, + users: Array, + + }; + } + + connectedCallback() { + super.connectedCallback(); + + if (!registeredDialog) { + registeredDialog = true; + this.fire('register-dialog', { + dialogShowEvent: 'show-add-user', + dialogTag: 'ha-dialog-add-user', + dialogImport: () => import('./ha-dialog-add-user.js'), + }); + } + } + + _withDefault(value, defaultValue) { + return value || defaultValue; + } + + _computeUrl(user) { + return `/config/users/${user.id}`; + } + + _addUser() { + this.fire('show-add-user', { + hass: this.hass, + dialogClosedCallback: async ({ userId }) => { + this.fire('reload-users'); + if (userId) this.navigate(`/config/users/${userId}`); + }, + }); + } +} + +customElements.define('ha-user-picker', HaUserPicker); diff --git a/src/translations/en.json b/src/translations/en.json index 58cf032a7d..0cffb0e0f2 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -672,6 +672,20 @@ "caption": "Script", "description": "Create and edit scripts" }, + "users": { + "caption": "Users", + "description": "Manage users", + "picker": { + "title": "Users" + }, + "editor": { + "rename_user": "Rename user", + "change_password": "Change password", + "activate_user": "Activate user", + "deactivate_user": "Deactivate user", + "delete_user": "Delete user" + } + }, "zwave": { "caption": "Z-Wave", "description": "Manage your Z-Wave network"