170 lines
3.9 KiB
Vue
170 lines
3.9 KiB
Vue
<template>
|
|
<div data-testid="offline-indicator" :aria-hidden="!visible">
|
|
<div v-if="offline || starting" class="modal-backdrop" />
|
|
<div
|
|
class="fixed-bottom alert d-flex justify-content-center align-items-center mb-0 rounded-0 p-2"
|
|
:class="{ visible: visible, 'alert-danger': showError, 'alert-secondary': !showError }"
|
|
role="alert"
|
|
data-testid="bottom-banner"
|
|
>
|
|
<div v-if="restarting" class="d-flex align-items-center">
|
|
<RestartButton restarting @restart="restart" />
|
|
{{ $t("offline.restarting") }}
|
|
</div>
|
|
<div
|
|
v-else-if="restartNeeded"
|
|
class="d-flex align-items-center"
|
|
data-testid="restart-needed"
|
|
>
|
|
<RestartButton @restart="restart" />
|
|
{{ $t("offline.restartNeeded") }}
|
|
</div>
|
|
<div v-else-if="offline" class="d-flex align-items-center">
|
|
<CloudOffline class="m-2" />
|
|
{{ $t("offline.message") }}
|
|
</div>
|
|
<div v-else-if="starting" class="d-flex align-items-center">
|
|
<span
|
|
class="spinner-border spinner-border-sm m-1 me-2"
|
|
role="status"
|
|
aria-hidden="true"
|
|
></span>
|
|
{{ $t("offline.starting") }}
|
|
</div>
|
|
<div
|
|
v-else-if="showError"
|
|
class="d-flex align-items-center container px-4 justify-content-center"
|
|
data-testid="fatal-error"
|
|
>
|
|
<shopicon-regular-car1
|
|
size="m"
|
|
class="fatal-icon flex-grow-0 flex-shrink-0"
|
|
></shopicon-regular-car1>
|
|
<div class="mx-4 mt-1">
|
|
<div>
|
|
<strong>
|
|
{{ $t("offline.configurationError") }}
|
|
</strong>
|
|
</div>
|
|
<div class="d-flex flex-column gap-1">
|
|
<div v-for="fatalText in fatalTexts" :key="fatalText" class="text-break">
|
|
{{ fatalText }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<RestartButton error @restart="restart" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { defineComponent, type PropType } from "vue";
|
|
import "@h2d2/shopicons/es/regular/car1";
|
|
import CloudOffline from "../MaterialIcon/CloudOffline.vue";
|
|
import RestartButton from "./RestartButton.vue";
|
|
import restart, { performRestart, restartComplete } from "@/restart";
|
|
import type { FatalError } from "@/types/evcc";
|
|
|
|
export default defineComponent({
|
|
name: "OfflineIndicator",
|
|
components: {
|
|
CloudOffline,
|
|
RestartButton,
|
|
},
|
|
props: {
|
|
offline: Boolean,
|
|
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
|
startupCompleted: Boolean,
|
|
},
|
|
data() {
|
|
return { dismissed: false };
|
|
},
|
|
computed: {
|
|
restartNeeded() {
|
|
return restart.restartNeeded;
|
|
},
|
|
restarting() {
|
|
return restart.restarting;
|
|
},
|
|
starting() {
|
|
return this.startupCompleted === false;
|
|
},
|
|
visible() {
|
|
return (
|
|
this.starting ||
|
|
this.offline ||
|
|
this.restartNeeded ||
|
|
this.restarting ||
|
|
this.showError
|
|
);
|
|
},
|
|
showError() {
|
|
return (
|
|
!this.offline &&
|
|
!this.restartNeeded &&
|
|
!this.restarting &&
|
|
this.fatal.length > 0 &&
|
|
!this.dismissed
|
|
);
|
|
},
|
|
fatalTexts() {
|
|
return this.fatal.map(({ error, class: errorClass }) =>
|
|
errorClass ? `${errorClass}: ${error}` : error
|
|
);
|
|
},
|
|
},
|
|
watch: {
|
|
offline() {
|
|
if (!this.offline) {
|
|
restartComplete();
|
|
this.dismissed = false;
|
|
}
|
|
},
|
|
},
|
|
methods: {
|
|
restart() {
|
|
performRestart();
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
<style scoped>
|
|
.alert {
|
|
opacity: 0;
|
|
transform: translateY(100%);
|
|
min-height: 58px;
|
|
transition:
|
|
transform var(--evcc-transition-fast) ease-in,
|
|
opacity var(--evcc-transition-fast) ease-in;
|
|
/* above backdrop, below modal https://getbootstrap.com/docs/5.3/layout/z-index/ */
|
|
z-index: 1054 !important;
|
|
}
|
|
.alert.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
transition:
|
|
transform var(--evcc-transition-medium) ease-in,
|
|
opacity var(--evcc-transition-medium) ease-in;
|
|
}
|
|
|
|
.fatal-icon {
|
|
transform-origin: 60% 40%;
|
|
animation: swinging 3.5s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes swinging {
|
|
0% {
|
|
transform: translateY(6px) rotate(170deg);
|
|
}
|
|
50% {
|
|
transform: translateY(6px) rotate(185deg);
|
|
}
|
|
100% {
|
|
transform: translateY(6px) rotate(170deg);
|
|
}
|
|
}
|
|
.btn-close {
|
|
filter: none;
|
|
}
|
|
</style>
|