UI: add restart button to fatal error banner (#21688)

This commit is contained in:
Michael Geers 2025-06-06 22:03:35 +02:00 • committed by GitHub
parent fdae6a72f9
commit 334ab6cc06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 42 deletions

View file

@ -1,5 +1,5 @@
<template>
<div>
<div data-testid="offline-indicator">
<div v-if="offline" class="modal-backdrop" />
<div
class="fixed-bottom alert d-flex justify-content-center align-items-center mb-0 rounded-0 p-2"
@ -8,19 +8,7 @@
data-testid="bottom-banner"
>
<div v-if="restarting" class="d-flex align-items-center">
<button
class="btn btn-secondary me-2 btn-sm d-flex align-items-center"
type="button"
disabled
tabindex="0"
>
<span
class="spinner-border spinner-border-sm m-1 me-2"
role="status"
aria-hidden="true"
></span>
{{ $t("offline.restart") }}
</button>
<RestartButton restarting @restart="restart" />
{{ $t("offline.restarting") }}
</div>
<div
@ -28,15 +16,7 @@
class="d-flex align-items-center"
data-testid="restart-needed"
>
<button
class="btn btn-secondary me-2 btn-sm d-flex align-items-center"
type="button"
tabindex="0"
@click="restart"
>
<Sync class="restart me-2" />
{{ $t("offline.restart") }}
</button>
<RestartButton @restart="restart" />
{{ $t("offline.restartNeeded") }}
</div>
<div v-else-if="offline" class="d-flex align-items-center">
@ -45,7 +25,7 @@
</div>
<div
v-else-if="showError"
class="d-flex align-items-start container px-4 justify-content-center"
class="d-flex align-items-center container px-4 justify-content-center"
data-testid="fatal-error"
>
<shopicon-regular-car1
@ -60,13 +40,7 @@
</div>
<div v-if="fatalText" class="text-break">{{ fatalText }}</div>
</div>
<button
type="button"
class="btn-close mt-1"
aria-label="Close"
tabindex="0"
@click="dismissed = true"
></button>
<RestartButton error @restart="restart" />
</div>
</div>
</div>
@ -76,7 +50,7 @@
import { defineComponent, type PropType } from "vue";
import "@h2d2/shopicons/es/regular/car1";
import CloudOffline from "../MaterialIcon/CloudOffline.vue";
import Sync from "../MaterialIcon/Sync.vue";
import RestartButton from "./RestartButton.vue";
import restart, { performRestart, restartComplete } from "@/restart";
import type { FatalError } from "@/types/evcc";
@ -84,7 +58,7 @@ export default defineComponent({
name: "OfflineIndicator",
components: {
CloudOffline,
Sync,
RestartButton,
},
props: {
offline: Boolean,
@ -134,9 +108,6 @@ export default defineComponent({
});
</script>
<style scoped>
.restart {
transform: scaleX(-1);
}
.alert {
opacity: 0;
transform: translateY(100%);

View file

@ -0,0 +1,61 @@
<template>
<button
class="btn me-2 btn-sm d-flex align-items-center"
:class="error ? 'btn-outline-danger' : 'btn-secondary'"
type="button"
:disabled="restarting"
tabindex="0"
@click="handleRestart"
>
<span
v-if="restarting"
class="spinner-border spinner-border-sm m-1 me-2"
role="status"
aria-hidden="true"
></span>
<Sync v-else :size="iconSize" class="restart me-2" />
{{ $t("offline.restart") }}
</button>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import Sync from "../MaterialIcon/Sync.vue";
import { ICON_SIZE } from "@/types/evcc";
export default defineComponent({
name: "RestartButton",
components: {
Sync,
},
props: {
restarting: {
type: Boolean,
default: false,
},
error: {
type: Boolean,
default: false,
},
},
emits: ["restart"],
computed: {
iconSize() {
return ICON_SIZE.S;
},
},
methods: {
handleRestart() {
if (!this.restarting) {
this.$emit("restart");
}
},
},
});
</script>
<style scoped>
.restart {
transform: scaleX(-1);
}
</style>

View file

@ -5,8 +5,8 @@ export default defineComponent({
props: {
size: {
type: String as PropType<ICON_SIZE>,
validator(value: string) {
return Object.keys(ICON_SIZE).includes(value);
validator(value: ICON_SIZE) {
return Object.values(ICON_SIZE).includes(value);
},
default: ICON_SIZE.S,
},

View file

@ -7,17 +7,21 @@ test.describe("fatal", async () => {
test("evcc yaml error", async ({ page }) => {
const instance = await start("fatal-syntax.evcc.yaml");
await page.goto("/");
await expect(page.getByTestId("bottom-banner")).toBeVisible();
await expect(page.getByTestId("bottom-banner")).toContainText("failed parsing config file");
const offline = page.getByTestId("offline-indicator");
await expect(offline.getByTestId("bottom-banner")).toBeVisible();
await expect(offline.getByTestId("bottom-banner")).toContainText("failed parsing config file");
await expect(page.getByTestId("generalconfig-password")).not.toBeVisible();
await expect(offline.getByRole("button", { name: "Restart" })).toBeVisible();
await stop(instance);
});
test("database error", async ({ page }) => {
const instance = await start("fatal-db.evcc.yaml");
await page.goto("/");
await expect(page.getByTestId("bottom-banner")).toBeVisible();
await expect(page.getByTestId("bottom-banner")).toContainText("invalid database");
const offline = page.getByTestId("offline-indicator");
await expect(offline.getByTestId("bottom-banner")).toBeVisible();
await expect(offline.getByTestId("bottom-banner")).toContainText("invalid database");
await expect(page.getByTestId("generalconfig-password")).not.toBeVisible();
await expect(offline.getByRole("button", { name: "Restart" })).toBeVisible();
await stop(instance);
});
});