@principal-ai/subsystems-react 0.56.10 → 0.56.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +48 -16
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailStepsNav.d.ts +3 -1
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
- package/dist/subsystem/TrailStepsNav.js +2 -2
- package/dist/subsystem/TrailStepsNav.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts +4 -2
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +2 -2
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/trailSidebarScope.d.ts +11 -0
- package/dist/subsystem/trailSidebarScope.d.ts.map +1 -0
- package/dist/subsystem/trailSidebarScope.js +47 -0
- package/dist/subsystem/trailSidebarScope.js.map +1 -0
- package/package.json +1 -1
- package/src/stories/Canvases/Subsystem/TrailSidebarViews.stories.tsx +68 -0
- package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +37 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +64 -28
- package/src/subsystem/TrailStepsNav.test.tsx +22 -0
- package/src/subsystem/TrailStepsNav.tsx +5 -2
- package/src/subsystem/TrailsPanel.tsx +5 -1
- package/src/subsystem/trailSidebarScope.test.ts +89 -0
- package/src/subsystem/trailSidebarScope.ts +62 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
14
|
-
import type { DragEvent as ReactDragEvent } from 'react';
|
|
14
|
+
import type { DragEvent as ReactDragEvent, ReactNode } from 'react';
|
|
15
15
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
16
16
|
import { Check, ChevronDown, Copy, Focus, GripVertical, Minimize2, Pause, Play, Route, Star } from 'lucide-react';
|
|
17
17
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
@@ -786,6 +786,8 @@ export interface TrailsPanelProps {
|
|
|
786
786
|
componentByAlias?: TrailComponentLabels;
|
|
787
787
|
/** Slide from the trail list into its steps, without playback controls. */
|
|
788
788
|
drilldown?: boolean;
|
|
789
|
+
/** Files or issues for the selected trail, beneath its shared header. */
|
|
790
|
+
detailContent?: ReactNode;
|
|
789
791
|
}
|
|
790
792
|
|
|
791
793
|
/**
|
|
@@ -813,6 +815,7 @@ export function TrailsPanel({
|
|
|
813
815
|
onRemoveStepEmphasis,
|
|
814
816
|
componentByAlias,
|
|
815
817
|
drilldown = false,
|
|
818
|
+
detailContent,
|
|
816
819
|
}: TrailsPanelProps) {
|
|
817
820
|
const { theme } = useTheme();
|
|
818
821
|
// Reordering is offered only while every row is collapsed: the compact list
|
|
@@ -1023,6 +1026,7 @@ export function TrailsPanel({
|
|
|
1023
1026
|
onLeaveStep={onLeaveStep}
|
|
1024
1027
|
componentByAlias={componentByAlias}
|
|
1025
1028
|
listContent={list}
|
|
1029
|
+
detailContent={detailContent}
|
|
1026
1030
|
renderSteps={(trail) => (
|
|
1027
1031
|
<TrailFlow
|
|
1028
1032
|
key={trail.id}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { SubsystemComponent, SubsystemTrail } from './model';
|
|
3
|
+
import type { SubsystemIssue, SubsystemIssueTarget } from './IssueList';
|
|
4
|
+
import { buildTrailSidebarScope } from './trailSidebarScope';
|
|
5
|
+
|
|
6
|
+
const components: SubsystemComponent[] = [
|
|
7
|
+
{ alias: 'entry', name: 'Entry', construct: 'function', file: 'src/entry.ts', purl: 'pkg:github/acme/app', module: 'src', process: 'web' },
|
|
8
|
+
{ alias: 'store', name: 'Store', construct: 'function', file: 'src/store.ts', purl: 'pkg:github/acme/app', module: 'src', process: 'host' },
|
|
9
|
+
{ alias: 'other', name: 'Other', construct: 'function', file: 'src/other.ts', purl: 'pkg:github/acme/other', module: 'other', process: 'worker' },
|
|
10
|
+
];
|
|
11
|
+
const trail: SubsystemTrail = {
|
|
12
|
+
id: 'save', title: 'Save record',
|
|
13
|
+
steps: [{
|
|
14
|
+
from: 'entry', to: 'store', mechanism: 'calls', file: 'src/seam.ts', purl: 'pkg:github/acme/app',
|
|
15
|
+
target: { file: 'src/target.ts', purl: 'pkg:github/acme/shared#src/target.ts', symbol: 'persist' },
|
|
16
|
+
}],
|
|
17
|
+
};
|
|
18
|
+
const finding = (id: string, target?: SubsystemIssueTarget): SubsystemIssue => ({
|
|
19
|
+
id, kind: 'trail', severity: 'error', message: id, target,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('trail sidebar scope', () => {
|
|
23
|
+
test('without a selected trail, keeps the model-wide files and findings', () => {
|
|
24
|
+
const issues = [finding('global')];
|
|
25
|
+
const scope = buildTrailSidebarScope(components, [trail], issues, null);
|
|
26
|
+
expect(scope.trail).toBeNull();
|
|
27
|
+
expect(scope.treePaths.entries.map((entry) => entry.file)).toEqual(components.map((component) => component.file));
|
|
28
|
+
expect(scope.issues).toBe(issues);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('includes endpoint declarations, seam sites, and target files, excluding unrelated files', () => {
|
|
32
|
+
const scope = buildTrailSidebarScope(components, [trail], [], trail.id);
|
|
33
|
+
expect(scope.trail).toBe(trail);
|
|
34
|
+
expect(scope.treePaths.entries.map((entry) => entry.file)).toEqual([
|
|
35
|
+
'src/entry.ts', 'src/store.ts', 'src/seam.ts', 'src/target.ts',
|
|
36
|
+
]);
|
|
37
|
+
expect(scope.treePaths.multiRepo).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('includes only findings for this trail and its participating constructs, boundaries, and repos', () => {
|
|
41
|
+
const relevant = [
|
|
42
|
+
finding('component', { kind: 'component', id: 'entry', label: 'Entry' }),
|
|
43
|
+
finding('legacy-label', { kind: 'component', label: 'Store' }),
|
|
44
|
+
finding('trail', { kind: 'trail', id: 'save', label: 'Save record' }),
|
|
45
|
+
finding('step', { kind: 'step', id: 'save', stepIndex: 0, label: 'Entry → Store' }),
|
|
46
|
+
finding('module', { kind: 'module', id: 'src', label: 'src' }),
|
|
47
|
+
finding('process', { kind: 'process', id: 'host', label: 'Host' }),
|
|
48
|
+
finding('repo', { kind: 'repo', id: 'pkg:github/acme/app#src/entry.ts', label: 'app' }),
|
|
49
|
+
finding('target-repo', { kind: 'repo', id: 'pkg:github/acme/shared', label: 'shared' }),
|
|
50
|
+
];
|
|
51
|
+
const unrelated = [
|
|
52
|
+
finding('other-component', { kind: 'component', id: 'other', label: 'Other' }),
|
|
53
|
+
finding('other-trail', { kind: 'trail', id: 'other', label: 'Other' }),
|
|
54
|
+
finding('other-step', { kind: 'step', id: 'other', stepIndex: 0, label: 'Other step' }),
|
|
55
|
+
finding('invalid-step', { kind: 'step', id: 'save', stepIndex: 10, label: 'Old step' }),
|
|
56
|
+
finding('other-module', { kind: 'module', id: 'other', label: 'other' }),
|
|
57
|
+
finding('other-process', { kind: 'process', id: 'worker', label: 'Worker' }),
|
|
58
|
+
finding('other-repo', { kind: 'repo', id: 'pkg:github/acme/other', label: 'other' }),
|
|
59
|
+
finding('graph', { kind: 'graph', label: 'Whole graph' }),
|
|
60
|
+
finding('untargeted'),
|
|
61
|
+
];
|
|
62
|
+
expect(buildTrailSidebarScope(components, [trail], [...relevant, ...unrelated], trail.id).issues).toEqual(relevant);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('deduplicates repeated sites without merging identical paths from different repos', () => {
|
|
66
|
+
const repeated: SubsystemTrail = {
|
|
67
|
+
...trail, steps: [trail.steps[0]!, trail.steps[0]!, {
|
|
68
|
+
...trail.steps[0]!, purl: 'pkg:github/acme/shared',
|
|
69
|
+
}],
|
|
70
|
+
};
|
|
71
|
+
const paths = buildTrailSidebarScope(components, [repeated], [], trail.id).treePaths;
|
|
72
|
+
expect(paths.entries.filter((entry) => entry.file === 'src/seam.ts')).toHaveLength(2);
|
|
73
|
+
expect(paths.toFile.get('acme/shared/src/seam.ts')).toBe('src/seam.ts');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a removed trail restores model-wide content', () => {
|
|
77
|
+
const scope = buildTrailSidebarScope(components, [], [], trail.id);
|
|
78
|
+
expect(scope.trail).toBeNull();
|
|
79
|
+
expect(scope.treePaths.entries).toHaveLength(3);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('an empty trail has empty files and issues rather than falling back to the full model', () => {
|
|
83
|
+
const empty: SubsystemTrail = { id: 'empty', title: 'Empty', steps: [] };
|
|
84
|
+
const scope = buildTrailSidebarScope(components, [empty], [finding('global')], empty.id);
|
|
85
|
+
expect(scope.trail).toBe(empty);
|
|
86
|
+
expect(scope.treePaths.entries).toHaveLength(0);
|
|
87
|
+
expect(scope.issues).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SubsystemComponent, SubsystemTrail } from './model';
|
|
2
|
+
import type { SubsystemIssue } from './IssueList';
|
|
3
|
+
import { buildTreePathMapping, purlOwnerName, purlRepoKey, type TreePathMapping } from './paths';
|
|
4
|
+
|
|
5
|
+
export interface TrailSidebarScope {
|
|
6
|
+
trail: SubsystemTrail | null;
|
|
7
|
+
treePaths: TreePathMapping;
|
|
8
|
+
issues: SubsystemIssue[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Scope sidebar content to a trail's endpoints and authored source sites. */
|
|
12
|
+
export function buildTrailSidebarScope(
|
|
13
|
+
components: SubsystemComponent[],
|
|
14
|
+
trails: SubsystemTrail[] | undefined,
|
|
15
|
+
issues: SubsystemIssue[] | undefined,
|
|
16
|
+
trailId: string | null,
|
|
17
|
+
): TrailSidebarScope {
|
|
18
|
+
const trail = trails?.find((item) => item.id === trailId) ?? null;
|
|
19
|
+
if (!trail) {
|
|
20
|
+
return { trail: null, treePaths: buildTreePathMapping(components), issues: issues ?? [] };
|
|
21
|
+
}
|
|
22
|
+
const aliases = new Set(trail.steps.flatMap((step) => [step.from, step.to]));
|
|
23
|
+
const participants = components.filter((component) => aliases.has(component.alias));
|
|
24
|
+
const files: Array<{ file?: string; purl?: string }> = [...participants];
|
|
25
|
+
for (const step of trail.steps) {
|
|
26
|
+
files.push(step);
|
|
27
|
+
if (step.target) files.push(step.target);
|
|
28
|
+
}
|
|
29
|
+
const modules = new Set(participants.map((component) => component.module?.trim()).filter(Boolean));
|
|
30
|
+
const processes = new Set(participants.map((component) => component.process).filter(Boolean));
|
|
31
|
+
const repos = new Set(files.flatMap((file) =>
|
|
32
|
+
[purlRepoKey(file.purl), purlOwnerName(file.purl)].filter((key): key is string => !!key),
|
|
33
|
+
));
|
|
34
|
+
const scopedIssues = (issues ?? []).filter((issue) => {
|
|
35
|
+
const target = issue.target;
|
|
36
|
+
if (!target) return false;
|
|
37
|
+
const key = target.id ?? target.label;
|
|
38
|
+
switch (target.kind) {
|
|
39
|
+
case 'component': {
|
|
40
|
+
const component = components.find((candidate) =>
|
|
41
|
+
candidate.alias === target.id || candidate.alias === target.label ||
|
|
42
|
+
candidate.name === target.label || candidate.symbol === target.label,
|
|
43
|
+
);
|
|
44
|
+
return component != null && aliases.has(component.alias);
|
|
45
|
+
}
|
|
46
|
+
case 'trail':
|
|
47
|
+
return target.id != null ? target.id === trail.id : target.label === trail.title;
|
|
48
|
+
case 'step':
|
|
49
|
+
return target.id === trail.id && target.stepIndex != null &&
|
|
50
|
+
Number.isInteger(target.stepIndex) && trail.steps[target.stepIndex] != null;
|
|
51
|
+
case 'module':
|
|
52
|
+
return modules.has(key);
|
|
53
|
+
case 'process':
|
|
54
|
+
return processes.has(key);
|
|
55
|
+
case 'repo':
|
|
56
|
+
return repos.has(purlRepoKey(key) ?? key);
|
|
57
|
+
default:
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return { trail, treePaths: buildTreePathMapping(files), issues: scopedIssues };
|
|
62
|
+
}
|