mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 10:43:36 +00:00
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:
@@ -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`)}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user