ui(infrastructure): strip explainer prose from empty + detail panes
Empty Infrastructure tabs were a wall of teaching text — every empty
category showed a dashed-border explainer button in the rail, the
right pane carried multi-paragraph explainer prose for every category,
and the LLM/Stripe/Storage/Secrets details all included instructional
prose for things the user hasn't done yet. On a brand-new project the
whole tab was 90% explainer.
Removed (everywhere):
- Per-category dashed empty-state buttons under each category
header. Empty categories now show just the header + "0", which
is enough — clicking the header still opens the details pane if
the user wants it.
- Right-pane Overview lead paragraph ("Infrastructure here is
auto-discovered…").
- CategoryDetail lead paragraph (def.label + def.blurb) and the
"Examples" chip row.
- CategoryDetail "Nothing detected yet — set a matching env var…"
info box. Replaced with a one-word "None yet." line.
- LLM CategoryDetail BYOK explainer box.
- DatabaseDetail "Expand this database in the left rail…" hint.
- DatabaseDetail "Set <KEY> on any app or service…" prose.
Renamed the section "How apps connect" → "Connection env" and
kept just the env-var snippet.
- StorageDetail unprovisioned-state explainer paragraph + "you can
also provision it now" info box. Replaced with a one-word
"No bucket provisioned yet." empty.
- StorageDetail "Vibn-bundled storage is GCS exposed via…" prose.
Kept the env snippet under a renamed "Connection env" section.
- ProviderDetail Stripe webhook hint box.
- ProviderDetail bottom "Values aren't shown here…" note.
- SecretsDetail "Values are never read on this surface…" note.
Kept (functional, non-prose):
- All KvRows and code snippets.
- Stripe "Connect Stripe (coming soon)" CTA.
- Storage "View workspace bucket" CTA.
- Provider dashboard external-link buttons.
- Edit / Rotate icon buttons on secrets (still disabled, tooltipped).
Cleaned up the now-unused Info icon import. The category def's
blurb / examples / dashboards fields stay in the source — they're
still useful as tooltip / search seed material for a follow-up
iteration that puts the teaching content somewhere less in-your-face
(like a help drawer).
Made-with: Cursor
This commit is contained in:
@@ -5,7 +5,7 @@ import { useParams } from "next/navigation";
|
||||
import {
|
||||
Loader2, AlertCircle, Database, KeyRound, CircleDot,
|
||||
ShieldCheck, Mail, CreditCard, Sparkles, HardDrive,
|
||||
ExternalLink, Info, Pencil, RotateCw,
|
||||
ExternalLink, Pencil, RotateCw,
|
||||
ChevronDown, ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy";
|
||||
@@ -219,7 +219,7 @@ function CategoryRail({
|
||||
return (
|
||||
<div style={railGroup}>
|
||||
<CategoryHeader def={def} count={present ? 1 : 0} active={headerActive} onClick={() => onSelect({ kind: "category", category: def.key })} />
|
||||
{present ? (
|
||||
{present && (
|
||||
<div style={railItems}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -235,10 +235,6 @@ function CategoryRail({
|
||||
<CircleDot size={9} style={{ color: storageColor(s.status), flexShrink: 0 }} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button type="button" onClick={() => onSelect({ kind: "category", category: def.key })} style={railEmptyButton}>
|
||||
{def.blurb}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -251,11 +247,7 @@ function CategoryRail({
|
||||
return (
|
||||
<div style={railGroup}>
|
||||
<CategoryHeader def={def} count={dbs.length} active={headerActive} onClick={() => onSelect({ kind: "category", category: def.key })} />
|
||||
{dbs.length === 0 ? (
|
||||
<button type="button" onClick={() => onSelect({ kind: "category", category: def.key })} style={railEmptyButton}>
|
||||
{def.blurb}
|
||||
</button>
|
||||
) : (
|
||||
{dbs.length > 0 && (
|
||||
<div style={railItems}>
|
||||
{dbs.map(db => {
|
||||
const open = expandedDbs.has(db.uuid);
|
||||
@@ -320,11 +312,7 @@ function CategoryRail({
|
||||
return (
|
||||
<div style={railGroup}>
|
||||
<CategoryHeader def={def} count={count} active={headerActive} onClick={() => onSelect({ kind: "category", category: def.key })} />
|
||||
{items.length === 0 ? (
|
||||
<button type="button" onClick={() => onSelect({ kind: "category", category: def.key })} style={railEmptyButton}>
|
||||
{def.blurb}
|
||||
</button>
|
||||
) : (
|
||||
{items.length > 0 && (
|
||||
<div style={railItems}>
|
||||
{items.map(item => renderRailItem(def.key, item, selection, onSelect))}
|
||||
</div>
|
||||
@@ -447,13 +435,6 @@ function Overview({
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<Para>
|
||||
Infrastructure here is <strong>auto-discovered</strong> from your Coolify
|
||||
project + the env vars across every app and service in it. Click any
|
||||
tile on the left for live details. Click a category header (or its
|
||||
empty hint) to learn what lives there.
|
||||
</Para>
|
||||
|
||||
<div style={overviewGrid}>
|
||||
<OverviewStat
|
||||
label="Databases" value={dbCount}
|
||||
@@ -486,7 +467,7 @@ function CategoryDetail({
|
||||
def.key === "storage" ? (anatomy.infrastructure.bundledStorage.status === "unprovisioned" ? 0 : 1) :
|
||||
items.length;
|
||||
|
||||
// Special CTAs per category
|
||||
// Per-category functional CTAs (no explainer prose).
|
||||
let actionRow: React.ReactNode = null;
|
||||
if (def.key === "payments" && !items.length) {
|
||||
actionRow = (
|
||||
@@ -494,15 +475,6 @@ function CategoryDetail({
|
||||
Connect Stripe (coming soon)
|
||||
</button>
|
||||
);
|
||||
} else if (def.key === "llm") {
|
||||
actionRow = (
|
||||
<div style={emptyBox}>
|
||||
<Info size={13} style={{ color: INK.muted, marginRight: 6, verticalAlign: "-2px" }} />
|
||||
BYOK (Bring Your Own Key) lets you use your own model provider keys
|
||||
instead of platform defaults — coming soon. Today, set keys via env vars
|
||||
and they'll appear here.
|
||||
</div>
|
||||
);
|
||||
} else if (def.key === "storage") {
|
||||
actionRow = (
|
||||
<button type="button" onClick={() => onJump({ kind: "storage" })} style={ctaButton}>
|
||||
@@ -513,17 +485,6 @@ function CategoryDetail({
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
||||
<Para><strong>{def.label}.</strong> {def.blurb}</Para>
|
||||
|
||||
{def.examples.length > 0 && (
|
||||
<div>
|
||||
<SectionTitle>Examples</SectionTitle>
|
||||
<div style={chipRow}>
|
||||
{def.examples.map(ex => <span key={ex} style={chip}>{ex}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actionRow}
|
||||
|
||||
<div>
|
||||
@@ -532,11 +493,7 @@ function CategoryDetail({
|
||||
<span style={{ color: INK.mid, fontWeight: 400 }}>({count})</span>
|
||||
</SectionTitle>
|
||||
{count === 0 ? (
|
||||
<div style={emptyBox}>
|
||||
<Info size={13} style={{ color: INK.muted, marginRight: 6, verticalAlign: "-2px" }} />
|
||||
Nothing detected yet. Set a matching env var (or provision a Coolify
|
||||
resource) and it'll appear here on next reload.
|
||||
</div>
|
||||
<div style={emptyBox}>None yet.</div>
|
||||
) : def.key === "secrets" ? (
|
||||
<div style={listBox}>
|
||||
{anatomy.infrastructure.secrets.byResource.map(r => (
|
||||
@@ -591,22 +548,11 @@ function DatabaseDetail({ uuid, anatomy }: { uuid: string; anatomy: Anatomy }) {
|
||||
{db.publicPort != null && <KvRow label="Public port" value={String(db.publicPort)} />}
|
||||
{db.internalAddress && <KvRow label="Internal address" value={db.internalAddress} mono />}
|
||||
|
||||
<div style={emptyBox}>
|
||||
<Info size={13} style={{ color: INK.muted, marginRight: 6, verticalAlign: "-2px" }} />
|
||||
Expand this database in the left rail to browse tables; click any
|
||||
table there to preview rows here.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SectionTitle>How apps connect</SectionTitle>
|
||||
<SectionTitle>Connection env</SectionTitle>
|
||||
<div style={codeBox}>
|
||||
<code style={code}>{db.consumerEnvKey}={"<set in Coolify env>"}</code>
|
||||
</div>
|
||||
<Para style={{ marginTop: 6 }}>
|
||||
Set <code style={inlineCode}>{db.consumerEnvKey}</code> on any app or
|
||||
service that needs to talk to this database. In-cluster apps reach it
|
||||
at <code style={inlineCode}>{db.internalAddress ?? db.name}</code>.
|
||||
</Para>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -634,16 +580,6 @@ function ProviderDetail({ id, anatomy }: { id: string; anatomy: Anatomy }) {
|
||||
</a>
|
||||
)}
|
||||
|
||||
{isStripe && (
|
||||
<div style={emptyBox}>
|
||||
<Info size={13} style={{ color: INK.muted, marginRight: 6, verticalAlign: "-2px" }} />
|
||||
Stripe's webhook URL should point at{" "}
|
||||
<code style={inlineCode}>https://<your-app-domain>/api/stripe/webhook</code>.
|
||||
Full Connect-with-Stripe OAuth flow is coming next so we can wire
|
||||
webhook secret + price IDs automatically.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<SectionTitle>Detected here</SectionTitle>
|
||||
{p.attachments.map(att => (
|
||||
@@ -657,12 +593,6 @@ function ProviderDetail({ id, anatomy }: { id: string; anatomy: Anatomy }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Para style={{ color: INK.mid }}>
|
||||
<Info size={12} style={{ marginRight: 4, verticalAlign: "-1px" }} />
|
||||
Values aren't shown here. To read or edit a key, open the matching
|
||||
resource under <strong>Secrets</strong> or ask the AI in chat.
|
||||
</Para>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -671,19 +601,7 @@ function StorageDetail({ anatomy }: { anatomy: Anatomy }) {
|
||||
const s = anatomy.infrastructure.bundledStorage;
|
||||
|
||||
if (s.status === "unprovisioned") {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
||||
<Para>
|
||||
This workspace doesn't have a bucket provisioned yet. The first time
|
||||
you ask the AI to upload, store or serve files, Vibn will create one
|
||||
automatically.
|
||||
</Para>
|
||||
<div style={emptyBox}>
|
||||
<Info size={13} style={{ color: INK.muted, marginRight: 6, verticalAlign: "-2px" }} />
|
||||
You can also provision it now from the workspace settings (coming soon).
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Empty>No bucket provisioned yet.</Empty>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -694,23 +612,16 @@ function StorageDetail({ anatomy }: { anatomy: Anatomy }) {
|
||||
{s.hmacAccessId && <KvRow label="HMAC access id" value={s.hmacAccessId} mono />}
|
||||
|
||||
<div>
|
||||
<SectionTitle>How apps connect</SectionTitle>
|
||||
<SectionTitle>Connection env</SectionTitle>
|
||||
<div style={codeBox}>
|
||||
<code style={code}>
|
||||
{`STORAGE_ENDPOINT=https://storage.googleapis.com
|
||||
STORAGE_REGION=${s.region ?? "auto"}
|
||||
STORAGE_BUCKET=${s.bucketName ?? "<bucket>"}
|
||||
STORAGE_ACCESS_KEY_ID=${s.hmacAccessId ?? "<access-id>"}
|
||||
STORAGE_SECRET_ACCESS_KEY=<set via AI: services.envs.upsert>`}
|
||||
STORAGE_SECRET_ACCESS_KEY=<set via AI>`}
|
||||
</code>
|
||||
</div>
|
||||
<Para style={{ marginTop: 6 }}>
|
||||
Vibn-bundled storage is GCS exposed via the S3-compatible HMAC
|
||||
interop API, so any AWS S3 SDK works. The secret access key is
|
||||
encrypted at rest and never returned to the browser — to inject it
|
||||
into an app, ask the AI to run{" "}
|
||||
<code style={inlineCode}>services.envs.upsert</code>.
|
||||
</Para>
|
||||
</div>
|
||||
|
||||
{s.errorMessage && (
|
||||
@@ -758,12 +669,6 @@ function SecretsDetail({
|
||||
|
||||
<div>
|
||||
<SectionTitle>Keys</SectionTitle>
|
||||
<Para style={{ color: INK.mid, marginBottom: 8 }}>
|
||||
Values are never read on this surface. <strong>Edit</strong> sets a new
|
||||
value via <code style={inlineCode}>{r.resourceKind}s.envs.upsert</code>;{" "}
|
||||
<strong>Rotate</strong> opens AI chat with a pre-filled rotation request
|
||||
for that key. (Both wire-ups land in the next iteration.)
|
||||
</Para>
|
||||
<div style={listBox}>
|
||||
{[...groups.values()].map(g => (
|
||||
<div key={g.label} style={{ borderBottom: `1px solid ${INK.borderSoft}` }}>
|
||||
|
||||
Reference in New Issue
Block a user