Unified error handling on the frontend
Diff
chartered-frontend/tsconfig.json | 2 +-
chartered-frontend/src/util.tsx | 23 +++++++++++++++++++++++
chartered-frontend/src/pages/ErrorPage.tsx | 11 +++++++++++
chartered-frontend/src/pages/Loading.tsx | 9 +++++++++
chartered-frontend/src/pages/SingleCrate.tsx | 43 +++++++++++++++++++++++++++++++++++++++----
chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx | 31 ++++++++++++++++++++++++++++---
6 files changed, 98 insertions(+), 21 deletions(-)
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"jsx": "react"
},
"lib": ["es2015"]
"lib": ["ES2015"]
}
@@ -1,3 +1,4 @@
import React = require("react");
import { AuthContext } from "./useAuth";
export const BASE_URL = 'http://localhost:8888';
@@ -8,4 +9,26 @@
export function authenticatedEndpoint(auth: AuthContext, endpoint: string): string {
return `${BASE_URL}/a/${auth.authKey}/web/v1/${endpoint}`;
}
export function useAuthenticatedRequest<S>({ auth, endpoint }: { auth: AuthContext, endpoint: string }, reloadOn = []): { response: S | null, error: string | null } {
const [error, setError] = React.useState(null);
const [response, setResponse] = React.useState(null);
React.useEffect(async () => {
try {
let req = await fetch(authenticatedEndpoint(auth, endpoint));
let res = await req.json();
if (res.error) {
setError(res.error);
} else {
setResponse(res);
}
} catch (e) {
setError(e.message);
}
}, reloadOn);
return { response, error };
}
@@ -1,0 +1,11 @@
import React = require("react");
export default function ErrorPage({ message }: { message: string }) {
return (
<div className="bg-primary min-vh-100 d-flex justify-content-center align-items-center">
<div className="alert alert-danger" role="alert">
{message}
</div>
</div>
);
}
@@ -1,0 +1,9 @@
import React = require("react");
export default function Loading() {
return <div className="min-vh-100 bg-primary d-flex justify-content-center align-items-center">
<div className="spinner-border text-light" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>;
}
@@ -1,13 +1,14 @@
import React = require('react');
import { useState, useEffect } from 'react';
import { Link } from "react-router-dom";
import { useAuth } from '../useAuth';
import Nav from "../sections/Nav";
import Loading from './Loading';
import ErrorPage from './ErrorPage';
import { Box, HouseDoor, Book, Building, PersonPlus } from 'react-bootstrap-icons';
import { useParams } from "react-router-dom";
import { authenticatedEndpoint } from '../util';
import { authenticatedEndpoint, useAuthenticatedRequest } from '../util';
import Prism from 'react-syntax-highlighter/dist/cjs/prism';
import ReactMarkdown from 'react-markdown';
@@ -15,21 +16,39 @@
type Tab = 'readme' | 'versions' | 'members';
interface CrateInfo {
versions: CrateInfoVersion[],
}
interface CrateInfoVersion {
vers: string,
homepage: string | null,
description: string | null,
documentation: string | null,
repository: string | null,
deps: CrateInfoVersionDependency[],
}
interface CrateInfoVersionDependency {
name: string,
version_req: string,
}
export default function SingleCrate() {
const auth = useAuth();
const { crate } = useParams();
const [crateInfo, setCrateInfo] = useState(null);
const [currentTab, setCurrentTab] = useState<Tab>('readme');
useEffect(async () => {
let res = await fetch(authenticatedEndpoint(auth, `crates/${crate}`));
let json = await res.json();
setCrateInfo(json);
}, []);
if (!crateInfo) {
return (<div>Loading...</div>);
const { response: crateInfo, error } = useAuthenticatedRequest<CrateInfo>({
auth,
endpoint: `crates/${crate}`,
});
if (error) {
return <ErrorPage message={error} />;
} else if (!crateInfo) {
return <Loading />;
}
const crateVersion = crateInfo.versions[crateInfo.versions.length - 1];
@@ -156,7 +175,7 @@
);
}
function Members(props: { crateInfo: any }) {
function Members(props: { crateInfo: CrateInfoVersion }) {
const x = ["John Paul", "David Davidson", "Andrew Smith"];
return <div className="container-fluid g-0">
@@ -1,30 +1,45 @@
import React = require("react");
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import Nav from "../../sections/Nav";
import { useAuth } from "../../useAuth";
import { authenticatedEndpoint } from "../../util";
import { useAuthenticatedRequest, authenticatedEndpoint } from "../../util";
import { Plus, Trash } from "react-bootstrap-icons";
import { Button, Modal } from "react-bootstrap";
import HumanTime from "react-human-time";
import ErrorPage from "../ErrorPage";
import Loading from "../Loading";
interface SshKeysResponse {
keys: SshKeysResponseKey[],
}
interface SshKeysResponseKey {
id: number,
name: string,
fingerprint: string,
created_at: string,
last_used_at: string,
}
export default function ListSshKeys() {
const auth = useAuth();
const [error, setError] = useState("");
const [deleting, setDeleting] = useState(null);
const [sshKeys, setSshKeys] = useState(null);
const [reloadSshKeys, setReloadSshKeys] = useState(0);
useEffect(async () => {
let res = await fetch(authenticatedEndpoint(auth, 'ssh-key'));
let json = await res.json();
setSshKeys(json);
const { response: sshKeys, error: loadError } = useAuthenticatedRequest<SshKeysResponse>({
auth,
endpoint: 'ssh-key',
}, [reloadSshKeys]);
if (!sshKeys) {
return (<div>loading...</div>);
if (loadError) {
return <ErrorPage message={loadError} />;
} else if (!sshKeys) {
return <Loading />;
}
const deleteKey = async () => {