Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Fix not found and loading error screens
Rūdolfs Ošiņš committed 2 years ago
commit 5736083d50be18bb9216db4bf83d6742ff44aaae
parent 0e33915e9a280cc492feb418ddc739a83b637813
16 files changed +256 -171
modified src/App.svelte
@@ -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}
modified src/components/LoadError.svelte
@@ -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>
deleted src/components/NotFound.svelte
@@ -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>
modified src/lib/router.ts
@@ -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);
  }
modified src/lib/router/definitions.ts
@@ -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
added src/views/NotFound.svelte
@@ -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>
modified src/views/home/router.ts
@@ -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 } };
}
modified src/views/nodes/router.ts
@@ -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",
      },
    };
  }
modified src/views/projects/router.ts
@@ -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,
+
        },
+
      };
+
    }
  }
}

modified tests/support/fixtures.ts
@@ -246,6 +246,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",
          baseUrl: {
@@ -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();
modified tests/support/peerManager.ts
@@ -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,
    );
  }
modified tests/visual/landingPage.spec.ts
@@ -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();
+
});
added tests/visual/node.spec.ts
@@ -0,0 +1,23 @@
+
import { test, expect } from "@tests/support/fixtures.js";
+

+
test("node page", 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.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();
+
});
added tests/visual/notFound.spec.ts
@@ -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();
+
});
modified tests/visual/project.spec.ts
@@ -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();
+
});
deleted tests/visual/seed.spec.ts
@@ -1,16 +0,0 @@
-
import { test, expect } from "@tests/support/fixtures.js";
-

-
test("node page", 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.goto("/nodes/radicle.local", { waitUntil: "networkidle" });
-
  await expect(page).toHaveScreenshot();
-
});