Changes for new storage (#2228)

* Changes for new storage

* Fix lint

* Use indexes for editing

* Use lovelace object

* Use lovelace object

* Lit conversion panel lovelace

* Lovelace obj

* Fix edit cards

* Fix some bugs

* Fix last bugs

* Lint

* Don't drop view content

* Move file

* Add skip button to card picker

* Correctly set lovelace mode
This commit is contained in:
Bram Kragten 2018-12-10 08:57:29 +01:00 committed by Paulus Schoutsen
parent 2a23487163
commit 4f0a965573
30 changed files with 702 additions and 861 deletions

View File

@ -1,22 +1,23 @@
import { HomeAssistant } from "../types";
export interface LovelaceConfig {
_frontendAuto: boolean;
title?: string;
views: LovelaceViewConfig[];
}
export interface LovelaceViewConfig {
index?: number;
title?: string;
badges?: string[];
cards?: LovelaceCardConfig[];
id?: string;
path?: string;
icon?: string;
theme?: string;
}
export interface LovelaceCardConfig {
id?: string;
index?: number;
view_index?: number;
type: string;
[key: string]: any;
}
@ -60,95 +61,11 @@ export const fetchConfig = (
force,
});
export const migrateConfig = (hass: HomeAssistant): Promise<void> =>
hass.callWS({
type: "lovelace/config/migrate",
});
export const saveConfig = (
hass: HomeAssistant,
config: LovelaceConfig | string,
format: "json" | "yaml"
config: LovelaceConfig
): Promise<void> =>
hass.callWS({
type: "lovelace/config/save",
config,
format,
});
export const getCardConfig = (
hass: HomeAssistant,
cardId: string
): Promise<string> =>
hass.callWS({
type: "lovelace/config/card/get",
card_id: cardId,
});
export const updateCardConfig = (
hass: HomeAssistant,
cardId: string,
config: LovelaceCardConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/update",
card_id: cardId,
card_config: config,
format,
});
export const deleteCard = (
hass: HomeAssistant,
cardId: string
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/delete",
card_id: cardId,
});
export const addCard = (
hass: HomeAssistant,
viewId: string,
config: LovelaceCardConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/add",
view_id: viewId,
card_config: config,
format,
});
export const updateViewConfig = (
hass: HomeAssistant,
viewId: string,
config: LovelaceViewConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/update",
view_id: viewId,
view_config: config,
format,
});
export const deleteView = (
hass: HomeAssistant,
viewId: string
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/delete",
view_id: viewId,
});
export const addView = (
hass: HomeAssistant,
config: LovelaceViewConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/add",
view_config: config,
format,
});

View File

@ -134,8 +134,14 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
this._roundSliderStyle = loaded.roundSliderStyle;
this._jQuery = loaded.jQuery;
const brightness = this.hass!.states[this._config!.entity].attributes
.brightness;
const stateObj = this.hass!.states[this._config!.entity] as LightEntity;
if (!stateObj) {
return;
}
const brightness = stateObj.attributes.brightness || 0;
this._jQuery("#light", this.shadowRoot).roundSlider({
...lightConfig,
change: (value) => this._setBrightness(value),
@ -152,7 +158,13 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
return;
}
const attrs = this.hass!.states[this._config!.entity].attributes;
const stateObj = this.hass!.states[this._config!.entity];
if (!stateObj) {
return;
}
const attrs = stateObj.attributes;
this._jQuery("#light", this.shadowRoot).roundSlider({
value: Math.round((attrs.brightness / 254) * 100) || 0,

View File

@ -94,7 +94,7 @@ const computeDefaultViewStates = (hass: HomeAssistant): HassEntities => {
const generateViewConfig = (
localize: LocalizeFunc,
id: string,
path: string,
title: string | undefined,
icon: string | undefined,
entities: HassEntities,
@ -158,7 +158,7 @@ const generateViewConfig = (
});
return {
id,
path,
title,
icon,
badges,
@ -228,7 +228,6 @@ export const generateLovelaceConfig = (
}
return {
_frontendAuto: true,
title,
views,
};

View File

@ -1,32 +1,22 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-icon-button/paper-icon-button";
import { fireEvent } from "../../../common/dom/fire_event";
import { showEditCardDialog } from "../editor/show-edit-card-dialog";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { confDeleteCard } from "../editor/delete-card";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
declare global {
// for fire event
interface HASSDomEvents {
"show-edit-card": {
cardConfig?: LovelaceCardConfig;
viewId?: string | number;
add: boolean;
reloadLovelace: () => void;
};
}
}
import { Lovelace } from "../types";
export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) {
public cardConfig?: LovelaceCardConfig;
protected hass?: HomeAssistant;
protected lovelace?: Lovelace;
protected path?: [number, number];
static get properties(): PropertyDeclarations {
return { hass: {} };
return { hass: {}, lovelace: {}, path: {} };
}
protected render() {
@ -66,26 +56,13 @@ export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) {
`;
}
private _editCard(): void {
if (!this.cardConfig) {
return;
}
showEditCardDialog(this, {
cardConfig: this.cardConfig,
add: false,
reloadLovelace: () => fireEvent(this, "config-refresh"),
lovelace: this.lovelace!,
path: this.path!,
});
}
private _deleteCard(): void {
if (!this.cardConfig) {
return;
}
if (!this.cardConfig.id) {
this._editCard();
return;
}
confDeleteCard(this.hass!, this.cardConfig.id, () =>
fireEvent(this, "config-refresh")
);
confDeleteCard(this.lovelace!, this.path!);
}
}

View File

@ -2,22 +2,11 @@ import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-button/paper-button";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { getCardElementTag } from "../common/get-card-element-tag";
import { CardPickTarget } from "./types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { uid } from "../../../common/util/uid";
declare global {
interface HASSDomEvents {
"card-picked": {
config: LovelaceCardConfig;
};
}
}
import { HomeAssistant } from "../../../../types";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import { getCardElementTag } from "../../common/get-card-element-tag";
import { CardPickTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
const cards = [
{ name: "Alarm panel", type: "alarm-panel" },
@ -47,7 +36,8 @@ const cards = [
];
export class HuiCardPicker extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
public hass?: HomeAssistant;
public cardPicked?: (cardConf: LovelaceCardConfig) => void;
protected render(): TemplateResult {
return html`
@ -90,16 +80,14 @@ export class HuiCardPicker extends hassLocalizeLitMixin(LitElement) {
const tag = getCardElementTag(type);
const elClass = customElements.get(tag);
let config: LovelaceCardConfig = { type, id: uid() };
let config: LovelaceCardConfig = { type };
if (elClass && elClass.getStubConfig) {
const cardConfig = elClass.getStubConfig(this.hass);
config = { ...config, ...cardConfig };
}
fireEvent(this, "card-picked", {
config,
});
this.cardPicked!(config);
}
}

View File

@ -1,12 +1,12 @@
import "@polymer/paper-input/paper-textarea";
import createCardElement from "../common/create-card-element";
import createErrorCardConfig from "../common/create-error-card-config";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { LovelaceCard } from "../types";
import { ConfigError } from "./types";
import { getCardElementTag } from "../common/get-card-element-tag";
import createCardElement from "../../common/create-card-element";
import createErrorCardConfig from "../../common/create-error-card-config";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import { LovelaceCard } from "../../types";
import { ConfigError } from "../types";
import { getCardElementTag } from "../../common/get-card-element-tag";
export class HuiCardPreview extends HTMLElement {
private _hass?: HomeAssistant;

View File

@ -1,11 +1,12 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { HASSDomEvent } from "../../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import "./hui-edit-card";
import "./hui-migrate-config";
import "./hui-dialog-pick-card";
import { EditCardDialogParams } from "./show-edit-card-dialog";
declare global {
// for fire event
@ -18,66 +19,67 @@ declare global {
}
}
export interface EditCardDialogParams {
cardConfig?: LovelaceCardConfig;
viewId?: string | number;
add: boolean;
reloadLovelace: () => void;
}
export class HuiDialogEditCard extends LitElement {
protected hass?: HomeAssistant;
private _params?: EditCardDialogParams;
private _cardConfig?: LovelaceCardConfig;
static get properties(): PropertyDeclarations {
return {
hass: {},
_params: {},
_cardConfig: {},
};
}
constructor() {
super();
this._cardPicked = this._cardPicked.bind(this);
this._cancel = this._cancel.bind(this);
}
public async showDialog(params: EditCardDialogParams): Promise<void> {
this._params = params;
await this.updateComplete;
(this.shadowRoot!.children[0] as any).showDialog();
this._cardConfig =
params.path.length === 2
? (this._cardConfig = params.lovelace.config.views[
params.path[0]
].cards![params.path[1]])
: undefined;
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
if (
(!this._params.add &&
this._params.cardConfig &&
!("id" in this._params.cardConfig)) ||
(this._params.add && !this._params.viewId)
) {
if (!this._cardConfig) {
// Card picker
return html`
<hui-migrate-config
<hui-dialog-pick-card
.hass="${this.hass}"
@reload-lovelace="${this._params.reloadLovelace}"
></hui-migrate-config>
.cardPicked="${this._cardPicked}"
></hui-dialog-pick-card>
`;
}
return html`
<hui-edit-card
.hass="${this.hass}"
.viewId="${this._params.viewId}"
.cardConfig="${this._params.cardConfig}"
@reload-lovelace="${this._params.reloadLovelace}"
@cancel-edit-card="${this._cancel}"
.lovelace="${this._params.lovelace}"
.path="${this._params.path}"
.cardConfig="${this._cardConfig}"
.closeDialog="${this._cancel}"
>
</hui-edit-card>
`;
}
private _cardPicked(cardConf: LovelaceCardConfig) {
this._cardConfig = cardConf;
}
private _cancel() {
this._params = {
add: false,
reloadLovelace: () => {
return;
},
};
this._params = undefined;
this._cardConfig = undefined;
}
}

View File

@ -0,0 +1,47 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "./hui-card-picker";
import { HomeAssistant } from "../../../../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { LovelaceCardConfig } from "../../../../data/lovelace";
export class HuiDialogPickCard extends hassLocalizeLitMixin(LitElement) {
public hass?: HomeAssistant;
public cardPicked?: (cardConf: LovelaceCardConfig) => void;
static get properties(): PropertyDeclarations {
return {};
}
protected render(): TemplateResult {
return html`
<paper-dialog with-backdrop opened>
<h2>${this.localize("ui.panel.lovelace.editor.edit_card.header")}</h2>
<paper-dialog-scrollable>
<hui-card-picker
.hass="${this.hass}"
.cardPicked="${this.cardPicked}"
></hui-card-picker>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<paper-button @click="${this._skipPick}">SKIP</paper-button>
</div>
</paper-dialog>
`;
}
private _skipPick() {
this.cardPicked!({ type: "" });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-pick-card": HuiDialogPickCard;
}
}
customElements.define("hui-dialog-pick-card", HuiDialogPickCard);

View File

@ -15,31 +15,22 @@ import "@polymer/paper-dialog/paper-dialog";
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import { HomeAssistant } from "../../../types";
import {
addCard,
updateCardConfig,
LovelaceCardConfig,
} from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import { fireEvent } from "../../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import "./hui-yaml-editor";
import "./hui-card-picker";
import "./hui-card-preview";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { HuiCardPreview } from "./hui-card-preview";
import { LovelaceCardEditor } from "../types";
import {
YamlChangedEvent,
CardPickedEvent,
ConfigValue,
ConfigError,
} from "./types";
import { extYamlSchema } from "./yaml-ext-schema";
import { EntityConfig } from "../entity-rows/types";
import { getCardElementTag } from "../common/get-card-element-tag";
import { LovelaceCardEditor, Lovelace } from "../../types";
import { YamlChangedEvent, ConfigValue, ConfigError } from "../types";
import { extYamlSchema } from "../yaml-ext-schema";
import { EntityConfig } from "../../entity-rows/types";
import { getCardElementTag } from "../../common/get-card-element-tag";
import { addCard, replaceCard } from "../config-util";
declare global {
interface HASSDomEvents {
@ -52,17 +43,30 @@ declare global {
"config-changed": {
config: LovelaceCardConfig;
};
"cancel-edit-card": {};
}
}
export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
public hass?: HomeAssistant;
public lovelace?: Lovelace;
public path?: [number] | [number, number];
public cardConfig?: LovelaceCardConfig;
public closeDialog?: () => void;
private _configElement?: LovelaceCardEditor | null;
private _uiEditor?: boolean;
private _configValue?: ConfigValue;
private _configState?: string;
private _loading?: boolean;
private _saving: boolean;
private _errorMsg?: TemplateResult;
private _cardType?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
cardConfig: {},
viewId: {},
_cardId: {},
viewIndex: {},
_cardIndex: {},
_configElement: {},
_configValue: {},
_configState: {},
@ -81,38 +85,15 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
return this.shadowRoot!.querySelector("hui-card-preview")!;
}
public cardConfig?: LovelaceCardConfig;
public viewId?: string | number;
protected hass?: HomeAssistant;
private _cardId?: string;
private _configElement?: LovelaceCardEditor | null;
private _uiEditor?: boolean;
private _configValue?: ConfigValue;
private _configState?: string;
private _loading?: boolean;
private _saving: boolean;
private _errorMsg?: TemplateResult;
private _cardType?: string;
protected constructor() {
super();
this._saving = false;
}
public async showDialog(): Promise<void> {
// Wait till dialog is rendered.
if (this._dialog == null) {
await this.updateComplete;
}
this._dialog.open();
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
!changedProperties.has("cardConfig") &&
!changedProperties.has("viewId")
) {
if (!changedProperties.has("cardConfig")) {
return;
}
@ -122,17 +103,8 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
this._errorMsg = undefined;
this._configElement = undefined;
if (this.cardConfig && String(this.cardConfig.id) !== this._cardId) {
this._loading = true;
this._cardId = String(this.cardConfig.id);
this._loadConfigElement(this.cardConfig);
} else {
this._cardId = undefined;
}
if (this.viewId && !this.cardConfig) {
this._resizeDialog();
}
this._loading = true;
this._loadConfigElement(this.cardConfig!);
}
protected render(): TemplateResult {
@ -147,7 +119,6 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
content = html`
<hui-yaml-editor
.hass="${this.hass}"
.cardId="${this._cardId}"
.yaml="${this._configValue!.value}"
@yaml-changed="${this._handleYamlChanged}"
></hui-yaml-editor>
@ -157,18 +128,11 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
<hr />
<hui-card-preview .hass="${this.hass}"> </hui-card-preview>
`;
} else if (this.viewId && !this.cardConfig) {
content = html`
<hui-card-picker
.hass="${this.hass}"
@card-picked="${this._handleCardPicked}"
></hui-card-picker>
`;
}
return html`
${this.renderStyle()}
<paper-dialog with-backdrop>
<paper-dialog with-backdrop opened>
<h2>${this.localize("ui.panel.lovelace.editor.edit_card.header")}</h2>
<paper-spinner
?active="${this._loading}"
@ -203,7 +167,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
)
}</paper-button
>
<paper-button @click="${this._closeDialog}"
<paper-button @click="${this.closeDialog}"
>${this.localize("ui.common.cancel")}</paper-button
>
<paper-button
@ -272,15 +236,6 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
`;
}
private _save(): void {
this._saving = true;
this._updateConfigInBackend();
}
private _saveDone(): void {
this._saving = false;
}
private async _loadedDialog(): Promise<void> {
await this.updateComplete;
this._loading = false;
@ -292,58 +247,42 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
fireEvent(this._dialog, "iron-resize");
}
private _closeDialog(): void {
this.cardConfig = undefined;
this.viewId = undefined;
fireEvent(this, "cancel-edit-card");
this._dialog.close();
}
private async _updateConfigInBackend(): Promise<void> {
private async _save(): Promise<void> {
if (!this._isConfigValid()) {
alert("Your config is not valid, please fix your config before saving.");
this._saveDone();
return;
}
if (!this._isConfigChanged()) {
this._closeDialog();
this._saveDone();
this.closeDialog!();
return;
}
this._saving = true;
const cardConf: LovelaceCardConfig =
this._configValue!.format === "yaml"
? yaml.safeLoad(this._configValue!.value!, {
schema: extYamlSchema,
})
: this._configValue!.value!;
try {
if (this.viewId) {
await addCard(
this.hass!,
String(this.viewId),
this._configValue!.value!,
this._configValue!.format
);
} else {
await updateCardConfig(
this.hass!,
this._cardId!,
this._configValue!.value!,
this._configValue!.format
);
}
fireEvent(this, "reload-lovelace");
this._closeDialog();
this._saveDone();
const lovelace = this.lovelace!;
await lovelace.saveConfig(
this._creatingCard
? addCard(lovelace.config, this.path as [number], cardConf)
: replaceCard(
lovelace.config,
this.path as [number, number],
cardConf
)
);
this.closeDialog!();
} catch (err) {
alert(`Saving failed: ${err.message}`);
this._saveDone();
}
}
private async _handleCardPicked(ev: CardPickedEvent): Promise<void> {
const succes = await this._loadConfigElement(ev.detail.config);
if (!succes) {
this._configValue = {
format: "yaml",
value: yaml.safeDump(ev.detail.config),
};
} finally {
this._saving = false;
}
}
@ -394,14 +333,10 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
private async _toggleEditor(): Promise<void> {
if (this._uiEditor && this._configValue!.format === "json") {
if (this._isConfigChanged()) {
this._configValue = {
format: "yaml",
value: yaml.safeDump(this._configValue!.value),
};
} else {
this._configValue = { format: "yaml", value: undefined };
}
this._configValue = {
format: "yaml",
value: yaml.safeDump(this._configValue!.value),
};
this._uiEditor = !this._uiEditor;
} else if (this._configElement && this._configValue!.format === "yaml") {
const yamlConfig = this._configValue!.value;
@ -438,12 +373,12 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
}
private _isConfigChanged(): boolean {
if (this.viewId) {
if (this._creatingCard) {
return true;
}
const configValue =
this._configValue!.format === "yaml"
? yaml.safeDump(this._configValue!.value)
? yaml.safeLoad(this._configValue!.value)
: this._configValue!.value;
return JSON.stringify(configValue) !== JSON.stringify(this.cardConfig);
}
@ -465,6 +400,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
if (elClass && elClass.getConfigElement) {
configElement = await elClass.getConfigElement();
} else {
this._configValue = { format: "yaml", value: yaml.safeDump(conf) };
this._uiEditor = false;
this._configElement = null;
return false;
@ -477,6 +413,10 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
Your config is not supported by the UI editor:<br /><b>${err.message}</b
><br />Falling back to YAML editor.
`;
this._configValue = {
format: "yaml",
value: yaml.safeDump(conf),
};
this._uiEditor = false;
this._configElement = null;
return false;
@ -492,6 +432,10 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
this._updatePreview(conf);
return true;
}
private get _creatingCard(): boolean {
return this.path!.length === 1;
}
}
declare global {

View File

@ -1,42 +1,29 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-spinner/paper-spinner";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { getCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
export class HuiYAMLEditor extends LitElement {
public cardId?: string;
protected hass?: HomeAssistant;
private _yaml?: string;
private _loading?: boolean;
static get properties(): PropertyDeclarations {
return { _yaml: {}, cardId: {} };
return { _yaml: {} };
}
set yaml(yaml: string) {
if (yaml === undefined) {
this._loading = true;
this._loadConfig();
return;
} else {
this._yaml = yaml;
if (this._loading) {
this._loading = false;
}
}
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<paper-spinner
?active="${this._loading}"
alt="Loading"
class="center"
></paper-spinner>
<paper-textarea
max-rows="10"
.value="${this._yaml}"
@ -51,31 +38,10 @@ export class HuiYAMLEditor extends LitElement {
paper-textarea {
--paper-input-container-shared-input-style_-_font-family: monospace;
}
.center {
margin-left: auto;
margin-right: auto;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
</style>
`;
}
private async _loadConfig(): Promise<void> {
if (!this.hass || !this.cardId) {
return;
}
this._yaml = await getCardConfig(this.hass, this.cardId);
if (this._loading) {
this._loading = false;
}
}
private _valueChanged(ev: Event): void {
const target = ev.target! as any;
this._yaml = target.value;

View File

@ -1,5 +1,5 @@
import { LovelaceCardConfig } from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Lovelace } from "../../types";
declare global {
// for fire event
@ -13,10 +13,8 @@ const dialogShowEvent = "show-edit-card";
const dialogTag = "hui-dialog-edit-card";
export interface EditCardDialogParams {
cardConfig?: LovelaceCardConfig;
viewId?: string | number;
add: boolean;
reloadLovelace: () => void;
lovelace: Lovelace;
path: [number] | [number, number];
}
const registerEditCardDialog = (element: HTMLElement) =>

View File

@ -33,7 +33,6 @@ const entitiesConfigStruct = struct.union([
const cardConfigStruct = struct({
type: "string",
id: "string|number",
title: "string|number?",
theme: "string?",
show_header_toggle: "boolean?",

View File

@ -32,7 +32,6 @@ const entitiesConfigStruct = struct.union([
const cardConfigStruct = struct({
type: "string",
id: "string|number",
title: "string|number?",
theme: "string?",
columns: "number?",

View File

@ -0,0 +1,117 @@
import {
LovelaceConfig,
LovelaceCardConfig,
LovelaceViewConfig,
} from "../../../data/lovelace";
export const addCard = (
config: LovelaceConfig,
path: [number],
cardConfig: LovelaceCardConfig
): LovelaceConfig => {
const [viewIndex] = path;
const views: LovelaceViewConfig[] = [];
config.views.forEach((viewConf, index) => {
if (index !== viewIndex) {
views.push(config.views[index]);
return;
}
const cards = viewConf.cards
? [...viewConf.cards, cardConfig]
: [cardConfig];
views.push({
...viewConf,
cards,
});
});
return {
...config,
views,
};
};
export const replaceCard = (
config: LovelaceConfig,
path: [number, number],
cardConfig: LovelaceCardConfig
): LovelaceConfig => {
const [viewIndex, cardIndex] = path;
const views: LovelaceViewConfig[] = [];
config.views.forEach((viewConf, index) => {
if (index !== viewIndex) {
views.push(config.views[index]);
return;
}
views.push({
...viewConf,
cards: (viewConf.cards || []).map((origConf, ind) =>
ind === cardIndex ? cardConfig : origConf
),
});
});
return {
...config,
views,
};
};
export const deleteCard = (
config: LovelaceConfig,
path: [number, number]
): LovelaceConfig => {
const [viewIndex, cardIndex] = path;
const views: LovelaceViewConfig[] = [];
config.views.forEach((viewConf, index) => {
if (index !== viewIndex) {
views.push(config.views[index]);
return;
}
views.push({
...viewConf,
cards: (viewConf.cards || []).filter(
(_origConf, ind) => ind !== cardIndex
),
});
});
return {
...config,
views,
};
};
export const addView = (
config: LovelaceConfig,
viewConfig: LovelaceViewConfig
): LovelaceConfig => ({
...config,
views: config.views.concat(viewConfig),
});
export const replaceView = (
config: LovelaceConfig,
viewIndex: number,
viewConfig: LovelaceViewConfig
): LovelaceConfig => ({
...config,
views: config.views.map((origView, index) =>
index === viewIndex ? viewConfig : origView
),
});
export const deleteView = (
config: LovelaceConfig,
viewIndex: number
): LovelaceConfig => ({
...config,
views: config.views.filter((_origView, index) => index !== viewIndex),
});

View File

@ -1,17 +1,15 @@
import { deleteCard } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { Lovelace } from "../types";
import { deleteCard } from "./config-util";
export async function confDeleteCard(
hass: HomeAssistant,
cardId: string,
reloadLovelace: () => void
lovelace: Lovelace,
path: [number, number]
): Promise<void> {
if (!confirm("Are you sure you want to delete this card?")) {
return;
}
try {
await deleteCard(hass, String(cardId));
reloadLovelace();
await lovelace.saveConfig(deleteCard(lovelace.config, path));
} catch (err) {
alert(`Deleting failed: ${err.message}`);
}

View File

@ -1,18 +0,0 @@
import { deleteView } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
export async function confDeleteView(
hass: HomeAssistant,
viewId: string,
reloadLovelace: () => void
): Promise<void> {
if (!confirm("Are you sure you want to delete this view?")) {
return;
}
try {
await deleteView(hass, String(viewId));
reloadLovelace();
} catch (err) {
alert(`Deleting failed: ${err.message}`);
}
}

View File

@ -10,40 +10,8 @@ import "@polymer/paper-button/paper-button";
import { HomeAssistant } from "../../../types";
import {
saveConfig,
migrateConfig,
LovelaceConfig,
} from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
declare global {
// for fire event
interface HASSDomEvents {
"show-save-config": SaveDialogParams;
}
}
const dialogShowEvent = "show-save-config";
const dialogTag = "hui-dialog-save-config";
export interface SaveDialogParams {
config: LovelaceConfig;
reloadLovelace: () => void;
}
export const registerSaveDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-save-config"),
});
export const showSaveDialog = (
element: HTMLElement,
saveDialogParams: SaveDialogParams
) => fireEvent(element, dialogShowEvent, saveDialogParams);
import { SaveDialogParams } from "./show-save-config-dialog";
export class HuiSaveConfig extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
@ -137,13 +105,11 @@ export class HuiSaveConfig extends hassLocalizeLitMixin(LitElement) {
return;
}
this._saving = true;
delete this._params.config._frontendAuto;
try {
await saveConfig(this.hass, this._params.config, "json");
await migrateConfig(this.hass);
const lovelace = this._params!.lovelace;
await lovelace.saveConfig(lovelace.config);
this._saving = false;
this._closeDialog();
this._params.reloadLovelace!();
} catch (err) {
alert(`Saving failed: ${err.message}`);
this._saving = false;
@ -157,4 +123,4 @@ declare global {
}
}
customElements.define(dialogTag, HuiSaveConfig);
customElements.define("hui-dialog-save-config", HuiSaveConfig);

View File

@ -1,113 +0,0 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-dialog/paper-dialog";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-button/paper-button";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { migrateConfig } from "../../../data/lovelace";
export class HuiMigrateConfig extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
private _migrating?: boolean;
static get properties(): PropertyDeclarations {
return { _hass: {}, _migrating: {} };
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
public async showDialog(): Promise<void> {
// Wait till dialog is rendered.
if (this._dialog == null) {
await this.updateComplete;
}
this._dialog.open();
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<paper-dialog with-backdrop>
<h2>${this.localize("ui.panel.lovelace.editor.migrate.header")}</h2>
<paper-dialog-scrollable>
<p>${this.localize("ui.panel.lovelace.editor.migrate.para_no_id")}</p>
<p>
${this.localize("ui.panel.lovelace.editor.migrate.para_migrate")}
</p>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<paper-button @click="${this._closeDialog}"
>${this.localize("ui.common.cancel")}</paper-button
>
<paper-button
?disabled="${this._migrating}"
@click="${this._migrateConfig}"
>
<paper-spinner
?active="${this._migrating}"
alt="Saving"
></paper-spinner>
${
this.localize("ui.panel.lovelace.editor.migrate.migrate")
}</paper-button
>
</div>
</paper-dialog>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
paper-dialog {
width: 650px;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
paper-button paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
}
</style>
`;
}
private _closeDialog(): void {
this._dialog.close();
}
private async _migrateConfig(): Promise<void> {
this._migrating = true;
try {
await migrateConfig(this.hass!);
this._closeDialog();
this._migrating = false;
fireEvent(this, "reload-lovelace");
} catch (err) {
alert(`Migration failed: ${err.message}`);
this._migrating = false;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-migrate-config": HuiMigrateConfig;
}
}
customElements.define("hui-migrate-config", HuiMigrateConfig);

View File

@ -0,0 +1,33 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { Lovelace } from "../types";
declare global {
// for fire event
interface HASSDomEvents {
"show-save-config": SaveDialogParams;
}
}
const dialogShowEvent = "show-save-config";
const dialogTag = "hui-dialog-save-config";
export interface SaveDialogParams {
lovelace: Lovelace;
}
let registeredDialog = false;
export const showSaveDialog = (
element: HTMLElement,
saveDialogParams: SaveDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-save-config"),
});
}
fireEvent(element, dialogShowEvent, saveDialogParams);
};

View File

@ -8,12 +8,6 @@ export interface YamlChangedEvent extends Event {
};
}
export interface CardPickedEvent extends Event {
detail: {
config: LovelaceCardConfig;
};
}
export interface ViewEditEvent extends Event {
detail: {
config: LovelaceViewConfig;

View File

@ -1,10 +1,9 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../types";
import { HASSDomEvent } from "../../../../common/dom/fire_event";
import "./hui-edit-view";
import "./hui-migrate-config";
import { EditViewDialogParams } from "./show-edit-view-dialog";
declare global {
@ -39,24 +38,11 @@ export class HuiDialogEditView extends LitElement {
if (!this._params) {
return html``;
}
if (
!this._params.add &&
this._params.viewConfig &&
!("id" in this._params.viewConfig)
) {
return html`
<hui-migrate-config
.hass="${this.hass}"
@reload-lovelace="${this._params.reloadLovelace}"
></hui-migrate-config>
`;
}
return html`
<hui-edit-view
.hass="${this.hass}"
.viewConfig="${this._params.viewConfig}"
.add="${this._params.add}"
.reloadLovelace="${this._params.reloadLovelace}"
.lovelace="${this._params.lovelace}"
.viewIndex="${this._params.viewIndex}"
>
</hui-edit-view>
`;

View File

@ -1,9 +1,4 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-spinner/paper-spinner";
@ -16,40 +11,25 @@ import "@polymer/paper-icon-button/paper-icon-button.js";
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../components/hui-entity-editor";
import "./config-elements/hui-view-editor";
import { HomeAssistant } from "../../../types";
import "../../components/hui-entity-editor";
import "./hui-view-editor";
import { HomeAssistant } from "../../../../types";
import {
addView,
updateViewConfig,
LovelaceViewConfig,
LovelaceCardConfig,
} from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { EntitiesEditorEvent, ViewEditEvent } from "./types";
import { processEditorEntities } from "./process-editor-entities";
import { EntityConfig } from "../entity-rows/types";
import { confDeleteView } from "./delete-view";
import { navigate } from "../../../common/navigate";
} from "../../../../data/lovelace";
import { fireEvent } from "../../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { EntitiesEditorEvent, ViewEditEvent } from "../types";
import { processEditorEntities } from "../process-editor-entities";
import { EntityConfig } from "../../entity-rows/types";
import { navigate } from "../../../../common/navigate";
import { Lovelace } from "../../types";
import { deleteView, addView, replaceView } from "../config-util";
export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
static get properties(): PropertyDeclarations {
return {
hass: {},
viewConfig: {},
add: {},
_config: {},
_badges: {},
_cards: {},
_saving: {},
_curTab: {},
};
}
public viewConfig?: LovelaceViewConfig;
public add?: boolean;
public reloadLovelace?: () => {};
public lovelace?: Lovelace;
public viewIndex?: number;
protected hass?: HomeAssistant;
private _config?: LovelaceViewConfig;
private _badges?: EntityConfig[];
@ -58,6 +38,19 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
private _curTabIndex: number;
private _curTab?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
lovelace: {},
viewIndex: {},
_config: {},
_badges: {},
_cards: {},
_saving: {},
_curTab: {},
};
}
protected constructor() {
super();
this._saving = false;
@ -69,30 +62,21 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
if (this._dialog == null) {
await this.updateComplete;
}
this._dialog.open();
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (!changedProperties.has("viewConfig") && !changedProperties.has("add")) {
return;
}
if (
this.viewConfig &&
(!changedProperties.get("viewConfig") ||
this.viewConfig.id !==
(changedProperties.get("viewConfig") as LovelaceViewConfig).id)
) {
const { cards, badges, ...viewConfig } = this.viewConfig;
this._config = viewConfig;
this._badges = badges ? processEditorEntities(badges) : [];
this._cards = cards;
} else if (changedProperties.has("add")) {
if (this.viewIndex === undefined) {
this._config = {};
this._badges = [];
this._cards = [];
} else {
const { cards, badges, ...viewConfig } = this.lovelace!.config.views[
this.viewIndex
];
this._config = viewConfig;
this._badges = badges ? processEditorEntities(badges) : [];
this._cards = cards;
}
this._resizeDialog();
this._dialog.open();
}
private get _dialog(): PaperDialogElement {
@ -142,7 +126,7 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
<paper-dialog-scrollable> ${content} </paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${
!this.add
this.viewIndex !== undefined
? html`
<paper-icon-button
class="delete"
@ -208,23 +192,27 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
`;
}
private _save(): void {
this._saving = true;
this._updateConfigInBackend();
}
private _delete() {
private async _delete() {
if (this._cards && this._cards.length > 0) {
alert(
"You can't delete a view that has cards in it. Remove the cards first."
);
return;
}
confDeleteView(this.hass!, String(this.viewConfig!.id!), () => {
if (!confirm("Are you sure you want to delete this view?")) {
return;
}
try {
await this.lovelace!.saveConfig(
deleteView(this.lovelace!.config, this.viewIndex!)
);
this._closeDialog();
this.reloadLovelace!();
navigate(this, `/lovelace/0`);
});
} catch (err) {
alert(`Deleting failed: ${err.message}`);
}
}
private async _resizeDialog(): Promise<void> {
@ -234,9 +222,9 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
private _closeDialog(): void {
this._curTabIndex = 0;
this.lovelace = undefined;
this._config = {};
this._badges = [];
this.viewConfig = undefined;
this._dialog.close();
}
@ -248,39 +236,35 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
this._resizeDialog();
}
private async _updateConfigInBackend(): Promise<void> {
private async _save(): Promise<void> {
if (!this._config) {
return;
}
if (!this._isConfigChanged()) {
this._closeDialog();
this._saving = false;
return;
}
if (this._badges) {
this._config.badges = this._badges.map((entityConf) => {
return entityConf.entity;
});
}
this._saving = true;
const viewConf: LovelaceViewConfig = {
cards: this._cards,
badges: this._badges!.map((entityConf) => entityConf.entity),
...this._config,
};
const lovelace = this.lovelace!;
try {
if (this.add) {
this._config.cards = [];
await addView(this.hass!, this._config, "json");
} else {
await updateViewConfig(
this.hass!,
String(this.viewConfig!.id!),
this._config,
"json"
);
}
this.reloadLovelace!();
await lovelace.saveConfig(
this._creatingView
? addView(lovelace.config, viewConf)
: replaceView(lovelace.config, this.viewIndex!, viewConf)
);
this._closeDialog();
this._saving = false;
} catch (err) {
alert(`Saving failed: ${err.message}`);
} finally {
this._saving = false;
}
}
@ -299,10 +283,15 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
}
private _isConfigChanged(): boolean {
if (!this.add) {
return true;
}
return JSON.stringify(this._config) !== JSON.stringify(this.viewConfig);
return (
this._creatingView ||
JSON.stringify(this._config) !==
JSON.stringify(this.lovelace!.config.views[this.viewIndex!])
);
}
private get _creatingView(): boolean {
return this.viewIndex === undefined;
}
}

View File

@ -6,7 +6,7 @@ import { EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { configElementStyle } from "./config-elements-style";
import { configElementStyle } from "../config-elements/config-elements-style";
import "../../components/hui-theme-select-editor";
import { LovelaceViewConfig } from "../../../../data/lovelace";
@ -24,12 +24,11 @@ export class HuiViewEditor extends hassLocalizeLitMixin(LitElement) {
return { hass: {}, _config: {} };
}
get _id(): string {
get _path(): string {
if (!this._config) {
return "";
}
return "id" in this._config ? this._config.id! : "";
return this._config.path || "";
}
get _title(): string {
@ -68,12 +67,6 @@ export class HuiViewEditor extends hassLocalizeLitMixin(LitElement) {
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="ID"
value="${this._id}"
.configValue="${"id"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Title"
value="${this._title}"
@ -86,6 +79,12 @@ export class HuiViewEditor extends hassLocalizeLitMixin(LitElement) {
.configValue="${"icon"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="URL Path"
value="${this._path}"
.configValue="${"path"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"

View File

@ -1,5 +1,5 @@
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { LovelaceViewConfig } from "../../../data/lovelace";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import { Lovelace } from "../../types";
declare global {
// for fire event
@ -18,9 +18,8 @@ const dialogShowEvent = "show-edit-view";
const dialogTag = "hui-dialog-edit-view";
export interface EditViewDialogParams {
viewConfig?: LovelaceViewConfig;
add?: boolean;
reloadLovelace: () => void;
lovelace: Lovelace;
viewIndex?: number;
}
const registerEditViewDialog = (element: HTMLElement) =>

View File

@ -1,154 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-button/paper-button";
import { registerSaveDialog } from "./editor/hui-dialog-save-config";
import { fetchConfig } from "../../data/lovelace";
import "../../layouts/hass-loading-screen";
import "../../layouts/hass-error-screen";
import "./hui-root";
import localizeMixin from "../../mixins/localize-mixin";
let registeredDialog = false;
class Lovelace extends localizeMixin(PolymerElement) {
static get template() {
return html`
<style>
paper-button {
color: var(--primary-color);
font-weight: 500;
}
</style>
<template is="dom-if" if="[[_equal(_state, &quot;loaded&quot;)]]" restamp>
<hui-root
narrow="[[narrow]]"
show-menu="[[showMenu]]"
hass="[[hass]]"
route="[[route]]"
config="[[_config]]"
columns="[[_columns]]"
on-config-refresh="_forceFetchConfig"
></hui-root>
</template>
<template
is="dom-if"
if="[[_equal(_state, &quot;loading&quot;)]]"
restamp
>
<hass-loading-screen
narrow="[[narrow]]"
show-menu="[[showMenu]]"
></hass-loading-screen>
</template>
<template is="dom-if" if="[[_equal(_state, &quot;error&quot;)]]" restamp>
<hass-error-screen
title="Lovelace"
error="[[_errorMsg]]"
narrow="[[narrow]]"
show-menu="[[showMenu]]"
>
<paper-button on-click="_forceFetchConfig"
>Reload ui-lovelace.yaml</paper-button
>
</hass-error-screen>
</template>
`;
}
static get properties() {
return {
hass: Object,
narrow: {
type: Boolean,
value: false,
},
showMenu: {
type: Boolean,
value: false,
},
route: Object,
_columns: {
type: Number,
value: 1,
},
_state: {
type: String,
value: "loading",
},
_errorMsg: String,
_config: {
type: Object,
value: null,
},
};
}
static get observers() {
return ["_updateColumns(narrow, showMenu)"];
}
ready() {
this._fetchConfig(false);
this._updateColumns = this._updateColumns.bind(this);
this.mqls = [300, 600, 900, 1200].map((width) => {
const mql = matchMedia(`(min-width: ${width}px)`);
mql.addListener(this._updateColumns);
return mql;
});
this._updateColumns();
super.ready();
}
_updateColumns() {
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
// Do -1 column if the menu is docked and open
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
}
_forceFetchConfig() {
this._fetchConfig(true);
}
async _fetchConfig(force) {
try {
const conf = await fetchConfig(this.hass, force);
this.setProperties({
_config: conf,
_state: "loaded",
});
} catch (err) {
if (err.code === "file_not_found") {
const {
generateLovelaceConfig,
} = await import("./common/generate-lovelace-config");
this.setProperties({
_config: generateLovelaceConfig(this.hass, this.localize),
_state: "loaded",
});
if (!registeredDialog) {
registeredDialog = true;
registerSaveDialog(this);
}
} else {
this.setProperties({
_state: "error",
_errorMsg: err.message,
});
}
}
}
_equal(a, b) {
return a === b;
}
}
customElements.define("ha-panel-lovelace", Lovelace);

View File

@ -0,0 +1,185 @@
import "@polymer/paper-button/paper-button";
import { fetchConfig, LovelaceConfig, saveConfig } from "../../data/lovelace";
import "../../layouts/hass-loading-screen";
import "../../layouts/hass-error-screen";
import "./hui-root";
import { HomeAssistant, PanelInfo } from "../../types";
import { Lovelace } from "./types";
import { LitElement, html, PropertyValues } from "@polymer/lit-element";
import { hassLocalizeLitMixin } from "../../mixins/lit-localize-mixin";
import { TemplateResult } from "lit-html";
import { showSaveDialog } from "./editor/show-save-config-dialog";
interface LovelacePanelConfig {
mode: "yaml" | "storage";
}
class LovelacePanel extends hassLocalizeLitMixin(LitElement) {
public panel?: PanelInfo<LovelacePanelConfig>;
public hass?: HomeAssistant;
public narrow?: boolean;
public showMenu?: boolean;
public route?: object;
private _columns?: number;
private _state?: "loading" | "loaded" | "error";
private _errorMsg?: string;
private lovelace?: Lovelace;
private mqls?: MediaQueryList[];
static get properties() {
return {
hass: {},
lovelace: {},
narrow: { type: Boolean, value: false },
showMenu: { type: Boolean, value: false },
route: {},
_columns: { type: Number, value: 1 },
_state: { type: String, value: "loading" },
_errorMsg: String,
_config: { type: {}, value: null },
};
}
public render(): TemplateResult {
const state = this._state!;
if (state === "loaded") {
return html`
<hui-root
.narrow="${this.narrow}"
.showMenu="${this.showMenu}"
.hass="${this.hass}"
.lovelace="${this.lovelace}"
.route="${this.route}"
.columns="${this._columns}"
@config-refresh="${this._forceFetchConfig}"
></hui-root>
`;
}
if (state === "error") {
return html`
<style>
paper-button {
color: var(--primary-color);
font-weight: 500;
}
</style>
<hass-error-screen
title="Lovelace"
.error="${this._errorMsg}"
.narrow="${this.narrow}"
.showMenu="${this.showMenu}"
>
<paper-button on-click="_forceFetchConfig"
>Reload ui-lovelace.yaml</paper-button
>
</hass-error-screen>
`;
}
return html`
<hass-loading-screen
.narrow="${this.narrow}"
.showMenu="${this.showMenu}"
></hass-loading-screen>
`;
}
public updated(changedProps: PropertyValues): void {
if (changedProps.has("narrow") || changedProps.has("showMenu")) {
this._updateColumns();
}
}
public firstUpdated() {
this._fetchConfig(false);
this._updateColumns = this._updateColumns.bind(this);
this.mqls = [300, 600, 900, 1200].map((width) => {
const mql = matchMedia(`(min-width: ${width}px)`);
mql.addListener(this._updateColumns);
return mql;
});
this._updateColumns();
}
private _updateColumns() {
const matchColumns = this.mqls!.reduce(
(cols, mql) => cols + Number(mql.matches),
0
);
// Do -1 column if the menu is docked and open
this._columns = Math.max(
1,
matchColumns - Number(!this.narrow && this.showMenu)
);
}
private _forceFetchConfig() {
this._fetchConfig(true);
}
private async _fetchConfig(force) {
let conf;
let gen: boolean;
try {
conf = await fetchConfig(this.hass!, force);
gen = false;
} catch (err) {
if (err.code !== "config_not_found") {
// tslint:disable-next-line
console.log(err);
this._state = "error";
this._errorMsg = err.message;
return;
}
const {
generateLovelaceConfig,
} = await import("./common/generate-lovelace-config");
conf = generateLovelaceConfig(this.hass!, this.localize);
gen = true;
}
this._state = "loaded";
this.lovelace = {
config: conf,
autoGen: gen,
editMode: this.lovelace ? this.lovelace.editMode : false,
mode: this.panel!.config.mode,
setEditMode: (editMode: boolean) => {
if (!editMode || !this.lovelace!.autoGen) {
this._updateLovelace({ editMode });
return;
}
showSaveDialog(this, {
lovelace: this.lovelace!,
});
},
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
const { config, autoGen } = this.lovelace!;
try {
// Optimistic update
this._updateLovelace({ config: newConfig, autoGen: false });
await saveConfig(this.hass!, newConfig);
} catch (err) {
// tslint:disable-next-line
console.error(err);
// Rollback the optimistic update
this._updateLovelace({ config, autoGen });
throw err;
}
},
};
}
private _updateLovelace(props: Partial<Lovelace>) {
this.lovelace = {
...this.lovelace!,
...props,
};
}
}
customElements.define("ha-panel-lovelace", LovelacePanel);

View File

@ -32,8 +32,7 @@ import "./hui-unused-entities";
import "./hui-view";
import debounce from "../../common/util/debounce";
import createCardElement from "./common/create-card-element";
import { showSaveDialog } from "./editor/hui-dialog-save-config";
import { showEditViewDialog } from "./editor/show-edit-view-dialog";
import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog";
// CSS and JS should only be imported once. Modules and HTML are safe.
const CSS_CACHE = {};
@ -181,8 +180,12 @@ class HUIRoot extends NavigateMixin(
},
config: {
type: Object,
computed: "_computeConfig(lovelace)",
observer: "_configChanged",
},
lovelace: {
type: Object,
},
columns: {
type: Number,
observer: "_columnsChanged",
@ -216,6 +219,7 @@ class HUIRoot extends NavigateMixin(
_editMode: {
type: Boolean,
value: false,
computed: "_computeEditMode(lovelace)",
observer: "_editModeChanged",
},
@ -258,12 +262,12 @@ class HUIRoot extends NavigateMixin(
_routeChanged(route) {
const views = this.config && this.config.views;
if (route.path === "" && route.prefix === "/lovelace" && views) {
this.navigate(`/lovelace/${views[0].id || 0}`, true);
this.navigate(`/lovelace/${views[0].path || 0}`, true);
} else if (this.routeData.view) {
const view = this.routeData.view;
let index = 0;
for (let i = 0; i < views.length; i++) {
if (views[i].id === view || i === parseInt(view)) {
if (views[i].path === view || i === parseInt(view)) {
index = i;
break;
}
@ -272,8 +276,8 @@ class HUIRoot extends NavigateMixin(
}
}
_computeViewId(id, index) {
return id || index;
_computeViewPath(path, index) {
return path || index;
}
_computeTitle(config) {
@ -305,17 +309,7 @@ class HUIRoot extends NavigateMixin(
}
_editModeEnable() {
if (this.config._frontendAuto) {
showSaveDialog(this, {
config: this.config,
reloadLovelace: () => {
this.fire("config-refresh");
this._editMode = true;
},
});
return;
}
this._editMode = true;
this.lovelace.setEditMode(true);
if (this.config.views.length < 2) {
this.$.view.classList.remove("tabs-hidden");
this.fire("iron-resize");
@ -323,7 +317,7 @@ class HUIRoot extends NavigateMixin(
}
_editModeDisable() {
this._editMode = false;
this.lovelace.setEditMode(false);
if (this.config.views.length < 2) {
this.$.view.classList.add("tabs-hidden");
this.fire("iron-resize");
@ -336,20 +330,14 @@ class HUIRoot extends NavigateMixin(
_editView() {
showEditViewDialog(this, {
viewConfig: this.config.views[this._curView],
add: false,
reloadLovelace: () => {
this.fire("config-refresh");
},
lovelace: this.lovelace,
viewIndex: this._curView,
});
}
_addView() {
showEditViewDialog(this, {
add: true,
reloadLovelace: () => {
this.fire("config-refresh");
},
lovelace: this.lovelace,
});
}
@ -360,8 +348,8 @@ class HUIRoot extends NavigateMixin(
_navigateView(viewIndex) {
if (viewIndex !== this._curView) {
const id = this.config.views[viewIndex].id || viewIndex;
this.navigate(`/lovelace/${id}`);
const path = this.config.views[viewIndex].path || viewIndex;
this.navigate(`/lovelace/${path}`);
}
scrollToTarget(this, this.$.layout.header.scrollTarget);
}
@ -390,12 +378,12 @@ class HUIRoot extends NavigateMixin(
if (viewConfig.panel) {
view = createCardElement(viewConfig.cards[0]);
view.isPanel = true;
view.editMode = this._editMode;
} else {
view = document.createElement("hui-view");
view.lovelace = this.lovelace;
view.config = viewConfig;
view.columns = this.columns;
view.editMode = this._editMode;
view.index = viewIndex;
}
if (viewConfig.background) background = viewConfig.background;
}
@ -452,5 +440,13 @@ class HUIRoot extends NavigateMixin(
}
});
}
_computeConfig(lovelace) {
return lovelace ? lovelace.config : null;
}
_computeEditMode(lovelace) {
return lovelace ? lovelace.editMode : false;
}
}
customElements.define("hui-root", HUIRoot);

View File

@ -11,7 +11,7 @@ import EventsMixin from "../../mixins/events-mixin";
import localizeMixin from "../../mixins/localize-mixin";
import createCardElement from "./common/create-card-element";
import { computeCardSize } from "./common/compute-card-size";
import { showEditCardDialog } from "./editor/show-edit-card-dialog";
import { showEditCardDialog } from "./editor/card-editor/show-edit-card-dialog";
class HUIView extends localizeMixin(EventsMixin(PolymerElement)) {
static get template() {
@ -82,7 +82,7 @@ class HUIView extends localizeMixin(EventsMixin(PolymerElement)) {
<div id="badges"></div>
<div id="columns"></div>
<paper-fab
hidden$="{{!editMode}}"
hidden$="[[!lovelace.editMode]]"
elevated="2"
icon="hass:plus"
title=[[localize("ui.panel.lovelace.editor.edit_card.add")]]
@ -97,9 +97,11 @@ class HUIView extends localizeMixin(EventsMixin(PolymerElement)) {
type: Object,
observer: "_hassChanged",
},
lovelace: Object,
config: Object,
columns: Number,
editMode: Boolean,
index: Number,
};
}
@ -119,11 +121,8 @@ class HUIView extends localizeMixin(EventsMixin(PolymerElement)) {
_addCard() {
showEditCardDialog(this, {
viewId: "id" in this.config ? String(this.config.id) : undefined,
add: true,
reloadLovelace: () => {
this.fire("config-refresh");
},
lovelace: this.lovelace,
path: [this.index],
});
}
@ -169,23 +168,23 @@ class HUIView extends localizeMixin(EventsMixin(PolymerElement)) {
const elements = [];
const elementsToAppend = [];
for (const cardConfig of config.cards) {
config.cards.forEach((cardConfig, cardIndex) => {
const element = createCardElement(cardConfig);
element.hass = this.hass;
elements.push(element);
if (!this.editMode) {
if (!this.lovelace.editMode) {
elementsToAppend.push(element);
continue;
return;
}
const wrapper = document.createElement("hui-card-options");
wrapper.hass = this.hass;
wrapper.cardConfig = cardConfig;
wrapper.editMode = this.editMode;
wrapper.lovelace = this.lovelace;
wrapper.path = [this.index, cardIndex];
wrapper.appendChild(element);
elementsToAppend.push(wrapper);
}
});
let columns = [];
const columnEntityCount = [];

View File

@ -1,5 +1,14 @@
import { HomeAssistant } from "../../types";
import { LovelaceCardConfig } from "../../data/lovelace";
import { LovelaceCardConfig, LovelaceConfig } from "../../data/lovelace";
export interface Lovelace {
config: LovelaceConfig;
editMode: boolean;
autoGen: boolean;
mode: "yaml" | "storage";
setEditMode: (editMode: boolean) => void;
saveConfig: (newConfig: LovelaceConfig) => Promise<void>;
}
export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant;

View File

@ -159,3 +159,11 @@ export type GroupEntity = HassEntityBase & {
control?: "hidden";
};
};
export interface PanelInfo<T = unknown> {
component_name: string;
icon?: string;
title?: string;
url_path: string;
config: T;
}