feat(frontend): Odśwież button beside every screen title, with its text
CI / Check (gateway, backend, frontend) (push) Successful in 57s
CI / Check (gateway, backend, frontend) (push) Successful in 57s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87d111398c
commit
64ffd035c5
5 files changed
+22
-12
No files matched your search
+1
-1
@@ -20,7 +20,7 @@ and gateway's versions in the "Wersje" card of the Stan screen.
|
||||
The Energia screen has three views, kept in the URL hash so Back works: `#energy` (Teraz), `#energy/battery` (Bateria, also
|
||||
opened by tapping the battery in the flow diagram) and `#energy/inverters` (Falowniki, the device details).
|
||||
|
||||
Every screen starts with the "Odśwież" button (`Refresh.tsx`): it posts `api/refresh`, waits 5 s for the gateway's extra poll, then
|
||||
Every screen title (`PageTitle`) has the "Odśwież" button (`Refresh.tsx`) beside it, with its status text below: it posts `api/refresh`, waits 5 s for the gateway's extra poll, then
|
||||
`refreshAll()` (`poll.ts`) fetches every poller that is on screen again. When the gateway skipped every device (polled within a minute) it says so and
|
||||
waits out the shortest cooldown.
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { celsius, hour, longDate } from "./format";
|
||||
import { Icon, type IconName } from "./icons";
|
||||
import { Energy } from "./Energy";
|
||||
import { Overview } from "./Overview";
|
||||
import { RefreshButton } from "./Refresh";
|
||||
import { usePoll } from "./poll";
|
||||
import { useTheme, type Theme } from "./theme";
|
||||
import type { HeatPump } from "./types";
|
||||
@@ -120,7 +119,6 @@ export function App() {
|
||||
<div className="hidden flex-1 lg:flex lg:flex-col"><SideFooter /></div>
|
||||
</nav>
|
||||
<main className="mx-auto max-w-[1500px] px-4 pt-5 pb-28 sm:px-6 lg:px-8 lg:pt-8 lg:pb-10">
|
||||
<RefreshButton />
|
||||
<Suspense fallback={<Loading />}><View /></Suspense>
|
||||
<ThemeSwitch className="mx-auto mt-8 max-w-xs border border-edge bg-panel text-ink lg:hidden" />
|
||||
</main>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { answer, example, serve } from "./fixtures";
|
||||
|
||||
const button = () => screen.getByRole("button", { name: "Odśwież" });
|
||||
const status = () => screen.getByRole("status");
|
||||
const noStatus = () => screen.queryByRole("status");
|
||||
|
||||
// The button and a poller that shows api/heat-pump, as a screen would.
|
||||
async function shown(onPost: () => Promise<Response>) {
|
||||
@@ -68,7 +69,7 @@ describe("RefreshButton", () => {
|
||||
expect(status()).toHaveTextContent("Dane są świeże – spróbuj za 20 s");
|
||||
|
||||
await act(() => vi.advanceTimersByTimeAsync(20_000));
|
||||
expect(status()).toBeEmptyDOMElement();
|
||||
expect(noStatus()).toBeNull();
|
||||
expect(button()).toBeEnabled();
|
||||
});
|
||||
|
||||
@@ -95,4 +96,13 @@ describe("RefreshButton", () => {
|
||||
|
||||
expect(status()).toHaveTextContent("Brak połączenia z siecią");
|
||||
});
|
||||
|
||||
it("sits in the heading row of a screen, next to the title", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(() => Promise.reject(new TypeError("offline"))));
|
||||
const { PageTitle } = await import("./ui");
|
||||
render(<PageTitle title="Stan" />);
|
||||
|
||||
expect(screen.getByRole("banner")).toContainElement(button());
|
||||
expect(button()).toHaveTextContent("Odśwież");
|
||||
});
|
||||
});
|
||||
@@ -56,13 +56,13 @@ export function RefreshButton() {
|
||||
}[state.kind];
|
||||
const busy = state.kind === "busy";
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-end gap-3">
|
||||
<p role="status" className={`text-sm ${state.kind === "error" ? "text-bad" : "text-soft"}`}>{text}</p>
|
||||
<button onClick={tap} disabled={busy || state.kind === "fresh"} aria-label="Odśwież"
|
||||
className="flex min-h-11 min-w-11 items-center justify-center gap-2 rounded-xl border-2 border-brand bg-panel px-3 font-bold text-brand transition active:scale-[0.98] disabled:opacity-40">
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
<button onClick={tap} disabled={busy || state.kind === "fresh"}
|
||||
className="flex min-h-11 items-center justify-center gap-2 rounded-xl border-2 border-brand bg-panel px-3 font-bold text-brand transition active:scale-[0.98] disabled:opacity-40">
|
||||
<Icon name="refresh" className={`size-5 ${busy ? "animate-spin" : ""}`} />
|
||||
<span className="hidden sm:inline">Odśwież</span>
|
||||
Odśwież
|
||||
</button>
|
||||
{text && <p role="status" className={`max-w-44 text-right text-xs ${state.kind === "error" ? "text-bad" : "text-soft"}`}>{text}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+5
-3
@@ -1,5 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { ageMinutes, ago } from "./format";
|
||||
import { RefreshButton } from "./Refresh";
|
||||
|
||||
export const STALE_MINUTES = 10;
|
||||
|
||||
@@ -103,14 +104,15 @@ export function Row({ label, value, icon }: { label: ReactNode; value: ReactNode
|
||||
);
|
||||
}
|
||||
|
||||
export function PageTitle({ title, subtitle, aside }: { title: string; subtitle?: ReactNode; aside?: ReactNode }) {
|
||||
// Every screen starts with this, so the refresh button sits beside each title.
|
||||
export function PageTitle({ title, subtitle }: { title: string; subtitle?: ReactNode }) {
|
||||
return (
|
||||
<header className="mb-5 flex flex-wrap items-end justify-between gap-3">
|
||||
<header className="mb-5 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1>
|
||||
{subtitle && <p className="mt-1 text-sm text-soft">{subtitle}</p>}
|
||||
</div>
|
||||
{aside}
|
||||
<RefreshButton />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user