Files
bruno/packages/bruno-app/src/components/Devtools/Console/index.js
naman-bruno b9da31d24e added: status bar & console (#4922)
* added: status bar & console
2025-07-14 19:05:57 +05:30

531 lines
16 KiB
JavaScript

import React, { useEffect, useRef, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import ReactJson from 'react-json-view';
import { useTheme } from 'providers/Theme';
import {
IconX,
IconTrash,
IconFilter,
IconAlertTriangle,
IconAlertCircle,
IconBug,
IconCode,
IconChevronDown,
IconTerminal2,
IconNetwork
} from '@tabler/icons';
import {
closeConsole,
clearLogs,
updateFilter,
toggleAllFilters,
setActiveTab,
clearDebugErrors,
updateNetworkFilter,
toggleAllNetworkFilters
} from 'providers/ReduxStore/slices/logs';
import NetworkTab from './NetworkTab';
import RequestDetailsPanel from './RequestDetailsPanel';
import DebugTab from './DebugTab';
import ErrorDetailsPanel from './ErrorDetailsPanel';
import StyledWrapper from './StyledWrapper';
const LogIcon = ({ type }) => {
const iconProps = { size: 16, strokeWidth: 1.5 };
switch (type) {
case 'error':
return <IconAlertCircle className="log-icon error" {...iconProps} />;
case 'warn':
return <IconAlertTriangle className="log-icon warn" {...iconProps} />;
case 'info':
return <IconAlertTriangle className="log-icon info" {...iconProps} />;
case 'debug':
return <IconBug className="log-icon debug" {...iconProps} />;
default:
return <IconCode className="log-icon log" {...iconProps} />;
}
};
const LogTimestamp = ({ timestamp }) => {
const date = new Date(timestamp);
const time = date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
return <span className="log-timestamp">{time}</span>;
};
const LogMessage = ({ message, args }) => {
const { displayedTheme } = useTheme();
const formatMessage = (msg, originalArgs) => {
if (originalArgs && originalArgs.length > 0) {
return originalArgs.map((arg, index) => {
if (typeof arg === 'object' && arg !== null) {
return (
<div key={index} className="log-object">
<ReactJson
src={arg}
theme={displayedTheme === 'light' ? 'rjv-default' : 'monokai'}
iconStyle="triangle"
indentWidth={2}
collapsed={1}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
name={false}
style={{
backgroundColor: 'transparent',
fontSize: '12px',
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
}}
/>
</div>
);
}
return String(arg);
});
}
return msg;
};
const formattedMessage = formatMessage(message, args);
return (
<span className="log-message">
{Array.isArray(formattedMessage) ? formattedMessage.map((item, index) => (
<span key={index}>{item} </span>
)) : formattedMessage}
</span>
);
};
const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const allFiltersEnabled = Object.values(filters).every(f => f);
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="filter-dropdown" ref={dropdownRef}>
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter logs by type"
>
<IconFilter size={16} strokeWidth={1.5} />
<span className="filter-summary">
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-header">
<span>Filter by Type</span>
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.entries(filters).map(([filterType, enabled]) => (
<label key={filterType} className="filter-option">
<input
type="checkbox"
checked={enabled}
onChange={(e) => onFilterToggle(filterType, e.target.checked)}
/>
<div className="filter-option-content">
<LogIcon type={filterType} />
<span className="filter-option-label">{filterType}</span>
<span className="filter-option-count">({logCounts[filterType] || 0})</span>
</div>
</label>
))}
</div>
</div>
)}
</div>
);
};
const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const allFiltersEnabled = Object.values(filters).every(f => f);
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
const getMethodColor = (method) => {
switch (method?.toUpperCase()) {
case 'GET': return '#10b981';
case 'POST': return '#8b5cf6';
case 'PUT': return '#f59e0b';
case 'DELETE': return '#ef4444';
case 'PATCH': return '#06b6d4';
case 'HEAD': return '#6b7280';
case 'OPTIONS': return '#84cc16';
default: return '#6b7280';
}
};
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="filter-dropdown" ref={dropdownRef}>
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter requests by method"
>
<IconFilter size={16} strokeWidth={1.5} />
<span className="filter-summary">
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-header">
<span>Filter by Method</span>
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.entries(filters).map(([method, enabled]) => (
<label key={method} className="filter-option">
<input
type="checkbox"
checked={enabled}
onChange={(e) => onFilterToggle(method, e.target.checked)}
/>
<div className="filter-option-content">
<span className="method-badge" style={{ backgroundColor: getMethodColor(method) }}>
{method}
</span>
<span className="filter-option-label">{method}</span>
<span className="filter-option-count">({requestCounts[method] || 0})</span>
</div>
</label>
))}
</div>
</div>
)}
</div>
);
};
const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onClearLogs }) => {
const logsEndRef = useRef(null);
const prevLogsCountRef = useRef(0);
useEffect(() => {
// Only scroll when new logs are added, not when switching tabs
if (logsEndRef.current && logs.length > prevLogsCountRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'auto' });
}
prevLogsCountRef.current = logs.length;
}, [logs]);
const filteredLogs = logs.filter(log => filters[log.type]);
return (
<div className="tab-content">
<div className="tab-content-area">
{filteredLogs.length === 0 ? (
<div className="console-empty">
<IconTerminal2 size={48} strokeWidth={1} />
<p>No logs to display</p>
<span>Logs will appear here as your application runs</span>
</div>
) : (
<div className="logs-container">
{filteredLogs.map((log) => (
<div key={log.id} className={`log-entry ${log.type}`}>
<div className="log-meta">
<LogTimestamp timestamp={log.timestamp} />
<LogIcon type={log.type} />
</div>
<LogMessage message={log.message} args={log.args} />
</div>
))}
<div ref={logsEndRef} />
</div>
)}
</div>
</div>
);
};
const Console = () => {
const dispatch = useDispatch();
const { logs, filters, activeTab, selectedRequest, selectedError, networkFilters, debugErrors } = useSelector(state => state.logs);
const collections = useSelector(state => state.collections.collections);
const consoleRef = useRef(null);
const logCounts = logs.reduce((counts, log) => {
counts[log.type] = (counts[log.type] || 0) + 1;
return counts;
}, {});
const allRequests = React.useMemo(() => {
const requests = [];
collections.forEach(collection => {
if (collection.timeline) {
collection.timeline
.filter(entry => entry.type === 'request')
.forEach(entry => {
requests.push({
...entry,
collectionName: collection.name,
collectionUid: collection.uid
});
});
}
});
return requests.sort((a, b) => a.timestamp - b.timestamp);
}, [collections]);
const filteredLogs = logs.filter(log => filters[log.type]);
const filteredRequests = allRequests.filter(request => {
const method = request.data?.request?.method?.toUpperCase() || 'GET';
return networkFilters[method];
});
const requestCounts = allRequests.reduce((counts, request) => {
const method = request.data?.request?.method?.toUpperCase() || 'GET';
counts[method] = (counts[method] || 0) + 1;
return counts;
}, {});
const handleFilterToggle = (filterType, enabled) => {
dispatch(updateFilter({ filterType, enabled }));
};
const handleNetworkFilterToggle = (method, enabled) => {
dispatch(updateNetworkFilter({ method, enabled }));
};
const handleClearLogs = () => {
dispatch(clearLogs());
};
const handleClearDebugErrors = () => {
dispatch(clearDebugErrors());
};
const handlecloseConsole = () => {
dispatch(closeConsole());
};
const handleToggleAllFilters = (enabled) => {
dispatch(toggleAllFilters(enabled));
};
const handleToggleAllNetworkFilters = (enabled) => {
dispatch(toggleAllNetworkFilters(enabled));
};
const handleTabChange = (tab) => {
dispatch(setActiveTab(tab));
};
const renderTabContent = () => {
switch (activeTab) {
case 'console':
return (
<ConsoleTab
logs={logs}
filters={filters}
logCounts={logCounts}
onFilterToggle={handleFilterToggle}
onToggleAll={handleToggleAllFilters}
onClearLogs={handleClearLogs}
/>
);
case 'network':
return <NetworkTab />;
case 'debug':
return <DebugTab />;
default:
return (
<ConsoleTab
logs={logs}
filters={filters}
logCounts={logCounts}
onFilterToggle={handleFilterToggle}
onToggleAll={handleToggleAllFilters}
onClearLogs={handleClearLogs}
/>
);
}
};
const renderTabControls = () => {
switch (activeTab) {
case 'console':
return (
<div className="tab-controls">
<div className="filter-controls">
<FilterDropdown
filters={filters}
logCounts={logCounts}
onFilterToggle={handleFilterToggle}
onToggleAll={handleToggleAllFilters}
/>
</div>
<div className="action-controls">
<button
className="control-button"
onClick={handleClearLogs}
title="Clear all logs"
>
<IconTrash size={16} strokeWidth={1.5} />
</button>
</div>
</div>
);
case 'network':
return (
<div className="tab-controls">
<div className="filter-controls">
<NetworkFilterDropdown
filters={networkFilters}
requestCounts={requestCounts}
onFilterToggle={handleNetworkFilterToggle}
onToggleAll={handleToggleAllNetworkFilters}
/>
</div>
</div>
);
case 'debug':
return (
<div className="tab-controls">
<div className="action-controls">
{debugErrors.length > 0 && (
<button
className="control-button"
onClick={handleClearDebugErrors}
title="Clear all errors"
>
<IconTrash size={16} strokeWidth={1.5} />
</button>
)}
</div>
</div>
);
default:
return null;
}
};
return (
<StyledWrapper ref={consoleRef}>
<div
className="console-resize-handle"
/>
<div className="console-header">
<div className="console-tabs">
<button
className={`console-tab ${activeTab === 'console' ? 'active' : ''}`}
onClick={() => handleTabChange('console')}
>
<IconTerminal2 size={16} strokeWidth={1.5} />
<span>Console</span>
</button>
<button
className={`console-tab ${activeTab === 'network' ? 'active' : ''}`}
onClick={() => handleTabChange('network')}
>
<IconNetwork size={16} strokeWidth={1.5} />
<span>Network</span>
</button>
<button
className={`console-tab ${activeTab === 'debug' ? 'active' : ''}`}
onClick={() => handleTabChange('debug')}
>
<IconBug size={16} strokeWidth={1.5} />
<span>Debug</span>
</button>
</div>
<div className="console-controls">
{renderTabControls()}
<button
className="control-button close-button"
onClick={handlecloseConsole}
title="Close console"
>
<IconX size={16} strokeWidth={1.5} />
</button>
</div>
</div>
<div className="console-content">
{activeTab === 'network' && selectedRequest ? (
<div className="network-with-details">
<div className="network-main">
{renderTabContent()}
</div>
<RequestDetailsPanel />
</div>
) : activeTab === 'debug' && selectedError ? (
<div className="debug-with-details">
<div className="debug-main">
{renderTabContent()}
</div>
<ErrorDetailsPanel />
</div>
) : (
renderTabContent()
)}
</div>
</StyledWrapper>
);
};
export default Console;