OXYGENHUB
# 991516a107fa oxygenhub-vscode / src / flowview.ts
5.4 KiB · last changed OxygenHub 0.13.0: Drops in the editor with Share as a Drop (closes #7); Flow approvals, expedite, reports, timings and test results (closes #8); ready/draft, landable, trust reports, asset downloads and new repositories (closes #9) 7 days ago source blame history raw
263de209 Andy McCall ↶ 1
// OxygenFlow in the editor, without VS Code: how runs and jobs look (icon,
2
// colour, labels) and which runs just finished. Shared by the Flow view, the
3
// status bar item and the tests.
4
991516a1 Andy McCall ↶ 5
import type { FlowJob, FlowRun, TestCase } from "./hubapi";
263de209 Andy McCall ↶ 6
7
/** A codicon and a theme colour id for a run's or job's status. */
8
export interface Look {
9
  /** Codicon id; `~spin` makes it turn. */
10
  icon: string;
11
  /** A theme colour id, e.g. `testing.iconPassed`. */
12
  color: string;
13
  /** A short word for screen readers and tooltips. */
14
  word: string;
15
}
16
17
export function look(status: string): Look {
18
  switch (status) {
19
    case "passed":
20
      return { icon: "pass-filled", color: "testing.iconPassed", word: "passed" };
21
    case "failed":
22
      return { icon: "error", color: "testing.iconFailed", word: "failed" };
23
    case "error":
24
      return { icon: "warning", color: "testing.iconErrored", word: "could not run" };
25
    case "running":
26
      return { icon: "sync~spin", color: "oxygenhub.flowRunning", word: "running" };
27
    case "queued":
28
      return { icon: "clock", color: "testing.iconQueued", word: "queued" };
29
    case "skipped":
30
      return { icon: "debug-step-over", color: "testing.iconSkipped", word: "skipped" };
31
    default:
32
      return { icon: "circle-large-outline", color: "descriptionForeground", word: status };
33
  }
34
}
35
36
export function live(status: string): boolean {
37
  return status === "queued" || status === "running";
38
}
39
40
/** "3.2 s", "4 min 10 s". */
41
export function duration(ms: number): string {
42
  const s = Math.max(0, ms) / 1000;
43
  if (s < 60) {
44
    return `${s.toFixed(1)} s`;
45
  }
46
  return `${Math.floor(s / 60)} min ${Math.floor(s % 60)} s`;
47
}
48
49
/** "just now", "5 minutes ago", as the hub says it. */
50
export function ago(ms: number, now = Date.now()): string {
51
  const s = Math.max(0, Math.floor((now - ms) / 1000));
52
  const unit = (n: number, w: string) => `${n} ${w}${n === 1 ? "" : "s"} ago`;
53
  if (s < 45) {
54
    return "just now";
55
  }
56
  if (s < 90) {
57
    return "a minute ago";
58
  }
59
  if (s < 2700) {
60
    return unit(Math.ceil(s / 60), "minute");
61
  }
62
  if (s < 5400) {
63
    return "an hour ago";
64
  }
65
  if (s < 79200) {
66
    return unit(Math.ceil(s / 3600), "hour");
67
  }
68
  return unit(Math.ceil(s / 86400), "day");
69
}
70
71
/** What a run ran on: its bookmark, or the change for a run on one change. */
72
export function subject(run: FlowRun): string {
73
  return run.bookmark || `change ${(run.heads[0] ?? "").slice(0, 12)}`;
74
}
75
76
/** "#12 main". */
77
export function runLabel(run: FlowRun): string {
78
  return `#${run.number} ${subject(run)}`;
79
}
80
81
/** "passed in 2 min 3 s · push by andy · 5 minutes ago". */
82
export function runDescription(run: FlowRun, now = Date.now()): string {
83
  const took = run.finished_ms ? ` in ${duration(run.finished_ms - run.created_ms)}` : "";
84
  const queued = run.status === "queued" && run.queue_position ? ` (#${run.queue_position} in line)` : "";
85
  const trigger = run.trigger.startsWith("rerun:") ? `re-run of #${run.trigger.slice(6)}` : run.trigger;
86
  return `${look(run.status).word}${took}${queued} · ${trigger} by ${run.actor} · ${ago(run.created_ms, now)}`;
87
}
88
89
/**
90
 * Runs that were live at the last look and are finished now, so the editor
91
 * can say so once. `before` maps a run's key to its last status.
92
 */
93
export function newlyFinished(before: Map<string, string>, runs: { key: string; run: FlowRun }[]): { key: string; run: FlowRun }[] {
94
  return runs.filter(({ key, run }) => {
95
    const was = before.get(key);
96
    return was !== undefined && live(was) && !live(run.status);
97
  });
98
}
991516a1 Andy McCall ↶ 99
100
/** Whether a job waits on someone's approval before it runs (oxygenflow#57). */
101
export function waitingApproval(job: FlowJob): boolean {
102
  return job.approval === "waiting" && job.status === "queued";
103
}
104
105
/** MiB as the hub's pages say it: "900 MiB", "2.1 GiB". */
106
function mib(n: number): string {
107
  return n >= 1024 ? `${(n / 1024).toFixed(1)} GiB` : `${n} MiB`;
108
}
109
110
/**
111
 * Where a job's time went (#348): how long it waited for a runner, each
112
 * phase its runner reported, and its peak disk and memory. Empty when the
113
 * hub reported none of it.
114
 */
115
export function jobTimings(job: FlowJob): string[] {
116
  const out: string[] = [];
117
  if (job.queued_ms && job.started_ms) {
118
    out.push(`waited ${duration(job.started_ms - job.queued_ms)}`);
119
  }
120
  for (const p of job.phases ?? []) {
121
    out.push(`${p.name} ${p.ms < 1000 ? `${p.ms} ms` : duration(p.ms)}`);
122
  }
123
  const peaks = [job.peak_disk_mib ? `disk ${mib(job.peak_disk_mib)}` : "", job.peak_memory_mib ? `memory ${mib(job.peak_memory_mib)}` : ""].filter(Boolean);
124
  if (peaks.length) {
125
    out.push(`peak ${peaks.join(", ")}`);
126
  }
127
  return out;
128
}
129
130
/** A test report's cases by suite, in the order they came, for the Testing view. */
131
export function casesBySuite(cases: TestCase[]): Map<string, TestCase[]> {
132
  const out = new Map<string, TestCase[]>();
133
  for (const c of cases) {
134
    const suite = c.suite || c.classname || "tests";
135
    out.set(suite, [...(out.get(suite) ?? []), c]);
136
  }
137
  return out;
138
}
139
140
/** A case's id in the Testing view: stable across reports, so its history lines up. */
141
export function caseId(repo: string, c: TestCase): string {
142
  return `${repo}::${c.suite || c.classname || "tests"}::${c.classname}::${c.name}`;
143
}
144
145
/** What VS Code should show for a case's status: passed, failed, errored or skipped. */
146
export function caseOutcome(status: string): "passed" | "failed" | "errored" | "skipped" {
147
  return status === "passed" ? "passed" : status === "failed" ? "failed" : status === "error" ? "errored" : "skipped";
148
}