Catch UI Errors nicely

This commit is contained in:
Arnaud 2024-08-30 11:47:16 +02:00
parent 92216453f4
commit cdaf500c97
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
19 changed files with 172 additions and 156 deletions

View File

@ -1,7 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { Button, Input, Toast } from "@codex/marketplace-ui-components";
import { CircleCheck } from "lucide-react";
import { CodexSdk } from "../sdk/codex";
export function CodexUrlSettings() {
@ -27,13 +26,6 @@ export function CodexUrlSettings() {
});
};
const Check = () => (
<CircleCheck
size="1.25rem"
fill="var(--codex-color-primary)"
stroke="var(--codex-background-light)"></CircleCheck>
);
return (
<>
<Input
@ -43,7 +35,7 @@ export function CodexUrlSettings() {
value={url}
className="settings-input"></Input>
<Button variant="primary" label="Save changes" onClick={onClick}></Button>
<Toast message={toast.message} time={toast.time} Icon={Check} />
<Toast message={toast.message} time={toast.time} variant="success" />
</>
);
}

View File

@ -0,0 +1,7 @@
.errorBoundary {
border-radius: var(--codex-border-radius);
border: 1px solid var(--codex-border-color);
background-color: var(--codex-background-secondary);
margin: auto;
padding: 1.5rem;
}

View File

@ -1,18 +1,14 @@
import { Placeholder } from "@codex/marketplace-ui-components";
import { CircleX } from "lucide-react";
import React, { ErrorInfo, ReactNode } from "react";
import { ErrorBoundaryContext } from "../../contexts/ErrorBoundaryContext";
import "./ErrorBoundary.css";
type State = {
hasError: boolean;
};
type Props = {
fallback: ({
children,
error,
}: {
children: ReactNode;
error: string;
}) => ReactNode;
card: boolean;
children: ReactNode;
};
@ -37,34 +33,21 @@ export class ErrorBoundary extends React.Component<Props, State> {
console.error("Got error", error, info);
}
private catch(error: Error) {
//logErrorToMyService(error);
console.error(error);
this.setState({ hasError: true });
}
render() {
if (this.state.hasError) {
const Fallback = this.props.fallback;
return (
<Fallback
error={
"Something went wrong, please try to load the component again."
}>
<button
onClick={() => this.setState({ hasError: false })}
className="button">
Retry
</button>
</Fallback>
<div className="errorBoundary">
<Placeholder
Icon={<CircleX size={"4em"}></CircleX>}
title="Something went wrong"
message="Something went wrong, please try to load the component again."
onRetry={() => this.setState({ hasError: false })}></Placeholder>
</div>
);
}
return (
<ErrorBoundaryContext.Provider value={(error) => this.catch(error)}>
{this.props.children}
</ErrorBoundaryContext.Provider>
);
console.info("couc");
return this.props.children;
}
}

View File

@ -1,35 +1,42 @@
import { CodexLogLevel } from "@codex/sdk-js";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useContext, useState } from "react";
import { ErrorBoundaryContext } from "../../contexts/ErrorBoundaryContext";
import { useState } from "react";
import { CodexSdk } from "../../sdk/codex";
import "./LogLevel.css";
import { Button, Select, Toast } from "@codex/marketplace-ui-components";
import { CircleCheck } from "lucide-react";
import { Promises } from "../../utils/promises";
export function LogLevel() {
const queryClient = useQueryClient();
const [level, setLevel] = useState<CodexLogLevel>("DEBUG");
const report = useContext(ErrorBoundaryContext);
const { mutateAsync, isPending, isError, error } = useMutation({
const { mutateAsync, isPending } = useMutation({
mutationKey: ["debug"],
mutationFn: (level: CodexLogLevel) =>
CodexSdk.debug().then((debug) => debug.setLogLevel(level)),
CodexSdk.debug()
.then((debug) => debug.setLogLevel(level))
.then((s) => Promises.rejectOnError(s)),
onSuccess: () => {
setToast({
message: "The log level has been updated successfully.",
time: Date.now(),
variant: "success",
});
queryClient.invalidateQueries({ queryKey: ["debug"] });
},
onError: (error) => {
// TODO report to sentry
setToast({
message: "Error when trying to update: " + error,
time: Date.now(),
variant: "error",
});
},
});
const [toast, setToast] = useState({
time: 0,
message: "",
variant: "success",
});
const [toast, setToast] = useState({ time: 0, message: "" });
if (isError) {
// TODO remove this
report(new Error(error.message));
return "";
}
function onChange(e: React.FormEvent<HTMLSelectElement>) {
const value = e.currentTarget.value;
@ -52,13 +59,6 @@ export function LogLevel() {
["FATAL", "FATAL"],
] satisfies [string, string][];
const Check = () => (
<CircleCheck
size="1.25rem"
fill="var(--codex-color-primary)"
stroke="var(--codex-background-light)"></CircleCheck>
);
return (
<>
<Select
@ -72,7 +72,7 @@ export function LogLevel() {
label="Save changes"
fetching={isPending}
onClick={onClick}></Button>
<Toast message={toast.message} time={toast.time} Icon={Check} />
<Toast message={toast.message} time={toast.time} variant="success" />
</>
);
}

View File

@ -1,28 +1,37 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { CodexSdk } from "../../sdk/codex";
import { Promises } from "../../utils/promises";
import { NetworkIndicator } from "@codex/marketplace-ui-components";
import { NetworkIndicator, Toast } from "@codex/marketplace-ui-components";
export function NodeIndicator() {
const queryClient = useQueryClient();
const [toast, setToast] = useState({
time: 0,
message: "",
});
function useNodeNetwork() {
const { data, isError } = useQuery({
queryKey: ["spr"],
queryFn: async () =>
CodexSdk.node()
.then((node) => node.spr())
.then(Promises.rejectOnError),
.then((data) => {
if (data.error) {
setToast({
message: "Cannot connect to the Codex node.",
time: Date.now(),
});
}
// TODO sentry debug
return data;
}),
retry: false,
refetchInterval: 5000,
});
// TODO handle error
return !isError && !!data;
}
export function NodeIndicator() {
const queryClient = useQueryClient();
const isCodexOnline = useNodeNetwork();
const isCodexOnline = !isError && !!data;
useEffect(() => {
queryClient.invalidateQueries({
@ -31,5 +40,10 @@ export function NodeIndicator() {
});
}, [queryClient, isCodexOnline]);
return <NetworkIndicator online={isCodexOnline} text="Codex node" />;
return (
<>
<Toast message={toast.message} time={toast.time} variant="success" />
<NetworkIndicator online={isCodexOnline} text="Codex node" />
</>
);
}

View File

@ -15,7 +15,7 @@ export function NodeSpaceAllocation() {
}
if (space.error) {
// TODO error
// TODO Sentry
return "";
}

View File

@ -0,0 +1,7 @@
.storageRequestDone {
margin: auto;
}
.storageRequestDone-icon {
color: var(--codex-color-primary);
}

View File

@ -1,30 +1,23 @@
import { Placeholder } from "@codex/marketplace-ui-components";
import { CircleCheck } from "lucide-react";
import { useEffect } from "react";
import "./StorageRequestDone.css";
type Props = {
onChangeNextState: (value: "enable" | "disable") => void;
};
// TODO define style in placeholder component
export function StorageRequestDone({ onChangeNextState }: Props) {
useEffect(() => {
onChangeNextState("enable");
}, [onChangeNextState]);
return (
<div className="emptyPlaceholder" style={{ margin: "auto" }}>
<div
style={{
marginBottom: "1rem",
color: "var(--codex-color-primary)",
}}>
<CircleCheck size="4rem" />
</div>
<b className="emptyPlaceholder-title">Your request is being processed.</b>
<div className="emptyPlaceholder-message">
Processing your request may take some time. Once completed, it will
appear in your purchase list. You can safely close this dialog.
</div>
</div>
<Placeholder
Icon={<CircleCheck size="4rem" className="storageRequestDone-icon" />}
className="storageRequestDone"
title="Your request is being processed."
message=" Processing your request may take some time. Once completed, it will
appear in your purchase list. You can safely close this dialog."></Placeholder>
);
}

View File

@ -33,11 +33,6 @@ export function StorageRequestFileChooser({ onChangeNextState }: Props) {
};
}, [onChangeNextState]);
// if (data?.error) {
// // TODO error
// return "";
// }
const onSelected = (o: DropdownOption) => {
setCid(o.subtitle || "");
onChangeNextState(!o.subtitle ? "disable" : "enable");

View File

@ -7,10 +7,11 @@ import { CodexCreateStorageRequestInput } from "@codex/sdk-js";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { CodexSdk } from "../../sdk/codex";
import { StorageAvailabilityUnit } from "./types";
import { Backdrop, Stepper } from "@codex/marketplace-ui-components";
import { Backdrop, Stepper, Toast } from "@codex/marketplace-ui-components";
import { classnames } from "../../utils/classnames";
import { StorageRequestDone } from "./StorageRequestDone";
import { PurchaseStorage } from "../../utils/purchases-storage";
import { Promises } from "../../utils/promises";
function calculateAvailability(value: number, unit: StorageAvailabilityUnit) {
switch (unit) {
@ -39,29 +40,32 @@ export function StorageRequestStepper({ className, open, onClose }: Props) {
const steps = useRef(["File", "Criteria", "Success"]);
const [isNextDisable, setIsNextDisable] = useState(true);
const queryClient = useQueryClient();
const [toast, setToast] = useState({
time: 0,
message: "",
});
const { mutateAsync, isPending, isError, error } = useMutation({
const { mutateAsync, isPending } = useMutation({
mutationKey: ["debug"],
mutationFn: (input: CodexCreateStorageRequestInput) =>
CodexSdk.marketplace().then((marketplace) =>
marketplace.createStorageRequest(input)
),
onSuccess: async (data, { cid }) => {
if (data.error) {
// TODO report error
console.error(data);
} else {
// setStep((s) => (s = 1));
queryClient.invalidateQueries({ queryKey: ["purchases"] });
CodexSdk.marketplace()
.then((marketplace) => marketplace.createStorageRequest(input))
.then((s) => Promises.rejectOnError(s)),
onSuccess: async (requestId, { cid }) => {
queryClient.invalidateQueries({ queryKey: ["purchases"] });
let requestId = data.data;
if (!requestId.startsWith("0x")) {
requestId = "0x" + requestId;
}
PurchaseStorage.set(requestId, cid);
if (!requestId.startsWith("0x")) {
requestId = "0x" + requestId;
}
PurchaseStorage.set(requestId, cid);
},
onError: (error) => {
// TODO Sentry
setToast({
message: "Error when trying to update: " + error,
time: Date.now(),
});
},
});
@ -80,12 +84,6 @@ export function StorageRequestStepper({ className, open, onClose }: Props) {
[]
);
if (isError) {
// TODO Report error
console.error(error);
return "";
}
const components = [
StorageRequestFileChooser,
// StorageRequestAvailability,
@ -189,6 +187,8 @@ export function StorageRequestStepper({ className, open, onClose }: Props) {
progress={progress || isPending}
isNextDisable={progress || isNextDisable}></Stepper>
</div>
<Toast message={toast.message} time={toast.time} variant="error" />
</>
);
}

View File

@ -1,5 +0,0 @@
import { createContext } from "react";
export const ErrorBoundaryContext = createContext<(error: Error) => void>(
() => ""
);

View File

@ -9,7 +9,7 @@ export function useData() {
const res = await data.cids();
if (res.error) {
// TODO error
// TODO Sentry
return [];
}

View File

@ -0,0 +1,6 @@
.availabilities {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

View File

@ -1,7 +1,16 @@
import { EmptyPlaceholder } from "@codex/marketplace-ui-components";
import { createFileRoute } from "@tanstack/react-router";
import "./availabilities.css";
export const Route = createFileRoute("/dashboard/availabilities")({
component: () => {
return <div>Coming soon</div>;
return (
<div className="availabilities">
<EmptyPlaceholder
title="Nothing to show"
message="This page is in progress."
/>
</div>
);
},
});

View File

@ -22,7 +22,7 @@ function About() {
return (
<>
<div className="dashboard">
<ErrorBoundary fallback={() => ""}>
<ErrorBoundary card={true}>
<Card title="Upload a file">
<Upload
multiple
@ -34,13 +34,13 @@ function About() {
</Card>
</ErrorBoundary>
<ErrorBoundary fallback={() => ""}>
<ErrorBoundary card={true}>
<Welcome />
</ErrorBoundary>
</div>
<div className="container-fluid">
<ErrorBoundary fallback={() => ""}>
<ErrorBoundary card={true}>
<Files />
</ErrorBoundary>
</div>

View File

@ -8,3 +8,8 @@
align-items: center;
justify-content: flex-end;
}
.purchases-loader {
margin: auto;
display: block;
}

View File

@ -3,29 +3,35 @@ import { createFileRoute } from "@tanstack/react-router";
import { CodexSdk } from "../../sdk/codex";
import { Plus } from "lucide-react";
import { useState } from "react";
import { Button, Cell, Table } from "@codex/marketplace-ui-components";
import { Button, Cell, Spinner, Table } from "@codex/marketplace-ui-components";
import { StorageRequestStepper } from "../../components/StorageRequestSetup/StorageRequestStepper";
import "./purchases.css";
import { classnames } from "../../utils/classnames";
import { FileCell } from "../../components/FileCellRender/FIleCell";
import { CustomStateCellRender } from "../../components/CustomStateCellRender/CustomStateCellRender";
import prettyMilliseconds from "pretty-ms";
import { ErrorBoundary } from "../../components/ErrorBoundary/ErrorBoundary";
import { Promises } from "../../utils/promises";
const Purchases = () => {
const [open, setOpen] = useState(false);
const { data, isPending } = useQuery({
queryFn: () =>
CodexSdk.marketplace().then((marketplace) => marketplace.purchases()),
CodexSdk.marketplace()
.then((marketplace) => marketplace.purchases())
.then((s) => Promises.rejectOnError(s)),
queryKey: ["purchases"],
refetchOnWindowFocus: false,
retry: false,
throwOnError: true,
});
if (isPending) {
return <div>Pending</div>;
}
if (data?.error) {
return <div>Error: {data.data.message}</div>;
// TODO Manage error
return (
<div className="purchases-loader">
<Spinner width="3rem" />
</div>
);
}
const headers = [
@ -37,7 +43,7 @@ const Purchases = () => {
"state",
];
const sorted = [...(data?.data || [])].reverse();
const sorted = [...(data || [])].reverse();
const cells =
sorted.map((p, index) => {
const r = p.request;
@ -81,5 +87,9 @@ const Purchases = () => {
};
export const Route = createFileRoute("/dashboard/purchases")({
component: Purchases,
component: () => (
<ErrorBoundary card={true}>
<Purchases />
</ErrorBoundary>
),
});

View File

@ -7,20 +7,19 @@ import { CodexUrlSettings } from "../../CodexUrllSettings/CodexUrlSettings";
export const Route = createFileRoute("/dashboard/settings")({
component: () => (
<>
<ErrorBoundary fallback={() => ""}>
<div className="settings">
<ErrorBoundary fallback={() => ""}>
<LogLevel />
</ErrorBoundary>
</div>
<div className="settings">
<ErrorBoundary card={true}>
<LogLevel />
</ErrorBoundary>
</div>
<div className="settings">
<ErrorBoundary fallback={() => ""}>
<CodexUrlSettings />
</ErrorBoundary>
</div>
<div className="settings">
<ErrorBoundary card={true}>
<CodexUrlSettings />
</ErrorBoundary>
</div>
{/* <div className="input-floating">
{/* <div className="input-floating">
<input
className="input input-floating-input"
id="input-floating"
@ -44,7 +43,6 @@ export const Route = createFileRoute("/dashboard/settings")({
Floating
</label>
</div> */}
</ErrorBoundary>
</>
),
});

View File

@ -1,6 +1,8 @@
import { SafeValue } from "@codex/sdk-js";
export const Promises = {
rejectOnError: <T>(safe: SafeValue<T>) =>
safe.error ? Promise.reject(safe.data) : Promise.resolve(safe.data),
rejectOnError: <T>(safe: SafeValue<T>) => {
// TODO Sentry
return safe.error ? Promise.reject(safe.data) : Promise.resolve(safe.data);
},
};