Add advanced mode (#3298)

* Add advanced mode

* Move advanced mode to profile

* Add promo for advanced mode
This commit is contained in:
Paulus Schoutsen 2019-06-27 16:17:32 -07:00 committed by GitHub
parent 2c3cc1fbc7
commit e804e62e66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 393 additions and 173 deletions

74
src/data/collection.ts Normal file
View File

@ -0,0 +1,74 @@
import {
Collection,
Connection,
getCollection,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
interface OptimisticCollection<T> extends Collection<T> {
save(data: T): Promise<unknown>;
}
/**
* Create an optimistic collection that includes a save function.
* When the collection is saved, the collection is optimistically updated.
* The update is reversed when the update failed.
*/
export const getOptimisticCollection = <StateType>(
saveCollection: (conn: Connection, data: StateType) => Promise<unknown>,
conn: Connection,
key: string,
fetchCollection: (conn: Connection) => Promise<StateType>,
subscribeUpdates?: (
conn: Connection,
store: Store<StateType>
) => Promise<UnsubscribeFunc>
): OptimisticCollection<StateType> => {
const updateKey = `${key}-optimistic`;
const collection = getCollection<StateType>(
conn,
key,
fetchCollection,
async (_conn, store) => {
// Subscribe to original updates
const subUpResult = subscribeUpdates
? subscribeUpdates(conn, store)
: undefined;
// Store the store
conn[updateKey] = store;
// Unsub function to undo both
return () => {
if (subUpResult) {
subUpResult.then((unsub) => unsub());
}
conn[updateKey] = undefined;
};
}
);
return {
...collection,
async save(data: StateType) {
const store: Store<StateType> | undefined = conn[updateKey];
let current;
// Can be undefined if currently no subscribers
if (store) {
current = store.state;
store.setState(data, true);
}
try {
return await saveCollection(conn, data);
} catch (err) {
if (store) {
store.setState(current as any, true);
}
throw err;
}
},
};
};

View File

@ -1,8 +1,14 @@
import { HomeAssistant } from "../types";
import { Connection } from "home-assistant-js-websocket";
import { getOptimisticCollection } from "./collection";
export interface CoreFrontendUserData {
showAdvanced?: boolean;
}
declare global {
// tslint:disable-next-line
interface FrontendUserData {}
interface FrontendUserData {
core: CoreFrontendUserData;
}
}
export type ValidUserDataKey = keyof FrontendUserData;
@ -10,10 +16,10 @@ export type ValidUserDataKey = keyof FrontendUserData;
export const fetchFrontendUserData = async <
UserDataKey extends ValidUserDataKey
>(
hass: HomeAssistant,
conn: Connection,
key: UserDataKey
): Promise<FrontendUserData[UserDataKey] | null> => {
const result = await hass.callWS<{
const result = await conn.sendMessagePromise<{
value: FrontendUserData[UserDataKey] | null;
}>({
type: "frontend/get_user_data",
@ -25,12 +31,31 @@ export const fetchFrontendUserData = async <
export const saveFrontendUserData = async <
UserDataKey extends ValidUserDataKey
>(
hass: HomeAssistant,
conn: Connection,
key: UserDataKey,
value: FrontendUserData[UserDataKey]
): Promise<void> =>
hass.callWS<void>({
conn.sendMessagePromise<void>({
type: "frontend/set_user_data",
key,
value,
});
export const getOptimisticFrontendUserDataCollection = <
UserDataKey extends ValidUserDataKey
>(
conn: Connection,
userDataKey: UserDataKey
) =>
getOptimisticCollection(
(_conn, data) =>
saveFrontendUserData(
conn,
userDataKey,
// @ts-ignore
data
),
conn,
`_frontendUserData-${userDataKey}`,
() => fetchFrontendUserData(conn, userDataKey)
);

View File

@ -12,12 +12,12 @@ declare global {
}
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass, "language");
fetchFrontendUserData(hass.connection, "language");
export const saveTranslationPreferences = (
hass: HomeAssistant,
data: FrontendTranslationData
) => saveFrontendUserData(hass, "language", data);
) => saveFrontendUserData(hass.connection, "language", data);
export const getHassTranslations = async (
hass: HomeAssistant,

View File

@ -37,6 +37,7 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) {
<div class$="[[computeClasses(isWide)]]">
<ha-config-section-core
is-wide="[[isWide]]"
show-advanced="[[showAdvanced]]"
hass="[[hass]]"
></ha-config-section-core>
</div>
@ -48,6 +49,7 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) {
return {
hass: Object,
isWide: Boolean,
showAdvanced: Boolean,
};
}

View File

@ -63,79 +63,80 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
<ha-card
header="[[localize('ui.panel.config.core.section.core.validation.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.core.section.core.validation.introduction')]]
<template is="dom-if" if="[[!validateLog]]">
<div class="validate-container">
<template is="dom-if" if="[[!validating]]">
<template is="dom-if" if="[[isValid]]">
<div class="validate-result" id="result">
[[localize('ui.panel.config.core.section.core.validation.valid')]]
</div>
<template is="dom-if" if="[[showAdvanced]]">
<ha-card
header="[[localize('ui.panel.config.core.section.core.validation.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.core.section.core.validation.introduction')]]
<template is="dom-if" if="[[!validateLog]]">
<div class="validate-container">
<template is="dom-if" if="[[!validating]]">
<template is="dom-if" if="[[isValid]]">
<div class="validate-result" id="result">
[[localize('ui.panel.config.core.section.core.validation.valid')]]
</div>
</template>
<mwc-button raised="" on-click="validateConfig">
[[localize('ui.panel.config.core.section.core.validation.check_config')]]
</mwc-button>
</template>
<template is="dom-if" if="[[validating]]">
<paper-spinner active=""></paper-spinner>
</template>
</div>
</template>
<template is="dom-if" if="[[validateLog]]">
<div class="config-invalid">
<span class="text">
[[localize('ui.panel.config.core.section.core.validation.invalid')]]
</span>
<mwc-button raised="" on-click="validateConfig">
[[localize('ui.panel.config.core.section.core.validation.check_config')]]
</mwc-button>
</template>
<template is="dom-if" if="[[validating]]">
<paper-spinner active=""></paper-spinner>
</template>
</div>
</template>
<template is="dom-if" if="[[validateLog]]">
<div class="config-invalid">
<span class="text">
[[localize('ui.panel.config.core.section.core.validation.invalid')]]
</span>
<mwc-button raised="" on-click="validateConfig">
[[localize('ui.panel.config.core.section.core.validation.check_config')]]
</mwc-button>
</div>
<div id="configLog" class="validate-log">[[validateLog]]</div>
</template>
</div>
</ha-card>
<ha-card
header="[[localize('ui.panel.config.core.section.core.reloading.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.core.section.core.reloading.introduction')]]
</div>
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="homeassistant"
service="reload_core_config"
>[[localize('ui.panel.config.core.section.core.reloading.core')]]
</ha-call-service-button>
<ha-call-service-button
hass="[[hass]]"
domain="group"
service="reload"
hidden$="[[!groupLoaded(hass)]]"
>[[localize('ui.panel.config.core.section.core.reloading.group')]]
</ha-call-service-button>
<ha-call-service-button
hass="[[hass]]"
domain="automation"
service="reload"
hidden$="[[!automationLoaded(hass)]]"
>[[localize('ui.panel.config.core.section.core.reloading.automation')]]
</ha-call-service-button>
<ha-call-service-button
hass="[[hass]]"
domain="script"
service="reload"
hidden$="[[!scriptLoaded(hass)]]"
>[[localize('ui.panel.config.core.section.core.reloading.script')]]
</ha-call-service-button>
</div>
</ha-card>
</div>
<div id="configLog" class="validate-log">[[validateLog]]</div>
</template>
</div>
</ha-card>
<ha-card
header="[[localize('ui.panel.config.core.section.core.reloading.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.core.section.core.reloading.introduction')]]
</div>
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="homeassistant"
service="reload_core_config"
>[[localize('ui.panel.config.core.section.core.reloading.core')]]
</ha-call-service-button>
<ha-call-service-button
hass="[[hass]]"
domain="group"
service="reload"
hidden$="[[!groupLoaded(hass)]]"
>[[localize('ui.panel.config.core.section.core.reloading.group')]]
</ha-call-service-button>
<ha-call-service-button
hass="[[hass]]"
domain="automation"
service="reload"
hidden$="[[!automationLoaded(hass)]]"
>[[localize('ui.panel.config.core.section.core.reloading.automation')]]
</ha-call-service-button>
<ha-call-service-button
hass="[[hass]]"
domain="script"
service="reload"
hidden$="[[!scriptLoaded(hass)]]"
>[[localize('ui.panel.config.core.section.core.reloading.script')]]
</ha-call-service-button>
</div>
</ha-card>
</template>
<ha-card
header="[[localize('ui.panel.config.core.section.core.server_management.heading')]]"
>
@ -188,6 +189,8 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
type: String,
value: "",
},
showAdvanced: Boolean,
};
}

View File

@ -31,10 +31,17 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
.content {
padding-bottom: 32px;
}
a {
ha-card a {
text-decoration: none;
color: var(--primary-text-color);
}
.promo-advanced {
text-align: center;
color: var(--secondary-text-color);
}
.promo-advanced a {
color: var(--secondary-text-color);
}
</style>
<app-header-layout has-scrolling-region="">
@ -99,7 +106,16 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
</a>
</ha-card>
<ha-config-navigation hass="[[hass]]"></ha-config-navigation>
<ha-config-navigation
hass="[[hass]]"
show-advanced="[[showAdvanced]]"
></ha-config-navigation>
<template is='dom-if' if='[[!showAdvanced]]'>
<div class='promo-advanced'>
Missing config options? Enable advanced mode on <a href="/profile">your profile page.</a>
</div>
</template>
</ha-config-section>
</div>
</app-header-layout>
@ -111,6 +127,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
hass: Object,
isWide: Boolean,
cloudStatus: Object,
showAdvanced: Boolean,
};
}

View File

@ -1,87 +0,0 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import NavigateMixin from "../../../mixins/navigate-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
const CORE_PAGES = ["core", "customize", "entity_registry", "area_registry"];
/*
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
*/
class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex">
ha-card {
overflow: hidden;
}
paper-item {
cursor: pointer;
}
</style>
<ha-card>
<template is="dom-repeat" items="[[pages]]">
<template is="dom-if" if="[[_computeLoaded(hass, item)]]">
<paper-item on-click="_navigate">
<paper-item-body two-line="">
[[_computeCaption(item, localize)]]
<div secondary="">[[_computeDescription(item, localize)]]</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</template>
</template>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
pages: {
type: Array,
value: [
"core",
"person",
"entity_registry",
"area_registry",
"automation",
"script",
"zha",
"zwave",
"customize",
],
},
};
}
_computeLoaded(hass, page) {
return CORE_PAGES.includes(page) || isComponentLoaded(hass, page);
}
_computeCaption(page, localize) {
return localize(`ui.panel.config.${page}.caption`);
}
_computeDescription(page, localize) {
return localize(`ui.panel.config.${page}.description`);
}
_navigate(ev) {
this.navigate("/config/" + ev.model.item);
}
}
customElements.define("ha-config-navigation", HaConfigNavigation);

View File

@ -0,0 +1,82 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-item/paper-item";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import {
LitElement,
html,
TemplateResult,
property,
customElement,
CSSResult,
css,
} from "lit-element";
import { HomeAssistant } from "../../../types";
const PAGES: Array<{
page: string;
core?: boolean;
advanced?: boolean;
}> = [
{ page: "core", core: true },
{ page: "person" },
{ page: "entity_registry", core: true },
{ page: "area_registry", core: true },
{ page: "automation" },
{ page: "script" },
{ page: "zha" },
{ page: "zwave" },
{ page: "customize", core: true, advanced: true },
];
@customElement("ha-config-navigation")
class HaConfigNavigation extends LitElement {
@property() public hass!: HomeAssistant;
@property() public showAdvanced!: boolean;
protected render(): TemplateResult | void {
return html`
<ha-card>
${PAGES.map(({ page, core, advanced }) =>
(core || isComponentLoaded(this.hass, page)) &&
(!advanced || this.showAdvanced)
? html`
<a href=${`/config/${page}`}>
<paper-item>
<paper-item-body two-line="">
${this.hass.localize(`ui.panel.config.${page}.caption`)}
<div secondary>
${this.hass.localize(
`ui.panel.config.${page}.description`
)}
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
`
: ""
)}
</ha-card>
`;
}
static get styles(): CSSResult {
return css`
a {
text-decoration: none;
color: var(--primary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-navigation": HaConfigNavigation;
}
}

View File

@ -5,6 +5,10 @@ import { HomeAssistant } from "../../types";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import { listenMediaQuery } from "../../common/dom/media_query";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import {
CoreFrontendUserData,
getOptimisticFrontendUserDataCollection,
} from "../../data/frontend";
declare global {
// for fire event
@ -17,8 +21,6 @@ declare global {
class HaPanelConfig extends HassRouterPage {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public _wideSidebar: boolean = false;
@property() public _wide: boolean = false;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
@ -93,6 +95,9 @@ class HaPanelConfig extends HassRouterPage {
},
};
@property() private _wideSidebar: boolean = false;
@property() private _wide: boolean = false;
@property() private _coreUserData?: CoreFrontendUserData;
@property() private _cloudStatus?: CloudStatus;
private _listeners: Array<() => void> = [];
@ -109,6 +114,14 @@ class HaPanelConfig extends HassRouterPage {
this._wideSidebar = matches;
})
);
this._listeners.push(
getOptimisticFrontendUserDataCollection(
this.hass.connection,
"core"
).subscribe((coreUserData) => {
this._coreUserData = coreUserData || {};
})
);
}
public disconnectedCallback() {
@ -131,6 +144,7 @@ class HaPanelConfig extends HassRouterPage {
protected updatePageEl(el) {
el.route = this.routeTail;
el.hass = this.hass;
el.showAdvanced = !!(this._coreUserData && this._coreUserData.showAdvanced);
el.isWide = this.hass.dockedSidebar ? this._wideSidebar : this._wide;
el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus;

View File

@ -0,0 +1,65 @@
import {
LitElement,
property,
TemplateResult,
html,
customElement,
CSSResult,
css,
} from "lit-element";
import "../../components/ha-card";
import { HomeAssistant } from "../../types";
import {
CoreFrontendUserData,
getOptimisticFrontendUserDataCollection,
} from "../../data/frontend";
@customElement("ha-advanced-mode-card")
class AdvancedModeCard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public coreUserData?: CoreFrontendUserData;
protected render(): TemplateResult | void {
return html`
<ha-card>
<div class="card-header">
<div class="title">Advanced mode</div>
<paper-toggle-button
.checked=${this.coreUserData && this.coreUserData.showAdvanced}
.disabled=${this.coreUserData === undefined}
@change=${this._advancedToggled}
></paper-toggle-button>
</div>
<div class="card-content">
Home Assistant hides advanced features and options by default. You can
make these features accessible by checking this toggle. This is a
user-specific setting and does not impact other users using Home
Assistant.
</div>
</ha-card>
`;
}
private async _advancedToggled(ev) {
getOptimisticFrontendUserDataCollection(this.hass.connection, "core").save({
showAdvanced: ev.currentTarget.checked,
});
}
static get styles(): CSSResult {
return css`
.card-header {
display: flex;
}
.title {
flex: 1;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-advanced-mode-card": AdvancedModeCard;
}
}

View File

@ -11,11 +11,14 @@ import "../../components/ha-card";
import "../../components/ha-menu-button";
import "../../resources/ha-style";
import { getOptimisticFrontendUserDataCollection } from "../../data/frontend";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import "./ha-change-password-card";
import "./ha-mfa-modules-card";
import "./ha-advanced-mode-card";
import "./ha-refresh-tokens-card";
import "./ha-long-lived-access-tokens-card";
@ -98,6 +101,11 @@ class HaPanelProfile extends EventsMixin(LocalizeMixin(PolymerElement)) {
mfa-modules="[[hass.user.mfa_modules]]"
></ha-mfa-modules-card>
<ha-advanced-mode-card
hass="[[hass]]"
core-user-data="[[_coreUserData]]"
></ha-mfa-modules-card>
<ha-refresh-tokens-card
hass="[[hass]]"
refresh-tokens="[[_refreshTokens]]"
@ -119,12 +127,27 @@ class HaPanelProfile extends EventsMixin(LocalizeMixin(PolymerElement)) {
hass: Object,
narrow: Boolean,
_refreshTokens: Array,
_coreUserData: Object,
};
}
connectedCallback() {
super.connectedCallback();
this._refreshRefreshTokens();
this._unsubCoreData = getOptimisticFrontendUserDataCollection(
this.hass.connection,
"core"
).subscribe((coreUserData) => {
this._coreUserData = coreUserData;
});
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubCoreData) {
this._unsubCoreData();
this._unsubCoreData = undefined;
}
}
async _refreshRefreshTokens() {

View File

@ -9,9 +9,11 @@ import { HassBaseEl } from "./hass-base-mixin";
import { computeLocalize } from "../common/translations/localize";
import { computeRTL } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types";
import { saveFrontendUserData } from "../data/frontend";
import { storeState } from "../util/ha-pref-storage";
import { getHassTranslations } from "../data/translation";
import {
getHassTranslations,
saveTranslationPreferences,
} from "../data/translation";
/*
* superClass needs to contain `this.hass` and `this._updateHass`.
@ -65,7 +67,7 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
this._updateHass({ language, selectedLanguage: language });
storeState(this.hass);
if (saveToBackend) {
saveFrontendUserData(this.hass, "language", { language });
saveTranslationPreferences(this.hass, { language });
}
this._applyTranslations(this.hass);

View File

@ -1,6 +1,6 @@
import { translationMetadata } from "../resources/translations-metadata";
import { fetchFrontendUserData } from "../data/frontend";
import { HomeAssistant } from "../types";
import { fetchTranslationPreferences } from "../data/translation";
const STORAGE = window.localStorage || {};
@ -43,7 +43,7 @@ function findAvailableLanguage(language: string) {
* Get user selected language from backend
*/
export async function getUserLanguage(hass: HomeAssistant) {
const result = await fetchFrontendUserData(hass, "language");
const result = await fetchTranslationPreferences(hass);
const language = result ? result.language : null;
if (language) {
const availableLanguage = findAvailableLanguage(language);