Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add commits listing
Sebastian Martinez committed 4 years ago
commit 02eb4463c11b9945c65828cbc61223d63ca7b813
parent 4b2c14e42e2ed790134d445d1615924d0d0673f9
8 files changed +311 -62
modified public/index.css
@@ -359,6 +359,9 @@ label.input {
.desktop {
  display: block !important;
}
+
.desktop-inline {
+
  display: inline !important;
+
}

@media (max-width: 720px) {
  .mobile {
@@ -367,6 +370,9 @@ label.input {
  .desktop {
    display: none !important;
  }
+
  .desktop-inline {
+
    display: none !important;
+
  }
}

span.small, .text-xsmall {
modified src/base/projects/Browser.svelte
@@ -79,32 +79,6 @@
    mobileFileTree = !mobileFileTree;
  };

-
  const GetAllAnchors = `
-
    query GetAllAnchors($project: Bytes!, $org: ID!) {
-
      anchors(orderBy: timestamp, orderDirection: desc, where: { objectId: $project, org: $org }) {
-
        multihash
-
        timestamp
-
      }
-
    }
-
  `;
-

-
  interface AnchorObject {
-
    timestamp: number;
-
    commit: string;
-
    multihash: string;
-
  }
-

-
  async function getAllAnchors(anchors: string | null, urn: string): Promise<string[] | null> {
-
    if (! anchors) {
-
      return null;
-
    }
-
    const unpadded = utils.decodeRadicleId(urn);
-
    const id = ethers.utils.hexZeroPad(unpadded, 32);
-
    const allAnchors = await utils.querySubgraph(config.orgs.subgraph, GetAllAnchors, { project: id, org: anchors });
-
    return allAnchors.anchors
-
      .map((anchor: AnchorObject) => utils.formatProjectHash(ethers.utils.arrayify(anchor.multihash)));
-
  }
-

  // This is reactive to respond to path changes that don't originate from this
  // component, eg. when using the browser's "back" button.
  $: getBlob = loadBlob(path);
added src/base/projects/Commit/CommitTeaser.svelte
@@ -0,0 +1,57 @@
+
<script lang="ts">
+
  import type { CommitHeader } from "@app/project";
+
  import { formatCommit } from "@app/utils";
+
  import { formatCommitTime } from "./lib";
+

+
  export let commit: CommitHeader;
+
</script>
+

+
<style>
+
  .hash {
+
    font-family: var(--font-family-monospace);
+
    padding: 0 4px 0 0;
+
  }
+
  .author {
+
    margin-right: 8px
+
  }
+
  .commit {
+
    display: flex;
+
    align-items: center;
+
    justify-content: space-between;
+
    padding: 0 1rem 0 0.6rem;
+
    height: 2.5rem;
+
  }
+
  .small {
+
    font-size: 14px;
+
  }
+
  @media (max-width: 720px) {
+
    .commit {
+
      flex-direction: column;
+
      justify-content: center;
+
      align-items: flex-start;
+
      padding-top: 0.3rem;
+
      padding-bottom: 0.3rem;
+
      height: unset;
+
    }
+
    .author, .hash {
+
      font-size: 12px;
+
    }
+
    .summary {
+
      overflow: hidden;
+
      white-space: nowrap;
+
      text-overflow: ellipsis;
+
      width: 100%;
+
    }
+
  }
+
</style>
+

+
<div class="commit small">
+
  <div class="summary">
+
    <span class="secondary desktop-inline hash bold">{formatCommit(commit.sha1)}</span>
+
    <span>{commit.summary}</span>
+
  </div>
+
  <div>
+
    <span class="bold author">{commit.committer.name}</span>
+
    <span class="desktop-inline">{formatCommitTime(commit.committerTime)}</span>
+
  </div>
+
</div>
added src/base/projects/Commit/History.svelte
@@ -0,0 +1,70 @@
+
<script lang="ts">
+
  import CommitTeaser from "./CommitTeaser.svelte";
+
  import { getCommits } from "@app/project";
+
  import type { Config } from "@app/config";
+
  import Loading from "@app/Loading.svelte";
+
  import { groupCommitHistory, GroupedCommitsHistory } from "./lib";
+

+
  export let commit: string;
+
  export let urn: string;
+
  export let config: Config;
+

+
  async function fetchCommits(): Promise<GroupedCommitsHistory> {
+
    const commitsQuery = await getCommits(urn, commit, config);
+
    return groupCommitHistory(commitsQuery);
+
  }
+
</script>
+

+
<style>
+
  .history {
+
    padding: 0 2rem 0 8rem;
+
  }
+
  .commit-group header {
+
    color: var(--color-foreground-6);
+
    padding-left: 0.6rem;
+
  }
+
  .commit-group-headers {
+
    border: 1px solid var(--color-foreground-3);
+
    border-radius: 0.5rem;
+
    margin-bottom: 2rem;
+
  }
+
  .commit {
+
    border-bottom: 1px solid var(--color-foreground-3);
+
    padding: 0.25rem 0;
+
  }
+
  .commit:first-child {
+
    border-top-left-radius: 0.5rem;
+
    border-top-right-radius: 0.5rem;
+
  }
+
  .commit:last-child {
+
    border-bottom: none;
+
    border-bottom-left-radius: 0.5rem;
+
    border-bottom-right-radius: 0.5rem;
+
  }
+
  @media (max-width: 720px) {
+
    .history {
+
      padding-left: 2rem;
+
    }
+
  }
+
</style>
+

+
{#await fetchCommits()}
+
  <Loading center />
+
{:then history}
+
  <div class="history">
+
    {#each history.headers as group (group.time)}
+
      <div class="commit-group">
+
        <header>
+
          <p>{group.time}</p>
+
        </header>
+
        <div class="commit-group-headers">
+
          {#each group.commits as commit (commit.sha1)}
+
            <div class="commit">
+
              <CommitTeaser {commit} />
+
            </div>
+
          {/each}
+
        </div>
+
      </div>
+
    {/each}
+
  </div>
+
{/await}
added src/base/projects/Commit/lib.ts
@@ -0,0 +1,83 @@
+
import type { CommitHeader, Stats } from "@app/project";
+

+
export interface CommitsHistory {
+
  headers: CommitHeader[];
+
  stats: Stats;
+
}
+
export interface GroupedCommitsHistory {
+
  headers: CommitGroup[];
+
  stats: Stats;
+
}
+

+
export interface CommitStats {
+
  branches: number;
+
  commits: number;
+
  contributors: number;
+
}
+

+
export interface GroupedCommitsHistory {
+
  headers: CommitGroup[];
+
  stats: Stats;
+
}
+

+
// A set of commits grouped by time.
+
export interface CommitGroup {
+
  time: string;
+
  commits: CommitHeader[];
+
}
+

+

+
export function formatGroupTime(timestamp: number): string {
+
  return new Date(timestamp).toLocaleDateString("en-US", {
+
    day: 'numeric',
+
    weekday: 'long',
+
    month: 'long',
+
    year: 'numeric'
+
  });
+
}
+

+
export const groupCommitHistory = (
+
  history: CommitsHistory
+
): GroupedCommitsHistory => {
+
  return { ...history, headers: groupCommits(history.headers) };
+
};
+

+
export function groupCommits(commits: CommitHeader[]): CommitGroup[] {
+
  const groupedCommits: CommitGroup[] = [];
+
  let groupDate: Date | undefined = undefined;
+

+
  commits = commits.sort((a, b) => {
+
    if (a.committerTime > b.committerTime) {
+
      return -1;
+
    } else if (a.committerTime < b.committerTime) {
+
      return 1;
+
    }
+

+
    return 0;
+
  });
+

+
  for (const commit of commits) {
+
    const time = commit.committerTime * 1000;
+
    const date = new Date(time);
+
    const isNewDay =
+
      !groupedCommits.length ||
+
      !groupDate ||
+
      date.getDate() < groupDate.getDate() ||
+
      date.getMonth() < groupDate.getMonth() ||
+
      date.getFullYear() < groupDate.getFullYear();
+

+
    if (isNewDay) {
+
      groupedCommits.push({
+
        time: formatGroupTime(time),
+
        commits: [],
+
      });
+
      groupDate = date;
+
    }
+
    groupedCommits[groupedCommits.length - 1].commits.push(commit);
+
  }
+
  return groupedCommits;
+
}
+

+
export const formatCommitTime = (t: number): string => {
+
  return new Date(t * 1000).toUTCString();
+
};
modified src/base/projects/Header.svelte
@@ -2,10 +2,10 @@
  import type { Config } from '@app/config';
  import * as utils from '@app/utils';
  import Loading from '@app/Loading.svelte';
-
  import { Org } from '@app/base/orgs/Org';
+
  import { ethers } from "ethers";
  import type { Info, Tree } from '@app/project';
+
  import { ProjectContent } from "@app/project";
  import type { Profile } from '@app/profile';
-
  import { createEventDispatcher } from "svelte";
  
  export let config: Config;
  export let anchors: string | null = null;
@@ -14,17 +14,45 @@
  export let project: Info;
  export let profile: Profile | null = null;
  export let tree: Tree;
+
  export let content: ProjectContent;

  // Whether the clone dropdown is visible.
  let cloneDropdown = false;
  // Whether the seed dropdown is visible.
  let seedDropdown = false;
-
  const dispatch = createEventDispatcher();
-
  const onClick = ({ detail: newCommit }: { detail: string }): void => {
-
    dispatch("commitChange", newCommit);
+

+
  // Switches between the browser and commit view
+
  const switchContent = () => {
+
    content = content == ProjectContent.Browser ? ProjectContent.Commits : ProjectContent.Browser;
  };

-
  $: getAnchor = anchors ? Org.getAnchor(anchors, urn, config) : null;
+
  const GetAllAnchors = `
+
    query GetAllAnchors($project: Bytes!, $org: ID!) {
+
      anchors(orderBy: timestamp, orderDirection: desc, where: { objectId: $project, org: $org }) {
+
        multihash
+
        timestamp
+
      }
+
    }
+
  `;
+

+
  interface AnchorObject {
+
    timestamp: number;
+
    multihash: string;
+
  }
+

+
  async function getAllAnchors(anchors: string | null, urn: string): Promise<string[] | null> {
+
    if (! anchors) {
+
      return null;
+
    }
+
    const unpadded = utils.decodeRadicleId(urn);
+
    const id = ethers.utils.hexZeroPad(unpadded, 32);
+
    const allAnchors = await utils.querySubgraph(config.orgs.subgraph, GetAllAnchors, { project: id, org: anchors });
+
    console.log(allAnchors);
+
    return allAnchors.anchors
+
      .map((anchor: AnchorObject) => utils.formatProjectHash(ethers.utils.arrayify(anchor.multihash)));
+
  }
+

+
  $: getAnchor = anchors ? getAllAnchors(anchors, urn) : null;
</script>

<style>
@@ -109,7 +137,7 @@
    background-color: var(--color-foreground-background-lighter);
  }

-
  .clone {
+
  .clone, .commit-count {
    color: var(--color-primary);
    background-color: var(--color-primary-background);
    font-family: var(--font-family-monospace);
@@ -118,7 +146,7 @@
    cursor: pointer;
    user-select: none;
  }
-
  .clone:hover {
+
  .clone:hover, .commit-count:hover {
    background-color: var(--color-primary-background-lighter);
  }
  .dropdown {
@@ -165,6 +193,11 @@
    padding: 0.5rem 0.75rem;
    background: var(--color-foreground-background);
  }
+

+
  .error {
+
    color: var(--color-negative);
+
    cursor: not-allowed;
+
  }
  @media (max-width: 960px) {
    header {
      padding-left: 2rem;
@@ -204,31 +237,38 @@
      {#await getAnchor}
        <Loading small margins />
      {:then anchor}
-
        {#if anchor === commit}
-
          {#if commit === project.head}
+
        {#if anchor}
+
          <!-- commit is head and latest anchor  -->
+
          {#if commit == anchor[0] && commit === project.head}
            <span class="anchor-widget anchor-latest">
-
              <span class="anchor-label" title={anchors}>anchored 🔒</span>
+
              <span class="anchor-label" title="{anchors}">latest 🔐</span>
+
            </span>
+
          <!-- commit is not head but latest anchor  -->
+
          {:else if commit == anchor[0] && commit !== project.head}
+
            <span class="anchor-widget" on:click={() => commit = project.head}>
+
              <span class="anchor-label" title="{anchors}">latest 🔐</span>
            </span>
+
          <!-- commit is not head a stale anchor  -->
+
          {:else if anchor?.includes(commit)}
+
            <span class="anchor-widget" on:click={() => commit = anchor[0]}>
+
              <span class="anchor-label" title="{anchors}">stale 🔒</span>
+
            </span>
+
          <!-- commit is not anchored, could be head or any other commit  -->
          {:else}
-
            <span
-
              class="anchor-widget"
-
              on:click={() => onClick({ detail: project.head })}
-
            >
-
              <span class="anchor-label" title={anchors}>anchored 🔒</span>
+
            <span class="anchor-widget not-anchored" on:click={() => commit = anchor[0]}>
+
              <span class="anchor-label">not anchored 🔓</span>
            </span>
          {/if}
-
        {:else if anchor}
-
          <span
-
            class="anchor-widget not-anchored"
-
            on:click={() => onClick({ detail: anchor })}
-
          >
-
            <span class="anchor-label">not anchored 🔓</span>
-
          </span>
        {:else}
+
          <!-- commit is not head and neither an anchor, and there are no anchors available  -->
          <span class="anchor-widget not-anchored not-allowed">
            <span class="anchor-label">not anchored 🔓</span>
          </span>
        {/if}
+
      {:catch}
+
        <span class="anchor-widget error" title="Not able to fetch anchor from subgraph">
+
          <span class="anchor-label">❌</span>
+
        </span>
      {/await}
    {/if}
  </div>
@@ -283,8 +323,12 @@
      {/if}
    </div>
  </span>
-
  <div class="stat">
-
    <strong>{tree.stats.commits}</strong> commit(s)
+
  <div class="stat commit-count" on:click={switchContent}>
+
    {#if content == ProjectContent.Browser}
+
      <strong>{tree.stats.commits}</strong> commit(s)
+
    {:else}
+
      <strong>Back to Browser</strong>
+
    {/if}
  </div>
  <div class="stat">
    <strong>{tree.stats.contributors}</strong> contributor(s)
modified src/base/projects/View.svelte
@@ -11,6 +11,7 @@

  import Browser from './Browser.svelte';
  import Header from './Header.svelte';
+
  import History from "./Commit/History.svelte";

  export let urn: string;
  export let org = "";
@@ -19,10 +20,10 @@
  export let config: Config;
  export let path: string;

+
  let content = proj.ProjectContent.Browser;
  let parentName = formatOrg(org || user, config);
  let pageTitle = parentName ? `${parentName}/${urn}` : urn;
  let projectInfo: Info | null = null;
-
  let projectRoot = proj.path({ urn, user, org, commit });
  let getProject = new Promise<Profile | null>(resolve => {
    if (org) {
      Profile.get(org, ProfileType.Project, config).then(p => resolve(p));
@@ -35,6 +36,7 @@
    const seed = profile?.seed;
    const cfg = seed ? config.withSeed(seed) : config;
    const info = await proj.getInfo(urn, cfg);
+
    commit = commit ? commit : info.head;

    projectInfo = info;

@@ -47,10 +49,6 @@
      : `/users/${profile.nameOrAddress}`;
  };

-
  const onCommitChange = ({ detail: newCommit }: { detail: string }): void => {
-
    commit = newCommit;
-
  };
-

  $: if (projectInfo) {
    const baseName = parentName
      ? `${parentName}/${projectInfo.meta.name}`
@@ -64,6 +62,8 @@
  }

  const back = () => window.history.back();
+
  // Reacts to change to the used commit
+
  $: projectRoot = proj.path({ urn, user, org, commit });
</script>

<style>
@@ -152,14 +152,18 @@
    {:then tree}
      <Header {urn} {tree}
        anchors={result.profile?.anchorsAccount ?? org}
-
        commit={commit || result.project.head}
        config={result.config}
        project={result.project}
-
        on:commitChange={onCommitChange}
-
      /> 
-
      <Browser {urn} {org} {user} {path} {tree}
-
        commit={commit || result.project.head}
-
        config={result.config} />
+
        bind:commit={commit}
+
        bind:content={content}
+
      />
+
      {#if content == proj.ProjectContent.Browser}
+
        <Browser {urn} {org} {user} {path} {tree}
+
          commit={commit}
+
          config={result.config} />
+
      {:else if content == proj.ProjectContent.Commits}
+
        <History {urn} config={result.config} bind:commit={commit}  />
+
      {/if}
    {:catch err}
      <div class="container center-content">
        <div class="error error-message text-xsmall">
modified src/project.ts
@@ -1,5 +1,6 @@
import type { Config } from '@app/config';
import * as api from '@app/api';
+
import type { CommitsHistory } from '@app/base/projects/Commit/lib';

export type Urn = string;

@@ -15,6 +16,12 @@ export interface PendingProject extends Project {
  confirmations: string[]; // Owner addresses who have confirmed.
}

+
// Enumerates the space below the Header component in the projects View component
+
export enum ProjectContent {
+
  Browser,
+
  Commits,
+
}
+

export interface Info {
  head: string;
  meta: Meta;
@@ -82,6 +89,10 @@ export async function getInfo(urn: string, config: Config): Promise<Info> {
  return api.get(`projects/${urn}`, {}, config);
}

+
export async function getCommits(urn: string, commit: string, config: Config): Promise<CommitsHistory> {
+
  return api.get(`projects/${urn}/commits/${commit}`, {}, config);
+
}
+

export async function getProjects(config: Config): Promise<any> {
  return api.get("projects", {}, config);
}