mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(dashboard): an empty project says it's empty instead of drawing empty charts (BEA-51) (#101)
A brand-new project's Dashboard rendered ~840px of empty bordered SVG
frames with the "hot + stale" / "hot + fresh" / "cold + stale" quadrant
labels floating over nothing: Treemap and Scatter had no empty guard, and
the quadrant labels come from the HOT_READS/STALE_DAYS constants rather
than from data. Only HotPath said anything, and what it said ("No reads in
the window yet") is the wrong claim — the project has no files at all.
The guard goes one level up in Insights, where the three panel headers and
the lens switcher also live, so those go away too instead of sitting over
nothing. Gated on "no files", never on "no reads": files-with-no-reads is
the other zero state and it already behaves correctly (Treemap pads every
file to reads + 1 so unread files keep a sliver).
Two new optional props. `installHref` puts a real <a> in the empty state,
routed through linkProps so it stays copyable and middle-clickable — the
dashboard route passes it, the project home doesn't, because ConnectGuide
directly above it IS the set-up-a-device guide. `loading` keeps the tree's
first frame from claiming a populated project has no files.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
820978cd76
commit
9c18c83845
@@ -13,3 +13,34 @@ test("reads × freshness names all four quadrants", async ({ page }) => {
|
||||
"cold + fresh",
|
||||
]);
|
||||
});
|
||||
|
||||
// A brand-new project used to draw ~840px of empty frames with the quadrant
|
||||
// labels floating over nothing — the first screen every project shows.
|
||||
// Created at runtime and deleted: a permanent fixture sorting before "wiki"
|
||||
// would move where the app lands and break home.spec.ts.
|
||||
test("a project with no files says so instead of drawing empty charts", async ({ page }) => {
|
||||
await login(page);
|
||||
const made = await (await page.request.post("/api/projects", { data: { name: "blank" } })).json();
|
||||
try {
|
||||
await page.goto(`/${made.project.id}/dashboard`);
|
||||
await expect(page.locator(".in-blank")).toContainText("no files");
|
||||
await expect(page.locator(".in-chart")).toHaveCount(0);
|
||||
await expect(page.locator("body")).not.toContainText("hot + stale");
|
||||
await expect(page.locator(".in-blank a")).toHaveAttribute(
|
||||
"href",
|
||||
`/${made.project.id}/install`,
|
||||
);
|
||||
} finally {
|
||||
await page.request.delete("/api/projects/" + made.project.id);
|
||||
}
|
||||
});
|
||||
|
||||
// The other zero state, which was never broken: files that nobody has read
|
||||
// still get a map, a scatter and a self-explaining hot path.
|
||||
test("files with no reads still chart", async ({ page }) => {
|
||||
await login(page);
|
||||
const pid = await wikiId(page);
|
||||
await page.goto(`/${pid}/dashboard`);
|
||||
await expect(page.locator(".in-treemap")).toBeVisible();
|
||||
await expect(page.locator(".in-blank")).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -330,6 +330,8 @@ export default function Browser(props: {
|
||||
heatMap={heatMap}
|
||||
devices={devices}
|
||||
scope={route.viewTarget || ""}
|
||||
loading={!loaded}
|
||||
installHref={project ? urlForView("install", project.id) : undefined}
|
||||
onOpenFile={openPath}
|
||||
onOpenFolder={openPath}
|
||||
isFolder={isFolderFn}
|
||||
@@ -420,10 +422,14 @@ export default function Browser(props: {
|
||||
<>
|
||||
<ConnectGuide project={project!} existing={route.connect === "existing"} />
|
||||
<div className="home-insights">
|
||||
{/* No install CTA here: ConnectGuide directly above IS the set-up-a-
|
||||
device guide, and a second button six inches under the first
|
||||
reads as two different steps. */}
|
||||
<Insights
|
||||
flatFiles={flatFiles}
|
||||
heatMap={heatMap}
|
||||
devices={devices}
|
||||
loading={!loaded}
|
||||
onOpenFile={openPath}
|
||||
onOpenFolder={openPath}
|
||||
isFolder={isFolderFn}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getJSON } from "../api/http";
|
||||
import type { HeatMap, Node } from "../api/types";
|
||||
import { heatTotal, hotPathSplit } from "../hooks/useBrowse";
|
||||
import { ageRange, ageSpanLabel, isFlatRange } from "../lib/heat";
|
||||
import { linkProps } from "../nav";
|
||||
|
||||
/* ---- the project Dashboard: the read×write matrix ----
|
||||
Every file plotted by how much it is read (30 days, from the heat API)
|
||||
@@ -55,6 +56,8 @@ export function Insights(props: {
|
||||
heatMap: HeatMap | null;
|
||||
devices: DeviceHeat[] | null;
|
||||
scope?: string; // "" = whole project; a folder scopes to its subtree, a file to itself
|
||||
loading?: boolean; // tree not in yet — "this project has no files" would be a lie for a frame
|
||||
installHref?: string; // omitted on the project home, which already leads with ConnectGuide
|
||||
onOpenFile: (path: string) => void;
|
||||
onOpenFolder: (path: string) => void;
|
||||
isFolder: (path: string) => boolean;
|
||||
@@ -64,6 +67,36 @@ export function Insights(props: {
|
||||
|
||||
const inScope = (p: string) => !scope || p === scope || p.startsWith(scope + "/");
|
||||
const scoped = scope ? flatFiles.filter((f) => inScope(f.path)) : flatFiles;
|
||||
|
||||
/* Two zero states, only one of them broken. No files at all draws ~840px of
|
||||
empty bordered frames with the quadrant labels — which come from the
|
||||
HOT_READS/STALE_DAYS constants, not from data — floating over nothing.
|
||||
Files-but-no-reads is already right: Treemap pads every file to reads + 1
|
||||
so unread files keep a sliver, and HotPath says so itself. So gate on
|
||||
"no files", never on "no reads". BEA-49 adds orphaned heat rows to the
|
||||
tree; when it lands they join this condition. */
|
||||
if (!props.loading && !scoped.length)
|
||||
return (
|
||||
<div className="insights">
|
||||
<h1 className="in-title">
|
||||
Knowledge insights{scope ? <span className="in-scope"> · {scope}</span> : null}
|
||||
</h1>
|
||||
<div className="dl-empty in-blank">
|
||||
<p>{scope ? `Nothing in ${scope} to chart yet.` : "Nothing to chart yet."}</p>
|
||||
<p>
|
||||
{scope
|
||||
? `No files under ${scope} are syncing here yet.`
|
||||
: "This project has no files. Once a device syncs files here, the map, the reads × freshness plot and the hot path fill in on their own."}
|
||||
</p>
|
||||
{props.installHref && (
|
||||
<a className="pbtn" {...linkProps(props.installHref)}>
|
||||
Set up a device →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const scopedDevices =
|
||||
devices && scope
|
||||
? devices
|
||||
|
||||
@@ -408,7 +408,7 @@ button, input, a.btn { font-family: inherit; }
|
||||
.ob-alt a:hover { color: var(--text); }
|
||||
|
||||
/* primary + secondary buttons */
|
||||
.pbtn { display: inline-flex; align-items: center; gap: 6px; flex: none; height: 32px; padding: 0 14px; border-radius: var(--r-ctl); border: none; background: var(--accent); color: #241704; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; }
|
||||
.pbtn { display: inline-flex; align-items: center; gap: 6px; flex: none; height: 32px; padding: 0 14px; border-radius: var(--r-ctl); border: none; background: var(--accent); color: #241704; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; text-decoration: none; }
|
||||
.pbtn:hover { background: var(--accent-bright); }
|
||||
.pbtn .ico { width: 15px; height: 15px; }
|
||||
.danger-btn { display: inline-flex; align-items: center; height: 32px; padding: 0 14px; border-radius: var(--r-ctl); border: none; background: #b3382e; color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; }
|
||||
@@ -597,6 +597,12 @@ a.ai-main:hover { color: var(--accent); }
|
||||
.gd-head .proj-mark { width: 22px; height: 22px; border-radius: 6px; }
|
||||
.gd-head .proj-mark svg { width: 13px; height: 13px; }
|
||||
.in-desc { color: var(--text-dim); font-size: 13.5px; line-height: 1.55; margin: 0 0 10px; max-width: 62ch; }
|
||||
/* Zero files: one empty state in place of the lens switcher and all three
|
||||
panels, so nothing draws a frame around no data. */
|
||||
.in-blank { display: grid; justify-items: center; gap: 10px; padding: 40px 18px; margin-top: 14px; max-width: 760px; }
|
||||
.in-blank p { margin: 0; max-width: 52ch; line-height: 1.55; }
|
||||
.in-blank p:first-child { color: var(--text); font-size: 14.5px; font-weight: 600; }
|
||||
.in-blank .pbtn { margin-top: 6px; }
|
||||
.in-lens { display: flex; gap: 6px; margin: 0 0 14px; }
|
||||
.in-lens-btn { font: inherit; font-size: 12px; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border); background: none; color: var(--text-faint); cursor: pointer; }
|
||||
.in-lens-btn:hover { color: var(--text); }
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+18
-18
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BearDrive</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
|
||||
<script type="module" crossorigin src="/assets/index-B02ab_uT.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-tRbQn818.css">
|
||||
<script type="module" crossorigin src="/assets/index-D_rNdtD6.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BXvFOzxD.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user