From 01338e4d8908850edadd76d64536c798ef1f0a2d Mon Sep 17 00:00:00 2001 From: Roland Date: Fri, 24 Jan 2025 15:35:26 -0700 Subject: [PATCH] Displaying plug icon when API fails to respond. Using window.apiDown to communicate to other hooks to stop fetching, the status interval will keep checking if the API is back up. --- app/src/App.js | 12 +++++++----- app/src/DashboardPage.js | 2 ++ app/src/Events.js | 10 +++++++++- app/src/Tags.js | 16 +++++++++++++++- app/src/api.js | 29 +++++++++++++++++------------ app/src/components/Nav.js | 9 +++++++++ app/src/hooks/customHooks.js | 19 +++++++++++++++++-- 7 files changed, 76 insertions(+), 21 deletions(-) diff --git a/app/src/App.js b/app/src/App.js index a5b81ce1..ca7af98f 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -13,7 +13,7 @@ import {FilesRoute} from "./components/Files"; import {QueryProvider, StatusProvider} from "./hooks/customHooks"; import {MapRoute} from "./components/Map"; import {MediaContextProvider, mediaStyles, StatusContext, ThemeContext} from "./contexts/contexts"; -import {Header, Segment, ThemeProvider} from "./components/Theme"; +import {Header, ThemeProvider} from "./components/Theme"; import {DashboardPage} from "./DashboardPage"; import {DonatePage} from "./components/DonatePage"; import {useEventsInterval} from "./Events"; @@ -21,8 +21,6 @@ import {SemanticToastContainer} from "react-semantic-toasts-2"; import {FilePreviewProvider} from "./components/FilePreview"; import {TagsProvider} from "./Tags"; import {ZimRoute} from "./components/Zim"; -import {IframeViewer} from "./components/Common"; -import {HELP_VIEWER_URI} from "./components/Vars"; function PageNotFound() { const {t} = useContext(ThemeContext); @@ -39,8 +37,12 @@ function Dot() { function Footer() { const {t} = useContext(ThemeContext); const {status} = useContext(StatusContext); - let {version} = status; - version = version ? `v${version}` : null; + let version; + try { + version = status?.version ? `v${status.version}` : null; + } catch (e) { + // Not logging because this is not that important. + } return WROLPi {version} diff --git a/app/src/DashboardPage.js b/app/src/DashboardPage.js index d8b9a7eb..f82c20cd 100644 --- a/app/src/DashboardPage.js +++ b/app/src/DashboardPage.js @@ -394,6 +394,8 @@ function DashboardStatus() { if (status && status['nic_bandwidth_stats']) { bandwidths = Object.entries(status['nic_bandwidth_stats']) .map(([name, bandwidth]) => ); + } else if (window.apiDown) { // apiDown is set in useStatus + bandwidths = null; } return diff --git a/app/src/Events.js b/app/src/Events.js index 76706d62..01dd297d 100644 --- a/app/src/Events.js +++ b/app/src/Events.js @@ -1,7 +1,8 @@ import React, {useEffect} from 'react'; import {useRecurringTimeout} from "./hooks/customHooks"; -import {getEvents} from "./api"; +import {ApiDownError, getEvents} from "./api"; import {toast} from "react-semantic-toasts-2"; +import {StatusContext} from "./contexts/contexts"; const apiEventName = 'apiEvent'; @@ -108,12 +109,19 @@ export function useEventsInterval() { } const fetchEvents = async () => { + if (window.apiDown) { // apiDown is set in useStatus + return; + } try { const response = await getEvents(now.current); setNow(response['now']); setEvents(response['events']); } catch (e) { setEvents(null); + if (e instanceof ApiDownError) { + // API is down, do not log this error. + return; + } // Ignore SyntaxError because they happen when the API is down. if (!(e instanceof SyntaxError)) { console.error(e); diff --git a/app/src/Tags.js b/app/src/Tags.js index ad0ec9d0..bffabb37 100644 --- a/app/src/Tags.js +++ b/app/src/Tags.js @@ -1,5 +1,5 @@ import React, {useEffect} from "react"; -import {deleteTag, getTags, saveTag} from "./api"; +import {ApiDownError, deleteTag, getTags, saveTag} from "./api"; import {Dimmer, Divider, Form, Grid, GridColumn, GridRow, Label, TableCell, TableRow,} from "semantic-ui-react"; import { APIButton, @@ -48,6 +48,9 @@ export function useTags() { const {getLocationStr} = React.useContext(QueryContext); const fetchTags = async () => { + if (window.apiDown) { // apiDown is set in useStatus + return; + } try { const t = await getTags(); setTags(t); @@ -55,6 +58,10 @@ export function useTags() { } catch (e) { setTags(undefined); setTagNames(undefined); + if (e instanceof ApiDownError) { + // API is down, do not log this error. + return; + } // Ignore SyntaxError because they happen when the API is down. if (!(e instanceof SyntaxError)) { console.error(e); @@ -62,6 +69,13 @@ export function useTags() { } } + React.useEffect(() => { + if (!window.apiDown) { + // Fetch tags when the API comes back up. + fetchTags(); + } + }, [window.apiDown]); + const findTagByName = (name) => { if (!tags || tags.length === 0) { return; diff --git a/app/src/api.js b/app/src/api.js index 4243aee1..6cb47dd1 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -20,22 +20,32 @@ function timeoutPromise(ms, promise) { }) } +export class ApiDownError extends Error { +} + +const apiDownError = new ApiDownError('API is down'); + async function apiCall(url, method, body, ms = 60_000) { let init = {method}; if (body !== undefined) { init.body = JSON.stringify(body); } - // Create a fetch promise, this will be awaited by the timeout. - let promise = fetch(url, init); - + let promise; try { + // Create a fetch promise, this will be awaited by the timeout. + promise = fetch(url, init); + // await the response or error. const response = await timeoutPromise(ms, promise); if (200 <= response.status && response.status < 300) { // Request was successful. return response; } + if (response.status === 502) { + // API is down. + throw apiDownError; + } // Request encountered an error. let copy = response.clone(); const content = await copy.json(); @@ -51,6 +61,10 @@ async function apiCall(url, method, body, ms = 60_000) { } return response; } catch (e) { + if (e instanceof ApiDownError) { + // Throw ApiDownError immediately without logging. + throw e; + } // Ignore SyntaxError because they happen when the API is down. if (!(e instanceof SyntaxError)) { console.error(e); @@ -236,15 +250,6 @@ export async function downloadVideoMetadata(videoUrl, destination) { return await postDownload(downloadData); } -export async function getDirectories(search_str) { - let form_data = {'search_str': search_str || null}; - const response = await apiPost(`${API_URI}/files/directories`, form_data); - if (response.ok) { - return await response.json(); - } - return []; -} - export async function getStatus() { const response = await apiGet(`${API_URI}/status`); if (response.status === 200) { diff --git a/app/src/components/Nav.js b/app/src/components/Nav.js index 1c75502e..e8327646 100644 --- a/app/src/components/Nav.js +++ b/app/src/components/Nav.js @@ -178,7 +178,16 @@ export function NavBar() { ; + let apiDownIcon; + if (window.apiDown) { + apiDownIcon = } + /> + } + const icons = + {apiDownIcon} {processingIcon} {powerIcon} {warningIcon} diff --git a/app/src/hooks/customHooks.js b/app/src/hooks/customHooks.js index c4963a42..b8890dd0 100644 --- a/app/src/hooks/customHooks.js +++ b/app/src/hooks/customHooks.js @@ -1,5 +1,6 @@ import React, {useContext, useEffect, useRef, useState} from "react"; import { + ApiDownError, createChannel, fetchDecoded, fetchDomains, @@ -804,6 +805,9 @@ export const useSettings = () => { const [pending, setPending] = useState(false); const fetchSettings = async () => { + if (window.apiDown) { + return; + } setPending(true); try { setSettings(await getSettings()) @@ -840,9 +844,15 @@ export const useStatus = () => { const fetchStatus = async () => { try { - const s = await getStatus(); - setStatus(s); + setStatus(await getStatus()); + window.apiDown = false; } catch (e) { + if (e instanceof ApiDownError) { + // API is down, do not log this error. + window.apiDown = true; + setStatus({}); + return; + } // Ignore SyntaxError because they happen when the API is down. if (!(e instanceof SyntaxError)) { console.error(e); @@ -850,6 +860,11 @@ export const useStatus = () => { } } + useEffect(() => { + // Used to tell other hooks to stop fetching intervals. + window.apiDown = false; + }, []); + return {status, fetchStatus} }