fix(onboarding): explain blocked Next, humanise errors, de-jargon provisioning
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user