feat: simplify setup wizard to 3 steps (welcome, security, complete)
Removed Connect and Features steps that were adding friction without much value. The wizard now focuses on: 1. Welcome - introduction and bootstrap token entry 2. Security - credential setup 3. Complete - shows install command and connected agents Agent polling now happens in CompleteStep instead of relying on WebSockets which aren't available during the pre-login setup phase.
This commit is contained in:
parent
3958b4c8c5
commit
8ed0851fb9
5 changed files with 470 additions and 144 deletions
|
|
@ -1,19 +1,17 @@
|
|||
import { Component, createSignal, Show } from 'solid-js';
|
||||
import { WelcomeStep } from './steps/WelcomeStep';
|
||||
import { SecurityStep } from './steps/SecurityStep';
|
||||
import { ConnectStep } from './steps/ConnectStep';
|
||||
import { FeaturesStep } from './steps/FeaturesStep';
|
||||
import { CompleteStep } from './steps/CompleteStep';
|
||||
import { StepIndicator } from './StepIndicator';
|
||||
|
||||
export type WizardStep = 'welcome' | 'security' | 'connect' | 'features' | 'complete';
|
||||
export type WizardStep = 'welcome' | 'security' | 'complete';
|
||||
|
||||
export interface WizardState {
|
||||
// Security
|
||||
username: string;
|
||||
password: string;
|
||||
apiToken: string;
|
||||
// Node
|
||||
// Node (for display in complete step)
|
||||
nodeAdded: boolean;
|
||||
nodeName: string;
|
||||
// Features
|
||||
|
|
@ -41,7 +39,7 @@ export const SetupWizard: Component<SetupWizardProps> = (props) => {
|
|||
const [bootstrapToken, setBootstrapToken] = createSignal(props.bootstrapToken || '');
|
||||
const [isUnlocked, setIsUnlocked] = createSignal(props.isUnlocked || false);
|
||||
|
||||
const steps: WizardStep[] = ['welcome', 'security', 'connect', 'features', 'complete'];
|
||||
const steps: WizardStep[] = ['welcome', 'security', 'complete'];
|
||||
|
||||
const currentStepIndex = () => steps.indexOf(currentStep());
|
||||
|
||||
|
|
@ -63,10 +61,6 @@ export const SetupWizard: Component<SetupWizardProps> = (props) => {
|
|||
setWizardState(prev => ({ ...prev, ...updates }));
|
||||
};
|
||||
|
||||
const skipToComplete = () => {
|
||||
setCurrentStep('complete');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
class="min-h-screen bg-gradient-to-br from-slate-900 via-blue-900 to-indigo-900 flex flex-col"
|
||||
|
|
@ -80,12 +74,12 @@ export const SetupWizard: Component<SetupWizardProps> = (props) => {
|
|||
<div class="absolute -bottom-40 right-1/3 w-80 h-80 bg-purple-500/20 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
{/* Step indicator - only show after welcome */}
|
||||
<Show when={currentStep() !== 'welcome' && currentStep() !== 'complete'}>
|
||||
{/* Step indicator - only show during security step */}
|
||||
<Show when={currentStep() === 'security'}>
|
||||
<div class="relative z-10 pt-8 px-4" role="navigation" aria-label="Setup progress">
|
||||
<StepIndicator
|
||||
steps={['Security', 'Connect', 'Features']}
|
||||
currentStep={currentStepIndex() - 1}
|
||||
steps={['Welcome', 'Security', 'Done']}
|
||||
currentStep={1}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -113,25 +107,6 @@ export const SetupWizard: Component<SetupWizardProps> = (props) => {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={currentStep() === 'connect'}>
|
||||
<ConnectStep
|
||||
state={wizardState()}
|
||||
updateState={updateState}
|
||||
onNext={nextStep}
|
||||
onBack={prevStep}
|
||||
onSkip={skipToComplete}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={currentStep() === 'features'}>
|
||||
<FeaturesStep
|
||||
state={wizardState()}
|
||||
updateState={updateState}
|
||||
onNext={nextStep}
|
||||
onBack={prevStep}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={currentStep() === 'complete'}>
|
||||
<CompleteStep
|
||||
state={wizardState()}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, createSignal } from 'solid-js';
|
||||
import { Component, createSignal, createEffect, onCleanup, Show, For } from 'solid-js';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { getPulseBaseUrl } from '@/utils/url';
|
||||
import { SecurityAPI } from '@/api/security';
|
||||
import type { WizardState } from '../SetupWizard';
|
||||
|
||||
interface CompleteStepProps {
|
||||
|
|
@ -8,18 +9,178 @@ interface CompleteStepProps {
|
|||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export const CompleteStep: Component<CompleteStepProps> = (props) => {
|
||||
const [copied, setCopied] = createSignal<'password' | 'token' | null>(null);
|
||||
type Platform = 'proxmox' | 'docker' | 'kubernetes' | 'host';
|
||||
|
||||
const handleCopy = async (type: 'password' | 'token') => {
|
||||
const value = type === 'password' ? props.state.password : props.state.apiToken;
|
||||
const success = await copyToClipboard(value);
|
||||
interface ConnectedAgent {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
host: string;
|
||||
addedAt: Date;
|
||||
}
|
||||
|
||||
export const CompleteStep: Component<CompleteStepProps> = (props) => {
|
||||
const [copied, setCopied] = createSignal<'password' | 'token' | 'install' | null>(null);
|
||||
const [showCredentials, setShowCredentials] = createSignal(false);
|
||||
const [selectedPlatforms, setSelectedPlatforms] = createSignal<Platform[]>([]);
|
||||
const [connectedAgents, setConnectedAgents] = createSignal<ConnectedAgent[]>([]);
|
||||
const [currentInstallToken, setCurrentInstallToken] = createSignal(props.state.apiToken);
|
||||
const [generatingToken, setGeneratingToken] = createSignal(false);
|
||||
|
||||
// Available optional platforms (host monitoring is always enabled)
|
||||
const platforms = [
|
||||
{ id: 'proxmox' as Platform, name: 'Proxmox VE', desc: 'VMs & containers via API', icon: '🖥️' },
|
||||
{ id: 'docker' as Platform, name: 'Docker', desc: 'Container monitoring', icon: '🐳' },
|
||||
{ id: 'kubernetes' as Platform, name: 'Kubernetes', desc: 'Cluster monitoring', icon: '☸️' },
|
||||
];
|
||||
|
||||
// Poll for agent connections since WebSocket isn't available during setup
|
||||
createEffect(() => {
|
||||
let pollInterval: number | undefined;
|
||||
let previousCount = 0;
|
||||
|
||||
const checkForAgents = async () => {
|
||||
try {
|
||||
console.log('[CompleteStep] Checking for agents with token:', props.state.apiToken?.slice(0, 8) + '...');
|
||||
|
||||
const response = await fetch('/api/state', {
|
||||
headers: {
|
||||
'X-API-Token': props.state.apiToken,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[CompleteStep] API response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('[CompleteStep] API returned non-OK status, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const state = await response.json();
|
||||
const nodes = state.nodes || [];
|
||||
const hosts = state.hosts || [];
|
||||
|
||||
console.log('[CompleteStep] Received:', { nodes: nodes.length, hosts: hosts.length, hostNames: hosts.map((h: any) => h.hostname || h.displayName) });
|
||||
|
||||
// Check if we have new connections
|
||||
const totalAgents = nodes.length + hosts.length;
|
||||
|
||||
if (totalAgents > previousCount || totalAgents !== connectedAgents().length) {
|
||||
// Group agents by hostname to avoid duplicates
|
||||
const agentMap = new Map<string, ConnectedAgent>();
|
||||
|
||||
for (const node of nodes) {
|
||||
const name = node.name || node.displayName || 'Unknown';
|
||||
const existing = agentMap.get(name);
|
||||
if (existing) {
|
||||
// Add PVE to existing agent's types
|
||||
if (!existing.type.includes('Proxmox')) {
|
||||
existing.type = `${existing.type} + Proxmox VE`;
|
||||
}
|
||||
if (node.host && !existing.host) {
|
||||
existing.host = node.host;
|
||||
}
|
||||
} else {
|
||||
agentMap.set(name, {
|
||||
id: node.id || `node-${name}`,
|
||||
name,
|
||||
type: 'Proxmox VE',
|
||||
host: node.host || '',
|
||||
addedAt: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const host of hosts) {
|
||||
const name = host.displayName || host.hostname || 'Unknown';
|
||||
const existing = agentMap.get(name);
|
||||
if (existing) {
|
||||
// Add Host Agent to existing PVE node
|
||||
if (!existing.type.includes('Host')) {
|
||||
existing.type = `${existing.type} + Host Agent`;
|
||||
}
|
||||
} else {
|
||||
agentMap.set(name, {
|
||||
id: host.id || `host-${name}`,
|
||||
name,
|
||||
type: 'Host Agent',
|
||||
host: '',
|
||||
addedAt: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setConnectedAgents(Array.from(agentMap.values()));
|
||||
|
||||
// Generate new token if agents increased
|
||||
if (previousCount > 0 && totalAgents > previousCount) {
|
||||
generateNewToken();
|
||||
}
|
||||
|
||||
previousCount = totalAgents;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check for agents:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Poll every 3 seconds
|
||||
pollInterval = window.setInterval(checkForAgents, 3000);
|
||||
|
||||
// Initial check
|
||||
checkForAgents();
|
||||
|
||||
onCleanup(() => {
|
||||
if (pollInterval) {
|
||||
window.clearInterval(pollInterval);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const togglePlatform = (platform: Platform) => {
|
||||
const current = selectedPlatforms();
|
||||
if (current.includes(platform)) {
|
||||
setSelectedPlatforms(current.filter(p => p !== platform));
|
||||
} else {
|
||||
setSelectedPlatforms([...current, platform]);
|
||||
}
|
||||
};
|
||||
|
||||
const generateNewToken = async () => {
|
||||
if (generatingToken()) return;
|
||||
|
||||
setGeneratingToken(true);
|
||||
try {
|
||||
// Don't specify scopes - tokens without scopes default to wildcard access
|
||||
const result = await SecurityAPI.createToken(`agent-install-${Date.now()}`);
|
||||
if (result.token) {
|
||||
setCurrentInstallToken(result.token);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate new token:', error);
|
||||
} finally {
|
||||
setGeneratingToken(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async (type: 'password' | 'token' | 'install', value?: string) => {
|
||||
const copyValue = value || (type === 'password' ? props.state.password : props.state.apiToken);
|
||||
const success = await copyToClipboard(copyValue);
|
||||
if (success) {
|
||||
setCopied(type);
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyInstall = async () => {
|
||||
const command = getInstallCommand();
|
||||
const success = await copyToClipboard(command);
|
||||
if (success) {
|
||||
setCopied('install');
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadCredentials = () => {
|
||||
const baseUrl = getPulseBaseUrl();
|
||||
const content = `Pulse Credentials
|
||||
|
|
@ -38,7 +199,7 @@ ${props.state.apiToken}
|
|||
|
||||
Example: curl -H "X-API-Token: ${props.state.apiToken}" ${baseUrl}/api/state
|
||||
|
||||
⚠️ Keep these credentials secure!
|
||||
Keep these credentials secure!
|
||||
`;
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
|
|
@ -52,104 +213,266 @@ Example: curl -H "X-API-Token: ${props.state.apiToken}" ${baseUrl}/api/state
|
|||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const getInstallCommand = () => {
|
||||
const baseUrl = getPulseBaseUrl();
|
||||
// Host monitoring is always enabled by default, only add flags for optional integrations
|
||||
const platformFlags = selectedPlatforms()
|
||||
.filter(p => p !== 'host') // host is default, don't need flag
|
||||
.map(p => `--enable-${p}`)
|
||||
.join(' ');
|
||||
const flagsPart = platformFlags ? ` ${platformFlags}` : '';
|
||||
return `curl -sSL ${baseUrl}/install.sh | sudo bash -s -- --url "${baseUrl}" --token "${currentInstallToken()}"${flagsPart}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="text-center">
|
||||
{/* Success animation */}
|
||||
<div class="mb-8">
|
||||
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 shadow-2xl shadow-green-500/30 mb-6">
|
||||
<svg class="w-12 h-12 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<div class="mb-5">
|
||||
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 shadow-xl shadow-green-500/30 mb-3">
|
||||
<svg class="w-7 h-7 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-3">
|
||||
You're All Set! 🎉
|
||||
<h1 class="text-xl font-bold text-white mb-1">
|
||||
Security Configured
|
||||
</h1>
|
||||
<p class="text-xl text-green-200/80">
|
||||
Pulse is ready to monitor your infrastructure
|
||||
<p class="text-sm text-green-200/80">
|
||||
Install Pulse Agents on hosts you want to monitor
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Credentials box */}
|
||||
<div class="bg-white/10 backdrop-blur-xl rounded-2xl border border-white/20 p-6 text-left mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-white">Your Credentials</h3>
|
||||
<button
|
||||
onClick={downloadCredentials}
|
||||
class="text-sm text-blue-300 hover:text-blue-200 flex items-center gap-1"
|
||||
>
|
||||
{/* Connected Agents - show when agents connect */}
|
||||
<Show when={connectedAgents().length > 0}>
|
||||
<div class="bg-green-500/20 backdrop-blur-xl rounded-xl border border-green-400/30 p-4 text-left mb-4">
|
||||
<h3 class="text-sm font-semibold text-green-300 mb-2 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Download
|
||||
</button>
|
||||
Connected ({connectedAgents().length} host{connectedAgents().length !== 1 ? 's' : ''})
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<For each={connectedAgents()}>
|
||||
{(agent) => (
|
||||
<div class="flex items-center justify-between bg-black/20 rounded-lg px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
|
||||
<span class="text-white text-sm font-medium">{agent.name}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-green-300/80 bg-green-500/20 px-1.5 py-0.5 rounded">{agent.type}</span>
|
||||
<Show when={agent.host}>
|
||||
<span class="text-[10px] text-white/40">{agent.host}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="space-y-3">
|
||||
{/* Username */}
|
||||
<div class="bg-black/20 rounded-xl p-3">
|
||||
<div class="text-xs text-white/60 mb-1">Username</div>
|
||||
<div class="text-white font-mono">{props.state.username}</div>
|
||||
</div>
|
||||
{/* Platform selection */}
|
||||
<div class="bg-white/10 backdrop-blur-xl rounded-xl border border-white/20 p-4 text-left mb-4">
|
||||
<h3 class="text-sm font-semibold text-white mb-3">What does this host have?</h3>
|
||||
|
||||
{/* Password */}
|
||||
<div class="bg-black/20 rounded-xl p-3">
|
||||
<div class="text-xs text-white/60 mb-1">Password</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<code class="text-white font-mono text-sm break-all">{props.state.password}</code>
|
||||
<button
|
||||
onClick={() => handleCopy('password')}
|
||||
class="ml-2 p-1.5 hover:bg-white/10 rounded transition-all"
|
||||
>
|
||||
{copied() === 'password' ? '✓' : '📋'}
|
||||
</button>
|
||||
{/* Always included - Host monitoring */}
|
||||
<div class="bg-green-500/10 border border-green-400/30 rounded-lg p-2.5 mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-green-500 flex items-center justify-center">
|
||||
<svg class="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Token */}
|
||||
<div class="bg-black/20 rounded-xl p-3">
|
||||
<div class="text-xs text-white/60 mb-1">API Token</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<code class="text-white font-mono text-xs break-all">{props.state.apiToken}</code>
|
||||
<button
|
||||
onClick={() => handleCopy('token')}
|
||||
class="ml-2 p-1.5 hover:bg-white/10 rounded transition-all"
|
||||
>
|
||||
{copied() === 'token' ? '✓' : '📋'}
|
||||
</button>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-white font-medium text-xs">Host Monitoring</span>
|
||||
<span class="text-[9px] text-green-300 bg-green-500/20 px-1.5 py-0.5 rounded">Always included</span>
|
||||
</div>
|
||||
<p class="text-[10px] text-white/50">CPU, memory, disk, network on any Linux/macOS/Windows server</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-3 bg-amber-500/20 border border-amber-400/30 rounded-xl">
|
||||
<p class="text-amber-200 text-sm">
|
||||
⚠️ Save these now — they won't be shown again!
|
||||
</p>
|
||||
{/* Optional integrations */}
|
||||
<p class="text-[10px] text-white/50 mb-2">Enable if this host runs:</p>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<For each={platforms}>
|
||||
{(platform) => (
|
||||
<button
|
||||
onClick={() => togglePlatform(platform.id)}
|
||||
class={`p-2 rounded-lg text-left transition-all border ${selectedPlatforms().includes(platform.id)
|
||||
? 'bg-blue-500/20 border-blue-400/50'
|
||||
: 'bg-white/5 border-white/10 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class={`w-3.5 h-3.5 rounded border-2 flex items-center justify-center ${selectedPlatforms().includes(platform.id)
|
||||
? 'bg-blue-500 border-blue-500'
|
||||
: 'border-white/40'
|
||||
}`}>
|
||||
<Show when={selectedPlatforms().includes(platform.id)}>
|
||||
<svg class="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-white font-medium text-xs">{platform.name}</span>
|
||||
</div>
|
||||
<p class="text-[9px] text-white/40 ml-5 mt-0.5">{platform.desc}</p>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick links */}
|
||||
<div class="grid grid-cols-3 gap-3 mb-8">
|
||||
<a href="/proxmox/overview" class="bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl p-4 text-center transition-all">
|
||||
<div class="text-2xl mb-2">📊</div>
|
||||
<div class="text-sm text-white/80">Dashboard</div>
|
||||
</a>
|
||||
<a href="/settings/proxmox" class="bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl p-4 text-center transition-all">
|
||||
<div class="text-2xl mb-2">⚙️</div>
|
||||
<div class="text-sm text-white/80">Add More Nodes</div>
|
||||
</a>
|
||||
<a href="/settings/system-ai" class="bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl p-4 text-center transition-all">
|
||||
<div class="text-2xl mb-2">🤖</div>
|
||||
<div class="text-sm text-white/80">Configure AI</div>
|
||||
</a>
|
||||
{/* Agent installation */}
|
||||
<div class="bg-white/10 backdrop-blur-xl rounded-xl border border-white/20 p-4 text-left mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Install Command
|
||||
</h3>
|
||||
<Show when={connectedAgents().length > 0}>
|
||||
<button
|
||||
onClick={generateNewToken}
|
||||
disabled={generatingToken()}
|
||||
class="text-xs text-blue-300 hover:text-blue-200 flex items-center gap-1 disabled:opacity-50"
|
||||
>
|
||||
{generatingToken() ? (
|
||||
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
)}
|
||||
New Token
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<p class="text-white/70 text-xs mb-2">
|
||||
Run on any host you want to monitor. Each copy uses a unique token.
|
||||
</p>
|
||||
|
||||
<div class="bg-black/40 rounded-lg p-2.5 font-mono text-[10px] mb-2 relative group">
|
||||
<code class="text-green-400 break-all block leading-relaxed">{getInstallCommand()}</code>
|
||||
<button
|
||||
onClick={handleCopyInstall}
|
||||
class="absolute top-1.5 right-1.5 p-1.5 bg-white/10 hover:bg-white/20 rounded-md transition-all"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied() === 'install' ? (
|
||||
<svg class="w-3.5 h-3.5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg class="w-3.5 h-3.5 text-white/60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-[10px] text-white/40">
|
||||
Agents auto-register with Pulse. Install on as many hosts as you like.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Credentials section (collapsible) */}
|
||||
<div class="bg-white/5 backdrop-blur rounded-lg border border-white/10 mb-4 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setShowCredentials(!showCredentials())}
|
||||
class="w-full p-2.5 flex items-center justify-between text-left hover:bg-white/5 transition-all"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-3.5 h-3.5 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span class="text-white/80 text-xs">Your Credentials</span>
|
||||
<span class="text-[10px] text-amber-300/80 bg-amber-500/20 px-1.5 py-0.5 rounded">Save these</span>
|
||||
</div>
|
||||
<svg class={`w-3.5 h-3.5 text-white/40 transition-transform ${showCredentials() ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Show when={showCredentials()}>
|
||||
<div class="p-2.5 pt-0 space-y-1.5">
|
||||
<div class="bg-black/20 rounded-md p-2">
|
||||
<div class="text-[10px] text-white/50 mb-0.5">Username</div>
|
||||
<div class="text-white font-mono text-xs">{props.state.username}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-black/20 rounded-md p-2">
|
||||
<div class="text-[10px] text-white/50 mb-0.5">Password</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<code class="text-white font-mono text-xs break-all">{props.state.password}</code>
|
||||
<button
|
||||
onClick={() => handleCopy('password')}
|
||||
class="ml-2 p-0.5 hover:bg-white/10 rounded transition-all text-white/40 hover:text-white"
|
||||
>
|
||||
{copied() === 'password' ? (
|
||||
<svg class="w-3 h-3 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-black/20 rounded-md p-2">
|
||||
<div class="text-[10px] text-white/50 mb-0.5">API Token (for web login)</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<code class="text-white font-mono text-[10px] break-all">{props.state.apiToken}</code>
|
||||
<button
|
||||
onClick={() => handleCopy('token')}
|
||||
class="ml-2 p-0.5 hover:bg-white/10 rounded transition-all text-white/40 hover:text-white"
|
||||
>
|
||||
{copied() === 'token' ? (
|
||||
<svg class="w-3 h-3 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={downloadCredentials}
|
||||
class="w-full py-1.5 text-[10px] text-blue-300 hover:text-blue-200 flex items-center justify-center gap-1 hover:bg-white/5 rounded-md transition-all"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Download credentials
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Launch button */}
|
||||
<button
|
||||
onClick={props.onComplete}
|
||||
class="w-full py-4 px-8 bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white text-lg font-medium rounded-xl transition-all shadow-lg shadow-green-500/25"
|
||||
class="w-full py-2.5 px-5 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white text-sm font-medium rounded-lg transition-all shadow-lg shadow-blue-500/25"
|
||||
>
|
||||
Launch Pulse →
|
||||
{connectedAgents().length > 0 ? 'Go to Dashboard' : 'Skip for Now'}
|
||||
</button>
|
||||
|
||||
<p class="mt-2 text-[10px] text-white/40">
|
||||
You can add more agents anytime from Settings
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,19 +33,54 @@ export const ConnectStep: Component<ConnectStepProps> = (props) => {
|
|||
const [tokenSecret, setTokenSecret] = createSignal('');
|
||||
|
||||
const platforms = [
|
||||
{ id: 'proxmox' as Platform, name: 'Proxmox VE', icon: '🖥️', desc: 'Hypervisor & containers', ports: ['8006'] },
|
||||
{ id: 'docker' as Platform, name: 'Docker', icon: '🐳', desc: 'Container hosts', ports: ['2375', '2376'] },
|
||||
{ id: 'kubernetes' as Platform, name: 'Kubernetes', icon: '☸️', desc: 'Container orchestration', ports: ['6443'] },
|
||||
{ id: 'proxmox' as Platform, name: 'Proxmox VE', desc: 'Hypervisor & containers', ports: ['8006'] },
|
||||
{ id: 'docker' as Platform, name: 'Docker', desc: 'Container hosts', ports: ['2375', '2376'] },
|
||||
{ id: 'kubernetes' as Platform, name: 'Kubernetes', desc: 'Container orchestration', ports: ['6443'] },
|
||||
];
|
||||
|
||||
const PlatformIcon: Component<{ platform: Platform; class?: string }> = (iconProps) => {
|
||||
const iconClass = iconProps.class || 'w-10 h-10';
|
||||
switch (iconProps.platform) {
|
||||
case 'proxmox':
|
||||
// Official Proxmox VE logo (from simple-icons)
|
||||
return (
|
||||
<svg class={iconClass} viewBox="0 0 24 24" fill="#E57000">
|
||||
<path d="M4.928 1.825c-1.09.553-1.09.64-.07 1.78 5.655 6.295 7.004 7.782 7.107 7.782.139.017 7.971-8.542 8.058-8.801.034-.07-.208-.312-.519-.536-.415-.312-.864-.433-1.712-.467-1.59-.104-2.144.242-4.115 2.455-.899 1.003-1.66 1.833-1.66 1.833-.017 0-.76-.813-1.642-1.798S8.473 2.1 8.127 1.91c-.796-.45-2.421-.484-3.2-.086zM1.297 4.367C.45 4.695 0 5.007 0 5.248c0 .121 1.331 1.678 2.94 3.459 1.625 1.78 2.939 3.268 2.939 3.302 0 .035-1.331 1.522-2.94 3.303C1.314 17.11.017 18.683.035 18.822c.086.467 1.504 1.055 2.541 1.055 1.678-.018 2.058-.312 5.603-4.202 1.78-1.954 3.233-3.614 3.233-3.666 0-.069-1.435-1.694-3.199-3.63-2.3-2.508-3.423-3.632-3.96-3.874-.812-.398-2.126-.467-2.956-.138zm18.467.12c-.502.26-1.764 1.505-3.943 3.891-1.763 1.937-3.199 3.562-3.199 3.631 0 .07 1.453 1.712 3.234 3.666 3.544 3.89 3.925 4.184 5.602 4.202 1.038 0 2.455-.588 2.542-1.055.017-.156-1.28-1.712-2.905-3.493-1.608-1.78-2.94-3.285-2.94-3.32 0-.034 1.332-1.539 2.94-3.32C22.72 6.91 24.017 5.352 24 5.214c-.087-.45-1.366-.968-2.473-1.038-.795-.034-1.21.035-1.763.312zM7.954 16.973c-2.144 2.369-3.908 4.374-3.943 4.46-.034.07.208.312.52.537.414.311.864.432 1.711.467 1.574.103 2.161-.26 4.15-2.508.864-.968 1.608-1.78 1.625-1.78s.761.812 1.643 1.798c2.023 2.248 2.559 2.576 4.132 2.49.848-.035 1.297-.156 1.712-.467.311-.225.553-.467.519-.536-.087-.26-7.92-8.819-8.058-8.801-.069 0-1.867 1.954-4.011 4.34z" />
|
||||
</svg>
|
||||
);
|
||||
case 'docker':
|
||||
// Official Docker Moby whale logo
|
||||
return (
|
||||
<svg class={iconClass} viewBox="0 0 24 24" fill="#2496ED">
|
||||
<path d="M13.983 11.078h2.119a.186.186 0 0 0 .186-.185V9.006a.186.186 0 0 0-.186-.186h-2.119a.185.185 0 0 0-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 0 0 .186-.186V3.574a.186.186 0 0 0-.186-.185h-2.118a.185.185 0 0 0-.185.185v1.888c0 .102.082.185.185.186m0 2.716h2.118a.187.187 0 0 0 .186-.186V6.29a.186.186 0 0 0-.186-.185h-2.118a.185.185 0 0 0-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 0 0 .184-.186V6.29a.185.185 0 0 0-.185-.185H8.1a.185.185 0 0 0-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 0 0 .185-.186V6.29a.185.185 0 0 0-.185-.185H5.136a.186.186 0 0 0-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 0 0 .186-.185V9.006a.186.186 0 0 0-.186-.186h-2.118a.185.185 0 0 0-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 0 0 .184-.185V9.006a.185.185 0 0 0-.184-.186h-2.12a.185.185 0 0 0-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 0 0 .185-.185V9.006a.185.185 0 0 0-.184-.186h-2.12a.186.186 0 0 0-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 0 0 .184-.185V9.006a.185.185 0 0 0-.184-.186h-2.12a.185.185 0 0 0-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 0 0-.75.748 11.376 11.376 0 0 0 .692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 0 0 3.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288z" />
|
||||
</svg>
|
||||
);
|
||||
case 'kubernetes':
|
||||
// Official Kubernetes logo (from simple-icons)
|
||||
return (
|
||||
<svg class={iconClass} viewBox="0 0 24 24" fill="#326CE5">
|
||||
<path d="M10.204 14.35l.007.01-.999 2.413a5.171 5.171 0 0 1-2.075-2.597l2.578-.437.004.005a.44.44 0 0 1 .484.606zm-.833-2.129a.44.44 0 0 0 .173-.756l.002-.011L7.585 9.7a5.143 5.143 0 0 0-.73 3.255l2.514-.725.002-.009zm1.145-1.98a.44.44 0 0 0 .699-.337l.01-.005.15-2.62a5.144 5.144 0 0 0-3.01 1.442l2.147 1.523.004-.002zm.76 2.75l.723.349.722-.347.18-.78-.5-.623h-.804l-.5.623.179.779zm1.5-3.095a.44.44 0 0 0 .7.336l.008.003 2.134-1.513a5.188 5.188 0 0 0-2.992-1.442l.148 2.615.002.001zm10.876 5.97l-5.773 7.181a1.6 1.6 0 0 1-1.248.594l-9.261.003a1.6 1.6 0 0 1-1.247-.596l-5.776-7.18a1.583 1.583 0 0 1-.307-1.34L2.1 5.573c.108-.47.425-.864.863-1.073L11.305.513a1.606 1.606 0 0 1 1.385 0l8.345 3.985c.438.209.755.604.863 1.073l2.062 8.955c.108.47-.005.963-.308 1.34zm-3.289-2.057c-.042-.01-.103-.026-.145-.034-.174-.033-.315-.025-.479-.038-.35-.037-.638-.067-.895-.148-.105-.04-.18-.165-.216-.216l-.201-.059a6.45 6.45 0 0 0-.105-2.332 6.465 6.465 0 0 0-.936-2.163c.052-.047.15-.133.177-.159.008-.09.001-.183.094-.282.197-.185.444-.338.743-.522.142-.084.273-.137.415-.242.032-.024.076-.062.11-.089.24-.191.295-.52.123-.736-.172-.216-.506-.236-.745-.045-.034.027-.08.062-.111.088-.134.116-.217.23-.33.35-.246.25-.45.458-.673.609-.097.056-.239.037-.303.033l-.19.135a6.545 6.545 0 0 0-4.146-2.003l-.012-.223c-.065-.062-.143-.115-.163-.25-.022-.268.015-.557.057-.905.023-.163.061-.298.068-.475.001-.04-.001-.099-.001-.142 0-.306-.224-.555-.5-.555-.275 0-.499.249-.499.555l.001.014c0 .041-.002.092 0 .128.006.177.044.312.067.475.042.348.078.637.056.906a.545.545 0 0 1-.162.258l-.012.211a6.424 6.424 0 0 0-4.166 2.003 8.373 8.373 0 0 1-.18-.128c-.09.012-.18.04-.297-.029-.223-.15-.427-.358-.673-.608-.113-.12-.195-.234-.329-.349-.03-.026-.077-.062-.111-.088a.594.594 0 0 0-.348-.132.481.481 0 0 0-.398.176c-.172.216-.117.546.123.737l.007.005.104.083c.142.105.272.159.414.242.299.185.546.338.743.522.076.082.09.226.1.288l.16.143a6.462 6.462 0 0 0-1.02 4.506l-.208.06c-.055.072-.133.184-.215.217-.257.081-.546.11-.895.147-.164.014-.305.006-.48.039-.037.007-.09.02-.133.03l-.004.002-.007.002c-.295.071-.484.342-.423.608.061.267.349.429.645.365l.007-.001.01-.003.129-.029c.17-.046.294-.113.448-.172.33-.118.604-.217.87-.256.112-.009.23.069.288.101l.217-.037a6.5 6.5 0 0 0 2.88 3.596l-.09.218c.033.084.069.199.044.282-.097.252-.263.517-.452.813-.091.136-.185.242-.268.399-.02.037-.045.095-.064.134-.128.275-.034.591.213.71.248.12.556-.007.69-.282v-.002c.02-.039.046-.09.062-.127.07-.162.094-.301.144-.458.132-.332.205-.68.387-.897.05-.06.13-.082.215-.105l.113-.205a6.453 6.453 0 0 0 4.609.012l.106.192c.086.028.18.042.256.155.136.232.229.507.342.84.05.156.074.295.145.457.016.037.043.09.062.129.133.276.442.402.69.282.247-.118.341-.435.213-.71-.02-.039-.045-.096-.065-.134-.083-.156-.177-.261-.268-.398-.19-.296-.346-.541-.443-.793-.04-.13.007-.21.038-.294-.018-.022-.059-.144-.083-.202a6.499 6.499 0 0 0 2.88-3.622c.064.01.176.03.213.038.075-.05.144-.114.28-.104.266.039.54.138.87.256.154.06.277.128.448.173.036.01.088.019.13.028l.009.003.007.001c.297.064.584-.098.645-.365.06-.266-.128-.537-.423-.608zM16.4 9.701l-1.95 1.746v.005a.44.44 0 0 0 .173.757l.003.01 2.526.728a5.199 5.199 0 0 0-.108-1.674A5.208 5.208 0 0 0 16.4 9.7zm-4.013 5.325a.437.437 0 0 0-.404-.232.44.44 0 0 0-.372.233h-.002l-1.268 2.292a5.164 5.164 0 0 0 3.326.003l-1.27-2.296h-.01zm1.888-1.293a.44.44 0 0 0-.27.036.44.44 0 0 0-.214.572l-.003.004 1.01 2.438a5.15 5.15 0 0 0 2.081-2.615l-2.6-.44-.004.005z" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const runDiscovery = async () => {
|
||||
setIsScanning(true);
|
||||
setDiscoveredNodes([]);
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (props.state.apiToken) {
|
||||
headers['X-API-Token'] = props.state.apiToken;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/discover', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ subnet: 'auto' }),
|
||||
});
|
||||
|
||||
|
|
@ -54,7 +89,10 @@ export const ConnectStep: Component<ConnectStepProps> = (props) => {
|
|||
// Poll for results
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
const results = await fetch('/api/discover/results');
|
||||
const results = await fetch('/api/discover/results', {
|
||||
headers: props.state.apiToken ? { 'X-API-Token': props.state.apiToken } : {},
|
||||
credentials: 'include',
|
||||
});
|
||||
if (results.ok) {
|
||||
const data = await results.json();
|
||||
if (data.nodes && data.nodes.length > 0) {
|
||||
|
|
@ -79,6 +117,11 @@ export const ConnectStep: Component<ConnectStepProps> = (props) => {
|
|||
const connectNode = async (node?: DiscoveredNode) => {
|
||||
setIsConnecting(true);
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (props.state.apiToken) {
|
||||
headers['X-API-Token'] = props.state.apiToken;
|
||||
}
|
||||
|
||||
try {
|
||||
const nodeData = node ? {
|
||||
type: node.type === 'pbs' ? 'pbs' : node.type === 'pmg' ? 'pmg' : 'pve',
|
||||
|
|
@ -96,7 +139,8 @@ export const ConnectStep: Component<ConnectStepProps> = (props) => {
|
|||
|
||||
const response = await fetch('/api/nodes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(nodeData),
|
||||
});
|
||||
|
||||
|
|
@ -129,7 +173,7 @@ export const ConnectStep: Component<ConnectStepProps> = (props) => {
|
|||
onClick={() => setSelectedPlatform(platform.id)}
|
||||
class="bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/30 rounded-xl p-4 text-center transition-all"
|
||||
>
|
||||
<div class="text-3xl mb-2">{platform.icon}</div>
|
||||
<div class="flex justify-center mb-2"><PlatformIcon platform={platform.id} /></div>
|
||||
<div class="text-white font-medium">{platform.name}</div>
|
||||
<div class="text-white/50 text-xs mt-1">{platform.desc}</div>
|
||||
</button>
|
||||
|
|
@ -148,7 +192,7 @@ export const ConnectStep: Component<ConnectStepProps> = (props) => {
|
|||
>
|
||||
←
|
||||
</button>
|
||||
<span class="text-xl">🖥️</span>
|
||||
<PlatformIcon platform="proxmox" class="w-6 h-6" />
|
||||
<span class="text-white font-medium">Proxmox VE</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -159,14 +203,14 @@ export const ConnectStep: Component<ConnectStepProps> = (props) => {
|
|||
disabled={isScanning()}
|
||||
class="bg-blue-500/20 hover:bg-blue-500/30 border border-blue-400/30 rounded-xl p-4 text-left transition-all"
|
||||
>
|
||||
<div class="text-lg mb-1">🔍 Auto-Discover</div>
|
||||
<div class="text-lg mb-1 flex items-center gap-2"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg> Auto-Discover</div>
|
||||
<div class="text-sm text-white/60">Scan your network</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowManualForm(true)}
|
||||
class="bg-white/5 hover:bg-white/10 border border-white/20 rounded-xl p-4 text-left transition-all"
|
||||
>
|
||||
<div class="text-lg mb-1">✏️ Manual Setup</div>
|
||||
<div class="text-lg mb-1 flex items-center gap-2"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg> Manual Setup</div>
|
||||
<div class="text-sm text-white/60">Enter details</div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -240,7 +284,7 @@ export const ConnectStep: Component<ConnectStepProps> = (props) => {
|
|||
{/* Docker placeholder */}
|
||||
<Show when={selectedPlatform() === 'docker'}>
|
||||
<div class="text-center py-8">
|
||||
<div class="text-4xl mb-4">🐳</div>
|
||||
<div class="flex justify-center mb-4"><PlatformIcon platform="docker" class="w-12 h-12" /></div>
|
||||
<p class="text-white/70 mb-4">
|
||||
Docker monitoring requires the Pulse Agent.<br />
|
||||
Install it on your Docker host after setup.
|
||||
|
|
@ -257,7 +301,7 @@ export const ConnectStep: Component<ConnectStepProps> = (props) => {
|
|||
{/* Kubernetes placeholder */}
|
||||
<Show when={selectedPlatform() === 'kubernetes'}>
|
||||
<div class="text-center py-8">
|
||||
<div class="text-4xl mb-4">☸️</div>
|
||||
<div class="flex justify-center mb-4"><PlatformIcon platform="kubernetes" class="w-12 h-12" /></div>
|
||||
<p class="text-white/70 mb-4">
|
||||
Kubernetes monitoring requires the Pulse Agent.<br />
|
||||
Deploy via Helm after setup.
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
|||
|
||||
const handleSetup = async () => {
|
||||
if (useCustomPassword()) {
|
||||
if (!password() || password().length < 12) {
|
||||
showError('Password must be at least 12 characters');
|
||||
if (!password()) {
|
||||
showError('Please enter a password');
|
||||
return;
|
||||
}
|
||||
if (password() !== confirmPassword()) {
|
||||
|
|
@ -115,21 +115,21 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
|||
type="button"
|
||||
onClick={() => setUseCustomPassword(false)}
|
||||
class={`py-3 px-4 rounded-xl text-sm font-medium transition-all ${!useCustomPassword()
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-white/10 text-white/70 hover:bg-white/20'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-white/10 text-white/70 hover:bg-white/20'
|
||||
}`}
|
||||
>
|
||||
🔐 Generate Secure
|
||||
Generate Secure
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUseCustomPassword(true)}
|
||||
class={`py-3 px-4 rounded-xl text-sm font-medium transition-all ${useCustomPassword()
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-white/10 text-white/70 hover:bg-white/20'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-white/10 text-white/70 hover:bg-white/20'
|
||||
}`}
|
||||
>
|
||||
✏️ Custom Password
|
||||
Custom Password
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
|||
value={password()}
|
||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||
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"
|
||||
placeholder="Password (min 12 characters)"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
|
|
|
|||
|
|
@ -99,22 +99,6 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
|||
</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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue