Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add code activity graph to project widget
Sebastian Martinez committed 4 years ago
commit 5384556f145c28bff65fd64c5ddb286a9341a280
parent 2bed0e4fd018aab050115b22d5d0d04021e4c3ca
8 files changed +258 -38
modified src/App.svelte
@@ -16,6 +16,7 @@
  import Header from '@app/Header.svelte';
  import Loading from '@app/Loading.svelte';
  import Modal from '@app/Modal.svelte';
+
  import LinearGradient from "@app/LinearGradient.svelte";

  const loadConfig = getConfig().then(async cfg => {
    if ($state.connection === Connection.Connected) {
@@ -112,4 +113,6 @@
      </Modal>
    </div>
  {/await}
+
  <!-- Adds a svg linear gradient that can be used by any other svg under the id #gradient -->
+
  <LinearGradient id="gradient" />
</div>
added src/Diagram.svelte
@@ -0,0 +1,78 @@
+
<script lang="ts">
+
  import { onMount } from "svelte";
+
  import type { CommitGroup } from "./commit";
+

+
  export let strokeWidth: number;
+
  export let points: CommitGroup[];
+
  export let strokeColor = "#ff55ff";
+
  export let viewBoxWidth: number;
+
  export let viewBoxHeight: number;
+

+
  // The path strings to be inserted into the svg <path>
+
  let path = "";
+
  let areaPath = "";
+

+
  let heightWithPadding = viewBoxHeight + 10;
+

+
  // The latest point on the x axis, starting at 0 until `viewBoxWidth`
+
  let lastWidthPoint = viewBoxWidth;
+

+
  // The amount of points on the x axis
+
  const widthIteration = viewBoxWidth / 52;
+

+
  // The highest value on the y axis
+
  let commitCountArray: number[] = [];
+

+
  let week = 0;
+

+
  for (const point of points) {
+
    if (point.week - week > 1) {
+
      commitCountArray.push(...new Array(point.week - week).fill(0));
+
    }
+
    commitCountArray.push(point.commits.length);
+
    week = point.week;
+
  }
+

+
  // Formats the points passed in, into a svg path string, without closing the area
+
  function createPath() {
+
    let i = 1;
+

+
    if (commitCountArray.length < 52) commitCountArray.push(...new Array(52 - commitCountArray.length).fill(0));
+

+
    let maxValue = Math.max(...commitCountArray);
+
    let minValue = Math.min(...commitCountArray);
+

+
    // Normalizes the values to the viewBox dimensions
+
    let normalizedArray =
+
      commitCountArray.map(c =>
+
        // When max and min value are 0 we want to make sure the normalization is not being run since it would return NaN
+
        c === 0 ? 0 : (viewBoxHeight - 0) * (c - minValue) / (maxValue - minValue)
+
      );
+

+
    let path = normalizedArray
+
      .slice(1)
+
      .reduce((acc, curr) => {
+
        let s = `${viewBoxWidth - widthIteration * i},${viewBoxHeight - curr}`;
+
        lastWidthPoint = viewBoxWidth - widthIteration * i;
+
        i += 1;
+
        return acc.concat(s);
+
      },
+
      [`M${viewBoxWidth},${viewBoxHeight - normalizedArray[0]}`]);
+
    return path.join();
+
  }
+

+
  onMount(() => {
+
    // Creates the stroke path with the array of points
+
    path = createPath();
+
    // Concats a path closing for it to be the area under the stroke
+
    areaPath = path.concat(`L${lastWidthPoint},${heightWithPadding}L${viewBoxWidth},${viewBoxHeight+10}Z`);
+
  });
+
</script>
+

+
<svg viewBox="0 0 {viewBoxWidth} {heightWithPadding}" xmlns="http://www.w3.org/2000/svg">
+
  <use fill="url(#gradient)" />
+
  <g>
+
    <path fill="transparent" stroke={strokeColor} stroke-width={strokeWidth} d={path} />
+
    <path fill="url('#gradient')" stroke="transparent" d={areaPath} />
+
  </g>
+
</svg>
added src/LinearGradient.svelte
@@ -0,0 +1,13 @@
+
<script lang="ts">
+
  export let id: string;
+
  export let fillColor="#ff55ff";
+
</script>
+

+
<svg style="height: 0; width: 0;" xmlns="http://www.w3.org/2000/svg">
+
  <defs>
+
    <linearGradient {id} x1="0" y1="1" x2="0" y2="0">
+
      <stop offset="0%" stop-color={fillColor} stop-opacity="0" />
+
      <stop offset="100%" stop-color={fillColor} stop-opacity="0.4" />
+
    </linearGradient>
+
  </defs>
+
</svg>
modified src/base/orgs/View/Projects.svelte
@@ -67,7 +67,7 @@
      {@const pendingAnchor = pendingAnchors[project.urn]}
      {#if project.head}
        <div class="project">
-
          <Widget {project} {anchor} on:click={() => onClick(project)}>
+
          <Widget {project} {seed} {anchor} on:click={() => onClick(project)}>
            <span class="actions" slot="actions">
              {#if profile?.org?.safe && account && anchor}
                {#if pendingAnchor} <!-- Pending anchor -->
modified src/base/projects/History.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
  import CommitTeaser from "./Commit/CommitTeaser.svelte";
-
  import type { Project } from "@app/project";
+
  import { Project } from "@app/project";
+
  import { ProjectContent } from "@app/project";
  import Loading from "@app/Loading.svelte";
  import { groupCommitHistory, GroupedCommitsHistory } from "@app/commit";
  import Message from "@app/Message.svelte";
@@ -9,7 +10,7 @@
  export let commit: string;

  const fetchCommits = async (parentCommit: string): Promise<GroupedCommitsHistory> => {
-
    const commitsQuery = await project.getCommits(parentCommit);
+
    const commitsQuery = await Project.getCommits(project.urn, project.seed.api, parentCommit);
    return groupCommitHistory(commitsQuery);
  };
</script>
@@ -55,7 +56,7 @@
    {#each history.headers as group (group.time)}
      <div class="commit-group">
        <header class="commit-date">
-
          <p>{group.time}</p>
+
          <p>{group.date}</p>
        </header>
        <div class="commit-group-headers">
          {#each group.commits as commit (commit.header.sha1)}
modified src/base/projects/Widget.svelte
@@ -1,27 +1,70 @@
<script lang="ts">
  import type * as proj from '@app/project';
  import AnchorBadge from '@app/base/profiles/AnchorBadge.svelte';
+
  import Diagram from '@app/Diagram.svelte';
+
  import { groupCommitsByWeek } from '@app/commit';
+
  import { Project } from '@app/project';
+
  import type { Seed } from '@app/base/seeds/Seed';

  export let project: proj.ProjectInfo;
+
  export let seed: Seed;
  export let faded = false;
  export let anchor: proj.Anchor | null = null;
+

+
  const getTimestampOneYearAgo = () => {
+
    const now = new Date();
+
    const oneYearAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
+
    return Math.floor(oneYearAgo.getTime() / 1000).toString();
+
  };
+

+
  const loadCommits = async () => {
+
    const commits = await Project.getCommits(project.urn, seed.api, project.head ?? undefined, getTimestampOneYearAgo(), undefined, "500", "0");
+
    return groupCommitsByWeek(commits.headers);
+
  };
</script>

<style>
  article {
+
    display: flex;
+
    flex-direction: row;
+
    justify-content: space-between;
    padding: 1rem;
    border: 1px solid var(--color-secondary-faded);
    border-radius: 0.25rem;
    min-width: 36rem;
+
    min-height: 140px;
    cursor: pointer;
  }
+
  article .right {
+
    display: flex;
+
    flex-direction: column;
+
    justify-content: space-between;
+
    align-items: flex-end;
+
  }
+
  article .left {
+
    width: 50%;
+
  }
+
  div .description {
+
    overflow-x: hidden;
+
    text-overflow: ellipsis;
+
  }
  article.project-faded {
    border: 1px dashed var(--color-foreground-subtle);
    cursor: not-allowed;
  }
+
  .activity {
+
    width: 100%;
+
    max-width: 14rem;
+
  }
  article:hover {
    border-color: var(--color-secondary);
  }
+
  article:hover .anchor {
+
    display: block;
+
  }
+
  article:hover .activity {
+
    display: none !important;
+
  }
  article.project-faded:hover {
    border-color: var(--color-foreground-faded);
  }
@@ -34,7 +77,7 @@
    margin-bottom: 0.25rem;
    font-size: 0.75rem;
  }
-
  article .anchor {
+
  article .stateHash {
    color: var(--color-secondary);
    font-size: 0.75rem;
    min-height: 2rem;
@@ -82,36 +125,53 @@
</style>

<article on:click class:project-faded={faded}>
-
  <div class="id">
-
    <span class="name">{project.name}</span>
-
    <span class="urn desktop">{project.urn}</span>
-
  </div>
-
  <div class="description">{project.description || ""}</div>
-
  <div class="anchor">
-
    <span class="commit">
-
      <slot name="stateHash">
-
        {#if project.head}
-
          {project.head}
-
        {:else}
-
          <span class="subtle">✗ No head</span>
-
        {/if}
-
      </slot>
-
    </span>
-
    <span class="anchor-info">
-
      <span class="actions">
-
        <slot name="actions">
-
        </slot>
-
      </span>
-
      <span class="anchor-badge">
-
        <slot name="anchor">
-
          {#if anchor && project.head}
-
            <AnchorBadge
-
              commit={project.head}
-
              head={project.head} noText noBg
-
              anchors={[anchor.anchor.stateHash]} />
+
  <div class="left">
+
    <div class="id">
+
      <span class="name">{project.name}</span>
+
    </div>
+
    <div class="description">{project.description || ""}</div>
+
    <div class="stateHash">
+
      <span class="commit">
+
        <slot name="stateHash">
+
          {#if project.head}
+
            {project.head}
+
          {:else}
+
            <span class="subtle">✗ No head</span>
          {/if}
        </slot>
      </span>
-
    </span>
+
    </div>
+
  </div>
+
  <div class="right">
+
    <div class="id">
+
      <span class="urn desktop">{project.urn}</span>
+
    </div>
+
    <div class="anchor">
+
      <span class="anchor-info">
+
        <span class="actions">
+
          <slot name="actions">
+
          </slot>
+
        </span>
+
        <span class="anchor-badge">
+
          <slot name="anchor">
+
            {#if anchor && project.head}
+
              <AnchorBadge
+
                commit={project.head}
+
                head={project.head} noText noBg
+
                anchors={[anchor.anchor.stateHash]} />
+
            {/if}
+
          </slot>
+
        </span>
+
      </span>
+
    </div>
+
    {#await loadCommits() then points}
+
      <div class="desktop activity">
+
        <Diagram {points}
+
          strokeWidth={1.25}
+
          viewBoxHeight={100}
+
          viewBoxWidth={600}
+
        />
+
      </div>
+
    {/await}
  </div>
</article>
modified src/commit.ts
@@ -17,7 +17,6 @@ export interface GroupedCommitsHistory {
}

export interface Author {
-
  avatar: string;
  email: string;
  name: string;
}
@@ -54,8 +53,10 @@ export interface CommitHeader {

// A set of commits grouped by time.
export interface CommitGroup {
-
  time: string;
+
  date: string;
+
  time: number;
  commits: CommitMetadata[];
+
  week: number;
}

export interface CommitStats {
@@ -112,8 +113,10 @@ export function groupCommits(commits: { header: CommitHeader; context: CommitCon

      if (isNewDay) {
        groupedCommits.push({
-
          time: formatGroupTime(time),
+
          date: formatGroupTime(time),
+
          time,
          commits: [],
+
          week: 0
        });
        groupDate = date;
      }
@@ -128,3 +131,61 @@ export function groupCommits(commits: { header: CommitHeader; context: CommitCon
export const formatCommitTime = (t: number): string => {
  return new Date(t * 1000).toUTCString();
};
+

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

+
  if (commits.length === 0) {
+
    return [];
+
  }
+

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

+
    return 0;
+
  });
+

+
  // A accumulator that increments by the amount of weeks between weekly commit groups
+
  let weekAccumulator = Math.floor(getDaysPassed(new Date(commits[0].header.committerTime * 1000), new Date()) / 7);
+

+
  // Loops over all commits and stores them by week with some additional metadata in groupedCommits.
+
  for (const commit of commits) {
+
    const time = commit.header.committerTime * 1000;
+
    const date = new Date(time);
+
    const isNewWeek =
+
      !groupedCommits.length ||
+
      !groupDate ||
+
      getDaysPassed(date, groupDate) > 7 ||
+
      date.getFullYear() < groupDate.getFullYear();
+

+
    if (isNewWeek) {
+
      let daysPassed = 0;
+
      if (groupDate) {
+
        daysPassed = getDaysPassed(date, groupDate);
+
      }
+
      groupedCommits.push({
+
        date: formatGroupTime(time),
+
        time,
+
        commits: [],
+
        week: Math.floor(daysPassed / 7) + weekAccumulator
+
      });
+
      groupDate = date;
+
      weekAccumulator += Math.floor(daysPassed / 7);
+
    }
+
    groupedCommits[groupedCommits.length - 1].commits.push(commit);
+
  }
+

+
  return groupedCommits;
+
}
+

+
// Get amount of days passed between two dates
+
function getDaysPassed(from: Date, to: Date): number {
+
  return Math.floor(
+
    (to.getTime() - from.getTime()) / (24 * 60 * 60 * 1000)
+
  );
+
}
modified src/project.ts
@@ -274,8 +274,12 @@ export class Project implements ProjectInfo {
    return api.get(`projects/${urn}/remotes`, {}, host);
  }

-
  async getCommits(commit: string): Promise<CommitsHistory> {
-
    return api.get(`projects/${this.urn}/commits?parent=${commit}`, {}, this.seed.api);
+
  static async getCommits(urn: string, host: api.Host, parent?: string, since?: string, until?: string, perPage?: string, page?: string): Promise<CommitsHistory> {
+
    const params: Record<string, string | undefined> = { parent, since, until, "per-page": perPage, page };
+
    // Removes the undefined params.
+
    Object.keys(params).forEach(key => params[key] === undefined && delete params[key]);
+

+
    return api.get(`projects/${urn}/commits`, params, host);
  }

  async getCommit(commit: string): Promise<Commit> {