Fix not found and loading error screens

Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
Rūdolfs Ošiņš
2023-08-16 09:35:44 +02:00
parent 0e33915e9a
commit 5736083d50
15 changed files with 240 additions and 155 deletions
+2 -6
View File
@@ -17,6 +17,7 @@
import Issues from "@app/views/projects/Issues.svelte";
import NewIssue from "@app/views/projects/Issue/New.svelte";
import Nodes from "@app/views/nodes/View.svelte";
import NotFound from "@app/views/NotFound.svelte";
import Patch from "@app/views/projects/Patch.svelte";
import Patches from "@app/views/projects/Patches.svelte";
import Session from "@app/views/session/Index.svelte";
@@ -24,7 +25,6 @@
import LoadError from "@app/components/LoadError.svelte";
import Loading from "@app/components/Loading.svelte";
import NotFound from "@app/components/NotFound.svelte";
const activeRouteStore = router.activeRouteStore;
@@ -95,11 +95,7 @@
{:else if $activeRouteStore.resource === "loadError"}
<LoadError {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "notFound"}
<div class="layout-centered">
<NotFound
title="Page not found"
subtitle={`${$activeRouteStore.params.url.replace("/", "")}`} />
</div>
<NotFound {...$activeRouteStore.params} />
{:else}
{unreachable($activeRouteStore)}
{/if}
+5 -12
View File
@@ -1,10 +1,9 @@
<script lang="ts">
import { twemoji } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
import ErrorMessage from "@app/components/ErrorMessage.svelte";
export let title: string | undefined = undefined;
export let title: string;
export let errorMessage: string;
export let stackTrace: string;
</script>
@@ -22,15 +21,9 @@
<div class="wrapper layout-centered">
<div class="emoji" use:twemoji>🏜️</div>
{#if title}
<div class="title txt-medium txt-bold txt-highlight">
{title}
</div>
{/if}
<ErrorMessage message={errorMessage} {stackTrace} />
<div style:margin-top="1rem">
<Button variant="foreground" on:click={() => window.history.back()}>
Back
</Button>
<div class="title txt-medium txt-bold txt-highlight">
{title}
</div>
<ErrorMessage message={errorMessage} {stackTrace} />
</div>
-42
View File
@@ -1,42 +0,0 @@
<script lang="ts">
import { twemoji } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
export let title: string;
export let subtitle: string;
</script>
<style>
.container {
display: flex;
align-items: center;
flex-direction: column;
gap: 1rem;
}
.emoji {
display: flex;
font-size: var(--font-size-xx-large);
}
.title {
color: var(--color-secondary);
}
.actions {
margin-top: 1rem;
}
</style>
<div class="container">
<div class="emoji" use:twemoji>🏜️</div>
<div class="title txt-medium txt-bold">{title}</div>
<div>{subtitle}</div>
<div class="actions">
<Button variant="foreground" on:click={() => window.history.back()}>
Back
</Button>
</div>
</div>
+6 -6
View File
@@ -76,7 +76,7 @@ export async function navigateToUrl(
} else {
await navigate(action, {
resource: "notFound",
params: { url: relativeUrl },
params: { title: "Page not found" },
});
}
}
@@ -241,8 +241,6 @@ export function routeToPath(route: Route): string {
return `/session?id=${route.params.id}&sig=${route.params.signature}&pk=${route.params.publicKey}`;
} else if (route.resource === "nodes") {
return nodePath(route.params.baseUrl);
} else if (route.resource === "loadError") {
return "";
} else if (
route.resource === "project.source" ||
route.resource === "project.history" ||
@@ -254,10 +252,12 @@ export function routeToPath(route: Route): string {
route.resource === "project.patch"
) {
return projectRouteToPath(route);
} else if (route.resource === "booting") {
} else if (
route.resource === "booting" ||
route.resource === "notFound" ||
route.resource === "loadError"
) {
return "";
} else if (route.resource === "notFound") {
return route.params.url;
} else {
return utils.unreachable(route);
}
+6 -6
View File
@@ -13,9 +13,9 @@ interface BootingRoute {
resource: "booting";
}
interface NotFoundRoute {
export interface NotFoundRoute {
resource: "notFound";
params: { url: string };
params: { title: string };
}
interface SessionRoute {
@@ -23,10 +23,10 @@ interface SessionRoute {
params: { id: string; signature: string; publicKey: string };
}
export interface LoadError {
export interface LoadErrorRoute {
resource: "loadError";
params: {
title?: string;
title: string;
errorMessage: string;
stackTrace: string;
};
@@ -35,7 +35,7 @@ export interface LoadError {
export type Route =
| BootingRoute
| HomeRoute
| LoadError
| LoadErrorRoute
| NotFoundRoute
| ProjectRoute
| NodesRoute
@@ -44,7 +44,7 @@ export type Route =
export type LoadedRoute =
| BootingRoute
| HomeLoadedRoute
| LoadError
| LoadErrorRoute
| NotFoundRoute
| ProjectLoadedRoute
| NodesLoadedRoute
+30
View File
@@ -0,0 +1,30 @@
<script lang="ts">
import { twemoji } from "@app/lib/utils";
export let title: string;
</script>
<style>
.container {
display: flex;
align-items: center;
flex-direction: column;
gap: 1rem;
}
.emoji {
display: flex;
font-size: var(--font-size-xx-large);
}
.title {
color: var(--color-secondary);
}
</style>
<div class="layout-centered">
<div class="container">
<div class="emoji" use:twemoji>🏜️</div>
<div class="title txt-medium txt-bold">{title}</div>
</div>
</div>
+18 -26
View File
@@ -1,4 +1,4 @@
import type { LoadError } from "@app/lib/router/definitions";
import type { LoadErrorRoute } from "@app/lib/router/definitions";
import type { ProjectBaseUrl } from "@app/lib/search";
import type { WeeklyActivity } from "@app/lib/commit";
@@ -19,30 +19,22 @@ export interface HomeLoadedRoute {
params: { projects: ProjectBaseUrlActivity[] };
}
export async function loadHomeRoute(): Promise<HomeLoadedRoute | LoadError> {
try {
const projects = await getProjectsFromNodes(config.projects.pinned);
const results = await Promise.all(
projects.map(async projectNode => {
const activity = await loadProjectActivity(
projectNode.project.id,
projectNode.baseUrl,
);
return {
...projectNode,
activity,
};
}),
);
export async function loadHomeRoute(): Promise<
HomeLoadedRoute | LoadErrorRoute
> {
const projects = await getProjectsFromNodes(config.projects.pinned);
const results = await Promise.all(
projects.map(async projectNode => {
const activity = await loadProjectActivity(
projectNode.project.id,
projectNode.baseUrl,
);
return {
...projectNode,
activity,
};
}),
);
return { resource: "home", params: { projects: results } };
} catch (error: any) {
return {
resource: "loadError",
params: {
errorMessage: "Could not load pinned projects.",
stackTrace: error.stack,
},
};
}
return { resource: "home", params: { projects: results } };
}
+4 -6
View File
@@ -1,5 +1,5 @@
import type { BaseUrl, Project } from "@httpd-client";
import type { LoadError } from "@app/lib/router/definitions";
import type { NotFoundRoute } from "@app/lib/router/definitions";
import type { WeeklyActivity } from "@app/lib/commit";
import { HttpdClient } from "@httpd-client";
@@ -81,7 +81,7 @@ export function nodePath(baseUrl: BaseUrl) {
export async function loadNodeRoute(
params: NodesRouteParams,
): Promise<NodesLoadedRoute | LoadError> {
): Promise<NodesLoadedRoute | NotFoundRoute> {
const api = new HttpdClient(params.baseUrl);
try {
const projectPageIndex = 0;
@@ -102,11 +102,9 @@ export async function loadNodeRoute(
};
} catch (error: any) {
return {
resource: "loadError",
resource: "notFound",
params: {
title: `${params.baseUrl.hostname}:${params.baseUrl.port}`,
errorMessage: "Not able to query this node.",
stackTrace: error.stack,
title: "Node not found",
},
};
}
+51 -38
View File
@@ -1,4 +1,7 @@
import type { LoadError } from "@app/lib/router/definitions";
import type {
LoadErrorRoute,
NotFoundRoute,
} from "@app/lib/router/definitions";
import type {
BaseUrl,
Blob,
@@ -230,13 +233,13 @@ function parseRevisionToOid(
export async function loadProjectRoute(
route: ProjectRoute,
): Promise<ProjectLoadedRoute | LoadError> {
): Promise<ProjectLoadedRoute | LoadErrorRoute | NotFoundRoute> {
const api = new HttpdClient(route.node);
try {
if (route.resource === "project.source") {
return loadTreeView(route);
return await loadTreeView(route);
} else if (route.resource === "project.history") {
return loadHistoryView(route);
return await loadHistoryView(route);
} else if (route.resource === "project.commit") {
const [project, commit] = await Promise.all([
api.project.getById(route.project),
@@ -252,33 +255,22 @@ export async function loadProjectRoute(
},
};
} else if (route.resource === "project.issue") {
try {
const [project, issue] = await Promise.all([
api.project.getById(route.project),
api.project.getIssueById(route.project, route.issue),
]);
return {
resource: "project.issue",
params: {
baseUrl: route.node,
project,
issue,
},
};
} catch (error: any) {
return {
resource: "loadError",
params: {
title: route.issue,
errorMessage: "Not able to load this issue.",
stackTrace: error.stack,
},
};
}
const [project, issue] = await Promise.all([
api.project.getById(route.project),
api.project.getIssueById(route.project, route.issue),
]);
return {
resource: "project.issue",
params: {
baseUrl: route.node,
project,
issue,
},
};
} else if (route.resource === "project.patch") {
return loadPatchView(route);
return await loadPatchView(route);
} else if (route.resource === "project.issues") {
return loadIssuesView(route);
return await loadIssuesView(route);
} else if (route.resource === "project.newIssue") {
const project = await api.project.getById(route.project);
return {
@@ -289,19 +281,40 @@ export async function loadProjectRoute(
},
};
} else if (route.resource === "project.patches") {
return loadPatchesView(route);
return await loadPatchesView(route);
} else {
return unreachable(route);
}
} catch (error: any) {
return {
resource: "loadError",
params: {
title: route.project,
errorMessage: "Not able to load this project.",
stackTrace: error.stack,
},
};
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,
},
};
}
}
}
+20 -2
View File
@@ -245,6 +245,24 @@ export function appConfigWithFixture() {
},
projects: {
pinned: [
{
name: "cobs",
id: "rad:z3fpY7nttPPa6MBnAv2DccHzQJnqe",
baseUrl: {
hostname: "127.0.0.1",
port: 8081,
scheme: "http",
},
},
{
name: "markdown",
id: "rad:z2tchH2Ti4LxRKdssPQYs6VHE5rsg",
baseUrl: {
hostname: "127.0.0.1",
port: 8081,
scheme: "http",
},
},
{
name: "source-browsing",
id: "rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir",
@@ -349,11 +367,11 @@ export async function createSourceBrowsingFixture(
await bob.git(["push", "rad"], { cwd: bobProjectPath });
await bob.waitForEvent(
{ type: "refs-synced", remote: palm.nodeId, rid },
{ type: "refsSynced", remote: palm.nodeId, rid },
2000,
);
await bob.waitForEvent(
{ type: "refs-synced", remote: alice.nodeId, rid },
{ type: "refsSynced", remote: alice.nodeId, rid },
2000,
);
await alice.stopNode();
+13 -8
View File
@@ -24,29 +24,34 @@ export type RefsUpdate =
export type NodeEvent =
| {
type: "refs-fetched";
type: "refsFetched";
remote: string;
rid: string;
updated: RefsUpdate[];
}
| {
type: "refs-synced";
type: "refsSynced";
remote: string;
rid: string;
}
| {
type: "seed-discovered";
type: "seedDiscovered";
rid: string;
nid: string;
}
| {
type: "seed-dropped";
type: "seedDropped";
nid: string;
rid: string;
}
| {
type: "peer-connected";
type: "peerConnected";
nid: string;
}
| {
type: "peerDisconnected";
nid: string;
reason: string;
};
export interface RoutingEntry {
@@ -340,7 +345,7 @@ export class RadiclePeer {
});
await this.waitForEvent(
{ type: "seed-discovered", rid, nid: this.nodeId },
{ type: "seedDiscovered", rid, nid: this.nodeId },
6000,
);
}
@@ -356,11 +361,11 @@ export class RadiclePeer {
);
await this.waitForEvent(
{ type: "peer-connected", nid: remote.nodeId },
{ type: "peerConnected", nid: remote.nodeId },
1000,
);
await remote.waitForEvent(
{ type: "peer-connected", nid: this.nodeId },
{ type: "peerConnected", nid: this.nodeId },
1000,
);
}
+22 -1
View File
@@ -4,7 +4,7 @@ test.use({
customAppConfig: true,
});
test("landing page", async ({ page }) => {
test("pinned projects", async ({ page }) => {
await page.addInitScript(() => {
window.initializeTestStubs = () => {
window.e2eTestStubs.FakeTimers.install({
@@ -19,3 +19,24 @@ test("landing page", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
test("load error", async ({ page }) => {
await page.addInitScript(() => {
window.initializeTestStubs = () => {
window.e2eTestStubs.FakeTimers.install({
now: new Date("November 24 2022 12:00:00").valueOf(),
shouldClearNativeTimers: true,
shouldAdvanceTime: false,
});
};
});
await page.route(
"**/api/v1/projects/rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir",
route => route.fulfill({ status: 500 }),
);
await page.addInitScript(appConfigWithFixture);
await page.goto("/", { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
@@ -14,3 +14,10 @@ test("node page", async ({ page }) => {
await page.goto("/nodes/radicle.local", { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
test("node not found", async ({ page }) => {
await page.goto("/nodes/this.node.does.not.exist.xyz", {
waitUntil: "networkidle",
});
await expect(page).toHaveScreenshot();
});
+8
View File
@@ -0,0 +1,8 @@
import { expect, test } from "@tests/support/fixtures.js";
test("page not found", async ({ page }) => {
await page.goto("/this/page/does/not/exist", {
waitUntil: "networkidle",
});
await expect(page).toHaveScreenshot();
});
+48 -2
View File
@@ -6,12 +6,12 @@ import {
aliceRemote,
} from "@tests/support/fixtures.js";
test("source tree page", async ({ page }) => {
test("source page", async ({ page }) => {
await page.goto(sourceBrowsingUrl, { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
test("commits page", async ({ page }) => {
test("history page", async ({ page }) => {
await page.addInitScript(() => {
window.initializeTestStubs = () => {
window.e2eTestStubs.FakeTimers.install({
@@ -68,3 +68,49 @@ test("diff selection", async ({ page }) => {
);
await expect(page).toHaveScreenshot({ fullPage: true });
});
test("project load error", async ({ page }) => {
await page.goto(
`${sourceBrowsingUrl}/remotes/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz`,
{ waitUntil: "networkidle" },
);
await expect(page).toHaveScreenshot();
});
test("project not found", async ({ page }) => {
await page.goto(`/nodes/127.0.0.1/rad:z4Vzzzzzzzzzzzzzzzzzzzzzzzzzz`, {
waitUntil: "networkidle",
});
await expect(page).toHaveScreenshot();
});
test("file not found", async ({ page }) => {
await page.goto(`${sourceBrowsingUrl}/tree/this.file.does.not.exist`, {
waitUntil: "networkidle",
});
await expect(page).toHaveScreenshot();
});
test("commit not found", async ({ page }) => {
await page.goto(
`${sourceBrowsingUrl}/commits/0000000000000000000000000000000000000000`,
{ waitUntil: "networkidle" },
);
await expect(page).toHaveScreenshot();
});
test("issue not found", async ({ page }) => {
await page.goto(
`${sourceBrowsingUrl}/issues/0000000000000000000000000000000000000000`,
{ waitUntil: "networkidle" },
);
await expect(page).toHaveScreenshot();
});
test("patch not found", async ({ page }) => {
await page.goto(
`${sourceBrowsingUrl}/patches/0000000000000000000000000000000000000000`,
{ waitUntil: "networkidle" },
);
await expect(page).toHaveScreenshot();
});