mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
feat: Add multi-step Setup Wizard for Pulse 5.0 onboarding
- New SetupWizard component with 5-step flow: 1. Welcome: Bootstrap token unlock, platform showcase 2. Security: Admin account + API token creation 3. Connect: Multi-platform infrastructure (Proxmox, Docker, K8s) 4. Features: AI and auto-updates toggles 5. Complete: Credentials display with copy/download - Replaced FirstRunSetup with SetupWizard in Login.tsx - Added Install Update button to UpdatesSettingsPanel - Enhanced UpdatesSettingsPanel with update plan integration - Added UpdateConfirmationModal to Settings for inline updates Positions Pulse as a unified infrastructure monitoring platform, not just a Proxmox-specific tool.
This commit is contained in:
parent
5c4069cdbf
commit
c3fdb6d6f8
12 changed files with 1707 additions and 198 deletions
156
frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx
Normal file
156
frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import { Component, createSignal, Show } from 'solid-js';
|
||||
import { showError, showSuccess } from '@/utils/toast';
|
||||
|
||||
interface WelcomeStepProps {
|
||||
onNext: () => void;
|
||||
bootstrapToken: string;
|
||||
setBootstrapToken: (token: string) => void;
|
||||
isUnlocked: boolean;
|
||||
setIsUnlocked: (unlocked: boolean) => void;
|
||||
}
|
||||
|
||||
export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
const [isValidating, setIsValidating] = createSignal(false);
|
||||
const [tokenPath, setTokenPath] = createSignal('');
|
||||
const [isDocker, setIsDocker] = createSignal(false);
|
||||
const [inContainer, setInContainer] = createSignal(false);
|
||||
const [lxcCtid, setLxcCtid] = createSignal('');
|
||||
|
||||
// Fetch bootstrap info on mount
|
||||
const fetchBootstrapInfo = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/security/status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.bootstrapTokenPath) {
|
||||
setTokenPath(data.bootstrapTokenPath);
|
||||
setIsDocker(data.isDocker || false);
|
||||
setInContainer(data.inContainer || false);
|
||||
setLxcCtid(data.lxcCtid || '');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch bootstrap info:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Call on component load
|
||||
fetchBootstrapInfo();
|
||||
|
||||
const handleUnlock = async () => {
|
||||
if (!props.bootstrapToken.trim()) {
|
||||
showError('Please enter the bootstrap token');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidating(true);
|
||||
try {
|
||||
const response = await fetch('/api/security/validate-bootstrap-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: props.bootstrapToken.trim() }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Invalid bootstrap token');
|
||||
}
|
||||
|
||||
props.setIsUnlocked(true);
|
||||
showSuccess('Token verified!');
|
||||
props.onNext();
|
||||
} catch (error) {
|
||||
showError('Invalid bootstrap token. Please check and try again.');
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getTokenCommand = () => {
|
||||
const path = tokenPath() || '/etc/pulse/.bootstrap_token';
|
||||
if (isDocker()) {
|
||||
return `docker exec <container> cat ${path}`;
|
||||
}
|
||||
if (inContainer() && lxcCtid()) {
|
||||
return `pct exec ${lxcCtid()} -- cat ${path}`;
|
||||
}
|
||||
if (inContainer()) {
|
||||
return `pct exec <ctid> -- cat ${path}`;
|
||||
}
|
||||
return `cat ${path}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="text-center">
|
||||
{/* Logo */}
|
||||
<div class="mb-8">
|
||||
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 shadow-2xl shadow-blue-500/30 mb-6">
|
||||
<svg width="56" height="56" viewBox="0 0 256 256" class="text-white">
|
||||
<circle class="fill-current opacity-20" cx="128" cy="128" r="122" />
|
||||
<circle class="fill-none stroke-current" stroke-width="14" cx="128" cy="128" r="84" opacity="0.9" />
|
||||
<circle class="fill-current" cx="128" cy="128" r="26" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold text-white mb-3">
|
||||
Welcome to Pulse
|
||||
</h1>
|
||||
<p class="text-xl text-blue-200/80">
|
||||
Unified infrastructure monitoring
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature highlights */}
|
||||
<div class="grid grid-cols-3 gap-4 mb-8">
|
||||
<div class="bg-white/5 backdrop-blur rounded-xl p-4 border border-white/10">
|
||||
<div class="text-2xl mb-2">🖥️</div>
|
||||
<div class="text-sm text-white/80">Proxmox</div>
|
||||
</div>
|
||||
<div class="bg-white/5 backdrop-blur rounded-xl p-4 border border-white/10">
|
||||
<div class="text-2xl mb-2">🐳</div>
|
||||
<div class="text-sm text-white/80">Docker</div>
|
||||
</div>
|
||||
<div class="bg-white/5 backdrop-blur rounded-xl p-4 border border-white/10">
|
||||
<div class="text-2xl mb-2">☸️</div>
|
||||
<div class="text-sm text-white/80">Kubernetes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bootstrap token unlock */}
|
||||
<Show when={!props.isUnlocked}>
|
||||
<div class="bg-white/10 backdrop-blur-xl rounded-2xl p-6 border border-white/20 text-left">
|
||||
<h3 class="text-lg font-semibold text-white mb-2">Unlock Setup</h3>
|
||||
<p class="text-sm text-white/70 mb-4">
|
||||
Retrieve the bootstrap token from your host:
|
||||
</p>
|
||||
<div class="bg-black/30 rounded-lg p-3 font-mono text-sm text-green-400 mb-4">
|
||||
{getTokenCommand()}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.bootstrapToken}
|
||||
onInput={(e) => props.setBootstrapToken(e.currentTarget.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleUnlock()}
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
|
||||
placeholder="Paste your bootstrap token"
|
||||
autofocus
|
||||
/>
|
||||
<button
|
||||
onClick={handleUnlock}
|
||||
disabled={isValidating() || !props.bootstrapToken.trim()}
|
||||
class="w-full mt-4 py-3 px-6 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium rounded-xl transition-all shadow-lg shadow-blue-500/25"
|
||||
>
|
||||
{isValidating() ? 'Validating...' : 'Continue →'}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.isUnlocked}>
|
||||
<button
|
||||
onClick={props.onNext}
|
||||
class="py-4 px-8 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white text-lg font-medium rounded-xl transition-all shadow-lg shadow-blue-500/25"
|
||||
>
|
||||
Get Started →
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue