feat(v5): add user friendly error toasts
This commit is contained in:
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"react-dom": "latest",
|
"react-dom": "latest",
|
||||||
"react-quick-pinch-zoom": "latest",
|
"react-quick-pinch-zoom": "latest",
|
||||||
"recharts": "latest",
|
"recharts": "latest",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"vite": "latest"
|
"vite": "latest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -3027,6 +3028,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sonner": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://package-mirror.liara.ir/repository/npm/sonner/-/sonner-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://package-mirror.liara.ir/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://package-mirror.liara.ir/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
"react-quick-pinch-zoom": "latest",
|
"react-quick-pinch-zoom": "latest",
|
||||||
"recharts": "latest",
|
"recharts": "latest",
|
||||||
"lucide-react": "latest",
|
"lucide-react": "latest",
|
||||||
"prop-types": "latest"
|
"prop-types": "latest",
|
||||||
|
"sonner": "^2.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.17",
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Toaster, toast } from "sonner";
|
||||||
import CanvasPane, { CanvasThumbnail } from "./components/CanvasPane.jsx";
|
import CanvasPane, { CanvasThumbnail } from "./components/CanvasPane.jsx";
|
||||||
import Controls from "./components/Controls.jsx";
|
import Controls from "./components/Controls.jsx";
|
||||||
import HistogramPanel from "./components/HistogramPanel.jsx";
|
import HistogramPanel from "./components/HistogramPanel.jsx";
|
||||||
import { applyStateOperation, combineStates, deleteState, getOperations, listStates, uploadImage } from "./lib/api.js";
|
import { applyStateOperation, combineStates, deleteState, getOperations, listStates, uploadImage } from "./lib/api.js";
|
||||||
|
|
||||||
|
function showErrorToast(error, fallback = "Something went wrong.") {
|
||||||
|
const message = error?.message || fallback;
|
||||||
|
toast.error(message, {
|
||||||
|
description: "Please check the selected image, operation, and parameters."
|
||||||
|
});
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [session, setSession] = useState(null);
|
const [session, setSession] = useState(null);
|
||||||
const [states, setStates] = useState([]);
|
const [states, setStates] = useState([]);
|
||||||
@@ -26,7 +35,7 @@ export default function App() {
|
|||||||
setParams({ _repeat: 1, ...Object.fromEntries(Object.entries(first.params || {}).map(([key, schema]) => [key, schema.default])) });
|
setParams({ _repeat: 1, ...Object.fromEntries(Object.entries(first.params || {}).map(([key, schema]) => [key, schema.default])) });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => setStatus(error.message));
|
.catch((error) => setStatus(showErrorToast(error, "Unable to load operations.")));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function refreshStates(sessionId, nextActiveId = null) {
|
async function refreshStates(sessionId, nextActiveId = null) {
|
||||||
@@ -51,7 +60,7 @@ export default function App() {
|
|||||||
setTransform({ x: 0, y: 0, scale: 1 });
|
setTransform({ x: 0, y: 0, scale: 1 });
|
||||||
setStatus(`${payload.width} x ${payload.height} ${payload.color_mode} image loaded as S0.`);
|
setStatus(`${payload.width} x ${payload.height} ${payload.color_mode} image loaded as S0.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(error.message);
|
setStatus(showErrorToast(error, "Unable to upload image."));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -76,7 +85,7 @@ export default function App() {
|
|||||||
setSelectedStateIds([state.state_id]);
|
setSelectedStateIds([state.state_id]);
|
||||||
setStatus(`${state.label} created.`);
|
setStatus(`${state.label} created.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(error.message);
|
setStatus(showErrorToast(error, "Unable to apply operation."));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -92,7 +101,7 @@ export default function App() {
|
|||||||
setSelectedStateIds([state.state_id]);
|
setSelectedStateIds([state.state_id]);
|
||||||
setStatus(`${state.label} created.`);
|
setStatus(`${state.label} created.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(error.message);
|
setStatus(showErrorToast(error, "Unable to combine states."));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -117,7 +126,7 @@ export default function App() {
|
|||||||
setSelectedStateIds((current) => current.filter((id) => id !== state.state_id));
|
setSelectedStateIds((current) => current.filter((id) => id !== state.state_id));
|
||||||
setStatus(`${state.label} deleted.`);
|
setStatus(`${state.label} deleted.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(error.message);
|
setStatus(showErrorToast(error, "Unable to delete state."));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -131,6 +140,7 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-zinc-950 text-zinc-100">
|
<div className="flex h-screen overflow-hidden bg-zinc-950 text-zinc-100">
|
||||||
|
<Toaster richColors theme="dark" position="top-right" closeButton />
|
||||||
<Controls
|
<Controls
|
||||||
operations={operations}
|
operations={operations}
|
||||||
selectedOperation={selectedOperation}
|
selectedOperation={selectedOperation}
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||||
|
|
||||||
|
function humanizeErrorDetail(detail) {
|
||||||
|
if (!detail) return "Request failed";
|
||||||
|
if (typeof detail === "string") return detail;
|
||||||
|
if (Array.isArray(detail)) return detail.map(humanizeErrorDetail).join(" ");
|
||||||
|
if (typeof detail === "object") {
|
||||||
|
return Object.entries(detail)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const message = humanizeErrorDetail(value);
|
||||||
|
return key === "non_field_errors" || key === "detail" ? message : `${key}: ${message}`;
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
return String(detail);
|
||||||
|
}
|
||||||
|
|
||||||
async function parseResponse(response) {
|
async function parseResponse(response) {
|
||||||
const payload = await response.json().catch(() => ({}));
|
const payload = await response.json().catch(() => ({}));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(payload.detail || "Request failed");
|
throw new Error(humanizeErrorDetail(payload.detail || payload));
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user