feat(FN-2482): add severity filtering for dashboard logs

- Add severity filter controls to the dashboard log viewer and apply filtering to rendered entries
- Extend dashboard-tui log streaming command to support severity filtering in output
- Update and stabilize dashboard and settings modal tests covering filter behavior and loading readiness
- Document the severity filter usage in CLI reference and add a changeset for @runfusion/fusion
This commit is contained in:
Fusion
2026-04-24 13:29:48 -07:00
committed by gsxdsm
parent 4335c462a6
commit 76961d41d3
8 changed files with 493 additions and 213 deletions

View File

@@ -13,6 +13,9 @@ interface DevServerLogViewerProps {
isRunning: boolean;
}
type LogSeverity = "info" | "warn" | "error";
type LogSeverityFilter = "all" | LogSeverity;
// eslint-disable-next-line no-control-regex -- ANSI escape stripping is required for readable terminal logs.
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
@@ -42,6 +45,23 @@ function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function getEntrySeverity(entry: DevServerLogEntry): LogSeverity {
if (entry.stream === "stderr") {
return "error";
}
const normalizedText = stripAnsi(entry.text).toLowerCase();
if (/\b(warn|warning)\b/.test(normalizedText)) {
return "warn";
}
if (/\b(error|fatal)\b/.test(normalizedText)) {
return "error";
}
return "info";
}
function highlightText(value: string, search: string): ReactNode {
if (!search) {
return value;
@@ -77,15 +97,24 @@ export function DevServerLogViewer({
const [isFullscreen, setIsFullscreen] = useState(false);
const [isUserScrolling, setIsUserScrolling] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [severityFilter, setSeverityFilter] = useState<LogSeverityFilter>("all");
const filteredBySeverity = useMemo(() => {
if (severityFilter === "all") {
return entries;
}
return entries.filter((entry) => getEntrySeverity(entry) === severityFilter);
}, [entries, severityFilter]);
const filteredEntries = useMemo(() => {
const normalizedSearch = searchQuery.trim().toLowerCase();
if (!normalizedSearch) {
return entries;
return filteredBySeverity;
}
return entries.filter((entry) => stripAnsi(entry.text).toLowerCase().includes(normalizedSearch));
}, [entries, searchQuery]);
return filteredBySeverity.filter((entry) => stripAnsi(entry.text).toLowerCase().includes(normalizedSearch));
}, [filteredBySeverity, searchQuery]);
const matchCount = filteredEntries.length;
@@ -159,7 +188,25 @@ export function DevServerLogViewer({
</div>
<div className="devserver-log-viewer__toolbar-actions">
<label className="devserver-log-viewer__severity" htmlFor="devserver-log-severity-filter">
<span className="visually-hidden">Filter logs by severity</span>
<select
id="devserver-log-severity-filter"
className="select devserver-log-viewer__severity-select"
value={severityFilter}
onChange={(event) => setSeverityFilter(event.target.value as LogSeverityFilter)}
data-testid="devserver-log-severity-filter"
aria-label="Filter logs by severity"
>
<option value="all">All severities</option>
<option value="info">Info</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</label>
<label className="devserver-log-viewer__search" htmlFor="devserver-log-search">
<span className="visually-hidden">Search logs</span>
<Search size={14} />
<input
id="devserver-log-search"
@@ -169,6 +216,7 @@ export function DevServerLogViewer({
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="Search logs"
data-testid="devserver-log-search-input"
aria-label="Search logs"
/>
</label>
@@ -222,7 +270,9 @@ export function DevServerLogViewer({
<p className="devserver-log-viewer__empty" data-testid="devserver-log-empty">
{entries.length === 0
? "No logs yet. Start the dev server to see output."
: "No log lines match your search."}
: (filteredBySeverity.length === 0
? "No log lines match the selected severity."
: "No log lines match your search.")}
</p>
)}