fix(onboarding): explain blocked Next, humanise errors, de-jargon provisioning

This commit is contained in:
2026-05-29 23:28:45 +02:00
parent 08f28aeb93
commit 3110b40cf9
2 changed files with 99 additions and 42 deletions

View File

@@ -432,25 +432,35 @@ export function ProvisioningStatus({ requestId, canAct }: Props) {
<span className="text-xs text-text-muted">{t("phase")}</span>
<StatusBadge phase={phase} />
</div>
{conditions.map((c, i) => (
<div
key={i}
className="flex items-center justify-between bg-surface-2 border border-border rounded-lg px-4 py-2"
>
<span className="text-xs text-text-muted">{c.type}</span>
<span
className={`text-xs font-mono ${
c.status === "True"
? "text-emerald-400"
: c.status === "False"
? "text-red-400"
: "text-text-muted"
}`}
>
{c.reason || c.status}
</span>
</div>
))}
{/* Setup progress. The operator reports readiness as a list of
internal K8s conditions (OpenBao policy, LiteLLM key, network
policy, …) — meaningful to operators, jargon to customers.
We surface the *shape* of that progress (how many steps are
done) without leaking the internal names. */}
{conditions.length > 0 &&
(() => {
const done = conditions.filter((c) => c.status === "True").length;
const total = conditions.length;
const pct = Math.round((done / total) * 100);
return (
<div className="bg-surface-2 border border-border rounded-lg px-4 py-3">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-text-muted">
{t("setupProgress")}
</span>
<span className="text-xs font-medium text-text-secondary tabular-nums">
{t("setupStepsComplete", { done, total })}
</span>
</div>
<div className="h-1.5 w-full rounded-full bg-surface-3 overflow-hidden">
<div
className="h-full bg-accent transition-all duration-500"
style={{ width: `${pct}%` }}
/>
</div>
</div>
);
})()}
</div>
</Card>
);

View File

@@ -420,18 +420,51 @@ export function OnboardingWizard({
[]
);
// Validate that all secret-requiring enabled packages have complete credentials
const packageCredentialsValid = (): boolean => {
// Enabled packages that still need something from the user before the
// configure step can advance — a missing credential field or an
// unaccepted disclaimer. Returns the package defs so the UI can name
// exactly what's blocking the (otherwise silently disabled) Next
// button instead of greying it out with no explanation.
const incompletePackages = (): PackageDef[] => {
const out: PackageDef[] = [];
for (const pkgId of config.packages) {
const def = PACKAGE_CATALOG.find((p) => p.id === pkgId);
if (!def?.requiresSecrets) continue;
const secrets = packageSecrets[pkgId] || {};
for (const field of def.secrets || []) {
if (!secrets[field.key]?.trim()) return false;
if (!def) continue;
let incomplete = false;
if (def.requiresSecrets) {
const secrets = packageSecrets[pkgId] || {};
for (const field of def.secrets || []) {
if (!secrets[field.key]?.trim()) {
incomplete = true;
break;
}
}
}
if (def.disclaimerKey && !disclaimerAccepted[pkgId]) return false;
if (def.disclaimerKey && !disclaimerAccepted[pkgId]) incomplete = true;
if (incomplete) out.push(def);
}
return true;
return out;
};
const packageCredentialsValid = (): boolean =>
incompletePackages().length === 0;
// Map zod field paths to human labels for the confirm-step error
// summary, so a stray validation failure reads "Postal code" rather
// than "billingAddress.postalCode". Unknown paths fall back to the
// raw path (this defence-in-depth list should rarely render at all).
const fieldLabel = (path: string): string => {
const map: Record<string, string> = {
instanceName: t("instanceName"),
agentName: t("agentName"),
"billingAddress.company": t("billingCompany"),
"billingAddress.street": t("billingStreet"),
"billingAddress.postalCode": t("billingPostalCode"),
"billingAddress.city": t("billingCity"),
"billingAddress.country": t("billingCountry"),
"billingAddress.vatNumber": t("billingVatNumber"),
};
return map[path] ?? path;
};
const handleSubmit = async () => {
@@ -984,20 +1017,33 @@ export function OnboardingWizard({
</div>
</div>
<div className="flex justify-between mt-6">
<button
onClick={goBack}
className="py-2 px-4 text-sm text-text-secondary hover:text-text-primary transition-colors"
>
{t("back")}
</button>
<button
onClick={goNext}
disabled={!packageCredentialsValid()}
className="py-2 px-6 bg-accent text-surface-0 text-sm font-medium rounded-lg hover:bg-accent-dim transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{t("next")}
</button>
<div className="mt-6">
{(() => {
const blocking = incompletePackages();
if (blocking.length === 0) return null;
return (
<p className="text-xs text-amber-400/90 mb-3 text-right">
{t("packagesIncompleteHint", {
packages: blocking.map((p) => p.name).join(", "),
})}
</p>
);
})()}
<div className="flex justify-between">
<button
onClick={goBack}
className="py-2 px-4 text-sm text-text-secondary hover:text-text-primary transition-colors"
>
{t("back")}
</button>
<button
onClick={goNext}
disabled={!packageCredentialsValid()}
className="py-2 px-6 bg-accent text-surface-0 text-sm font-medium rounded-lg hover:bg-accent-dim transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{t("next")}
</button>
</div>
</div>
</Card>
)}
@@ -1380,7 +1426,8 @@ export function OnboardingWizard({
<ul className="list-disc list-inside space-y-0.5">
{Object.entries(errors).map(([path, msg]) => (
<li key={path}>
<span className="font-mono">{path}</span>: {msg}
<span className="font-medium">{fieldLabel(path)}</span>:{" "}
{msg}
</li>
))}
</ul>