# orca.ui.step_cards

> A module of morca's API.

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`

```mojo
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`

```mojo
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`

```mojo
def json(self) -> String
```

### `AttemptCard`

```mojo
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`

```mojo
def json(self) -> String
```

## Functions

### `short_type`

```mojo
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`

```mojo
def function_name(function: StringSpan) -> String
```

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

### `signature`

```mojo
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`

```mojo
def signature_html(sig: StringSpan) -> String
```

`sig`, coloured (`orca.highlight`).

### `def_summary`

```mojo
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`

```mojo
def first_line(s: StringSpan) -> String
```

`s` up to its first line break, trimmed.

### `duration_text`

```mojo
def duration_text(seconds: Float64) -> String
```

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

### `cards_script`

```mojo
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`

```mojo
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

### `LEAD_JS`

```mojo
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.

### `STEP_CARDS_JS`

```mojo
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.
