Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Refactor seed view
Rūdolfs Ošiņš committed 3 years ago
commit 970431824802da293005c6702fc89a1dc192b67a
parent 26419a74e26ec8a538b7c6ba54957ba252c86249
8 files changed +201 -213
modified src/components/Clipboard.svelte
@@ -13,6 +13,7 @@

  export let text: string;
  export let small = false;
+
  export let tooltip: string | undefined = undefined;
</script>

<style>
@@ -40,7 +41,11 @@
</style>

<!-- svelte-ignore a11y-click-events-have-key-events -->
-
<span class="clipboard" class:small on:click|stopPropagation={copy}>
+
<span
+
  title={tooltip}
+
  class="clipboard"
+
  class:small
+
  on:click|stopPropagation={copy}>
  {#if small}
    <Icon name="clipboard-small" />
  {:else}
added src/components/ErrorMessage.svelte
@@ -0,0 +1,32 @@
+
<script lang="ts">
+
  import Clipboard from "@app/components/Clipboard.svelte";
+

+
  export let message: string;
+
  export let stackTrace: string | undefined = undefined;
+
</script>
+

+
<style>
+
  .error {
+
    padding: 1rem;
+
    color: var(--color-negative);
+
    border-radius: var(--border-radius);
+
    background-color: var(--color-negative-3);
+
    display: flex;
+
    align-items: center;
+
  }
+
  .stack-trace {
+
    display: flex;
+
    align-self: flex-end;
+
  }
+
</style>
+

+
<div class="error">
+
  {message}
+
  {#if stackTrace}
+
    <div class="stack-trace">
+
      <Clipboard
+
        tooltip="Copy error to clipboard"
+
        text={JSON.stringify({ errorMessage: message, stackTrace }, null, 2)} />
+
    </div>
+
  {/if}
+
</div>
deleted src/components/Message.svelte
@@ -1,18 +0,0 @@
-
<script lang="ts">
-
  export let error = false;
-
</script>
-

-
<style>
-
  .message {
-
    padding: 1rem;
-
  }
-
  .message-error {
-
    color: var(--color-negative);
-
    border-radius: var(--border-radius);
-
    background-color: var(--color-negative-3);
-
  }
-
</style>
-

-
<div class="message" class:message-error={error}>
-
  <slot />
-
</div>
modified src/views/home/Index.svelte
@@ -8,7 +8,7 @@
  import { twemoji } from "@app/lib/utils";

  import Loading from "@app/components/Loading.svelte";
-
  import Message from "@app/components/Message.svelte";
+
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import ProjectCard from "@app/components/ProjectCard.svelte";

  function goToProject(project: Project, baseUrl: BaseUrl) {
@@ -110,12 +110,9 @@
        {/each}
      </div>
    {/if}
-
  {:catch}
+
  {:catch error}
    <div class="padding">
-
      <Message error>
-
        <span class="txt-bold">Error:</span>
-
        failed to load projects.
-
      </Message>
+
      <ErrorMessage message="Couldn't load projects." stackTrace={error} />
    </div>
  {/await}
</main>
modified src/views/projects/View.svelte
@@ -11,7 +11,7 @@
  import { sessionStore } from "@app/lib/session";

  import Loading from "@app/components/Loading.svelte";
-
  import Message from "@app/components/Message.svelte";
+
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import NotFound from "@app/components/NotFound.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";

@@ -207,7 +207,7 @@
            history={history.commits.map(c => c.commit)} />
        {:catch e}
          <div class="message">
-
            <Message error>{e.message}</Message>
+
            <ErrorMessage message="Couldn't load commits." stackTrace={e} />
          </div>
        {/await}
      {:else if activeRoute.params.view.resource === "commits"}
@@ -217,7 +217,7 @@
          <Commit {commit} />
        {:catch e}
          <div class="message">
-
            <Message error>{e.message}</Message>
+
            <ErrorMessage message="Couln't load commit." stackTrace={e} />
          </div>
        {/await}
      {:else if activeRoute.params.view.resource === "issues" && activeRoute.params.view.params?.view.resource === "new"}
@@ -230,10 +230,8 @@
            {baseUrl} />
        {:else}
          <div class="message">
-
            <Message error>
-
              Could not access the issue creation. Make sure you're still logged
-
              in.
-
            </Message>
+
            <ErrorMessage
+
              message="Couldn't access issue creation. Make sure you're still logged in." />
          </div>
        {/if}
      {:else if activeRoute.params.view.resource === "issues"}
@@ -247,7 +245,7 @@
            {issues} />
        {:catch e}
          <div class="message">
-
            <Message error>{e.message}</Message>
+
            <ErrorMessage message="Couldn't load issues." stackTrace={e} />
          </div>
        {/await}
      {:else if activeRoute.params.view.resource === "issue"}
@@ -262,7 +260,7 @@
            {issue} />
        {:catch e}
          <div class="message">
-
            <Message error>{e.message}</Message>
+
            <ErrorMessage message="Couldn't load issue." stackTrace={e} />
          </div>
        {/await}
      {:else if activeRoute.params.view.resource === "patches"}
@@ -277,7 +275,7 @@
            projectPatches={project.patches} />
        {:catch e}
          <div class="message">
-
            <Message error>{e.message}</Message>
+
            <ErrorMessage message="Couldn't load patches." stackTrace={e} />
          </div>
        {/await}
      {:else if activeRoute.params.view.resource === "patch"}
@@ -293,7 +291,7 @@
            {patch} />
        {:catch e}
          <div class="message">
-
            <Message error>{e.message}</Message>
+
            <ErrorMessage message="Couldn't load patch." stackTrace={e} />
          </div>
        {/await}
      {:else}
modified src/views/seeds/View.svelte
@@ -1,14 +1,18 @@
<script lang="ts">
  import type { Project, NodeStats } from "@httpd-client";
+
  import type { WeeklyActivity } from "@app/lib/commit";

-
  import { config } from "@app/lib/config";
+
  import * as router from "@app/lib/router";
  import { HttpdClient } from "@httpd-client";
+
  import { config } from "@app/lib/config";
  import { extractBaseUrl, isLocal, truncateId } from "@app/lib/utils";
+
  import { loadProjectActivity } from "@app/lib/commit";

+
  import Button from "@app/components/Button.svelte";
  import Clipboard from "@app/components/Clipboard.svelte";
+
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import Loading from "@app/components/Loading.svelte";
-
  import NotFound from "@app/components/NotFound.svelte";
-
  import Projects from "@app/views/seeds/View/Projects.svelte";
+
  import ProjectCard from "@app/components/ProjectCard.svelte";

  export let hostnamePort: string;

@@ -18,22 +22,74 @@
    : baseUrl.hostname;
  const api = new HttpdClient(baseUrl);

-
  const getProjectsAndStats = async (): Promise<{
-
    stats: NodeStats;
-
    projects: Project[];
-
  }> => {
-
    const stats = await api.getStats();
-
    const projects = await api.project.getAll({ page: 0, perPage: 10 });
-
    return { stats, projects };
-
  };
+
  const perPage = 10;
+
  let page = 0;
+
  let error: any;
+
  let loadingProjects = false;
+

+
  let projects: Project[] = [];
+
  let nodeStats: NodeStats | undefined = undefined;
+
  let projectsWithActivity: { project: Project; activity: WeeklyActivity[] }[] =
+
    [];
+

+
  async function loadProjects(): Promise<void> {
+
    loadingProjects = true;
+
    try {
+
      [nodeStats, projects] = await Promise.all([
+
        api.getStats(),
+
        api.project.getAll({ page, perPage }),
+
      ]);
+

+
      const results = await Promise.all(
+
        projects.map(async project => {
+
          const activity = await loadProjectActivity(project.id, baseUrl);
+
          return {
+
            project,
+
            activity,
+
          };
+
        }),
+
      );
+
      projectsWithActivity = [...projectsWithActivity, ...results];
+
      page += 1;
+
    } catch (e) {
+
      error = e;
+
    } finally {
+
      loadingProjects = false;
+
    }
+
  }
+

+
  function goToProject(project: Project) {
+
    router.push({
+
      resource: "projects",
+
      params: {
+
        view: { resource: "tree" },
+
        id: project.id,
+
        hostnamePort:
+
          baseUrl.port === config.seeds.defaultHttpdPort
+
            ? baseUrl.hostname
+
            : `${baseUrl.hostname}:${baseUrl.port}`,
+
        revision: undefined,
+
        hash: undefined,
+
        search: undefined,
+
      },
+
    });
+
  }
+

+
  $: showMoreButton =
+
    !loadingProjects &&
+
    !error &&
+
    nodeStats &&
+
    projectsWithActivity.length < nodeStats.projects.count;
+

+
  loadProjects();
</script>

<style>
-
  main {
-
    padding: 5rem 0;
+
  .wrapper {
    width: 720px;
+
    margin: 5rem 0;
  }
-
  main > header {
+
  .header {
    display: flex;
    width: 100%;
    flex-direction: row;
@@ -41,42 +97,28 @@
    justify-content: space-between;
    margin-bottom: 2rem;
  }
-
  .fields {
-
    display: grid;
-
    grid-template-columns: 5rem 4fr 0fr;
-
    gap: 1rem 2rem;
-
    margin-bottom: 2rem;
+
  table {
+
    border-collapse: collapse;
  }
-
  .fields > div {
-
    place-self: center start;
-
    height: 2rem;
-
    line-height: 2rem;
-
  }
-
  .title {
-
    display: flex;
-
    align-items: center;
+
  td {
+
    padding-bottom: 1.5rem;
+
    padding-right: 3rem;
  }
-
  .seed-wrapper {
+
  .seed-address {
    display: flex;
    align-items: center;
-
    gap: 0.2rem;
-
  }
-
  .seed-address {
-
    display: inline-flex;
-
    font-size: var(--font-size-regular);
-
    line-height: 2rem;
    color: var(--color-foreground-6);
-
    vertical-align: middle;
+
    white-space: nowrap;
+
  }
+
  .more {
+
    margin-top: 2rem;
+
    text-align: center;
  }
-

  @media (max-width: 720px) {
-
    main {
+
    .wrapper {
      width: 100%;
      padding: 1.5rem;
    }
-
    .fields {
-
      grid-template-columns: 5rem auto;
-
    }
  }
</style>

@@ -84,55 +126,73 @@
  <title>{hostName}</title>
</svelte:head>

-
{#await api.getRoot()}
-
  <main class="layout-centered">
+
<div class="wrapper">
+
  <div class="header">
+
    <span class="txt-title txt-bold">
+
      {hostName}
+
    </span>
+
  </div>
+

+
  {#await api.getRoot()}
    <Loading center />
-
  </main>
-
{:then nodeInfo}
-
  <main>
-
    <header>
-
      <span class="title txt-title">
-
        <span class="txt-bold">
-
          {hostName}
-
        </span>
-
      </span>
-
    </header>
-

-
    <div class="fields">
-
      <!-- Seed Address -->
-
      <div class="txt-highlight">Address</div>
-
      <div class="seed-wrapper">
-
        <div class="seed-address">
-
          {truncateId(nodeInfo.node.id)}@{baseUrl.hostname}
-
        </div>
-
        <Clipboard
-
          small
-
          text={`${nodeInfo.node.id}@${baseUrl.hostname}:${config.seeds.defaultNodePort}`} />
-
      </div>
-
      <div class="layout-desktop" />
-
      <!-- API Version -->
-
      <div class="txt-highlight">Version</div>
-
      <div>{nodeInfo.version}</div>
-
      <div class="layout-desktop" />
+
  {:then nodeInfo}
+
    <table>
+
      <tr>
+
        <td class="txt-highlight">Address</td>
+
        <td>
+
          <div class="seed-address">
+
            {truncateId(nodeInfo.node.id)}@{baseUrl.hostname}
+
            <Clipboard
+
              small
+
              text={`${nodeInfo.node.id}@${baseUrl.hostname}:${config.seeds.defaultNodePort}`} />
+
          </div>
+
        </td>
+
      </tr>
+
      <tr>
+
        <td class="txt-highlight">Version</td>
+
        <td>
+
          {nodeInfo.version}
+
        </td>
+
      </tr>
+
    </table>
+
  {:catch error}
+
    <div style:margin-bottom="2rem">
+
      <ErrorMessage
+
        message="Not able to query information from this seed."
+
        stackTrace={error.stack} />
    </div>
-
    <!-- Seed Projects -->
-
    {#await getProjectsAndStats()}
-
      <Loading center />
-
    {:then result}
-
      <Projects {baseUrl} projects={result.projects} stats={result.stats} />
-
    {:catch err}
-
      <div class="error txt-tiny">
-
        <div>
-
          API request to <span class="txt-monospace">{err.url}</span>
-
          failed.
-
        </div>
+
  {/await}
+

+
  <div style:margin-bottom="5rem">
+
    {#if projects}
+
      <div style:margin-top="1rem">
+
        {#each projectsWithActivity as { project, activity }}
+
          <div style:margin-bottom="0.5rem">
+
            <ProjectCard
+
              {activity}
+
              id={project.id}
+
              name={project.name}
+
              description={project.description}
+
              head={project.head}
+
              on:click={() => goToProject(project)} />
+
          </div>
+
        {/each}
      </div>
-
    {/await}
-
  </main>
-
{:catch}
-
  <div class="layout-centered">
-
    <NotFound
-
      title={baseUrl.hostname}
-
      subtitle="Not able to query information from this seed." />
+
      {#if loadingProjects}
+
        <div class="more">
+
          <Loading small />
+
        </div>
+
      {/if}
+
      {#if showMoreButton}
+
        <div class="more">
+
          <Button variant="foreground" on:click={loadProjects}>More</Button>
+
        </div>
+
      {/if}
+
    {/if}
+
    {#if error}
+
      <ErrorMessage
+
        message="Not able to load projects from this seed."
+
        stackTrace={error.stack} />
+
    {/if}
  </div>
-
{/await}
+
</div>
deleted src/views/seeds/View/Projects.svelte
@@ -1,88 +0,0 @@
-
<script lang="ts">
-
  import type { BaseUrl, Project, NodeStats } from "@httpd-client";
-

-
  import * as router from "@app/lib/router";
-
  import { HttpdClient } from "@httpd-client";
-
  import { config } from "@app/lib/config";
-
  import { loadProjectActivity } from "@app/lib/commit";
-

-
  import List from "@app/components/List.svelte";
-
  import ProjectCard from "@app/components/ProjectCard.svelte";
-

-
  export let baseUrl: BaseUrl;
-
  export let projects: Project[];
-
  export let stats: NodeStats;
-

-
  const api = new HttpdClient(baseUrl);
-
  // A pointer to the current page of projects added to the listing
-
  let page = 0;
-

-
  const fetchMoreProjects = async (): Promise<Project[]> => {
-
    try {
-
      stats = await api.getStats();
-
      const projects = await api.project.getAll({
-
        page: (page += 1),
-
        perPage: 10,
-
      });
-

-
      if (projects.length > 0) {
-
        return projects;
-
      }
-
    } catch (e) {
-
      console.error(e);
-
    }
-

-
    // We return an empty array, for when no more projects are found, or an error is thrown
-
    // since List is looking for an iterable.
-
    return [];
-
  };
-

-
  const onClick = (project: Project) => {
-
    router.push({
-
      resource: "projects",
-
      params: {
-
        view: { resource: "tree" },
-
        id: project.id,
-
        hostnamePort:
-
          baseUrl.port === config.seeds.defaultHttpdPort
-
            ? baseUrl.hostname
-
            : `${baseUrl.hostname}:${baseUrl.port}`,
-
        revision: undefined,
-
        hash: undefined,
-
        search: undefined,
-
      },
-
    });
-
  };
-
</script>
-

-
<style>
-
  .projects {
-
    margin-top: 1rem;
-
  }
-
  .projects .project {
-
    margin-bottom: 0.5rem;
-
  }
-
</style>
-

-
<div class="projects">
-
  <List
-
    bind:items={projects}
-
    complete={projects.length === stats.projects.count}
-
    query={fetchMoreProjects}>
-
    <svelte:fragment slot="list" let:items>
-
      {#each items as project}
-
        {#await loadProjectActivity(project.id, baseUrl) then activity}
-
          <div class="project">
-
            <ProjectCard
-
              {activity}
-
              id={project.id}
-
              name={project.name}
-
              description={project.description}
-
              head={project.head}
-
              on:click={() => onClick(project)} />
-
          </div>
-
        {/await}
-
      {/each}
-
    </svelte:fragment>
-
  </List>
-
</div>
modified tests/e2e/seed.spec.ts
@@ -9,7 +9,9 @@ import {
test("seed metadata", async ({ page }) => {
  await page.goto("/seeds/radicle.local");

-
  await expect(page.locator("header").getByText("radicle.local")).toBeVisible();
+
  await expect(
+
    page.locator(".header").getByText("radicle.local"),
+
  ).toBeVisible();
  await expect(page.locator("text=radicle.local")).toBeVisible();
  await expect(
    page.locator(`text=${seedRemote.substring(0, 6)}…${seedRemote.slice(-6)}`),