Clean up users table (#11333)

* Clean up users table

* Add decicated icon for data tables

* Change tooltip and icons

* Only use icons for narrow view

* Shorten headers

* Add chips to the user detail dialog

* Lint

* Hide system badge on mobile
This commit is contained in:
Paulus Schoutsen 2022-01-19 12:28:13 -08:00 committed by GitHub
parent 7d1ce1b240
commit 21a099ee9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 180 additions and 56 deletions

View File

@ -0,0 +1,64 @@
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../ha-svg-icon";
@customElement("ha-data-table-icon")
class HaDataTableIcon extends LitElement {
@property() public tooltip!: string;
@property() public path!: string;
@state() private _hovered = false;
protected render(): TemplateResult {
return html`
${this._hovered ? html`<div>${this.tooltip}</div>` : ""}
<ha-svg-icon .path=${this.path}></ha-svg-icon>
`;
}
protected override firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const show = () => {
this._hovered = true;
};
const hide = () => {
this._hovered = false;
};
this.addEventListener("mouseenter", show);
this.addEventListener("focus", show);
this.addEventListener("mouseleave", hide);
this.addEventListener("blur", hide);
this.addEventListener("tap", hide);
}
static get styles() {
return css`
:host {
display: inline-block;
position: relative;
}
ha-svg-icon {
color: var(--secondary-text-color);
}
div {
position: absolute;
right: 28px;
z-index: 1002;
outline: none;
font-size: 10px;
line-height: 1;
background-color: var(--paper-tooltip-background, #616161);
color: var(--paper-tooltip-text-color, white);
padding: 8px;
border-radius: 2px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-data-table-icon": HaDataTableIcon;
}
}

View File

@ -56,8 +56,8 @@ export interface SortingChangedEvent {
export type SortingDirection = "desc" | "asc" | null; export type SortingDirection = "desc" | "asc" | null;
export interface DataTableColumnContainer { export interface DataTableColumnContainer<T = any> {
[key: string]: DataTableColumnData; [key: string]: DataTableColumnData<T>;
} }
export interface DataTableSortColumnData { export interface DataTableSortColumnData {
@ -68,10 +68,10 @@ export interface DataTableSortColumnData {
direction?: SortingDirection; direction?: SortingDirection;
} }
export interface DataTableColumnData extends DataTableSortColumnData { export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
title: TemplateResult | string; title: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button" | "overflow-menu"; type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
template?: <T>(data: any, row: T) => TemplateResult | string; template?: (data: any, row: T) => TemplateResult | string;
width?: string; width?: string;
maxWidth?: string; maxWidth?: string;
grows?: boolean; grows?: boolean;

View File

@ -1,3 +1,9 @@
import {
mdiCrownCircleOutline,
mdiAlphaSCircleOutline,
mdiHomeCircleOutline,
mdiCancel,
} from "@mdi/js";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { Credential } from "./auth"; import { Credential } from "./auth";
@ -73,7 +79,36 @@ export const computeUserInitials = (name: string) => {
.split(" ") .split(" ")
.slice(0, 3) .slice(0, 3)
// Of each word, take first letter // Of each word, take first letter
.map((s) => s.substr(0, 1)) .map((s) => s.substring(0, 1))
.join("") .join("")
); );
}; };
const OWNER_ICON = mdiCrownCircleOutline;
const SYSTEM_ICON = mdiAlphaSCircleOutline;
const LOCAL_ICON = mdiHomeCircleOutline;
const DISABLED_ICON = mdiCancel;
export const computeUserBadges = (
hass: HomeAssistant,
user: User,
includeSystem: boolean
) => {
const labels: [string, string][] = [];
const translate = (key) => hass.localize(`ui.panel.config.users.${key}`);
if (user.is_owner) {
labels.push([OWNER_ICON, translate("is_owner")]);
}
if (includeSystem && user.system_generated) {
labels.push([SYSTEM_ICON, translate("is_system")]);
}
if (user.local_only) {
labels.push([LOCAL_ICON, translate("is_local")]);
}
if (!user.is_active) {
labels.push([DISABLED_ICON, translate("is_not_active")]);
}
return labels;
};

View File

@ -3,13 +3,18 @@ import "@polymer/paper-input/paper-input";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-help-tooltip"; import "../../../components/ha-help-tooltip";
import "../../../components/ha-chip-set";
import "../../../components/ha-chip";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import { adminChangePassword } from "../../../data/auth"; import { adminChangePassword } from "../../../data/auth";
import { import {
computeUserBadges,
SYSTEM_GROUP_ID_ADMIN, SYSTEM_GROUP_ID_ADMIN,
SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_USER,
} from "../../../data/user"; } from "../../../data/user";
@ -55,6 +60,7 @@ class DialogUserDetail extends LitElement {
return html``; return html``;
} }
const user = this._params.entry; const user = this._params.entry;
const badges = computeUserBadges(this.hass, user, true);
return html` return html`
<ha-dialog <ha-dialog
open open
@ -71,26 +77,20 @@ class DialogUserDetail extends LitElement {
${this.hass.localize("ui.panel.config.users.editor.username")}: ${this.hass.localize("ui.panel.config.users.editor.username")}:
${user.username} ${user.username}
</div> </div>
<div> ${badges.length === 0
${user.is_owner ? ""
? html` : html`
<span class="state" <ha-chip-set>
>${this.hass.localize( ${badges.map(
"ui.panel.config.users.editor.owner" ([icon, label]) => html`
)}</span <ha-chip hasIcon>
> <ha-svg-icon slot="icon" .path=${icon}></ha-svg-icon>
` ${label}
: ""} </ha-chip>
${user.system_generated `
? html` )}
<span class="state"> </ha-chip-set>
${this.hass.localize( `}
"ui.panel.config.users.editor.system_generated"
)}
</span>
`
: ""}
</div>
<div class="form"> <div class="form">
<paper-input <paper-input
.value=${this._name} .value=${this._name}
@ -321,6 +321,9 @@ class DialogUserDetail extends LitElement {
.secondary { .secondary {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-chip-set {
display: block;
}
.state { .state {
background-color: rgba(var(--rgb-primary-text-color), 0.15); background-color: rgba(var(--rgb-primary-text-color), 0.15);
border-radius: 16px; border-radius: 16px;

View File

@ -3,13 +3,22 @@ import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../common/dom/fire_event"; import { HASSDomEvent } from "../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../common/translations/localize";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-icon";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-help-tooltip";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { deleteUser, fetchUsers, updateUser, User } from "../../../data/user"; import {
computeUserBadges,
deleteUser,
fetchUsers,
updateUser,
User,
} from "../../../data/user";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
@ -30,23 +39,21 @@ export class HaConfigUsers extends LitElement {
@property() public route!: Route; @property() public route!: Route;
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow: boolean, _language): DataTableColumnContainer => { (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer = { const columns: DataTableColumnContainer<User> = {
name: { name: {
title: this.hass.localize( title: localize("ui.panel.config.users.picker.headers.name"),
"ui.panel.config.users.picker.headers.name"
),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "25%", width: "25%",
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (name, user: any) => template: (name, user) =>
narrow narrow
? html` ${name}<br /> ? html` ${name}<br />
<div class="secondary"> <div class="secondary">
${user.username} | ${user.username ? `${user.username} |` : ""}
${this.hass.localize(`groups.${user.group_ids[0]}`)} ${localize(`groups.${user.group_ids[0]}`)}
</div>` </div>`
: html` ${name || : html` ${name ||
this.hass!.localize( this.hass!.localize(
@ -54,31 +61,22 @@ export class HaConfigUsers extends LitElement {
)}`, )}`,
}, },
username: { username: {
title: this.hass.localize( title: localize("ui.panel.config.users.picker.headers.username"),
"ui.panel.config.users.picker.headers.username"
),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "20%", width: "20%",
direction: "asc", direction: "asc",
hidden: narrow, hidden: narrow,
template: (username) => html` template: (username) => html` ${username || "-"} `,
${username ||
this.hass!.localize("ui.panel.config.users.editor.unnamed_user")}
`,
}, },
group_ids: { group_ids: {
title: this.hass.localize( title: localize("ui.panel.config.users.picker.headers.group"),
"ui.panel.config.users.picker.headers.group"
),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "20%", width: "20%",
direction: "asc", direction: "asc",
hidden: narrow, hidden: narrow,
template: (groupIds) => html` template: (groupIds) => html` ${localize(`groups.${groupIds[0]}`)} `,
${this.hass.localize(`groups.${groupIds[0]}`)}
`,
}, },
is_active: { is_active: {
title: this.hass.localize( title: this.hass.localize(
@ -88,6 +86,7 @@ export class HaConfigUsers extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "80px", width: "80px",
hidden: narrow,
template: (is_active) => template: (is_active) =>
is_active is_active
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
@ -100,7 +99,8 @@ export class HaConfigUsers extends LitElement {
type: "icon", type: "icon",
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "160px", width: "80px",
hidden: narrow,
template: (generated) => template: (generated) =>
generated generated
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
@ -113,10 +113,29 @@ export class HaConfigUsers extends LitElement {
type: "icon", type: "icon",
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "160px", width: "80px",
hidden: narrow,
template: (local) => template: (local) =>
local ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "", local ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "",
}, },
icons: {
title: "",
type: "icon",
sortable: false,
filterable: false,
width: "104px",
hidden: !narrow,
template: (_, user) => {
const badges = computeUserBadges(this.hass, user, false);
return html`${badges.map(
([icon, tooltip]) =>
html`<ha-data-table-icon
.path=${icon}
.tooltip=${tooltip}
></ha-data-table-icon>`
)}`;
},
},
}; };
return columns; return columns;
@ -136,7 +155,7 @@ export class HaConfigUsers extends LitElement {
.route=${this.route} .route=${this.route}
backPath="/config" backPath="/config"
.tabs=${configSections.persons} .tabs=${configSections.persons}
.columns=${this._columns(this.narrow, this.hass.language)} .columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._users} .data=${this._users}
@row-click=${this._editUser} @row-click=${this._editUser}
hasFab hasFab

View File

@ -2518,15 +2518,18 @@
"caption": "Users", "caption": "Users",
"description": "Manage the Home Assistant user accounts", "description": "Manage the Home Assistant user accounts",
"users_privileges_note": "The user group feature 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.", "users_privileges_note": "The user group feature 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.",
"is_not_active": "Disabled",
"is_system": "System user",
"is_local": "Local user",
"is_owner": "Owner",
"picker": { "picker": {
"headers": { "headers": {
"name": "Display name", "name": "Display name",
"username": "Username", "username": "Username",
"group": "Group", "group": "Group",
"system": "System generated", "system": "System",
"is_active": "Active", "is_active": "Active",
"is_owner": "Owner", "local": "Local"
"local": "Local only"
}, },
"add_user": "Add user" "add_user": "Add user"
}, },
@ -2547,9 +2550,9 @@
"group": "Group", "group": "Group",
"active": "Active", "active": "Active",
"local_only": "Can only log in from the local network", "local_only": "Can only log in from the local network",
"system_generated": "System generated", "system_generated": "System user",
"system_generated_users_not_removable": "Unable to remove system generated users.", "system_generated_users_not_removable": "Unable to remove system users.",
"system_generated_users_not_editable": "Unable to update system generated users.", "system_generated_users_not_editable": "Unable to update system users.",
"unnamed_user": "Unnamed User", "unnamed_user": "Unnamed User",
"confirm_user_deletion": "Are you sure you want to delete {name}?", "confirm_user_deletion": "Are you sure you want to delete {name}?",
"active_tooltip": "Controls if user can login" "active_tooltip": "Controls if user can login"