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}
+
+ `
+ )}
+
+ `}