Update user config pages (#5354)

* Update user config pages

* remove user-editor

* Update

* Update dialog-add-user.ts
This commit is contained in:
Bram Kragten 2020-03-30 20:52:53 +02:00 committed by GitHub
parent 8a6bd04543
commit 20cc9c9b42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 712 additions and 741 deletions

View File

@ -25,3 +25,16 @@ export const fetchAuthProviders = () =>
fetch("/auth/providers", { fetch("/auth/providers", {
credentials: "same-origin", credentials: "same-origin",
}); });
export const createAuthForUser = async (
hass: HomeAssistant,
userId: string,
username: string,
password: string
) =>
hass.callWS({
type: "config/auth_provider/homeassistant/create",
user_id: userId,
username,
password,
});

View File

@ -5,6 +5,8 @@ export const SYSTEM_GROUP_ID_ADMIN = "system-admin";
export const SYSTEM_GROUP_ID_USER = "system-users"; export const SYSTEM_GROUP_ID_USER = "system-users";
export const SYSTEM_GROUP_ID_READ_ONLY = "system-read-only"; export const SYSTEM_GROUP_ID_READ_ONLY = "system-read-only";
export const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN];
export interface User { export interface User {
id: string; id: string;
name: string; name: string;
@ -15,7 +17,7 @@ export interface User {
credentials: Credential[]; credentials: Credential[];
} }
interface UpdateUserParams { export interface UpdateUserParams {
name?: User["name"]; name?: User["name"];
group_ids?: User["group_ids"]; group_ids?: User["group_ids"];
} }
@ -25,10 +27,16 @@ export const fetchUsers = async (hass: HomeAssistant) =>
type: "config/auth/list", type: "config/auth/list",
}); });
export const createUser = async (hass: HomeAssistant, name: string) => export const createUser = async (
hass: HomeAssistant,
name: string,
// tslint:disable-next-line: variable-name
group_ids?: User["group_ids"]
) =>
hass.callWS<{ user: User }>({ hass.callWS<{ user: User }>({
type: "config/auth/create", type: "config/auth/create",
name, name,
group_ids,
}); });
export const updateUser = async ( export const updateUser = async (

View File

@ -0,0 +1,242 @@
import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner";
import "../../../components/ha-dialog";
import "../../../resources/ha-style";
import {
LitElement,
html,
TemplateResult,
customElement,
property,
PropertyValues,
} from "lit-element";
import { HomeAssistant } from "../../../types";
import { PolymerChangedEvent } from "../../../polymer-types";
import { AddUserDialogParams } from "./show-dialog-add-user";
import {
User,
SYSTEM_GROUP_ID_USER,
GROUPS,
createUser,
deleteUser,
} from "../../../data/user";
import { createAuthForUser } from "../../../data/auth";
@customElement("dialog-add-user")
export class DialogAddUser extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _loading = false;
// Error message when can't talk to server etc
@property() private _error?: string;
@property() private _params?: AddUserDialogParams;
@property() private _name?: string;
@property() private _username?: string;
@property() private _password?: string;
@property() private _group?: string;
public showDialog(params: AddUserDialogParams) {
this._params = params;
this._name = "";
this._username = "";
this._password = "";
this._group = SYSTEM_GROUP_ID_USER;
this._error = undefined;
this._loading = false;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._createUser(ev);
}
});
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
return html`
<ha-dialog
open
@closing=${this._close}
scrimClickAction
escapeKeyAction
.heading=${this.hass.localize("ui.panel.config.users.add_user.caption")}
>
<div>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<paper-input
class="name"
.label=${this.hass.localize("ui.panel.config.users.add_user.name")}
.value=${this._name}
required
auto-validate
autocapitalize="on"
error-message="Required"
@value-changed=${this._nameChanged}
@blur=${this._maybePopulateUsername}
></paper-input>
<paper-input
class="username"
.label=${this.hass.localize(
"ui.panel.config.users.add_user.username"
)}
.value=${this._username}
required
auto-validate
autocapitalize="none"
@value-changed=${this._usernameChanged}
error-message="Required"
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.users.add_user.password"
)}
type="password"
.value=${this._password}
required
auto-validate
@value-changed=${this._passwordChanged}
error-message="Required"
></paper-input>
<ha-paper-dropdown-menu
.label=${this.hass.localize("ui.panel.config.users.editor.group")}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._group}
@iron-select=${this._handleGroupChange}
attr-for-selected="group-id"
>
${GROUPS.map(
(groupId) => html`
<paper-item group-id=${groupId}>
${this.hass.localize(`groups.${groupId}`)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
${this._group === SYSTEM_GROUP_ID_USER
? html`
<br />
The users group is a work in progress. The user will be unable
to administer the instance via the UI. We're still auditing all
management API endpoints to ensure that they correctly limit
access to administrators.
`
: ""}
</div>
<mwc-button
slot="secondaryAction"
@click="${this._close}"
.disabled=${this._loading}
>
${this.hass!.localize("ui.common.cancel")}
</mwc-button>
${this._loading
? html`
<div slot="primaryAction" class="submit-spinner">
<paper-spinner active></paper-spinner>
</div>
`
: html`
<mwc-button
slot="primaryAction"
.disabled=${!this._name || !this._username || !this._password}
@click=${this._createUser}
>
${this.hass.localize("ui.panel.config.users.add_user.create")}
</mwc-button>
`}
</ha-dialog>
`;
}
private _close() {
this._params = undefined;
}
private _maybePopulateUsername() {
if (this._username || !this._name) {
return;
}
const parts = this._name.split(" ");
if (parts.length) {
this._username = parts[0].toLowerCase();
}
}
private _nameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._name = ev.detail.value;
}
private _usernameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._username = ev.detail.value;
}
private _passwordChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._password = ev.detail.value;
}
private async _handleGroupChange(ev): Promise<void> {
this._group = ev.detail.item.getAttribute("group-id");
}
private async _createUser(ev) {
ev.preventDefault();
if (!this._name || !this._username || !this._password) {
return;
}
this._loading = true;
this._error = "";
let user: User;
try {
const userResponse = await createUser(this.hass, this._name, [
this._group!,
]);
user = userResponse.user;
} catch (err) {
this._loading = false;
this._error = err.code;
return;
}
try {
await createAuthForUser(
this.hass,
user.id,
this._username,
this._password
);
} catch (err) {
await deleteUser(this.hass, user.id);
this._loading = false;
this._error = err.code;
return;
}
this._params!.userAddedCallback(user);
this._close();
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-add-user": DialogAddUser;
}
}

View File

@ -0,0 +1,209 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-tooltip/paper-tooltip";
import {
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
css,
} from "lit-element";
import "../../../components/entity/ha-entities-picker";
import "../../../components/user/ha-user-picker";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { UserDetailDialogParams } from "./show-dialog-user-detail";
import { createCloseHeading } from "../../../components/ha-dialog";
import { GROUPS, SYSTEM_GROUP_ID_USER } from "../../../data/user";
@customElement("dialog-user-detail")
class DialogUserDetail extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _name!: string;
@property() private _group?: string;
@property() private _error?: string;
@property() private _params?: UserDetailDialogParams;
@property() private _submitting: boolean = false;
public async showDialog(params: UserDetailDialogParams): Promise<void> {
this._params = params;
this._error = undefined;
this._name = params.entry.name || "";
this._group = params.entry.group_ids[0];
await this.updateComplete;
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
const user = this._params.entry;
return html`
<ha-dialog
open
@closing=${this._close}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(this.hass, user.name)}
>
<div>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<div class="form">
<paper-input
.value=${this._name}
@value-changed=${this._nameChanged}
label="${this.hass!.localize("ui.panel.config.user.editor.name")}"
></paper-input>
<ha-paper-dropdown-menu
.label=${this.hass.localize("ui.panel.config.users.editor.group")}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._group}
@iron-select=${this._handleGroupChange}
attr-for-selected="group-id"
>
${GROUPS.map(
(groupId) => html`
<paper-item group-id=${groupId}>
${this.hass.localize(`groups.${groupId}`)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
${this._group === SYSTEM_GROUP_ID_USER
? html`
<br />
The users group is a work in progress. The user will be unable
to administer the instance via the UI. We're still auditing
all management API endpoints to ensure that they correctly
limit access to administrators.
`
: ""}
<table>
<tr>
<td>
${this.hass.localize("ui.panel.config.users.editor.id")}
</td>
<td>${user.id}</td>
</tr>
<tr>
<td>
${this.hass.localize("ui.panel.config.users.editor.owner")}
</td>
<td>${user.is_owner}</td>
</tr>
<tr>
<td>
${this.hass.localize("ui.panel.config.users.editor.active")}
</td>
<td>${user.is_active}</td>
</tr>
<tr>
<td>
${this.hass.localize(
"ui.panel.config.users.editor.system_generated"
)}
</td>
<td>${user.system_generated}</td>
</tr>
</table>
</div>
</div>
<div slot="secondaryAction">
<mwc-button
class="warning"
@click=${this._deleteEntry}
.disabled=${this._submitting || user.system_generated}
>
${this.hass!.localize("ui.panel.config.users.editor.delete_user")}
</mwc-button>
${user.system_generated
? html`
<paper-tooltip position="right"
>${this.hass.localize(
"ui.panel.config.users.editor.system_generated_users_not_removable"
)}</paper-tooltip
>
`
: ""}
</div>
<mwc-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!this._name}
>
${this.hass!.localize("ui.panel.config.users.editor.update_user")}
</mwc-button>
</ha-dialog>
`;
}
private _nameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._name = ev.detail.value;
}
private async _handleGroupChange(ev): Promise<void> {
this._group = ev.detail.item.getAttribute("group-id");
}
private async _updateEntry() {
this._submitting = true;
try {
await this._params!.updateEntry({
name: this._name.trim(),
group_ids: [this._group!],
});
this._close();
} catch (err) {
this._error = err?.message || "Unknown error";
} finally {
this._submitting = false;
}
}
private async _deleteEntry() {
this._submitting = true;
try {
if (await this._params!.removeEntry()) {
this._params = undefined;
}
} finally {
this._submitting = false;
}
}
private _close(): void {
this._params = undefined;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-min-width: 500px;
}
table {
width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-user-detail": DialogUserDetail;
}
}

View File

@ -1,166 +0,0 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../layouts/hass-tabs-subpage";
import "../../../components/ha-icon-next";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import LocalizeMixin from "../../../mixins/localize-mixin";
import NavigateMixin from "../../../mixins/navigate-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import { computeRTL } from "../../../common/util/compute_rtl";
import { configSections } from "../ha-panel-config";
let registeredDialog = false;
/*
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
* @appliesMixin EventsMixin
*/
class HaUserPicker extends EventsMixin(
NavigateMixin(LocalizeMixin(PolymerElement))
) {
static get template() {
return html`
<style>
ha-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
ha-fab[is-wide] {
bottom: 24px;
right: 24px;
}
ha-fab[rtl] {
right: auto;
left: 16px;
}
ha-fab[narrow] {
bottom: 84px;
}
ha-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
ha-card {
max-width: 600px;
margin: 16px auto;
overflow: hidden;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
</style>
<hass-tabs-subpage
hass="[[hass]]"
narrow="[[narrow]]"
route="[[route]]"
back-path="/config"
tabs="[[_computeTabs()]]"
>
<ha-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="">
[[_computeGroup(localize, user)]]
<template is="dom-if" if="[[user.system_generated]]">
-
[[localize('ui.panel.config.users.picker.system_generated')]]
</template>
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
</template>
</ha-card>
<ha-fab
is-wide$="[[isWide]]"
narrow$="[[narrow]]"
icon="hass:plus"
title="[[localize('ui.panel.config.users.picker.add_user')]]"
on-click="_addUser"
rtl$="[[rtl]]"
></ha-fab>
</hass-tabs-subpage>
`;
}
static get properties() {
return {
hass: Object,
users: Array,
isWide: Boolean,
narrow: Boolean,
route: Object,
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
connectedCallback() {
super.connectedCallback();
if (!registeredDialog) {
registeredDialog = true;
this.fire("register-dialog", {
dialogShowEvent: "show-add-user",
dialogTag: "ha-dialog-add-user",
dialogImport: () =>
import(
/* webpackChunkName: "ha-dialog-add-user" */ "./ha-dialog-add-user"
),
});
}
}
_withDefault(value, defaultValue) {
return value || defaultValue;
}
_computeUrl(user) {
return `/config/users/${user.id}`;
}
_computeGroup(localize, user) {
return localize(`groups.${user.group_ids[0]}`);
}
_computeRTL(hass) {
return computeRTL(hass);
}
_computeTabs() {
return configSections.persons;
}
_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-config-user-picker", HaUserPicker);

View File

@ -1,109 +0,0 @@
import "@polymer/app-route/app-route";
import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import NavigateMixin from "../../../mixins/navigate-mixin";
import "./ha-config-user-picker";
import "./ha-user-editor";
import { fireEvent } from "../../../common/dom/fire_event";
import { fetchUsers } from "../../../data/user";
/*
* @appliesMixin NavigateMixin
*/
class HaConfigUsers extends NavigateMixin(PolymerElement) {
static get template() {
return html`
<app-route
route="[[route]]"
pattern="/:user"
data="{{_routeData}}"
></app-route>
<template is="dom-if" if='[[_equals(_routeData.user, "picker")]]'>
<ha-config-user-picker
hass="[[hass]]"
users="[[_users]]"
is-wide="[[isWide]]"
narrow="[[narrow]]"
route="[[route]]"
></ha-config-user-picker>
</template>
<template
is="dom-if"
if='[[!_equals(_routeData.user, "picker")]]'
restamp
>
<ha-user-editor
hass="[[hass]]"
user="[[_computeUser(_users, _routeData.user)]]"
narrow="[[narrow]]"
route="[[route]]"
></ha-user-editor>
</template>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
narrow: Boolean,
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) {
// prevent list getting under toolbar
fireEvent(this, "iron-resize");
this._debouncer = Debouncer.debounce(
this._debouncer,
timeOut.after(0),
() => {
if (route.path === "") {
this.navigate(`${route.prefix}/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 fetchUsers(this.hass);
}
}
customElements.define("ha-config-users", HaConfigUsers);

View File

@ -0,0 +1,192 @@
import "../../../layouts/hass-tabs-subpage-data-table";
import "../../../components/ha-fab";
import { computeRTL } from "../../../common/util/compute_rtl";
import { configSections } from "../ha-panel-config";
import {
LitElement,
property,
css,
PropertyValues,
customElement,
} from "lit-element";
import { HomeAssistant, Route } from "../../../types";
import { html } from "lit-html";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { User, fetchUsers, updateUser, deleteUser } from "../../../data/user";
import memoizeOne from "memoize-one";
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
import { showUserDetailDialog } from "./show-dialog-user-detail";
import { showAddUserDialog } from "./show-dialog-add-user";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
@customElement("ha-config-users")
export class HaConfigUsers extends LitElement {
@property() public hass!: HomeAssistant;
@property() public _users: User[] = [];
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public route!: Route;
private _columns = memoizeOne(
(_language): DataTableColumnContainer => {
return {
name: {
title: this.hass.localize(
"ui.panel.config.users.picker.headers.name"
),
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
${name ||
this.hass!.localize("ui.panel.config.users.editor.unnamed_user")}
`,
},
group_ids: {
title: this.hass.localize(
"ui.panel.config.users.picker.headers.group"
),
sortable: true,
filterable: true,
width: "25%",
template: (groupIds) => html`
${this.hass.localize(`groups.${groupIds[0]}`)}
`,
},
system_generated: {
title: this.hass.localize(
"ui.panel.config.users.picker.headers.system"
),
type: "icon",
width: "10%",
sortable: true,
filterable: true,
template: (generated) => html`
${generated
? html`
<ha-icon icon="hass:check-circle-outline"></ha-icon>
`
: ""}
`,
},
};
}
);
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this._fetchUsers();
}
protected render() {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
backPath="/config"
.tabs=${configSections.persons}
.columns=${this._columns(this.hass.language)}
.data=${this._users}
@row-click=${this._editUser}
>
</hass-tabs-subpage-data-table>
<ha-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
icon="hass:plus"
.title=${this.hass.localize("ui.panel.config.users.picker.add_user")}
@click=${this._addUser}
?rtl=${computeRTL(this.hass)}
></ha-fab>
`;
}
private async _fetchUsers() {
this._users = await fetchUsers(this.hass);
}
private _editUser(ev: HASSDomEvent<RowClickedEvent>) {
const id = ev.detail.id;
const entry = this._users.find((user) => user.id === id);
if (!entry) {
return;
}
showUserDetailDialog(this, {
entry,
updateEntry: async (values) => {
const updated = await updateUser(this.hass!, entry!.id, values);
this._users = this._users!.map((ent) =>
ent === entry ? updated.user : ent
);
},
removeEntry: async () => {
if (
!(await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.users.editor.confirm_user_deletion",
"name",
entry.name
),
dismissText: this.hass!.localize("ui.common.no"),
confirmText: this.hass!.localize("ui.common.yes"),
}))
) {
return false;
}
try {
await deleteUser(this.hass!, entry!.id);
this._users = this._users!.filter((ent) => ent !== entry);
return true;
} catch (err) {
return false;
}
},
});
}
private _addUser() {
showAddUserDialog(this, {
userAddedCallback: async (user: User) => {
if (user) {
this._users = { ...this._users, ...user };
}
},
});
}
static get styles() {
return css`
ha-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
ha-fab[is-wide] {
bottom: 24px;
right: 24px;
}
ha-fab[rtl] {
right: auto;
left: 16px;
}
ha-fab[narrow] {
bottom: 84px;
}
ha-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
`;
}
}

View File

@ -1,201 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/dialog/ha-paper-dialog";
import "../../../resources/ha-style";
import LocalizeMixin from "../../../mixins/localize-mixin";
/*
* @appliesMixin LocalizeMixin
*/
class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style-dialog">
.error {
color: red;
}
ha-paper-dialog {
max-width: 500px;
}
.username {
margin-top: -8px;
}
</style>
<ha-paper-dialog
id="dialog"
with-backdrop
opened="{{_opened}}"
on-opened-changed="_openedChanged"
>
<h2>[[localize('ui.panel.config.users.add_user.caption')]]</h2>
<div>
<template is="dom-if" if="[[_errorMsg]]">
<div class="error">[[_errorMsg]]</div>
</template>
<paper-input
class="name"
label="[[localize('ui.panel.config.users.add_user.name')]]"
value="{{_name}}"
required
auto-validate
autocapitalize="on"
error-message="Required"
on-blur="_maybePopulateUsername"
></paper-input>
<paper-input
class="username"
label="[[localize('ui.panel.config.users.add_user.username')]]"
value="{{_username}}"
required
auto-validate
autocapitalize="none"
error-message="Required"
></paper-input>
<paper-input
label="[[localize('ui.panel.config.users.add_user.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]]">
<mwc-button on-click="_createUser"
>[[localize('ui.panel.config.users.add_user.create')]]</mwc-button
>
</template>
</div>
</ha-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,
},
_name: String,
_username: String,
_password: String,
};
}
ready() {
super.ready();
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._createUser(ev);
}
});
}
showDialog({ hass, dialogClosedCallback }) {
this.hass = hass;
this._dialogClosedCallback = dialogClosedCallback;
this._loading = false;
this._opened = true;
setTimeout(() => this.shadowRoot.querySelector("paper-input").focus(), 0);
}
_maybePopulateUsername() {
if (this._username) return;
const parts = this._name.split(" ");
if (parts.length) {
this._username = parts[0].toLowerCase();
}
}
async _createUser(ev) {
ev.preventDefault();
if (!this._name || !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._name,
});
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

@ -1,257 +0,0 @@
import {
LitElement,
TemplateResult,
html,
customElement,
CSSResultArray,
css,
property,
} from "lit-element";
import { until } from "lit-html/directives/until";
import "@material/mwc-button";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import "../../../components/ha-card";
import { HomeAssistant, Route } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import {
User,
deleteUser,
updateUser,
SYSTEM_GROUP_ID_USER,
SYSTEM_GROUP_ID_ADMIN,
} from "../../../data/user";
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { configSections } from "../ha-panel-config";
declare global {
interface HASSDomEvents {
"reload-users": undefined;
}
}
const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN];
@customElement("ha-user-editor")
class HaUserEditor extends LitElement {
@property() public hass?: HomeAssistant;
@property() public user?: User;
@property() public narrow?: boolean;
@property() public route!: Route;
protected render(): TemplateResult {
const hass = this.hass;
const user = this.user;
if (!hass || !user) {
return html``;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configSections.persons}
>
<ha-card .header=${this._name}>
<table class="card-content">
<tr>
<td>${hass.localize("ui.panel.config.users.editor.id")}</td>
<td>${user.id}</td>
</tr>
<tr>
<td>${hass.localize("ui.panel.config.users.editor.owner")}</td>
<td>${user.is_owner}</td>
</tr>
<tr>
<td>${hass.localize("ui.panel.config.users.editor.group")}</td>
<td>
<select
@change=${this._handleGroupChange}
.value=${until(
this.updateComplete.then(() => user.group_ids[0])
)}
>
${GROUPS.map(
(groupId) => html`
<option value=${groupId}>
${hass.localize(`groups.${groupId}`)}
</option>
`
)}
</select>
</td>
</tr>
${user.group_ids[0] === SYSTEM_GROUP_ID_USER
? html`
<tr>
<td colspan="2" class="user-experiment">
The users group is a work in progress. The user will be
unable to administer the instance via the UI. We're still
auditing all management API endpoints to ensure that they
correctly limit access to administrators.
</td>
</tr>
`
: ""}
<tr>
<td>${hass.localize("ui.panel.config.users.editor.active")}</td>
<td>${user.is_active}</td>
</tr>
<tr>
<td>
${hass.localize(
"ui.panel.config.users.editor.system_generated"
)}
</td>
<td>${user.system_generated}</td>
</tr>
</table>
<div class="card-actions">
<mwc-button @click=${this._handlePromptRenameUser}>
${hass.localize("ui.panel.config.users.editor.rename_user")}
</mwc-button>
<mwc-button
class="warning"
@click=${this._promptDeleteUser}
.disabled=${user.system_generated}
>
${hass.localize("ui.panel.config.users.editor.delete_user")}
</mwc-button>
${user.system_generated
? html`
${hass.localize(
"ui.panel.config.users.editor.system_generated_users_not_removable"
)}
`
: ""}
</div>
</ha-card>
</hass-tabs-subpage>
`;
}
private get _name() {
return (
this.user &&
(this.user.name ||
this.hass!.localize("ui.panel.config.users.editor.unnamed_user"))
);
}
private async _handleRenameUser(newName?: string) {
if (newName === null || newName === this.user!.name) {
return;
}
try {
await updateUser(this.hass!, this.user!.id, {
name: newName,
});
fireEvent(this, "reload-users");
} catch (err) {
showAlertDialog(this, {
text: `${this.hass!.localize(
"ui.panel.config.users.editor.user_rename_failed"
)} ${err.message}`,
});
}
}
private async _handlePromptRenameUser(ev): Promise<void> {
ev.currentTarget.blur();
showPromptDialog(this, {
title: this.hass!.localize("ui.panel.config.users.editor.enter_new_name"),
defaultValue: this.user!.name,
inputLabel: this.hass!.localize("ui.panel.config.users.add_user.name"),
confirm: (text) => this._handleRenameUser(text),
});
}
private async _handleGroupChange(ev): Promise<void> {
const selectEl = ev.currentTarget as HTMLSelectElement;
const newGroup = selectEl.value;
try {
await updateUser(this.hass!, this.user!.id, {
group_ids: [newGroup],
});
showSaveSuccessToast(this, this.hass!);
fireEvent(this, "reload-users");
} catch (err) {
showAlertDialog(this, {
text: `${this.hass!.localize(
"ui.panel.config.users.editor.group_update_failed"
)} ${err.message}`,
});
selectEl.value = this.user!.group_ids[0];
}
}
private async _deleteUser() {
try {
await deleteUser(this.hass!, this.user!.id);
} catch (err) {
showAlertDialog(this, {
text: err.code,
});
return;
}
fireEvent(this, "reload-users");
navigate(this, "/config/users");
}
private async _promptDeleteUser(_ev): Promise<void> {
showConfirmationDialog(this, {
text: this.hass!.localize(
"ui.panel.config.users.editor.confirm_user_deletion",
"name",
this._name
),
confirm: () => this._deleteUser(),
});
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
ha-card {
max-width: 600px;
margin: 16px auto 16px;
}
hass-subpage ha-card:first-of-type {
direction: ltr;
}
table {
width: 100%;
}
td {
vertical-align: top;
}
.user-experiment {
padding: 8px 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-user-editor": HaUserEditor;
}
}

View File

@ -0,0 +1,20 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { User } from "../../../data/user";
export interface AddUserDialogParams {
userAddedCallback: (user: User) => void;
}
export const loadAddUserDialog = () =>
import(/* webpackChunkName: "add-user-dialog" */ "./dialog-add-user");
export const showAddUserDialog = (
element: HTMLElement,
dialogParams: AddUserDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-add-user",
dialogImport: loadAddUserDialog,
dialogParams,
});
};

View File

@ -0,0 +1,22 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { User, UpdateUserParams } from "../../../data/user";
export interface UserDetailDialogParams {
entry: User;
updateEntry: (updates: Partial<UpdateUserParams>) => Promise<unknown>;
removeEntry: () => Promise<boolean>;
}
export const loadUserDetailDialog = () =>
import(/* webpackChunkName: "user-detail-dialog" */ "./dialog-user-detail");
export const showUserDetailDialog = (
element: HTMLElement,
detailParams: UserDetailDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-user-detail",
dialogImport: loadUserDetailDialog,
dialogParams: detailParams,
});
};

View File

@ -1229,6 +1229,7 @@
"learn_more": "Learn more about scripts", "learn_more": "Learn more about scripts",
"no_scripts": "We couldnt find any editable scripts", "no_scripts": "We couldnt find any editable scripts",
"add_script": "Add script", "add_script": "Add script",
"show_info": "Show info about script",
"trigger_script": "Trigger script", "trigger_script": "Trigger script",
"edit_script": "Edit script", "edit_script": "Edit script",
"headers": { "headers": {
@ -1664,16 +1665,16 @@
"caption": "Users", "caption": "Users",
"description": "Manage users", "description": "Manage users",
"picker": { "picker": {
"title": "Users", "headers": { "name": "Name", "group": "Group", "system": "System" }
"system_generated": "System generated"
}, },
"editor": { "editor": {
"caption": "View user", "caption": "View user",
"rename_user": "Rename user", "name": "Name",
"change_password": "Change password", "change_password": "Change password",
"activate_user": "Activate user", "activate_user": "Activate user",
"deactivate_user": "Deactivate user", "deactivate_user": "Deactivate user",
"delete_user": "Delete user", "delete_user": "Delete user",
"update_user": "Update",
"id": "ID", "id": "ID",
"owner": "Owner", "owner": "Owner",
"group": "Group", "group": "Group",
@ -1681,9 +1682,6 @@
"system_generated": "System generated", "system_generated": "System generated",
"system_generated_users_not_removable": "Unable to remove system generated users.", "system_generated_users_not_removable": "Unable to remove system generated users.",
"unnamed_user": "Unnamed User", "unnamed_user": "Unnamed User",
"enter_new_name": "Enter new name",
"user_rename_failed": "User rename failed:",
"group_update_failed": "Group update failed:",
"confirm_user_deletion": "Are you sure you want to delete {name}?" "confirm_user_deletion": "Are you sure you want to delete {name}?"
}, },
"add_user": { "add_user": {