mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 13:26:34 +00:00
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:
parent
7d1ce1b240
commit
21a099ee9f
64
src/components/data-table/ha-data-table-icon.ts
Normal file
64
src/components/data-table/ha-data-table-icon.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user