Skip to content

Commit cfa8a54

Browse files
committed
fix: also match top-level component_id and gate fallback on peer source
- filterAppsByComponent now also matches `app.component_id` directly (not only `x-medkit.component_id` and `_links.is-located-on`) so the helper remains robust if future transforms lift component_id to the top level. - Add isPeerSourcedComponent helper and gate the /apps fallback on it, so legitimate empty manifest/plugin components do not trigger a full app-list fetch on every expand. Components with unknown source metadata still trigger the fallback to err on the side of completeness.
1 parent 29e4bc8 commit cfa8a54

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

src/lib/store-helpers.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
inferEntityTypeFromDepth,
2121
parseTreePath,
2222
filterAppsByComponent,
23+
isPeerSourcedComponent,
2324
} from './store';
2425
import type { SovdEntity, EntityTreeNode } from './types';
2526

@@ -425,10 +426,16 @@ describe('filterAppsByComponent', () => {
425426
{ id: 'local-app', 'x-medkit': { component_id: 'ecu-primary' } },
426427
{ id: 'data_viewer', 'x-medkit': { component_id: 'ecu-rtmaps' } },
427428
{ id: 'lidar_sim', _links: { 'is-located-on': '/api/v1/components/ecu-rtmaps' } },
429+
{ id: 'top-level-app', component_id: 'ecu-tesla' },
428430
{ id: 'orphan-app', 'x-medkit': {} },
429431
{ id: 'bare-app' },
430432
];
431433

434+
it('matches apps by top-level component_id', () => {
435+
const result = filterAppsByComponent(apps, 'ecu-tesla');
436+
expect(result.map((a) => a.id)).toEqual(['top-level-app']);
437+
});
438+
432439
it('matches apps by x-medkit.component_id', () => {
433440
const result = filterAppsByComponent(apps, 'ecu-primary');
434441
expect(result.map((a) => a.id)).toEqual(['local-app']);
@@ -457,3 +464,26 @@ describe('filterAppsByComponent', () => {
457464
expect(result).toEqual([]);
458465
});
459466
});
467+
468+
// =============================================================================
469+
// isPeerSourcedComponent
470+
// =============================================================================
471+
472+
describe('isPeerSourcedComponent', () => {
473+
it('returns true for peer-sourced components', () => {
474+
expect(isPeerSourcedComponent({ 'x-medkit': { source: 'peer:ecu-rtmaps' } })).toBe(true);
475+
});
476+
477+
it('returns false for manifest-sourced components', () => {
478+
expect(isPeerSourcedComponent({ 'x-medkit': { source: 'manifest' } })).toBe(false);
479+
});
480+
481+
it('returns false for plugin-sourced components', () => {
482+
expect(isPeerSourcedComponent({ 'x-medkit': { source: 'plugin' } })).toBe(false);
483+
});
484+
485+
it('returns true when source metadata is missing (err on the side of completeness)', () => {
486+
expect(isPeerSourcedComponent({})).toBe(true);
487+
expect(isPeerSourcedComponent({ 'x-medkit': {} })).toBe(true);
488+
});
489+
});

src/lib/store.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,20 +615,38 @@ export function parseTreePath(path: string): {
615615

616616
/**
617617
* Filter apps that belong to a given component by checking
618-
* `x-medkit.component_id` or `_links.is-located-on`.
618+
* the top-level `component_id` field, `x-medkit.component_id`,
619+
* or `_links.is-located-on`.
619620
*
620621
* Used as a fallback when `/components/{id}/hosts` returns empty
621622
* (peer-sourced components).
622623
*/
623624
export function filterAppsByComponent(apps: Record<string, unknown>[], componentId: string): Record<string, unknown>[] {
624625
return apps.filter((app) => {
626+
if (app['component_id'] === componentId) return true;
625627
const xMedkit = app['x-medkit'] as Record<string, unknown> | undefined;
626628
if (xMedkit?.component_id === componentId) return true;
627629
const links = app['_links'] as Record<string, string> | undefined;
628630
return links?.['is-located-on']?.endsWith(`/components/${componentId}`);
629631
});
630632
}
631633

634+
/**
635+
* Detect whether a component node is peer-sourced (aggregated from a
636+
* remote gateway). Peer-sourced components have `x-medkit.source`
637+
* starting with `"peer:"`.
638+
*
639+
* Components with unknown source metadata are treated as peer-sourced
640+
* so the fallback still discovers their apps; this errs on the side of
641+
* completeness over saving one extra request.
642+
*/
643+
export function isPeerSourcedComponent(node: Record<string, unknown>): boolean {
644+
const xMedkit = node['x-medkit'] as Record<string, unknown> | undefined;
645+
const source = xMedkit?.source;
646+
if (typeof source !== 'string') return true;
647+
return source.startsWith('peer:');
648+
}
649+
632650
/** Fallback: fetch entity details from API when not in tree */
633651
async function fetchEntityFromApi(
634652
path: string,
@@ -1016,7 +1034,13 @@ export const useAppStore = create<AppState>()(
10161034
// Fallback for peer-sourced components: /hosts may return
10171035
// empty while apps still reference this component via
10181036
// x-medkit.component_id. Fetch all apps and filter.
1019-
if (rawApps.length === 0) {
1037+
// Gated on isPeerSourcedComponent so we don't pay the
1038+
// full /apps fetch for components that legitimately
1039+
// have no apps (e.g. manifest or plugin components).
1040+
if (
1041+
rawApps.length === 0 &&
1042+
isPeerSourcedComponent(node as unknown as Record<string, unknown>)
1043+
) {
10201044
const allAppsRes = await client.GET('/apps').catch(() => ({ data: undefined }));
10211045
const allApps = allAppsRes.data
10221046
? unwrapItems<Record<string, unknown>>(allAppsRes.data)

0 commit comments

Comments
 (0)