Config UI: General layout (#13166)
This commit is contained in:
parent
1e6780b8b7
commit
198e94e695
9 changed files with 351 additions and 145 deletions
151
assets/js/components/Config/GeneralConfig.vue
Normal file
151
assets/js/components/Config/GeneralConfig.vue
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div class="group pt-4 px-4 pb-1">
|
||||
<dl class="row" data-testid="generalconfig-title">
|
||||
<dt class="col-sm-4">Title</dt>
|
||||
<dd class="col-sm-8">
|
||||
{{ title || "---" }}
|
||||
<a
|
||||
href="#"
|
||||
class="ms-2 d-inline-block text-muted"
|
||||
@click.prevent="openModal('titleModal')"
|
||||
>
|
||||
edit
|
||||
</a>
|
||||
<TitleModal ref="titleModal" @changed="load" />
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Telemetry</dt>
|
||||
<dd class="col-sm-8">
|
||||
{{ telemetryEnabled ? "on" : "off" }}
|
||||
<a
|
||||
href="#"
|
||||
class="ms-2 d-inline-block text-muted"
|
||||
@click.prevent="openModal('globalSettingsModal')"
|
||||
>
|
||||
change
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row wip">
|
||||
<dt class="col-sm-4">Password</dt>
|
||||
<dd class="col-sm-8">
|
||||
*******
|
||||
<a href="#" class="ms-2 d-inline-block text-muted" @click.prevent="todo">edit</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row wip">
|
||||
<dt class="col-sm-4">API-Key</dt>
|
||||
<dd class="col-sm-8">
|
||||
*******
|
||||
<a href="#" class="ms-2 d-inline-block text-muted" @click.prevent="todo">show</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row wip">
|
||||
<dt class="col-sm-4">Sponsoring</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="text-primary"> valid </span>
|
||||
<a href="#" class="ms-2 d-inline-block text-muted" @click.prevent="todo">change</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row wip">
|
||||
<dt class="col-sm-4">Server</dt>
|
||||
<dd class="col-sm-8">
|
||||
http://evcc.local:7070
|
||||
<a href="#" class="ms-2 d-inline-block text-muted" @click.prevent="todo">edit</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row wip">
|
||||
<dt class="col-sm-4">Update Interval</dt>
|
||||
<dd class="col-sm-8">
|
||||
30s
|
||||
<a href="#" class="ms-2 d-inline-block text-muted" @click.prevent="todo">edit</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import TitleModal from "./TitleModal.vue";
|
||||
import api from "../../api";
|
||||
import settings from "../../settings";
|
||||
|
||||
export default {
|
||||
name: "GeneralConfig",
|
||||
data() {
|
||||
return {
|
||||
title: "",
|
||||
};
|
||||
},
|
||||
components: { TitleModal },
|
||||
emits: ["site-changed"],
|
||||
async mounted() {
|
||||
await this.load();
|
||||
},
|
||||
computed: {
|
||||
telemetryEnabled() {
|
||||
return settings.telemetry === true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async changed() {
|
||||
this.$emit("site-changed");
|
||||
this.load();
|
||||
},
|
||||
async load() {
|
||||
try {
|
||||
let res = await api.get("/config/site");
|
||||
this.title = res.data.result.title;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
todo() {
|
||||
alert("not implemented");
|
||||
},
|
||||
openModal(id) {
|
||||
const $el = document.getElementById(id);
|
||||
if ($el) {
|
||||
Modal.getOrCreateInstance($el).show();
|
||||
} else {
|
||||
console.error(`modal ${id} not found`);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group {
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 0 0 0 var(--evcc-gray-50);
|
||||
color: var(--evcc-default-text);
|
||||
background: var(--evcc-box);
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
min-height: 10rem;
|
||||
margin-bottom: 5rem;
|
||||
border: 1px solid var(--evcc-gray-50);
|
||||
transition: box-shadow var(--evcc-transition-fast) linear;
|
||||
}
|
||||
|
||||
.group:hover {
|
||||
border-color: var(--evcc-gray);
|
||||
}
|
||||
|
||||
.group:focus-within {
|
||||
box-shadow: 0 0 1rem 0 var(--evcc-gray-50);
|
||||
}
|
||||
|
||||
.wip {
|
||||
opacity: 0.2;
|
||||
}
|
||||
dt {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
dd {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
<template>
|
||||
<div class="group p-4 pb-2">
|
||||
<form class="container mx-0 px-0">
|
||||
<FormRow id="siteTitle" label="Site title">
|
||||
<input
|
||||
id="siteTitle"
|
||||
v-model="modifiedFormData.title"
|
||||
class="form-control"
|
||||
placeholder="Home"
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow id="adminPassword" label="Admin password" class="wip">
|
||||
<input
|
||||
id="adminPassword"
|
||||
type="password"
|
||||
value="- not set -"
|
||||
class="form-control"
|
||||
disabled
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow id="sponsorToken" label="Sponsor token" class="wip">
|
||||
<textarea id="sponsorToken" class="form-control" rows="3" disabled value="tba" />
|
||||
</FormRow>
|
||||
<div class="my-4 d-flex justify-content-end">
|
||||
<button type="reset" class="btn btn-link text-muted" @click.prevent="reset">
|
||||
{{ $t("config.site.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="!saveNeeded || saving"
|
||||
@click.prevent="save"
|
||||
>
|
||||
<span
|
||||
v-if="saving"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("config.site.save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../api";
|
||||
import FormRow from "../FormRow.vue";
|
||||
|
||||
export default {
|
||||
name: "SiteSettings",
|
||||
components: { FormRow },
|
||||
data() {
|
||||
return {
|
||||
originalFormData: { title: "" },
|
||||
modifiedFormData: { title: "" },
|
||||
saving: false,
|
||||
};
|
||||
},
|
||||
emits: ["site-changed"],
|
||||
async mounted() {
|
||||
await this.load();
|
||||
},
|
||||
computed: {
|
||||
saveNeeded() {
|
||||
return JSON.stringify(this.modifiedFormData) !== JSON.stringify(this.originalFormData);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const { data } = await api.get("/config/site");
|
||||
this.originalFormData.title = data.result.title;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.modifiedFormData = { ...this.originalFormData };
|
||||
},
|
||||
async save() {
|
||||
this.saving = true;
|
||||
try {
|
||||
await api.put("/config/site", this.modifiedFormData);
|
||||
this.$emit("site-changed");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.saving = false;
|
||||
await this.load();
|
||||
},
|
||||
reset() {
|
||||
this.modifiedFormData = { ...this.originalFormData };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group {
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 0 0 0 var(--evcc-gray-50);
|
||||
color: var(--evcc-default-text);
|
||||
background: var(--evcc-box);
|
||||
padding: 1rem 1rem 0.5rem;
|
||||
display: block;
|
||||
list-style-type: none;
|
||||
min-height: 10rem;
|
||||
margin-bottom: 5rem;
|
||||
border: 1px solid var(--evcc-gray-50);
|
||||
transition: box-shadow var(--evcc-transition-fast) linear;
|
||||
}
|
||||
|
||||
.group:hover {
|
||||
border-color: var(--evcc-gray);
|
||||
}
|
||||
|
||||
.group:focus-within {
|
||||
box-shadow: 0 0 1rem 0 var(--evcc-gray-50);
|
||||
}
|
||||
|
||||
.wip {
|
||||
opacity: 0.2;
|
||||
}
|
||||
</style>
|
||||
90
assets/js/components/Config/TitleModal.vue
Normal file
90
assets/js/components/Config/TitleModal.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<GenericModal
|
||||
id="titleModal"
|
||||
ref="modal"
|
||||
:title="$t('config.title.title')"
|
||||
data-testid="title-modal"
|
||||
>
|
||||
<p class="text-danger" v-if="error">{{ error }}</p>
|
||||
<form ref="form" class="container mx-0 px-0">
|
||||
<FormRow
|
||||
id="siteTitle"
|
||||
:label="$t('config.title.label')"
|
||||
:help="$t('config.title.description')"
|
||||
>
|
||||
<input id="siteTitle" v-model="title" class="form-control" />
|
||||
</FormRow>
|
||||
<div class="my-4 d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-link text-muted" data-bs-dismiss="modal">
|
||||
{{ $t("config.general.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="saving"
|
||||
@click.prevent="save"
|
||||
>
|
||||
<span
|
||||
v-if="saving"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("config.general.save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</GenericModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericModal from "../GenericModal.vue";
|
||||
import FormRow from "./FormRow.vue";
|
||||
import api from "../../api";
|
||||
|
||||
export default {
|
||||
name: "TitleModal",
|
||||
components: { FormRow, GenericModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
error: "",
|
||||
title: "",
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.load();
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const { data } = await api.get("/config/site");
|
||||
this.title = data.result.title;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
async save() {
|
||||
this.saving = true;
|
||||
this.error = "";
|
||||
try {
|
||||
await api.put("/config/site", { title: this.title });
|
||||
this.$emit("changed");
|
||||
this.$refs.modal.close();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = e.response.data.error;
|
||||
}
|
||||
this.saving = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
margin-left: calc(var(--bs-gutter-x) * -0.5);
|
||||
margin-right: calc(var(--bs-gutter-x) * -0.5);
|
||||
padding-right: 0;
|
||||
}
|
||||
</style>
|
||||
74
assets/js/components/GenericModal.vue
Normal file
74
assets/js/components/GenericModal.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
:id="id"
|
||||
ref="modal"
|
||||
class="modal fade text-dark"
|
||||
data-bs-backdrop="true"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
:data-testid="dataTestid"
|
||||
>
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{ title }}
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
|
||||
export default {
|
||||
name: "GenericModal",
|
||||
props: {
|
||||
id: String,
|
||||
title: String,
|
||||
dataTestid: String,
|
||||
},
|
||||
emits: ["visibilityChanged"],
|
||||
watch: {
|
||||
isModalVisible(visible) {
|
||||
this.$emit("changed", visible);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.modal.addEventListener("show.bs.modal", this.modalVisible);
|
||||
this.$refs.modal.addEventListener("hide.bs.modal", this.modalInvisible);
|
||||
},
|
||||
unmounted() {
|
||||
this.$refs.modal?.removeEventListener("show.bs.modal", this.modalVisible);
|
||||
this.$refs.modal?.removeEventListener("hide.bs.modal", this.modalInvisible);
|
||||
},
|
||||
methods: {
|
||||
modalVisible() {
|
||||
this.isModalVisible = true;
|
||||
},
|
||||
modalInvisible() {
|
||||
this.isModalVisible = false;
|
||||
},
|
||||
open() {
|
||||
Modal.getOrCreateInstance(this.$refs.modal).show();
|
||||
},
|
||||
close() {
|
||||
Modal.getOrCreateInstance(this.$refs.modal).hide();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="form-check form-switch my-3">
|
||||
<input
|
||||
id="telemetryEnabled"
|
||||
v-model="enabled"
|
||||
:checked="enabled"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
|
|
@ -75,6 +75,7 @@ export default {
|
|||
}
|
||||
try {
|
||||
const response = await api.get("settings/telemetry");
|
||||
console.log("update in settings", response.data.result);
|
||||
settings.telemetry = response.data.result;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
</div>
|
||||
|
||||
<h2 class="my-4 mt-5">General</h2>
|
||||
<SiteSettings @site-changed="siteChanged" />
|
||||
<GeneralConfig @site-changed="siteChanged" />
|
||||
|
||||
<h2 class="my-4 mt-5">Grid, PV & Battery Systems</h2>
|
||||
<ul class="p-0 config-list">
|
||||
|
|
@ -236,14 +236,14 @@ import DeviceCard from "../components/Config/DeviceCard.vue";
|
|||
import DeviceTags from "../components/Config/DeviceTags.vue";
|
||||
import AddDeviceButton from "../components/Config/AddDeviceButton.vue";
|
||||
import MeterModal from "../components/Config/MeterModal.vue";
|
||||
import SiteSettings from "../components/Config/SiteSettings.vue";
|
||||
import GeneralConfig from "../components/Config/GeneralConfig.vue";
|
||||
import formatter from "../mixins/formatter";
|
||||
|
||||
export default {
|
||||
name: "Config",
|
||||
components: {
|
||||
TopHeader,
|
||||
SiteSettings,
|
||||
GeneralConfig,
|
||||
VehicleIcon,
|
||||
VehicleModal,
|
||||
DeviceCard,
|
||||
|
|
@ -457,10 +457,6 @@ export default {
|
|||
grid-gap: 1rem;
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
.wrapper {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.wip {
|
||||
opacity: 0.2 !important;
|
||||
}
|
||||
|
|
|
|||
11
i18n/de.toml
11
i18n/de.toml
|
|
@ -46,6 +46,10 @@ socLimit = "SoC Begrenzung"
|
|||
example = "Beispiel"
|
||||
optional = "optional"
|
||||
|
||||
[config.general]
|
||||
cancel = "Abbrechen"
|
||||
save = "Speichern"
|
||||
|
||||
[config.grid]
|
||||
titleAdd = "Stromzähler (Netz) hinzufügen"
|
||||
titleEdit = "Stromzähler (Netz) bearbeiten"
|
||||
|
|
@ -72,9 +76,10 @@ validateSave = "Überprüfen & speichern"
|
|||
titleAdd = "Zähler (PV) hinzufügen"
|
||||
titleEdit = "Zähler (PV) bearbeiten"
|
||||
|
||||
[config.site]
|
||||
cancel = "Abbrechen"
|
||||
save = "Speichern"
|
||||
[config.title]
|
||||
description = "Wird in der Hauptansicht und im Browser-Tab angezeigt."
|
||||
label = "Titel"
|
||||
title = "Titel bearbeiten"
|
||||
|
||||
[config.validation]
|
||||
failed = "fehlgeschlagen"
|
||||
|
|
|
|||
11
i18n/en.toml
11
i18n/en.toml
|
|
@ -46,6 +46,10 @@ socLimit = "Limit"
|
|||
example = "Example"
|
||||
optional = "optional"
|
||||
|
||||
[config.general]
|
||||
cancel = "Cancel"
|
||||
save = "Save"
|
||||
|
||||
[config.grid]
|
||||
titleAdd = "Add Grid Meter"
|
||||
titleEdit = "Edit Grid Meter"
|
||||
|
|
@ -72,9 +76,10 @@ validateSave = "Validate & save"
|
|||
titleAdd = "Add Solar Meter"
|
||||
titleEdit = "Edit Solar Meter"
|
||||
|
||||
[config.site]
|
||||
cancel = "Cancel"
|
||||
save = "Save"
|
||||
[config.title]
|
||||
description = "Displayed on main screen and browser tab."
|
||||
label = "Title"
|
||||
title = "Edit Title"
|
||||
|
||||
[config.validation]
|
||||
failed = "failed"
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ test.describe("meters", async () => {
|
|||
});
|
||||
});
|
||||
|
||||
test.describe("site", async () => {
|
||||
test.describe("general", async () => {
|
||||
test("change site title", async ({ page }) => {
|
||||
// initial value on main ui
|
||||
await page.goto("/");
|
||||
|
|
@ -195,15 +195,23 @@ test.describe("site", async () => {
|
|||
|
||||
// change value in config
|
||||
await page.goto("/#/config");
|
||||
await page.getByLabel("Site title").fill("Whoops World");
|
||||
await expect(page.getByTestId("generalconfig-title")).toContainText("Hello World");
|
||||
await page.getByTestId("generalconfig-title").getByRole("link", { name: "edit" }).click();
|
||||
const modal = page.getByTestId("title-modal");
|
||||
await expect(modal).toBeVisible();
|
||||
await modal.getByLabel("Title").fill("Whoops World");
|
||||
|
||||
// reset form to initial value
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(page.getByLabel("Site title")).toHaveValue("Hello World");
|
||||
// close modal and ignore entry on cancel
|
||||
await modal.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(modal).not.toBeVisible();
|
||||
await expect(page.getByTestId("generalconfig-title")).toContainText("Hello World");
|
||||
|
||||
// change and save value
|
||||
await page.getByLabel("Site title").fill("Ahoy World");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await page.getByTestId("generalconfig-title").getByRole("link", { name: "edit" }).click();
|
||||
await modal.getByLabel("Title").fill("Ahoy World");
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(modal).not.toBeVisible();
|
||||
await expect(page.getByTestId("generalconfig-title")).toContainText("Ahoy World");
|
||||
|
||||
// check changed value on main ui
|
||||
await page.getByTestId("home-link").click();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue