diff --git a/src/components/data-table/ha-data-table-icon.ts b/src/components/data-table/ha-data-table-icon.ts new file mode 100644 index 0000000000..597d702dc8 --- /dev/null +++ b/src/components/data-table/ha-data-table-icon.ts @@ -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`
${this.tooltip}
` : ""} + + `; + } + + 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; + } +} diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index a9c3f300b5..b2d669105b 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -56,8 +56,8 @@ export interface SortingChangedEvent { export type SortingDirection = "desc" | "asc" | null; -export interface DataTableColumnContainer { - [key: string]: DataTableColumnData; +export interface DataTableColumnContainer { + [key: string]: DataTableColumnData; } export interface DataTableSortColumnData { @@ -68,10 +68,10 @@ export interface DataTableSortColumnData { direction?: SortingDirection; } -export interface DataTableColumnData extends DataTableSortColumnData { +export interface DataTableColumnData extends DataTableSortColumnData { title: TemplateResult | string; type?: "numeric" | "icon" | "icon-button" | "overflow-menu"; - template?: (data: any, row: T) => TemplateResult | string; + template?: (data: any, row: T) => TemplateResult | string; width?: string; maxWidth?: string; grows?: boolean; diff --git a/src/data/user.ts b/src/data/user.ts index c4afd4aa68..39493be53e 100644 --- a/src/data/user.ts +++ b/src/data/user.ts @@ -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; +}; diff --git a/src/panels/config/users/dialog-user-detail.ts b/src/panels/config/users/dialog-user-detail.ts index f8d10d5ab7..6e83c11685 100644 --- a/src/panels/config/users/dialog-user-detail.ts +++ b/src/panels/config/users/dialog-user-detail.ts @@ -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` -
- ${user.is_owner - ? html` - ${this.hass.localize( - "ui.panel.config.users.editor.owner" - )} - ` - : ""} - ${user.system_generated - ? html` - - ${this.hass.localize( - "ui.panel.config.users.editor.system_generated" - )} - - ` - : ""} -
+ ${badges.length === 0 + ? "" + : html` + + ${badges.map( + ([icon, label]) => html` + + + ${label} + + ` + )} + + `}
{ - const columns: DataTableColumnContainer = { + (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => { + const columns: DataTableColumnContainer = { 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}
- ${user.username} | - ${this.hass.localize(`groups.${user.group_ids[0]}`)} + ${user.username ? `${user.username} |` : ""} + ${localize(`groups.${user.group_ids[0]}`)}
` : 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`` @@ -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`` @@ -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`` : "", }, + 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`` + )}`; + }, + }, }; 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 diff --git a/src/translations/en.json b/src/translations/en.json index 55f163fe90..1e25eb8b9f 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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"