feat: improve UI consistency with refined styling

- Reduced text sizes across all tables for better information density
  - Guest names now use text-sm for better readability
  - Node names in summary tables use smaller text-[11px]
  - All table text sizes are now consistent

- Unified table header styling with compromise design
  - Subtle bg-gray-50 dark:bg-gray-700/50 background
  - Consistent px-2 py-1.5 padding for comfortable spacing
  - text-[11px] sm:text-xs responsive sizing
  - Softer border-gray-200 dark:border-gray-600 borders

- Updated Storage tab progress bars to match Dashboard style
  - Increased height from h-2.5 to h-3.5
  - Simplified color thresholds (green/yellow/red)
  - Consistent text formatting with Dashboard

Applied consistently across Dashboard, Storage, Backups tabs and all node tables for a cohesive look
This commit is contained in:
Pulse Monitor
2025-08-26 22:45:55 +00:00
parent 6a97995afb
commit b05fb62099
10 changed files with 200 additions and 191 deletions
@@ -88,7 +88,7 @@ export function ResourceTable(props: ResourceTableProps) {
<>
{/* Node group header */}
<tr class="bg-gray-50 dark:bg-gray-700/50">
<td colspan={props.columns.length + 5} class="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400">
<td colspan={props.columns.length + 5} class="p-1 px-2 text-xs font-medium text-gray-500 dark:text-gray-400">
{nodeName}
</td>
</tr>
@@ -107,7 +107,7 @@ export function ResourceTable(props: ResourceTableProps) {
return (
<tr class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${resource.disabled ? 'opacity-40' : ''}`}>
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<div class="flex items-center gap-2">
<span class={`text-sm font-medium ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}>
{resource.name}
@@ -122,7 +122,7 @@ export function ResourceTable(props: ResourceTableProps) {
</Show>
</div>
</td>
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<span class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
resource.type === 'pbs' ? 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300' :
resource.type === 'node' ? 'bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300' :
@@ -133,7 +133,7 @@ export function ResourceTable(props: ResourceTableProps) {
{resource.resourceType}
</span>
</td>
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<span class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
resource.status === 'online' || resource.status === 'running' ?
'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300' :
@@ -166,7 +166,7 @@ export function ResourceTable(props: ResourceTableProps) {
};
return (
<td class="px-3 py-1.5 text-center">
<td class="p-1 px-2 text-center">
<Show when={showMetric()} fallback={
<span class="text-sm text-gray-400 dark:text-gray-500">-</span>
}>
@@ -198,7 +198,7 @@ export function ResourceTable(props: ResourceTableProps) {
</For>
{/* Alerts column */}
<td class="px-3 py-1.5 text-center">
<td class="p-1 px-2 text-center">
<Show when={resource.type === 'guest' && props.onToggleDisabled}>
<button type="button"
onClick={() => props.onToggleDisabled!(resource.id)}
@@ -251,7 +251,7 @@ export function ResourceTable(props: ResourceTableProps) {
</td>
{/* Actions column */}
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<div class="flex items-center justify-center gap-1">
<Show when={!isEditing()} fallback={
<>
@@ -322,7 +322,7 @@ export function ResourceTable(props: ResourceTableProps) {
return (
<tr class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${resource.disabled ? 'opacity-40' : ''}`}>
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<div class="flex items-center gap-2">
<span class={`text-sm font-medium ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}>
{resource.name}
@@ -343,7 +343,7 @@ export function ResourceTable(props: ResourceTableProps) {
</Show>
</div>
</td>
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<span class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
resource.type === 'pbs' ? 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300' :
resource.type === 'node' ? 'bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300' :
@@ -354,7 +354,7 @@ export function ResourceTable(props: ResourceTableProps) {
{resource.resourceType}
</span>
</td>
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<span class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
resource.status === 'online' || resource.status === 'running' ?
'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300' :
@@ -387,7 +387,7 @@ export function ResourceTable(props: ResourceTableProps) {
};
return (
<td class="px-3 py-1.5 text-center">
<td class="p-1 px-2 text-center">
<Show when={showMetric()} fallback={
<span class="text-sm text-gray-400 dark:text-gray-500">-</span>
}>
@@ -419,7 +419,7 @@ export function ResourceTable(props: ResourceTableProps) {
</For>
{/* Alerts column */}
<td class="px-3 py-1.5 text-center">
<td class="p-1 px-2 text-center">
<Show when={resource.type === 'guest' && props.onToggleDisabled}>
<button type="button"
onClick={() => props.onToggleDisabled!(resource.id)}
@@ -472,7 +472,7 @@ export function ResourceTable(props: ResourceTableProps) {
</td>
{/* Actions column */}
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<div class="flex items-center justify-center gap-1">
<Show when={!isEditing()} fallback={
<>
@@ -621,8 +621,8 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
</thead>
<tbody>
<tr class="border-b border-gray-100 dark:border-gray-700/50">
<td class="py-3 px-3 font-medium text-gray-700 dark:text-gray-300 text-sm">VMs & Containers</td>
<td class="text-center px-3 py-3">
<td class="p-1 px-2 font-medium text-gray-700 dark:text-gray-300 text-sm">VMs & Containers</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -636,7 +636,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3">
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -650,7 +650,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3">
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -664,8 +664,8 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3">
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -679,7 +679,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3">
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -693,7 +693,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3">
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -707,7 +707,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3">
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -723,8 +723,8 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-700/50">
<td class="py-3 px-3 font-medium text-gray-700 dark:text-gray-300 text-sm">Proxmox Nodes</td>
<td class="text-center px-3 py-3">
<td class="p-1 px-2 font-medium text-gray-700 dark:text-gray-300 text-sm">Proxmox Nodes</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -738,7 +738,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3">
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -752,7 +752,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3">
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -766,18 +766,18 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
</tr>
<tr>
<td class="py-3 px-3 font-medium text-gray-700 dark:text-gray-300 text-sm">Storage</td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3">
<td class="p-1 px-2 font-medium text-gray-700 dark:text-gray-300 text-sm">Storage</td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2">
<input
type="number"
min="0"
@@ -791,10 +791,10 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center px-3 py-3"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
</tr>
</tbody>
</table>
@@ -893,14 +893,14 @@ const UnifiedBackups: Component = () => {
<table class="w-full">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">PBS Instance</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">CPU</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Memory</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Storage</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Datastores</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Backups</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Uptime</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">PBS Instance</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">Status</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">CPU</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">Memory</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">Storage</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">Datastores</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">Backups</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">Uptime</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@@ -952,7 +952,7 @@ const UnifiedBackups: Component = () => {
}
}}
>
<td class="px-2 py-0.5 whitespace-nowrap">
<td class="p-0.5 px-1.5 whitespace-nowrap">
<div class="flex items-center gap-1">
<a
href={pbs.host || `https://${pbs.name}:8007`}
@@ -969,7 +969,7 @@ const UnifiedBackups: Component = () => {
</Show>
</div>
</td>
<td class="px-2 py-0.5 whitespace-nowrap">
<td class="p-0.5 px-1.5 whitespace-nowrap">
<div class="flex items-center gap-1">
<span class={`h-2 w-2 rounded-full ${
isOnline() ? 'bg-green-500' : 'bg-red-500'
@@ -979,14 +979,14 @@ const UnifiedBackups: Component = () => {
</span>
</div>
</td>
<td class="px-2 py-0.5 w-[180px]">
<td class="p-0.5 px-1.5 w-[180px]">
<MetricBar
value={cpuPercent()}
label={`${cpuPercent()}%`}
type="cpu"
/>
</td>
<td class="px-2 py-0.5 w-[180px]">
<td class="p-0.5 px-1.5 w-[180px]">
<MetricBar
value={memPercent()}
label={`${memPercent()}%`}
@@ -994,7 +994,7 @@ const UnifiedBackups: Component = () => {
type="memory"
/>
</td>
<td class="px-2 py-0.5 w-[180px]">
<td class="p-0.5 px-1.5 w-[180px]">
<MetricBar
value={storage.percent}
label={`${storage.percent}%`}
@@ -1002,15 +1002,15 @@ const UnifiedBackups: Component = () => {
type="disk"
/>
</td>
<td class="px-2 py-0.5 whitespace-nowrap text-center">
<td class="p-0.5 px-1.5 whitespace-nowrap text-center">
<span class="text-xs text-gray-700 dark:text-gray-300">
{pbs.datastores?.length || 0}
</span>
</td>
<td class="px-2 py-0.5 whitespace-nowrap text-center">
<td class="p-0.5 px-1.5 whitespace-nowrap text-center">
<span class="text-xs text-gray-700 dark:text-gray-300">{pbsBackups()}</span>
</td>
<td class="px-2 py-0.5 whitespace-nowrap">
<td class="p-0.5 px-1.5 whitespace-nowrap">
<span class="text-xs text-gray-600 dark:text-gray-400">
<Show when={isOnline() && pbs.uptime} fallback="-">
{formatUptime(pbs.uptime)}
@@ -1040,7 +1040,7 @@ const UnifiedBackups: Component = () => {
<div class="flex items-center gap-1">
<button type="button"
onClick={() => setChartTimeRange(7)}
class={`px-2 py-0.5 text-xs border rounded transition-colors ${
class={`p-0.5 px-1.5 text-xs border rounded transition-colors ${
chartTimeRange() === 7
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700'
@@ -1050,7 +1050,7 @@ const UnifiedBackups: Component = () => {
</button>
<button type="button"
onClick={() => setChartTimeRange(30)}
class={`px-2 py-0.5 text-xs border rounded transition-colors ${
class={`p-0.5 px-1.5 text-xs border rounded transition-colors ${
chartTimeRange() === 30
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700'
@@ -1060,7 +1060,7 @@ const UnifiedBackups: Component = () => {
</button>
<button type="button"
onClick={() => setChartTimeRange(90)}
class={`px-2 py-0.5 text-xs border rounded transition-colors ${
class={`p-0.5 px-1.5 text-xs border rounded transition-colors ${
chartTimeRange() === 90
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700'
@@ -1070,7 +1070,7 @@ const UnifiedBackups: Component = () => {
</button>
<button type="button"
onClick={() => setChartTimeRange(365)}
class={`px-2 py-0.5 text-xs border rounded transition-colors ${
class={`p-0.5 px-1.5 text-xs border rounded transition-colors ${
chartTimeRange() === 365
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700'
@@ -1086,7 +1086,7 @@ const UnifiedBackups: Component = () => {
<Show when={selectedDateRange()}>
<button type="button"
onClick={() => setSelectedDateRange(null)}
class="px-2 py-0.5 text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800/50 transition-colors"
class="p-0.5 px-1.5 text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800/50 transition-colors"
>
Clear filter
</button>
@@ -1551,33 +1551,37 @@ const UnifiedBackups: Component = () => {
{/* Desktop Table View */}
<table class="backup-table hidden lg:table">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" style="width: 150px;">
Name
<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 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('name')}
style="width: 150px;"
>
Name {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('type')}
style="width: 60px;"
>
Type {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('vmid')}
style="width: 60px;"
>
VMID {sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('node')}
style="width: 100px;"
>
Node {sortKey() === 'node' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('backupTime')}
style="width: 140px;"
>
@@ -1585,7 +1589,7 @@ const UnifiedBackups: Component = () => {
</th>
<Show when={backupTypeFilter() !== 'snapshot'}>
<th
class="sortable p-1 px-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('size')}
style="width: 80px;"
>
@@ -1593,23 +1597,31 @@ const UnifiedBackups: Component = () => {
</th>
</Show>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('backupType')}
style="width: 80px;"
>
Backup {sortKey() === 'backupType' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}>
<th class="p-1 px-2 text-center" style="width: 60px;">
Verified
<th
class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('verified')}
style="width: 60px;"
>
Verified {sortKey() === 'verified' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
</Show>
<Show when={backupTypeFilter() !== 'snapshot'}>
<th class="p-1 px-2" style="width: 150px;">
Location
<th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('storage')}
style="width: 150px;"
>
Location {sortKey() === 'storage' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
</Show>
<th class="p-1 px-2" style="width: 200px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider" style="width: 200px;">
Details
</th>
</tr>
@@ -1619,7 +1631,7 @@ const UnifiedBackups: Component = () => {
{(group) => (
<>
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
<td class="px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400 whitespace-nowrap">
<td class="p-0.5 px-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 whitespace-nowrap">
{group.label} ({group.items.length})
</td>
<td colspan={(() => {
@@ -1628,16 +1640,16 @@ const UnifiedBackups: Component = () => {
if (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote') cols++; // Add Verified column
if (backupTypeFilter() !== 'snapshot') cols++; // Add Location column
return cols;
})()} class="px-2 py-1 text-xs font-medium text-gray-500 dark:text-gray-400"></td>
})()} class="p-0.5 px-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"></td>
</tr>
<For each={group.items}>
{(item) => (
<tr class="border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="p-1 px-2">
<td class="p-0.5 px-1.5 text-sm">
{item.name || '-'}
</td>
<td class="p-1 px-2">
<span class={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
<td class="p-0.5 px-1.5">
<span class={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${
item.type === 'VM'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
: 'bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300'
@@ -1645,20 +1657,20 @@ const UnifiedBackups: Component = () => {
{item.type}
</span>
</td>
<td class="p-1 px-2 font-medium">{item.vmid}</td>
<td class="p-1 px-2 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400">
<td class="p-0.5 px-1.5 text-sm">{item.vmid}</td>
<td class="p-0.5 px-1.5 text-sm cursor-pointer hover:text-blue-600 dark:hover:text-blue-400">
{item.node}
</td>
<td class={`p-1 px-2 text-xs ${getAgeColorClass(item.backupTime)}`}>
<td class={`p-0.5 px-1.5 text-xs ${getAgeColorClass(item.backupTime)}`}>
{formatTime(item.backupTime * 1000)}
</td>
<Show when={backupTypeFilter() !== 'snapshot'}>
<td class={`p-1 px-2 ${getSizeColor(item.size)}`}>
<td class={`p-0.5 px-1.5 ${getSizeColor(item.size)}`}>
{item.size ? formatBytes(item.size) : '-'}
</td>
</Show>
<td class="p-1 px-2">
<span class={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
<td class="p-0.5 px-1.5">
<span class={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${
item.backupType === 'snapshot'
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-300'
: item.backupType === 'local'
@@ -1669,7 +1681,7 @@ const UnifiedBackups: Component = () => {
</span>
</td>
<Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}>
<td class="p-1 px-2 text-center">
<td class="p-0.5 px-1.5 text-center">
{item.backupType === 'remote' ? (
item.verified ? (
<span title="PBS backup verified" class="text-green-500 dark:text-green-400">✓</span>
@@ -1682,7 +1694,7 @@ const UnifiedBackups: Component = () => {
</td>
</Show>
<Show when={backupTypeFilter() !== 'snapshot'}>
<td class="p-1 px-2">
<td class="p-0.5 px-1.5 text-sm">
{item.storage || (item.datastore && (
item.namespace && item.namespace !== 'root'
? `${item.datastore}/${item.namespace}`
@@ -1691,7 +1703,7 @@ const UnifiedBackups: Component = () => {
</td>
</Show>
<td
class="p-1 px-2 cursor-help"
class="p-0.5 px-1.5 cursor-help"
onMouseEnter={(e) => {
const details = [];
@@ -404,7 +404,7 @@ export function Dashboard(props: DashboardProps) {
}
}}
>
<td class="px-2 py-0.5 whitespace-nowrap">
<td class="p-1 px-2 whitespace-nowrap">
<div class="flex items-center gap-1">
<a
href={node.host || `https://${node.name}:8006`}
@@ -425,7 +425,7 @@ export function Dashboard(props: DashboardProps) {
</Show>
</div>
</td>
<td class="px-2 py-0.5 whitespace-nowrap">
<td class="p-1 px-2 whitespace-nowrap">
<div class="flex items-center gap-1">
<span class={`h-2 w-2 rounded-full ${
isOnline() ? 'bg-green-500' : 'bg-red-500'
@@ -435,7 +435,7 @@ export function Dashboard(props: DashboardProps) {
</span>
</div>
</td>
<td class="px-2 py-0.5 w-[180px]">
<td class="p-1 px-2 w-[180px]">
<MetricBar
value={cpuPercent()}
label={`${cpuPercent()}%`}
@@ -443,7 +443,7 @@ export function Dashboard(props: DashboardProps) {
type="cpu"
/>
</td>
<td class="px-2 py-0.5 w-[180px]">
<td class="p-1 px-2 w-[180px]">
<MetricBar
value={memPercent()}
label={`${memPercent()}%`}
@@ -451,7 +451,7 @@ export function Dashboard(props: DashboardProps) {
type="memory"
/>
</td>
<td class="px-2 py-0.5 w-[180px]">
<td class="p-1 px-2 w-[180px]">
<MetricBar
value={diskPercent()}
label={`${diskPercent()}%`}
@@ -459,13 +459,13 @@ export function Dashboard(props: DashboardProps) {
type="disk"
/>
</td>
<td class="px-2 py-0.5 whitespace-nowrap text-center">
<td class="p-1 px-2 whitespace-nowrap text-center">
<span class="text-xs text-gray-700 dark:text-gray-300">{nodeVMs()}</span>
</td>
<td class="px-2 py-0.5 whitespace-nowrap text-center">
<td class="p-1 px-2 whitespace-nowrap text-center">
<span class="text-xs text-gray-700 dark:text-gray-300">{nodeContainers()}</span>
</td>
<td class="px-2 py-0.5 whitespace-nowrap">
<td class="p-1 px-2 whitespace-nowrap">
<span class="text-xs text-gray-600 dark:text-gray-400">
{formatUptime(node.uptime)}
</span>
@@ -554,9 +554,9 @@ export function Dashboard(props: DashboardProps) {
>
<table class="w-full min-w-[900px] table-fixed">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<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 text-left text-xs font-medium text-gray-500 dark:text-gray-400 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="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"
onClick={() => handleSort('name')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('name')}
tabindex="0"
@@ -566,61 +566,61 @@ export function Dashboard(props: DashboardProps) {
Name {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[60px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[60px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('type')}
>
Type {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[70px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[70px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('vmid')}
>
VMID {sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[100px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[100px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('uptime')}
>
Uptime {sortKey() === 'uptime' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[140px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[140px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('cpu')}
>
CPU {sortKey() === 'cpu' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[140px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[140px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('memory')}
>
Memory {sortKey() === 'memory' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[140px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[140px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('disk')}
>
Disk {sortKey() === 'disk' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[90px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[90px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('diskRead')}
>
Disk Read {sortKey() === 'diskRead' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[90px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[90px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('diskWrite')}
>
Disk Write {sortKey() === 'diskWrite' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[90px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[90px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('networkIn')}
>
Net In {sortKey() === 'networkIn' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[90px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[90px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('networkOut')}
>
Net Out {sortKey() === 'networkOut' && (sortDirection() === 'asc' ? '▲' : '▼')}
@@ -633,7 +633,7 @@ export function Dashboard(props: DashboardProps) {
<>
<Show when={node}>
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
<td class="px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400 w-[200px]">
<td class="p-1 px-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"
@@ -644,7 +644,7 @@ export function Dashboard(props: DashboardProps) {
{node}
</a>
</td>
<td colspan="10" class="px-2 py-0.5"></td>
<td colspan="10" class="p-1 px-2"></td>
</tr>
</Show>
<For each={guests} fallback={<></>}>
@@ -97,12 +97,12 @@ export function GuestRow(props: GuestRowProps) {
<div class="flex items-center gap-2">
{/* Status indicator */}
<span class={`h-2 w-2 rounded-full flex-shrink-0 ${
isRunning() ? 'bg-green-500' : 'bg-red-500'
isRunning() ? 'bg-green-500' : 'bg-gray-400'
}`} title={props.guest.status}></span>
{/* Name - clickable if custom URL is set */}
<Show when={customUrl()} fallback={
<span class="font-medium text-gray-900 dark:text-gray-100 truncate" title={props.guest.name}>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate" title={props.guest.name}>
{props.guest.name}
</span>
}>
@@ -110,7 +110,7 @@ export function GuestRow(props: GuestRowProps) {
href={customUrl()}
target="_blank"
rel="noopener noreferrer"
class="font-medium text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-150 cursor-pointer truncate"
class="text-sm font-medium text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-150 cursor-pointer truncate"
title={`${props.guest.name} - Click to open custom URL`}
>
{props.guest.name}
@@ -217,12 +217,12 @@ export function GuestURLs(props: GuestURLsProps) {
return (
<tr class="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{guest.name}
</div>
</td>
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<span class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${
guest.type === 'qemu'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
@@ -231,10 +231,10 @@ export function GuestURLs(props: GuestURLsProps) {
{guest.type === 'qemu' ? 'VM' : 'LXC'}
</span>
</td>
<td class="px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400">
<td class="p-1 px-2 text-sm text-gray-600 dark:text-gray-400">
{guest.vmid}
</td>
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<input
type="text"
placeholder="https://192.168.1.100:8006"
@@ -245,7 +245,7 @@ export function GuestURLs(props: GuestURLsProps) {
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</td>
<td class="px-3 py-1.5">
<td class="p-1 px-2">
<div class="flex items-center gap-2">
<Show when={url}>
<a
@@ -109,12 +109,10 @@ const Storage: Component = () => {
});
const getProgressBarColor = (usage: number) => {
// Match MetricBar component styling - use the same disk/generic logic
// Match MetricBar component styling exactly - disk type thresholds
if (usage >= 90) return 'bg-red-500/60 dark:bg-red-500/50';
if (usage >= 80) return 'bg-yellow-500/60 dark:bg-yellow-500/50';
if (usage >= 70) return 'bg-amber-500/60 dark:bg-amber-500/50';
if (usage >= 60) return 'bg-yellow-500/60 dark:bg-yellow-500/50';
return 'bg-emerald-500/60 dark:bg-emerald-500/50';
return 'bg-green-500/60 dark:bg-green-500/50';
};
const resetFilters = () => {
@@ -266,20 +264,20 @@ const Storage: Component = () => {
`}</style>
<table class="w-full">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Storage</th>
<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">Storage</th>
<Show when={viewMode() === 'node'}>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider hidden sm:table-cell">Node</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden sm:table-cell">Node</th>
</Show>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider hidden md:table-cell">Type</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider hidden lg:table-cell">Content</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider hidden sm:table-cell">Status</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden md:table-cell">Type</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden lg:table-cell">Content</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden sm:table-cell">Status</th>
<Show when={viewMode() === 'node'}>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider hidden lg:table-cell">Shared</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden lg:table-cell">Shared</th>
</Show>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider min-w-[100px] sm:min-w-[150px] md:min-w-[200px]">Usage</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider hidden sm:table-cell">Free</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-[100px] sm:min-w-[150px] md:min-w-[200px]">Usage</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden sm:table-cell">Free</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">Total</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@@ -289,7 +287,7 @@ const Storage: Component = () => {
{/* Group Header */}
<Show when={viewMode() === 'node'}>
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
<td class="px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400">
<td class="p-0.5 px-1.5 text-xs font-medium text-gray-600 dark:text-gray-400">
<a
href={nodeHostMap()[groupName] || (groupName.includes(':') ? `https://${groupName}` : `https://${groupName}:8006`)}
target="_blank"
@@ -300,7 +298,7 @@ const Storage: Component = () => {
{groupName}
</a>
</td>
<td class="px-2 py-0.5 text-[10px] text-gray-500 dark:text-gray-400" colspan="8">
<td class="p-0.5 px-1.5 text-[10px] text-gray-500 dark:text-gray-400" colspan="8">
{getTotalByNode(storages).total > 0 && (
<span>
{formatBytes(getTotalByNode(storages).used)} / {formatBytes(getTotalByNode(storages).total)} ({calculateOverallUsage(storages).toFixed(1)}%)
@@ -321,9 +319,9 @@ const Storage: Component = () => {
return (
<tr class={`${rowClass} hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors`}>
<td class="px-2 py-0.5">
<td class="p-0.5 px-1.5">
<div class="flex items-center gap-2">
<span class="font-medium text-gray-900 dark:text-gray-100">
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
{storage.name}
</span>
<Show when={alertStyles.hasAlert}>
@@ -337,9 +335,9 @@ const Storage: Component = () => {
</div>
</td>
<Show when={viewMode() === 'node'}>
<td class="p-1 px-2 text-xs text-gray-600 dark:text-gray-400 hidden sm:table-cell">{storage.node}</td>
<td class="p-0.5 px-1.5 text-xs text-gray-600 dark:text-gray-400 hidden sm:table-cell">{storage.node}</td>
</Show>
<td class="p-1 px-2 hidden md:table-cell">
<td class="p-0.5 px-1.5 hidden md:table-cell">
<span class={`inline-block px-1.5 py-0.5 text-[10px] font-medium rounded ${
storage.type === 'dir' ? 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300' :
storage.type === 'pbs' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300' :
@@ -348,10 +346,10 @@ const Storage: Component = () => {
{storage.type}
</span>
</td>
<td class="p-1 px-2 text-xs text-gray-600 dark:text-gray-400 hidden lg:table-cell">
<td class="p-0.5 px-1.5 text-xs text-gray-600 dark:text-gray-400 hidden lg:table-cell">
{storage.content || '-'}
</td>
<td class="p-1 px-2 text-xs hidden sm:table-cell">
<td class="p-0.5 px-1.5 text-xs hidden sm:table-cell">
<span class={`${
storage.status === 'available' ? 'text-green-600 dark:text-green-400' :
'text-red-600 dark:text-red-400'
@@ -360,27 +358,26 @@ const Storage: Component = () => {
</span>
</td>
<Show when={viewMode() === 'node'}>
<td class="p-1 px-2 text-xs text-gray-600 dark:text-gray-400 hidden lg:table-cell">
<td class="p-0.5 px-1.5 text-xs text-gray-600 dark:text-gray-400 hidden lg:table-cell">
{storage.shared ? '✓' : '-'}
</td>
</Show>
<td class="px-2 py-0.5">
<td class="p-0.5 px-1.5">
<div class="relative w-full h-3.5 rounded overflow-hidden bg-gray-200 dark:bg-gray-600">
<div
class={`absolute top-0 left-0 h-full ${getProgressBarColor(usagePercent)}`}
style={{ width: `${usagePercent}%` }}
/>
<span class="absolute inset-0 flex items-center justify-center text-[10px] font-medium text-gray-800 dark:text-gray-100 leading-none">
<span class="truncate px-1">
<span class="sm:hidden">{usagePercent.toFixed(0)}%</span>
<span class="hidden sm:inline">{formatBytes(storage.used || 0)} / {formatBytes(storage.total || 0)} ({usagePercent.toFixed(1)}%)</span>
<span class="whitespace-nowrap px-0.5">
{usagePercent.toFixed(0)}% ({formatBytes(storage.used || 0)}/{formatBytes(storage.total || 0)})
</span>
</span>
</div>
</td>
<td class="p-1 px-2 text-xs hidden sm:table-cell">{formatBytes(storage.free || 0)}</td>
<td class="p-1 px-2 text-xs">{formatBytes(storage.total || 0)}</td>
<td class="p-0.5 px-1.5 text-xs hidden sm:table-cell">{formatBytes(storage.free || 0)}</td>
<td class="p-0.5 px-1.5 text-xs">{formatBytes(storage.total || 0)}</td>
</tr>
);
}}
@@ -90,22 +90,22 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
<div class="overflow-x-auto" style="overflow-x: auto;">
<table style="width: 1000px; table-layout: fixed;">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" style="width: 200px; white-space: nowrap;">
<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" style="width: 200px; white-space: nowrap;">
{props.currentTab === 'backups' ? 'Node / PBS' : 'Node'}
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" style="width: 80px; white-space: nowrap;">Status</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" style="width: 200px; white-space: nowrap;">CPU</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" style="width: 200px; white-space: nowrap;">Memory</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" style="width: 200px; white-space: nowrap;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider" style="width: 80px; white-space: nowrap;">Status</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider" style="width: 200px; white-space: nowrap;">CPU</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider" style="width: 200px; white-space: nowrap;">Memory</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider" style="width: 200px; white-space: nowrap;">
{props.currentTab === 'backups' && props.pbsInstances ? 'Storage / Disk' : 'Disk'}
</th>
<For each={getCountHeader()}>
{(header) => (
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" style="width: 80px; white-space: nowrap;">{header}</th>
<th class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider" style="width: 80px; white-space: nowrap;">{header}</th>
)}
</For>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" style="width: 100px; white-space: nowrap;">Uptime</th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider" style="width: 100px; white-space: nowrap;">Uptime</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@@ -172,7 +172,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
href={isPVE ? (node!.host || `https://${node!.name}:8006`) : (pbs!.host || `https://${pbs!.name}:8007`)}
target="_blank"
onClick={(e) => e.stopPropagation()}
class="font-medium text-xs text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 whitespace-nowrap"
class="font-medium text-[11px] text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 whitespace-nowrap"
>
{item.data.name}
</a>
@@ -91,29 +91,29 @@ export const PBSNodeTable: Component<PBSNodeTableProps> = (props) => {
`}</style>
<table class="w-full" style="min-width: 1000px;">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 200px;">
<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 whitespace-nowrap" style="min-width: 200px;">
PBS Instances
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 80px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 80px;">
Status
</th>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 100px;">
<th class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 100px;">
Datastores
</th>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 80px;">
<th class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 80px;">
Backups
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
CPU
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
Memory
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
Storage Used
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap">
Uptime
</th>
</tr>
@@ -164,7 +164,7 @@ export const PBSNodeTable: Component<PBSNodeTableProps> = (props) => {
isClickable && props.onNodeClick(pbs.name);
}}
>
<td class="px-2 py-0.5 whitespace-nowrap">
<td class="p-1 px-2 whitespace-nowrap">
<div class="flex items-center gap-2">
<Show when={hasDatastoresWithNamespaces()}>
<button
@@ -198,22 +198,22 @@ export const PBSNodeTable: Component<PBSNodeTableProps> = (props) => {
</span>
</div>
</td>
<td class="px-2 py-0.5">
<td class="p-1 px-2">
<span class={`text-xs ${isOnline() ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{pbs.status}
</span>
</td>
<td class="px-2 py-0.5 text-center">
<td class="p-1 px-2 text-center">
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">
{pbs.datastores?.length || 0}
</span>
</td>
<td class="px-2 py-0.5 text-center">
<td class="p-1 px-2 text-center">
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">
{props.backupCounts?.[pbs.name] || 0}
</span>
</td>
<td class="px-2 py-0.5" style="min-width: 180px;">
<td class="p-1 px-2" style="min-width: 180px;">
<Show when={isOnline()} fallback={<span class="text-xs text-gray-400">-</span>}>
<MetricBar
value={cpuPercent()}
@@ -222,7 +222,7 @@ export const PBSNodeTable: Component<PBSNodeTableProps> = (props) => {
/>
</Show>
</td>
<td class="px-2 py-0.5" style="min-width: 180px;">
<td class="p-1 px-2" style="min-width: 180px;">
<Show when={isOnline()} fallback={<span class="text-xs text-gray-400">-</span>}>
<MetricBar
value={memPercent()}
@@ -232,7 +232,7 @@ export const PBSNodeTable: Component<PBSNodeTableProps> = (props) => {
/>
</Show>
</td>
<td class="px-2 py-0.5" style="min-width: 180px;">
<td class="p-1 px-2" style="min-width: 180px;">
<Show when={isOnline() && totalStorage().total > 0} fallback={<span class="text-xs text-gray-400">-</span>}>
<MetricBar
value={totalStorage().percent}
@@ -242,7 +242,7 @@ export const PBSNodeTable: Component<PBSNodeTableProps> = (props) => {
/>
</Show>
</td>
<td class="px-2 py-0.5">
<td class="p-1 px-2">
<span class="text-xs text-gray-600 dark:text-gray-400">
<Show when={isOnline() && pbs.uptime} fallback="-">
{formatUptime(pbs.uptime)}
@@ -157,33 +157,33 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
`}</style>
<table class="w-full" style="min-width: 1000px;">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 200px;">
<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 whitespace-nowrap" style="min-width: 200px;">
PVE Nodes
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 80px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 80px;">
Status
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 100px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 100px;">
Cluster
</th>
<For each={getCountHeaders()}>
{(header) => (
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 80px;">
<th class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 80px;">
{header}
</th>
)}
</For>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
CPU
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
Memory
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 180px;">
Storage
</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap">
Uptime
</th>
</tr>
@@ -224,7 +224,7 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
`}
onClick={() => props.onNodeClick(node.name)}
>
<td class="px-2 py-0.5 whitespace-nowrap">
<td class="p-1 px-2 whitespace-nowrap">
<div class="flex items-center gap-2">
<span class={`h-2 w-2 rounded-full flex-shrink-0 ${isOnline() ? 'bg-green-500' : 'bg-red-500'}`}></span>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate" title={node.name}>{node.name}</span>
@@ -235,26 +235,26 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
</Show>
</div>
</td>
<td class="px-2 py-0.5">
<td class="p-1 px-2">
<span class={`text-xs whitespace-nowrap ${isOnline() ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{node.status}
</span>
</td>
<td class="px-2 py-0.5">
<td class="p-1 px-2">
<span class="text-xs text-gray-600 dark:text-gray-400 block truncate" title={node.clusterName || ''}>
{node.clusterName || '-'}
</span>
</td>
<For each={counts}>
{(count) => (
<td class="px-2 py-0.5 text-center">
<td class="p-1 px-2 text-center">
<span class="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">
{count}
</span>
</td>
)}
</For>
<td class="px-2 py-0.5" style="min-width: 180px;">
<td class="p-1 px-2" style="min-width: 180px;">
<Show when={isOnline()} fallback={<span class="text-xs text-gray-400">-</span>}>
<MetricBar
value={cpuPercent()}
@@ -264,7 +264,7 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
/>
</Show>
</td>
<td class="px-2 py-0.5" style="min-width: 180px;">
<td class="p-1 px-2" style="min-width: 180px;">
<Show when={isOnline()} fallback={<span class="text-xs text-gray-400">-</span>}>
<MetricBar
value={memPercent()}
@@ -274,7 +274,7 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
/>
</Show>
</td>
<td class="px-2 py-0.5" style="min-width: 180px;">
<td class="p-1 px-2" style="min-width: 180px;">
<Show when={isOnline()} fallback={<span class="text-xs text-gray-400">-</span>}>
<MetricBar
value={storagePercent()}
@@ -284,7 +284,7 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
/>
</Show>
</td>
<td class="px-2 py-0.5">
<td class="p-1 px-2">
<span class="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap">
<Show when={isOnline() && node.uptime} fallback="-">
{formatUptime(node.uptime)}