Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
472
admin-ui/js/components/tools/ds-network-monitor.js
Normal file
472
admin-ui/js/components/tools/ds-network-monitor.js
Normal file
@@ -0,0 +1,472 @@
|
||||
/**
|
||||
* ds-network-monitor.js
|
||||
* Network request monitoring and debugging
|
||||
*
|
||||
* REFACTORED: DSS-compliant version using DSBaseTool + table-template.js
|
||||
* - Extends DSBaseTool for Shadow DOM, AbortController, and standardized lifecycle
|
||||
* - Uses table-template.js for DSS-compliant table rendering (NO inline events/styles)
|
||||
* - Event delegation pattern for all interactions
|
||||
* - Logger utility instead of console.*
|
||||
*
|
||||
* Reference: .knowledge/dss-coding-standards.json
|
||||
*/
|
||||
|
||||
import DSBaseTool from '../base/ds-base-tool.js';
|
||||
import toolBridge from '../../services/tool-bridge.js';
|
||||
import { ComponentHelpers } from '../../utils/component-helpers.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { createTableView, setupTableEvents, createStatsCard } from '../../templates/table-template.js';
|
||||
|
||||
class DSNetworkMonitor extends DSBaseTool {
|
||||
constructor() {
|
||||
super();
|
||||
this.requests = [];
|
||||
this.filteredRequests = [];
|
||||
this.filterUrl = '';
|
||||
this.filterType = 'all';
|
||||
this.autoRefresh = false;
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.loadRequests();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component (required by DSBaseTool)
|
||||
*/
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.network-monitor-container {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.filter-input:focus {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 150px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.auto-refresh-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Badge styles */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: rgba(75, 181, 211, 0.2);
|
||||
color: #4bb5d3;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: rgba(137, 209, 133, 0.2);
|
||||
color: #89d185;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: rgba(206, 145, 120, 0.2);
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background: rgba(244, 135, 113, 0.2);
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: 'Courier New', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 12px;
|
||||
padding: 8px;
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.info-count {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="network-monitor-container">
|
||||
<!-- Filter Controls -->
|
||||
<div class="filter-controls">
|
||||
<input
|
||||
type="text"
|
||||
id="network-filter"
|
||||
placeholder="Filter by URL or method..."
|
||||
class="filter-input"
|
||||
/>
|
||||
<select id="network-type-filter" class="filter-select">
|
||||
<option value="all">All Types</option>
|
||||
</select>
|
||||
<label class="auto-refresh-label">
|
||||
<input type="checkbox" id="auto-refresh-toggle" />
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button
|
||||
id="network-refresh-btn"
|
||||
data-action="refresh"
|
||||
class="refresh-btn"
|
||||
type="button"
|
||||
aria-label="Refresh network requests">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper" id="network-content">
|
||||
<div class="loading">
|
||||
<div class="loading-spinner">⏳</div>
|
||||
<div>Initializing...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners (required by DSBaseTool)
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// EVENT-002: Event delegation
|
||||
this.delegateEvents('.network-monitor-container', 'click', (action, e) => {
|
||||
if (action === 'refresh') {
|
||||
this.loadRequests();
|
||||
}
|
||||
});
|
||||
|
||||
// Filter input with debounce
|
||||
const filterInput = this.$('#network-filter');
|
||||
if (filterInput) {
|
||||
const debouncedFilter = ComponentHelpers.debounce((term) => {
|
||||
this.filterUrl = term.toLowerCase();
|
||||
this.applyFilters();
|
||||
}, 300);
|
||||
|
||||
this.bindEvent(filterInput, 'input', (e) => debouncedFilter(e.target.value));
|
||||
}
|
||||
|
||||
// Type filter
|
||||
const typeFilter = this.$('#network-type-filter');
|
||||
if (typeFilter) {
|
||||
this.bindEvent(typeFilter, 'change', (e) => {
|
||||
this.filterType = e.target.value;
|
||||
this.applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-refresh toggle
|
||||
const autoRefreshToggle = this.$('#auto-refresh-toggle');
|
||||
if (autoRefreshToggle) {
|
||||
this.bindEvent(autoRefreshToggle, 'change', (e) => {
|
||||
this.autoRefresh = e.target.checked;
|
||||
if (this.autoRefresh) {
|
||||
this.refreshInterval = setInterval(() => this.loadRequests(), 2000);
|
||||
logger.debug('[DSNetworkMonitor] Auto-refresh enabled');
|
||||
} else {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
logger.debug('[DSNetworkMonitor] Auto-refresh disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadRequests() {
|
||||
const content = this.$('#network-content');
|
||||
if (!content) return;
|
||||
|
||||
// Only show loading on first load
|
||||
if (this.requests.length === 0) {
|
||||
content.innerHTML = '<div class="loading"><div class="loading-spinner">⏳</div><div>Loading network requests...</div></div>';
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await toolBridge.getNetworkRequests(null, 100);
|
||||
|
||||
if (result && result.requests) {
|
||||
this.requests = result.requests;
|
||||
this.updateTypeFilter();
|
||||
this.applyFilters();
|
||||
logger.debug('[DSNetworkMonitor] Loaded requests', { count: this.requests.length });
|
||||
} else {
|
||||
this.requests = [];
|
||||
content.innerHTML = '<div class="table-empty"><div class="table-empty-icon">🌐</div><div class="table-empty-text">No network requests captured</div></div>';
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[DSNetworkMonitor] Failed to load network requests', error);
|
||||
content.innerHTML = ComponentHelpers.renderError('Failed to load network requests', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateTypeFilter() {
|
||||
const typeFilter = this.$('#network-type-filter');
|
||||
if (!typeFilter) return;
|
||||
|
||||
const types = this.getResourceTypes();
|
||||
const currentValue = typeFilter.value;
|
||||
|
||||
typeFilter.innerHTML = `
|
||||
<option value="all">All Types</option>
|
||||
${types.map(type => `<option value="${type}" ${type === currentValue ? 'selected' : ''}>${type}</option>`).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
applyFilters() {
|
||||
let filtered = [...this.requests];
|
||||
|
||||
// Filter by URL
|
||||
if (this.filterUrl) {
|
||||
filtered = filtered.filter(req =>
|
||||
req.url.toLowerCase().includes(this.filterUrl) ||
|
||||
req.method.toLowerCase().includes(this.filterUrl)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
if (this.filterType !== 'all') {
|
||||
filtered = filtered.filter(req => req.resourceType === this.filterType);
|
||||
}
|
||||
|
||||
this.filteredRequests = filtered;
|
||||
this.renderRequests();
|
||||
}
|
||||
|
||||
getResourceTypes() {
|
||||
if (!this.requests) return [];
|
||||
const types = new Set(this.requests.map(r => r.resourceType).filter(Boolean));
|
||||
return Array.from(types).sort();
|
||||
}
|
||||
|
||||
getStatusColor(status) {
|
||||
if (status >= 200 && status < 300) return 'success';
|
||||
if (status >= 300 && status < 400) return 'info';
|
||||
if (status >= 400 && status < 500) return 'warning';
|
||||
if (status >= 500) return 'error';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
renderRequests() {
|
||||
const content = this.$('#network-content');
|
||||
if (!content) return;
|
||||
|
||||
if (!this.filteredRequests || this.filteredRequests.length === 0) {
|
||||
content.innerHTML = `
|
||||
<div class="table-empty">
|
||||
<div class="table-empty-icon">🔍</div>
|
||||
<div class="table-empty-text">${this.filterUrl ? 'No requests match your filter' : 'No network requests captured yet'}</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Render info count
|
||||
const infoHtml = `
|
||||
<div class="info-count">
|
||||
Showing ${this.filteredRequests.length} of ${this.requests.length} requests
|
||||
${this.autoRefresh ? '• Auto-refreshing every 2s' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Use table-template.js for DSS-compliant rendering
|
||||
const { html: tableHtml, styles: tableStyles } = createTableView({
|
||||
columns: [
|
||||
{ header: 'Method', key: 'method', width: '80px', align: 'left' },
|
||||
{ header: 'Status', key: 'status', width: '80px', align: 'left' },
|
||||
{ header: 'URL', key: 'url', align: 'left' },
|
||||
{ header: 'Type', key: 'resourceType', width: '100px', align: 'left' },
|
||||
{ header: 'Time', key: 'timing', width: '80px', align: 'left' }
|
||||
],
|
||||
rows: this.filteredRequests,
|
||||
renderCell: (col, row) => this.renderCell(col, row),
|
||||
renderDetails: (row) => this.renderDetails(row),
|
||||
emptyMessage: 'No network requests',
|
||||
emptyIcon: '🌐'
|
||||
});
|
||||
|
||||
// Adopt table styles
|
||||
this.adoptStyles(tableStyles);
|
||||
|
||||
// Render table
|
||||
content.innerHTML = infoHtml + tableHtml + '<div class="hint">💡 Click any row to view full request details</div>';
|
||||
|
||||
// Setup table event handlers
|
||||
setupTableEvents(this.shadowRoot);
|
||||
|
||||
logger.debug('[DSNetworkMonitor] Rendered requests', { count: this.filteredRequests.length });
|
||||
}
|
||||
|
||||
renderCell(col, row) {
|
||||
const method = row.method || 'GET';
|
||||
const status = row.status || '-';
|
||||
const statusColor = this.getStatusColor(status);
|
||||
const resourceType = row.resourceType || 'other';
|
||||
const url = row.url || 'Unknown URL';
|
||||
const timing = row.timing ? `${Math.round(row.timing)}ms` : '-';
|
||||
|
||||
switch (col.key) {
|
||||
case 'method':
|
||||
const methodColor = method === 'GET' ? 'info' : method === 'POST' ? 'success' : 'warning';
|
||||
return `<span class="badge badge-${methodColor}">${this.escapeHtml(method)}</span>`;
|
||||
|
||||
case 'status':
|
||||
return `<span class="badge badge-${statusColor}">${this.escapeHtml(String(status))}</span>`;
|
||||
|
||||
case 'url':
|
||||
return `<span class="code" style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block;">${this.escapeHtml(url)}</span>`;
|
||||
|
||||
case 'resourceType':
|
||||
return `<span class="badge badge-info">${this.escapeHtml(resourceType)}</span>`;
|
||||
|
||||
case 'timing':
|
||||
return `<span style="color: var(--vscode-descriptionForeground);">${timing}</span>`;
|
||||
|
||||
default:
|
||||
return this.escapeHtml(String(row[col.key] || '-'));
|
||||
}
|
||||
}
|
||||
|
||||
renderDetails(row) {
|
||||
const method = row.method || 'GET';
|
||||
const status = row.status || '-';
|
||||
const url = row.url || 'Unknown URL';
|
||||
const resourceType = row.resourceType || 'other';
|
||||
|
||||
return `
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="detail-label">URL:</span>
|
||||
<span class="detail-value code">${this.escapeHtml(url)}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="detail-label">Method:</span>
|
||||
<span class="detail-value code">${this.escapeHtml(method)}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="detail-label">Status:</span>
|
||||
<span class="detail-value code">${this.escapeHtml(String(status))}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="detail-label">Type:</span>
|
||||
<span class="detail-value code">${this.escapeHtml(resourceType)}</span>
|
||||
</div>
|
||||
${row.headers ? `
|
||||
<div style="margin-top: 12px;">
|
||||
<div class="detail-label" style="display: block; margin-bottom: 4px;">Headers:</div>
|
||||
<pre class="detail-code">${this.escapeHtml(JSON.stringify(row.headers, null, 2))}</pre>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ds-network-monitor', DSNetworkMonitor);
|
||||
|
||||
export default DSNetworkMonitor;
|
||||
Reference in New Issue
Block a user