132 lines
6.0 KiB
TypeScript
132 lines
6.0 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { ALLOWED_MODES, defaultStackConfig } from '../../types/provisioning';
|
|
import type { ComponentMode, ProvisioningRequest, StackConfig, TenantEnvironment, TenantTier } from '../../types/provisioning';
|
|
|
|
interface Props {
|
|
signalParent: (state: { isValid: boolean }) => void;
|
|
data: ProvisioningRequest;
|
|
onChange: (updated: Partial<ProvisioningRequest>) => void;
|
|
}
|
|
|
|
const ENVIRONMENTS: { value: TenantEnvironment; label: string; description: string }[] = [
|
|
{ value: 'fdev', label: 'Dev (fdev)', description: 'Feature dev — fast provisioning, no production data.' },
|
|
{ value: 'uat', label: 'UAT', description: 'User acceptance testing — mirrors production config.' },
|
|
{ value: 'prod', label: 'Production', description: 'Live production. Full isolation enforced.' },
|
|
];
|
|
|
|
const TIERS: { value: TenantTier; label: string; badge: string; description: string }[] = [
|
|
{ value: 'Trial', label: 'Trial', badge: 'Sandbox', description: 'Ephemeral all-in-one sandbox. No persistent data guarantee.' },
|
|
{ value: 'Shared', label: 'Shared', badge: 'Standard', description: 'Shared platform services, isolated by realm/schema/bucket.' },
|
|
{ value: 'Dedicated', label: 'Dedicated', badge: 'Professional', description: 'Own sidecar containers per component on the shared host.' },
|
|
{ value: 'Enterprise', label: 'Enterprise', badge: 'Enterprise', description: 'Full VM isolation per component, provisioned via Pulumi.' },
|
|
];
|
|
|
|
const MODE_LABELS: Record<ComponentMode, string> = {
|
|
SharedPlatform: 'Shared Platform',
|
|
Bundled: 'Bundled (in image)',
|
|
OwnContainer: 'Own Container',
|
|
VpsDocker: 'VPS — Docker',
|
|
VpsBareMetal: 'VPS — Bare Metal',
|
|
};
|
|
|
|
const COMPONENTS: { key: keyof StackConfig; label: string; description: string }[] = [
|
|
{ key: 'postgres', label: 'PostgreSQL', description: 'Relational database for tenant data.' },
|
|
{ key: 'keycloak', label: 'Keycloak', description: 'Identity & access management (realms, OIDC clients).' },
|
|
{ key: 'vault', label: 'Vault', description: 'Secrets management and dynamic credentials.' },
|
|
{ key: 'minio', label: 'MinIO', description: 'Object storage (S3-compatible).' },
|
|
];
|
|
|
|
export default function DeploymentConfigStep({ signalParent, data, onChange }: Props) {
|
|
useEffect(() => {
|
|
signalParent({ isValid: !!data.tier && !!data.environment });
|
|
}, [data.tier, data.environment, signalParent]);
|
|
|
|
function handleTierChange(tier: TenantTier) {
|
|
// Reset stackConfig to the default for the new tier so nothing is invalid
|
|
onChange({ tier, stackConfig: defaultStackConfig(tier) });
|
|
}
|
|
|
|
function handleComponentChange(key: keyof StackConfig, mode: ComponentMode) {
|
|
onChange({ stackConfig: { ...data.stackConfig, [key]: mode } });
|
|
}
|
|
|
|
const allowed = ALLOWED_MODES[data.tier];
|
|
|
|
return (
|
|
<div className="wizard-step">
|
|
<p className="step-description">Choose the deployment environment, isolation tier, and per-component infrastructure mode.</p>
|
|
|
|
{/* ── Environment ───────────────────────────────────────── */}
|
|
<h4 style={{ marginBottom: '0.5rem' }}>Environment</h4>
|
|
<div className="tier-cards" style={{ marginBottom: '1.5rem' }}>
|
|
{ENVIRONMENTS.map((env) => (
|
|
<button
|
|
key={env.value}
|
|
type="button"
|
|
className={`tier-card${data.environment === env.value ? ' selected' : ''}`}
|
|
onClick={() => onChange({ environment: env.value })}
|
|
>
|
|
<div className="tier-card-header">
|
|
<span className="tier-card-label">{env.label}</span>
|
|
</div>
|
|
<p className="tier-card-description">{env.description}</p>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* ── Isolation Tier ────────────────────────────────────── */}
|
|
<h4 style={{ marginBottom: '0.5rem' }}>Isolation Tier</h4>
|
|
<div className="tier-cards" style={{ marginBottom: '1.5rem' }}>
|
|
{TIERS.map((tier) => (
|
|
<button
|
|
key={tier.value}
|
|
type="button"
|
|
className={`tier-card${data.tier === tier.value ? ' selected' : ''}`}
|
|
onClick={() => handleTierChange(tier.value)}
|
|
>
|
|
<div className="tier-card-header">
|
|
<span className="tier-card-label">{tier.label}</span>
|
|
<span className={`tier-card-badge tier-badge-${tier.value.toLowerCase()}`}>{tier.badge}</span>
|
|
</div>
|
|
<p className="tier-card-description">{tier.description}</p>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* ── Per-Component Stack Config ────────────────────────── */}
|
|
<h4 style={{ marginBottom: '0.25rem' }}>Stack Configuration</h4>
|
|
<p style={{ fontSize: '0.85rem', color: '#5f6b7c', marginBottom: '0.75rem' }}>
|
|
Defaults are set by the tier. Override individual components as needed.
|
|
</p>
|
|
<table className="stack-config-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Component</th>
|
|
<th>Description</th>
|
|
<th>Mode</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{COMPONENTS.map(({ key, label, description }) => (
|
|
<tr key={key}>
|
|
<td><strong>{label}</strong></td>
|
|
<td style={{ color: '#5f6b7c', fontSize: '0.85rem' }}>{description}</td>
|
|
<td>
|
|
<select
|
|
className="stack-config-select"
|
|
value={data.stackConfig[key]}
|
|
onChange={(e) => handleComponentChange(key, e.target.value as ComponentMode)}
|
|
>
|
|
{allowed.map((mode) => (
|
|
<option key={mode} value={mode}>{MODE_LABELS[mode]}</option>
|
|
))}
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|