mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 17:26:42 +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", {
|
||||
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_READ_ONLY = "system-read-only";
|
||||
|
||||
export const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN];
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -15,7 +17,7 @@ export interface User {
|
||||
credentials: Credential[];
|
||||
}
|
||||
|
||||
interface UpdateUserParams {
|
||||
export interface UpdateUserParams {
|
||||
name?: User["name"];
|
||||
group_ids?: User["group_ids"];
|
||||
}
|
||||
@ -25,10 +27,16 @@ export const fetchUsers = async (hass: HomeAssistant) =>
|
||||
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 }>({
|
||||
type: "config/auth/create",
|
||||
name,
|
||||
group_ids,
|
||||
});
|
||||
|
||||
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",
|
||||
"no_scripts": "We couldn’t find any editable scripts",
|
||||
"add_script": "Add script",
|
||||
"show_info": "Show info about script",
|
||||
"trigger_script": "Trigger script",
|
||||
"edit_script": "Edit script",
|
||||
"headers": {
|
||||
@ -1664,16 +1665,16 @@
|
||||
"caption": "Users",
|
||||
"description": "Manage users",
|
||||
"picker": {
|
||||
"title": "Users",
|
||||
"system_generated": "System generated"
|
||||
"headers": { "name": "Name", "group": "Group", "system": "System" }
|
||||
},
|
||||
"editor": {
|
||||
"caption": "View user",
|
||||
"rename_user": "Rename user",
|
||||
"name": "Name",
|
||||
"change_password": "Change password",
|
||||
"activate_user": "Activate user",
|
||||
"deactivate_user": "Deactivate user",
|
||||
"delete_user": "Delete user",
|
||||
"update_user": "Update",
|
||||
"id": "ID",
|
||||
"owner": "Owner",
|
||||
"group": "Group",
|
||||
@ -1681,9 +1682,6 @@
|
||||
"system_generated": "System generated",
|
||||
"system_generated_users_not_removable": "Unable to remove system generated users.",
|
||||
"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}?"
|
||||
},
|
||||
"add_user": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user