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

View File

@ -1,3 +1,9 @@
import {
mdiCrownCircleOutline,
mdiAlphaSCircleOutline,
mdiHomeCircleOutline,
mdiCancel,
} from "@mdi/js";
import { HomeAssistant } from "../types";
import { Credential } from "./auth";
@ -73,7 +79,36 @@ export const computeUserInitials = (name: string) => {
.split(" ")
.slice(0, 3)
// Of each word, take first letter
.map((s) => s.substr(0, 1))
.map((s) => s.substring(0, 1))
.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 { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield";
import "../../../components/ha-help-tooltip";
import "../../../components/ha-chip-set";
import "../../../components/ha-chip";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import { adminChangePassword } from "../../../data/auth";
import {
computeUserBadges,
SYSTEM_GROUP_ID_ADMIN,
SYSTEM_GROUP_ID_USER,
} from "../../../data/user";
@ -55,6 +60,7 @@ class DialogUserDetail extends LitElement {
return html``;
}
const user = this._params.entry;
const badges = computeUserBadges(this.hass, user, true);
return html`
<ha-dialog
open
@ -71,26 +77,20 @@ class DialogUserDetail extends LitElement {
${this.hass.localize("ui.panel.config.users.editor.username")}:
${user.username}
</div>
<div>
${user.is_owner
? html`
<span class="state"
>${this.hass.localize(
"ui.panel.config.users.editor.owner"
)}</span
>
`
: ""}
${user.system_generated
? html`
<span class="state">
${this.hass.localize(
"ui.panel.config.users.editor.system_generated"
)}
</span>
`
: ""}
</div>
${badges.length === 0
? ""
: html`
<ha-chip-set>
${badges.map(
([icon, label]) => html`
<ha-chip hasIcon>
<ha-svg-icon slot="icon" .path=${icon}></ha-svg-icon>
${label}
</ha-chip>
`
)}
</ha-chip-set>
`}
<div class="form">
<paper-input
.value=${this._name}
@ -321,6 +321,9 @@ class DialogUserDetail extends LitElement {
.secondary {
color: var(--secondary-text-color);
}
ha-chip-set {
display: block;
}
.state {
background-color: rgba(var(--rgb-primary-text-color), 0.15);
border-radius: 16px;

View File

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

View File

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