diff --git a/src/components/EntityDetailPanel.test.tsx b/src/components/EntityDetailPanel.test.tsx
new file mode 100644
index 0000000..b1993a4
--- /dev/null
+++ b/src/components/EntityDetailPanel.test.tsx
@@ -0,0 +1,103 @@
+// Copyright 2026 bburda
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import { EntityDetailPanel } from './EntityDetailPanel';
+
+// Mock heavy child components - we only care about the top-level routing
+// (header + tab bar + tab content selection) for these tests.
+vi.mock('@/components/DataPanel', () => ({ DataPanel: () =>
}));
+vi.mock('@/components/ConfigurationPanel', () => ({ ConfigurationPanel: () => }));
+vi.mock('@/components/OperationsPanel', () => ({ OperationsPanel: () => }));
+vi.mock('@/components/AreasPanel', () => ({ AreasPanel: () => }));
+vi.mock('@/components/AppsPanel', () => ({ AppsPanel: () => }));
+vi.mock('@/components/FunctionsPanel', () => ({ FunctionsPanel: () => }));
+vi.mock('@/components/ServerInfoPanel', () => ({ ServerInfoPanel: () => }));
+vi.mock('@/components/FaultsDashboard', () => ({ FaultsDashboard: () => }));
+vi.mock('@/components/UpdatesDashboard', () => ({ UpdatesDashboard: () => }));
+vi.mock('@/components/EmptyState', () => ({ EmptyState: () => }));
+vi.mock('@/components/EntityDetailSkeleton', () => ({ EntityDetailSkeleton: () => }));
+vi.mock('@/components/ResourceTabs', async () => {
+ const actual = await vi.importActual('@/components/ResourceTabs');
+ return {
+ ...actual,
+ renderResourceTabContent: (tab: string) => ,
+ };
+});
+
+const mockPrefetchResourceCounts = vi.fn();
+const mockFetchEntityData = vi.fn();
+const mockSelectEntity = vi.fn();
+const mockRefreshSelectedEntity = vi.fn();
+
+let storeState: Record = {};
+
+vi.mock('@/lib/store', () => ({
+ useAppStore: vi.fn((selector: (s: Record) => unknown) => selector(storeState)),
+}));
+
+function setStore(overrides: Record) {
+ storeState = {
+ selectedPath: null,
+ selectedEntity: null,
+ isLoadingDetails: false,
+ isRefreshing: false,
+ isConnected: true,
+ selectEntity: mockSelectEntity,
+ refreshSelectedEntity: mockRefreshSelectedEntity,
+ prefetchResourceCounts: mockPrefetchResourceCounts,
+ fetchEntityData: mockFetchEntityData,
+ ...overrides,
+ };
+}
+
+describe('EntityDetailPanel - subcomponent entity type', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockPrefetchResourceCounts.mockResolvedValue({ data: 0, operations: 0, configurations: 0, faults: 0, logs: 0 });
+ mockFetchEntityData.mockResolvedValue([]);
+ });
+
+ it('renders resource tabs and fetches counts as components for subcomponent entity', async () => {
+ setStore({
+ selectedPath: '/server/area1/component1/planning-ecu',
+ selectedEntity: {
+ id: 'planning-ecu',
+ name: 'planning-ecu',
+ type: 'subcomponent',
+ },
+ });
+
+ render( {}} />);
+
+ // Bug repro: subcomponent should fetch resource counts using the
+ // 'components' entity type (gateway routes subcomponents through
+ // /api/v1/components/{id}/...).
+ await waitFor(() => {
+ expect(mockPrefetchResourceCounts).toHaveBeenCalledWith('components', 'planning-ecu', expect.anything());
+ });
+
+ // Bug repro: subcomponent should render the resource tab bar
+ // (Data / Operations / Config / Faults / Logs) just like a component.
+ expect(screen.getByRole('button', { name: /Data/ })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Operations/ })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Config/ })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Faults/ })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Logs/ })).toBeInTheDocument();
+
+ // The fallback "No detailed information available" must not appear.
+ expect(screen.queryByText(/No detailed information available/i)).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/EntityDetailPanel.tsx b/src/components/EntityDetailPanel.tsx
index 12baed2..fc4a74b 100644
--- a/src/components/EntityDetailPanel.tsx
+++ b/src/components/EntityDetailPanel.tsx
@@ -53,6 +53,7 @@ function getEntityTypeForApi(entityType: string | undefined): SovdResourceEntity
case 'app':
return 'apps';
case 'component':
+ case 'subcomponent':
return 'components';
case 'function':
return 'functions';
@@ -444,7 +445,7 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
}
const entityId = selectedEntity.id;
- const isComponent = selectedEntity.type === 'component';
+ const isComponent = selectedEntity.type === 'component' || selectedEntity.type === 'subcomponent';
const isApp = selectedEntity.type === 'app';
const isArea = selectedEntity.type === 'area';
const isFunction = selectedEntity.type === 'function';
@@ -554,7 +555,7 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
// Entity detail view
if (selectedEntity) {
const isTopic = selectedEntity.type === 'topic';
- const isComponent = selectedEntity.type === 'component';
+ const isComponent = selectedEntity.type === 'component' || selectedEntity.type === 'subcomponent';
const isArea = selectedEntity.type === 'area';
const isApp = selectedEntity.type === 'app';
const isFunction = selectedEntity.type === 'function';
@@ -628,6 +629,7 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
case 'area':
return ;
case 'component':
+ case 'subcomponent':
return ;
case 'app':
return ;
@@ -646,6 +648,7 @@ export function EntityDetailPanel({ onConnectClick, viewMode = 'entity', onEntit
case 'area':
return 'bg-cyan-100 dark:bg-cyan-900';
case 'component':
+ case 'subcomponent':
return 'bg-indigo-100 dark:bg-indigo-900';
case 'app':
return 'bg-emerald-100 dark:bg-emerald-900';
diff --git a/src/lib/store.ts b/src/lib/store.ts
index 37fcd0a..3cfef93 100644
--- a/src/lib/store.ts
+++ b/src/lib/store.ts
@@ -1326,6 +1326,7 @@ export const useAppStore = create()(
const typeMap: Record = {
area: 'areas',
component: 'components',
+ subcomponent: 'components',
app: 'apps',
function: 'functions',
};