fix: Improve table rendering in Netifyd flows and applications views
- Replace div-based tables with native HTML tables for proper column alignment - Add inline styles for consistent rendering across themes - Fix Flow Activity by Interface table layout - Fix Application List table layout with sortable headers - Add pill-style badges and progress bars for better UX Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d5d263f3c5
commit
25385fc35d
@ -160,107 +160,106 @@ return view.extend({
|
||||
|
||||
var getSortIcon = function(column) {
|
||||
if (this.sortColumn !== column) {
|
||||
return E('i', { 'class': 'fa fa-sort', 'style': 'opacity: 0.3' });
|
||||
return E('i', { 'class': 'fa fa-sort', 'style': 'opacity: 0.3; margin-left: 0.25rem;' });
|
||||
}
|
||||
return E('i', {
|
||||
'class': 'fa fa-sort-' + (this.sortDirection === 'asc' ? 'up' : 'down'),
|
||||
'style': 'color: #3b82f6'
|
||||
'style': 'color: #3b82f6; margin-left: 0.25rem;'
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
dom.content(container, [
|
||||
apps.length > 0 ? E('div', { 'class': 'table', 'style': 'font-size: 0.95em' },
|
||||
[
|
||||
// Header
|
||||
E('div', { 'class': 'tr table-titles' }, [
|
||||
E('div', {
|
||||
'class': 'th left',
|
||||
'style': 'width: 30%; cursor: pointer',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'name')
|
||||
}, [
|
||||
_('Application'),
|
||||
' ',
|
||||
getSortIcon('name')
|
||||
]),
|
||||
E('div', {
|
||||
'class': 'th center',
|
||||
'style': 'width: 15%; cursor: pointer',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'flows')
|
||||
}, [
|
||||
_('Flows'),
|
||||
' ',
|
||||
getSortIcon('flows')
|
||||
]),
|
||||
E('div', {
|
||||
'class': 'th center',
|
||||
'style': 'width: 15%; cursor: pointer',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'packets')
|
||||
}, [
|
||||
_('Packets'),
|
||||
' ',
|
||||
getSortIcon('packets')
|
||||
]),
|
||||
E('div', {
|
||||
'class': 'th right',
|
||||
'style': 'width: 20%; cursor: pointer',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'bytes')
|
||||
}, [
|
||||
_('Total Traffic'),
|
||||
' ',
|
||||
getSortIcon('bytes')
|
||||
]),
|
||||
E('div', { 'class': 'th', 'style': 'width: 20%' }, _('% of Total'))
|
||||
])
|
||||
].concat(
|
||||
// Rows
|
||||
sortedApps.map(function(app, idx) {
|
||||
var percentage = totalBytes > 0 ? (app.bytes / totalBytes * 100) : 0;
|
||||
var color = appColors[idx % appColors.length];
|
||||
var tableStyle = 'width: 100%; border-collapse: collapse;';
|
||||
var thStyle = 'padding: 0.75rem 1rem; text-align: left; font-weight: 600; background: #f8fafc; border-bottom: 2px solid #e2e8f0; cursor: pointer; user-select: none;';
|
||||
var tdStyle = 'padding: 0.75rem 1rem; border-bottom: 1px solid #e2e8f0; vertical-align: middle;';
|
||||
|
||||
return E('div', {
|
||||
'class': 'tr',
|
||||
'style': idx % 2 === 0 ? 'background: #f9fafb' : ''
|
||||
if (apps.length === 0) {
|
||||
dom.content(container, [
|
||||
E('div', {
|
||||
'style': 'text-align: center; padding: 3rem; background: #f9fafb; border-radius: 8px;'
|
||||
}, [
|
||||
E('i', { 'class': 'fa fa-cubes', 'style': 'font-size: 3em; opacity: 0.3; display: block; margin-bottom: 1rem; color: #6b7280;' }),
|
||||
E('h4', { 'style': 'margin: 0 0 0.5rem 0; color: #374151;' }, _('No Application Data')),
|
||||
E('p', { 'style': 'margin: 0 0 0.5rem 0; color: #6b7280;' }, _('No applications have been detected yet')),
|
||||
E('small', { 'style': 'color: #9ca3af;' }, _('Data will appear once network traffic is analyzed'))
|
||||
])
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
var tableContent = E('table', { 'style': tableStyle }, [
|
||||
E('thead', {}, [
|
||||
E('tr', {}, [
|
||||
E('th', {
|
||||
'style': thStyle,
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'name')
|
||||
}, [
|
||||
E('div', { 'class': 'td left', 'style': 'width: 30%' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem' }, [
|
||||
E('div', {
|
||||
'style': 'width: 10px; height: 10px; border-radius: 50%; background: ' + color + '; flex-shrink: 0'
|
||||
}),
|
||||
E('strong', app.name || 'Unknown')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'td center', 'style': 'width: 15%' },
|
||||
(app.flows || 0).toLocaleString()),
|
||||
E('div', { 'class': 'td center', 'style': 'width: 15%' },
|
||||
(app.packets || 0).toLocaleString()),
|
||||
E('div', { 'class': 'td right', 'style': 'width: 20%' }, [
|
||||
E('strong', { 'style': 'color: ' + color },
|
||||
netifydAPI.formatBytes(app.bytes || 0))
|
||||
]),
|
||||
E('div', { 'class': 'td', 'style': 'width: 20%' }, [
|
||||
E('div', {
|
||||
'style': 'background: #e5e7eb; border-radius: 10px; height: 24px; position: relative; overflow: hidden'
|
||||
}, [
|
||||
E('div', {
|
||||
'style': 'background: ' + color + '; height: 100%; width: ' + percentage + '%; transition: width 0.3s ease; border-radius: 10px'
|
||||
}),
|
||||
E('span', {
|
||||
'style': 'position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); font-size: 0.8em; font-weight: bold; color: ' + (percentage > 40 ? '#fff' : '#374151')
|
||||
}, percentage.toFixed(1) + '%')
|
||||
])
|
||||
_('Application'),
|
||||
getSortIcon('name')
|
||||
]),
|
||||
E('th', {
|
||||
'style': thStyle + ' text-align: center;',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'flows')
|
||||
}, [
|
||||
_('Flows'),
|
||||
getSortIcon('flows')
|
||||
]),
|
||||
E('th', {
|
||||
'style': thStyle + ' text-align: center;',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'packets')
|
||||
}, [
|
||||
_('Packets'),
|
||||
getSortIcon('packets')
|
||||
]),
|
||||
E('th', {
|
||||
'style': thStyle + ' text-align: right;',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'bytes')
|
||||
}, [
|
||||
_('Total Traffic'),
|
||||
getSortIcon('bytes')
|
||||
]),
|
||||
E('th', { 'style': thStyle + ' text-align: center; cursor: default;' }, _('% of Total'))
|
||||
])
|
||||
]),
|
||||
E('tbody', {}, sortedApps.map(function(app, idx) {
|
||||
var percentage = totalBytes > 0 ? (app.bytes / totalBytes * 100) : 0;
|
||||
var color = appColors[idx % appColors.length];
|
||||
var rowBg = idx % 2 === 0 ? '' : 'background: #f9fafb;';
|
||||
|
||||
return E('tr', { 'style': rowBg }, [
|
||||
E('td', { 'style': tdStyle }, [
|
||||
E('span', { 'style': 'display: inline-flex; align-items: center; gap: 0.5rem;' }, [
|
||||
E('span', {
|
||||
'style': 'width: 10px; height: 10px; border-radius: 50%; background: ' + color + '; flex-shrink: 0; display: inline-block;'
|
||||
}),
|
||||
E('strong', {}, app.name || 'Unknown')
|
||||
])
|
||||
]);
|
||||
}.bind(this))
|
||||
)
|
||||
) : E('div', {
|
||||
'class': 'alert-message info',
|
||||
'style': 'text-align: center; padding: 3rem'
|
||||
}, [
|
||||
E('i', { 'class': 'fa fa-cubes', 'style': 'font-size: 3em; opacity: 0.3; display: block; margin-bottom: 1rem' }),
|
||||
E('h4', _('No Application Data')),
|
||||
E('p', { 'class': 'text-muted' }, _('No applications have been detected yet')),
|
||||
E('small', _('Data will appear once network traffic is analyzed'))
|
||||
])
|
||||
]),
|
||||
E('td', { 'style': tdStyle + ' text-align: center;' },
|
||||
(app.flows || 0).toLocaleString()),
|
||||
E('td', { 'style': tdStyle + ' text-align: center;' },
|
||||
(app.packets || 0).toLocaleString()),
|
||||
E('td', { 'style': tdStyle + ' text-align: right;' }, [
|
||||
E('strong', { 'style': 'color: ' + color + ';' },
|
||||
netifydAPI.formatBytes(app.bytes || 0))
|
||||
]),
|
||||
E('td', { 'style': tdStyle + ' text-align: center; min-width: 120px;' }, [
|
||||
E('div', {
|
||||
'style': 'background: #e5e7eb; border-radius: 12px; height: 24px; position: relative; overflow: hidden;'
|
||||
}, [
|
||||
E('div', {
|
||||
'style': 'background: ' + color + '; height: 100%; width: ' + percentage + '%; transition: width 0.3s ease; border-radius: 12px;'
|
||||
}),
|
||||
E('span', {
|
||||
'style': 'position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); font-size: 0.8em; font-weight: 600; color: ' + (percentage > 40 ? '#fff' : '#374151') + ';'
|
||||
}, percentage.toFixed(1) + '%')
|
||||
])
|
||||
])
|
||||
]);
|
||||
}.bind(this)))
|
||||
]);
|
||||
|
||||
dom.content(container, [
|
||||
E('div', { 'style': 'overflow-x: auto;' }, [tableContent])
|
||||
]);
|
||||
},
|
||||
|
||||
|
||||
@ -143,72 +143,74 @@ return view.extend({
|
||||
}
|
||||
}
|
||||
|
||||
var tableStyle = 'width: 100%; border-collapse: collapse; margin-top: 1rem;';
|
||||
var thStyle = 'padding: 0.75rem 1rem; text-align: left; font-weight: 600; background: #f8fafc; border-bottom: 2px solid #e2e8f0;';
|
||||
var tdStyle = 'padding: 0.75rem 1rem; border-bottom: 1px solid #e2e8f0; vertical-align: middle;';
|
||||
|
||||
var tableContent = E('table', { 'style': tableStyle }, [
|
||||
E('thead', {}, [
|
||||
E('tr', {}, [
|
||||
E('th', { 'style': thStyle }, _('Interface')),
|
||||
E('th', { 'style': thStyle + ' text-align: center;' }, _('TCP')),
|
||||
E('th', { 'style': thStyle + ' text-align: center;' }, _('UDP')),
|
||||
E('th', { 'style': thStyle + ' text-align: center;' }, _('ICMP')),
|
||||
E('th', { 'style': thStyle + ' text-align: right;' }, _('Total Traffic')),
|
||||
E('th', { 'style': thStyle + ' text-align: center;' }, _('Status'))
|
||||
])
|
||||
]),
|
||||
E('tbody', {}, interfaceList.map(function(iface) {
|
||||
var totalPackets = iface.tcp + iface.udp + iface.icmp;
|
||||
var isActive = totalPackets > 0;
|
||||
|
||||
return E('tr', { 'style': isActive ? '' : 'opacity: 0.6;' }, [
|
||||
E('td', { 'style': tdStyle }, [
|
||||
E('span', { 'style': 'display: inline-flex; align-items: center; gap: 0.5rem;' }, [
|
||||
E('i', { 'class': 'fa fa-ethernet', 'style': 'color: ' + (isActive ? '#3b82f6' : '#9ca3af') }),
|
||||
E('strong', {}, self.formatInterfaceLabel(iface.name))
|
||||
])
|
||||
]),
|
||||
E('td', { 'style': tdStyle + ' text-align: center;' }, [
|
||||
E('span', {
|
||||
'style': 'display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; background: #3b82f6; color: white; font-weight: 500; min-width: 3rem;'
|
||||
}, iface.tcp.toLocaleString())
|
||||
]),
|
||||
E('td', { 'style': tdStyle + ' text-align: center;' }, [
|
||||
E('span', {
|
||||
'style': 'display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; background: #10b981; color: white; font-weight: 500; min-width: 3rem;'
|
||||
}, iface.udp.toLocaleString())
|
||||
]),
|
||||
E('td', { 'style': tdStyle + ' text-align: center;' }, [
|
||||
E('span', {
|
||||
'style': 'display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; background: #f59e0b; color: white; font-weight: 500; min-width: 3rem;'
|
||||
}, iface.icmp.toLocaleString())
|
||||
]),
|
||||
E('td', { 'style': tdStyle + ' text-align: right; font-weight: 500;' },
|
||||
netifydAPI.formatBytes ? netifydAPI.formatBytes(iface.bytes) : (iface.bytes + ' B')),
|
||||
E('td', { 'style': tdStyle + ' text-align: center;' }, [
|
||||
E('span', {
|
||||
'style': 'display: inline-flex; align-items: center; gap: 0.5rem;'
|
||||
}, [
|
||||
E('i', {
|
||||
'class': 'fa fa-circle',
|
||||
'style': 'color: ' + (isActive ? '#10b981' : '#9ca3af'),
|
||||
'title': isActive ? _('Active') : _('Idle')
|
||||
}),
|
||||
iface.dropped > 0 ? E('span', {
|
||||
'style': 'padding: 0.125rem 0.5rem; border-radius: 9999px; background: #ef4444; color: white; font-size: 0.75rem;'
|
||||
}, iface.dropped + ' dropped') : null
|
||||
])
|
||||
])
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', [
|
||||
E('i', { 'class': 'fa fa-network-wired', 'style': 'margin-right: 0.5rem' }),
|
||||
E('h3', { 'style': 'display: flex; align-items: center; margin-bottom: 0.5rem;' }, [
|
||||
E('i', { 'class': 'fa fa-network-wired', 'style': 'margin-right: 0.5rem; color: #3b82f6;' }),
|
||||
_('Flow Activity by Interface')
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('div', { 'class': 'table', 'style': 'font-size: 0.95em' }, [
|
||||
// Header
|
||||
E('div', { 'class': 'tr table-titles' }, [
|
||||
E('div', { 'class': 'th', 'style': 'width: 25%' }, _('Interface')),
|
||||
E('div', { 'class': 'th center', 'style': 'width: 15%' }, _('TCP')),
|
||||
E('div', { 'class': 'th center', 'style': 'width: 15%' }, _('UDP')),
|
||||
E('div', { 'class': 'th center', 'style': 'width: 15%' }, _('ICMP')),
|
||||
E('div', { 'class': 'th right', 'style': 'width: 20%' }, _('Total Traffic')),
|
||||
E('div', { 'class': 'th center', 'style': 'width: 10%' }, _('Status'))
|
||||
]),
|
||||
// Rows
|
||||
interfaceList.map(function(iface) {
|
||||
var totalPackets = iface.tcp + iface.udp + iface.icmp;
|
||||
var isActive = totalPackets > 0;
|
||||
|
||||
return E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td', 'style': 'width: 25%' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem' }, [
|
||||
E('i', { 'class': 'fa fa-ethernet', 'style': 'color: ' + (isActive ? '#3b82f6' : '#9ca3af') }),
|
||||
E('strong', self.formatInterfaceLabel(iface.name))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'td center', 'style': 'width: 15%' }, [
|
||||
E('span', {
|
||||
'class': 'badge',
|
||||
'style': 'background: #3b82f6; color: white'
|
||||
}, iface.tcp.toLocaleString())
|
||||
]),
|
||||
E('div', { 'class': 'td center', 'style': 'width: 15%' }, [
|
||||
E('span', {
|
||||
'class': 'badge',
|
||||
'style': 'background: #10b981; color: white'
|
||||
}, iface.udp.toLocaleString())
|
||||
]),
|
||||
E('div', { 'class': 'td center', 'style': 'width: 15%' }, [
|
||||
E('span', {
|
||||
'class': 'badge',
|
||||
'style': 'background: #f59e0b; color: white'
|
||||
}, iface.icmp.toLocaleString())
|
||||
]),
|
||||
E('div', { 'class': 'td right', 'style': 'width: 20%' },
|
||||
netifydAPI.formatBytes(iface.bytes)),
|
||||
E('div', { 'class': 'td center', 'style': 'width: 10%' }, [
|
||||
isActive ? E('i', {
|
||||
'class': 'fa fa-circle',
|
||||
'style': 'color: #10b981',
|
||||
'title': _('Active')
|
||||
}) : E('i', {
|
||||
'class': 'fa fa-circle',
|
||||
'style': 'color: #9ca3af',
|
||||
'title': _('Idle')
|
||||
}),
|
||||
iface.dropped > 0 ? E('span', {
|
||||
'class': 'badge',
|
||||
'style': 'background: #ef4444; color: white; margin-left: 0.5rem; font-size: 0.75em'
|
||||
}, iface.dropped + ' ⚠') : null
|
||||
])
|
||||
]);
|
||||
})
|
||||
])
|
||||
E('div', { 'class': 'cbi-section-node', 'style': 'overflow-x: auto;' }, [
|
||||
tableContent
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user