UI: persist experimental flag (BC) (#27074)
This commit is contained in:
parent
3f1df220b5
commit
4c5935b60d
52 changed files with 137 additions and 112 deletions
|
|
@ -34,7 +34,6 @@ export default {
|
|||
} satisfies Preview;
|
||||
|
||||
setup((app) => {
|
||||
app.config.globalProperties.$hiddenFeatures = () => true;
|
||||
app.use(setupI18n());
|
||||
|
||||
// Mock router-link for Storybook
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ const errorInterceptor = (error: any) => {
|
|||
message.push(`${error.response.data.error}.`);
|
||||
}
|
||||
if (error.config) {
|
||||
const method = error.config.method.toUpperCase();
|
||||
const url = error.request.responseURL;
|
||||
message.push(`${method} ${url}`);
|
||||
const { baseURL, url, method } = error.config;
|
||||
const fullUrl = error.request.responseURL || `${baseURL}${url}`;
|
||||
message.push(`${method.toUpperCase()} ${fullUrl}`);
|
||||
}
|
||||
window.app.raise({ message });
|
||||
return Promise.reject(error);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { VueHeadMixin, createHead } from "@unhead/vue/client";
|
|||
import App from "./views/App.vue";
|
||||
import setupRouter from "./router.ts";
|
||||
import setupI18n from "./i18n.ts";
|
||||
import featureflags from "./featureflags.ts";
|
||||
import { watchThemeChanges } from "./theme.ts";
|
||||
import { appDetection, sendToApp } from "./utils/native";
|
||||
import type { Notification } from "./types/evcc";
|
||||
|
|
@ -74,7 +73,6 @@ const head = createHead();
|
|||
|
||||
app.use(i18n);
|
||||
app.use(setupRouter(i18n.global));
|
||||
app.use(featureflags);
|
||||
app.use(head);
|
||||
app.mixin(VueHeadMixin);
|
||||
window.app = app.mount("#app");
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
@open="open"
|
||||
@closed="closed"
|
||||
>
|
||||
<p v-if="error" class="text-danger">{{ $t("passwordModal.error") }} "{{ error }}"</p>
|
||||
<ErrorMessage :error="error" />
|
||||
<p v-if="isSetupMode" class="mb-4">{{ $t("passwordModal.description") }}</p>
|
||||
<form
|
||||
v-if="modalVisible"
|
||||
|
|
@ -87,6 +87,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import GenericModal from "../Helper/GenericModal.vue";
|
||||
import ErrorMessage from "../Helper/ErrorMessage.vue";
|
||||
import FormRow from "../Helper/FormRow.vue";
|
||||
import api from "@/api";
|
||||
import { updateAuthStatus } from "./auth";
|
||||
|
|
@ -94,7 +95,7 @@ import { defineComponent } from "vue";
|
|||
|
||||
export default defineComponent({
|
||||
name: "PasswordModal",
|
||||
components: { FormRow, GenericModal },
|
||||
components: { FormRow, GenericModal, ErrorMessage },
|
||||
props: {
|
||||
updateMode: Boolean,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="auth.error" class="text-danger">{{ auth.error }}</p>
|
||||
<ErrorMessage :error="auth.error" />
|
||||
|
||||
<div
|
||||
class="my-4 d-flex align-items-stretch justify-content-sm-between align-items-sm-baseline flex-column-reverse flex-sm-row gap-2"
|
||||
|
|
@ -167,6 +167,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import GenericModal, { type ModalFade } from "../../Helper/GenericModal.vue";
|
||||
import ErrorMessage from "../../Helper/ErrorMessage.vue";
|
||||
import PropertyEntry from "../PropertyEntry.vue";
|
||||
import PropertyCollapsible from "../PropertyCollapsible.vue";
|
||||
import Modbus from "./Modbus.vue";
|
||||
|
|
@ -203,6 +204,7 @@ export default defineComponent({
|
|||
name: "DeviceModalBase",
|
||||
components: {
|
||||
GenericModal,
|
||||
ErrorMessage,
|
||||
PropertyEntry,
|
||||
PropertyCollapsible,
|
||||
Modbus,
|
||||
|
|
|
|||
|
|
@ -1,46 +1,56 @@
|
|||
<template>
|
||||
<GenericModal
|
||||
id="experimentalModal"
|
||||
:title="$t('config.experimental.title')"
|
||||
:title="`${$t('config.experimental.title')} 🧪`"
|
||||
data-testid="experimental-modal"
|
||||
>
|
||||
<p>{{ $t("config.experimental.description") }}</p>
|
||||
<ErrorMessage :error="error" />
|
||||
<div class="form-check form-switch my-3">
|
||||
<input
|
||||
id="experimentalEnabled"
|
||||
v-model="hiddenFeatures"
|
||||
:checked="experimental"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
@change="change"
|
||||
/>
|
||||
<div class="form-check-label">
|
||||
<label for="experimentalEnabled">
|
||||
{{ $t("settings.hiddenFeatures.value") }} 🧪
|
||||
{{ $t("settings.hiddenFeatures.value") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
{{ $t("settings.deviceInfo") }}
|
||||
</div>
|
||||
</GenericModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import GenericModal from "../Helper/GenericModal.vue";
|
||||
import { getHiddenFeatures, setHiddenFeatures } from "@/featureflags.ts";
|
||||
import ErrorMessage from "../Helper/ErrorMessage.vue";
|
||||
import api from "@/api";
|
||||
import type { AxiosError } from "axios";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ExperimentalModal",
|
||||
components: { GenericModal },
|
||||
components: { GenericModal, ErrorMessage },
|
||||
props: {
|
||||
experimental: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hiddenFeatures: getHiddenFeatures(),
|
||||
error: null as string | null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
hiddenFeatures(value) {
|
||||
setHiddenFeatures(value);
|
||||
methods: {
|
||||
async change(e: Event) {
|
||||
try {
|
||||
this.error = null;
|
||||
await api.post(`config/experimental/${(e.target as HTMLInputElement).checked}`);
|
||||
} catch (err) {
|
||||
const e = err as AxiosError<{ error: string }>;
|
||||
this.error = e.response?.data?.error || e.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<GeneralConfigEntry
|
||||
test-id="generalconfig-experimental"
|
||||
:label="$t('config.general.experimental')"
|
||||
:text="$t(`config.general.${hiddenFeatures ? 'on' : 'off'}`)"
|
||||
:text="$t(`config.general.${experimental ? 'on' : 'off'}`)"
|
||||
modal-id="experimentalModal"
|
||||
/>
|
||||
|
||||
|
|
@ -66,7 +66,6 @@
|
|||
import TitleModal from "./TitleModal.vue";
|
||||
import GeneralConfigEntry from "./GeneralConfigEntry.vue";
|
||||
import api from "@/api";
|
||||
import settings from "@/settings";
|
||||
import store from "@/store";
|
||||
import formatter from "@/mixins/formatter";
|
||||
|
||||
|
|
@ -76,6 +75,7 @@ export default {
|
|||
mixins: [formatter],
|
||||
props: {
|
||||
sponsorError: Boolean,
|
||||
experimental: Boolean,
|
||||
},
|
||||
emits: ["site-changed"],
|
||||
data() {
|
||||
|
|
@ -87,9 +87,6 @@ export default {
|
|||
telemetryEnabled() {
|
||||
return store.state?.telemetry === true;
|
||||
},
|
||||
hiddenFeatures() {
|
||||
return settings.hiddenFeatures === true;
|
||||
},
|
||||
networkStatus() {
|
||||
return `${store.state?.network?.port ?? ""}`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
</a>
|
||||
</p>
|
||||
<slot name="afterDescription" />
|
||||
<p v-if="error" class="text-danger" data-testid="error">{{ error }}</p>
|
||||
<ErrorMessage :error="error" data-testid="error" />
|
||||
<form ref="form" class="container mx-0 px-0">
|
||||
<div v-if="!noYamlEditor" class="editor-container">
|
||||
<YamlEditorContainer
|
||||
|
|
@ -49,13 +49,14 @@
|
|||
|
||||
<script>
|
||||
import GenericModal from "../Helper/GenericModal.vue";
|
||||
import ErrorMessage from "../Helper/ErrorMessage.vue";
|
||||
import api from "@/api";
|
||||
import { docsPrefix } from "@/i18n";
|
||||
import YamlEditorContainer from "./YamlEditorContainer.vue";
|
||||
|
||||
export default {
|
||||
name: "YamlModal",
|
||||
components: { GenericModal, YamlEditorContainer },
|
||||
components: { GenericModal, ErrorMessage, YamlEditorContainer },
|
||||
props: {
|
||||
title: String,
|
||||
description: String,
|
||||
|
|
|
|||
|
|
@ -333,6 +333,7 @@ export default defineComponent({
|
|||
mixins: [formatter, collector],
|
||||
props: {
|
||||
gridConfigured: Boolean,
|
||||
experimental: Boolean,
|
||||
gridPower: { type: Number, default: 0 },
|
||||
homePower: { type: Number, default: 0 },
|
||||
pvConfigured: Boolean,
|
||||
|
|
@ -510,7 +511,7 @@ export default defineComponent({
|
|||
return undefined;
|
||||
}
|
||||
const { today, scale } = this.forecast.solar || {};
|
||||
const factor = this.$hiddenFeatures() && settings.solarAdjusted && scale ? scale : 1;
|
||||
const factor = this.experimental && settings.solarAdjusted && scale ? scale : 1;
|
||||
const energy = today?.energy || 0;
|
||||
return energy * factor;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export default defineComponent({
|
|||
props: {
|
||||
forecast: { type: Object as PropType<Forecast>, default: () => ({}) },
|
||||
currency: { type: String as PropType<CURRENCY> },
|
||||
experimental: Boolean,
|
||||
},
|
||||
data(): {
|
||||
isModalVisible: boolean;
|
||||
|
|
@ -94,7 +95,7 @@ export default defineComponent({
|
|||
return settings.solarAdjusted;
|
||||
},
|
||||
showSolarAdjust() {
|
||||
return !!this.forecast.solar && this.$hiddenFeatures();
|
||||
return !!this.forecast.solar && this.experimental;
|
||||
},
|
||||
solar() {
|
||||
return this.showSolarAdjust && this.solarAdjusted
|
||||
|
|
|
|||
19
assets/js/components/Helper/ErrorMessage.vue
Normal file
19
assets/js/components/Helper/ErrorMessage.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<p v-if="error" class="text-danger text-break">
|
||||
<strong>{{ $t("config.general.error") }}:</strong> {{ error }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ErrorMessage",
|
||||
props: {
|
||||
error: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -163,6 +163,7 @@ export default defineComponent({
|
|||
forecast: Object as PropType<Forecast>,
|
||||
circuits: Object as PropType<Record<string, Circuit>>,
|
||||
telemetry: Boolean,
|
||||
experimental: Boolean,
|
||||
evopt: { type: Object as PropType<EvOpt> },
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<ErrorMessage :error="error" />
|
||||
<div class="form-check form-switch my-3">
|
||||
<input
|
||||
id="telemetryEnabled"
|
||||
|
|
@ -25,27 +25,20 @@
|
|||
</i18n-t>
|
||||
<span v-else>{{ $t("footer.telemetry.optInSponsorship") }}</span>
|
||||
</label>
|
||||
<div v-if="error" class="errorMessage my-1 text-danger" v-html="error" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import ErrorMessage from "./Helper/ErrorMessage.vue";
|
||||
import api from "../api";
|
||||
import { docsPrefix } from "../i18n";
|
||||
import type { AxiosError } from "axios";
|
||||
|
||||
function parseMarkdown(markdownText: string) {
|
||||
const htmlText = markdownText
|
||||
.replace(/\*\*(.*)\*\*/gim, "<b>$1</b>")
|
||||
.replace(/\*(.*)\*/gim, "<i>$1</i>")
|
||||
.replace(/`(.*)`/gim, "<pre>$1</pre>");
|
||||
return htmlText.trim();
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "TelemetrySettings",
|
||||
components: { ErrorMessage },
|
||||
props: { sponsorActive: Boolean, telemetry: Boolean },
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -64,9 +57,7 @@ export default defineComponent({
|
|||
await api.post(`settings/telemetry/${(e.target as HTMLInputElement).checked}`);
|
||||
} catch (err) {
|
||||
const e = err as AxiosError<{ error: string }>;
|
||||
if (e.response) {
|
||||
this.error = parseMarkdown("**Error:** " + e.response.data.error);
|
||||
}
|
||||
this.error = e.response?.data?.error || e.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -79,8 +70,4 @@ export default defineComponent({
|
|||
.form-check-label {
|
||||
max-width: 100%;
|
||||
}
|
||||
.errorMessage :deep(pre) {
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ export default defineComponent({
|
|||
battery: { type: Object as PropType<Battery> },
|
||||
evopt: { type: Object as PropType<EvOpt>, required: false },
|
||||
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
||||
experimental: Boolean,
|
||||
},
|
||||
emits: ["auth-required"],
|
||||
data() {
|
||||
|
|
@ -213,7 +214,7 @@ export default defineComponent({
|
|||
return grid || solar || co2;
|
||||
},
|
||||
optimizeAvailable() {
|
||||
return !!this.evopt && this.$hiddenFeatures();
|
||||
return !!this.evopt && this.experimental;
|
||||
},
|
||||
showLogout() {
|
||||
return isLoggedIn();
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import type { App } from "vue";
|
||||
import settings from "./settings";
|
||||
|
||||
export function setHiddenFeatures(value: boolean) {
|
||||
settings.hiddenFeatures = value;
|
||||
}
|
||||
|
||||
export function getHiddenFeatures() {
|
||||
return settings.hiddenFeatures;
|
||||
}
|
||||
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
app.config.globalProperties.$hiddenFeatures = getHiddenFeatures;
|
||||
},
|
||||
};
|
||||
|
|
@ -6,7 +6,6 @@ const SETTINGS_LOCALE = "settings_locale";
|
|||
const SETTINGS_THEME = "settings_theme";
|
||||
const SETTINGS_UNIT = "settings_unit";
|
||||
const SETTINGS_12H_FORMAT = "settings_12h_format";
|
||||
const SETTINGS_HIDDEN_FEATURES = "settings_hidden_features";
|
||||
const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details";
|
||||
const SETTINGS_ENERGYFLOW_CO2 = "settings_energyflow_co2";
|
||||
const SETTINGS_ENERGYFLOW_PV = "settings_energyflow_pv";
|
||||
|
|
@ -108,7 +107,6 @@ export interface Settings {
|
|||
theme: THEME | null;
|
||||
unit: string;
|
||||
is12hFormat: boolean;
|
||||
hiddenFeatures: boolean;
|
||||
energyflowDetails: boolean;
|
||||
energyflowCo2: boolean;
|
||||
energyflowPv: boolean;
|
||||
|
|
@ -133,7 +131,6 @@ const settings: Settings = reactive({
|
|||
theme: read(SETTINGS_THEME),
|
||||
unit: read(SETTINGS_UNIT),
|
||||
is12hFormat: readBool(SETTINGS_12H_FORMAT),
|
||||
hiddenFeatures: readBool(SETTINGS_HIDDEN_FEATURES),
|
||||
energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS),
|
||||
energyflowCo2: readBool(SETTINGS_ENERGYFLOW_CO2),
|
||||
energyflowPv: readBool(SETTINGS_ENERGYFLOW_PV),
|
||||
|
|
@ -157,7 +154,6 @@ watch(() => settings.locale, save(SETTINGS_LOCALE));
|
|||
watch(() => settings.theme, save(SETTINGS_THEME));
|
||||
watch(() => settings.unit, save(SETTINGS_UNIT));
|
||||
watch(() => settings.is12hFormat, saveBool(SETTINGS_12H_FORMAT));
|
||||
watch(() => settings.hiddenFeatures, saveBool(SETTINGS_HIDDEN_FEATURES));
|
||||
watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS));
|
||||
watch(() => settings.energyflowCo2, saveBool(SETTINGS_ENERGYFLOW_CO2));
|
||||
watch(() => settings.energyflowPv, saveBool(SETTINGS_ENERGYFLOW_PV));
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ export interface FatalError {
|
|||
|
||||
export interface State {
|
||||
offline: boolean;
|
||||
telemetry?: boolean;
|
||||
experimental?: boolean;
|
||||
setupRequired?: boolean;
|
||||
startupCompleted?: boolean;
|
||||
loadpoints: Loadpoint[];
|
||||
|
|
|
|||
4
assets/js/types/vue.d.ts
vendored
4
assets/js/types/vue.d.ts
vendored
|
|
@ -3,10 +3,6 @@ import type { ComponentCustomProperties } from "vue";
|
|||
|
||||
declare module "vue" {
|
||||
interface ComponentCustomProperties {
|
||||
/**
|
||||
* Whether experimental UI features should be shown.
|
||||
*/
|
||||
$hiddenFeatures: () => boolean;
|
||||
$refs: { [key: string]: HTMLElement | undefined };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
<h2 class="my-4 mt-5">{{ $t("config.section.general") }}</h2>
|
||||
<GeneralConfig
|
||||
:experimental="experimental"
|
||||
:sponsor-error="hasClassError('sponsorship')"
|
||||
@site-changed="siteChanged"
|
||||
/>
|
||||
|
|
@ -371,7 +372,7 @@
|
|||
<MessagingModal @changed="yamlChanged" />
|
||||
<TariffsModal @changed="yamlChanged" />
|
||||
<TelemetryModal :sponsor="sponsor" :telemetry="telemetry" />
|
||||
<ExperimentalModal />
|
||||
<ExperimentalModal :experimental="experimental" />
|
||||
<ModbusProxyModal :is-sponsor="isSponsor" @changed="loadDirty" />
|
||||
<CircuitsModal
|
||||
:gridMeter="gridMeter"
|
||||
|
|
@ -684,8 +685,10 @@ export default defineComponent({
|
|||
return store.state?.ocpp;
|
||||
},
|
||||
telemetry() {
|
||||
// @ts-expect-error: telemetry property exists but not in TypeScript definitions
|
||||
return store.state?.telemetry === true;
|
||||
return store.state?.telemetry;
|
||||
},
|
||||
experimental() {
|
||||
return store.state?.experimental;
|
||||
},
|
||||
eebus() {
|
||||
return store.state?.eebus;
|
||||
|
|
|
|||
|
|
@ -533,12 +533,14 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// read essential config data from state
|
||||
["modbusproxy", "mqtt", "influx", "shm", "interval"].forEach((key) => {
|
||||
const value = store.state[key as keyof State];
|
||||
if (value) {
|
||||
configs[key] = value;
|
||||
["modbusproxy", "mqtt", "influx", "shm", "interval", "experimental"].forEach(
|
||||
(key) => {
|
||||
const value = store.state[key as keyof State];
|
||||
if (value !== undefined && value !== null) {
|
||||
configs[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
this.sections.uiConfig.content = formatJson(configs, EXPAND_KEYS);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -378,6 +378,7 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
valueChan <- util.Param{Key: keys.Database, Val: db.FilePath}
|
||||
valueChan <- util.Param{Key: keys.System, Val: util.System()}
|
||||
valueChan <- util.Param{Key: keys.Timezone, Val: time.Now().Format("MST -07:00")}
|
||||
valueChan <- util.Param{Key: keys.Experimental, Val: isExperimental()}
|
||||
|
||||
// run shutdown functions on stop
|
||||
var once sync.Once
|
||||
|
|
@ -427,7 +428,7 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
site.DumpConfig()
|
||||
site.Prepare(valueChan, pushChan)
|
||||
|
||||
httpd.RegisterSiteHandlers(site, valueChan)
|
||||
httpd.RegisterSiteHandlers(site)
|
||||
|
||||
go func() {
|
||||
site.Run(stopC, conf.Interval)
|
||||
|
|
|
|||
|
|
@ -1175,3 +1175,9 @@ func configureAuth(router *mux.Router, paramC chan<- util.Param) {
|
|||
// wire the handler
|
||||
providerauth.Setup(auth, paramC)
|
||||
}
|
||||
|
||||
// isExperimental returns if experimental features are enabled
|
||||
func isExperimental() bool {
|
||||
b, _ := settings.Bool(keys.Experimental)
|
||||
return b
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package keys
|
|||
|
||||
const (
|
||||
Interval = "interval"
|
||||
Experimental = "experimental"
|
||||
PasswordConfigured = "passwordConfigured"
|
||||
Sponsor = "sponsor"
|
||||
SponsorToken = "sponsorToken"
|
||||
|
|
|
|||
|
|
@ -1111,7 +1111,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Задайте парола за защита на настройките на конфигурацията. Използването на основния екран е възможно и без влизане в системата.",
|
||||
"empty": "Паролата не трябва да бъде празна",
|
||||
"error": "Грешка: ",
|
||||
"labelCurrent": "Текуща парола",
|
||||
"labelNew": "Нова парола",
|
||||
"labelRepeat": "Повторете паролата",
|
||||
|
|
|
|||
|
|
@ -1116,7 +1116,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Nastavte heslo pro zabezpečení konfiguračního menu. Obrazovka přehledu bude stále dostupná i bez ověření heslem.",
|
||||
"empty": "Heslo nesmí být prázdné",
|
||||
"error": "Chyba: ",
|
||||
"labelCurrent": "Současné heslo",
|
||||
"labelNew": "Nové heslo",
|
||||
"labelRepeat": "Opakujte heslo",
|
||||
|
|
|
|||
|
|
@ -1118,7 +1118,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Angiv adgangskode for at beskytte konfigurations indstillinger. Det er stadig mulig at benytte hovedskærmen uden at logge på.",
|
||||
"empty": "Adgangskode må ikke være tomt",
|
||||
"error": "Fejl: ",
|
||||
"labelCurrent": "Nuværende adgangskode",
|
||||
"labelNew": "Ny adgangskode",
|
||||
"labelRepeat": "Gentag adgangskoden",
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@
|
|||
"title": "EEBus"
|
||||
},
|
||||
"experimental": {
|
||||
"description": "Aktiviert die Benutzeroberfläche für Funktionen, die sich noch in der Testphase befinden und sich in zukünftigen Versionen ändern können.",
|
||||
"description": "Bietet frühzeitigen Zugriff auf Funktionen, die sich noch in der Testphase befinden. Diese können instabil sein und in zukünftigen Versionen geändert oder entfernt werden.",
|
||||
"title": "Experimentell"
|
||||
},
|
||||
"ext": {
|
||||
|
|
@ -223,6 +223,7 @@
|
|||
"dragHandle": "Verschieben",
|
||||
"dragItem": "Verschiebbar: {title}",
|
||||
"dragList": "Sortierbare Liste",
|
||||
"error": "Fehler",
|
||||
"experimental": "Experimentell",
|
||||
"forceSave": "Trotzdem speichern",
|
||||
"fromYamlHint": "Hinweis: Konfiguriert über evcc.yaml. Entferne den Eintrag aus der Datei, um die Bearbeitung hier zu aktivieren.",
|
||||
|
|
@ -1124,7 +1125,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Setze ein Passwort, um die Konfiguration zu schützen. Die Hauptansicht bleibt ohne Login zugänglich.",
|
||||
"empty": "Passwort darf nicht leer sein",
|
||||
"error": "Fehler: ",
|
||||
"labelCurrent": "Aktuelles Passwort",
|
||||
"labelNew": "Neues Passwort",
|
||||
"labelRepeat": "Neues Passwort wiederholen",
|
||||
|
|
@ -1238,7 +1238,7 @@
|
|||
},
|
||||
"hiddenFeatures": {
|
||||
"label": "Experimentell",
|
||||
"value": "Experimentelle Funktionen anzeigen."
|
||||
"value": "Experimentelle Funktionen aktivieren."
|
||||
},
|
||||
"language": {
|
||||
"auto": "Automatisch",
|
||||
|
|
|
|||
|
|
@ -1116,7 +1116,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Ορίστε έναν κωδικό πρόσβασης για την προστασία των ρυθμίσεων διαμόρφωσης. Η χρήση της κύριας οθόνης εξακολουθεί να είναι δυνατή χωρίς σύνδεση.",
|
||||
"empty": "Ο κωδικός πρόσβασης δεν πρέπει να είναι κενός",
|
||||
"error": "Σφάλμα: ",
|
||||
"labelCurrent": "Τρέχων κωδικός",
|
||||
"labelNew": "Νέος κωδικός",
|
||||
"labelRepeat": "Επανάληψη κωδικού",
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@
|
|||
"title": "EEBus"
|
||||
},
|
||||
"experimental": {
|
||||
"description": "Enable user interface for features that are still being tested and may change in future versions.",
|
||||
"description": "Provides early access to features that are still being tested. These may be unstable and could change or be removed in future versions.",
|
||||
"title": "Experimental"
|
||||
},
|
||||
"ext": {
|
||||
|
|
@ -223,6 +223,7 @@
|
|||
"dragHandle": "Drag handle",
|
||||
"dragItem": "Draggable: {title}",
|
||||
"dragList": "Reorderable list",
|
||||
"error": "Error",
|
||||
"experimental": "Experimental",
|
||||
"forceSave": "Save anyway",
|
||||
"fromYamlHint": "Note: Configured via evcc.yaml. Remove the entry from the file to enable editing here.",
|
||||
|
|
@ -1124,7 +1125,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Set a password to protect the configuration settings. Using the main screen is still possible without login.",
|
||||
"empty": "Password should not be empty",
|
||||
"error": "Error: ",
|
||||
"labelCurrent": "Current password",
|
||||
"labelNew": "New password",
|
||||
"labelRepeat": "Repeat password",
|
||||
|
|
@ -1238,7 +1238,7 @@
|
|||
},
|
||||
"hiddenFeatures": {
|
||||
"label": "Experimental",
|
||||
"value": "Show experimental features."
|
||||
"value": "Enable experimental features."
|
||||
},
|
||||
"language": {
|
||||
"auto": "Automatic",
|
||||
|
|
|
|||
|
|
@ -1111,7 +1111,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Establezca una contraseña para proteger los ajustes de configuración. Se puede seguir utilizando la pantalla principal sin necesidad de iniciar sesión.",
|
||||
"empty": "La contraseña no debe estar vacía",
|
||||
"error": "Error: ",
|
||||
"labelCurrent": "Contraseña actual",
|
||||
"labelNew": "Nueva contraseña",
|
||||
"labelRepeat": "Repita la contraseña",
|
||||
|
|
|
|||
|
|
@ -1117,7 +1117,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Aseta salasana suojellaksesi määritys asetuksia. Pääikkunan käyttö on silti mahdollista ilman kirjautumista.",
|
||||
"empty": "Salasana ei tulisi olla tyhjä",
|
||||
"error": "Virhe: ",
|
||||
"labelCurrent": "Nykyinen salasana",
|
||||
"labelNew": "Uusi salasana",
|
||||
"labelRepeat": "Toista salasana",
|
||||
|
|
|
|||
|
|
@ -1117,7 +1117,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Configurez un mot de passe pour protéger la configuration. L’utilisation de l’écran principal et toujours possible sans authentification.",
|
||||
"empty": "Le mot de passe ne doit pas être vide",
|
||||
"error": "Erreur : ",
|
||||
"labelCurrent": "Mot de passe actuel",
|
||||
"labelNew": "Nouveau mot de passe",
|
||||
"labelRepeat": "Répétez le mot de passe",
|
||||
|
|
|
|||
|
|
@ -1117,7 +1117,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Postavite lozinku za zaštitu postavki. Početna stranica se može koristiti i bez prijave.",
|
||||
"empty": "Lozinka ne bi trebala biti prazna",
|
||||
"error": "Greška: ",
|
||||
"labelCurrent": "Trenutna lozinka",
|
||||
"labelNew": "Nova lozinka",
|
||||
"labelRepeat": "Ponovite lozinku",
|
||||
|
|
|
|||
|
|
@ -1116,7 +1116,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Állítson be egy jelszót a konfigurációs beállítások védelmére. A főképernyő használata továbbra is lehetséges bejelentkezés nélkül.",
|
||||
"empty": "A jelszó nem lehet üres",
|
||||
"error": "Hiba: ",
|
||||
"labelCurrent": "Jelenlegi jelszó",
|
||||
"labelNew": "Új jelszó",
|
||||
"labelRepeat": "Jelszó megismétlése",
|
||||
|
|
|
|||
|
|
@ -1111,7 +1111,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Imposta una password per proteggere le impostazioni di configurazione. È comunque possibile utilizzare la schermata principale senza effettuare il login.",
|
||||
"empty": "La password non deve essere vuota",
|
||||
"error": "Errore: ",
|
||||
"labelCurrent": "Password attuale",
|
||||
"labelNew": "Nuova password",
|
||||
"labelRepeat": "Ripeti la password",
|
||||
|
|
|
|||
|
|
@ -1116,7 +1116,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Definéiert e Passwuert fir d'Konfiguratiounsastellungen ze schützen. Et kann een den Haaptbildschierm och ouni Login nach ëmmer benotzen.",
|
||||
"empty": "Passwuert duerf net eidel sinn",
|
||||
"error": "Feeler: ",
|
||||
"labelCurrent": "Aktuellt Passwuert",
|
||||
"labelNew": "Neit Passwuert",
|
||||
"labelRepeat": "Neit Passwuert widderhuelen",
|
||||
|
|
|
|||
|
|
@ -1118,7 +1118,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Nustatykite slaptažodį konfigūracijos apsaugai. Pagrindiniu ekranu galima naudotis ir neprisijungus.",
|
||||
"empty": "Slaptažodis negali būti tuščias",
|
||||
"error": "Klaida: ",
|
||||
"labelCurrent": "Dabartinis slaptažodis",
|
||||
"labelNew": "Naujas slaptažodis",
|
||||
"labelRepeat": "Pakartoti slaptažodį",
|
||||
|
|
|
|||
|
|
@ -1117,7 +1117,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Stel een wachtwoord in voor het afschermen van de instellingen. Het hoofdscherm blijft beschikbaar zonder wachtwoord.",
|
||||
"empty": "Wachtwoord mag niet leeg zijn",
|
||||
"error": "Fout: ",
|
||||
"labelCurrent": "Huidig wachtwoord",
|
||||
"labelNew": "Nieuw wachtwoord",
|
||||
"labelRepeat": "Wachtwoord herhalen",
|
||||
|
|
|
|||
|
|
@ -1108,7 +1108,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Angi et passord for å beskytte konfigurasjonsinnstillingene. Det er fortsatt mulig å bruke hovedskjermen uten å logge inn.",
|
||||
"empty": "Passordet må ikke være tomt",
|
||||
"error": "Feil: ",
|
||||
"labelCurrent": "Gjeldende passord",
|
||||
"labelNew": "Nytt passord",
|
||||
"labelRepeat": "Gjenta passord",
|
||||
|
|
|
|||
|
|
@ -1109,7 +1109,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Ustaw hasło, aby chronić ustawienia konfiguracji. Korzystanie z ekranu głównego jest nadal możliwe bez logowania.",
|
||||
"empty": "Hasło nie powinno być puste",
|
||||
"error": "Błąd: ",
|
||||
"labelCurrent": "Aktualne hasło",
|
||||
"labelNew": "Nowe hasło",
|
||||
"labelRepeat": "Powtórz hasło",
|
||||
|
|
|
|||
|
|
@ -1116,7 +1116,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Defina uma Palavra-passe para proteger as definições de configuração. Continua a ser possível utilizar o menu principal sem iniciar sessão.",
|
||||
"empty": "A Palavra-passe não deve ficar por preencher",
|
||||
"error": "Erro: ",
|
||||
"labelCurrent": "Palavra-passe atual",
|
||||
"labelNew": "Nova Palavra-passe",
|
||||
"labelRepeat": "Repetir palavra-passe",
|
||||
|
|
|
|||
|
|
@ -1108,7 +1108,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Setați o parolă pentru a proteja setările de configurare. Utilizarea ecranului principal este în continuare posibilă fără autentificare.",
|
||||
"empty": "Parola nu trebuie să fie goală",
|
||||
"error": "Eroare: ",
|
||||
"labelCurrent": "Parola actuală",
|
||||
"labelNew": "Parolă nouă",
|
||||
"labelRepeat": "Repetați parola",
|
||||
|
|
|
|||
|
|
@ -1107,7 +1107,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Установите пароль для защиты настроек конфигурации. Использование главного экрана по-прежнему возможно без входа в систему.",
|
||||
"empty": "Пароль не должен быть пустым",
|
||||
"error": "Ошибка: ",
|
||||
"labelCurrent": "Текущий пароль",
|
||||
"labelNew": "Новый пароль",
|
||||
"labelRepeat": "Повторите пароль",
|
||||
|
|
|
|||
|
|
@ -1121,7 +1121,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Nastavite geslo za zaščito konfiguracijskih nastavitev. Uporaba glavnega vmesnika je še vedno možna brez prijave.",
|
||||
"empty": "Geslo ne sme biti prazno",
|
||||
"error": "Napaka: ",
|
||||
"labelCurrent": "Trenutno geslo",
|
||||
"labelNew": "Novo geslo",
|
||||
"labelRepeat": "Ponovi geslo",
|
||||
|
|
|
|||
|
|
@ -1118,7 +1118,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Använd lösenord för att skydda inställningarna. Huvudskärmen kan användas utan att logga in.",
|
||||
"empty": "Lösenordet får inte vara tomt",
|
||||
"error": "Fel: ",
|
||||
"labelCurrent": "Nuvarande lösenord",
|
||||
"labelNew": "Nytt lösenord",
|
||||
"labelRepeat": "Upprepa lösenord",
|
||||
|
|
|
|||
|
|
@ -895,7 +895,6 @@
|
|||
"passwordModal": {
|
||||
"description": "உள்ளமைவு அமைப்புகளைப் பாதுகாக்க கடவுச்சொல்லை அமைக்கவும். உள்நுழைவு இல்லாமல் முதன்மையான திரையைப் பயன்படுத்துவது இன்னும் சாத்தியமாகும்.",
|
||||
"empty": "கடவுச்சொல் காலியாக இருக்கக்கூடாது",
|
||||
"error": "பிழை: ",
|
||||
"labelCurrent": "தற்போதைய கடவுச்சொல்",
|
||||
"labelNew": "புதிய கடவுச்சொல்",
|
||||
"labelRepeat": "கடவுச்சொல்லை மீண்டும்",
|
||||
|
|
|
|||
|
|
@ -1116,7 +1116,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Yapılandırma ayarlarını korumak için bir şifre belirle. Ana görünüme erişim oturum açmadan da mümkün.",
|
||||
"empty": "Şifre boş olamaz",
|
||||
"error": "Hata: ",
|
||||
"labelCurrent": "Mevcut şifre",
|
||||
"labelNew": "Yeni şifre",
|
||||
"labelRepeat": "Yeni şifreyi tekrarla",
|
||||
|
|
|
|||
|
|
@ -1118,7 +1118,6 @@
|
|||
"passwordModal": {
|
||||
"description": "Встановіть пароль для захисту параметрів конфігурації. Використання головного екрана все ще можливо без входу.",
|
||||
"empty": "Пароль не повинен бути порожнім",
|
||||
"error": "Помилка: ",
|
||||
"labelCurrent": "Поточний пароль",
|
||||
"labelNew": "Новий пароль",
|
||||
"labelRepeat": "Повторіть пароль",
|
||||
|
|
|
|||
|
|
@ -849,7 +849,6 @@
|
|||
"passwordModal": {
|
||||
"description": "设置密码以保护配置设置。主屏幕仍可在未登录情况下使用。",
|
||||
"empty": "密码不能为空",
|
||||
"error": "错误: ",
|
||||
"labelCurrent": "当前密码",
|
||||
"labelNew": "新密码",
|
||||
"labelRepeat": "重复新密码",
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ func (s *HTTPd) Router() *mux.Router {
|
|||
}
|
||||
|
||||
// RegisterSiteHandlers connects the http handlers to the site
|
||||
func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param) {
|
||||
func (s *HTTPd) RegisterSiteHandlers(site site.API) {
|
||||
router := s.Server.Handler.(*mux.Router)
|
||||
|
||||
// api
|
||||
|
|
@ -293,6 +293,7 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *uti
|
|||
"interval": {"POST", "/interval/{value:[0-9.]+}", settingsSetDurationHandler(keys.Interval, pub)},
|
||||
"updatesponsortoken": {"POST", "/sponsortoken", updateSponsortokenHandler(pub)},
|
||||
"deletesponsortoken": {"DELETE", "/sponsortoken", deleteSponsorTokenHandler(pub)},
|
||||
"experimental": {"POST", "/experimental/{value:[01truefalse]+}", boolHandler(setExperimental(pub), getExperimental)},
|
||||
}
|
||||
|
||||
// yaml handlers
|
||||
|
|
|
|||
|
|
@ -10,6 +10,19 @@ import (
|
|||
"github.com/evcc-io/evcc/util/sponsor"
|
||||
)
|
||||
|
||||
func setExperimental(pub publisher) func(bool) error {
|
||||
return func(b bool) error {
|
||||
settings.SetBool(keys.Experimental, b)
|
||||
pub(keys.Experimental, b)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getExperimental() bool {
|
||||
b, _ := settings.Bool(keys.Experimental)
|
||||
return b
|
||||
}
|
||||
|
||||
func updateSponsortokenHandler(pub publisher) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,32 @@ test.describe("general", async () => {
|
|||
await page.getByTestId("home-link").click();
|
||||
await expect(page.getByRole("heading", { name: "Ahoy World" })).toBeVisible();
|
||||
});
|
||||
test("enable experimental", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
|
||||
const experimentalEntry = page.getByTestId("generalconfig-experimental");
|
||||
|
||||
await expect(experimentalEntry).toContainText("off");
|
||||
await experimentalEntry.getByRole("button", { name: "edit" }).click();
|
||||
|
||||
const modal = page.getByTestId("experimental-modal");
|
||||
await expectModalVisible(modal);
|
||||
|
||||
const experimentalInput = modal.getByLabel("Enable experimental features.");
|
||||
await expect(experimentalInput).not.toBeChecked();
|
||||
await experimentalInput.click();
|
||||
await modal.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expect(experimentalEntry).toContainText("on");
|
||||
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
|
||||
await expect(experimentalEntry).toContainText("on");
|
||||
await experimentalEntry.getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(modal);
|
||||
await expect(experimentalInput).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("network modal", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue