Allow managing users (#1436)

* Allow managing users

* Address comments

* table -> card-content

* Don't close dialog on error
This commit is contained in:
Paulus Schoutsen 2018-07-13 15:31:22 +02:00 committed by GitHub
parent c11cf53f96
commit ed9c73429f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 580 additions and 2 deletions

View File

@ -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 = () => {

View File

@ -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) {
<span slot="header">[[localize('ui.panel.config.header')]]</span>
<span slot="introduction">[[localize('ui.panel.config.introduction')]]</span>
<template is="dom-if" if="[[computeIsLoaded(hass, &quot;cloud&quot;)]]">
<template is="dom-if" if="[[computeIsLoaded(hass, 'cloud')]]">
<ha-config-cloud-menu hass="[[hass]]" account="[[account]]"></ha-config-cloud-menu>
</template>
<template is="dom-if" if="[[computeIsLoaded(hass, &quot;config.config_entries&quot;)]]">
<template is="dom-if" if="[[computeIsLoaded(hass, 'config.config_entries')]]">
<ha-config-entries-menu hass="[[hass]]"></ha-config-entries-menu>
</template>
<template is="dom-if" if="[[computeIsLoaded(hass, 'config.auth_provider_homeassistant')]]">
<ha-config-users-menu hass="[[hass]]"></ha-config-users-menu>
</template>
<ha-config-navigation hass="[[hass]]"></ha-config-navigation>
</ha-config-section>
</div>

View File

@ -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`
<style include="iron-flex">
paper-card {
display: block;
}
a {
color: var(--primary-text-color);
}
</style>
<paper-card>
<a href='/config/users'>
<paper-item>
<paper-item-body two-line>
[[localize('ui.panel.config.users.caption')]]
<div secondary>
[[localize('ui.panel.config.users.description')]]
</div>
</paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon>
</paper-item>
</a>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
};
}
}
customElements.define('ha-config-users-menu', HaConfigUsersMenu);

View File

@ -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]]'
></ha-config-entries>
</template>
<template is="dom-if" if='[[_equals(_routeData.page, "users")]]' restamp>
<ha-config-users
page-name='users'
route='[[route]]'
hass='[[hass]]'
></ha-config-users>
</template>
`;
}

View File

@ -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`
<app-route
route='[[route]]'
pattern='/users/:user'
data="{{_routeData}}"
></app-route>
<template is='dom-if' if='[[_equals(_routeData.user, "picker")]]'>
<ha-user-picker
hass='[[hass]]'
users='[[_users]]'
></ha-user-picker>
</template>
<template is='dom-if' if='[[!_equals(_routeData.user, "picker")]]' restamp>
<ha-user-editor
hass='[[hass]]'
user='[[_computeUser(_users, _routeData.user)]]'
></ha-user-editor>
</template>
`;
}
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);

View File

@ -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`
<style include="ha-style-dialog">
.error {
color: red;
}
paper-dialog {
max-width: 500px;
}
.username {
margin-top: -8px;
}
</style>
<paper-dialog id="dialog" with-backdrop opened="{{_opened}}" on-opened-changed="_openedChanged">
<h2>Add user</h2>
<div>
<template is="dom-if" if="[[_errorMsg]]">
<div class='error'>[[_errorMsg]]</div>
</template>
<paper-input
autofocus
class='username'
label='Username'
value='{{_username}}'
required
auto-validate
error-message='Required'
></paper-input>
<paper-input
label='Password'
type='password'
value='{{_password}}'
required
auto-validate
error-message='Required'
></paper-input>
</div>
<div class="buttons">
<template is="dom-if" if="[[_loading]]">
<div class='submit-spinner'><paper-spinner active></paper-spinner></div>
</template>
<template is="dom-if" if="[[!_loading]]">
<paper-button on-click="_createUser">Create</paper-button>
</template>
</div>
</paper-dialog>
`;
}
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);

View File

@ -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`
<style>
paper-card {
display: block;
max-width: 600px;
margin: 0 auto 16px;
}
paper-card:first-child {
margin-top: 16px;
}
paper-card:last-child {
margin-bottom: 16px;
}
paper-button {
display: block;
}
</style>
<hass-subpage header="View user">
<paper-card heading="[[_computeName(user)]]">
<table class='card-content'>
<tr>
<td>ID</td>
<td>[[user.id]]</td>
</tr>
<tr>
<td>Owner</td>
<td>[[user.is_owner]]</td>
</tr>
<tr>
<td>Active</td>
<td>[[user.is_active]]</td>
</tr>
<tr>
<td>System generated</td>
<td>[[user.system_generated]]</td>
</tr>
</table>
</paper-card>
<paper-card>
<div class='card-actions'>
<paper-button on-click='_deleteUser'>
[[localize('ui.panel.config.users.editor.delete_user')]]
</paper-button>
</div>
</paper-card>
</hass-subpage>
`;
}
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);

View File

@ -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`
<style>
paper-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
paper-fab[is-wide] {
bottom: 24px;
right: 24px;
}
paper-card {
display: block;
max-width: 600px;
margin: 16px auto;
}
a {
color: var(--primary-text-color);
}
</style>
<hass-subpage header="[[localize('ui.panel.config.users.picker.title')]]">
<paper-card>
<template is="dom-repeat" items="[[users]]" as="user">
<a href='[[_computeUrl(user)]]'>
<paper-item>
<paper-item-body two-line>
<div>[[_withDefault(user.name, 'Unnamed User')]]</div>
<div secondary="">[[user.id]]</div>
</paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon>
</paper-item>
</a>
</template>
</paper-card>
<paper-fab
is-wide$="[[isWide]]"
icon="hass:plus"
title="[[localize('ui.panel.config.users.picker.add_user')]]"
on-click="_addUser"
></paper-fab>
</hass-subpage>
`;
}
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);

View File

@ -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"