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.

This commit is contained in:
Roland 2025-01-24 15:35:26 -07:00
parent e7415873a5
commit 01338e4d89
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
7 changed files with 76 additions and 21 deletions

View file

@ -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 <Container textAlign='center' style={{marginTop: '1.5em', marginBottom: '1em', ...t}}>
<span {...t}>
WROLPi {version} <Dot/>

View file

@ -394,6 +394,8 @@ function DashboardStatus() {
if (status && status['nic_bandwidth_stats']) {
bandwidths = Object.entries(status['nic_bandwidth_stats'])
.map(([name, bandwidth]) => <BandwidthProgressCombined key={name} bandwidth={bandwidth}/>);
} else if (window.apiDown) { // apiDown is set in useStatus
bandwidths = null;
}
return <Segment>

View file

@ -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);

View file

@ -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;

View file

@ -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) {

View file

@ -178,7 +178,16 @@ export function NavBar() {
<Icon loading name='circle notch' size='large'/>
</Link>;
let apiDownIcon;
if (window.apiDown) {
apiDownIcon = <Popup
content='API is not responding'
trigger={<Icon name='plug'/>}
/>
}
const icons = <React.Fragment>
<NavIconWrapper>{apiDownIcon}</NavIconWrapper>
<NavIconWrapper>{processingIcon}</NavIconWrapper>
<NavIconWrapper>{powerIcon}</NavIconWrapper>
<NavIconWrapper>{warningIcon}</NavIconWrapper>

View file

@ -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}
}