evcc-io/assets/js/components/Config/OcppForwarderModal.vue
2026-06-07 11:17:54 +00:00

280 lines
7.6 KiB
Vue

<template>
<JsonModal
name="ocppforwarder"
:title="$t('config.ocppforwarder.editTitle')"
:description="$t('config.ocppforwarder.description')"
endpoint="/config/ocppforwarder"
state-key="ocppforwarder.config"
no-buttons
:transform-read-values="transformReadValues"
:transform-write-values="transformWriteValues"
@changed="$emit('changed')"
>
<template
#default="{
values,
changes,
save,
}: {
values: OcppForwarderRule;
changes: boolean;
save: (close?: boolean) => void;
}"
>
<p v-if="ruleExists" class="mb-3" data-testid="ocppforwarder-status">
{{ $t("config.ocppforwarder.status") }}:
<span :class="connectionConnected ? 'text-success' : 'text-danger'">{{
connectionLabel
}}</span>
</p>
<div
v-if="sessionError"
class="alert alert-danger"
role="alert"
data-testid="ocppforwarder-error"
>
{{ sessionError }}
</div>
<FormRow
id="ocppforwarderUpstreamUrl"
:label="$t('config.ocppforwarder.upstreamUrl')"
:help="$t('config.ocppforwarder.upstreamUrlHelp')"
example="wss://billing.example.com/ocpp"
>
<input
id="ocppforwarderUpstreamUrl"
v-model="values.upstreamUrl"
type="text"
class="form-control"
inputmode="url"
spellcheck="false"
autocomplete="off"
required
/>
</FormRow>
<FormRow
id="ocppforwarderUsername"
:label="$t('config.ocppforwarder.username')"
:help="$t('config.ocppforwarder.usernameHelp')"
optional
>
<input
id="ocppforwarderUsername"
v-model="values.username"
type="text"
class="form-control"
spellcheck="false"
autocomplete="off"
/>
</FormRow>
<FormRow
id="ocppforwarderPassword"
:label="$t('config.ocppforwarder.password')"
:help="$t('config.ocppforwarder.passwordHelp')"
optional
>
<input
id="ocppforwarderPassword"
v-model="values.password"
type="password"
class="form-control"
autocomplete="new-password"
/>
</FormRow>
<FormRow
id="ocppforwarderUpstreamStationId"
:label="$t('config.ocppforwarder.upstreamStationId')"
:help="$t('config.ocppforwarder.upstreamStationIdHelp')"
optional
>
<input
id="ocppforwarderUpstreamStationId"
v-model="values.upstreamStationId"
type="text"
class="form-control"
:placeholder="targetStationId"
spellcheck="false"
autocomplete="off"
/>
</FormRow>
<PropertyCollapsible>
<template #advanced>
<FormRow
id="ocppforwarderReadOnly"
:label="$t('config.ocppforwarder.readOnly.label')"
:help="getReadOnlyHelp(values.readOnly)"
>
<div class="d-flex">
<input
id="ocppforwarderReadOnly"
v-model="values.readOnly"
class="form-check-input"
type="checkbox"
/>
<label class="form-check-label ms-2" for="ocppforwarderReadOnly">
{{ $t("config.ocppforwarder.readOnly.check") }}
</label>
</div>
</FormRow>
<FormRow
id="ocppforwarderInsecure"
:label="$t('config.ocppforwarder.labelInsecure')"
>
<div class="d-flex">
<input
id="ocppforwarderInsecure"
v-model="values.insecure"
class="form-check-input"
type="checkbox"
/>
<label class="form-check-label ms-2" for="ocppforwarderInsecure">
{{ $t("config.ocppforwarder.labelCheckInsecure") }}
</label>
</div>
</FormRow>
<FormRow
id="ocppforwarderCaCert"
:label="$t('config.ocppforwarder.labelCaCert')"
optional
>
<PropertyCertField id="ocppforwarderCaCert" v-model="values.caCert" />
</FormRow>
</template>
</PropertyCollapsible>
<div class="mt-4 d-flex justify-content-between gap-2 flex-column flex-sm-row">
<div
class="d-flex justify-content-between order-2 order-sm-1 gap-2 flex-grow-1 flex-sm-grow-0"
>
<button
type="button"
class="btn btn-link text-muted btn-cancel"
data-bs-dismiss="modal"
>
{{ $t("config.general.cancel") }}
</button>
<button
v-if="ruleExists"
type="button"
class="btn btn-link text-danger"
:disabled="removing"
@click="removeRule"
>
{{ $t("config.general.remove") }}
</button>
</div>
<button
v-if="changes"
type="button"
class="btn btn-primary order-1 order-sm-2 flex-grow-1 flex-sm-grow-0 px-4"
:disabled="!values.upstreamUrl"
@click="save(false)"
>
{{ $t("config.general.save") }}
</button>
<button
v-else
type="button"
class="btn btn-outline-primary order-1 order-sm-2 flex-grow-1 flex-sm-grow-0 px-4"
data-bs-dismiss="modal"
>
{{ $t("config.general.close") }}
</button>
</div>
</template>
</JsonModal>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import JsonModal from "./JsonModal.vue";
import FormRow from "./FormRow.vue";
import PropertyCollapsible from "./PropertyCollapsible.vue";
import PropertyCertField from "./PropertyCertField.vue";
import type { OcppForwarderRule, OcppForwarderSession } from "@/types/evcc";
import { getModal, closeModal } from "@/configModal";
import api from "@/api";
import store from "@/store";
export default defineComponent({
name: "OcppForwarderModal",
components: { JsonModal, FormRow, PropertyCollapsible, PropertyCertField },
emits: ["changed"],
data() {
return { removing: false };
},
computed: {
// station id the modal is editing, carried via the config modal stack
targetStationId(): string {
return getModal("ocppforwarder")?.station || "";
},
rules(): OcppForwarderRule[] {
return store.state?.ocppforwarder?.config || [];
},
ruleExists(): boolean {
return this.rules.some((r) => r.stationId === this.targetStationId);
},
session(): OcppForwarderSession | undefined {
return store.state?.ocppforwarder?.status?.find(
(s) => s.chargerId === this.targetStationId
);
},
sessionError(): string | undefined {
return this.session?.error;
},
connectionConnected(): boolean {
return !!this.session?.upstreamConnected;
},
connectionLabel(): string {
return this.$t(
this.connectionConnected
? "config.ocpp.status.connected"
: "config.ocpp.status.configured"
);
},
},
methods: {
getReadOnlyHelp(readOnly?: boolean): string {
return this.$t(`config.ocppforwarder.readOnly.help.${readOnly ? "true" : "false"}`);
},
// pick the rule for the target station, or seed a new one prefilled with the station id
transformReadValues(rules: OcppForwarderRule[]): OcppForwarderRule {
const list = Array.isArray(rules) ? rules : [];
const existing = list.find((r) => r.stationId === this.targetStationId);
return existing
? { ...existing }
: {
stationId: this.targetStationId,
upstreamUrl: "",
};
},
// merge the edited rule back into the complete set that gets persisted
transformWriteValues(rule: OcppForwarderRule): OcppForwarderRule[] {
const list = this.rules.map((r) => ({ ...r }));
const index = list.findIndex((r) => r.stationId === rule.stationId);
if (index >= 0) {
list[index] = rule;
} else {
list.push(rule);
}
return list;
},
async removeRule() {
this.removing = true;
try {
const list = this.rules.filter((r) => r.stationId !== this.targetStationId);
const res = await api.post("/config/ocppforwarder", list, {
validateStatus: (code: number) => [200, 202, 400].includes(code),
});
if (res.status === 200 || res.status === 202) {
this.$emit("changed");
await closeModal();
}
} catch (e) {
console.error(e);
}
this.removing = false;
},
},
});
</script>