Skip to content

Commit d5f060a

Browse files
authored
Simplify front end error UI (#80)
* Replace React error overlay with plain vanilla JS * Fix keydown listener leak and deduplicate prop error message logic * Remove ui-checks GitHub Action
1 parent 7590b1d commit d5f060a

33 files changed

Lines changed: 165 additions & 31927 deletions

.github/workflows/tests.yml

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,3 @@ jobs:
4545
- name: Run tests
4646
run: |
4747
tox
48-
49-
ui-checks:
50-
runs-on: ubuntu-latest
51-
defaults:
52-
run:
53-
working-directory: ./ui
54-
55-
steps:
56-
- uses: actions/checkout@v6
57-
58-
- name: Set up Node.js 18
59-
uses: actions/setup-node@v6
60-
with:
61-
node-version: "18"
62-
63-
- name: Install Node.js dependencies
64-
run: npm ci --no-option --no-audit --progress=false
65-
66-
- name: Run tests
67-
run: npm run test
68-
69-
- name: Build error UI
70-
run: npm run build

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,3 @@ cython_debug/
142142
.vim
143143
.nvimrc
144144
Session.vim
145-
slippers/static

slippers/static/slippers/main.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
(function () {
2+
"use strict";
3+
4+
// ---------------------------------------------------------------------------
5+
// Escaping function for HTML content
6+
// ---------------------------------------------------------------------------
7+
8+
function esc(value) {
9+
return String(value)
10+
.replace(/&/g, "&")
11+
.replace(/</g, "&lt;")
12+
.replace(/>/g, "&gt;")
13+
.replace(/"/g, "&quot;")
14+
.replace(/'/g, "&#39;");
15+
}
16+
17+
// ---------------------------------------------------------------------------
18+
// Styles
19+
// ---------------------------------------------------------------------------
20+
21+
const STYLES = `
22+
#slippers_errors_ui_root {
23+
position: relative;
24+
z-index: 99999;
25+
}
26+
#slippers-overlay {
27+
position: fixed; inset: 0;
28+
background: rgba(0,0,0,0.92);
29+
color: #e4e4e7;
30+
font-family: sans-serif;
31+
padding: 3rem;
32+
overflow-y: auto;
33+
z-index: 99999;
34+
}
35+
#slippers-overlay h1 { color: #f87171; font-size: 1.5rem; margin: 0 0 2rem; }
36+
.slippers-close {
37+
margin-left: auto; padding: 0.5rem 1rem;
38+
background: #000; color: #fff; border: 1px solid #555; cursor: pointer;
39+
}
40+
.slippers-header { display: flex; align-items: flex-start; }
41+
.slippers-error-box { margin-bottom: 2rem; }
42+
.slippers-tag-label { font-size: 1.1rem; font-weight: bold; }
43+
.slippers-location { color: #a1a1aa; font-family: monospace; word-break: break-all; }
44+
.slippers-error-list {
45+
margin-top: 1rem; padding: 1rem;
46+
background: #27272a; list-style: disc; list-style-position: inside;
47+
}
48+
.slippers-error-list li + li { margin-top: 0.5rem; }
49+
.slippers-var { color: #818cf8; font-family: monospace; }
50+
.slippers-tag { color: #fb923c; font-family: monospace; }
51+
.slippers-type { color: #2dd4bf; font-family: monospace; }
52+
`;
53+
54+
// ---------------------------------------------------------------------------
55+
// Components
56+
// ---------------------------------------------------------------------------
57+
58+
function formatPropErrorMessage(propError, tagName) {
59+
if (propError.error === "invalid")
60+
return `Invalid prop ${propError.name} set on ${tagName}. Expected ${propError.expected}, got ${propError.actual}.`;
61+
if (propError.error === "missing")
62+
return `Required prop ${propError.name} of type ${propError.expected} not set on ${tagName}.`;
63+
if (propError.error === "extra")
64+
return `Extra prop ${propError.name} of type ${propError.actual} set on ${tagName}.`;
65+
return propError.error;
66+
}
67+
68+
function renderPropError(propError, tagName) {
69+
const name = `<span class="slippers-var">${esc(propError.name)}</span>`;
70+
const tag = `<span class="slippers-tag">${esc(tagName)}</span>`;
71+
const exp = `<span class="slippers-type">${esc(propError.expected)}</span>`;
72+
const act = `<span class="slippers-type">${esc(propError.actual)}</span>`;
73+
74+
if (propError.error === "invalid")
75+
return `Invalid prop ${name} set on ${tag}. Expected ${exp}, got ${act}.`;
76+
if (propError.error === "missing")
77+
return `Required prop ${name} of type ${exp} not set on ${tag}.`;
78+
if (propError.error === "extra")
79+
return `Extra prop ${name} of type ${act} set on ${tag}.`;
80+
return esc(propError.error);
81+
}
82+
83+
function renderErrorBox(slippersError) {
84+
const items = slippersError.errors
85+
.map(
86+
(e) => `<li>${renderPropError(e, slippersError.tag_name)}</li>`,
87+
)
88+
.join("");
89+
90+
return `
91+
<div class="slippers-error-box">
92+
<p class="slippers-tag-label">${esc(slippersError.tag_name)}</p>
93+
<p class="slippers-location">${esc(slippersError.template_name)}:${esc(slippersError.lineno)}</p>
94+
<ul class="slippers-error-list">${items}</ul>
95+
</div>`;
96+
}
97+
98+
function renderOverlay(errors) {
99+
const boxes = errors.map(renderErrorBox).join("");
100+
return `
101+
<style>${STYLES}</style>
102+
<div id="slippers-overlay">
103+
<div class="slippers-header">
104+
<h1>Slippers: Failed prop types</h1>
105+
<button class="slippers-close" id="slippers-close-btn">Close</button>
106+
</div>
107+
${boxes}
108+
</div>`;
109+
}
110+
111+
// ---------------------------------------------------------------------------
112+
// Console reporting
113+
// ---------------------------------------------------------------------------
114+
115+
function reportErrorsToConsole(errors) {
116+
console.group("Slippers: Failed prop types");
117+
errors.forEach(function (slippersError) {
118+
console.groupCollapsed(
119+
slippersError.tag_name +
120+
" in " +
121+
slippersError.template_name +
122+
":" +
123+
slippersError.lineno,
124+
);
125+
slippersError.errors.forEach(function (propError) {
126+
console.error(formatPropErrorMessage(propError, slippersError.tag_name));
127+
});
128+
console.groupEnd();
129+
});
130+
console.groupEnd();
131+
}
132+
133+
// ---------------------------------------------------------------------------
134+
// Entry point
135+
// ---------------------------------------------------------------------------
136+
137+
document.addEventListener("DOMContentLoaded", function () {
138+
var errors = window.slippersPropErrors || [];
139+
var configEl = document.getElementById("slippers_type_checking_output");
140+
var config = JSON.parse((configEl && configEl.textContent) || "[]");
141+
142+
if (errors.length === 0) return;
143+
144+
if (config.includes("console")) reportErrorsToConsole(errors);
145+
146+
if (config.includes("overlay")) {
147+
var root = document.getElementById("slippers_errors_ui_root");
148+
if (!root) return;
149+
150+
root.innerHTML = renderOverlay(errors);
151+
152+
function closeOverlay() {
153+
root.innerHTML = "";
154+
document.removeEventListener("keydown", onKey);
155+
}
156+
157+
function onKey(ev) {
158+
if (ev.key === "Escape") closeOverlay();
159+
}
160+
161+
document.getElementById("slippers-close-btn").addEventListener("click", closeOverlay);
162+
document.addEventListener("keydown", onKey);
163+
}
164+
});
165+
})();

slippers/templates/slippers/overlay.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22
{% if SLIPPERS_RUNTIME_TYPE_CHECKING %}
33
<div id="slippers_errors_ui_root"></div>
44
{{ SLIPPERS_TYPE_CHECKING_OUTPUT|json_script:"slippers_type_checking_output" }}
5-
<link rel="stylesheet" href="{% static 'slippers/main.css' %}">
65
<script src="{% static "slippers/main.js" %}"></script>
76
{% endif %}

ui/.gitignore

Lines changed: 0 additions & 26 deletions
This file was deleted.

ui/.nvmrc

Lines changed: 0 additions & 1 deletion
This file was deleted.

ui/README.md

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)