mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-16 05:46:35 +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 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;
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user