Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Support for browsing seed projects
Alexis Sellier committed 4 years ago
commit 05ed8e15a0f65f5949e3832562f43d44f8a314cf
parent 8b18e8006849e36872cb928d87179e4e222fb689
14 files changed +110 -82
modified src/api.ts
@@ -21,6 +21,8 @@ export async function get(
  const isLocalhost = /^0.0.0.0$/.test(base);
  const protocol = isLocalhost ? "http://" : "https://";

+
  path = path.startsWith("/") ? path.slice(1) : path;
+

  const baseUrl = path
    ? `${protocol}${base}:${port}/v1/${path}`
    : `${protocol}${base}:${port}`;
modified src/base/projects/Browser.svelte
@@ -21,6 +21,7 @@
  export let config: Config;
  export let org: string | null = null;
  export let user: string | null = null;
+
  export let seed: string | null = null;
  export let tree: proj.Tree;
  export let project: proj.Info;
  export let branches: [string, string][];
@@ -78,6 +79,8 @@
      navigate(proj.path({ urn, org, revision, path }));
    } else if (user) {
      navigate(proj.path({ urn, user, revision, path }));
+
    } else if (seed) {
+
      navigate(proj.path({ urn, seed, revision, path }));
    } else {
      navigate(proj.path({ urn, revision, path }));
    }
modified src/base/projects/ProjectContentRoutes.svelte
@@ -11,6 +11,7 @@
  export let org: string;
  export let tree: Tree;
  export let user: string;
+
  export let seed: string;
  export let branches: [string, string][];
  export let content: ProjectContent;
  export let revision: string;
@@ -20,21 +21,21 @@
<Router>
  <!-- The default action is to render Browser with the default branch head -->
  <Route path="/">
-
    <Browser {urn} {org} {user} {config} {tree} {project} {branches}
+
    <Browser {urn} {org} {user} {seed} {config} {tree} {project} {branches}
      locator={project.head}
      bind:content={content}
      bind:path={path}
      bind:revision={revision} />
  </Route>
  <Route path="/tree">
-
    <Browser {urn} {org} {user} {config} {tree} {project} {branches}
+
    <Browser {urn} {org} {user} {seed} {config} {tree} {project} {branches}
      locator={project.head}
      bind:content={content}
      bind:path={path}
      bind:revision={revision} />
  </Route>
  <Route path="/tree/*" let:params>
-
    <Browser {urn} {org} {user} {config} {tree} {project} {branches}
+
    <Browser {urn} {org} {user} {seed} {config} {tree} {project} {branches}
      locator={params["*"]}
      bind:content={content}
      bind:path={path}
modified src/base/projects/Routes.svelte
@@ -7,8 +7,10 @@

</script>

-
<Route path="/projects/:urn/*" let:params>
-
  <View {config} urn={params.urn} />
+
<!-- With an Seed context -->
+

+
<Route path="/seeds/:seed/projects/:urn/*" let:params>
+
  <View {config} seed={params.seed} urn={params.urn} />
</Route>

<!-- With an Org context -->
modified src/base/projects/View.svelte
@@ -9,6 +9,7 @@
  import type { Info } from '@app/project';
  import { formatOrg } from '@app/utils';
  import { getOid } from '@app/project';
+
  import { Seed } from '@app/base/seeds/Seed';

  import Header from '@app/base/projects/Header.svelte';
  import ProjectContentRoutes from '@app/base/projects/ProjectContentRoutes.svelte';
@@ -16,6 +17,7 @@
  export let urn: string;
  export let org = "";
  export let user = "";
+
  export let seed = "";
  export let config: Config;

  let parentName = formatOrg(org || user, config);
@@ -24,16 +26,19 @@
  let revision: string;
  let content: proj.ProjectContent;
  let path: string;
-
  let getProject = new Promise<Profile | null>(resolve => {
+
  let getProject = new Promise<{ profile?: Profile; seed?: Seed } | null>(resolve => {
    if (org) {
-
      Profile.get(org, ProfileType.Project, config).then(p => resolve(p));
+
      Profile.get(org, ProfileType.Project, config).then(p => resolve({ profile: p }));
    } else if (user) {
-
      Profile.get(user, ProfileType.Project, config).then(p => resolve(p));
+
      Profile.get(user, ProfileType.Project, config).then(p => resolve({ profile: p }));
+
    } else if (seed) {
+
      Seed.get(config.withSeed({ host: seed })).then(s => resolve({ seed: s }));
    } else {
      resolve(null);
    }
-
  }).then(async (profile) => {
-
    const seed = profile?.seed;
+
  }).then(async (result) => {
+
    const profile = result?.profile;
+
    const seed = profile?.seed ?? result?.seed;
    const cfg = seed ? config.withSeed(seed) : config;
    const info = await proj.getInfo(urn, cfg);
    projectInfo = info;
@@ -65,7 +70,7 @@
  }

  function updateRouteParams({ detail: newParams }: { detail: { path: string; revision: string; content: proj.ProjectContent } }) {
-
    let newLocation = proj.path({ urn, user, org, content: newParams.content, revision: newParams.revision, path: newParams.path });
+
    let newLocation = proj.path({ urn, user, org, seed, content: newParams.content, revision: newParams.revision, path: newParams.path });
    if (newLocation !== window.location.pathname) {
      navigate(newLocation);
    }
@@ -75,8 +80,7 @@
  }

  const back = () => window.history.back();
-
  // React to changes to the project commit. We have to manually
-
  // set the URL as well, to match the current commit.
+

  $: projectRoot = proj.path({ urn, user, org });
</script>

@@ -169,7 +173,7 @@
        branches={result.branches}
        profile={result.profile}
        on:routeParamsChange={updateRouteParams} />
-
      <ProjectContentRoutes {urn} {org} {user} {tree}
+
      <ProjectContentRoutes {urn} {org} {user} {seed} {tree}
        project={result.project}
        branches={result.branches}
        config={result.config}
modified src/base/projects/Widget.svelte
@@ -18,6 +18,7 @@
  export let config: Config;
  export let org: string | undefined = undefined;
  export let user: string | undefined = undefined;
+
  export let seed: string | undefined = undefined;
  export let faded = false;

  let state: State = { status: Status.Loading };
@@ -41,6 +42,7 @@
          urn: project.id,
          org,
          user,
+
          seed,
          revision: project.anchor?.stateHash,
        })
      );
modified src/base/registrations/View.svelte
@@ -82,10 +82,10 @@
          description: "The seed host address. " +
            "Only domain names with TLS are supported. " +
            `HTTP(S) API requests use port ${config.seed.api.port}.`,
-
          value: r.profile.seed.host, editable: true },
+
          value: r.profile.seed?.host, editable: true },
        { name: "seed.id", label: "Seed ID", validate: "id", placeholder: "hynkyndc6w3p8urucakobzncqny7xxtw88...",
          description: "The Device ID of a Radicle Link node that hosts entities associated with this name.",
-
          value: r.profile.seed.id, editable: true },
+
          value: r.profile.seed?.id, editable: true },
        { name: "anchors", label: "Anchors", validate: "URN", placeholder: "URN, eg. eip155:1:0x4a9cf21...",
          description: "URN under which associated project anchors can be found. "
            + "To point to a Radicle org on Ethereum, use the CAIP-10 ID, eg. *eip155:1:0x4a9cf21...*",
modified src/base/registrations/registrar.ts
@@ -20,7 +20,7 @@ export interface EnsProfile {
  name: string;
  owner?: string;
  address?: string;
-
  seed: Seed;
+
  seed?: Seed;
  anchorsAccount?: string;
  url?: string;
  avatar?: string;
@@ -86,25 +86,21 @@ export async function getRegistration(name: string, config: Config, resolver?: E
  const [address, avatar, url, seedId, seedHost, seedGit, seedApi, anchorsAccount, twitter, github] =
    meta.map(r => r.status == "fulfilled" && r.value ? r.value : undefined);

-
  return {
-
    resolver,
-
    profile: {
-
      name,
-
      url,
-
      avatar,
-
      seed: new Seed(
-
        config,
-
        seedHost,
-
        seedId,
-
        seedGit,
-
        seedApi,
-
      ),
-
      anchorsAccount,
-
      address,
-
      twitter,
-
      github,
-
    },
+
  const profile: EnsProfile = {
+
    name,
+
    url,
+
    avatar,
+
    anchorsAccount,
+
    address,
+
    twitter,
+
    github,
  };
+

+
  if (seedHost && seedId) {
+
    profile.seed = new Seed(seedHost, seedId, seedGit, seedApi);
+
  }
+

+
  return { resolver, profile };
}

export async function getAvatar(name: string, config: Config, resolver?: EnsResolver | null): Promise<string | null> {
@@ -142,7 +138,7 @@ export async function getSeed(name: string, config: Config, resolver?: EnsResolv
    resolver.getText('eth.radicle.seed.api'),
  ]);

-
  return new Seed(config, host, id, git, api);
+
  return new Seed(host, id, git, api);
}

export function registrar(config: Config): ethers.Contract {
modified src/base/seeds/Seed.ts
@@ -1,46 +1,58 @@
import * as api from '@app/api';
import type { Config } from '@app/config';
-
import { getInfo, getProjects } from '@app/project';
-
import type { Info, Project } from "@app/project";
+
import * as proj from '@app/project';
+
import type { Project } from "@app/project";
import { isDomain } from '@app/utils';
-

-
export interface SeedInfo {
-
  version: string;
-
}
+
import { assert } from '@app/error';

export class Seed {
-
  host?: string;
-
  id?: string;
-
  git?: string;
+
  host: string;
+
  id: string;
+

  api?: string;
-
  config: Config;
+
  git?: string;
+
  version?: string;
+

+
  constructor(host: string, id: string, git?: string, api?: string) {
+
    assert(isDomain(host));
+
    assert(/^[a-z0-9]+$/.test(id));
+

+
    this.host = host;
+
    this.id = id;

-
  constructor(config: Config, host?: string, id?: string, git?: string, api?: string) {
-
    if (id && /^[a-z0-9]+$/.test(id)) {
-
      this.id = id;
-
    }
-
    if (host && isDomain(host)) {
-
      this.host = host;
-
    }
    if (api && isDomain(api)) {
      this.api = api;
    }
    if (git && isDomain(git)) {
      this.git = git;
    }
-
    this.config = config.withSeed(this);
  }
-
  async getInfo(): Promise<SeedInfo> {
-
    return api.get(``, {}, this.config);
-
  }
-
  async getPeer(): Promise<{ id: string }> {
-
    return api.get("peer", {}, this.config);
+

+
  static async getPeer(config: Config): Promise<{ id: string }> {
+
    return api.get("/peer", {}, config);
  }
-
  async getProject(urn: string): Promise<Info> {
-
    return getInfo(urn, this.config);
+

+
  static async getProject(urn: string, config: Config): Promise<proj.Info> {
+
    return proj.getInfo(urn, config);
  }
-
  async getProjects(): Promise<Project[]> {
-
    const result = await getProjects(this.config);
+

+
  static async getProjects(config: Config): Promise<Project[]> {
+
    const result = await proj.getProjects(config);
    return result.map((project: any) => ({ ...project, id: project.urn }));
  }
+

+
  static async get(config: Config): Promise<Seed> {
+
    assert(config.seed.api.host);
+

+
    const [info, peer] = await Promise.all([
+
      api.get("/", {}, config),
+
      Seed.getPeer(config),
+
    ]);
+

+
    return {
+
      host: config.seed.api.host,
+
      id: peer.id,
+
      version: info.version,
+
    };
+
  }
}
modified src/base/seeds/View.svelte
@@ -8,7 +8,7 @@
  export let config: Config;
  export let seedAddress: string;

-
  const seed = new Seed(config, seedAddress);
+
  config = config.withSeed({ host: seedAddress });
</script>

<style>
@@ -70,10 +70,10 @@
</style>

<svelte:head>
-
  <title>{seed.host}</title>
+
  <title>{seedAddress}</title>
</svelte:head>

-
{#await seed.getInfo()}
+
{#await Seed.get(config)}
  <main class="off-centered">
    <Loading center />
  </main>
@@ -83,7 +83,7 @@
      <div class="info">
        <span class="title">
          <span class="bold">
-
            {seed.host}
+
            {seedAddress}
          </span>
        </span>
      </div>
@@ -92,17 +92,15 @@
    <div class="fields">
      <!-- Seed Address -->
      <div class="label">Seed</div>
-
      {#if info.version === "0.2.0" && seed.host}
-
        {#await seed.getPeer() then peer}
-
          <SeedAddress {config} id={peer.id} host={seed.host} port={config.seed.link.port} />
-
        {/await}
+
      {#if info.version === "0.2.0" && info.host}
+
        <SeedAddress {config} id={info.id} host={info.host} port={config.seed.link.port} />
      {:else}
        <div class="seed-address subtle">N/A</div>
        <div class="desktop" />
      {/if}
      <!-- API Port -->
      <div class="label">API Port</div>
-
      <div>{seed.config.seed.api.port}</div>
+
      <div>{config.seed.api.port}</div>
      <div class="desktop" />
      <!-- API Version -->
      <div class="label">Version</div>
@@ -111,11 +109,11 @@
    </div>
    <!-- Seed Projects -->
    {#if info.version === "0.2.0"}
-
      {#await seed.getProjects() then projects}
+
      {#await Seed.getProjects(config) then projects}
        <div class="projects">
          {#each projects as project}
            <div class="project">
-
              <Widget {project} {config} />
+
              <Widget {project} {config} seed={seedAddress} />
            </div>
          {/each}
        </div>
modified src/config.ts
@@ -7,7 +7,6 @@ import { Core } from '@self.id/core';
import WalletConnect from "@walletconnect/client";
import config from "@app/config.json";
import { WalletConnectSigner } from "./WalletConnectSigner";
-
import type { Seed } from '@app/base/seeds/Seed';

declare global {
  interface Window {
@@ -138,7 +137,7 @@ export class Config {
  }

  // Return the config with an overwritten seed URL.
-
  withSeed(seed: Seed): Config {
+
  withSeed(seed: { host: string; id?: string; api?: string; git?: string }): Config {
    const cfg = {} as Config;
    Object.assign(cfg, this);

modified src/profile.ts
@@ -75,7 +75,7 @@ export class Profile {
  }

  get seedId(): string | undefined {
-
    return this.profile?.ens?.seed.id;
+
    return this.profile?.ens?.seed?.id;
  }

  get seed(): Seed | undefined {
modified src/project.ts
@@ -138,15 +138,25 @@ export async function getReadme(
}

export function path(
-
  opts: { urn: string; org?: string; content?: ProjectContent; user?: string; revision?: string; path?: string }
+
  opts: {
+
    urn: string;
+
    org?: string;
+
    user?: string;
+
    seed?: string;
+
    content?: ProjectContent;
+
    revision?: string;
+
    path?: string;
+
  }
): string {
-
  const { urn, org, user, content, revision, path } = opts;
+
  const { urn, org, user, seed, content, revision, path } = opts;
  const result = [];

  if (org) {
    result.push("orgs", org);
  } else if (user) {
    result.push("users", user);
+
  } else if (seed) {
+
    result.push("seeds", seed);
  }
  result.push("projects", urn);

modified src/utils.ts
@@ -243,9 +243,8 @@ export async function querySubgraph(
  const json = await response.json();

  if (json.errors) {
-
    for (const e of json.errors) {
-
      console.error("querySubgraph:", e.message);
-
    }
+
    console.error("querySubgraph:", json.errors);
+

    if (retries > 0) querySubgraph(url, query, variables, retries - 1);
    else return null;
  }