Getting started on Configuration Changes (#12309)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Zack Barett 2022-04-20 16:57:51 -05:00 committed by GitHub
parent aa562c21a8
commit cabe10ffdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 742 additions and 387 deletions

View File

@ -12,7 +12,7 @@ export const isNavigationClick = (e: MouseEvent) => {
const anchor = e
.composedPath()
.filter((n) => (n as HTMLElement).tagName === "A")[0] as
.find((n) => (n as HTMLElement).tagName === "A") as
| HTMLAnchorElement
| undefined;
if (

View File

@ -2,12 +2,12 @@ import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { Analytics, AnalyticsPreferences } from "../data/analytics";
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import type { HomeAssistant } from "../types";
import "./ha-settings-row";
import "./ha-switch";
import type { HaSwitch } from "./ha-switch";
const ADDITIONAL_PREFERENCES = [
{
@ -40,62 +40,62 @@ export class HaAnalytics extends LitElement {
return html`
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${baseEnabled}
.preference=${"base"}
.disabled=${loading}
name="base"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="base"> Basic analytics </span>
<span slot="description" data-for="base">
This includes information about your system.
</span>
<ha-switch
@change=${this._handleRowClick}
.checked=${baseEnabled}
.preference=${"base"}
.disabled=${loading}
name="base"
>
</ha-switch>
</ha-settings-row>
${ADDITIONAL_PREFERENCES.map(
(preference) =>
html`<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics?.preferences[preference.key]}
.preference=${preference.key}
name=${preference.key}
>
</ha-checkbox>
${!baseEnabled
? html`<paper-tooltip animation-delay="0" position="right">
You need to enable basic analytics for this option to be
available
</paper-tooltip>`
: ""}
</span>
<span slot="heading" data-for=${preference.key}>
${preference.title}
</span>
<span slot="description" data-for=${preference.key}>
${preference.description}
</span>
</ha-settings-row>`
html`
<ha-settings-row>
<span slot="heading" data-for=${preference.key}>
${preference.title}
</span>
<span slot="description" data-for=${preference.key}>
${preference.description}
</span>
<span>
<ha-switch
@change=${this._handleRowClick}
.checked=${this.analytics?.preferences[preference.key]}
.preference=${preference.key}
name=${preference.key}
>
</ha-switch>
${!baseEnabled
? html`
<paper-tooltip animation-delay="0" position="right">
You need to enable basic analytics for this option to be
available
</paper-tooltip>
`
: ""}
</span>
</ha-settings-row>
`
)}
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics?.preferences.diagnostics}
.preference=${"diagnostics"}
.disabled=${loading}
name="diagnostics"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="diagnostics"> Diagnostics </span>
<span slot="description" data-for="diagnostics">
Share crash reports when unexpected errors occur.
</span>
<ha-switch
@change=${this._handleRowClick}
.checked=${this.analytics?.preferences.diagnostics}
.preference=${"diagnostics"}
.disabled=${loading}
name="diagnostics"
>
</ha-switch>
</ha-settings-row>
`;
}
@ -120,23 +120,23 @@ export class HaAnalytics extends LitElement {
});
}
private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const preference = (checkbox as any).preference;
private _handleRowClick(ev: Event) {
const target = ev.currentTarget as HaSwitch;
const preference = (target as any).preference;
const preferences = this.analytics ? { ...this.analytics.preferences } : {};
if (preferences[preference] === checkbox.checked) {
if (preferences[preference] === target.checked) {
return;
}
preferences[preference] = checkbox.checked;
preferences[preference] = target.checked;
if (
ADDITIONAL_PREFERENCES.some((entry) => entry.key === preference) &&
checkbox.checked
target.checked
) {
preferences.base = true;
} else if (preference === "base" && !checkbox.checked) {
} else if (preference === "base" && !target.checked) {
preferences.usage = false;
preferences.statistics = false;
}

View File

@ -0,0 +1,69 @@
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
import { styles } from "@material/mwc-list/mwc-list-item.css";
import { css, CSSResult, html } from "lit";
import { customElement, property, query } from "lit/decorators";
@customElement("ha-clickable-list-item")
export class HaClickableListItem extends ListItemBase {
@property() public href?: string;
@property({ type: Boolean }) public disableHref = false;
// property used only in css
@property({ type: Boolean, reflect: true }) public rtl = false;
@query("a") private _anchor!: HTMLAnchorElement;
public render() {
const r = super.render();
const href = this.href || "";
return html`${this.disableHref
? html`<a aria-role="option">${r}</a>`
: html`<a aria-role="option" href=${href}>${r}</a>`}`;
}
firstUpdated() {
super.firstUpdated();
this.addEventListener("keydown", (ev) => {
if (ev.key === "Enter" || ev.key === " ") {
this._anchor.click();
}
});
}
static get styles(): CSSResult[] {
return [
styles,
css`
:host {
padding-left: 0px;
padding-right: 0px;
}
:host([rtl]) span {
margin-left: var(--mdc-list-item-graphic-margin, 20px) !important;
margin-right: 0px !important;
}
:host([graphic="avatar"]:not([twoLine])),
:host([graphic="icon"]:not([twoLine])) {
height: 48px;
}
a {
width: 100%;
height: 100%;
display: flex;
align-items: center;
padding-left: var(--mdc-list-side-padding, 20px);
padding-right: var(--mdc-list-side-padding, 20px);
font-weight: 500;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-clickable-list-item": HaClickableListItem;
}
}

View File

@ -0,0 +1,92 @@
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../types";
import "./ha-icon-next";
import "./ha-svg-icon";
import "./ha-clickable-list-item";
@customElement("ha-navigation-list")
class HaNavigationList extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public pages!: PageNavigation[];
@property({ type: Boolean }) public hasSecondary = false;
public render(): TemplateResult {
return html`
<mwc-list>
${this.pages.map(
(page) => html`
<ha-clickable-list-item
graphic="avatar"
.twoline=${this.hasSecondary}
.hasMeta=${!this.narrow}
@click=${this._entryClicked}
href=${page.path}
>
<div
slot="graphic"
class=${page.iconColor ? "icon-background" : ""}
.style="background-color: ${page.iconColor || "undefined"}"
>
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
</div>
<span>${page.name}</span>
${this.hasSecondary
? html`<span slot="secondary">${page.description}</span>`
: ""}
${!this.narrow
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""}
</ha-clickable-list-item>
`
)}
</mwc-list>
`;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
static styles: CSSResultGroup = css`
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
height: 24px;
width: 24px;
}
ha-svg-icon {
padding: 8px;
}
.icon-background {
border-radius: 50%;
}
.icon-background ha-svg-icon {
color: #fff;
}
mwc-list-item {
cursor: pointer;
font-size: var(--navigation-list-item-title-font-size);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-navigation-list": HaNavigationList;
}
}

View File

@ -19,9 +19,9 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { HomeAssistant } from "../../types";
import "../ha-input-helper-text";
import "./ha-map";
import type { HaMap } from "./ha-map";
import "../ha-input-helper-text";
declare global {
// for fire event
@ -297,7 +297,7 @@ export class HaLocationsEditor extends LitElement {
return css`
ha-map {
display: block;
height: 300px;
height: 100%;
}
`;
}

View File

@ -202,7 +202,7 @@ class HaConfigAreaPage extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.tabs=${configSections.devices}
.tabs=${configSections.areas}
.route=${this.route}
>
${this.narrow

View File

@ -82,7 +82,7 @@ export class HaConfigAreasDashboard extends LitElement {
.narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config"
.tabs=${configSections.devices}
.tabs=${configSections.areas}
.route=${this.route}
>
<ha-icon-button

View File

@ -34,7 +34,6 @@ import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { configSections } from "../ha-panel-config";
@customElement("ha-config-backup")
class HaConfigBackup extends LitElement {
@ -129,13 +128,15 @@ class HaConfigBackup extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
back-path="/config/system"
.route=${this.route}
.tabs=${configSections.backup}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._getItems(this._backupData.backups)}
.noDataText=${this.hass.localize("ui.panel.config.backup.no_bakcups")}
>
<span slot="header"
>${this.hass.localize("ui.panel.config.backup.caption")}</span
>
<ha-fab
slot="fab"
?disabled=${this._backupData.backing_up}

View File

@ -38,7 +38,7 @@ class ConfigAnalytics extends LitElement {
: undefined;
return html`
<ha-card header="Analytics">
<ha-card outlined>
<div class="card-content">
${error ? html`<div class="error">${error}</div>` : ""}
<p>

View File

@ -3,11 +3,11 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../layouts/hass-tabs-subpage";
import "../../../layouts/hass-subpage";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import { configSections } from "../ha-panel-config";
import "./ha-config-section-core";
import "./ha-config-core-form";
import "./ha-config-name-form";
/*
* @appliesMixin LocalizeMixin
@ -17,36 +17,29 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) {
return html`
<style include="iron-flex ha-style">
.content {
padding-bottom: 32px;
}
.border {
margin: 32px auto 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
.narrow .border {
max-width: 640px;
ha-config-name-form,
ha-config-core-form {
display: block;
margin-top: 24px;
}
</style>
<hass-tabs-subpage
<hass-subpage
hass="[[hass]]"
narrow="[[narrow]]"
route="[[route]]"
back-path="/config"
tabs="[[_computeTabs()]]"
show-advanced="[[showAdvanced]]"
header="[[localize('ui.panel.config.core.caption')]]"
back-path="/config/system"
>
<div class$="[[computeClasses(isWide)]]">
<ha-config-section-core
is-wide="[[isWide]]"
show-advanced="[[showAdvanced]]"
hass="[[hass]]"
></ha-config-section-core>
<div class="content">
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
</div>
</hass-tabs-subpage>
</hass-subpage>
`;
}
@ -59,14 +52,6 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) {
route: Object,
};
}
_computeTabs() {
return configSections.general;
}
computeClasses(isWide) {
return isWide ? "content" : "content narrow";
}
}
customElements.define("ha-config-core", HaConfigCore);

View File

@ -9,6 +9,7 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-checkbox";
import "../../../components/ha-network";
@ -28,7 +29,7 @@ class ConfigNetwork extends LitElement {
@state() private _networkConfig?: NetworkConfig;
@state() private _error?: string;
@state() private _error?: { code: string; message: string };
protected render(): TemplateResult {
if (
@ -39,9 +40,15 @@ class ConfigNetwork extends LitElement {
}
return html`
<ha-card header="Network">
<ha-card outlined header="Network">
<div class="card-content">
${this._error ? html`<div class="error">${this._error}</div>` : ""}
${this._error
? html`
<ha-alert alert-type="error"
>${this._error.message || this._error.code}</ha-alert
>
`
: ""}
<p>
Configure which network adapters integrations will use. Currently
this setting only affects multicast traffic. A restart is required

View File

@ -0,0 +1,43 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-analytics";
@customElement("ha-config-section-analytics")
class HaConfigSectionAnalytics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
protected render(): TemplateResult {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.analytics.caption")}
>
<div class="content">
<ha-config-analytics .hass=${this.hass}></ha-config-analytics>
</div>
</hass-subpage>
`;
}
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-analytics": HaConfigSectionAnalytics;
}
}

View File

@ -1,70 +0,0 @@
import "@material/mwc-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import "../ha-config-section";
import "./ha-config-analytics";
import "./ha-config-core-form";
import "./ha-config-name-form";
import "./ha-config-network";
import "./ha-config-url-form";
/*
* @appliesMixin LocalizeMixin
*/
class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<ha-config-section is-wide="[[isWide]]">
<span slot="header"
>[[localize('ui.panel.config.core.section.core.header')]]</span
>
<span slot="introduction"
>[[localize('ui.panel.config.core.section.core.introduction')]]</span
>
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
<ha-config-url-form hass="[[hass]]"></ha-config-url-form>
<ha-config-network hass="[[hass]]"></ha-config-network>
<ha-config-analytics hass="[[hass]]"></ha-config-analytics>
</ha-config-section>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
isWide: {
type: Boolean,
value: false,
},
validating: {
type: Boolean,
value: false,
},
isValid: {
type: Boolean,
value: null,
},
validateLog: {
type: String,
value: "",
},
showAdvanced: Boolean,
};
}
}
customElements.define("ha-config-section-core", HaConfigSectionCore);

View File

@ -0,0 +1,49 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-network";
import "./ha-config-url-form";
@customElement("ha-config-section-network")
class HaConfigSectionNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
protected render(): TemplateResult {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.network.caption")}
>
<div class="content">
<ha-config-url-form .hass=${this.hass}></ha-config-url-form>
<ha-config-network .hass=${this.hass}></ha-config-network>
</div>
</hass-subpage>
`;
}
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-config-network {
display: block;
margin-top: 24px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-network": HaConfigSectionNetwork;
}
}

View File

@ -0,0 +1,40 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-analytics";
@customElement("ha-config-section-storage")
class HaConfigSectionStorage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
protected render(): TemplateResult {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
>
<div class="content"></div>
</hass-subpage>
`;
}
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-storage": HaConfigSectionStorage;
}
}

View File

@ -0,0 +1,115 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-card";
import "../../../components/ha-navigation-list";
import { CloudStatus } from "../../../data/cloud";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
@customElement("ha-config-system-navigation")
class HaConfigSystemNavigation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ type: Boolean }) public showAdvanced!: boolean;
protected render(): TemplateResult {
const pages = configSections.general.map((page) => ({
...page,
name: page.translationKey
? this.hass.localize(page.translationKey)
: page.name,
}));
return html`
<hass-subpage
back-path="/config"
.header=${this.hass.localize("ui.panel.config.dashboard.system.title")}
>
<ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
full-width
>
<ha-card>
${this.narrow
? html`<div class="title">
${this.hass.localize(
"ui.panel.config.dashboard.system.title"
)}
</div>`
: ""}
<ha-navigation-list
.hass=${this.hass}
.narrow=${this.narrow}
.pages=${pages}
></ha-navigation-list>
</ha-card>
</ha-config-section>
</hass-subpage>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-card {
margin-bottom: env(safe-area-inset-bottom);
}
:host(:not([narrow])) ha-card {
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
ha-config-section {
margin: auto;
margin-top: -32px;
max-width: 600px;
}
ha-card {
overflow: hidden;
}
ha-card a {
text-decoration: none;
color: var(--primary-text-color);
}
.title {
font-size: 16px;
padding: 16px;
padding-bottom: 0;
}
:host([narrow]) ha-card {
border-radius: 0;
box-shadow: unset;
}
:host([narrow]) ha-config-section {
margin-top: -42px;
}
ha-navigation-list {
--navigation-list-item-title-font-size: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-system-navigation": HaConfigSystemNavigation;
}
}

View File

@ -9,17 +9,17 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-switch";
import { isIPAddress } from "../../../common/string/is_ip_address";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-formfield";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { CloudStatus, fetchCloudStatus } from "../../../data/cloud";
import { saveCoreConfig } from "../../../data/core";
import type { PolymerChangedEvent } from "../../../polymer-types";
import type { HomeAssistant } from "../../../types";
import { isIPAddress } from "../../../common/string/is_ip_address";
@customElement("ha-config-url-form")
class ConfigUrlForm extends LitElement {
@ -74,7 +74,10 @@ class ConfigUrlForm extends LitElement {
}
return html`
<ha-card .header=${this.hass.localize("ui.panel.config.url.caption")}>
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.url.caption")}
>
<div class="card-content">
${!canEdit
? html`
@ -335,6 +338,7 @@ class ConfigUrlForm extends LitElement {
a {
color: var(--primary-color);
text-decoration: none;
}
`;
}

View File

@ -34,6 +34,7 @@ import { updateCanInstall, UpdateEntity } from "../../../data/update";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import "../../../layouts/ha-app-layout";
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@ -119,10 +120,26 @@ class HaConfigDashboard extends LitElement {
private _notifyUpdates = false;
private _pages = memoizeOne((clouStatus, isLoaded) => {
const pages: PageNavigation[] = [];
if (clouStatus && isLoaded) {
pages.push({
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
});
}
return [...pages, ...configSections.dashboard];
});
protected render(): TemplateResult {
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
this.hass.states
);
return html`
<ha-app-layout>
<app-header fixed slot="header">
@ -175,30 +192,14 @@ class HaConfigDashboard extends LitElement {
${this.hass.localize("panel.config")}
</div>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
},
]}
></ha-config-navigation>
`
: ""}
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
.pages=${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud")
)}
></ha-config-navigation>
</ha-card>
<div class="tips">

View File

@ -1,13 +1,14 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { canShowPage } from "../../../common/config/can_show_page";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-navigation-list";
import type { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../../../types";
@customElement("ha-config-navigation")
class HaConfigNavigation extends LitElement {
@ -15,129 +16,71 @@ class HaConfigNavigation extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property() public showAdvanced!: boolean;
@property() public pages!: PageNavigation[];
@property({ attribute: false }) public pages!: PageNavigation[];
protected render(): TemplateResult {
const pages = this.pages
.filter((page) =>
page.path === "#external-app-configuration"
? this.hass.auth.external?.config.hasSettingsScreen
: canShowPage(this.hass, page)
)
.map((page) => ({
...page,
name:
page.name ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.title`
),
description:
page.component === "cloud" && (page.info as CloudStatus)
? page.info.logged_in
? `
${this.hass.localize(
"ui.panel.config.cloud.description_login",
"email",
(page.info as CloudStatusLoggedIn).email
)}
`
: `
${this.hass.localize(
"ui.panel.config.cloud.description_features"
)}
`
: `
${
page.description ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.description`
)
}
`,
}));
return html`
${this.pages.map((page) =>
(
page.path === "#external-app-configuration"
? this.hass.auth.external?.config.hasSettingsScreen
: canShowPage(this.hass, page)
)
? html`
<a href=${page.path} role="option" tabindex="-1">
<paper-icon-item @click=${this._entryClicked}>
<div
class=${page.iconColor ? "icon-background" : ""}
slot="item-icon"
.style="background-color: ${page.iconColor || "undefined"}"
>
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
</div>
<paper-item-body two-line>
${page.name ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.title`
)}
${page.component === "cloud" && (page.info as CloudStatus)
? page.info.logged_in
? html`
<div secondary>
${this.hass.localize(
"ui.panel.config.cloud.description_login",
"email",
(page.info as CloudStatusLoggedIn).email
)}
</div>
`
: html`
<div secondary>
${this.hass.localize(
"ui.panel.config.cloud.description_features"
)}
</div>
`
: html`
<div secondary>
${page.description ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.description`
)}
</div>
`}
</paper-item-body>
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
</paper-icon-item>
</a>
`
: ""
)}
<ha-navigation-list
hasSecondary
.hass=${this.hass}
.narrow=${this.narrow}
.pages=${pages}
@click=${this._entryClicked}
></ha-navigation-list>
`;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
if (
ev.currentTarget.parentElement.href.endsWith(
"#external-app-configuration"
)
) {
const anchor = ev
.composedPath()
.find((n) => (n as HTMLElement).tagName === "A") as
| HTMLAnchorElement
| undefined;
if (anchor?.href?.endsWith("#external-app-configuration")) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
}
static get styles(): CSSResultGroup {
return css`
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
height: 24px;
width: 24px;
}
ha-svg-icon {
padding: 8px;
}
.iron-selected paper-item::before,
a:not(.iron-selected):focus::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
transition: opacity 15ms linear;
will-change: opacity;
}
a:not(.iron-selected):focus::before {
background-color: currentColor;
opacity: var(--dark-divider-opacity);
}
.iron-selected paper-item:focus::before,
.iron-selected:focus paper-item::before {
opacity: 0.2;
}
.icon-background {
border-radius: 50%;
}
.icon-background ha-svg-icon {
color: #fff;
}
`;
}
}
declare global {

View File

@ -4,12 +4,15 @@ import {
mdiBadgeAccountHorizontal,
mdiCellphoneCog,
mdiCog,
mdiCpu32Bit,
mdiDevices,
mdiHomeAssistant,
mdiInformation,
mdiInformationOutline,
mdiLightningBolt,
mdiMapMarkerRadius,
mdiMathLog,
mdiNetwork,
mdiNfcVariant,
mdiPalette,
mdiPaletteSwatch,
@ -20,6 +23,7 @@ import {
mdiShape,
mdiSofa,
mdiTools,
mdiUpdate,
mdiViewDashboard,
} from "@mdi/js";
import { PolymerElement } from "@polymer/polymer";
@ -58,11 +62,11 @@ export const configSections: { [name: string]: PageNavigation[] } = {
core: true,
},
{
path: "/config/blueprint",
translationKey: "blueprints",
iconPath: mdiPaletteSwatch,
iconColor: "#64B5F6",
component: "blueprint",
path: "/config/areas",
translationKey: "areas",
iconPath: mdiSofa,
iconColor: "#E48629",
components: ["zone"],
},
{
path: "/config/backup",
@ -74,8 +78,8 @@ export const configSections: { [name: string]: PageNavigation[] } = {
{
path: "/hassio",
translationKey: "supervisor",
iconPath: mdiHomeAssistant,
iconColor: "#4084CD",
iconPath: mdiPuzzle,
iconColor: "#F1C447",
component: "hassio",
},
{
@ -97,7 +101,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
translationKey: "people",
iconPath: mdiAccount,
iconColor: "#E48629",
components: ["person", "zone", "users"],
components: ["person", "users"],
},
{
path: "#external-app-configuration",
@ -106,9 +110,16 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#8E24AA",
},
{
path: "/config/server_control",
translationKey: "settings",
path: "/config/system",
translationKey: "system",
iconPath: mdiCog,
iconColor: "#301ABE",
core: true,
},
{
path: "/config/info",
translationKey: "about",
iconPath: mdiInformationOutline,
iconColor: "#4A5963",
core: true,
},
@ -148,11 +159,11 @@ export const configSections: { [name: string]: PageNavigation[] } = {
core: true,
},
{
component: "areas",
path: "/config/areas",
translationKey: "ui.panel.config.areas.caption",
iconPath: mdiSofa,
iconColor: "#2D338F",
component: "helpers",
path: "/config/helpers",
translationKey: "ui.panel.config.helpers.caption",
iconPath: mdiTools,
iconColor: "#4D2EA4",
core: true,
},
],
@ -178,16 +189,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiScriptText,
iconColor: "#518C43",
},
{
component: "helpers",
path: "/config/helpers",
translationKey: "ui.panel.config.helpers.caption",
iconPath: mdiTools,
iconColor: "#4D2EA4",
core: true,
},
],
blueprints: [
{
component: "blueprint",
path: "/config/blueprint",
@ -232,13 +233,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiAccount,
iconColor: "#E48629",
},
{
component: "zone",
path: "/config/zone",
translationKey: "ui.panel.config.zone.caption",
iconPath: mdiMapMarkerRadius,
iconColor: "#E48629",
},
{
component: "users",
path: "/config/users",
@ -249,6 +243,23 @@ export const configSections: { [name: string]: PageNavigation[] } = {
advancedOnly: true,
},
],
areas: [
{
component: "areas",
path: "/config/areas",
translationKey: "ui.panel.config.areas.caption",
iconPath: mdiSofa,
iconColor: "#2D338F",
core: true,
},
{
component: "zone",
path: "/config/location",
translationKey: "ui.panel.config.zone.caption",
iconPath: mdiMapMarkerRadius,
iconColor: "#E48629",
},
],
general: [
{
component: "core",
@ -274,6 +285,45 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#4A5963",
core: true,
},
{
path: "/config/backup",
translationKey: "ui.panel.config.backup.caption",
iconPath: mdiBackupRestore,
iconColor: "#4084CD",
component: "backup",
},
{
path: "/config/analytics",
translationKey: "ui.panel.config.analytics.caption",
iconPath: mdiShape,
iconColor: "#f1c447",
},
{
path: "/config/hardware",
translationKey: "ui.panel.config.hardware.caption",
iconPath: mdiCpu32Bit,
iconColor: "#4A5963",
},
{
path: "/config/network",
translationKey: "ui.panel.config.network.caption",
iconPath: mdiNetwork,
iconColor: "#B1345C",
},
{
path: "/config/storage",
translationKey: "ui.panel.config.storage.caption",
iconPath: mdiServer,
iconColor: "#518C43",
},
{
path: "/config/update",
translationKey: "ui.panel.config.updates.caption",
iconPath: mdiUpdate,
iconColor: "#4A5963",
},
],
about: [
{
component: "info",
path: "/config/info",
@ -296,6 +346,10 @@ class HaPanelConfig extends HassRouterPage {
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
routes: {
analytics: {
tag: "ha-config-section-analytics",
load: () => import("./core/ha-config-section-analytics"),
},
areas: {
tag: "ha-config-areas",
load: () => import("./areas/ha-config-areas"),
@ -328,9 +382,9 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-devices",
load: () => import("./devices/ha-config-devices"),
},
server_control: {
tag: "ha-config-server-control",
load: () => import("./server_control/ha-config-server-control"),
system: {
tag: "ha-config-system-navigation",
load: () => import("./core/ha-config-system-navigation"),
},
logs: {
tag: "ha-config-logs",
@ -362,6 +416,10 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-lovelace",
load: () => import("./lovelace/ha-config-lovelace"),
},
network: {
tag: "ha-config-section-network",
load: () => import("./core/ha-config-section-network"),
},
person: {
tag: "ha-config-person",
load: () => import("./person/ha-config-person"),
@ -378,11 +436,15 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-helpers",
load: () => import("./helpers/ha-config-helpers"),
},
storage: {
tag: "ha-config-section-storage",
load: () => import("./core/ha-config-section-storage"),
},
users: {
tag: "ha-config-users",
load: () => import("./users/ha-config-users"),
},
zone: {
location: {
tag: "ha-config-zone",
load: () => import("./zone/ha-config-zone"),
},

View File

@ -6,21 +6,29 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { domainIcon } from "../../../common/entity/domain_icon";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import { extractSearchParam } from "../../../common/url/search-params";
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-icon";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-svg-icon";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@ -29,14 +37,6 @@ import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
import { configSections } from "../ha-panel-config";
import { HELPER_DOMAINS } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import { navigate } from "../../../common/navigate";
import { extractSearchParam } from "../../../common/url/search-params";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
// This groups items by a key but only returns last entry per key.
const groupByOne = <T>(
@ -196,7 +196,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automations}
.tabs=${configSections.devices}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._getItems(
this._stateItems,

View File

@ -1,7 +1,7 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators";
import "../../../layouts/hass-tabs-subpage";
import "../../../components/ha-logo-svg";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@ -34,7 +34,7 @@ class HaConfigInfo extends LitElement {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.general}
.tabs=${configSections.about}
>
<div class="about">
<a

View File

@ -1,11 +1,11 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/search-input";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import "../../../components/search-input";
import { extractSearchParam } from "../../../common/url/search-params";
import "./error-log-card";
import "./system-log-card";
import type { SystemLogCard } from "./system-log-card";
@ -62,12 +62,11 @@ export class HaConfigLogs extends LitElement {
`;
return html`
<hass-tabs-subpage
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.general}
.header=${this.hass.localize("ui.panel.config.logs.caption")}
back-path="/config/system"
>
${search}
<div class="content">
@ -80,7 +79,7 @@ export class HaConfigLogs extends LitElement {
.filter=${this._filter}
></error-log-card>
</div>
</hass-tabs-subpage>
</hass-subpage>
`;
}

View File

@ -8,11 +8,11 @@ import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import { checkCoreConfig } from "../../../data/core";
import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
@customElement("ha-config-server-control")
export class HaConfigServerControl extends LitElement {
@ -49,12 +49,10 @@ export class HaConfigServerControl extends LitElement {
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.tabs=${configSections.general}
back-path="/config/system"
.showAdvanced=${this.showAdvanced}
>
<ha-config-section .isWide=${this.isWide}>
@ -203,7 +201,7 @@ export class HaConfigServerControl extends LitElement {
`
: ""}
</ha-config-section>
</hass-tabs-subpage>
</hass-subpage>
`;
}

View File

@ -157,7 +157,7 @@ export class HaConfigUsers extends LitElement {
.narrow=${this.narrow}
.route=${this.route}
backPath="/config"
.tabs=${configSections.persons}
.tabs=${configSections.areas}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._users}
@row-click=${this._editUser}

View File

@ -228,7 +228,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.tabs=${configSections.persons}
.tabs=${configSections.areas}
>
${this.narrow
? html`

View File

@ -1067,23 +1067,19 @@
"dashboard": {
"devices": {
"title": "Devices & Services",
"description": "Integrations, devices, entities and areas"
"description": "Integrations, devices, entities and helpers"
},
"automations": {
"title": "Automations & Scenes",
"description": "Manage automations, scenes, scripts and helpers"
"description": "Manage automations, scenes, scripts and blueprints"
},
"backup": {
"title": "Backup",
"description": "Generate backups of your Home Assistant configuration"
},
"blueprints": {
"title": "Blueprints",
"description": "Pre-made automations and scripts by the community"
},
"supervisor": {
"title": "Add-ons, Backups & Supervisor",
"description": "Create backups, check logs or reboot your system"
"title": "Add-ons",
"description": "Extend the function around Home Assistant"
},
"dashboards": {
"title": "Dashboards",
@ -1098,16 +1094,24 @@
"description": "Trigger automations when an NFC tag, QR code, etc. is scanned"
},
"people": {
"title": "People & Zones",
"description": "Manage the people and zones that Home Assistant tracks"
"title": "People",
"description": "Manage the people that Home Assistant tracks"
},
"areas": {
"title": "Areas & Zones",
"description": "Manage areas & zones that Home Assistant tracks"
},
"companion": {
"title": "Companion App",
"description": "Location and notifications"
},
"settings": {
"title": "Settings",
"description": "Basic settings, server controls, logs and info"
"system": {
"title": "System",
"description": "Create backups, check logs or reboot your system"
},
"about": {
"title": "About",
"description": "Version, system health and links to documentation"
}
},
"common": {
@ -1117,6 +1121,7 @@
"learn_more": "Learn more"
},
"updates": {
"caption": "Updates",
"no_update_entities": {
"title": "Unable to check for updates",
"description": "You do not have any integrations that provide updates."
@ -1168,7 +1173,7 @@
}
},
"backup": {
"caption": "[%key:ui::panel::config::dashboard::backup::title%]",
"caption": "Backups",
"create_backup": "[%key:supervisor::backup::create_backup%]",
"creating_backup": "Backup is currently being created",
"download_backup": "[%key:supervisor::backup::download_backup%]",
@ -1458,6 +1463,9 @@
"internal_url_https_error_title": "Invalid local network URL",
"internal_url_https_error_description": "You have configured an HTTPS certificate in Home Assistant. This means that your internal URL needs to be set to a domain covered by the certficate."
},
"hardware": {
"caption": "Hardware"
},
"info": {
"caption": "Info",
"copy_menu": "Copy menu",
@ -1764,7 +1772,7 @@
"geo_location": {
"label": "Geolocation",
"source": "Source",
"zone": "Zone",
"zone": "Location",
"event": "Event",
"enter": "Enter",
"leave": "Leave"
@ -3087,6 +3095,15 @@
"tips": {
"tip": "Tip!",
"join": "Join the community on our {forums}, {twitter}, {discord}, {blog} or {newsletter}"
},
"analytics": {
"caption": "Analytics"
},
"network": {
"caption": "Network"
},
"storage": {
"caption": "Storage"
}
},
"lovelace": {
@ -3680,7 +3697,7 @@
}
},
"map": {
"edit_zones": "Edit Zones"
"edit_zones": "Edit zones"
},
"profile": {
"current_user": "You are currently logged in as {fullName}.",