Skip to content

Commit 286f2bf

Browse files
committed
feat: unify resource tab bar across all entity types + review fixes
Unification: - New resource-tabs.tsx module exports RESOURCE_TABS config and renderResourceTabContent helper as the single source of truth for Data / Operations / Configurations / Faults / Logs tabs. - AppsPanel, FunctionsPanel, and EntityDetailPanel component view merge RESOURCE_TABS into their flat tab bars so the Logs tab now appears on all four entity types, not just Areas. LogsPanel polish: - Table caps at 60vh with a scrollable body and sticky column header so the page no longer grows unboundedly. - 404 responses render the same "Logs not available" state as 503, distinguished by message (entity-specific vs gateway-wide). - Windows-safe download filename (colons/dots replaced with hyphens). - Config Save button disabled until the initial GET succeeds; a "Failed to load configuration" state with Retry replaces the silent default-form bug where the user could overwrite server config with defaults. - Row expansion is keyboard-accessible (role, tabIndex, onKeyDown, aria-expanded). - Config-row state resets on entity change so navigating between entities no longer leaks cached config. Tests: +5 new LogsPanel tests covering the review findings.
1 parent 0733942 commit 286f2bf

7 files changed

Lines changed: 412 additions & 194 deletions

File tree

src/components/AppsPanel.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,27 @@
11
import { useState, useEffect } from 'react';
22
import { useShallow } from 'zustand/shallow';
3-
import { Cpu, Database, Zap, Settings, AlertTriangle, ChevronRight, Box, Network, FileCode } from 'lucide-react';
3+
import { AlertTriangle, Box, ChevronRight, Cpu, Database, FileCode, Network, Settings, Zap } from 'lucide-react';
44
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
55
import { Badge } from '@/components/ui/badge';
66
import { Button } from '@/components/ui/button';
77
import { useAppStore } from '@/lib/store';
8-
import { ConfigurationPanel } from '@/components/ConfigurationPanel';
9-
import { FaultsPanel } from '@/components/FaultsPanel';
10-
import { OperationsPanel } from '@/components/OperationsPanel';
8+
import {
9+
RESOURCE_TABS,
10+
renderResourceTabContent,
11+
isResourceTabId,
12+
type ResourceTabId,
13+
} from '@/components/resource-tabs';
1114
import type { ComponentTopic, Operation, Fault } from '@/lib/types';
1215

13-
type AppTab = 'overview' | 'data' | 'operations' | 'configurations' | 'faults';
16+
type AppTab = 'overview' | ResourceTabId;
1417

1518
interface TabConfig {
1619
id: AppTab;
1720
label: string;
1821
icon: typeof Database;
1922
}
2023

21-
const APP_TABS: TabConfig[] = [
22-
{ id: 'overview', label: 'Overview', icon: Cpu },
23-
{ id: 'data', label: 'Data', icon: Database },
24-
{ id: 'operations', label: 'Operations', icon: Zap },
25-
{ id: 'configurations', label: 'Config', icon: Settings },
26-
{ id: 'faults', label: 'Faults', icon: AlertTriangle },
27-
];
24+
const APP_TABS: TabConfig[] = [{ id: 'overview', label: 'Overview', icon: Cpu }, ...RESOURCE_TABS];
2825

2926
interface AppsPanelProps {
3027
appId: string;
@@ -313,11 +310,11 @@ export function AppsPanel({ appId, appName, fqn, nodeName, namespace, componentI
313310
</Card>
314311
)}
315312

316-
{activeTab === 'operations' && <OperationsPanel entityId={appId} entityType="apps" />}
317-
318-
{activeTab === 'configurations' && <ConfigurationPanel entityId={appId} entityType="apps" />}
319-
320-
{activeTab === 'faults' && <FaultsPanel entityId={appId} entityType="apps" />}
313+
{/* Operations / Configurations / Faults / Logs delegated to the shared helper */}
314+
{activeTab !== 'overview' &&
315+
activeTab !== 'data' &&
316+
isResourceTabId(activeTab) &&
317+
renderResourceTabContent(activeTab, appId, 'apps')}
321318

322319
{isLoading && <div className="text-center text-muted-foreground py-4">Loading app resources...</div>}
323320
</div>

src/components/EntityDetailPanel.tsx

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@ import {
77
ArrowUp,
88
ArrowDown,
99
Database,
10-
Zap,
1110
Settings,
1211
RefreshCw,
1312
Box,
1413
Layers,
1514
Cpu,
1615
GitBranch,
1716
Home,
18-
AlertTriangle,
1917
Server,
2018
} from 'lucide-react';
2119
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
@@ -26,7 +24,7 @@ import { EntityDetailSkeleton } from '@/components/EntityDetailSkeleton';
2624
import { DataPanel } from '@/components/DataPanel';
2725
import { ConfigurationPanel } from '@/components/ConfigurationPanel';
2826
import { OperationsPanel } from '@/components/OperationsPanel';
29-
import { FaultsPanel } from '@/components/FaultsPanel';
27+
import { RESOURCE_TABS, renderResourceTabContent, type ResourceTabId } from '@/components/resource-tabs';
3028
import { AreasPanel } from '@/components/AreasPanel';
3129
import { AppsPanel } from '@/components/AppsPanel';
3230
import { FunctionsPanel } from '@/components/FunctionsPanel';
@@ -35,21 +33,16 @@ import { FaultsDashboard } from '@/components/FaultsDashboard';
3533
import { useAppStore, type AppState } from '@/lib/store';
3634
import type { ComponentTopic, Parameter, SovdResourceEntityType } from '@/lib/types';
3735

38-
type ComponentTab = 'data' | 'operations' | 'configurations' | 'faults';
36+
type ComponentTab = ResourceTabId;
3937

4038
interface TabConfig {
4139
id: ComponentTab;
4240
label: string;
4341
icon: typeof Database;
44-
description: string;
42+
description?: string;
4543
}
4644

47-
const COMPONENT_TABS: TabConfig[] = [
48-
{ id: 'data', label: 'Data', icon: Database, description: 'Data items & messages' },
49-
{ id: 'operations', label: 'Operations', icon: Zap, description: 'Services & actions' },
50-
{ id: 'configurations', label: 'Config', icon: Settings, description: 'Parameters' },
51-
{ id: 'faults', label: 'Faults', icon: AlertTriangle, description: 'Diagnostic trouble codes' },
52-
];
45+
const COMPONENT_TABS: TabConfig[] = RESOURCE_TABS;
5346

5447
/**
5548
* Determine entity type for API calls based on entity type
@@ -113,26 +106,18 @@ function ComponentTabContent({
113106
entityType,
114107
topicsData,
115108
}: ComponentTabContentProps) {
116-
switch (activeTab) {
117-
case 'data':
118-
return (
119-
<DataTabContent
120-
selectedPath={selectedPath}
121-
selectedEntity={selectedEntity}
122-
hasTopicsInfo={hasTopicsInfo}
123-
selectEntity={selectEntity}
124-
topicsData={topicsData}
125-
/>
126-
);
127-
case 'operations':
128-
return <OperationsPanel entityId={entityId} entityType={entityType} />;
129-
case 'configurations':
130-
return <ConfigurationPanel entityId={entityId} entityType={entityType} />;
131-
case 'faults':
132-
return <FaultsPanel entityId={entityId} entityType={entityType} />;
133-
default:
134-
return null;
109+
if (activeTab === 'data') {
110+
return (
111+
<DataTabContent
112+
selectedPath={selectedPath}
113+
selectedEntity={selectedEntity}
114+
hasTopicsInfo={hasTopicsInfo}
115+
selectEntity={selectEntity}
116+
topicsData={topicsData}
117+
/>
118+
);
135119
}
120+
return <>{renderResourceTabContent(activeTab, entityId, entityType)}</>;
136121
}
137122

138123
/**
@@ -351,12 +336,13 @@ interface EntityDetailPanelProps {
351336

352337
export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntitySelect }: EntityDetailPanelProps) {
353338
const [activeTab, setActiveTab] = useState<ComponentTab>('data');
354-
const [resourceCounts, setResourceCounts] = useState<{
355-
data: number;
356-
operations: number;
357-
configurations: number;
358-
faults: number;
359-
}>({ data: 0, operations: 0, configurations: 0, faults: 0 });
339+
const [resourceCounts, setResourceCounts] = useState<Record<ResourceTabId, number>>({
340+
data: 0,
341+
operations: 0,
342+
configurations: 0,
343+
faults: 0,
344+
logs: 0,
345+
});
360346
// Store fetched topics data for the Data tab
361347
const [topicsData, setTopicsData] = useState<ComponentTopic[]>([]);
362348

@@ -393,9 +379,16 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
393379

394380
// Fetch resource counts when entity changes
395381
useEffect(() => {
382+
const emptyCounts: Record<ResourceTabId, number> = {
383+
data: 0,
384+
operations: 0,
385+
configurations: 0,
386+
faults: 0,
387+
logs: 0,
388+
};
396389
const doFetchResourceCounts = async () => {
397390
if (!selectedEntity) {
398-
setResourceCounts({ data: 0, operations: 0, configurations: 0, faults: 0 });
391+
setResourceCounts(emptyCounts);
399392
setTopicsData([]);
400393
return;
401394
}
@@ -408,7 +401,7 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
408401

409402
// Only fetch counts for entity types that have resources
410403
if (!isComponent && !isApp && !isArea && !isFunction) {
411-
setResourceCounts({ data: 0, operations: 0, configurations: 0, faults: 0 });
404+
setResourceCounts(emptyCounts);
412405
setTopicsData([]);
413406
return;
414407
}
@@ -431,7 +424,7 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
431424
setTopicsData(fetchedData);
432425

433426
// Use the already-fetched data length instead of a separate request
434-
setResourceCounts({ ...counts, data: fetchedData.length });
427+
setResourceCounts({ ...counts, data: fetchedData.length, logs: 0 });
435428
} catch {
436429
// Silently handle errors - counts will stay at 0
437430
}

src/components/EntityResourceTabs.tsx

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,13 @@
11
import { useState, useEffect, useCallback, useRef } from 'react';
22
import { useShallow } from 'zustand/shallow';
3-
import { Database, Zap, Settings, AlertTriangle, Loader2, MessageSquare, ScrollText } from 'lucide-react';
3+
import { Database, Loader2, MessageSquare } from 'lucide-react';
44
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
55
import { Badge } from '@/components/ui/badge';
66
import { useAppStore } from '@/lib/store';
7-
import { ConfigurationPanel } from '@/components/ConfigurationPanel';
8-
import { OperationsPanel } from '@/components/OperationsPanel';
9-
import { FaultsPanel } from '@/components/FaultsPanel';
10-
import { LogsPanel } from '@/components/LogsPanel';
7+
import { RESOURCE_TABS, renderResourceTabContent, type ResourceTabId } from '@/components/resource-tabs';
118
import type { SovdResourceEntityType } from '@/lib/types';
129
import type { ComponentTopic, Operation, Fault } from '@/lib/types';
1310

14-
type ResourceTab = 'data' | 'operations' | 'configurations' | 'faults' | 'logs';
15-
16-
interface TabConfig {
17-
id: ResourceTab;
18-
label: string;
19-
icon: typeof Database;
20-
}
21-
22-
const RESOURCE_TABS: TabConfig[] = [
23-
{ id: 'data', label: 'Data', icon: Database },
24-
{ id: 'operations', label: 'Operations', icon: Zap },
25-
{ id: 'configurations', label: 'Config', icon: Settings },
26-
{ id: 'faults', label: 'Faults', icon: AlertTriangle },
27-
{ id: 'logs', label: 'Logs', icon: ScrollText },
28-
];
29-
3011
interface EntityResourceTabsProps {
3112
entityId: string;
3213
entityType: SovdResourceEntityType;
@@ -51,7 +32,7 @@ interface LoadedResources {
5132
* Resources are lazy-loaded per tab to avoid unnecessary API calls.
5233
*/
5334
export function EntityResourceTabs({ entityId, entityType, basePath, onNavigate }: EntityResourceTabsProps) {
54-
const [activeTab, setActiveTab] = useState<ResourceTab>('data');
35+
const [activeTab, setActiveTab] = useState<ResourceTabId>('data');
5536
const [isLoading, setIsLoading] = useState(false);
5637
const [loadedTabs, setLoadedTabs] = useState<LoadedResources>({
5738
data: false,
@@ -86,7 +67,7 @@ export function EntityResourceTabs({ entityId, entityType, basePath, onNavigate
8667

8768
// Lazy load resources for the active tab
8869
const loadTabResources = useCallback(
89-
async (tab: ResourceTab) => {
70+
async (tab: ResourceTabId) => {
9071
if (loadedTabsRef.current[tab]) return;
9172

9273
setIsLoading(true);
@@ -235,19 +216,8 @@ export function EntityResourceTabs({ entityId, entityType, basePath, onNavigate
235216
</Card>
236217
)}
237218

238-
{/* Operations Tab */}
239-
{activeTab === 'operations' && <OperationsPanel entityId={entityId} entityType={entityType} />}
240-
241-
{/* Configurations Tab */}
242-
{activeTab === 'configurations' && (
243-
<ConfigurationPanel entityId={entityId} entityType={entityType} />
244-
)}
245-
246-
{/* Faults Tab */}
247-
{activeTab === 'faults' && <FaultsPanel entityId={entityId} entityType={entityType} />}
248-
249-
{/* Logs Tab */}
250-
{activeTab === 'logs' && <LogsPanel entityId={entityId} entityType={entityType} />}
219+
{/* Operations / Configurations / Faults / Logs delegated to shared helper */}
220+
{activeTab !== 'data' && renderResourceTabContent(activeTab, entityId, entityType)}
251221
</>
252222
)}
253223
</div>

src/components/FunctionsPanel.tsx

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import { useState, useEffect } from 'react';
22
import { useShallow } from 'zustand/shallow';
33
import {
4-
GitBranch,
4+
AlertTriangle,
5+
ChevronRight,
56
Cpu,
67
Database,
7-
Zap,
8-
ChevronRight,
9-
Users,
8+
GitBranch,
109
Info,
11-
Settings,
12-
AlertTriangle,
1310
Loader2,
11+
Settings,
12+
Users,
13+
Zap,
1414
} from 'lucide-react';
1515
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
1616
import { Badge } from '@/components/ui/badge';
1717
import { useAppStore } from '@/lib/store';
18-
import { ConfigurationPanel } from '@/components/ConfigurationPanel';
19-
import { OperationsPanel } from '@/components/OperationsPanel';
20-
import { FaultsPanel } from '@/components/FaultsPanel';
18+
import {
19+
RESOURCE_TABS,
20+
renderResourceTabContent,
21+
isResourceTabId,
22+
type ResourceTabId,
23+
} from '@/components/resource-tabs';
2124
import type { ComponentTopic, Operation, Fault } from '@/lib/types';
2225

2326
/** Host app object returned from /functions/{id}/hosts */
@@ -27,7 +30,7 @@ interface FunctionHost {
2730
href: string;
2831
}
2932

30-
type FunctionTab = 'overview' | 'hosts' | 'data' | 'operations' | 'configurations' | 'faults';
33+
type FunctionTab = 'overview' | 'hosts' | ResourceTabId;
3134

3235
interface TabConfig {
3336
id: FunctionTab;
@@ -38,10 +41,7 @@ interface TabConfig {
3841
const FUNCTION_TABS: TabConfig[] = [
3942
{ id: 'overview', label: 'Overview', icon: Info },
4043
{ id: 'hosts', label: 'Hosts', icon: Cpu },
41-
{ id: 'data', label: 'Data', icon: Database },
42-
{ id: 'operations', label: 'Operations', icon: Zap },
43-
{ id: 'configurations', label: 'Config', icon: Settings },
44-
{ id: 'faults', label: 'Faults', icon: AlertTriangle },
44+
...RESOURCE_TABS,
4545
];
4646

4747
interface FunctionsPanelProps {
@@ -357,11 +357,12 @@ export function FunctionsPanel({ functionId, functionName, description, path, on
357357
</Card>
358358
)}
359359

360-
{activeTab === 'operations' && <OperationsPanel entityId={functionId} entityType="functions" />}
361-
362-
{activeTab === 'configurations' && <ConfigurationPanel entityId={functionId} entityType="functions" />}
363-
364-
{activeTab === 'faults' && <FaultsPanel entityId={functionId} entityType="functions" />}
360+
{/* Operations / Configurations / Faults / Logs delegated to the shared helper */}
361+
{activeTab !== 'overview' &&
362+
activeTab !== 'hosts' &&
363+
activeTab !== 'data' &&
364+
isResourceTabId(activeTab) &&
365+
renderResourceTabContent(activeTab, functionId, 'functions')}
365366

366367
{isLoading && (
367368
<Card>

0 commit comments

Comments
 (0)