Dashboard

RiskMatrix

Risks on a 5 by 5 heat grid with keyboard placement: score, band, the mitigated position beside the inherent one, a sorted register, and a filter by cell.

#smart#interactive#risk#heatmap#register#keyboard

Demo

Preview

Operational risks

Likelihood by impactNegligible1Minor2Moderate3Major4Severe5
Almost certain5
Likely4
Possible3
Unlikely2
Rare1
  • As it stands
  • After mitigation
  • Risks now to after:
  • Low 0 to 0
  • Medium 3 to 4
  • High 1 to 1
  • Critical 1 to 0

5 risks, worst first
RefRiskOwnerAs it standsAfter mitigationMove
R2Cloud region outageBen16 Critical9 Medium
R1Customer data breachAna15 High8 Medium
R4Lead engineer leavesDee9 Medium12 HighWorse than before: check this mitigation.
R3Key vendor failsChi8 MediumNot assessed
R5Exchange rate swingEli5 Medium5 Medium

Launch risks

Likelihood by impactNegligible1Minor2Moderate3Major4Severe5
Almost certain5
Likely4
Possible3
Unlikely2
Rare1
  • As it stands
  • After mitigation
  • Risks now to after:
  • Accept 1 to 1
  • Act 0 to 0

1 risk, worst first
RefRiskOwnerAs it standsAfter mitigationMove
R1Launch slips4 AcceptNot assessed

Move a risk to see on_change.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/risk_matrix.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/risk_matrix.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/risk_matrix.qwe
let risk_matrix_demo_risks = [    { id: "breach", title: "Customer data breach", owner: "Ana", likelihood: 3, impact: 5, mitigated_likelihood: 2, mitigated_impact: 4 },    { id: "outage", title: "Cloud region outage", owner: "Ben", likelihood: 4, impact: 4, mitigated_likelihood: 3, mitigated_impact: 3 },    { id: "vendor", title: "Key vendor fails", owner: "Chi", likelihood: 2, impact: 4 },    { id: "churn", title: "Lead engineer leaves", owner: "Dee", likelihood: 3, impact: 3, mitigated_likelihood: 4, mitigated_impact: 3 },    { id: "fx", title: "Exchange rate swing", owner: "Eli", likelihood: 5, impact: 1, mitigated_likelihood: 5, mitigated_impact: 1 },    { id: "flood", title: "Office flood", owner: "Ana", likelihood: 9, impact: 2 },]component RiskMatrixDemoLive() {    state seen = "Move a risk to see on_change."    <div.demo-stack>        <RiskMatrix id="risk-matrix-demo-live" title="Launch risks" name="launch" bands={[{ name: "Accept", upto: 8 }, { name: "Act", upto: 25 }]}                    risks={[{ id: "late", title: "Launch slips", likelihood: 2, impact: 2 }]}                    on_change={(rows) => { seen = `on_change: ${rows[0].title} scores ${rows[0].score}, ${rows[0].band}.` }}/>        <p.demo-note>{seen}</p>    </div>}view RiskMatrixDemo() =    <div.demo-stack>        <RiskMatrix id="risk-matrix-demo" title="Operational risks" risks={risk_matrix_demo_risks}/>        <RiskMatrixDemoLive/>    </div>

Source

ui/risk_matrix.qwe
// RiskMatrix: risks on a 5 by 5 grid of likelihood against impact, each with its// score and band, its mitigated position beside the inherent one, and a register// sorted worst first. Clicking a cell keeps the register to the risks in it. A risk// is moved with the keyboard as easily as with the mouse: Move picks it up, the// arrow keys carry it across the grid, Enter puts it down and Escape takes it back.////   <RiskMatrix risks={[{ title: "Data breach", likelihood: 3, impact: 5, mitigated_likelihood: 2, mitigated_impact: 4, owner: "Ana" }]}/>//   <RiskMatrix risks={risks} bands={[{ name: "Accept", upto: 8 }, { name: "Act", upto: 25 }]}//               on_change={(rows) => { saved = rows }}/>//// On the grid: the arrow keys go from cell to cell, Home and End to the ends of the// row, Enter or Space filters by the cell (again to stop), Escape clears the filter.//// risks: each { title, likelihood, impact }, those two whole numbers from 1 to 5, with// id, owner, mitigated_likelihood and mitigated_impact if known. A risk outside the// grid is left off it, and named in a message; a risk without both mitigated numbers// counts as not assessed. Up to 200 are taken. bands: the score bands, lowest first,// each { name, upto }, the last reaching 25 (Low to 4, Medium to 9, High to 15,// Critical to 25); the first four are coloured from calm to loud. likelihood_labels,// impact_labels: five names each for the grid's sides, lowest first. title: the// heading ("Risk matrix"). id: a prefix for its ids ("risk-matrix"), unique on the// page. Hidden fields named risk_id, likelihood, impact, mitigated_likelihood and// mitigated_impact hold each risk's place, in order, so a form around it sends them// (the mitigated two are empty when not assessed); name: a prefix for those names.// on_change(rows): inside one of your components, given every risk as { id, title,// owner, likelihood, impact, score, band, mitigated_likelihood, mitigated_impact,// mitigated_score, mitigated_band } after each move that is kept.//// risk_matrix_clean(risks) gives { rows, problems }, risk_matrix_band(score, bands)// the band, and risk_matrix_register(rows) the order, for a program's own use.import ui/theme.qwe use ui_clslet risk_matrix_css = css`.ui-risk-matrix { display: grid; gap: 14px; min-width: 0; padding: 20px 22px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-risk-matrix .ui-risk-matrix-title { display: block; margin: 0; font-size: 16px; font-weight: 650; }.ui-risk-matrix-grid { width: 100%; max-width: 640px; table-layout: fixed; border-collapse: separate; border-spacing: 3px; font-size: 12px; }.ui-risk-matrix-grid th { padding: 2px 4px; color: var(--ui-muted); font-size: 11px; font-weight: 500; overflow-wrap: anywhere; }.ui-risk-matrix-grid thead th { text-align: center; vertical-align: bottom; }.ui-risk-matrix-grid tbody th { width: 62px; text-align: right; }.ui-risk-matrix-short { display: none; }.ui-risk-matrix-cell { position: relative; height: 58px; padding: 16px 3px 3px; border-radius: var(--ui-radius-sm); vertical-align: top; cursor: pointer; }.ui-risk-matrix-cell:focus { outline: 2px solid var(--ui-accent); outline-offset: 1px; }.ui-risk-matrix-cell[aria-selected="true"] { box-shadow: inset 0 0 0 2px var(--ui-fg); }.ui-risk-matrix-b0 { background: var(--ui-success-soft); }.ui-risk-matrix-b1 { background: var(--ui-warning-soft); }.ui-risk-matrix-b2 { background: color-mix(in srgb, var(--ui-warning) 22%, var(--ui-warning-soft)); }.ui-risk-matrix-b3 { background: color-mix(in srgb, var(--ui-danger) 30%, var(--ui-danger-soft)); }.ui-risk-matrix-corner { position: absolute; top: 2px; right: 5px; color: var(--ui-fg); font-size: 10px; font-variant-numeric: tabular-nums; }.ui-risk-matrix-chips { display: flex; flex-wrap: wrap; gap: 2px; }.ui-risk-matrix-chip { padding: 0 4px; border: 1px solid var(--ui-fg); border-radius: 4px; background: var(--ui-fg); color: var(--ui-bg); font-size: 11px; font-weight: 600; line-height: 16px; }.ui-risk-matrix-after { background: transparent; color: var(--ui-fg); border-style: dashed; }.ui-risk-matrix-held { outline: 2px solid var(--ui-accent); outline-offset: 1px; }.ui-risk-matrix-key { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 12px; }.ui-risk-matrix-key li { display: inline-flex; align-items: center; gap: 6px; }.ui-risk-matrix-live { display: grid; gap: 6px; min-width: 0; }.ui-risk-matrix-say { margin: 0; color: var(--ui-muted); font-size: 13px; min-height: 19px; }.ui-risk-matrix-problems { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; color: var(--ui-danger); font-size: 13px; }.ui-risk-matrix-scroll { position: relative; min-width: 0; overflow-x: auto; }.ui-risk-matrix-scroll:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-risk-matrix-table { width: 100%; min-width: 620px; border-collapse: collapse; }.ui-risk-matrix-table th { padding: 6px 8px; border-bottom: 1px solid var(--ui-border); color: var(--ui-muted); font-size: 12px; font-weight: 600; text-align: left; white-space: nowrap; }.ui-risk-matrix-table td { padding: 6px 8px; border-bottom: 1px solid var(--ui-border); vertical-align: top; }.ui-risk-matrix-band { padding: 0 6px; border-radius: 4px; color: var(--ui-fg); font-size: 12px; font-weight: 600; white-space: nowrap; }.ui-risk-matrix-worse { display: block; color: var(--ui-danger); font-size: 12px; }.ui-risk-matrix-muted { color: var(--ui-muted); }.ui-risk-matrix-actions { display: flex; flex-wrap: wrap; gap: 4px; }.ui-risk-matrix-button { min-height: 28px; padding: 0 8px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg);  font: 12px var(--ui-font); white-space: nowrap; cursor: pointer; }.ui-risk-matrix-button:hover { border-color: var(--ui-strong); }.ui-risk-matrix-button:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-risk-matrix-button[aria-pressed="true"] { border-color: var(--ui-accent); color: var(--ui-accent); }.ui-risk-matrix-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }@media (max-width: 480px) { .ui-risk-matrix-grid tbody th { width: 16px; } .ui-risk-matrix-cell { height: 48px; }  .ui-risk-matrix-long { display: none; } .ui-risk-matrix-short { display: inline; } }`let risk_matrix_bands = [{ name: "Low", upto: 4 }, { name: "Medium", upto: 9 }, { name: "High", upto: 15 }, { name: "Critical", upto: 25 }]let risk_matrix_likelihoods = ["Rare", "Unlikely", "Possible", "Likely", "Almost certain"]let risk_matrix_impacts = ["Negligible", "Minor", "Moderate", "Major", "Severe"]let risk_matrix_cap = 200// A place on a side of the grid: a whole number from 1 to 5, as a number or its text, else null.fn risk_matrix_side(v) {    let t = trim(str(v ?? ""))    re.test(t, "^[1-5]$") ? int(t) : null}fn risk_matrix_band(score: int, bands: list) -> map {    let at = find(range(len(bands)), (i) => score <= bands[i].upto) ?? len(bands) - 1    { name: bands[at].name, level: min([at, 3]) }}// The risks given to rows of this component's own, and what is wrong with those it left out.fn risk_matrix_clean(risks: list) -> map {    let taken = slice(risks, 0, risk_matrix_cap)    let read = for n in range(len(taken)) {        let r = taken[n]        let label = type_of(r) == "map" ? (filled(trim(str(r.title ?? ""))) ?? `Risk ${n + 1}`) : `Item ${n + 1}`        if type_of(r) != "map" { { problem: `${label} is not a risk, so it is left off the grid.` } } else {            let l = risk_matrix_side(r.likelihood)            let i = risk_matrix_side(r.impact)            let ml = risk_matrix_side(r.mitigated_likelihood)            let mi = risk_matrix_side(r.mitigated_impact)            let known = ml != null && mi != null            if l == null || i == null {                { problem: `${label}: likelihood and impact must be whole numbers from 1 to 5, so it is left off the grid.` }            } else {                { id: str(r.id ?? `r${n + 1}`), mark: `R${n + 1}`, title: label, owner: str(r.owner ?? ""), l, i, ml: known ? ml : null, mi: known ? mi : null }            }        }    }    let over = len(risks) > risk_matrix_cap ? [`Only the first ${risk_matrix_cap} of ${len(risks)} risks are shown.`] : []    { rows: read | filter((r) => r.problem == null), problems: (read | filter((r) => r.problem != null) | map((r) => r.problem)) + over }}fn risk_matrix_after(r: map) -> int? = r.ml == null ? null : r.ml * r.mi// Worst first: by score, then by what is left after mitigation, then by title.fn risk_matrix_register(rows: list) -> list =    rows | sort_by((r) => r.title) | sort_by((r) => -(risk_matrix_after(r) ?? r.l * r.i)) | sort_by((r) => -(r.l * r.i))// The risks standing in a cell: { mark, title, after }, inherent ones first.fn risk_matrix_in_cell(rows: list, l: int, i: int) -> list {    let here = rows | filter((r) => r.l == l && r.i == i) | map((r) => ({ id: r.id, mark: r.mark, title: r.title, after: false }))    let later = rows | filter((r) => r.ml == l && r.mi == i) | map((r) => ({ id: r.id, mark: r.mark, title: r.title, after: true }))    here + later}fn risk_matrix_cell_text(rows: list, l: int, i: int, bands: list) -> str {    let found = risk_matrix_in_cell(rows, l, i)    let names = found | map((f) => f.after ? `${f.title} after mitigation` : f.title)    `Likelihood ${l}, impact ${i}, score ${l * i}, ${risk_matrix_band(l * i, bands).name}. ${len(found) == 0 ? "No risks." : `${len(found) == 1 ? "1 risk" : `${len(found)} risks`}: ${join(names, ", ")}.`}`}fn risk_matrix_clamp(n: int) -> int = n < 1 ? 1 : (n > 5 ? 5 : n)fn risk_matrix_public(rows: list, bands: list) -> list =    rows | map((r) => {        let after = risk_matrix_after(r)        { id: r.id, title: r.title, owner: r.owner, likelihood: r.l, impact: r.i, score: r.l * r.i, band: risk_matrix_band(r.l * r.i, bands).name,          mitigated_likelihood: r.ml, mitigated_impact: r.mi, mitigated_score: after, mitigated_band: after == null ? null : risk_matrix_band(after, bands).name }    })// How many risks sit in each band, before and after mitigation; a risk not assessed stays where it is.fn risk_matrix_tally(rows: list, bands: list) -> list =    for b in range(len(bands)) {        let name = bands[b].name        let band_of = (s: int) => risk_matrix_band(s, bands).name        { name, level: min([b, 3]), before: len(rows | filter((r) => band_of(r.l * r.i) == name)),          after: len(rows | filter((r) => band_of(risk_matrix_after(r) ?? r.l * r.i) == name)) }    }// The matrix an event came from, and a cell in it. Top-level functions, not a component's// `let`s: a `let` that touches the page is waited for, which would move the focus only once// the handler is over, too late for a key pressed straight after.fn risk_matrix_root(e) = e.target.closest(".ui-risk-matrix")fn risk_matrix_cell(root, l: int, i: int) = root.querySelector(`[data-l="${l}"][data-i="${i}"]`)component RiskMatrix(risks: list = [], bands: list = risk_matrix_bands, likelihood_labels: list = risk_matrix_likelihoods, impact_labels: list = risk_matrix_impacts,                     title: str = "Risk matrix", id: str = "risk-matrix", name: str? = null, on_change = null, class: str? = null) {    style global risk_matrix_css    state rows = risk_matrix_clean(risks).rows    state problems = risk_matrix_clean(risks).problems    state at_l = 3    state at_i = 3    state only = null    state held = null    state said = ""    let named = (n: str) => name != null ? `${name}_${n}` : n    let sorted = risk_matrix_register(rows)    let shown = only == null ? sorted : sorted | filter((r) => (r.l == only.l && r.i == only.i) || (r.ml == only.l && r.mi == only.i))    let tally = risk_matrix_tally(rows, bands)    let move_held = (l: int, i: int) => {        rows = rows | map((r) => r.id != held.id ? r : (held.which == "after" ? { ...r, ml: l, mi: i } : { ...r, l, i }))    }    let go = (e, l: int, i: int) => {        at_l = l        at_i = i        if held != null { move_held(l, i) }        risk_matrix_cell(risk_matrix_root(e), l, i).focus()    }    let pick_up = (e, r: map, which: str) => {        let l = which == "after" ? (r.ml ?? r.l) : r.l        let i = which == "after" ? (r.mi ?? r.i) : r.i        only = null        held = { id: r.id, which, title: r.title, from_l: which == "after" ? r.ml : r.l, from_i: which == "after" ? r.mi : r.i }        at_l = l        at_i = i        move_held(l, i)        said = `Moving ${r.title}. Arrow keys to move, Enter to place, Escape to cancel.`        risk_matrix_cell(risk_matrix_root(e), l, i).focus()    }    let let_go = (e) => {        let key_of = `${held.id}-${held.which}`        held = null        risk_matrix_root(e).querySelector(`[data-move="${key_of}"]`).focus()    }    let put_down = (e, l: int, i: int) => {        move_held(l, i)        let s = l * i        said = `${held.title} placed at likelihood ${l}, impact ${i}: score ${s}, ${risk_matrix_band(s, bands).name}.`        let_go(e)        if on_change != null { on_change(risk_matrix_public(rows, bands)) }    }    let cancel = (e) => {        let h = held        rows = rows | map((r) => r.id != h.id ? r : (h.which == "after" ? { ...r, ml: h.from_l, mi: h.from_i } : { ...r, l: h.from_l, i: h.from_i }))        said = `${h.title} left where it was.`        let_go(e)    }    let toggle = (l: int, i: int) => {        let same = only != null && only.l == l && only.i == i        only = same ? null : { l, i }        said = same ? "Showing every risk." : `Showing ${len(risk_matrix_in_cell(rows, l, i))} in likelihood ${l}, impact ${i}.`    }    let act = (e, l: int, i: int) => {        at_l = l        at_i = i        if held != null { put_down(e, l, i) } else { toggle(l, i) }    }    let press = (e, l: int, i: int) => {        let k = e.key        let to = k == "ArrowUp" ? [l + 1, i] : (k == "ArrowDown" ? [l - 1, i] : (k == "ArrowLeft" ? [l, i - 1] : (k == "ArrowRight" ? [l, i + 1]            : (k == "Home" ? [l, 1] : (k == "End" ? [l, 5] : null)))))        if to != null {            e.preventDefault()            go(e, risk_matrix_clamp(to[0]), risk_matrix_clamp(to[1]))        } else if k == "Enter" || k == " " {            e.preventDefault()            act(e, l, i)        } else if k == "Escape" {            if held != null { cancel(e) } else if only != null { only = null; said = "Showing every risk." }        }    }    <section class={ui_cls(["ui-risk-matrix", class])} aria-labelledby={`${id}-title`}>        <h3.ui-risk-matrix-title id={`${id}-title`}>{title}</h3>        <table.ui-risk-matrix-grid role="grid" aria-label="Risks by likelihood and impact">            <thead>                <tr><th scope="col"><span.ui-sr>Likelihood by impact</span></th>{for i in [1, 2, 3, 4, 5] => <th scope="col"><span.ui-risk-matrix-long>{impact_labels[i - 1]}</span><span.ui-risk-matrix-short>{i}</span></th>}</tr>            </thead>            <tbody>                {for l in [5, 4, 3, 2, 1] => <tr>                    <th scope="row"><span.ui-risk-matrix-long>{likelihood_labels[l - 1]}</span><span.ui-risk-matrix-short>{l}</span></th>                    {for i in [1, 2, 3, 4, 5] {                        let found = risk_matrix_in_cell(rows, l, i)                        let tone = risk_matrix_band(l * i, bands).level                        <td role="gridcell" data-l={l} data-i={i} class={ui_cls(["ui-risk-matrix-cell", `ui-risk-matrix-b${tone}`])} tabindex={l == at_l && i == at_i ? "0" : "-1"}                            aria-label={risk_matrix_cell_text(rows, l, i, bands)} aria-selected={only != null && only.l == l && only.i == i ? "true" : "false"}                            @click={(e) => { act(e, l, i) }} @keydown={(e) => { press(e, l, i) }} @focus={() => { at_l = l; at_i = i }}>                            <span.ui-risk-matrix-corner aria-hidden="true">{l * i}</span>                            <span.ui-risk-matrix-chips aria-hidden="true">                                {for f in found => <span key={`${f.id}-${f.after}`} title={f.after ? `${f.title}, after mitigation` : f.title}                                    class={ui_cls(["ui-risk-matrix-chip", f.after && "ui-risk-matrix-after", held != null && held.id == f.id && (held.which == "after") == f.after && "ui-risk-matrix-held"])}>{f.mark}</span>}                            </span>                        </td>                    }}                </tr>}            </tbody>        </table>        <ul.ui-risk-matrix-key>            <li><span.ui-risk-matrix-chip aria-hidden="true">R1</span> As it stands</li>            <li><span.ui-risk-matrix-chip.ui-risk-matrix-after aria-hidden="true">R1</span> After mitigation</li>            <li>Risks now to after:</li>            {for t in tally => <li><span class={ui_cls(["ui-risk-matrix-band", `ui-risk-matrix-b${t.level}`])}>{t.name}</span> <span data-k={`tally-${t.level}`}>{t.before} to {t.after}</span></li>}        </ul>        <div.ui-risk-matrix-live>            <p.ui-risk-matrix-say aria-live="polite">{said}</p>            {len(problems) > 0 && <ul.ui-risk-matrix-problems role="alert">{for p in problems => <li>{p}</li>}</ul>}        </div>        <div hidden={true}>            {for r in rows => <span key={r.id}>                <input type="hidden" name={named("risk_id")} value={r.id}/>                <input type="hidden" name={named("likelihood")} value={r.l}/>                <input type="hidden" name={named("impact")} value={r.i}/>                <input type="hidden" name={named("mitigated_likelihood")} value={r.ml ?? ""}/>                <input type="hidden" name={named("mitigated_impact")} value={r.mi ?? ""}/>            </span>}        </div>        <div.ui-risk-matrix-bar>            <strong>{only == null ? `${len(rows) == 1 ? "1 risk" : `${len(rows)} risks`}, worst first` : `${len(shown)} of ${len(rows)} in likelihood ${only.l}, impact ${only.i}`}</strong>            {only != null && <button.ui-risk-matrix-button type="button" @click={() => { only = null; said = "Showing every risk." }}>Show all</button>}        </div>        <div.ui-risk-matrix-scroll tabindex="0" role="group" aria-label="Risk register">            <table.ui-risk-matrix-table>                <thead><tr><th>Ref</th><th>Risk</th><th>Owner</th><th>As it stands</th><th>After mitigation</th><th>Move</th></tr></thead>                <tbody>                    {for r in shown {                        let score = r.l * r.i                        let after = risk_matrix_after(r)                        let band = risk_matrix_band(score, bands)                        <tr key={r.id} data-risk={r.id}>                            <td>{r.mark}</td>                            <td>{r.title}</td>                            <td>{r.owner}</td>                            <td data-k="now">{score} <span class={ui_cls(["ui-risk-matrix-band", `ui-risk-matrix-b${band.level}`])}>{band.name}</span></td>                            <td data-k="then">                                {after == null ? <span.ui-risk-matrix-muted>Not assessed</span>                                    : <span>{after} <span class={ui_cls(["ui-risk-matrix-band", `ui-risk-matrix-b${risk_matrix_band(after, bands).level}`])}>{risk_matrix_band(after, bands).name}</span></span>}                                {after != null && after > score && <span.ui-risk-matrix-worse>Worse than before: check this mitigation.</span>}                            </td>                            <td><span.ui-risk-matrix-actions>                                <button.ui-risk-matrix-button type="button" data-move={`${r.id}-now`} aria-pressed={held != null && held.id == r.id && held.which == "now" ? "true" : "false"}                                        aria-label={`Move ${r.title}`} @click={(e) => { pick_up(e, r, "now") }}>Move</button>                                <button.ui-risk-matrix-button type="button" data-move={`${r.id}-after`} aria-pressed={held != null && held.id == r.id && held.which == "after" ? "true" : "false"}                                        aria-label={`Move ${r.title} after mitigation`} @click={(e) => { pick_up(e, r, "after") }}>Move mitigated</button>                            </span></td>                        </tr>                    }}                </tbody>            </table>        </div>        {len(rows) == 0 && <p.ui-risk-matrix-say>No risks to show.</p>}    </section>}
Thread
27Useful? Like it, or add to its thread below.

Thread 0 posts

On RiskMatrix: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.