mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-27 07:30:34 +08:00
Improve catching of errors when loading routes
- Handle ResponseParseErros, ResponseErrors and normal errors differently. - Show them all in the same `LoadError` view - Add a subtitle to give users some more information on what could have gone wrong. - Add more properties to the copyable error message to make debugging easier.
This commit is contained in:
+3
-3
@@ -24,7 +24,7 @@
|
||||
import Session from "@app/views/session/Index.svelte";
|
||||
import Source from "@app/views/projects/Source.svelte";
|
||||
|
||||
import LoadError from "@app/components/LoadError.svelte";
|
||||
import Error from "@app/views/error/View.svelte";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
|
||||
const activeRouteStore = router.activeRouteStore;
|
||||
@@ -87,8 +87,8 @@
|
||||
<Patches {...$activeRouteStore.params} />
|
||||
{:else if $activeRouteStore.resource === "project.patch"}
|
||||
<Patch {...$activeRouteStore.params} />
|
||||
{:else if $activeRouteStore.resource === "loadError"}
|
||||
<LoadError {...$activeRouteStore.params} />
|
||||
{:else if $activeRouteStore.resource === "error"}
|
||||
<Error {...$activeRouteStore.params} />
|
||||
{:else if $activeRouteStore.resource === "notFound"}
|
||||
<NotFound {...$activeRouteStore.params} />
|
||||
{:else}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if $activeRouteStore.resource === "booting" || $activeRouteStore.resource === "home" || $activeRouteStore.resource === "session" || $activeRouteStore.resource === "loadError" || $activeRouteStore.resource === "notFound"}
|
||||
{#if $activeRouteStore.resource === "booting" || $activeRouteStore.resource === "home" || $activeRouteStore.resource === "session" || $activeRouteStore.resource === "error" || $activeRouteStore.resource === "notFound"}
|
||||
<!-- Don't render breadcrumbs for these routes. -->
|
||||
{:else if $activeRouteStore.resource === "nodes"}
|
||||
<div class="breadcrumbs">
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type {
|
||||
ResponseError,
|
||||
ResponseParseError,
|
||||
} from "@httpd-client/lib/fetcher";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
import { config } from "@app/lib/config";
|
||||
import Command from "./Command.svelte";
|
||||
import ExternalLink from "./ExternalLink.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
export let message: string;
|
||||
export let error: any | undefined = undefined;
|
||||
export let title: string;
|
||||
export let description: string;
|
||||
export let error: Error | ResponseParseError | ResponseError | undefined =
|
||||
undefined;
|
||||
export let icon: ComponentProps<Icon>["name"] = "alert";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -19,6 +29,18 @@
|
||||
border-radius: var(--border-radius-small);
|
||||
gap: 1rem;
|
||||
}
|
||||
.label {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-regular);
|
||||
max-width: 36rem;
|
||||
}
|
||||
.error :global(code) {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-small);
|
||||
background-color: var(--color-fill-ghost);
|
||||
border-radius: var(--border-radius-tiny);
|
||||
padding: 0.125rem 0.25rem;
|
||||
}
|
||||
|
||||
.help {
|
||||
font-size: var(--font-size-small);
|
||||
@@ -27,8 +49,12 @@
|
||||
</style>
|
||||
|
||||
<div class="error">
|
||||
<Icon name="alert" size="48" />
|
||||
{message}
|
||||
<Icon name={icon} size="48" />
|
||||
<div class="txt-medium txt-bold">
|
||||
{title}
|
||||
</div>
|
||||
<!-- This @html is secure since we don't allow user input -->
|
||||
<div class="label">{@html description}</div>
|
||||
{#if error}
|
||||
<div class="help">
|
||||
If you need help resolving this issue, copy the error message
|
||||
@@ -40,11 +66,7 @@
|
||||
</div>
|
||||
<div style:max-width="25rem">
|
||||
<Command
|
||||
command={JSON.stringify({
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
...error,
|
||||
})}
|
||||
command={JSON.stringify(error, Object.getOwnPropertyNames(error))}
|
||||
fullWidth
|
||||
showPrompt={false} />
|
||||
</div>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<script lang="ts">
|
||||
import AppLayout from "@app/App/AppLayout.svelte";
|
||||
import Command from "./Command.svelte";
|
||||
import ExternalLink from "./ExternalLink.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
export let title: string;
|
||||
export let errorMessage: string;
|
||||
export let stackTrace: string;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
gap: 1.5rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.help {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
||||
<AppLayout>
|
||||
<div class="wrapper">
|
||||
<Icon name="desert" size="48" />
|
||||
<div class="container">
|
||||
<div class="txt-medium txt-bold">
|
||||
{title}
|
||||
</div>
|
||||
<div class="help">
|
||||
If you need help resolving this issue, copy the error message
|
||||
<br class="global-hide-on-mobile" />
|
||||
below and send it to us on
|
||||
<ExternalLink href="https://radicle.zulipchat.com">
|
||||
radicle.zulipchat.com
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style:max-width="25rem">
|
||||
<Command
|
||||
command={JSON.stringify({ errorMessage, stackTrace })}
|
||||
fullWidth
|
||||
showPrompt={false} />
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
+3
-3
@@ -122,8 +122,8 @@ function setTitle(loadedRoute: LoadedRoute) {
|
||||
|
||||
if (loadedRoute.resource === "booting" || loadedRoute.resource === "home") {
|
||||
title.push("Radicle");
|
||||
} else if (loadedRoute.resource === "loadError") {
|
||||
title.push("Load error");
|
||||
} else if (loadedRoute.resource === "error") {
|
||||
title.push("Error");
|
||||
title.push("Radicle");
|
||||
} else if (loadedRoute.resource === "notFound") {
|
||||
title.push("Page not found");
|
||||
@@ -259,7 +259,7 @@ export function routeToPath(route: Route): string {
|
||||
} else if (
|
||||
route.resource === "booting" ||
|
||||
route.resource === "notFound" ||
|
||||
route.resource === "loadError"
|
||||
route.resource === "error"
|
||||
) {
|
||||
return "";
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { HomeRoute, HomeLoadedRoute } from "@app/views/home/router";
|
||||
import type {
|
||||
ResponseParseError,
|
||||
ResponseError,
|
||||
} from "@httpd-client/lib/fetcher";
|
||||
import type {
|
||||
ProjectLoadedRoute,
|
||||
ProjectRoute,
|
||||
@@ -29,19 +33,19 @@ interface SessionRoute {
|
||||
};
|
||||
}
|
||||
|
||||
export interface LoadErrorRoute {
|
||||
resource: "loadError";
|
||||
export interface ErrorRoute {
|
||||
resource: "error";
|
||||
params: {
|
||||
title: string;
|
||||
errorMessage: string;
|
||||
stackTrace: string;
|
||||
description: string;
|
||||
error: Error | ResponseError | ResponseParseError;
|
||||
};
|
||||
}
|
||||
|
||||
export type Route =
|
||||
| BootingRoute
|
||||
| HomeRoute
|
||||
| LoadErrorRoute
|
||||
| ErrorRoute
|
||||
| NotFoundRoute
|
||||
| ProjectRoute
|
||||
| NodesRoute
|
||||
@@ -50,7 +54,7 @@ export type Route =
|
||||
export type LoadedRoute =
|
||||
| BootingRoute
|
||||
| HomeLoadedRoute
|
||||
| LoadErrorRoute
|
||||
| ErrorRoute
|
||||
| NotFoundRoute
|
||||
| ProjectLoadedRoute
|
||||
| NodesLoadedRoute
|
||||
|
||||
+5
-1
@@ -1,4 +1,4 @@
|
||||
import type { Comment } from "@httpd-client";
|
||||
import type { BaseUrl, Comment } from "@httpd-client";
|
||||
|
||||
import md5 from "md5";
|
||||
import bs58 from "bs58";
|
||||
@@ -93,6 +93,10 @@ export function formatEditedCaption(lastEdit: Comment["edits"][0]) {
|
||||
} edited ${formatTimestamp(lastEdit.timestamp / 1000)}`;
|
||||
}
|
||||
|
||||
export function baseUrlToUrl(baseUrl: BaseUrl): URL {
|
||||
return new URL(`${baseUrl.scheme}://${baseUrl.hostname}:${baseUrl.port}`);
|
||||
}
|
||||
|
||||
// Generates a publicly shareable link.
|
||||
export function formatPublicExplorer(
|
||||
publicExplorer: string,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
import AppLayout from "@app/App/AppLayout.svelte";
|
||||
import ErrorMessage from "@app/components/ErrorMessage.svelte";
|
||||
|
||||
export let title: string;
|
||||
export let description: string;
|
||||
export let error: ComponentProps<ErrorMessage>["error"];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
padding: 4rem 0 2rem 0;
|
||||
gap: 1.5rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<AppLayout>
|
||||
<div class="wrapper">
|
||||
<div class="container">
|
||||
<ErrorMessage icon="desert" {title} {description} {error} />
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
+37
-33
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { ProjectWithListingData } from "@app/lib/projects";
|
||||
import type { BaseUrl } from "@httpd-client";
|
||||
import type { ProjectWithListingData } from "@app/lib/projects";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
import storedWritable from "@efstajas/svelte-stored-writable";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
@@ -9,15 +10,17 @@
|
||||
|
||||
import { api, httpdStore } from "@app/lib/httpd";
|
||||
import { deduplicateStore } from "@app/lib/deduplicateStore";
|
||||
import { baseUrlToUrl } from "@app/lib/utils";
|
||||
import { getProjectsListingData } from "@app/lib/projects";
|
||||
import { handleError } from "@app/views/home/error";
|
||||
import { isDelegate } from "@app/lib/roles";
|
||||
import { preferredSeeds } from "@app/lib/seeds";
|
||||
|
||||
import AppLayout from "@app/App/AppLayout.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import ConnectInstructions from "@app/components/ConnectInstructions.svelte";
|
||||
import ProjectCard from "@app/components/ProjectCard.svelte";
|
||||
|
||||
import ErrorMessage from "@app/components/ErrorMessage.svelte";
|
||||
import FilterButton from "./components/FilterButton.svelte";
|
||||
import HomepageSection from "./components/HomepageSection.svelte";
|
||||
import NewProjectButton from "./components/NewProjectButton.svelte";
|
||||
@@ -36,8 +39,14 @@
|
||||
"all",
|
||||
);
|
||||
|
||||
let localProjects: ProjectWithListingData[] | "error" | undefined;
|
||||
let preferredSeedProjects: ProjectWithListingData[] | "error" | undefined;
|
||||
let localProjects:
|
||||
| ProjectWithListingData[]
|
||||
| ComponentProps<ErrorMessage>["error"]
|
||||
| undefined;
|
||||
let preferredSeedProjects:
|
||||
| ProjectWithListingData[]
|
||||
| ComponentProps<ErrorMessage>["error"]
|
||||
| undefined;
|
||||
|
||||
async function fetchProjects(baseUrl: BaseUrl, show: "all" | "pinned") {
|
||||
const api = new HttpdClient(baseUrl);
|
||||
@@ -52,14 +61,10 @@
|
||||
return await getProjectsListingData(projects);
|
||||
}
|
||||
|
||||
function handleProjectLoadError(): "error" {
|
||||
return "error";
|
||||
}
|
||||
|
||||
async function loadLocalProjects() {
|
||||
localProjects = undefined;
|
||||
localProjects = await fetchProjects(api.baseUrl, "all").catch(
|
||||
handleProjectLoadError,
|
||||
error => error,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,12 +73,12 @@
|
||||
|
||||
if (!$selectedSeed) return;
|
||||
preferredSeedProjects = await fetchProjects($selectedSeed, "pinned").catch(
|
||||
handleProjectLoadError,
|
||||
error => error,
|
||||
);
|
||||
}
|
||||
|
||||
function isSeeding(projectId: string) {
|
||||
if (localProjects === "error") return false;
|
||||
if (localProjects instanceof Error) return false;
|
||||
return localProjects?.some(p => p.project.id === projectId) ?? false;
|
||||
}
|
||||
|
||||
@@ -82,7 +87,7 @@
|
||||
$: $selectedSeed && void loadPreferredSeedProjects();
|
||||
$: filteredLocalProjects =
|
||||
$localProjectsFilter === "all" ||
|
||||
localProjects === "error" ||
|
||||
localProjects instanceof Error ||
|
||||
localProjects === undefined
|
||||
? localProjects
|
||||
: localProjects.filter(p => isDelegate(nodeId, p.project.delegates));
|
||||
@@ -114,9 +119,6 @@
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
.empty-state .action {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
@@ -140,11 +142,12 @@
|
||||
<div class="global-hide-on-mobile">
|
||||
<HomepageSection
|
||||
loading={$httpdStore.state !== "stopped" && localProjects === undefined}
|
||||
empty={localProjects === "error" ||
|
||||
$httpdStore.state === "stopped" ||
|
||||
!filteredLocalProjects?.length}
|
||||
empty={$httpdStore.state === "stopped" ||
|
||||
(filteredLocalProjects instanceof Array &&
|
||||
!filteredLocalProjects.length) ||
|
||||
localProjects instanceof Error}
|
||||
title="Local projects"
|
||||
subtitle="Projects you’re seeding with your local node">
|
||||
subtitle="Projects you're seeding with your local node">
|
||||
<svelte:fragment slot="actions">
|
||||
<FilterButton disabled={!nodeId} bind:value={$localProjectsFilter} />
|
||||
<NewProjectButton disabled={!nodeId} />
|
||||
@@ -155,12 +158,12 @@
|
||||
<div style="text-align: left; width: 100%;">
|
||||
<ConnectInstructions />
|
||||
</div>
|
||||
{:else if localProjects === "error"}
|
||||
<div class="heading">Error loading projects</div>
|
||||
<div class="label">
|
||||
There was an error loading projects from your local node.
|
||||
</div>
|
||||
<div class="action"><Button>Learn more</Button></div>
|
||||
{:else if localProjects instanceof Error}
|
||||
<ErrorMessage
|
||||
{...handleError(
|
||||
localProjects,
|
||||
baseUrlToUrl(api.baseUrl).toString(),
|
||||
)} />
|
||||
{:else if !localProjects?.length}
|
||||
<div class="heading">No local projects</div>
|
||||
<div class="label">
|
||||
@@ -175,7 +178,7 @@
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="project-grid">
|
||||
{#if filteredLocalProjects && filteredLocalProjects !== "error"}
|
||||
{#if filteredLocalProjects && !(filteredLocalProjects instanceof Error)}
|
||||
{#each filteredLocalProjects as { project, baseUrl, activity, lastCommit }}
|
||||
<ProjectCard
|
||||
id={project.id}
|
||||
@@ -197,7 +200,7 @@
|
||||
|
||||
<HomepageSection
|
||||
loading={preferredSeedProjects === undefined}
|
||||
empty={preferredSeedProjects === "error" ||
|
||||
empty={preferredSeedProjects instanceof Error ||
|
||||
preferredSeedProjects?.length === 0}
|
||||
title="Explore"
|
||||
subtitle="Pinned projects on your selected seed node">
|
||||
@@ -212,11 +215,12 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="empty">
|
||||
<div class="empty-state">
|
||||
{#if preferredSeedProjects === "error"}
|
||||
<div class="heading">Something went wrong</div>
|
||||
<div class="label">
|
||||
There was an error loading projects from your preferred seed node.
|
||||
</div>
|
||||
{#if preferredSeedProjects instanceof Error}
|
||||
<ErrorMessage
|
||||
{...handleError(
|
||||
preferredSeedProjects,
|
||||
baseUrlToUrl(api.baseUrl).toString(),
|
||||
)} />
|
||||
{:else}
|
||||
<div class="heading">Nothing to see here</div>
|
||||
<div class="label">
|
||||
@@ -226,7 +230,7 @@
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="project-grid">
|
||||
{#if preferredSeedProjects && preferredSeedProjects !== "error"}
|
||||
{#if preferredSeedProjects && !(preferredSeedProjects instanceof Error)}
|
||||
{#each preferredSeedProjects as { project, baseUrl, activity, lastCommit }}
|
||||
<ProjectCard
|
||||
id={project.id}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
|
||||
|
||||
export function handleError(
|
||||
error: Error | ResponseParseError | ResponseError,
|
||||
url: string,
|
||||
): {
|
||||
error: Error | ResponseParseError | ResponseError;
|
||||
title: string;
|
||||
description: string;
|
||||
} {
|
||||
if (error instanceof ResponseParseError) {
|
||||
return {
|
||||
error,
|
||||
title: "Could not parse the request",
|
||||
description:
|
||||
"The response received from the seed does not match the expected schema, this is usually due to a version mismatch between the seed and the web interface.",
|
||||
};
|
||||
} else if (error instanceof ResponseError) {
|
||||
return {
|
||||
error,
|
||||
title: "Could not load the projects",
|
||||
description: `You're trying to fetch projects from a node that is not reachable, make sure the address <a href="${url}">${url}</a> is correct and the right ports are exposed if its your node.`,
|
||||
};
|
||||
} else if (
|
||||
error instanceof TypeError &&
|
||||
error.message === "Failed to fetch"
|
||||
) {
|
||||
return {
|
||||
error,
|
||||
title: "Resource not found",
|
||||
description: `You're trying to fetch a resource that is not available. Check that the ids is correct, and try to run <code>$ rad sync</code>.`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error,
|
||||
title: "Could not load this view",
|
||||
description:
|
||||
"You stumbled on an unknown error, we aren't exactly sure what happened.",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { LoadErrorRoute } from "@app/lib/router/definitions";
|
||||
import type { ErrorRoute } from "@app/lib/router/definitions";
|
||||
|
||||
import * as seeds from "@app/lib/seeds";
|
||||
|
||||
export interface HomeRoute {
|
||||
resource: "home";
|
||||
}
|
||||
@@ -9,9 +11,7 @@ export interface HomeLoadedRoute {
|
||||
params: Record<string, never>;
|
||||
}
|
||||
|
||||
export async function loadHomeRoute(): Promise<
|
||||
HomeLoadedRoute | LoadErrorRoute
|
||||
> {
|
||||
export async function loadHomeRoute(): Promise<HomeLoadedRoute | ErrorRoute> {
|
||||
seeds.initialize();
|
||||
await seeds.waitForLoad();
|
||||
|
||||
|
||||
@@ -178,7 +178,8 @@
|
||||
|
||||
{#if error}
|
||||
<ErrorMessage
|
||||
message="Not able to load more projects from this node"
|
||||
title="Not able to load more projects from this node"
|
||||
description="You either loaded all remaining projects, or there was a network issue with this seed"
|
||||
{error} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
|
||||
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
|
||||
|
||||
export function handleError(
|
||||
error: Error | ResponseParseError | ResponseError,
|
||||
url: string,
|
||||
): NotFoundRoute | ErrorRoute {
|
||||
if (error instanceof ResponseParseError) {
|
||||
return {
|
||||
resource: "error",
|
||||
params: {
|
||||
error,
|
||||
title: "Could not parse the request",
|
||||
description:
|
||||
"The response received from the seed does not match the expected schema, this is usually due to a version mismatch between the seed and the web interface.",
|
||||
},
|
||||
};
|
||||
} else if (error instanceof ResponseError) {
|
||||
return {
|
||||
resource: "error",
|
||||
params: {
|
||||
error,
|
||||
title: "Could not load this node",
|
||||
description: `You're trying to access a node that is not reachable, make sure the address <a href="${url}">${url}</a> is correct and the right ports are exposed if its your node.`,
|
||||
},
|
||||
};
|
||||
} else if (
|
||||
error instanceof TypeError &&
|
||||
error.message === "Failed to fetch"
|
||||
) {
|
||||
return {
|
||||
resource: "notFound",
|
||||
params: { title: "Node not found" },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
resource: "error",
|
||||
params: {
|
||||
error,
|
||||
title: "Could not load this node",
|
||||
description:
|
||||
"You stumbled on an unknown error, we aren't exactly sure what happened.",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { BaseUrl } from "@httpd-client";
|
||||
import type {
|
||||
LoadErrorRoute,
|
||||
NotFoundRoute,
|
||||
} from "@app/lib/router/definitions";
|
||||
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
|
||||
import type { ProjectWithListingData } from "@app/lib/projects";
|
||||
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { config } from "@app/lib/config";
|
||||
import { baseUrlToUrl } from "@app/lib/utils";
|
||||
import { getProjectsListingData } from "@app/lib/projects";
|
||||
import { handleError } from "@app/views/nodes/error";
|
||||
|
||||
export interface NodesRouteParams {
|
||||
baseUrl: BaseUrl;
|
||||
@@ -70,7 +69,7 @@ export function nodePath(baseUrl: BaseUrl) {
|
||||
|
||||
export async function loadNodeRoute(
|
||||
params: NodesRouteParams,
|
||||
): Promise<NodesLoadedRoute | NotFoundRoute | LoadErrorRoute> {
|
||||
): Promise<NodesLoadedRoute | NotFoundRoute | ErrorRoute> {
|
||||
const api = new HttpdClient(params.baseUrl);
|
||||
try {
|
||||
const projectPageIndex = 0;
|
||||
@@ -91,22 +90,6 @@ export async function loadNodeRoute(
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error.message === "Failed to fetch") {
|
||||
return {
|
||||
resource: "notFound",
|
||||
params: {
|
||||
title: "Node not found",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
resource: "loadError",
|
||||
params: {
|
||||
title: "Not able to load this node",
|
||||
errorMessage: error.message,
|
||||
stackTrace: error.stackTrace,
|
||||
},
|
||||
};
|
||||
}
|
||||
return handleError(error, baseUrlToUrl(api.baseUrl).toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +493,10 @@
|
||||
class="diff-error txt-monospace txt-small"
|
||||
style:border-radius="var(--border-radius-small">
|
||||
<ErrorMessage
|
||||
message="Failed to load diff for this revision"
|
||||
title="Failed to load diff for this revision"
|
||||
description="Make sure you are able to connect to the seed <code>${utils
|
||||
.baseUrlToUrl(api.baseUrl)
|
||||
.toString()}</code>"
|
||||
{error} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
} from "@httpd-client";
|
||||
import type { Route } from "@app/lib/router";
|
||||
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { groupCommits } from "@app/lib/commit";
|
||||
import { COMMITS_PER_PAGE } from "./router";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { baseUrlToUrl } from "@app/lib/utils";
|
||||
import { groupCommits } from "@app/lib/commit";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import CommitTeaser from "./Commit/CommitTeaser.svelte";
|
||||
@@ -157,7 +158,12 @@
|
||||
|
||||
{#if error}
|
||||
<div class="message">
|
||||
<ErrorMessage message="Couldn't load commits" {error} />
|
||||
<ErrorMessage
|
||||
title="Couldn't load commits"
|
||||
description="Make sure you are able to connect to the seed <code>${baseUrlToUrl(
|
||||
api.baseUrl,
|
||||
).toString()}</code>"
|
||||
{error} />
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<ErrorMessage
|
||||
message="Couldn't access issue creation. Make sure you're authenticated." />
|
||||
title="Not able to create a new issue"
|
||||
description="Couldn't access issue creation. Make sure you're authenticated." />
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { ISSUES_PER_PAGE } from "./router";
|
||||
import { closeFocused } from "@app/components/Popover.svelte";
|
||||
import { httpdStore } from "@app/lib/httpd";
|
||||
import { isLocal } from "@app/lib/utils";
|
||||
import { baseUrlToUrl, isLocal } from "@app/lib/utils";
|
||||
import capitalize from "lodash/capitalize";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
@@ -174,7 +174,12 @@
|
||||
</List>
|
||||
|
||||
{#if error}
|
||||
<ErrorMessage message="Couldn't load issues" {error} />
|
||||
<ErrorMessage
|
||||
title="Couldn't load issues"
|
||||
description="Please make sure you are able to connect to the seed <code>${baseUrlToUrl(
|
||||
api.baseUrl,
|
||||
).toString()}</code>"
|
||||
{error} />
|
||||
{/if}
|
||||
|
||||
{#if project.issues[state] === 0}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { PATCHES_PER_PAGE } from "./router";
|
||||
import { httpdStore } from "@app/lib/httpd";
|
||||
import { isLocal } from "@app/lib/utils";
|
||||
import { baseUrlToUrl, isLocal } from "@app/lib/utils";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import DropdownList from "@app/components/DropdownList.svelte";
|
||||
@@ -193,7 +193,12 @@
|
||||
</List>
|
||||
|
||||
{#if error}
|
||||
<ErrorMessage message="Couldn't load patches" {error} />
|
||||
<ErrorMessage
|
||||
title="Couldn't load patches"
|
||||
description="Please make sure you are able to connect to the seed <code>${baseUrlToUrl(
|
||||
api.baseUrl,
|
||||
).toString()}</code>"
|
||||
{error} />
|
||||
{/if}
|
||||
|
||||
{#if project.patches[state] === 0}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
|
||||
import type { ProjectRoute } from "@app/views/projects/router";
|
||||
|
||||
import { baseUrlToUrl } from "@app/lib/utils";
|
||||
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
|
||||
|
||||
export function handleError(
|
||||
error: Error | ResponseParseError | ResponseError,
|
||||
route: ProjectRoute,
|
||||
): NotFoundRoute | ErrorRoute {
|
||||
const url = baseUrlToUrl(route.node);
|
||||
if (error instanceof ResponseError && error.status === 404) {
|
||||
let subject;
|
||||
|
||||
if (route.resource === "project.commit") {
|
||||
subject = "Commit";
|
||||
} else if (route.resource === "project.issue") {
|
||||
subject = "Issue";
|
||||
} else if (route.resource === "project.patch") {
|
||||
subject = "Patch";
|
||||
} else {
|
||||
subject = "Project";
|
||||
}
|
||||
|
||||
return {
|
||||
resource: "notFound",
|
||||
params: { title: `${subject} not found` },
|
||||
};
|
||||
} else if (error instanceof ResponseError) {
|
||||
return {
|
||||
resource: "error",
|
||||
params: {
|
||||
error,
|
||||
title: "Could not load this project",
|
||||
description: `Make sure you are able to connect to the seed <a href="${url}">${url}</a>.`,
|
||||
},
|
||||
};
|
||||
} else if (error instanceof ResponseParseError) {
|
||||
return {
|
||||
resource: "error",
|
||||
params: {
|
||||
error,
|
||||
title: "Could not parse the request",
|
||||
description:
|
||||
"The response received from the seed does not match the expected schema, this is usually due to a version mismatch between the seed and the web interface.",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
resource: "error",
|
||||
params: {
|
||||
error,
|
||||
title: "Could not load this project",
|
||||
description:
|
||||
"You stumbled on an unknown error, we aren't exactly sure what happened.",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
LoadErrorRoute,
|
||||
NotFoundRoute,
|
||||
} from "@app/lib/router/definitions";
|
||||
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
|
||||
import type {
|
||||
BaseUrl,
|
||||
Blob,
|
||||
@@ -21,9 +18,10 @@ import type {
|
||||
|
||||
import * as Syntax from "@app/lib/syntax";
|
||||
import * as httpd from "@app/lib/httpd";
|
||||
import { config } from "@app/lib/config";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { ResponseError } from "@httpd-client/lib/fetcher";
|
||||
import { config } from "@app/lib/config";
|
||||
import { handleError } from "@app/views/projects/error";
|
||||
import { nodePath } from "@app/views/nodes/router";
|
||||
import { unreachable } from "@app/lib/utils";
|
||||
|
||||
@@ -271,7 +269,7 @@ async function isLocalNodeSeeding(route: ProjectRoute): Promise<boolean> {
|
||||
|
||||
export async function loadProjectRoute(
|
||||
route: ProjectRoute,
|
||||
): Promise<ProjectLoadedRoute | LoadErrorRoute | NotFoundRoute> {
|
||||
): Promise<ProjectLoadedRoute | ErrorRoute | NotFoundRoute> {
|
||||
const api = new HttpdClient(route.node);
|
||||
const rawPath = (commit?: string) =>
|
||||
`${route.node.scheme}://${route.node.hostname}:${route.node.port}/raw/${
|
||||
@@ -341,35 +339,7 @@ export async function loadProjectRoute(
|
||||
return unreachable(route);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.status === 404) {
|
||||
let subject;
|
||||
|
||||
if (route.resource === "project.commit") {
|
||||
subject = "Commit";
|
||||
} else if (route.resource === "project.issue") {
|
||||
subject = "Issue";
|
||||
} else if (route.resource === "project.patch") {
|
||||
subject = "Patch";
|
||||
} else {
|
||||
subject = "Project";
|
||||
}
|
||||
|
||||
return {
|
||||
resource: "notFound",
|
||||
params: {
|
||||
title: `${subject} not found`,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
resource: "loadError",
|
||||
params: {
|
||||
title: "Could not load this project",
|
||||
errorMessage: error.message,
|
||||
stackTrace: error.stack,
|
||||
},
|
||||
};
|
||||
}
|
||||
return handleError(error, route);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,3 +40,26 @@ test("load error", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "networkidle" });
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response parse error", async ({ page }) => {
|
||||
await page.addInitScript(appConfigWithFixture);
|
||||
await page.route("*/**/v1/projects*", route => {
|
||||
return route.fulfill({
|
||||
json: [{ name: 1337 }],
|
||||
});
|
||||
});
|
||||
await page.goto("/", { waitUntil: "networkidle" });
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response error", async ({ page }) => {
|
||||
await page.addInitScript(appConfigWithFixture);
|
||||
await page.route("*/**/v1/projects*", route => {
|
||||
return route.fulfill({
|
||||
status: 500,
|
||||
body: "There is an error in the response",
|
||||
});
|
||||
});
|
||||
await page.goto("/", { waitUntil: "networkidle" });
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -37,3 +37,29 @@ test("node not found", async ({ page }) => {
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response parse error", async ({ page }) => {
|
||||
await page.route("*/**/v1/projects*", route => {
|
||||
return route.fulfill({
|
||||
json: [{ name: 1337 }],
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/nodes/radicle.local", {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response error", async ({ page }) => {
|
||||
await page.route("*/**/v1/projects*", route => {
|
||||
return route.fulfill({
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/nodes/radicle.local", {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
sourceBrowsingUrl,
|
||||
aliceRemote,
|
||||
markdownUrl,
|
||||
sourceBrowsingRid,
|
||||
} from "@tests/support/fixtures.js";
|
||||
|
||||
test("source page", async ({ page }) => {
|
||||
@@ -99,6 +100,26 @@ test("project not found", async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response parse error", async ({ page }) => {
|
||||
await page.route(`*/**/v1/projects/${sourceBrowsingRid}`, route => {
|
||||
return route.fulfill({
|
||||
json: [{ name: 1337 }],
|
||||
});
|
||||
});
|
||||
await page.goto(sourceBrowsingUrl, { waitUntil: "networkidle" });
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response error", async ({ page }) => {
|
||||
await page.route(`*/**/v1/projects/${sourceBrowsingRid}`, route => {
|
||||
return route.fulfill({
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
await page.goto(sourceBrowsingUrl, { waitUntil: "networkidle" });
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("readme not found", async ({ page }) => {
|
||||
await page.goto(`${markdownUrl}/tree`, {
|
||||
waitUntil: "networkidle",
|
||||
|
||||
Reference in New Issue
Block a user