Skip to content

Commit 091814e

Browse files
committed
fix: distinguish loading from empty in Data tab to avoid empty-state flash
Before: the Data tab panel read `topicsData` as `ComponentTopic[]` seeded with `[]`. When the user switched entities the parent cleared the array and then started a fetch; the child could render a full "no data" panel for the new entity before the fetch resolved, regardless of what the new entity actually had. Now: `topicsData` is `ComponentTopic[] | null`. The parent resets it to `null` at the start of every fetch (including the unsupported-type branches and on error), and sets it to an array once the response is in. The Data tab renders a skeleton for `null`, the normal list for a non-empty array, and the empty/fallback state for `[]`. This removes the need for a second in-flight request from the child component and gives the fetch effect a single source of truth.
1 parent 9f862a5 commit 091814e

1 file changed

Lines changed: 48 additions & 9 deletions

File tree

src/components/EntityDetailPanel.tsx

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ interface ComponentTabContentProps {
9494
hasTopicsInfo: boolean;
9595
selectEntity: (path: string) => void;
9696
entityType: SovdResourceEntityType;
97-
topicsData: ComponentTopic[];
97+
topicsData: ComponentTopic[] | null;
9898
}
9999

100100
function ComponentTabContent({
@@ -122,14 +122,22 @@ function ComponentTabContent({
122122
}
123123

124124
/**
125-
* Data tab content - shows data items
125+
* Data tab content - shows data items.
126+
*
127+
* `topicsData` uses a three-state convention:
128+
* - `null` -> parent fetch is still in flight, render a skeleton
129+
* - `[]` -> fetch completed with no items, fall through to topicsInfo / empty state
130+
* - `[...]` -> render the list
131+
*
132+
* This lets the tab distinguish "loading" from "loaded empty" without a
133+
* second self-fetch or a duplicate network request.
126134
*/
127135
interface DataTabContentProps {
128136
selectedPath: string;
129137
selectedEntity: NonNullable<AppState['selectedEntity']>;
130138
hasTopicsInfo: boolean;
131139
selectEntity: (path: string) => void;
132-
topicsData: ComponentTopic[];
140+
topicsData: ComponentTopic[] | null;
133141
}
134142

135143
function DataTabContent({
@@ -139,9 +147,30 @@ function DataTabContent({
139147
selectEntity,
140148
topicsData,
141149
}: DataTabContentProps) {
142-
// Use topicsData from props (fetched via API), or fall back to selectedEntity.topics
143-
const topics = topicsData.length > 0 ? topicsData : (selectedEntity.topics as ComponentTopic[] | undefined);
144-
const hasTopics = topics && topics.length > 0;
150+
if (topicsData === null) {
151+
return (
152+
<Card>
153+
<CardHeader className="pb-3">
154+
<div className="flex items-center gap-2">
155+
<Database className="w-5 h-5 text-muted-foreground" />
156+
<CardTitle className="text-base">Data</CardTitle>
157+
</div>
158+
</CardHeader>
159+
<CardContent>
160+
<div className="grid gap-3 md:grid-cols-2">
161+
{[0, 1, 2, 3].map((i) => (
162+
<div key={i} className="h-14 rounded-lg border bg-muted/30 animate-pulse" />
163+
))}
164+
</div>
165+
</CardContent>
166+
</Card>
167+
);
168+
}
169+
170+
// topicsData is loaded at this point. Prefer it over the entity-level fallback
171+
// so we never surface stale `selectedEntity.topics` when the fresh fetch is empty.
172+
const topics = topicsData.length > 0 ? topicsData : (selectedEntity.topics as ComponentTopic[] | undefined) || [];
173+
const hasTopics = topics.length > 0;
145174

146175
if (hasTopics) {
147176
return (
@@ -344,8 +373,11 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
344373
faults: 0,
345374
logs: 0,
346375
});
347-
// Store fetched topics data for the Data tab
348-
const [topicsData, setTopicsData] = useState<ComponentTopic[]>([]);
376+
// Store fetched topics data for the Data tab. `null` means "not yet loaded
377+
// for the current entity" so the Data tab can render a skeleton instead of
378+
// an empty-state flash while the fetch is in flight. `[]` means "loaded,
379+
// no items". `[...]` means "loaded with items".
380+
const [topicsData, setTopicsData] = useState<ComponentTopic[] | null>(null);
349381

350382
const {
351383
selectedPath,
@@ -394,6 +426,11 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
394426
logs: 0,
395427
};
396428
const doFetchResourceCounts = async () => {
429+
// Mark topicsData as "not loaded yet for the current entity" so the
430+
// Data tab renders a skeleton instead of an empty-state flash while
431+
// the fetch is in flight. Any previous entity's data is discarded.
432+
setTopicsData(null);
433+
397434
if (!selectedEntity) {
398435
setResourceCounts(emptyCounts);
399436
setTopicsData([]);
@@ -433,7 +470,9 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
433470
// Use the already-fetched data length instead of a separate request
434471
setResourceCounts({ ...counts, data: fetchedData.length, logs: 0 });
435472
} catch {
436-
// Silently handle errors - counts will stay at 0
473+
// On unexpected failure fall back to "loaded empty" so the UI
474+
// doesn't get stuck showing the skeleton forever.
475+
setTopicsData([]);
437476
}
438477
};
439478

0 commit comments

Comments
 (0)