Add MVP person editor (#2703)

* Add MVP person editor

* Better highlight the config.yaml people

* Add note
This commit is contained in:
Paulus Schoutsen 2019-02-09 10:41:45 -08:00 committed by GitHub
parent 8938ad8f8d
commit 46e1139946
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 484 additions and 15 deletions

46
src/data/person.ts Normal file
View File

@ -0,0 +1,46 @@
import { HomeAssistant } from "../types";
export interface Person {
id: string;
name: string;
user_id?: string;
device_trackers?: string[];
}
export interface PersonMutableParams {
name: string;
user_id: string | null;
device_trackers: string[];
}
export const fetchPersons = (hass: HomeAssistant) =>
hass.callWS<{
storage: Person[];
config: Person[];
}>({ type: "person/list" });
export const createPerson = (
hass: HomeAssistant,
values: PersonMutableParams
) =>
hass.callWS<Person>({
type: "person/create",
...values,
});
export const updatePerson = (
hass: HomeAssistant,
personId: string,
updates: Partial<PersonMutableParams>
) =>
hass.callWS<Person>({
type: "person/update",
person_id: personId,
...updates,
});
export const deletePerson = (hass: HomeAssistant, personId: string) =>
hass.callWS({
type: "person/delete",
person_id: personId,
});

View File

@ -14,7 +14,7 @@ export interface AreaRegistryDetailDialogParams {
}
export const loadAreaRegistryDetailDialog = () =>
import(/* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-area-registry-detail");
import(/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail");
export const showAreaRegistryDetailDialog = (
element: HTMLElement,

View File

@ -52,13 +52,14 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
type: Array,
value: [
"core",
"customize",
"person",
"entity_registry",
"area_registry",
"automation",
"script",
"zha",
"zwave",
"customize",
],
},
};

View File

@ -9,19 +9,6 @@ import isComponentLoaded from "../../common/config/is_component_loaded";
import EventsMixin from "../../mixins/events-mixin";
import NavigateMixin from "../../mixins/navigate-mixin";
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry");
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation");
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud");
import(/* webpackChunkName: "panel-config-config" */ "./config-entries/ha-config-entries");
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core");
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize");
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry");
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
/*
* @appliesMixin EventsMixin
* @appliesMixin NavigateMixin
@ -136,6 +123,15 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
></ha-config-zwave>
</template>
<template is="dom-if" if='[[_equals(_routeData.page, "person")]]' restamp>
<ha-config-person
page-name="person"
route="[[route]]"
hass="[[hass]]"
is-wide="[[isWide]]"
></ha-config-person>
</template>
<template
is="dom-if"
if='[[_equals(_routeData.page, "customize")]]'
@ -207,6 +203,19 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
this.addEventListener("ha-refresh-cloud-status", () =>
this._updateCloudStatus()
);
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry");
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation");
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud");
import(/* webpackChunkName: "panel-config-config" */ "./config-entries/ha-config-entries");
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core");
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize");
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry");
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
import(/* webpackChunkName: "panel-config-person" */ "./person/ha-config-person");
}
async _updateCloudStatus() {

View File

@ -0,0 +1,169 @@
import {
LitElement,
html,
css,
PropertyDeclarations,
CSSResult,
TemplateResult,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import { PersonMutableParams } from "../../../data/person";
class DialogPersonDetail extends LitElement {
public hass!: HomeAssistant;
private _name!: string;
private _error?: string;
private _params?: PersonDetailDialogParams;
private _submitting?: boolean;
static get properties(): PropertyDeclarations {
return {
_error: {},
_name: {},
_params: {},
};
}
public async showDialog(params: PersonDetailDialogParams): Promise<void> {
this._params = params;
this._error = undefined;
this._name = this._params.entry ? this._params.entry.name : "";
await this.updateComplete;
}
protected render(): TemplateResult | void {
if (!this._params) {
return html``;
}
const nameInvalid = this._name.trim() === "";
return html`
<paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>${this._params.entry ? this._params.entry.name : "New Person"}</h2>
<paper-dialog-scrollable>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<div class="form">
<paper-input
.value=${this._name}
@value-changed=${this._nameChanged}
label="Name"
error-message="Name is required"
.invalid=${nameInvalid}
></paper-input>
</div>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${this._params.entry
? html`
<paper-button
class="danger"
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
DELETE
</paper-button>
`
: html``}
<paper-button
@click="${this._updateEntry}"
.disabled=${nameInvalid || this._submitting}
>
${this._params.entry ? "UPDATE" : "CREATE"}
</paper-button>
</div>
</paper-dialog>
`;
}
private _nameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._name = ev.detail.value;
}
private async _updateEntry() {
this._submitting = true;
try {
const values: PersonMutableParams = {
name: this._name.trim(),
// Temp, we will add this in a future PR.
user_id: null,
device_trackers: [],
};
if (this._params!.entry) {
await this._params!.updateEntry(values);
} else {
await this._params!.createEntry(values);
}
this._params = undefined;
} catch (err) {
this._error = err;
} finally {
this._submitting = false;
}
}
private async _deleteEntry() {
this._submitting = true;
try {
if (await this._params!.removeEntry()) {
this._params = undefined;
}
} finally {
this._submitting = false;
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
paper-dialog {
min-width: 400px;
}
.form {
padding-bottom: 24px;
}
paper-button {
font-weight: 500;
}
paper-button.danger {
font-weight: 500;
color: var(--google-red-500);
margin-left: -12px;
margin-right: auto;
}
.error {
color: var(--google-red-500);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-person-detail": DialogPersonDetail;
}
}
customElements.define("dialog-person-detail", DialogPersonDetail);

View File

@ -0,0 +1,217 @@
import {
LitElement,
TemplateResult,
html,
css,
CSSResult,
PropertyDeclarations,
} from "lit-element";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-fab/paper-fab";
import { HomeAssistant } from "../../../types";
import {
Person,
fetchPersons,
updatePerson,
deletePerson,
createPerson,
} from "../../../data/person";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-loading-screen";
import compare from "../../../common/string/compare";
import "../ha-config-section";
import {
showPersonDetailDialog,
loadPersonDetailDialog,
} from "./show-dialog-person-detail";
class HaConfigPerson extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _storageItems?: Person[];
private _configItems?: Person[];
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_storageItems: {},
_configItems: {},
};
}
protected render(): TemplateResult | void {
if (
!this.hass ||
this._storageItems === undefined ||
this._configItems === undefined
) {
return html`
<hass-loading-screen></hass-loading-screen>
`;
}
return html`
<hass-subpage header="Persons">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Persons</span>
<span slot="introduction">
Here you can define each person of interest in Home Assistant.
${this._configItems.length > 0
? html`
<p>
Note: people configured via configuration.yaml cannot be
edited via the UI.
</p>
`
: ""}
</span>
<paper-card class="storage">
${this._storageItems.map((entry) => {
return html`
<paper-item @click=${this._openEditEntry} .entry=${entry}>
<paper-item-body>
${entry.name}
</paper-item-body>
</paper-item>
`;
})}
${this._storageItems.length === 0
? html`
<div class="empty">
Looks like you have no people yet!
<paper-button @click=${this._createPerson}>
CREATE PERSON</paper-button
>
</div>
`
: html``}
</paper-card>
${this._configItems.length > 0
? html`
<paper-card heading="Configuration.yaml people">
${this._configItems.map((entry) => {
return html`
<paper-item>
<paper-item-body>
${entry.name}
</paper-item-body>
</paper-item>
`;
})}
</paper-card>
`
: ""}
</ha-config-section>
</hass-subpage>
<paper-fab
?is-wide=${this.isWide}
icon="hass:plus"
title="Create Area"
@click=${this._createPerson}
></paper-fab>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._fetchData();
loadPersonDetailDialog();
}
private async _fetchData() {
const personData = await fetchPersons(this.hass!);
this._storageItems = personData.storage.sort((ent1, ent2) =>
compare(ent1.name, ent2.name)
);
this._configItems = personData.config.sort((ent1, ent2) =>
compare(ent1.name, ent2.name)
);
}
private _createPerson() {
this._openDialog();
}
private _openEditEntry(ev: MouseEvent) {
const entry: Person = (ev.currentTarget! as any).entry;
this._openDialog(entry);
}
private _openDialog(entry?: Person) {
showPersonDetailDialog(this, {
entry,
createEntry: async (values) => {
const created = await createPerson(this.hass!, values);
this._storageItems = this._storageItems!.concat(created).sort(
(ent1, ent2) => compare(ent1.name, ent2.name)
);
},
updateEntry: async (values) => {
const updated = await updatePerson(this.hass!, entry!.id, values);
this._storageItems = this._storageItems!.map((ent) =>
ent === entry ? updated : ent
);
},
removeEntry: async () => {
if (
!confirm(`Are you sure you want to delete this area?
All devices in this area will become unassigned.`)
) {
return false;
}
try {
await deletePerson(this.hass!, entry!.id);
this._storageItems = this._storageItems!.filter(
(ent) => ent !== entry
);
return true;
} catch (err) {
return false;
}
},
});
}
static get styles(): CSSResult {
return css`
a {
color: var(--primary-color);
}
paper-card {
display: block;
max-width: 600px;
margin: 16px auto;
}
.empty {
text-align: center;
}
paper-item {
padding-top: 4px;
padding-bottom: 4px;
}
paper-card.storage paper-item {
cursor: pointer;
}
paper-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
paper-fab[is-wide] {
bottom: 24px;
right: 24px;
}
`;
}
}
customElements.define("ha-config-person", HaConfigPerson);

View File

@ -0,0 +1,23 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { Person, PersonMutableParams } from "../../../data/person";
export interface PersonDetailDialogParams {
entry?: Person;
createEntry: (values: PersonMutableParams) => Promise<unknown>;
updateEntry: (updates: Partial<PersonMutableParams>) => Promise<unknown>;
removeEntry: () => Promise<boolean>;
}
export const loadPersonDetailDialog = () =>
import(/* webpackChunkName: "person-detail-dialog" */ "./dialog-person-detail");
export const showPersonDetailDialog = (
element: HTMLElement,
systemLogDetailParams: PersonDetailDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-person-detail",
dialogImport: loadPersonDetailDialog,
dialogParams: systemLogDetailParams,
});
};

View File

@ -757,6 +757,10 @@
"caption": "Entity Registry",
"description": "Overview of all known entities."
},
"person": {
"caption": "People",
"description": "Manage the people in Home Assistant."
},
"integrations": {
"caption": "Integrations",
"description": "Manage connected devices and services",