Allow changing group (#2908)

* Allow changing group

* Styling + rename

* Fix type
This commit is contained in:
Paulus Schoutsen 2019-03-11 12:08:09 -07:00 committed by GitHub
parent 1890dd8683
commit 86548052e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 343 additions and 191 deletions

View File

@ -22,6 +22,7 @@ import { DEFAULT_PANEL } from "../common/const";
const computeUrl = (urlPath) => `/${urlPath}`;
const computePanels = (hass: HomeAssistant) => {
const isAdmin = hass.user.is_admin;
const panels = hass.panels;
const sortValue = {
map: 1,
@ -30,9 +31,9 @@ const computePanels = (hass: HomeAssistant) => {
};
const result: Panel[] = [];
Object.keys(panels).forEach((key) => {
if (panels[key].title) {
result.push(panels[key]);
Object.values(panels).forEach((panel) => {
if (panel.title && (panel.component_name !== "config" || isAdmin)) {
result.push(panel);
}
});
@ -129,6 +130,9 @@ class HaSidebar extends LitElement {
: html``}
</paper-listbox>
${!hass.user.is_admin
? ""
: html`
<div>
<div class="divider"></div>
@ -185,6 +189,7 @@ class HaSidebar extends LitElement {
</a>
</div>
</div>
`}
`;
}

View File

@ -8,7 +8,7 @@ import {
customElement,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { User } from "../../data/auth";
import { User } from "../../data/user";
import { CurrentUser } from "../../types";
const computeInitials = (name: string) => {

View File

@ -15,7 +15,7 @@ import {
} from "lit-element";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { User, fetchUsers } from "../../data/auth";
import { User, fetchUsers } from "../../data/user";
import compare from "../../common/string/compare";
class HaEntityPicker extends LitElement {

View File

@ -1,26 +1,9 @@
import { HomeAssistant } from "../types";
export interface AuthProvider {
name: string;
id: string;
type: string;
}
interface Credential {
export interface Credential {
type: string;
}
export interface User {
id: string;
name: string;
is_owner: boolean;
is_active: boolean;
system_generated: boolean;
group_ids: string[];
credentials: Credential[];
}
export const fetchUsers = async (hass: HomeAssistant) =>
hass.callWS<User[]>({
type: "config/auth/list",
});

49
src/data/user.ts Normal file
View File

@ -0,0 +1,49 @@
import { HomeAssistant } from "../types";
import { Credential } from "./auth";
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 interface User {
id: string;
name: string;
is_owner: boolean;
is_active: boolean;
system_generated: boolean;
group_ids: string[];
credentials: Credential[];
}
interface UpdateUserParams {
name?: User["name"];
group_ids?: User["group_ids"];
}
export const fetchUsers = async (hass: HomeAssistant) =>
hass.callWS<User[]>({
type: "config/auth/list",
});
export const createUser = async (hass: HomeAssistant, name: string) =>
hass.callWS<{ user: User }>({
type: "config/auth/create",
name,
});
export const updateUser = async (
hass: HomeAssistant,
userId: string,
params: UpdateUserParams
) =>
hass.callWS<{ user: User }>({
...params,
type: "config/auth/update",
user_id: userId,
});
export const deleteUser = async (hass: HomeAssistant, userId: string) =>
hass.callWS<void>({
type: "config/auth/delete",
user_id: userId,
});

View File

@ -130,6 +130,7 @@ export const provideHass = (
user: {
credentials: [],
id: "abcd",
is_admin: true,
is_owner: true,
mfa_modules: [],
name: "Demo User",

View File

@ -27,7 +27,7 @@ import {
showPersonDetailDialog,
loadPersonDetailDialog,
} from "./show-dialog-person-detail";
import { User, fetchUsers } from "../../../data/auth";
import { User, fetchUsers } from "../../../data/user";
class HaConfigPerson extends LitElement {
public hass?: HomeAssistant;

View File

@ -1,6 +1,6 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { Person, PersonMutableParams } from "../../../data/person";
import { User } from "../../../data/auth";
import { User } from "../../../data/user";
export interface PersonDetailDialogParams {
entry?: Person;

View File

@ -66,7 +66,7 @@ class HaUserPicker extends EventsMixin(
<paper-item-body two-line>
<div>[[_withDefault(user.name, 'Unnamed User')]]</div>
<div secondary="">
[[user.id]]
[[_computeGroup(localize, user)]]
<template is="dom-if" if="[[user.system_generated]]">
- System Generated
</template>
@ -124,6 +124,10 @@ class HaUserPicker extends EventsMixin(
return `/config/users/${user.id}`;
}
_computeGroup(localize, user) {
return localize(`groups.${user.group_ids[0]}`);
}
_computeRTL(hass) {
return computeRTL(hass);
}

View File

@ -9,7 +9,7 @@ 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/auth";
import { fetchUsers } from "../../../data/user";
/*
* @appliesMixin NavigateMixin

View File

@ -1,113 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../layouts/hass-subpage";
import LocalizeMixin from "../../../mixins/localize-mixin";
import NavigateMixin from "../../../mixins/navigate-mixin";
import EventsMixin from "../../../mixins/events-mixin";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
* @appliesMixin EventsMixin
*/
class HaUserEditor extends EventsMixin(
NavigateMixin(LocalizeMixin(PolymerElement))
) {
static get template() {
return html`
<style include="ha-style">
paper-card {
display: block;
max-width: 600px;
margin: 0 auto 16px;
}
paper-card:first-child {
margin-top: 16px;
}
paper-card:last-child {
margin-bottom: 16px;
}
hass-subpage paper-card:first-of-type {
direction: ltr;
}
</style>
<hass-subpage
header="[[localize('ui.panel.config.users.editor.caption')]]"
>
<paper-card heading="[[_computeName(user)]]">
<table class="card-content">
<tr>
<td>ID</td>
<td>[[user.id]]</td>
</tr>
<tr>
<td>Owner</td>
<td>[[user.is_owner]]</td>
</tr>
<tr>
<td>Active</td>
<td>[[user.is_active]]</td>
</tr>
<tr>
<td>System generated</td>
<td>[[user.system_generated]]</td>
</tr>
</table>
</paper-card>
<paper-card>
<div class="card-actions">
<mwc-button
class="warning"
on-click="_deleteUser"
disabled="[[user.system_generated]]"
>
[[localize('ui.panel.config.users.editor.delete_user')]]
</mwc-button>
<template is="dom-if" if="[[user.system_generated]]">
Unable to remove system generated users.
</template>
</div>
</paper-card>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
user: Object,
};
}
_computeName(user) {
return user && (user.name || "Unnamed user");
}
async _deleteUser(ev) {
if (
!confirm(
`Are you sure you want to delete ${this._computeName(this.user)}`
)
) {
ev.target.blur();
return;
}
try {
await this.hass.callWS({
type: "config/auth/delete",
user_id: this.user.id,
});
} catch (err) {
alert(err.code);
return;
}
this.fire("reload-users");
this.navigate("/config/users");
}
}
customElements.define("ha-user-editor", HaUserEditor);

View File

@ -0,0 +1,217 @@
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-subpage";
import { haStyle } from "../../../resources/styles";
import "../../../components/ha-card";
import { HomeAssistant } 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";
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;
protected render(): TemplateResult | void {
const hass = this.hass;
const user = this.user;
if (!hass || !user) {
return html``;
}
return html`
<hass-subpage
.header=${hass.localize("ui.panel.config.users.editor.caption")}
>
<ha-card .header=${this._name}>
<table class="card-content">
<tr>
<td>ID</td>
<td>${user.id}</td>
</tr>
<tr>
<td>Owner</td>
<td>${user.is_owner}</td>
</tr>
<tr>
<td>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>Active</td>
<td>${user.is_active}</td>
</tr>
<tr>
<td>System generated</td>
<td>${user.system_generated}</td>
</tr>
</table>
<div class="card-actions">
<mwc-button @click=${this._handleRenameUser}>
${hass.localize("ui.panel.config.users.editor.rename_user")}
</mwc-button>
<mwc-button
class="warning"
@click=${this._deleteUser}
.disabled=${user.system_generated}
>
${hass.localize("ui.panel.config.users.editor.delete_user")}
</mwc-button>
${user.system_generated
? html`
Unable to remove system generated users.
`
: ""}
</div>
</ha-card>
</hass-subpage>
`;
}
private get _name() {
return this.user && (this.user.name || "Unnamed user");
}
private async _handleRenameUser(ev): Promise<void> {
ev.currentTarget.blur();
const newName = prompt("New name?", this.user!.name);
if (newName === null || newName === this.user!.name) {
return;
}
try {
await updateUser(this.hass!, this.user!.id, {
name: newName,
});
fireEvent(this, "reload-users");
} catch (err) {
alert(`User rename failed: ${err.message}`);
}
}
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],
});
fireEvent(this, "reload-users");
} catch (err) {
alert(`Group update failed: ${err.message}`);
selectEl.value = this.user!.group_ids[0];
}
}
private async _deleteUser(ev): Promise<void> {
if (!confirm(`Are you sure you want to delete ${this._name}`)) {
ev.target.blur();
return;
}
try {
await deleteUser(this.hass!, this.user!.id);
} catch (err) {
alert(err.code);
return;
}
fireEvent(this, "reload-users");
navigate(this, "/config/users");
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
ha-card {
display: block;
max-width: 600px;
margin: 0 auto 16px;
}
ha-card:first-child {
margin-top: 16px;
}
ha-card:last-child {
margin-bottom: 16px;
}
.card-content {
padding: 0 16px 16px;
}
.card-actions {
padding: 0 8px;
}
hass-subpage ha-card:first-of-type {
direction: ltr;
}
table {
width: 100%;
}
td {
vertical-align: top;
}
.user-experiment {
padding: 8px 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-user-editor": HaUserEditor;
}
}

View File

@ -355,6 +355,11 @@
"not_home": "[%key:state::person::not_home%]"
}
},
"groups": {
"system-admin": "Administrators",
"system-users": "Users",
"system-read-only": "Read-Only Users"
},
"ui": {
"auth_store": {
"ask": "Do you want to save this login?",

View File

@ -57,6 +57,7 @@ export interface MFAModule {
export interface CurrentUser {
id: string;
is_owner: boolean;
is_admin: boolean;
name: string;
credentials: Credential[];
mfa_modules: MFAModule[];