Pulse/frontend-modern/src/components/UpdateBanner.tsx
rcourtman 6eb1a10d9b Refactor: Code cleanup and localStorage consolidation
This commit includes comprehensive codebase cleanup and refactoring:

## Code Cleanup
- Remove dead TypeScript code (types/monitoring.ts - 194 lines duplicate)
- Remove unused Go functions (GetClusterNodes, MigratePassword, GetClusterHealthInfo)
- Clean up commented-out code blocks across multiple files
- Remove unused TypeScript exports (helpTextClass, private tag color helpers)
- Delete obsolete test files and components

## localStorage Consolidation
- Centralize all storage keys into STORAGE_KEYS constant
- Update 5 files to use centralized keys:
  * utils/apiClient.ts (AUTH, LEGACY_TOKEN)
  * components/Dashboard/Dashboard.tsx (GUEST_METADATA)
  * components/Docker/DockerHosts.tsx (DOCKER_METADATA)
  * App.tsx (PLATFORMS_SEEN)
  * stores/updates.ts (UPDATES)
- Benefits: Single source of truth, prevents typos, better maintainability

## Previous Work Committed
- Docker monitoring improvements and disk metrics
- Security enhancements and setup fixes
- API refactoring and cleanup
- Documentation updates
- Build system improvements

## Testing
- All frontend tests pass (29 tests)
- All Go tests pass (15 packages)
- Production build successful
- Zero breaking changes

Total: 186 files changed, 5825 insertions(+), 11602 deletions(-)
2025-11-04 21:50:46 +00:00

312 lines
13 KiB
TypeScript

import { Show, createSignal, createEffect, For, useContext } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { updateStore } from '@/stores/updates';
import { UpdatesAPI, type UpdatePlan } from '@/api/updates';
import { UpdateConfirmationModal } from './UpdateConfirmationModal';
import { UpdateProgressModal } from './UpdateProgressModal';
import { WebSocketContext } from '@/App';
import { copyToClipboard } from '@/utils/clipboard';
import { logger } from '@/utils/logger';
export function UpdateBanner() {
const navigate = useNavigate();
const wsContext = useContext(WebSocketContext);
const [isExpanded, setIsExpanded] = createSignal(false);
const [updatePlan, setUpdatePlan] = createSignal<UpdatePlan | null>(null);
const [showConfirmModal, setShowConfirmModal] = createSignal(false);
const [showProgressModal, setShowProgressModal] = createSignal(false);
const [isApplying, setIsApplying] = createSignal(false);
const [copiedIndex, setCopiedIndex] = createSignal<number | null>(null);
// Fetch update plan when update info is available
createEffect(async () => {
const info = updateStore.updateInfo();
if (info?.available && info.latestVersion) {
try {
const plan = await UpdatesAPI.getUpdatePlan(info.latestVersion);
setUpdatePlan(plan);
} catch (error) {
logger.error('Failed to fetch update plan', error);
}
}
});
const handleApplyUpdate = () => {
setShowConfirmModal(true);
};
const handleConfirmUpdate = async () => {
const info = updateStore.updateInfo();
if (!info?.downloadUrl) return;
setIsApplying(true);
try {
await UpdatesAPI.applyUpdate(info.downloadUrl);
// Close confirmation and show progress
setShowConfirmModal(false);
setShowProgressModal(true);
} catch (error) {
logger.error('Failed to start update', error);
alert('Failed to start update. Please try again.');
} finally {
setIsApplying(false);
}
};
const handleCopy = async (text: string, index: number) => {
const success = await copyToClipboard(text);
if (!success) {
logger.error('Failed to copy update instruction to clipboard', new Error('clipboard copy failed'));
return;
}
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
};
// Get deployment type message
const getUpdateInstructions = () => {
const versionInfo = updateStore.versionInfo();
const deploymentType = versionInfo?.deploymentType || 'systemd';
switch (deploymentType) {
case 'proxmoxve':
return "ProxmoxVE users: type 'update' in console";
case 'docker':
return 'Docker: pull latest image';
case 'source':
return 'Source: pull and rebuild';
default:
return ''; // No message, just the version info
}
};
const getShortMessage = () => {
const info = updateStore.updateInfo();
if (!info) return '';
return `New version available: ${info.latestVersion}`;
};
return (
<Show when={updateStore.isUpdateVisible()}>
<div class="bg-blue-50 dark:bg-blue-900/20 border-b border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200 relative animate-slideDown">
<div class="px-4 py-1.5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
{/* Update icon */}
<svg
class="w-4 h-4 flex-shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M12 2v6m0 0l3-3m-3 3l-3-3"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M2 17l.621 2.485A2 2 0 0 0 4.561 21h14.878a2 2 0 0 0 1.94-1.515L22 17"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div class="flex items-center gap-3 flex-wrap">
<span class="text-sm font-medium">{getShortMessage()}</span>
{/* Apply Update Button (automated deployments) */}
<Show when={updatePlan()?.canAutoUpdate && !isExpanded()}>
<button
onClick={handleApplyUpdate}
class="px-3 py-1 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors"
>
Apply Update
</button>
</Show>
{/* Manual Steps Badge (non-automated deployments) */}
<Show when={updatePlan() && !updatePlan()?.canAutoUpdate && !isExpanded()}>
<span class="px-2 py-0.5 text-xs font-medium bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-200 rounded">
Manual steps required
</span>
</Show>
{!isExpanded() && getUpdateInstructions() && (
<>
<span class="text-blue-600 dark:text-blue-400 text-sm hidden sm:inline"></span>
<span class="text-blue-600 dark:text-blue-400 text-sm hidden sm:inline">
{getUpdateInstructions()}
</span>
</>
)}
{!isExpanded() && (
<a
href={`https://github.com/rcourtman/Pulse/releases/tag/${updateStore.updateInfo()?.latestVersion}`}
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 underline text-sm hidden sm:inline hover:text-blue-700 dark:hover:text-blue-300"
>
View details
</a>
)}
</div>
</div>
<div class="flex items-center gap-2">
{/* Expand/Collapse button */}
<button
onClick={() => setIsExpanded(!isExpanded())}
class="p-1 hover:bg-blue-100 dark:hover:bg-blue-800/30 rounded transition-colors"
title={isExpanded() ? 'Show less' : 'Show more'}
>
<svg
class={`w-4 h-4 transform transition-transform ${isExpanded() ? 'rotate-180' : ''}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
{/* Dismiss button */}
<button
onClick={() => updateStore.dismissUpdate()}
class="p-1 hover:bg-blue-100 dark:hover:bg-blue-800/30 rounded transition-colors"
title="Dismiss this update"
>
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
{/* Expanded content */}
<Show when={isExpanded()}>
<div class="mt-2 pb-1">
<div class="text-sm text-blue-700 dark:text-blue-300 space-y-1">
<p>
<span class="font-medium">Current:</span>{' '}
{updateStore.versionInfo()?.version || 'Unknown'}
<span class="font-medium ml-1">Latest:</span>{' '}
{updateStore.updateInfo()?.latestVersion}
</p>
{getUpdateInstructions() && (
<p>
<span class="font-medium">Quick upgrade:</span> {getUpdateInstructions()}
</p>
)}
<Show when={updateStore.updateInfo()?.isPrerelease}>
<p class="text-orange-600 dark:text-orange-400 text-xs">
This is a pre-release version
</p>
</Show>
{/* Manual Update Instructions */}
<Show when={updatePlan()?.instructions && (updatePlan()?.instructions?.length ?? 0) > 0}>
<div class="mt-3 pt-3 border-t border-blue-200 dark:border-blue-800">
<div class="font-medium mb-2">Update Instructions:</div>
<div class="space-y-2">
<For each={updatePlan()?.instructions || []}>
{(instruction, index) => (
<div class="bg-gray-50 dark:bg-gray-900/50 rounded border border-blue-200 dark:border-blue-700 p-2">
<div class="flex items-start justify-between gap-2">
<code class="text-xs text-gray-800 dark:text-gray-200 font-mono flex-1 break-all">
{instruction}
</code>
<button
onClick={() => handleCopy(instruction, index())}
class="flex-shrink-0 p-1 hover:bg-blue-100 dark:hover:bg-blue-800/30 rounded transition-colors"
title="Copy to clipboard"
>
<Show when={copiedIndex() === index()} fallback={
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
}>
<svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</Show>
</button>
</div>
</div>
)}
</For>
</div>
</div>
</Show>
{/* Apply Update Button (expanded view for automated deployments) */}
<Show when={updatePlan()?.canAutoUpdate}>
<div class="mt-3 pt-3 border-t border-blue-200 dark:border-blue-800">
<button
onClick={handleApplyUpdate}
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors"
>
Apply Update Automatically
</button>
</div>
</Show>
<div class="flex gap-3 mt-2">
<a
href={`https://github.com/rcourtman/Pulse/releases/tag/${updateStore.updateInfo()?.latestVersion}`}
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 underline hover:text-blue-700 dark:hover:text-blue-300 text-xs"
>
View release notes
</a>
<button
onClick={() => updateStore.dismissUpdate()}
class="text-blue-600/70 dark:text-blue-400/70 hover:text-blue-700 dark:hover:text-blue-300 text-xs underline"
>
Don't show again for this version
</button>
</div>
</div>
</div>
</Show>
</div>
</div>
{/* Update Confirmation Modal */}
<UpdateConfirmationModal
isOpen={showConfirmModal()}
onClose={() => setShowConfirmModal(false)}
onConfirm={handleConfirmUpdate}
currentVersion={updateStore.versionInfo()?.version || 'Unknown'}
latestVersion={updateStore.updateInfo()?.latestVersion || ''}
plan={updatePlan() || {
canAutoUpdate: false,
requiresRoot: false,
rollbackSupport: false,
}}
isApplying={isApplying()}
/>
{/* Update Progress Modal */}
<UpdateProgressModal
isOpen={showProgressModal()}
onClose={() => setShowProgressModal(false)}
onViewHistory={() => {
setShowProgressModal(false);
navigate('/settings/updates');
}}
connected={wsContext?.connected}
reconnecting={wsContext?.reconnecting}
/>
</Show>
);
}