Watch
1
0
Fork
You've already forked RedFlag
0

feat: component manifest drives installs, checkoff, and desktop lockstep

INSTALL-001: manifest schema with components+artifacts, CI generation
in release gate, manifest-driven install template with --guided and
--checkoff modes, post-install provisioning checks, desktop joins
version lockstep (bump-version.sh + CI build + gate enforcement).

Setup.tsx reduced to primitives (FormSection, TextField, Alert).
This commit is contained in:
Fimeg 2026-06-12 15:23:15 -04:00
commit 88b612c77e
6 changed files with 805 additions and 663 deletions

View file

@ -1,9 +1,10 @@
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Settings, Database, User, Shield, Eye, EyeOff, CheckCircle, Key } from 'lucide-react';
import { Database, Settings, Shield, User, Eye, EyeOff, CheckCircle, Key } from 'lucide-react';
import { toast } from 'react-hot-toast';
import { setupApi } from '@/lib/api';
import { useAuthStore } from '@/lib/store';
import { cn } from '@/lib/utils';
interface SetupFormData {
adminUser: string;
@ -26,6 +27,63 @@ interface SigningKeys {
algorithm: string;
}
// ---- local primitives (reused within Setup) ----
/** Section header with icon — repeated 4× in the form. */
const FormSection: React.FC<{ icon: React.ElementType; title: string; children: React.ReactNode }> = ({
icon: Icon, title, children,
}) => (
<div>
<div className="flex items-center mb-4">
<Icon className="h-5 w-5 text-indigo-600 mr-2" />
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
</div>
{children}
</div>
);
type AlertKind = 'error' | 'info' | 'success' | 'warning';
const ALERT_STYLES: Record<AlertKind, string> = {
error: 'bg-red-50 border-red-200 text-red-800',
info: 'bg-blue-50 border-blue-200 text-blue-800',
success: 'bg-green-50 border-green-200 text-green-800',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
};
const Alert: React.FC<{ kind: AlertKind; children: React.ReactNode; className?: string }> = ({
kind, children, className,
}) => (
<div className={cn('border rounded-md p-3 text-sm', ALERT_STYLES[kind], className)}>
{children}
</div>
);
/** Standard text input with label — 15+ instances in the form. */
const TextField: React.FC<{
id: string; label: string; name: string; value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
type?: string; placeholder?: string; required?: boolean; readOnly?: boolean;
rightButton?: React.ReactNode;
}> = ({ id, label, name, value, onChange, type = 'text', placeholder, required, readOnly, rightButton }) => (
<div>
<label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1">{label}</label>
<div className="relative">
<input
type={type} id={id} name={name} value={value} onChange={onChange}
placeholder={placeholder} required={required} readOnly={readOnly}
className={cn(
'block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm sm:text-sm',
'placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500',
readOnly && 'bg-gray-100',
rightButton && 'pr-10',
)}
/>
{rightButton}
</div>
</div>
);
const Setup: React.FC = () => {
const navigate = useNavigate();
const { logout } = useAuthStore();
@ -33,11 +91,18 @@ const Setup: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [envContent, setEnvContent] = useState<string | null>(null);
const [showSuccess, setShowSuccess] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showDbPassword, setShowDbPassword] = useState(false);
const [visiblePasswords, setVisiblePasswords] = useState<Set<string>>(new Set());
const [signingKeys, setSigningKeys] = useState<SigningKeys | null>(null);
const [generatingKeys, setGeneratingKeys] = useState(false);
const togglePassword = useCallback((field: string) => {
setVisiblePasswords(prev => {
const next = new Set(prev);
next.has(field) ? next.delete(field) : next.add(field);
return next;
});
}, []);
const [formData, setFormData] = useState<SetupFormData>({
adminUser: 'admin',
adminPassword: '',
@ -337,299 +402,97 @@ const Setup: React.FC = () => {
{/* Setup Form */}
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
<form onSubmit={handleSubmit} className="space-y-8">
{/* Error Display */}
{error && (
<div className="alert alert-danger rounded-md">
<div className="text-sm text-red-800">{error}</div>
</div>
)}
{error && <Alert kind="error">{error}</Alert>}
{/* Administrator Account */}
<div>
<div className="flex items-center mb-4">
<User className="h-5 w-5 text-indigo-600 mr-2" />
<h3 className="text-lg font-semibold text-gray-900">Administrator Account</h3>
</div>
<FormSection icon={User} title="Administrator Account">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label htmlFor="adminUser" className="block text-sm font-medium text-gray-700 mb-1">
Admin Username
</label>
<input
type="text"
id="adminUser"
name="adminUser"
value={formData.adminUser}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="admin"
required
/>
</div>
<div>
<label htmlFor="adminPassword" className="block text-sm font-medium text-gray-700 mb-1">
Admin Password
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
id="adminPassword"
name="adminPassword"
value={formData.adminPassword}
onChange={handleInputChange}
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Enter secure password"
required
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-gray-400" />
) : (
<Eye className="h-4 w-4 text-gray-400" />
)}
<TextField id="adminUser" label="Admin Username" name="adminUser"
value={formData.adminUser} onChange={handleInputChange} placeholder="admin" required />
<TextField id="adminPassword" label="Admin Password" name="adminPassword"
value={formData.adminPassword} onChange={handleInputChange}
type={visiblePasswords.has('admin') ? 'text' : 'password'}
placeholder="Enter secure password" required
rightButton={
<button type="button" className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => togglePassword('admin')}>
{visiblePasswords.has('admin')
? <EyeOff className="h-4 w-4 text-gray-400" />
: <Eye className="h-4 w-4 text-gray-400" />}
</button>
</div>
</div>
</div>
</div>
{/* Security Keys Section */}
<div>
<div className="flex items-center mb-4">
<Key className="h-5 w-5 text-indigo-600 mr-2" />
<h3 className="text-lg font-semibold text-gray-900">Security Keys</h3>
</div>
<div className="alert alert-info rounded-md mb-4">
<p className="text-sm text-blue-800">
Generate Ed25519 signing keys for secure agent updates.
<strong> Save the private key securely</strong> - it will be included in your configuration.
</p>
} />
</div>
</FormSection>
<FormSection icon={Key} title="Security Keys">
<Alert kind="info" className="mb-4">
Generate Ed25519 signing keys for secure agent updates.
<strong> Save the private key securely</strong> it will be included in your configuration.
</Alert>
{!signingKeys ? (
<button
type="button"
onClick={handleGenerateKeys}
disabled={generatingKeys}
className="w-full py-2 px-4 border border-indigo-600 text-indigo-600 rounded-md hover:bg-indigo-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
<button type="button" onClick={handleGenerateKeys} disabled={generatingKeys}
className="w-full py-2 px-4 border border-indigo-600 text-indigo-600 rounded-md hover:bg-indigo-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center">
{generatingKeys ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-indigo-600 mr-2"></div>
Generating Keys...
</>
<><div className="animate-spin rounded-full h-4 w-4 border-b-2 border-indigo-600 mr-2" />Generating Keys...</>
) : (
<>
<Key className="h-4 w-4 mr-2" />
Generate Signing Keys
</>
<><Key className="h-4 w-4 mr-2" />Generate Signing Keys</>
)}
</button>
) : (
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Public Key Fingerprint
</label>
<input
readOnly
value={signingKeys.fingerprint}
className="block w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Algorithm
</label>
<input
readOnly
value={signingKeys.algorithm.toUpperCase()}
className="block w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md text-sm"
/>
</div>
<div className="alert alert-success rounded-md p-3">
<p className="text-sm text-green-800">
Keys generated! Private key will be securely included in your configuration file.
</p>
</div>
<TextField id="keyFingerprint" label="Public Key Fingerprint" name="keyFingerprint"
value={signingKeys.fingerprint} onChange={() => {}} readOnly />
<TextField id="keyAlgorithm" label="Algorithm" name="keyAlgorithm"
value={signingKeys.algorithm.toUpperCase()} onChange={() => {}} readOnly />
<Alert kind="success">
Keys generated! Private key will be securely included in your configuration file.
</Alert>
</div>
)}
</div>
</FormSection>
{/* Database Configuration */}
<div>
<div className="flex items-center mb-4">
<Database className="h-5 w-5 text-indigo-600 mr-2" />
<h3 className="text-lg font-semibold text-gray-900">Database Configuration</h3>
</div>
<FormSection icon={Database} title="Database Configuration">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label htmlFor="dbHost" className="block text-sm font-medium text-gray-700 mb-1">
Database Host
</label>
<input
type="text"
id="dbHost"
name="dbHost"
value={formData.dbHost}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="postgres"
required
/>
</div>
<div>
<label htmlFor="dbPort" className="block text-sm font-medium text-gray-700 mb-1">
Database Port
</label>
<input
type="number"
id="dbPort"
name="dbPort"
value={formData.dbPort}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
required
/>
</div>
<div>
<label htmlFor="dbName" className="block text-sm font-medium text-gray-700 mb-1">
Database Name
</label>
<input
type="text"
id="dbName"
name="dbName"
value={formData.dbName}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="redflag"
required
/>
</div>
<div>
<label htmlFor="dbUser" className="block text-sm font-medium text-gray-700 mb-1">
Database User
</label>
<input
type="text"
id="dbUser"
name="dbUser"
value={formData.dbUser}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="redflag"
required
/>
</div>
<div>
<label htmlFor="dbPassword" className="block text-sm font-medium text-gray-700 mb-1">
Database Password
</label>
<div className="relative">
<input
type={showDbPassword ? 'text' : 'password'}
id="dbPassword"
name="dbPassword"
value={formData.dbPassword}
onChange={handleInputChange}
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Enter database password"
required
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowDbPassword(!showDbPassword)}
>
{showDbPassword ? (
<EyeOff className="h-4 w-4 text-gray-400" />
) : (
<Eye className="h-4 w-4 text-gray-400" />
)}
<TextField id="dbHost" label="Database Host" name="dbHost"
value={formData.dbHost} onChange={handleInputChange} placeholder="postgres" required />
<TextField id="dbPort" label="Database Port" name="dbPort"
value={formData.dbPort} onChange={handleInputChange} type="number" required />
<TextField id="dbName" label="Database Name" name="dbName"
value={formData.dbName} onChange={handleInputChange} placeholder="redflag" required />
<TextField id="dbUser" label="Database User" name="dbUser"
value={formData.dbUser} onChange={handleInputChange} placeholder="redflag" required />
<TextField id="dbPassword" label="Database Password" name="dbPassword"
value={formData.dbPassword} onChange={handleInputChange}
type={visiblePasswords.has('db') ? 'text' : 'password'}
placeholder="Enter database password" required
rightButton={
<button type="button" className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => togglePassword('db')}>
{visiblePasswords.has('db')
? <EyeOff className="h-4 w-4 text-gray-400" />
: <Eye className="h-4 w-4 text-gray-400" />}
</button>
</div>
</div>
} />
</div>
</div>
</FormSection>
{/* Server Configuration */}
<div>
<div className="flex items-center mb-4">
<Settings className="h-5 w-5 text-indigo-600 mr-2" />
<h3 className="text-lg font-semibold text-gray-900">Server Configuration</h3>
</div>
<FormSection icon={Settings} title="Server Configuration">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label htmlFor="serverHost" className="block text-sm font-medium text-gray-700 mb-1">
Server Host
</label>
<input
type="text"
id="serverHost"
name="serverHost"
value={formData.serverHost}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="0.0.0.0"
required
/>
</div>
<div>
<label htmlFor="serverPort" className="block text-sm font-medium text-gray-700 mb-1">
Server Port
</label>
<input
type="number"
id="serverPort"
name="serverPort"
value={formData.serverPort}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="8080"
required
/>
</div>
<div>
<label htmlFor="maxSeats" className="block text-sm font-medium text-gray-700 mb-1">
Maximum Agent Seats
</label>
<input
type="number"
id="maxSeats"
name="maxSeats"
value={formData.maxSeats}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
min="1"
max="1000"
placeholder="50"
required
/>
<p className="mt-1 text-xs text-gray-500">Security limit for agent registration</p>
</div>
<TextField id="serverHost" label="Server Host" name="serverHost"
value={formData.serverHost} onChange={handleInputChange} placeholder="0.0.0.0" required />
<TextField id="serverPort" label="Server Port" name="serverPort"
value={formData.serverPort} onChange={handleInputChange} type="number" placeholder="8080" required />
<TextField id="maxSeats" label="Maximum Agent Seats" name="maxSeats"
value={formData.maxSeats} onChange={handleInputChange} type="number" required
placeholder="50" />
<p className="mt-1 text-xs text-gray-500">Security limit for agent registration</p>
<div className="sm:col-span-2">
<label htmlFor="publicURL" className="block text-sm font-medium text-gray-700 mb-1">
Agent-facing Server URL
</label>
<input
type="url"
id="publicURL"
name="publicURL"
value={formData.publicURL}
onChange={handleInputChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="http://redflag.example.com:8080"
required
/>
<TextField id="publicURL" label="Agent-facing Server URL" name="publicURL"
value={formData.publicURL} onChange={handleInputChange} type="url"
placeholder="http://redflag.example.com:8080" required />
<p className="mt-1 text-xs text-gray-500">Used in generated install commands and agent callbacks</p>
</div>
</div>
</div>
</FormSection>
{/* Submit Button */}
<div className="pt-6 border-t border-gray-200">