mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 17:26:42 +00:00
Add UI to create and manage Lovelace dashboards and resources (#5012)
* Add UI to create and manage Lovelace dashboards and resources * update, comments, fixes * Align icons with seach icon and checkboxes * Fix * Remove js and html resource types * Allow it for existing ones
This commit is contained in:
parent
33d65bcefc
commit
5646045e9e
@ -571,6 +571,18 @@ export class HaDataTable extends BaseElement {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon:first-child ha-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon:first-child state-badge {
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@ -598,10 +610,6 @@ export class HaDataTable extends BaseElement {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* custom from here */
|
||||
|
||||
:host {
|
||||
@ -615,27 +623,39 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
.mdc-data-table__header-cell {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.mdc-data-table__header-cell span {
|
||||
position: relative;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell.sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon)
|
||||
span {
|
||||
position: relative;
|
||||
left: -24px;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted > * {
|
||||
.mdc-data-table__header-cell > * {
|
||||
transition: left 0.2s ease 0s;
|
||||
}
|
||||
.mdc-data-table__header-cell ha-icon {
|
||||
top: 15px;
|
||||
position: absolute;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted ha-icon {
|
||||
left: -36px;
|
||||
left: -20px;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon):hover
|
||||
.mdc-data-table__header-cell:not(.not-sorted) span,
|
||||
.mdc-data-table__header-cell.not-sorted:hover span {
|
||||
left: 24px;
|
||||
}
|
||||
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric:not(.not-sorted)
|
||||
span,
|
||||
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric.not-sorted:hover
|
||||
span {
|
||||
left: 0px;
|
||||
left: 12px;
|
||||
}
|
||||
.mdc-data-table__header-cell:not(.not-sorted) ha-icon,
|
||||
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
|
||||
left: 0px;
|
||||
left: 12px;
|
||||
}
|
||||
.table-header {
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
|
@ -1,12 +1,23 @@
|
||||
import { customElement, CSSResult, css } from "lit-element";
|
||||
import { customElement, CSSResult, css, html } from "lit-element";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@material/mwc-dialog";
|
||||
import { style } from "@material/mwc-dialog/mwc-dialog-css";
|
||||
// tslint:disable-next-line
|
||||
import { Dialog } from "@material/mwc-dialog";
|
||||
import { Constructor } from "../types";
|
||||
import { Constructor, HomeAssistant } from "../types";
|
||||
// tslint:disable-next-line
|
||||
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
||||
|
||||
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
||||
${title}
|
||||
<paper-icon-button
|
||||
aria-label=${hass.localize("ui.dialogs.generic.close")}
|
||||
icon="hass:close"
|
||||
dialogAction="close"
|
||||
class="close_button"
|
||||
></paper-icon-button>
|
||||
`;
|
||||
|
||||
@customElement("ha-dialog")
|
||||
export class HaDialog extends MwcDialog {
|
||||
protected static get styles(): CSSResult[] {
|
||||
@ -19,6 +30,15 @@ export class HaDialog extends MwcDialog {
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertial-align-dialog, center);
|
||||
}
|
||||
.mdc-dialog__title::before {
|
||||
display: block;
|
||||
height: 20px;
|
||||
}
|
||||
.close_button {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 12px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -12,10 +12,47 @@ export interface LovelaceConfig {
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export type LovelaceResources = Array<{
|
||||
export interface LovelaceResource {
|
||||
id: string;
|
||||
type: "css" | "js" | "module" | "html";
|
||||
url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface LovelaceResourcesMutableParams {
|
||||
res_type: "css" | "js" | "module" | "html";
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type LovelaceDashboard =
|
||||
| LovelaceYamlDashboard
|
||||
| LovelaceStorageDashboard;
|
||||
|
||||
interface LovelaceGenericDashboard {
|
||||
id: string;
|
||||
url_path: string;
|
||||
require_admin: boolean;
|
||||
sidebar?: { icon: string; title: string };
|
||||
}
|
||||
|
||||
export interface LovelaceYamlDashboard extends LovelaceGenericDashboard {
|
||||
mode: "yaml";
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface LovelaceStorageDashboard extends LovelaceGenericDashboard {
|
||||
mode: "storage";
|
||||
}
|
||||
|
||||
export interface LovelaceDashboardMutableParams {
|
||||
require_admin: boolean;
|
||||
sidebar: { icon: string; title: string } | null;
|
||||
}
|
||||
|
||||
export interface LovelaceDashboardCreateParams
|
||||
extends LovelaceDashboardMutableParams {
|
||||
url_path: string;
|
||||
mode: "storage";
|
||||
}
|
||||
|
||||
export interface LovelaceViewConfig {
|
||||
index?: number;
|
||||
@ -111,10 +148,70 @@ type LovelaceUpdatedEvent = HassEventBase & {
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchResources = (conn: Connection): Promise<LovelaceResources> =>
|
||||
export const fetchResources = (conn: Connection): Promise<LovelaceResource[]> =>
|
||||
conn.sendMessagePromise({
|
||||
type: "lovelace/resources",
|
||||
});
|
||||
|
||||
export const createResource = (
|
||||
hass: HomeAssistant,
|
||||
values: LovelaceResourcesMutableParams
|
||||
) =>
|
||||
hass.callWS<LovelaceResource>({
|
||||
type: "lovelace/resources/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updateResource = (
|
||||
hass: HomeAssistant,
|
||||
id: string,
|
||||
updates: Partial<LovelaceResourcesMutableParams>
|
||||
) =>
|
||||
hass.callWS<LovelaceResource>({
|
||||
type: "lovelace/resources/update",
|
||||
resource_id: id,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteResource = (hass: HomeAssistant, id: string) =>
|
||||
hass.callWS({
|
||||
type: "lovelace/resources/delete",
|
||||
resource_id: id,
|
||||
});
|
||||
|
||||
export const fetchDashboards = (
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceDashboard[]> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/dashboards/list",
|
||||
});
|
||||
|
||||
export const createDashboard = (
|
||||
hass: HomeAssistant,
|
||||
values: LovelaceDashboardCreateParams
|
||||
) =>
|
||||
hass.callWS<LovelaceDashboard>({
|
||||
type: "lovelace/dashboards/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updateDashboard = (
|
||||
hass: HomeAssistant,
|
||||
id: string,
|
||||
updates: Partial<LovelaceDashboardMutableParams>
|
||||
) =>
|
||||
hass.callWS<LovelaceDashboard>({
|
||||
type: "lovelace/dashboards/update",
|
||||
dashboard_id: id,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteDashboard = (hass: HomeAssistant, id: string) =>
|
||||
hass.callWS({
|
||||
type: "lovelace/dashboards/delete",
|
||||
dashboard_id: id,
|
||||
});
|
||||
|
||||
export const fetchConfig = (
|
||||
conn: Connection,
|
||||
urlPath: string | null,
|
||||
@ -125,6 +222,7 @@ export const fetchConfig = (
|
||||
url_path: urlPath,
|
||||
force,
|
||||
});
|
||||
|
||||
export const saveConfig = (
|
||||
hass: HomeAssistant,
|
||||
urlPath: string | null,
|
||||
@ -174,7 +272,7 @@ export const getLovelaceCollection = (
|
||||
|
||||
export interface WindowWithLovelaceProm extends Window {
|
||||
llConfProm?: Promise<LovelaceConfig>;
|
||||
llResProm?: Promise<LovelaceResources>;
|
||||
llResProm?: Promise<LovelaceResource[]>;
|
||||
}
|
||||
|
||||
export interface ActionHandlerOptions {
|
||||
|
@ -15,6 +15,7 @@ import { Route, HomeAssistant } from "../types";
|
||||
import { navigate } from "../common/navigate";
|
||||
import "@material/mwc-ripple";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
export interface PageNavigation {
|
||||
path: string;
|
||||
@ -22,7 +23,7 @@ export interface PageNavigation {
|
||||
component?: string;
|
||||
name?: string;
|
||||
core?: boolean;
|
||||
exportOnly?: boolean;
|
||||
advancedOnly?: boolean;
|
||||
icon?: string;
|
||||
info?: any;
|
||||
}
|
||||
@ -33,12 +34,57 @@ class HassTabsSubpage extends LitElement {
|
||||
@property({ type: String, attribute: "back-path" }) public backPath?: string;
|
||||
@property() public backCallback?: () => void;
|
||||
@property({ type: Boolean }) public hassio = false;
|
||||
@property({ type: Boolean }) public showAdvanced = false;
|
||||
@property() public route!: Route;
|
||||
@property() public tabs!: PageNavigation[];
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@property() private _activeTab: number = -1;
|
||||
|
||||
private _getTabs = memoizeOne(
|
||||
(
|
||||
tabs: PageNavigation[],
|
||||
activeTab: number,
|
||||
showAdvanced: boolean | undefined,
|
||||
_components,
|
||||
_language
|
||||
) => {
|
||||
const shownTabs = tabs.filter(
|
||||
(page) =>
|
||||
(!page.component ||
|
||||
page.core ||
|
||||
isComponentLoaded(this.hass, page.component)) &&
|
||||
(!page.advancedOnly || showAdvanced)
|
||||
);
|
||||
|
||||
return shownTabs.map(
|
||||
(page, index) => html`
|
||||
<div
|
||||
class="tab ${classMap({
|
||||
active: index === activeTab,
|
||||
})}"
|
||||
@click=${this._tabTapped}
|
||||
.path=${page.path}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-icon .icon=${page.icon}></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
${!this.narrow || index === activeTab
|
||||
? html`
|
||||
<span class="name"
|
||||
>${page.translationKey
|
||||
? this.hass.localize(page.translationKey)
|
||||
: name}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
<mwc-ripple></mwc-ripple>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("route")) {
|
||||
@ -49,6 +95,14 @@ class HassTabsSubpage extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const tabs = this._getTabs(
|
||||
this.tabs,
|
||||
this._activeTab,
|
||||
this.hass.userData?.showAdvanced,
|
||||
this.hass.config.components,
|
||||
this.hass.language
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="toolbar">
|
||||
<ha-paper-icon-button-arrow-prev
|
||||
@ -61,41 +115,13 @@ class HassTabsSubpage extends LitElement {
|
||||
<div main-title><slot name="header"></slot></div>
|
||||
`
|
||||
: ""}
|
||||
<div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}>
|
||||
${this.tabs.map((page, index) =>
|
||||
(!page.component ||
|
||||
page.core ||
|
||||
isComponentLoaded(this.hass, page.component)) &&
|
||||
(!page.exportOnly || this.showAdvanced)
|
||||
? html`
|
||||
<div
|
||||
class="tab ${classMap({
|
||||
active: index === this._activeTab,
|
||||
})}"
|
||||
@click=${this._tabTapped}
|
||||
.path=${page.path}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-icon .icon=${page.icon}></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
${!this.narrow || index === this._activeTab
|
||||
? html`
|
||||
<span class="name"
|
||||
>${page.translationKey
|
||||
? this.hass.localize(page.translationKey)
|
||||
: name}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
<mwc-ripple></mwc-ripple>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
)}
|
||||
</div>
|
||||
|
||||
${tabs.length > 1 || !this.narrow
|
||||
? html`
|
||||
<div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}>
|
||||
${tabs}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div id="toolbar-icon">
|
||||
<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@ class HaConfigNavigation extends LitElement {
|
||||
(!page.component ||
|
||||
page.core ||
|
||||
isComponentLoaded(this.hass, page.component)) &&
|
||||
(!page.exportOnly || this.showAdvanced)
|
||||
(!page.advancedOnly || this.showAdvanced)
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/${page.component}`}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
TemplateResult,
|
||||
property,
|
||||
@ -22,6 +21,7 @@ import {
|
||||
fetchDeviceConditions,
|
||||
fetchDeviceActions,
|
||||
} from "../../../../data/device_automation";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
|
||||
@customElement("dialog-device-automation")
|
||||
export class DialogDeviceAutomation extends LitElement {
|
||||
@ -129,16 +129,7 @@ export class DialogDeviceAutomation extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-title-ink-color: var(--primary-text-color);
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 600px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
return haStyleDialog;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import "@polymer/app-route/app-route";
|
||||
|
||||
import "./ha-config-devices-dashboard";
|
||||
import "./ha-config-device-page";
|
||||
import { compare } from "../../../common/string/compare";
|
||||
|
@ -75,6 +75,14 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
core: true,
|
||||
},
|
||||
],
|
||||
lovelace: [
|
||||
{
|
||||
component: "lovelace",
|
||||
path: "/config/lovelace/dashboards",
|
||||
translationKey: "ui.panel.config.lovelace.caption",
|
||||
icon: "hass:view-dashboard",
|
||||
},
|
||||
],
|
||||
persons: [
|
||||
{
|
||||
component: "person",
|
||||
@ -117,7 +125,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
translationKey: "ui.panel.config.customize.caption",
|
||||
icon: "hass:pencil",
|
||||
core: true,
|
||||
exportOnly: true,
|
||||
advancedOnly: true,
|
||||
},
|
||||
],
|
||||
other: [
|
||||
@ -217,6 +225,13 @@ class HaPanelConfig extends HassRouterPage {
|
||||
/* webpackChunkName: "panel-config-integrations" */ "./integrations/ha-config-integrations"
|
||||
),
|
||||
},
|
||||
lovelace: {
|
||||
tag: "ha-config-lovelace",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-config-lovelace" */ "./lovelace/ha-config-lovelace"
|
||||
),
|
||||
},
|
||||
person: {
|
||||
tag: "ha-config-person",
|
||||
load: () =>
|
||||
|
@ -25,6 +25,7 @@ import "./forms/ha-input_select-form";
|
||||
import "./forms/ha-input_number-form";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
|
||||
const HELPERS = {
|
||||
input_boolean: createInputBoolean,
|
||||
@ -156,37 +157,18 @@ export class DialogHelperDetail extends LitElement {
|
||||
this._platform = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-title-ink-color: var(--primary-text-color);
|
||||
--justify-action-buttons: space-between;
|
||||
}
|
||||
ha-dialog.button-left {
|
||||
--justify-action-buttons: flex-start;
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 600px;
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog.button-left {
|
||||
--justify-action-buttons: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* make dialog fullscreen on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-height: 100vh;
|
||||
--mdc-dialog-shape-radius: 0px;
|
||||
--vertial-align-dialog: flex-end;
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,262 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../../components/ha-icon-input";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
LovelaceDashboard,
|
||||
LovelaceDashboardMutableParams,
|
||||
LovelaceDashboardCreateParams,
|
||||
} from "../../../../data/lovelace";
|
||||
import { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { PolymerChangedEvent } from "../../../../polymer-types";
|
||||
import { HaSwitch } from "../../../../components/ha-switch";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() private _params?: LovelaceDashboardDetailsDialogParams;
|
||||
@property() private _urlPath!: LovelaceDashboard["url_path"];
|
||||
@property() private _showSidebar!: boolean;
|
||||
@property() private _sidebarIcon!: string;
|
||||
@property() private _sidebarTitle!: string;
|
||||
@property() private _requireAdmin!: LovelaceDashboard["require_admin"];
|
||||
|
||||
@property() private _error?: string;
|
||||
@property() private _submitting = false;
|
||||
|
||||
public async showDialog(
|
||||
params: LovelaceDashboardDetailsDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (this._params.dashboard) {
|
||||
this._urlPath = this._params.dashboard.url_path || "";
|
||||
this._showSidebar = !!this._params.dashboard.sidebar;
|
||||
this._sidebarIcon = this._params.dashboard.sidebar?.icon || "";
|
||||
this._sidebarTitle = this._params.dashboard.sidebar?.title || "";
|
||||
this._requireAdmin = this._params.dashboard.require_admin || false;
|
||||
} else {
|
||||
this._urlPath = "";
|
||||
this._showSidebar = true;
|
||||
this._sidebarIcon = "";
|
||||
this._sidebarTitle = "";
|
||||
this._requireAdmin = false;
|
||||
}
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
const urlInvalid = !/^[a-zA-Z0-9_-]+$/.test(this._urlPath);
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closing="${this._close}"
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.dashboard
|
||||
? this._sidebarTitle ||
|
||||
this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.edit_dashboard"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.new_dashboard"
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`
|
||||
<div class="error">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-switch
|
||||
.checked=${this._showSidebar}
|
||||
@change=${this._showSidebarChanged}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.show_sidebar"
|
||||
)}</ha-switch
|
||||
>
|
||||
${this._showSidebar
|
||||
? html`
|
||||
<ha-icon-input
|
||||
.value=${this._sidebarIcon}
|
||||
@value-changed=${this._sidebarIconChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.icon"
|
||||
)}
|
||||
></ha-icon-input>
|
||||
<paper-input
|
||||
.value=${this._sidebarTitle}
|
||||
@value-changed=${this._sidebarTitleChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.title"
|
||||
)}
|
||||
@blur=${this._fillUrlPath}
|
||||
></paper-input>
|
||||
`
|
||||
: ""}
|
||||
${!this._params.dashboard
|
||||
? html`
|
||||
<paper-input
|
||||
.value=${this._urlPath}
|
||||
@value-changed=${this._urlChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.url"
|
||||
)}
|
||||
.errorMessage=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.url_error_msg"
|
||||
)}
|
||||
.invalid=${urlInvalid}
|
||||
></paper-input>
|
||||
`
|
||||
: ""}
|
||||
<ha-switch
|
||||
.checked=${this._requireAdmin}
|
||||
@change=${this._requireAdminChanged}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.require_admin"
|
||||
)}</ha-switch
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
${this._params.dashboard
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click="${this._deleteDashboard}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.delete"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateDashboard}"
|
||||
.disabled=${urlInvalid || this._submitting}
|
||||
>
|
||||
${this._params.dashboard
|
||||
? this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.update"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.create"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _urlChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._urlPath = ev.detail.value;
|
||||
}
|
||||
|
||||
private _sidebarIconChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._sidebarIcon = ev.detail.value;
|
||||
}
|
||||
|
||||
private _sidebarTitleChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._sidebarTitle = ev.detail.value;
|
||||
}
|
||||
|
||||
private _fillUrlPath() {
|
||||
if (this._urlPath) {
|
||||
return;
|
||||
}
|
||||
const parts = this._sidebarTitle.split(" ");
|
||||
|
||||
if (parts.length) {
|
||||
this._urlPath = parts[0].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
private _showSidebarChanged(ev: Event) {
|
||||
this._showSidebar = (ev.target as HaSwitch).checked;
|
||||
}
|
||||
|
||||
private _requireAdminChanged(ev: Event) {
|
||||
this._requireAdmin = (ev.target as HaSwitch).checked;
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
const values: Partial<LovelaceDashboardMutableParams> = {
|
||||
require_admin: this._requireAdmin,
|
||||
sidebar: this._showSidebar
|
||||
? { icon: this._sidebarIcon, title: this._sidebarTitle }
|
||||
: null,
|
||||
};
|
||||
if (this._params!.dashboard) {
|
||||
await this._params!.updateDashboard(values);
|
||||
} else {
|
||||
(values as LovelaceDashboardCreateParams).url_path = this._urlPath.trim();
|
||||
(values as LovelaceDashboardCreateParams).mode = "storage";
|
||||
await this._params!.createDashboard(
|
||||
values as LovelaceDashboardCreateParams
|
||||
);
|
||||
}
|
||||
this._params = undefined;
|
||||
} catch (err) {
|
||||
this._error = err?.message || "Unknown error";
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteDashboard() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeDashboard()) {
|
||||
this._close();
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
ha-switch {
|
||||
padding: 16px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-lovelace-dashboard-detail": DialogLovelaceDashboardDetail;
|
||||
}
|
||||
}
|
@ -0,0 +1,276 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import memoize from "memoize-one";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../../types";
|
||||
import {
|
||||
LovelaceDashboard,
|
||||
fetchDashboards,
|
||||
createDashboard,
|
||||
updateDashboard,
|
||||
deleteDashboard,
|
||||
LovelaceDashboardCreateParams,
|
||||
} from "../../../../data/lovelace";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { compare } from "../../../../common/string/compare";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showAlertDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
|
||||
@customElement("ha-config-lovelace-dashboards")
|
||||
export class HaConfigLovelaceDashboards extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public isWide!: boolean;
|
||||
@property() public narrow!: boolean;
|
||||
@property() public route!: Route;
|
||||
@property() private _dashboards: LovelaceDashboard[] = [];
|
||||
|
||||
private _columns = memoize(
|
||||
(_language, dashboards): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (icon) =>
|
||||
icon
|
||||
? html`
|
||||
<ha-icon slot="item-icon" .icon=${icon}></ha-icon>
|
||||
`
|
||||
: html``,
|
||||
},
|
||||
title: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.title"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
},
|
||||
mode: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
template: (mode) =>
|
||||
html`
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${mode}`
|
||||
) || mode}
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
if (dashboards.some((dashboard) => dashboard.mode === "yaml")) {
|
||||
columns.filename = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.filename"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
};
|
||||
}
|
||||
|
||||
const columns2: DataTableColumnContainer = {
|
||||
require_admin: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.require_admin"
|
||||
),
|
||||
sortable: true,
|
||||
type: "icon",
|
||||
template: (requireAdmin: boolean) =>
|
||||
requireAdmin
|
||||
? html`
|
||||
<ha-icon icon="hass:check-circle-outline"></ha-icon>
|
||||
`
|
||||
: html`
|
||||
-
|
||||
`,
|
||||
},
|
||||
sidebar: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
|
||||
),
|
||||
type: "icon",
|
||||
template: (sidebar) =>
|
||||
sidebar
|
||||
? html`
|
||||
<ha-icon icon="hass:check-circle-outline"></ha-icon>
|
||||
`
|
||||
: html`
|
||||
-
|
||||
`,
|
||||
},
|
||||
url_path: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
filterable: true,
|
||||
template: (urlPath) =>
|
||||
html`
|
||||
<mwc-button .urlPath=${urlPath} @click=${this._navigate}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}</mwc-button
|
||||
>
|
||||
`,
|
||||
},
|
||||
};
|
||||
return { ...columns, ...columns2 };
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = memoize((dashboards: LovelaceDashboard[]) => {
|
||||
return dashboards.map((dashboard) => {
|
||||
return {
|
||||
filename: "",
|
||||
...dashboard,
|
||||
icon: dashboard.sidebar?.icon,
|
||||
title: dashboard.sidebar?.title || dashboard.url_path,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || this._dashboards === undefined) {
|
||||
return html`
|
||||
<hass-loading-screen></hass-loading-screen>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${lovelaceTabs}
|
||||
.columns=${this._columns(this.hass.language, this._dashboards)}
|
||||
.data=${this._getItems(this._dashboards)}
|
||||
@row-click=${this._editDashboard}
|
||||
>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.add_dashboard"
|
||||
)}"
|
||||
@click=${this._addDashboard}
|
||||
></ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._getDashboards();
|
||||
}
|
||||
|
||||
private async _getDashboards() {
|
||||
this._dashboards = await fetchDashboards(this.hass);
|
||||
}
|
||||
|
||||
private _navigate(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const url = `/${(ev.target as any).urlPath}`;
|
||||
navigate(this, url);
|
||||
}
|
||||
|
||||
private _editDashboard(ev: CustomEvent) {
|
||||
const id = (ev.detail as RowClickedEvent).id;
|
||||
const dashboard = id
|
||||
? this._dashboards.find((res) => res.id === id)
|
||||
: undefined;
|
||||
if (!dashboard) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._openDialog(dashboard);
|
||||
}
|
||||
|
||||
private _addDashboard() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private async _openDialog(dashboard?: LovelaceDashboard): Promise<void> {
|
||||
showDashboardDetailDialog(this, {
|
||||
dashboard,
|
||||
createDashboard: async (values: LovelaceDashboardCreateParams) => {
|
||||
const created = await createDashboard(this.hass!, values);
|
||||
this._dashboards = this._dashboards!.concat(
|
||||
created
|
||||
).sort((res1, res2) => compare(res1.url_path, res2.url_path));
|
||||
},
|
||||
updateDashboard: async (values) => {
|
||||
const updated = await updateDashboard(
|
||||
this.hass!,
|
||||
dashboard!.id,
|
||||
values
|
||||
);
|
||||
this._dashboards = this._dashboards!.map((res) =>
|
||||
res === dashboard ? updated : res
|
||||
);
|
||||
},
|
||||
removeDashboard: async () => {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.confirm_delete"
|
||||
),
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteDashboard(this.hass!, dashboard!.id);
|
||||
this._dashboards = this._dashboards!.filter(
|
||||
(res) => res !== dashboard
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
ha-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import {
|
||||
LovelaceDashboard,
|
||||
LovelaceDashboardMutableParams,
|
||||
LovelaceDashboardCreateParams,
|
||||
} from "../../../../data/lovelace";
|
||||
|
||||
export interface LovelaceDashboardDetailsDialogParams {
|
||||
dashboard?: LovelaceDashboard;
|
||||
createDashboard: (values: LovelaceDashboardCreateParams) => Promise<unknown>;
|
||||
updateDashboard: (
|
||||
updates: Partial<LovelaceDashboardMutableParams>
|
||||
) => Promise<unknown>;
|
||||
removeDashboard: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadDashboardDetailDialog = () =>
|
||||
import(
|
||||
/* webpackChunkName: "lovelace-dashboard-detail-dialog" */ "./dialog-lovelace-dashboard-detail"
|
||||
);
|
||||
|
||||
export const showDashboardDetailDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: LovelaceDashboardDetailsDialogParams
|
||||
) => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-lovelace-dashboard-detail",
|
||||
dialogImport: loadDashboardDetailDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
63
src/panels/config/lovelace/ha-config-lovelace.ts
Normal file
63
src/panels/config/lovelace/ha-config-lovelace.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../layouts/hass-router-page";
|
||||
import { property, customElement } from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
export const lovelaceTabs = [
|
||||
{
|
||||
component: "lovelace",
|
||||
path: "/config/lovelace/dashboards",
|
||||
translationKey: "ui.panel.config.lovelace.dashboards.caption",
|
||||
icon: "hass:view-dashboard",
|
||||
},
|
||||
{
|
||||
component: "lovelace",
|
||||
path: "/config/lovelace/resources",
|
||||
translationKey: "ui.panel.config.lovelace.resources.caption",
|
||||
icon: "hass:file-multiple",
|
||||
advancedOnly: true,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("ha-config-lovelace")
|
||||
class HaConfigLovelace extends HassRouterPage {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public narrow!: boolean;
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboards",
|
||||
routes: {
|
||||
dashboards: {
|
||||
tag: "ha-config-lovelace-dashboards",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-config-lovelace-dashboards" */ "./dashboards/ha-config-lovelace-dashboards"
|
||||
),
|
||||
cache: true,
|
||||
},
|
||||
resources: {
|
||||
tag: "ha-config-lovelace-resources",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-config-lovelace-resources" */ "./resources/ha-config-lovelace-resources"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(pageEl) {
|
||||
pageEl.hass = this.hass;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.route = this.routeTail;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-lovelace": HaConfigLovelace;
|
||||
}
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
LovelaceResource,
|
||||
LovelaceResourcesMutableParams,
|
||||
} from "../../../../data/lovelace";
|
||||
import { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail";
|
||||
import { PolymerChangedEvent } from "../../../../polymer-types";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
|
||||
@customElement("dialog-lovelace-resource-detail")
|
||||
export class DialogLovelaceResourceDetail extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() private _params?: LovelaceResourceDetailsDialogParams;
|
||||
@property() private _url!: LovelaceResource["url"];
|
||||
@property() private _type!: LovelaceResource["type"];
|
||||
@property() private _error?: string;
|
||||
@property() private _submitting = false;
|
||||
|
||||
public async showDialog(
|
||||
params: LovelaceResourceDetailsDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (this._params.resource) {
|
||||
this._url = this._params.resource.url || "";
|
||||
this._type = this._params.resource.type || "module";
|
||||
} else {
|
||||
this._url = "";
|
||||
this._type = "module";
|
||||
}
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
const urlInvalid = this._url.trim() === "";
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closing=${this._close}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.resource
|
||||
? this._params.resource.url
|
||||
: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.detail.new_resource"
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`
|
||||
<div class="error">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<h3 class="warning">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.detail.warning_header"
|
||||
)}
|
||||
</h3>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.detail.warning_text"
|
||||
)}
|
||||
<paper-input
|
||||
.value=${this._url}
|
||||
@value-changed=${this._urlChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.detail.url"
|
||||
)}
|
||||
.errorMessage=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.detail.url_error_msg"
|
||||
)}
|
||||
.invalid=${urlInvalid}
|
||||
></paper-input>
|
||||
<br />
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.detail.type"
|
||||
)}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${this._type}
|
||||
@iron-select=${this._typeChanged}
|
||||
attr-for-selected="type"
|
||||
>
|
||||
<paper-item type="module">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.types.module"
|
||||
)}
|
||||
</paper-item>
|
||||
${this._type === "js"
|
||||
? html`
|
||||
<paper-item type="js">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.types.js"
|
||||
)}
|
||||
</paper-item>
|
||||
`
|
||||
: ""}
|
||||
<paper-item type="css">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.types.css"
|
||||
)}
|
||||
</paper-item>
|
||||
${this._type === "html"
|
||||
? html`
|
||||
<paper-item type="html">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.types.html"
|
||||
)}
|
||||
</paper-item>
|
||||
`
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
${this._params.resource
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click="${this._deleteResource}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.detail.delete"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateResource}"
|
||||
.disabled=${urlInvalid || this._submitting}
|
||||
>
|
||||
${this._params.resource
|
||||
? this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.detail.update"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.detail.create"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _urlChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._url = ev.detail.value;
|
||||
}
|
||||
|
||||
private _typeChanged(ev: CustomEvent) {
|
||||
this._type = ev.detail.item.getAttribute("type");
|
||||
}
|
||||
|
||||
private async _updateResource() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
const values: LovelaceResourcesMutableParams = {
|
||||
url: this._url.trim(),
|
||||
res_type: this._type,
|
||||
};
|
||||
if (this._params!.resource) {
|
||||
await this._params!.updateResource(values);
|
||||
} else {
|
||||
await this._params!.createResource(values);
|
||||
}
|
||||
this._params = undefined;
|
||||
} catch (err) {
|
||||
this._error = err?.message || "Unknown error";
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteResource() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeResource()) {
|
||||
this._close();
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-lovelace-resource-detail": DialogLovelaceResourceDetail;
|
||||
}
|
||||
}
|
@ -0,0 +1,209 @@
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import memoize from "memoize-one";
|
||||
import "../../../../common/search/search-input";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../../types";
|
||||
import {
|
||||
LovelaceResource,
|
||||
fetchResources,
|
||||
createResource,
|
||||
updateResource,
|
||||
deleteResource,
|
||||
} from "../../../../data/lovelace";
|
||||
import { showResourceDetailDialog } from "./show-dialog-lovelace-resource-detail";
|
||||
import { compare } from "../../../../common/string/compare";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showAlertDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { loadLovelaceResources } from "../../../lovelace/common/load-resources";
|
||||
|
||||
@customElement("ha-config-lovelace-resources")
|
||||
export class HaConfigLovelaceRescources extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public isWide!: boolean;
|
||||
@property() public narrow!: boolean;
|
||||
@property() public route!: Route;
|
||||
@property() private _resources: LovelaceResource[] = [];
|
||||
|
||||
private _columns = memoize(
|
||||
(_language): DataTableColumnContainer => {
|
||||
return {
|
||||
url: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.resources.picker.headers.url"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
},
|
||||
type: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.resources.picker.headers.type"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
template: (type) =>
|
||||
html`
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.resources.types.${type}`
|
||||
) || type}
|
||||
`,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || this._resources === undefined) {
|
||||
return html`
|
||||
<hass-loading-screen></hass-loading-screen>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${lovelaceTabs}
|
||||
.columns=${this._columns(this.hass.language)}
|
||||
.data=${this._resources}
|
||||
@row-click=${this._editResource}
|
||||
>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.lovelace.resources.picker.add_resource"
|
||||
)}"
|
||||
@click=${this._addResource}
|
||||
></ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._getResources();
|
||||
}
|
||||
|
||||
private async _getResources() {
|
||||
this._resources = await fetchResources(this.hass.connection);
|
||||
}
|
||||
|
||||
private _editResource(ev: CustomEvent) {
|
||||
if ((this.hass.panels.lovelace?.config as any)?.mode !== "storage") {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.cant_edit_yaml"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const id = (ev.detail as RowClickedEvent).id;
|
||||
const resource = this._resources.find((res) => res.id === id);
|
||||
this._openDialog(resource);
|
||||
}
|
||||
|
||||
private _addResource() {
|
||||
if ((this.hass.panels.lovelace?.config as any)?.mode !== "storage") {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.cant_edit_yaml"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private async _openDialog(resource?: LovelaceResource): Promise<void> {
|
||||
showResourceDetailDialog(this, {
|
||||
resource,
|
||||
createResource: async (values) => {
|
||||
const created = await createResource(this.hass!, values);
|
||||
this._resources = this._resources!.concat(created).sort((res1, res2) =>
|
||||
compare(res1.url, res2.url)
|
||||
);
|
||||
loadLovelaceResources(this._resources, this.hass!.auth.data.hassUrl);
|
||||
},
|
||||
updateResource: async (values) => {
|
||||
const updated = await updateResource(this.hass!, resource!.id, values);
|
||||
this._resources = this._resources!.map((res) =>
|
||||
res === resource ? updated : res
|
||||
);
|
||||
loadLovelaceResources(this._resources, this.hass!.auth.data.hassUrl);
|
||||
},
|
||||
removeResource: async () => {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.confirm_delete"
|
||||
),
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteResource(this.hass!, resource!.id);
|
||||
this._resources = this._resources!.filter((res) => res !== resource);
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.refresh_header"
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.refresh_body"
|
||||
),
|
||||
confirm: () => location.reload(),
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
ha-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import {
|
||||
LovelaceResource,
|
||||
LovelaceResourcesMutableParams,
|
||||
} from "../../../../data/lovelace";
|
||||
|
||||
export interface LovelaceResourceDetailsDialogParams {
|
||||
resource?: LovelaceResource;
|
||||
createResource: (values: LovelaceResourcesMutableParams) => Promise<unknown>;
|
||||
updateResource: (
|
||||
updates: Partial<LovelaceResourcesMutableParams>
|
||||
) => Promise<unknown>;
|
||||
removeResource: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadResourceDetailDialog = () =>
|
||||
import(
|
||||
/* webpackChunkName: "lovelace-resource-detail-dialog" */ "./dialog-lovelace-resource-detail"
|
||||
);
|
||||
|
||||
export const showResourceDetailDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: LovelaceResourceDetailsDialogParams
|
||||
) => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-lovelace-resource-detail",
|
||||
dialogImport: loadResourceDetailDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@ -13,11 +13,12 @@ import "@material/mwc-button";
|
||||
|
||||
import "../../../components/entity/ha-entities-picker";
|
||||
import "../../../components/user/ha-user-picker";
|
||||
import "../../../components/ha-dialog";
|
||||
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { PersonMutableParams } from "../../../data/person";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
|
||||
class DialogPersonDetail extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@ -55,26 +56,18 @@ class DialogPersonDetail extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
const nameInvalid = this._name.trim() === "";
|
||||
const title = html`
|
||||
${this._params.entry
|
||||
? this._params.entry.name
|
||||
: this.hass!.localize("ui.panel.config.person.detail.new_person")}
|
||||
<paper-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.dismiss"
|
||||
)}
|
||||
icon="hass:close"
|
||||
dialogAction="close"
|
||||
style="position: absolute; right: 16px; top: 12px;"
|
||||
></paper-icon-button>
|
||||
`;
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closing="${this._close}"
|
||||
scrimClickAction=""
|
||||
escapeKeyAction=""
|
||||
.heading=${title}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.entry
|
||||
? this._params.entry.name
|
||||
: this.hass!.localize("ui.panel.config.person.detail.new_person")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
@ -236,34 +229,14 @@ class DialogPersonDetail extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 400px;
|
||||
--mdc-dialog-max-width: 600px;
|
||||
--mdc-dialog-title-ink-color: var(--primary-text-color);
|
||||
--justify-action-buttons: space-between;
|
||||
}
|
||||
/* make dialog fullscreen on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-height: 100vh;
|
||||
--mdc-dialog-shape-radius: 0px;
|
||||
--vertial-align-dialog: flex-end;
|
||||
}
|
||||
}
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
ha-user-picker {
|
||||
margin-top: 16px;
|
||||
}
|
||||
mwc-button.warning {
|
||||
--mdc-theme-primary: var(--google-red-500);
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import "@material/mwc-button";
|
||||
|
||||
import "../../../components/map/ha-location-editor";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-dialog";
|
||||
|
||||
import { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@ -23,6 +22,8 @@ import {
|
||||
getZoneEditorInitData,
|
||||
} from "../../../data/zone";
|
||||
import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
|
||||
class DialogZoneDetail extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@ -72,19 +73,6 @@ class DialogZoneDetail extends LitElement {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
const title = html`
|
||||
${this._params.entry
|
||||
? this._params.entry.name
|
||||
: this.hass!.localize("ui.panel.config.zone.detail.new_zone")}
|
||||
<paper-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.dismiss"
|
||||
)}
|
||||
icon="hass:close"
|
||||
dialogAction="close"
|
||||
style="position: absolute; right: 16px; top: 12px;"
|
||||
></paper-icon-button>
|
||||
`;
|
||||
const nameValid = this._name.trim() === "";
|
||||
const iconValid = !this._icon.trim().includes(":");
|
||||
const latValid = String(this._latitude) === "";
|
||||
@ -100,7 +88,12 @@ class DialogZoneDetail extends LitElement {
|
||||
@closing="${this._close}"
|
||||
scrimClickAction=""
|
||||
escapeKeyAction=""
|
||||
.heading=${title}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.entry
|
||||
? this._params.entry.name
|
||||
: this.hass!.localize("ui.panel.config.zone.detail.new_zone")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
@ -277,26 +270,8 @@ class DialogZoneDetail extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-title-ink-color: var(--primary-text-color);
|
||||
--justify-action-buttons: space-between;
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
/* make dialog fullscreen on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-height: 100vh;
|
||||
--mdc-dialog-shape-radius: 0px;
|
||||
--vertial-align-dialog: flex-end;
|
||||
}
|
||||
}
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
color: var(--primary-text-color);
|
||||
@ -320,12 +295,6 @@ class DialogZoneDetail extends LitElement {
|
||||
ha-user-picker {
|
||||
margin-top: 16px;
|
||||
}
|
||||
mwc-button.warning {
|
||||
--mdc-theme-primary: var(--google-red-500);
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { loadModule, loadCSS, loadJS } from "../../../common/dom/load_resource";
|
||||
|
||||
import { LovelaceResources } from "../../../data/lovelace";
|
||||
import { LovelaceResource } from "../../../data/lovelace";
|
||||
|
||||
// CSS and JS should only be imported once. Modules and HTML are safe.
|
||||
const CSS_CACHE = {};
|
||||
const JS_CACHE = {};
|
||||
|
||||
export const loadLovelaceResources = (
|
||||
resources: NonNullable<LovelaceResources>,
|
||||
resources: NonNullable<LovelaceResource[]>,
|
||||
hassUrl: string
|
||||
) =>
|
||||
resources.forEach((resource) => {
|
||||
|
@ -180,4 +180,27 @@ export const haStyleDialog = css`
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
/* mwc-dialog (ha-dialog) styles */
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 400px;
|
||||
--mdc-dialog-max-width: 600px;
|
||||
--mdc-dialog-title-ink-color: var(--primary-text-color);
|
||||
--justify-action-buttons: space-between;
|
||||
}
|
||||
/* make dialog fullscreen on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-height: 100vh;
|
||||
--mdc-dialog-shape-radius: 0px;
|
||||
--vertial-align-dialog: flex-end;
|
||||
}
|
||||
}
|
||||
mwc-button.warning {
|
||||
--mdc-theme-primary: var(--google-red-500);
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
`;
|
||||
|
@ -595,7 +595,8 @@
|
||||
"generic": {
|
||||
"cancel": "Cancel",
|
||||
"ok": "OK",
|
||||
"default_confirmation_title": "Are you sure?"
|
||||
"default_confirmation_title": "Are you sure?",
|
||||
"close": "close"
|
||||
},
|
||||
"more_info_control": {
|
||||
"dismiss": "Dismiss dialog",
|
||||
@ -847,6 +848,76 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lovelace": {
|
||||
"caption": "Lovelace Dashboards",
|
||||
"description": "Configure your Lovelace Dashboards",
|
||||
"dashboards": {
|
||||
"caption": "Dashboards",
|
||||
"conf_mode": {
|
||||
"yaml": "YAML file",
|
||||
"storage": "UI controlled"
|
||||
},
|
||||
"picker": {
|
||||
"headers": {
|
||||
"title": "Title",
|
||||
"conf_mode": "Configuration method",
|
||||
"require_admin": "Admin only",
|
||||
"sidebar": "Show in sidebar",
|
||||
"filename": "Filename"
|
||||
},
|
||||
"open": "Open dashboard",
|
||||
"add_dashboard": "Add dashboard"
|
||||
},
|
||||
"confirm_delete": "Are you sure you want to delete this dashboard?",
|
||||
"cant_edit_yaml": "Dashboards defined in YAML can not be edited from the UI. Change them in configuration.yaml.",
|
||||
"detail": {
|
||||
"edit_dashboard": "Edit dashboard",
|
||||
"new_dashboard": "Add new dashboard",
|
||||
"dismiss": "Close",
|
||||
"show_sidebar": "Show in sidebar",
|
||||
"icon": "Sidebar icon",
|
||||
"title": "Sidebar title",
|
||||
"url": "Url",
|
||||
"url_error_msg": "The url can not contain spaces or special characters, except for _ and -",
|
||||
"require_admin": "Admin only",
|
||||
"delete": "Delete",
|
||||
"update": "Update",
|
||||
"create": "Create"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"caption": "Resources",
|
||||
"types": {
|
||||
"css": "Stylesheet",
|
||||
"html": "HTML (deprecated)",
|
||||
"js": "JavaScript File (deprecated)",
|
||||
"module": "JavaScript Module"
|
||||
},
|
||||
"picker": {
|
||||
"headers": {
|
||||
"url": "Url",
|
||||
"type": "Type"
|
||||
},
|
||||
"add_resource": "Add resource"
|
||||
},
|
||||
"confirm_delete": "Are you sure you want to delete this resource?",
|
||||
"refresh_header": "Do you want to refresh?",
|
||||
"refresh_body": "You have to refresh the page to complete the removal, do you want to refresh now?",
|
||||
"cant_edit_yaml": "You are using Lovelace in YAML mode, therefore you can not manage your resources through the UI. Manage them in configuration.yaml.",
|
||||
"detail": {
|
||||
"new_resource": "Add new resource",
|
||||
"dismiss": "Close",
|
||||
"warning_header": "Be cautious!",
|
||||
"warning_text": "Adding resources can be dangerous, make sure you know the source of the resource and trust them. Bad resources could seriously harm your system.",
|
||||
"url": "Url",
|
||||
"url_error_msg": "Url is a required field",
|
||||
"type": "Resource type",
|
||||
"delete": "Delete",
|
||||
"update": "Update",
|
||||
"create": "Create"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_control": {
|
||||
"caption": "Server Controls",
|
||||
"description": "Restart and stop the Home Assistant server",
|
||||
|
Loading…
x
Reference in New Issue
Block a user