mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
Update user config pages (#5354)
* Update user config pages * remove user-editor * Update * Update dialog-add-user.ts
This commit is contained in:
parent
8a6bd04543
commit
20cc9c9b42
@ -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,
|
||||||
|
});
|
||||||
|
@ -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 (
|
||||||
|
242
src/panels/config/users/dialog-add-user.ts
Normal file
242
src/panels/config/users/dialog-add-user.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
209
src/panels/config/users/dialog-user-detail.ts
Normal file
209
src/panels/config/users/dialog-user-detail.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
@ -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);
|
|
192
src/panels/config/users/ha-config-users.ts
Normal file
192
src/panels/config/users/ha-config-users.ts
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
20
src/panels/config/users/show-dialog-add-user.ts
Normal file
20
src/panels/config/users/show-dialog-add-user.ts
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
22
src/panels/config/users/show-dialog-user-detail.ts
Normal file
22
src/panels/config/users/show-dialog-user-detail.ts
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
@ -1229,6 +1229,7 @@
|
|||||||
"learn_more": "Learn more about scripts",
|
"learn_more": "Learn more about scripts",
|
||||||
"no_scripts": "We couldn’t find any editable scripts",
|
"no_scripts": "We couldn’t 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": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user