fix: eliminate gaps in table rows with alert borders

- Replace border-left with inset box-shadow for alert indicators
- Prevents unprofessional gaps between background and table edges
- Add alert support to NodeSummaryTable with same gap-free technique
- Generate offline alerts in mock data for testing
- Fix alert matching to use full resource ID for nodes
This commit is contained in:
Pulse Monitor
2025-09-02 22:22:04 +00:00
parent f8de8d71d8
commit e3e554a4ad
4 changed files with 71 additions and 28 deletions
@@ -676,15 +676,15 @@ export function Dashboard(props: DashboardProps) {
{/* Table View */}
<Show when={connected() && initialDataReceived() && filteredGuests().length > 0}>
<ComponentErrorBoundary name="Guest Table">
<div class="mb-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<ScrollableTable
class="mb-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"
minWidth="900px"
>
<table class="w-full min-w-[900px] table-fixed">
<table class="w-full min-w-[900px] table-fixed border-collapse">
<thead>
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-600">
<th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[200px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
class="pl-6 pr-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[200px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('name')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('name')}
tabindex="0"
@@ -761,7 +761,7 @@ export function Dashboard(props: DashboardProps) {
<>
<Show when={node && groupingMode() === 'grouped'}>
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
<td class="p-1 px-2 text-xs font-medium text-gray-600 dark:text-gray-400 w-[200px]">
<td class="p-1 pl-6 pr-2 text-xs font-medium text-gray-600 dark:text-gray-400 w-[200px]">
<a
href={nodeHostMap()[node] || (node.includes(':') ? `https://${node}` : `https://${node}:8006`)}
target="_blank"
@@ -798,6 +798,7 @@ export function Dashboard(props: DashboardProps) {
</tbody>
</table>
</ScrollableTable>
</div>
</ComponentErrorBoundary>
</Show>
@@ -105,7 +105,7 @@ export function GuestRow(props: GuestRowProps) {
// Get row styling - include alert styles if present
const rowClass = createMemo(() => {
const base = 'transition-all duration-200';
const base = 'transition-all duration-200 relative';
const hover = 'hover:shadow-sm';
// Extract only the background color from alert styles, not the border
const alertBg = props.alertStyles?.hasAlert
@@ -118,19 +118,25 @@ export function GuestRow(props: GuestRowProps) {
return `${base} ${hover} ${defaultHover} ${alertBg} ${stoppedDimming}`;
});
// Get first cell styling with left border for alerts
// Get first cell styling
const firstCellClass = createMemo(() => {
const base = 'p-1 px-2 whitespace-nowrap relative';
const alertBorder = props.alertStyles?.hasAlert
? (props.alertStyles.severity === 'critical'
? 'border-l-4 border-l-red-500 dark:border-l-red-400'
: 'border-l-4 border-l-yellow-500 dark:border-l-yellow-400')
: '';
return `${base} ${alertBorder}`;
// Add extra padding when alert is present for visual spacing
const padding = props.alertStyles?.hasAlert ? 'pl-4' : '';
return `${base} ${padding}`;
});
// Get row styles including box-shadow for alert border
const rowStyle = createMemo(() => {
if (!props.alertStyles?.hasAlert) return {};
const color = props.alertStyles.severity === 'critical' ? '#ef4444' : '#eab308';
return {
'box-shadow': `inset 4px 0 0 0 ${color}`
};
});
return (
<tr class={rowClass()}>
<tr class={rowClass()} style={rowStyle()}>
{/* Name - Sticky column */}
<td class={firstCellClass()}>
<div class="flex items-center gap-2">
@@ -2,6 +2,8 @@ import { Component, For, Show, createMemo } from 'solid-js';
import type { Node, VM, Container, Storage, PBSInstance } from '@/types/api';
import { formatBytes, formatUptime } from '@/utils/format';
import { MetricBar } from '@/components/Dashboard/MetricBar';
import { useWebSocket } from '@/App';
import { getAlertStyles } from '@/utils/alerts';
interface NodeSummaryTableProps {
nodes: Node[];
@@ -16,6 +18,7 @@ interface NodeSummaryTableProps {
}
export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
const { activeAlerts } = useWebSocket();
// Combine and sort nodes based on tab
const sortedItems = createMemo(() => {
const items: Array<{ type: 'pve' | 'pbs'; data: Node | PBSInstance }> = [];
@@ -98,12 +101,12 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
// This prevents the table from disappearing on refresh while data loads
return (
<div class="mb-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="overflow-x-auto p-1">
<table class="w-full min-w-[600px]">
<div class="mb-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full min-w-[600px] border-collapse">
<thead>
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-600">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-1/4">
<th class="pl-3 pr-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-1/4">
{props.currentTab === 'backups' ? 'Node / PBS' : 'Node'}
</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-20">Status</th>
@@ -170,24 +173,40 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
const nodeId = isPVE ? node!.name : pbs!.name;
const isSelected = () => props.selectedNode === nodeId;
// Use the full resource ID for alert matching
const resourceId = isPVE ? (node!.id || node!.name) : (pbs!.id || pbs!.name);
const alertStyles = getAlertStyles(resourceId, activeAlerts);
// Get row styles including box-shadow for alert border
const rowStyle = createMemo(() => {
const styles: any = {};
if (isSelected()) {
styles['box-shadow'] = '0 0 0 1px rgba(59, 130, 246, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.1)';
}
if (alertStyles.hasAlert) {
const color = alertStyles.severity === 'critical' ? '#ef4444' : '#eab308';
styles['box-shadow'] = `inset 4px 0 0 0 ${color}${isSelected() ? ', 0 0 0 1px rgba(59, 130, 246, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.1)' : ''}`;
}
return styles;
});
return (
<tr
class={`cursor-pointer transition-all duration-200 ${
class={`cursor-pointer transition-all duration-200 relative ${
isSelected()
? 'bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 relative z-10'
: props.selectedNode
? 'opacity-50 hover:opacity-80 hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-sm'
: 'hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-sm'
? 'bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 z-10'
: alertStyles.hasAlert
? (alertStyles.severity === 'critical'
? 'bg-red-50 dark:bg-red-950/30 hover:bg-red-100 dark:hover:bg-red-950/40'
: 'bg-yellow-50 dark:bg-yellow-950/20 hover:bg-yellow-100 dark:hover:bg-yellow-950/30')
: props.selectedNode
? 'opacity-50 hover:opacity-80 hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-sm'
: 'hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-sm'
}`}
style={{
'box-shadow': isSelected()
? '0 0 0 1px rgba(59, 130, 246, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.1)'
: undefined
}}
style={rowStyle()}
onClick={() => props.onNodeClick(nodeId, item.type)}
>
<td class="px-2 py-0.5 whitespace-nowrap">
<td class={`pr-2 py-0.5 whitespace-nowrap ${alertStyles.hasAlert ? 'pl-4' : 'pl-3'}`}>
<div class="flex items-center gap-1">
<a
href={isPVE ? (node!.host || `https://${node!.name}:8006`) : (pbs!.host || `https://${pbs!.name}:8007`)}
+17
View File
@@ -557,6 +557,23 @@ func GenerateAlerts(nodes []models.Node, vms []models.VM, containers []models.Co
// Generate some node alerts
for _, node := range nodes {
// Add offline alert for offline nodes
if node.Status == "offline" {
alerts = append(alerts, models.Alert{
ID: fmt.Sprintf("node-offline-%s", node.ID),
Type: "connectivity",
Level: "critical",
ResourceID: node.ID,
ResourceName: node.Name,
Node: node.Name,
Message: fmt.Sprintf("Node '%s' is offline", node.Name),
Value: 0,
Threshold: 0,
StartTime: time.Now().Add(-time.Minute * 5), // Offline for 5 minutes
})
continue // Skip other checks for offline nodes
}
if node.CPU > 0.8 {
alerts = append(alerts, models.Alert{
ID: fmt.Sprintf("alert-%s-cpu", node.Name),