mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-08 09:56:36 +00:00
Allow managing users (#1436)
* Allow managing users * Address comments * table -> card-content * Don't close dialog on error
This commit is contained in:
parent
c11cf53f96
commit
ed9c73429f
@ -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 = () => {
|
||||
|
@ -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, "cloud")]]">
|
||||
<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, "config.config_entries")]]">
|
||||
<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>
|
||||
|
47
src/panels/config/dashboard/ha-config-users-menu.js
Normal file
47
src/panels/config/dashboard/ha-config-users-menu.js
Normal 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);
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
100
src/panels/config/users/ha-config-users.js
Normal file
100
src/panels/config/users/ha-config-users.js
Normal 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);
|
169
src/panels/config/users/ha-dialog-add-user.js
Normal file
169
src/panels/config/users/ha-dialog-add-user.js
Normal 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);
|
99
src/panels/config/users/ha-user-editor.js
Normal file
99
src/panels/config/users/ha-user-editor.js
Normal 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);
|
110
src/panels/config/users/ha-user-picker.js
Normal file
110
src/panels/config/users/ha-user-picker.js
Normal 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);
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user