mirror of
https://github.com/logos-storage/logos-storage-marketplace-ui.git
synced 2026-01-02 13:33:06 +00:00
Catch UI Errors nicely
This commit is contained in:
parent
92216453f4
commit
cdaf500c97
@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
7
src/components/ErrorBoundary/ErrorBoundary.css
Normal file
7
src/components/ErrorBoundary/ErrorBoundary.css
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export function NodeSpaceAllocation() {
|
||||
}
|
||||
|
||||
if (space.error) {
|
||||
// TODO error
|
||||
// TODO Sentry
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
.storageRequestDone {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.storageRequestDone-icon {
|
||||
color: var(--codex-color-primary);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const ErrorBoundaryContext = createContext<(error: Error) => void>(
|
||||
() => ""
|
||||
);
|
||||
@ -9,7 +9,7 @@ export function useData() {
|
||||
const res = await data.cids();
|
||||
|
||||
if (res.error) {
|
||||
// TODO error
|
||||
// TODO Sentry
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
6
src/routes/dashboard/availabilities.css
Normal file
6
src/routes/dashboard/availabilities.css
Normal file
@ -0,0 +1,6 @@
|
||||
.availabilities {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -8,3 +8,8 @@
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.purchases-loader {
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
),
|
||||
});
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user