morca · API · ui

orca.ui.step_cards

A card per step, on hover, keyboard focus or a tap, in the UI's steps views: a pipeline's Steps card (Graph and Table) and a run's (Table, Timeline and Graph).

What a card shows is in the page, so a hover asks nothing of the server: a <script type="application/json" class="step-cards"> in the Steps card holds each step's StepCard (its kind, after=, function signature, types, the datasets it reads and writes, where its code is, and on a pipeline's page what its latest runs took and used) and, on a run's page, each attempt's AttemptCard. An element with data-step (and on a run's page data-attempt) shows the card of that step (and attempt), drawn by STEP_CARDS_JS. The one thing fetched is the function's own signature and docstring, from the source at the commit (/code?u=&at=, def_summary), once per step per page; until it comes, or where the code can't be had, the signature is the one describe records, its argument names orca's.

Structs

DefSummary

struct DefSummary

A function's signature, the 1-based lines start through end (0 for none found), and its docstring's first paragraph.

Implements Copyable, Movable

Fields
start: Int
end: Int
doc: String

StepCard

struct StepCard

What a step's card shows: the step as registered, and on a pipeline's page what its latest runs did (recent_runs 0 for none).

Implements Copyable, Movable

Fields
name: String
kind: String
kind_help: String
after: String
function: String
input: String
output: String
reads: List[String]
writes: List[String]
src: String: A link to its def line on the code's host, or empty.
at: String: Where that is, file.mojo:26.
commit: String
recent_runs: Int
recent_attempts: Int
recent_failed: Int
took: String: Its succeeded attempts' p50 and max, as text, or empty.
peak: String: Its attempts' peak memory's p50 and max, as text, or empty.

json

def json(self) -> String

AttemptCard

struct AttemptCard

What an attempt adds to its step's card on a run's page; usage values are text, empty where it wasn't measured.

Implements Copyable, Movable

Fields
id: Int
step: String
partition: String
attempt: Int
status: String
status_help: String
started: String
ended: String
took: String
peak: String
cpu: String
cores: String
error: String: Its error's first line.
href: String: Its page.

json

def json(self) -> String

Functions

short_type

def short_type(name: StringSpan) -> String

A type's name as its code writes it: modules dropped (orca.month. Month -> Month, dista.seq.Seq[toy.Measured] -> Seq[Measured], a :: path too), and a scalar SIMD[DType.int, 1] -> Int.

function_name

def function_name(function: StringSpan) -> String

toy::days(orca::ctx::Ctx,...) -> days.

signature

def signature(function: StringSpan, input: StringSpan, output: StringSpan) -> String

The step function's signature from what describe records: def days(mut ctx: Ctx, input: Month) raises -> Measured. Its argument names are orca's, as describe has none.

signature_html

def signature_html(sig: StringSpan) -> String

sig, coloured (orca.highlight).

def_summary

def def_summary(text: StringSpan, line: Int) -> DefSummary

The signature of the def at line of text (through the line that closes its brackets and ends with :) and its docstring's first paragraph, joined onto one line; the start 0 if line isn't a def.

first_line

def first_line(s: StringSpan) -> String

s up to its first line break, trimmed.

duration_text

def duration_text(seconds: Float64) -> String

A duration as <1s, 42s, 3m 07s or 2h 05m; empty for a negative one.

cards_script

def cards_script(steps: List[StepCard], attempts: List[AttemptCard]) -> String

The page's cards, as JSON in a <script type="application/json"> (< written \u003c, so nothing in it ends the script).

def_json

def def_json(text: StringSpan, line: Int) -> String

/code?u=&at='s answer: the signature at line of text, its lines coloured, and its docstring's first paragraph: {"line": 26, "sig": ["<span ...>def</span> ..."], "doc": "..."}; sig is empty if line isn't a def.

Aliases

comptime LEAD_JS = "\nfunction orcaLead(doc) {\n  const t = String(doc || \22\22).trim();\n  if (!t) return [\22\22, \22\22];\n  const para = t.split(/\\n\\s*\\n/)[0], m = /[.](\\s|$)/.exec(para);\n  const end = m ? m.index + 1 : para.length;\n  let first = para.slice(0, end).replace(/\\s+/g, \22 \22).trim();\n  if (first.length > 160) first = first.slice(0, 159).trimEnd() + \22\E2\80\A6\22;\n  return [first, t.slice(end).replace(/\\s+/g, \22 \22).trim()];\n}\n"

A docstring's lead (orcaLead), which every code hover card opens with: its first sentence (to the first ". " or blank line), on one line, at most 160 characters with an ellipsis; and the rest, on one line too.

comptime STEP_CARDS_JS = "\n(() => {\n  const esc = (s) => String(s ?? \22\22).replace(/[&<>\22']/g, (c) => ({ \22&\22: \22&amp;\22, \22<\22: \22&lt;\22, \22>\22: \22&gt;\22, '\22': \22&quot;\22, \22'\22: \22&#39;\22 })[c]);\n  const parsed = new WeakMap(), code = new Map();\n  let box = null, cur = null, showT = 0, hideT = 0, touch = false;\n  // The cards of the Steps card `el` is in, or the page's first.\n  const data = (el) => {\n    const sec = el.closest(\22[data-step-cards]\22);\n    const s = (sec && sec.querySelector(\22script.step-cards\22)) || document.querySelector(\22script.step-cards\22);\n    if (!s) return null;\n    if (!parsed.has(s)) { try { parsed.set(s, JSON.parse(s.textContent)); } catch (e) { parsed.set(s, null); } }\n    return parsed.get(s);\n  };\n  const of = (t) => {\n    if (!t || !t.closest) return null;\n    const el = t.closest(\22[data-step]\22);\n    if (el) return el;\n    return t.matches && t.matches(\22a\22) ? t.querySelector(\22:scope > [data-step]\22) : null;\n  };\n  const datasets = (names) => names.length ? names.map((n) => '<a href=\22/datasets/' + encodeURIComponent(n) + '\22>' + esc(n) + \22</a>\22).join(\22, \22) : '<span class=\22muted\22>none</span>';\n  const fact = (k, v) => v ? \22<dt>\22 + k + \22</dt><dd>\22 + v + \22</dd>\22 : \22\22;\n  function render(el, d) {\n    const s = d.steps[el.dataset.step];\n    if (!s) return null;\n    const a = el.dataset.attempt ? d.attempts[el.dataset.attempt] : null;\n    let h = '<div class=\22scard__head\22><strong>' + esc(el.dataset.step) + \22</strong> \22 +\n      (s.kind_help ? '<span class=\22term\22 data-tip=\22' + esc(s.kind_help) + '\22>' + esc(s.kind) + \22</span>\22 : esc(s.kind)) +\n      (s.after ? ' <span class=\22muted\22>after</span> <code>' + esc(s.after) + \22</code>\22 : \22\22) + \22</div>\22;\n    if (s.src) h += '<p class=\22scard__lead muted\22>Loading its docstring\E2\80\A6</p>';\n    h += '<pre class=\22scard__sig\22><code>' + s.sig + \22</code></pre>\22;\n    if (s.src) h += '<p class=\22scard__doc muted\22></p><p class=\22scard__src\22><a href=\22' + esc(s.src) + '\22 target=\22_blank\22 rel=\22noopener\22 title=\22View source on GitHub at ' + esc(s.commit) + '\22>' + esc(s.at) + \22 at \22 + esc(s.commit) + \22 \E2\86\97</a></p>\22;\n    h += '<dl class=\22scard__facts\22>' + fact(\22In\22, \22<code>\22 + esc(s.input) + \22</code>\22) + fact(\22Out\22, \22<code>\22 + esc(s.output) + \22</code>\22) +\n      fact(\22Reads\22, datasets(s.reads)) + fact(\22Writes\22, datasets(s.writes));\n    if (a) {\n      h += fact(\22Attempt\22, '<a href=\22' + esc(a.href) + '\22>' + esc(a.attempt) + \22</a>\22 + (a.partition ? \22 of \22 + esc(a.partition) : \22\22) + ' <span class=\22status status--' + esc(a.status.replace(/ /g, \22-\22)) + '\22>' + esc(a.status) + \22</span>\22) +\n        fact(\22Started\22, esc(a.started)) + fact(\22Ended\22, esc(a.ended)) + fact(\22Took\22, esc(a.took)) +\n        fact(\22Peak\22, esc(a.peak)) + fact(\22CPU\22, esc(a.cpu) + (a.cores ? \22 on up to \22 + esc(a.cores) + \22 cores\22 : \22\22)) +\n        fact(\22Error\22, a.error ? '<span class=\22error\22>' + esc(a.error) + \22</span>\22 : \22\22);\n    } else if (s.recent) {\n      const r = s.recent;\n      h += fact(\22Last \22 + r.runs + (r.runs === 1 ? \22 run\22 : \22 runs\22), esc(r.attempts + (r.attempts === 1 ? \22 attempt\22 : \22 attempts\22)) + (r.failed ? ', <span class=\22error\22>' + esc(r.failed) + \22 failed</span>\22 : \22, none failed\22)) +\n        fact(\22Took\22, esc(r.took)) + fact(\22Peak\22, esc(r.peak));\n    } else if (!el.dataset.attempt && d.attempts && !Object.keys(d.attempts).length) {\n      h += fact(\22Runs\22, '<span class=\22muted\22>none yet</span>');\n    }\n    return { html: h + \22</dl>\22, src: s.src };\n  }\n  // The function's own signature and docstring, from its code (`/code`).\n  const load = (src) => {\n    if (!code.has(src)) {\n      const m = /^(.*)#L([0-9]+)$/.exec(src);\n      code.set(src, m ? fetch(\22/code?u=\22 + encodeURIComponent(m[1]) + \22&at=\22 + m[2]).then((r) => { if (!r.ok) throw new Error(r.status); return r.json(); }) : Promise.reject(new Error(\22no line\22)));\n    }\n    return code.get(src);\n  };\n  function place(el) {\n    const r = el.getBoundingClientRect(), w = box.offsetWidth, h = box.offsetHeight;\n    const below = r.bottom + 6 + h <= innerHeight || r.top < h + 6;\n    box.style.top = Math.max(8, below ? r.bottom + 6 : r.top - h - 6) + \22px\22;\n    box.style.left = Math.max(8, Math.min(r.left, innerWidth - w - 8)) + \22px\22;\n  }\n  async function show(el) {\n    const d = data(el);\n    const c = d && render(el, d);\n    if (!c) return;\n    if (!box) {\n      box = document.createElement(\22div\22);\n      box.className = \22scard\22;\n      box.id = \22scard\22;\n      box.setAttribute(\22role\22, \22tooltip\22);\n      box.addEventListener(\22mouseenter\22, () => clearTimeout(hideT));\n      box.addEventListener(\22mouseleave\22, () => hide(150));\n      document.body.append(box);\n    }\n    // Its own title would show over the card.\n    for (const t of el.querySelectorAll(\22title\22)) { el.dataset.title = t.textContent; t.remove(); }\n    cur = el;\n    box.innerHTML = c.html;\n    box.hidden = false;\n    el.setAttribute(\22aria-describedby\22, \22scard\22);\n    place(el);\n    if (!c.src) return;\n    let sig = null, doc = \22\22;\n    try {\n      const got = await load(c.src);\n      if (got.sig && got.sig.length) sig = got.sig.join(\22\\n\22);\n      doc = got.doc || \22\22;\n    } catch (e) {}\n    if (cur !== el) return;\n    if (sig) box.querySelector(\22.scard__sig code\22).innerHTML = sig;\n    const [first, rest] = orcaLead(doc);\n    const l = box.querySelector(\22.scard__lead\22), p = box.querySelector(\22.scard__doc\22);\n    if (l) { if (first) { l.textContent = first; l.classList.remove(\22muted\22); } else l.remove(); }\n    if (p) { if (rest) p.textContent = rest; else p.remove(); }\n    place(el);\n  }\n  function hide(after) {\n    clearTimeout(showT);\n    clearTimeout(hideT);\n    hideT = setTimeout(() => {\n      if (box) box.hidden = true;\n      if (cur) cur.removeAttribute(\22aria-describedby\22);\n      cur = null;\n    }, after);\n  }\n  const inBox = (t) => box && t && box.contains(t);\n  document.addEventListener(\22mouseover\22, (e) => {\n    const el = of(e.target);\n    if (!el || touch) return;\n    clearTimeout(hideT);\n    if (el === cur) return;\n    clearTimeout(showT);\n    showT = setTimeout(() => show(el), 200);\n  });\n  document.addEventListener(\22mouseout\22, (e) => {\n    const el = of(e.target);\n    if (el && !el.contains(e.relatedTarget) && !inBox(e.relatedTarget)) hide(200);\n  });\n  document.addEventListener(\22focusin\22, (e) => {\n    const el = of(e.target);\n    if (el) { clearTimeout(hideT); clearTimeout(showT); if (el !== cur) show(el); }\n  });\n  document.addEventListener(\22focusout\22, (e) => { if (of(e.target) && !inBox(e.relatedTarget)) hide(150); });\n  // A tap shows the card; on a box that links to its code, the first tap\n  // shows it and the next follows the link.\n  document.addEventListener(\22pointerdown\22, (e) => {\n    touch = e.pointerType === \22touch\22;\n    if (!of(e.target) && !inBox(e.target) && cur) hide(0);\n  }, true);\n  document.addEventListener(\22click\22, (e) => {\n    const el = of(e.target);\n    if (!el || !touch) return;\n    if (el !== cur || (box && box.hidden)) { e.preventDefault(); clearTimeout(hideT); show(el); }\n  });\n  document.addEventListener(\22keydown\22, (e) => { if (e.key === \22Escape\22 && cur) hide(0); });\n  // A scroll (focus can scroll a table or the timeline) moves the card with\n  // its step, and closes it once the step is out of sight.\n  addEventListener(\22scroll\22, (e) => {\n    if (!cur || !box || box.hidden || inBox(e.target)) return;\n    const r = cur.getBoundingClientRect();\n    if (r.bottom < 0 || r.top > innerHeight || r.right < 0 || r.left > innerWidth) hide(0);\n    else place(cur);\n  }, true);\n})();\n"

The step cards ([data-step], with data-attempt on a run's page): the card drawn from the page's script.step-cards after a moment's hover, on keyboard focus or on a tap, beside the element (above it where there's no room), kept while the pointer is over it, moved with it on a scroll, and closed by Escape, a tap elsewhere or its step scrolling out of sight. The function's own signature and docstring are fetched once per step (/code?u=&at=); without them the card keeps the recorded signature, and without scripts the page is as it was.