mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 03:33:53 +00:00
feat: improve UI consistency and visual feedback
- Remove redundant status column from node tables (green/red dot is sufficient) - Make stopped guests more visually distinct (red indicator + dimmed row) - Improve node selection behavior (visual only, no search field modification) - Fix hover vs selected visual states for clarity - Standardize hover highlighting across all tables (hover:bg-gray-50 dark:hover:bg-gray-700/30) - Add better visual separation for tooltips (tags and backups) - Remove unnecessary rebuild.sh script (only needed at release time) - Fix backend-watch.sh to use correct PORT environment variable - Update vite.config.ts to proxy to correct backend port (7656) - Clean up console.log debugging statements
This commit is contained in:
@@ -90,7 +90,7 @@ export function EmailProviderSelect(props: EmailProviderSelectProps) {
|
||||
class={`p-3 text-left rounded-lg border transition-all ${
|
||||
props.config.provider === provider.name
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/30'
|
||||
}`}
|
||||
>
|
||||
<div class="font-medium text-sm text-gray-800 dark:text-gray-200">
|
||||
|
||||
@@ -220,7 +220,7 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
class={`p-3 text-left rounded-lg border transition-all ${
|
||||
formData().service === service
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/30'
|
||||
}`}
|
||||
>
|
||||
<div class="font-medium text-sm text-gray-800 dark:text-gray-200">
|
||||
|
||||
@@ -956,7 +956,7 @@ const UnifiedBackups: Component = () => {
|
||||
|
||||
return (
|
||||
<tr
|
||||
class={`hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer transition-colors ${
|
||||
class={`hover:bg-gray-50 dark:hover:bg-gray-700/30 cursor-pointer transition-colors ${
|
||||
isSelected() ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
@@ -1682,7 +1682,7 @@ const UnifiedBackups: Component = () => {
|
||||
</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">
|
||||
<tr class="border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="p-0.5 px-1.5 text-sm align-middle">
|
||||
{item.name || '-'}
|
||||
</td>
|
||||
@@ -1726,14 +1726,14 @@ const UnifiedBackups: Component = () => {
|
||||
{item.backupType === 'snapshot' ? 'Snapshot' : item.backupType === 'local' ? 'PVE' : 'PBS'}
|
||||
</span>
|
||||
<Show when={item.encrypted}>
|
||||
<span title="Encrypted backup" class="text-green-600 dark:text-green-400">
|
||||
<span title="Encrypted backup" class="text-green-600 dark:text-green-400 inline-block ml-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item.protected}>
|
||||
<span title="Protected backup" class="text-blue-600 dark:text-blue-400">
|
||||
<span title="Protected backup" class="text-blue-600 dark:text-blue-400 inline-block ml-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
@@ -1850,7 +1850,7 @@ const UnifiedBackups: Component = () => {
|
||||
class="fixed z-[9999] px-3 py-2 text-sm bg-black text-white rounded-lg shadow-xl pointer-events-none"
|
||||
style={{
|
||||
left: `${tooltip()!.x - 75}px`,
|
||||
top: `${tooltip()!.y}px`,
|
||||
top: `${tooltip()!.y - 35}px`,
|
||||
"max-width": "200px",
|
||||
"white-space": "pre-line",
|
||||
"font-family": "system-ui, -apple-system, sans-serif"
|
||||
|
||||
@@ -27,6 +27,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
const { connected, activeAlerts, initialDataReceived } = useWebSocket();
|
||||
const [search, setSearch] = createSignal('');
|
||||
const [isSearchLocked, setIsSearchLocked] = createSignal(false);
|
||||
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
|
||||
|
||||
// Initialize from localStorage with proper type checking
|
||||
const storedViewMode = localStorage.getItem('dashboardViewMode');
|
||||
@@ -192,6 +193,14 @@ export function Dashboard(props: DashboardProps) {
|
||||
const filteredGuests = createMemo(() => {
|
||||
let guests = allGuests();
|
||||
|
||||
// Filter by selected node
|
||||
const node = selectedNode();
|
||||
console.log('Filtering guests - selected node:', node, 'total guests:', guests.length);
|
||||
if (node) {
|
||||
guests = guests.filter(g => g.node === node);
|
||||
console.log('After node filter:', guests.length);
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
if (viewMode() === 'vm') {
|
||||
guests = guests.filter(g => g.type === 'qemu');
|
||||
@@ -388,18 +397,15 @@ export function Dashboard(props: DashboardProps) {
|
||||
|
||||
|
||||
const handleNodeSelect = (nodeId: string | null, nodeType: 'pve' | 'pbs' | null) => {
|
||||
if (nodeId && nodeType === 'pve') {
|
||||
// Set search to filter by node
|
||||
const nodeFilter = `node:${nodeId}`;
|
||||
setSearch(nodeFilter);
|
||||
setIsSearchLocked(true);
|
||||
if (!showFilters()) {
|
||||
console.log('handleNodeSelect called:', nodeId, nodeType);
|
||||
// Track selected node for filtering
|
||||
if (nodeType === 'pve' || nodeType === null) {
|
||||
setSelectedNode(nodeId);
|
||||
console.log('Set selected node to:', nodeId);
|
||||
// Show filters if a node is selected
|
||||
if (nodeId && !showFilters()) {
|
||||
setShowFilters(true);
|
||||
}
|
||||
} else {
|
||||
// Clear node filter
|
||||
setSearch('');
|
||||
setIsSearchLocked(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -498,7 +504,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
|
||||
return (
|
||||
<tr
|
||||
class={`hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer transition-colors ${
|
||||
class={`hover:bg-gray-50 dark:hover:bg-gray-700/30 cursor-pointer transition-colors ${
|
||||
isSelected() ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
@@ -817,7 +823,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
</span>
|
||||
<span class="text-gray-400">|</span>
|
||||
<span class="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
<span class="h-2 w-2 bg-gray-400 rounded-full"></span>
|
||||
<span class="h-2 w-2 bg-red-500 rounded-full"></span>
|
||||
{totalStats().stopped} stopped
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -27,16 +27,10 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
||||
type="text"
|
||||
placeholder="Search by name, cpu>80, memory<20, tags:prod, node:pve1"
|
||||
value={props.search()}
|
||||
onInput={(e) => {
|
||||
if (!props.isSearchLocked()) {
|
||||
props.setSearch(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
disabled={props.isSearchLocked()}
|
||||
onInput={(e) => props.setSearch(e.currentTarget.value)}
|
||||
class={`w-full pl-9 pr-9 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500
|
||||
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all
|
||||
${props.isSearchLocked() ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all`}
|
||||
title="Search guests or use filters like cpu>80"
|
||||
/>
|
||||
<svg class="absolute left-3 top-2 h-4 w-4 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
||||
@@ -86,8 +86,9 @@ export function GuestRow(props: GuestRowProps) {
|
||||
? 'bg-red-50 dark:bg-red-950/30'
|
||||
: 'bg-yellow-50 dark:bg-yellow-950/20')
|
||||
: '';
|
||||
const defaultHover = props.alertStyles?.hasAlert ? '' : 'hover:bg-gray-50 dark:hover:bg-gray-700';
|
||||
return `${base} ${hover} ${defaultHover} ${alertBg}`;
|
||||
const defaultHover = props.alertStyles?.hasAlert ? '' : 'hover:bg-gray-50 dark:hover:bg-gray-700/30';
|
||||
const stoppedDimming = !isRunning() ? 'opacity-60' : '';
|
||||
return `${base} ${hover} ${defaultHover} ${alertBg} ${stoppedDimming}`;
|
||||
});
|
||||
|
||||
// Get first cell styling with left border for alerts
|
||||
@@ -108,7 +109,7 @@ 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-gray-400'
|
||||
isRunning() ? 'bg-green-500' : 'bg-red-500'
|
||||
}`} title={props.guest.status}></span>
|
||||
|
||||
{/* Name - clickable if custom URL is set */}
|
||||
|
||||
@@ -93,7 +93,7 @@ export const TagBadges: Component<TagBadgesProps> = (props) => {
|
||||
class="fixed px-2 py-1 bg-gray-800 dark:bg-gray-700 text-white text-xs rounded shadow-lg pointer-events-none"
|
||||
style={{
|
||||
left: `${tooltipPos()!.x}px`,
|
||||
top: `${tooltipPos()!.y - 28}px`,
|
||||
top: `${tooltipPos()!.y - 40}px`,
|
||||
transform: 'translateX(-50%)',
|
||||
'z-index': '999999',
|
||||
}}
|
||||
@@ -114,7 +114,7 @@ export const TagBadges: Component<TagBadgesProps> = (props) => {
|
||||
class="fixed px-2 py-1 text-xs rounded shadow-lg pointer-events-none"
|
||||
style={{
|
||||
left: `${tooltipPos()!.x}px`,
|
||||
top: `${tooltipPos()!.y - 24}px`,
|
||||
top: `${tooltipPos()!.y - 35}px`,
|
||||
transform: 'translateX(-50%)',
|
||||
'background-color': colors.bg,
|
||||
'color': colors.text,
|
||||
|
||||
@@ -325,7 +325,7 @@ export const DiscoveryModal: Component<DiscoveryModalProps> = (props) => {
|
||||
<For each={discoveryResult()!.servers}>
|
||||
{(server) => (
|
||||
<div
|
||||
class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 cursor-pointer transition-all hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 hover:bg-gray-50 dark:hover:bg-gray-700/50 group"
|
||||
class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 cursor-pointer transition-all hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 hover:bg-gray-50 dark:hover:bg-gray-700/30 group"
|
||||
onClick={() => handleAddServer(server)}
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
|
||||
@@ -328,7 +328,7 @@ const Storage: Component = () => {
|
||||
: 'p-0.5 px-1.5';
|
||||
|
||||
return (
|
||||
<tr class={`${rowClass} hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors`}>
|
||||
<tr class={`${rowClass} hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors`}>
|
||||
<td class={firstCellClass}>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
|
||||
@@ -161,7 +161,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
||||
|
||||
return (
|
||||
<tr
|
||||
class={`hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer transition-colors ${
|
||||
class={`hover:bg-gray-50 dark:hover:bg-gray-700/30 cursor-pointer transition-colors ${
|
||||
isSelected() ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||
}`}
|
||||
onClick={() => props.onNodeClick(nodeId, item.type)}
|
||||
|
||||
@@ -37,38 +37,15 @@ export const PBSNodeTable: Component<PBSNodeTableProps> = (props) => {
|
||||
return props.searchTerm === expectedFilter;
|
||||
};
|
||||
|
||||
// Filter and sort PBS instances
|
||||
// Filter and sort PBS instances - but don't hide them when filtering, similar to PVE nodes
|
||||
const sortedInstances = createMemo(() => {
|
||||
if (!props.pbsInstances) return [];
|
||||
|
||||
let instances = [...props.pbsInstances];
|
||||
|
||||
// If we have filtered backups in backups tab, only show PBS instances with matching backups
|
||||
if (props.currentTab === 'backups' && props.filteredBackups !== undefined) {
|
||||
const pbsWithBackups = new Set<string>();
|
||||
|
||||
props.filteredBackups.forEach(b => {
|
||||
// PBS backups can have node as the PBS instance name or 'PBS' generic
|
||||
// Check if it's a PBS backup (has datastore or node is PBS instance)
|
||||
if (b.datastore || b.node === 'PBS' || b.backupType === 'remote') {
|
||||
if (b.node === 'PBS' || !b.node) {
|
||||
// Generic PBS backup, show all PBS instances
|
||||
props.pbsInstances?.forEach(pbs => pbsWithBackups.add(pbs.name));
|
||||
} else if (props.pbsInstances?.some(pbs => pbs.name === b.node)) {
|
||||
// Specific PBS instance
|
||||
pbsWithBackups.add(b.node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If we have any PBS backups, filter to matching instances
|
||||
if (pbsWithBackups.size > 0) {
|
||||
instances = instances.filter(pbs => pbsWithBackups.has(pbs.name));
|
||||
} else {
|
||||
// No PBS backups found in the filtered results, hide PBS table
|
||||
instances = [];
|
||||
}
|
||||
}
|
||||
// For PBS instances, always show all of them
|
||||
// The selection/highlighting will indicate which one is filtered
|
||||
// This keeps the PBS cards as a stable navigation element
|
||||
|
||||
return instances.sort((a, b) => {
|
||||
// Healthy/online instances first
|
||||
|
||||
@@ -41,7 +41,7 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
|
||||
|
||||
let nodes = [...props.nodes];
|
||||
|
||||
// Special handling for backups tab since it uses different filtering logic
|
||||
// Special handling for backups tab - only hide nodes that have no backups at all
|
||||
if (props.currentTab === 'backups') {
|
||||
// Filter nodes to only show those with backups
|
||||
// First, check if nodes have any backups at all (from backupCounts)
|
||||
@@ -49,57 +49,11 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
|
||||
const backupCount = props.backupCounts?.[node.name] || 0;
|
||||
return backupCount > 0;
|
||||
});
|
||||
|
||||
// Then apply additional filtering if filteredBackups is provided
|
||||
if (props.filteredBackups !== undefined) {
|
||||
const nodesWithItems = new Set<string>();
|
||||
|
||||
// Filtering is active, only show nodes with matching backups
|
||||
props.filteredBackups.forEach(b => {
|
||||
// Only count snapshots and local backups that actually belong to PVE nodes
|
||||
if (b.backupType === 'snapshot' || b.backupType === 'local') {
|
||||
// Make sure it's actually a PVE node (not a PBS instance name)
|
||||
if (props.nodes.some(n => n.name === b.node)) {
|
||||
nodesWithItems.add(b.node);
|
||||
}
|
||||
}
|
||||
// PBS/remote backups have node set to PBS instance name, not PVE node
|
||||
// so we don't add them to PVE node filter
|
||||
});
|
||||
|
||||
// Only show nodes that have matching items
|
||||
if (nodesWithItems.size > 0) {
|
||||
nodes = nodes.filter(node => nodesWithItems.has(node.name));
|
||||
} else {
|
||||
// If we have active filtering but no nodes have matching items, hide all nodes
|
||||
nodes = [];
|
||||
}
|
||||
}
|
||||
// If filteredBackups is undefined, still filter by backupCounts
|
||||
} else if (hasActiveFilter()) {
|
||||
// Handle other tabs with normal filtering logic
|
||||
const nodesWithItems = new Set<string>();
|
||||
|
||||
switch (props.currentTab) {
|
||||
case 'dashboard':
|
||||
// Filter based on VMs and containers
|
||||
props.vms?.forEach(vm => nodesWithItems.add(vm.node));
|
||||
props.containers?.forEach(ct => nodesWithItems.add(ct.node));
|
||||
break;
|
||||
case 'storage':
|
||||
// Filter based on storage
|
||||
props.storage?.forEach(s => nodesWithItems.add(s.node));
|
||||
break;
|
||||
}
|
||||
|
||||
// Only show nodes that have filtered items
|
||||
if (nodesWithItems.size > 0) {
|
||||
nodes = nodes.filter(node => nodesWithItems.has(node.name));
|
||||
} else if (hasActiveFilter()) {
|
||||
// If we have active filtering but no nodes have matching items, hide all nodes
|
||||
nodes = [];
|
||||
}
|
||||
// Don't further filter based on search - let highlighting handle that
|
||||
}
|
||||
// For other tabs, ALWAYS show all nodes - don't filter them out
|
||||
// The node cards should remain as a stable navigation element
|
||||
// Only the data below gets filtered
|
||||
|
||||
return nodes.sort((a, b) => {
|
||||
// Online nodes first
|
||||
@@ -167,9 +121,6 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
|
||||
<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.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.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap" style="min-width: 100px;">
|
||||
Cluster
|
||||
</th>
|
||||
@@ -216,36 +167,29 @@ export const PVENodeTable: Component<PVENodeTableProps> = (props) => {
|
||||
const filteredGuestCount = createMemo(() => getFilteredGuestCount());
|
||||
|
||||
const counts = getNodeCounts(node);
|
||||
// Check if this node is selected
|
||||
const isSelected = () => props.selectedNode === node.name;
|
||||
|
||||
return (
|
||||
<tr
|
||||
class={`
|
||||
border-b border-gray-100 dark:border-gray-700/50 hover:bg-gray-50 dark:hover:bg-gray-700/30
|
||||
transition-all duration-150 ease-in-out cursor-pointer h-8
|
||||
hover:scale-[1.01] hover:shadow-md hover:z-10 relative
|
||||
hover:border-l-4 hover:border-l-blue-500 dark:hover:border-l-blue-400
|
||||
border-b border-gray-100 dark:border-gray-700/50
|
||||
transition-all duration-200 ease-out cursor-pointer h-8
|
||||
hover:bg-gray-50 dark:hover:bg-gray-700/30
|
||||
${!isOnline() ? 'opacity-60' : ''}
|
||||
${isSelected() ? 'bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 scale-[1.005] shadow-sm border-l-4 border-l-blue-600 dark:border-l-blue-500' : ''}
|
||||
${isSelected()
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-l-4 border-l-blue-500 dark:border-l-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/30'
|
||||
: ''
|
||||
}
|
||||
`}
|
||||
onClick={() => props.onNodeClick(node.name)}
|
||||
>
|
||||
<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>
|
||||
<Show when={filteredGuestCount() !== null && props.searchTerm && props.searchTerm.trim()}>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
({filteredGuestCount()} matched)
|
||||
</span>
|
||||
</Show>
|
||||
<span class={`text-sm truncate ${isSelected() ? 'font-semibold text-blue-700 dark:text-blue-300' : 'font-medium text-gray-900 dark:text-gray-100'}`} title={node.name}>{node.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<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="p-1 px-2">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 block truncate" title={node.clusterName || ''}>
|
||||
{node.clusterName || '-'}
|
||||
|
||||
@@ -36,10 +36,10 @@ const Tooltip: Component<TooltipProps> = (props) => {
|
||||
|
||||
// Calculate position to keep tooltip on screen
|
||||
const rect = tooltipRef.getBoundingClientRect();
|
||||
const padding = 10;
|
||||
const padding = 20; // Increased padding for better separation
|
||||
|
||||
let x = props.x + padding;
|
||||
let y = props.y - rect.height - padding;
|
||||
let y = props.y - rect.height - padding - 10; // Extra 10px vertical separation
|
||||
|
||||
// Keep within viewport
|
||||
if (x + rect.width > window.innerWidth) {
|
||||
|
||||
@@ -26,6 +26,9 @@ export const UnifiedNodeSelector: Component<UnifiedNodeSelectorProps> = (props)
|
||||
setSelectedNode(null);
|
||||
});
|
||||
|
||||
// No longer syncing with search term - selection is independent
|
||||
// This allows users to select a node AND search within it
|
||||
|
||||
// Calculate backup counts for nodes and PBS instances
|
||||
const backupCounts = createMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
|
||||
@@ -190,4 +190,21 @@
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Node row click animation */
|
||||
@keyframes nodeClick {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(0.98);
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.node-click {
|
||||
animation: nodeClick 0.15s ease-out;
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ export default defineConfig({
|
||||
host: '0.0.0.0', // Listen on all interfaces for remote access
|
||||
proxy: {
|
||||
'/ws': {
|
||||
target: 'ws://127.0.0.1:3000',
|
||||
target: 'ws://127.0.0.1:7656',
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:3000',
|
||||
target: 'http://127.0.0.1:7656',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Rebuild script for Pulse development
|
||||
|
||||
echo "Building frontend..."
|
||||
cd /opt/pulse/frontend-modern && npm run build
|
||||
|
||||
echo "Copying frontend to embed location..."
|
||||
cp -r /opt/pulse/frontend-modern/dist /opt/pulse/internal/api/frontend-modern/
|
||||
|
||||
echo "Building Go binary..."
|
||||
cd /opt/pulse && go build -o pulse ./cmd/pulse
|
||||
|
||||
echo "Restarting service..."
|
||||
sudo systemctl restart pulse-backend
|
||||
|
||||
echo "Done!"
|
||||
echo "Access Pulse at: http://localhost:7655"
|
||||
Reference in New Issue
Block a user