Skip to content

Commit b02faaf

Browse files
committed
Improve UI.
1 parent b03a94f commit b02faaf

5 files changed

Lines changed: 387 additions & 117 deletions

File tree

cli.tsx

Lines changed: 216 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,80 @@
11
#!/usr/bin/env node
2-
import React, { useEffect, useState } from "react";
2+
import React, { useEffect, useMemo, useRef, useState } from "react";
33
import { render, Box, Text, useApp } from "ink";
4+
import chalk from "chalk";
45
import {
56
Crawler,
67
CrawlConfig,
78
CrawlProgress,
9+
ScanProgress,
810
CrawlSummary,
11+
CrawlStages,
12+
Progress,
913
} from "./crawler.js";
1014

1115
import { argsToConfig } from "./config.js";
1216

13-
interface State {
14-
progress: CrawlProgress;
15-
summary?: CrawlSummary;
16-
startTime: number;
17-
}
18-
1917
const config = argsToConfig();
2018

2119
const Dashboard: React.FC<{ cfg: CrawlConfig }> = ({ cfg }) => {
2220
const { exit } = useApp();
23-
const [state, setState] = useState<State>({
24-
progress: { phase: "init" },
25-
startTime: Date.now(),
21+
const [phase, setPhase] = useState<"scanning" | "crawling" | "error">(
22+
"scanning",
23+
);
24+
const [scanProgress, setScanProgress] = useState<Partial<ScanProgress>>({
25+
queued: 0,
26+
finished: 0,
27+
url: "",
2628
});
29+
const [crawlProgress, setCrawlProgress] = useState<CrawlProgress>({
30+
totalPages: 0,
31+
finishedPages: 0,
32+
url: "",
33+
deviceIndex: 0,
34+
stageIndex: CrawlStages.Load,
35+
});
36+
const [summary, setSummary] = useState<CrawlSummary | null>(null);
37+
const startTimeRef = useRef<number>(Date.now());
2738

2839
useEffect(() => {
2940
const crawler = new Crawler(cfg);
30-
const onProgress = (p: Partial<CrawlProgress>) => {
31-
setState((s) => ({ ...s, progress: { ...s.progress, ...p } }));
41+
const onProgress = (p: Progress) => {
42+
if (p.phase) setPhase(p.phase);
43+
if (p.message) {
44+
switch (p.message.level) {
45+
case "warning":
46+
console.log(chalk.yellow(p.message.text));
47+
break;
48+
case "error":
49+
console.log(chalk.red(p.message.text));
50+
break;
51+
default: // info
52+
console.log(p.message.text);
53+
break;
54+
}
55+
}
56+
if (p.scanProgress) {
57+
setScanProgress((s) => ({ ...s, ...p.scanProgress }));
58+
}
59+
if (p.crawlProgress) {
60+
setCrawlProgress((s) => ({ ...s, ...p.crawlProgress }));
61+
}
3262
};
3363
crawler.on("progress", onProgress);
34-
crawler.start().then((summary) => {
35-
setState((s) => ({ ...s, summary }));
36-
setTimeout(() => {
37-
exit();
38-
process.exit(0);
39-
}, 200); // allow last frame render
40-
});
64+
crawler
65+
.start()
66+
.then((s: CrawlSummary) => {
67+
setSummary(s);
68+
setTimeout(() => {
69+
exit();
70+
process.exit(0);
71+
}, 200); // allow last frame render
72+
})
73+
.catch((e: any) => {
74+
setPhase("error");
75+
});
4176
const handleSig = () => {
42-
//console.log(isRawModeSupported);
43-
//if (isRawModeSupported) setRawMode(false);
44-
//spawnSync("stty", ["-a"], { stdio: "inherit" }); // reset terminal after raw mode
45-
// the ^[[A on terminal after ctrl-c is caused by node
46-
// https://github.com/nodejs/node/issues/41143
4777
crawler.stop();
48-
//rl.close();
49-
//exit();
5078
process.exit(0);
5179
};
5280
process.on("SIGINT", handleSig);
@@ -56,11 +84,8 @@ const Dashboard: React.FC<{ cfg: CrawlConfig }> = ({ cfg }) => {
5684
});
5785
// Extra cleanup on unhandled rejections
5886
process.on("unhandledRejection", (r) => {
59-
setState((s) => ({
60-
...s,
61-
progress: { ...s.progress, phase: "error", message: String(r) },
62-
}));
63-
crawler.stop();
87+
setPhase("error");
88+
handleSig();
6489
});
6590
return () => {
6691
crawler.stop();
@@ -71,58 +96,172 @@ const Dashboard: React.FC<{ cfg: CrawlConfig }> = ({ cfg }) => {
7196
};
7297
}, [cfg, exit]);
7398

74-
const { progress, summary, startTime } = state;
75-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
99+
// UI helpers
100+
const stageTexts: Array<NonNullable<string>> = [
101+
"Load",
102+
"Cascade",
103+
"Cascade Pseudo",
104+
];
105+
106+
const devices = useMemo(
107+
() => cfg.deviceWidths.map((w, i) => ({ index: i, width: w })),
108+
[cfg.deviceWidths],
109+
);
110+
const activeDeviceIndex = crawlProgress.deviceIndex;
111+
const pagesTotal = crawlProgress.totalPages;
112+
const finishedPages = crawlProgress.finishedPages;
113+
const currentStageIndex = crawlProgress.stageIndex;
114+
const elementsProgressPercent =
115+
(crawlProgress.totalElements || 0) > 0
116+
? Math.min(
117+
100,
118+
Math.round(
119+
((crawlProgress.processedElements ?? 0) /
120+
(crawlProgress.totalElements || 1)) *
121+
100,
122+
),
123+
)
124+
: undefined;
125+
126+
const ConfigEntry = ({
127+
name,
128+
value,
129+
valueColor,
130+
}: {
131+
name: string;
132+
value: string;
133+
valueColor?: string;
134+
}) => (
135+
<Text>
136+
{(name + ":").padEnd(16)} <Text color={valueColor}>{value}</Text>
137+
</Text>
138+
);
76139

77140
return (
78-
<Box
79-
flexDirection="column"
80-
padding={1}
81-
borderStyle="round"
82-
borderColor="cyan"
83-
>
84-
<Text>
85-
Crawl <Text color="green">{cfg.url}</Text> {"->"}{" "}
86-
<Text color="yellow">{cfg.outDir}</Text> elapsed {elapsed}s
87-
</Text>
88-
<Box>
89-
<Box flexDirection="column" width={50} marginRight={2}>
90-
<Text>
91-
Breakpoints:{" "}
92-
{cfg.breakpoints.length ? cfg.breakpoints.join(",") : "none"}
93-
</Text>
94-
{progress.currentUrl && <Text>URL: {progress.currentUrl}</Text>}
95-
{progress.queueSize !== undefined && (
96-
<Text>Queue: {progress.queueSize}</Text>
97-
)}
98-
{progress.visitedCount !== undefined && (
99-
<Text>Visited: {progress.visitedCount}</Text>
100-
)}
101-
<Text>Phase: {progress.phase}</Text>
102-
{progress.totalElements !== undefined && (
141+
<>
142+
{/* Configs Header */}
143+
<Box
144+
flexDirection="column"
145+
marginBottom={1}
146+
padding={1}
147+
paddingTop={0}
148+
borderStyle="round"
149+
borderColor="cyan"
150+
>
151+
<Text color={"cyan"} bold>
152+
Configs:
153+
</Text>
154+
155+
<ConfigEntry name="Site URL" value={cfg.url} valueColor="green" />
156+
<ConfigEntry name="Output Dir" value={cfg.outDir} valueColor="yellow" />
157+
<ConfigEntry name="Browser" value={cfg.browserPath} />
158+
<ConfigEntry
159+
name="Breakpoints"
160+
value={cfg.breakpoints.length ? cfg.breakpoints.join(", ") : "none"}
161+
valueColor="red"
162+
/>
163+
<ConfigEntry name="Device widths" value={cfg.deviceWidths.join(", ")} />
164+
<ConfigEntry name="Device Height" value={String(cfg.screenHeight)} />
165+
<ConfigEntry name="Scale" value={String(cfg.deviceScaleFactor)} />
166+
<ConfigEntry
167+
name="Delay after nav"
168+
value={String(cfg.delayAfterNavigateMs) + "ms"}
169+
/>
170+
<Text>
171+
Other Settings:{" "}
172+
<Text color={cfg.recursive ? "" : "gray"}>Recursive</Text>{" "}
173+
<Text color={cfg.browserScan ? "" : "gray"}>BrowserScan</Text>{" "}
174+
<Text color={cfg.headless ? "" : "gray"}>Headless</Text>
175+
</Text>
176+
</Box>
177+
178+
{/* Progress */}
179+
{phase === "scanning" && (
180+
<Box flexDirection="column" marginBottom={1}>
181+
{scanProgress.url && (
103182
<Text>
104-
Elements: {progress.processedElements ?? 0}/
105-
{progress.totalElements}
183+
Scanning: <Text color="magenta">{scanProgress.url}</Text>
106184
</Text>
107185
)}
108-
{progress.resourcesDownloaded !== undefined && (
109-
<Text>Resources: {progress.resourcesDownloaded}</Text>
110-
)}
111-
{progress.fontsExtracted !== undefined && (
112-
<Text>Fonts: {progress.fontsExtracted}</Text>
113-
)}
114-
{progress.message && <Text color="gray">{progress.message}</Text>}
115-
{summary && (
116-
<>
117-
<Text color="green">
118-
Done. Pages: {summary.visited.length} Fonts:{" "}
119-
{summary.fontsCssCount}
120-
</Text>
121-
</>
122-
)}
186+
<Text>
187+
Queue: {scanProgress.queued ?? 0} | Visited:{" "}
188+
{scanProgress.finished ?? 0}
189+
</Text>
123190
</Box>
124-
</Box>
125-
</Box>
191+
)}
192+
193+
{phase === "crawling" && (
194+
<>
195+
<Box flexDirection="column" marginBottom={1}>
196+
<Text>
197+
Page ({finishedPages + 1}/{pagesTotal}):{" "}
198+
<Text color="magenta">{crawlProgress.url}</Text>
199+
</Text>
200+
</Box>
201+
<Box flexDirection="column" marginBottom={1}>
202+
<Box rowGap={1}>
203+
{devices.map((d) => (
204+
<Text
205+
key={d.index}
206+
backgroundColor={d.index === activeDeviceIndex ? "cyan" : ""}
207+
color={
208+
d.index < activeDeviceIndex
209+
? "green"
210+
: d.index === activeDeviceIndex
211+
? "black"
212+
: "gray"
213+
}
214+
>
215+
{` ${d.width}px `}
216+
</Text>
217+
))}
218+
</Box>
219+
<Box marginBottom={1}>
220+
{stageTexts.map((stgText, i) => {
221+
const isActive = currentStageIndex === i;
222+
const color = isActive
223+
? ""
224+
: (currentStageIndex ?? -1) > i
225+
? "green"
226+
: "gray";
227+
const decoL = isActive ? "▶" : "";
228+
return (
229+
<Box key={stgText} marginRight={2}>
230+
<Text>{decoL}</Text>
231+
<Text> </Text>
232+
<Text color={color}>{stgText}</Text>
233+
</Box>
234+
);
235+
})}
236+
</Box>
237+
{elementsProgressPercent !== undefined &&
238+
(currentStageIndex === CrawlStages.Cascade ||
239+
currentStageIndex === CrawlStages.CascadePseudo) && (
240+
<Box paddingLeft={2}>
241+
<Text>
242+
{stageTexts[currentStageIndex]} |{" "}
243+
<Text color="yellow">
244+
{elementsProgressPercent}% (
245+
{crawlProgress.processedElements}/
246+
{crawlProgress.totalElements})
247+
</Text>
248+
</Text>
249+
{/*
250+
<ProgressBar value={elementsProgress} />
251+
*/}
252+
</Box>
253+
)}
254+
</Box>
255+
</>
256+
)}
257+
258+
{/* Footer / messages */}
259+
{summary && (
260+
<Text color="green">
261+
Done. Pages: {summary.visited.length} Fonts: {summary.fontsCssCount}
262+
</Text>
263+
)}
264+
</>
126265
);
127266
};
128267

0 commit comments

Comments
 (0)