@principal-ai/subsystems-react 0.56.10 → 0.56.12
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/DiagnosticToggle.d.ts.map +1 -1
- package/dist/subsystem/DiagnosticToggle.js +2 -1
- package/dist/subsystem/DiagnosticToggle.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +64 -27
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -1
- package/dist/subsystem/SubsystemFileTree.js +2 -1
- package/dist/subsystem/SubsystemFileTree.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 +13 -10
- package/dist/subsystem/TrailStepsNav.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts +5 -3
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +44 -68
- 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/DiagnosticToggle.tsx +2 -1
- package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +106 -5
- package/src/subsystem/SubsystemComponentGraph.tsx +87 -41
- package/src/subsystem/SubsystemFileTree.tsx +2 -1
- package/src/subsystem/TrailStepsNav.test.tsx +60 -0
- package/src/subsystem/TrailStepsNav.tsx +16 -10
- package/src/subsystem/TrailsPanel.test.tsx +99 -35
- package/src/subsystem/TrailsPanel.tsx +43 -86
- package/src/subsystem/trailSidebarScope.test.ts +89 -0
- package/src/subsystem/trailSidebarScope.ts +62 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildTreePathMapping, purlOwnerName, purlRepoKey } from './paths';
|
|
2
|
+
/** Scope sidebar content to a trail's endpoints and authored source sites. */
|
|
3
|
+
export function buildTrailSidebarScope(components, trails, issues, trailId) {
|
|
4
|
+
const trail = trails?.find((item) => item.id === trailId) ?? null;
|
|
5
|
+
if (!trail) {
|
|
6
|
+
return { trail: null, treePaths: buildTreePathMapping(components), issues: issues ?? [] };
|
|
7
|
+
}
|
|
8
|
+
const aliases = new Set(trail.steps.flatMap((step) => [step.from, step.to]));
|
|
9
|
+
const participants = components.filter((component) => aliases.has(component.alias));
|
|
10
|
+
const files = [...participants];
|
|
11
|
+
for (const step of trail.steps) {
|
|
12
|
+
files.push(step);
|
|
13
|
+
if (step.target)
|
|
14
|
+
files.push(step.target);
|
|
15
|
+
}
|
|
16
|
+
const modules = new Set(participants.map((component) => component.module?.trim()).filter(Boolean));
|
|
17
|
+
const processes = new Set(participants.map((component) => component.process).filter(Boolean));
|
|
18
|
+
const repos = new Set(files.flatMap((file) => [purlRepoKey(file.purl), purlOwnerName(file.purl)].filter((key) => !!key)));
|
|
19
|
+
const scopedIssues = (issues ?? []).filter((issue) => {
|
|
20
|
+
const target = issue.target;
|
|
21
|
+
if (!target)
|
|
22
|
+
return false;
|
|
23
|
+
const key = target.id ?? target.label;
|
|
24
|
+
switch (target.kind) {
|
|
25
|
+
case 'component': {
|
|
26
|
+
const component = components.find((candidate) => candidate.alias === target.id || candidate.alias === target.label ||
|
|
27
|
+
candidate.name === target.label || candidate.symbol === target.label);
|
|
28
|
+
return component != null && aliases.has(component.alias);
|
|
29
|
+
}
|
|
30
|
+
case 'trail':
|
|
31
|
+
return target.id != null ? target.id === trail.id : target.label === trail.title;
|
|
32
|
+
case 'step':
|
|
33
|
+
return target.id === trail.id && target.stepIndex != null &&
|
|
34
|
+
Number.isInteger(target.stepIndex) && trail.steps[target.stepIndex] != null;
|
|
35
|
+
case 'module':
|
|
36
|
+
return modules.has(key);
|
|
37
|
+
case 'process':
|
|
38
|
+
return processes.has(key);
|
|
39
|
+
case 'repo':
|
|
40
|
+
return repos.has(purlRepoKey(key) ?? key);
|
|
41
|
+
default:
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return { trail, treePaths: buildTreePathMapping(files), issues: scopedIssues };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=trailSidebarScope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trailSidebarScope.js","sourceRoot":"","sources":["../../src/subsystem/trailSidebarScope.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import '@xyflow/react/dist/style.css';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
5
|
+
import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
|
|
6
|
+
import type { SubsystemComponent, SubsystemTrail } from '../../../subsystem/model';
|
|
7
|
+
import type { SubsystemIssue } from '../../../subsystem/IssueList';
|
|
8
|
+
|
|
9
|
+
const components: SubsystemComponent[] = [
|
|
10
|
+
{ alias: 'checkout', name: 'Checkout', construct: 'function', file: 'src/checkout.ts', purl: 'pkg:github/acme/app' },
|
|
11
|
+
{ alias: 'store', name: 'Store', construct: 'function', file: 'src/store.ts', purl: 'pkg:github/acme/app' },
|
|
12
|
+
{ alias: 'report', name: 'Report', construct: 'function', file: 'src/report.ts', purl: 'pkg:github/acme/app' },
|
|
13
|
+
{ alias: 'mail', name: 'Mail', construct: 'function', file: 'src/mail.ts', purl: 'pkg:github/acme/app' },
|
|
14
|
+
];
|
|
15
|
+
const trails: SubsystemTrail[] = [
|
|
16
|
+
{ id: 'checkout', title: 'Validate the checkout and persist the customer’s order with all selected items', steps: [
|
|
17
|
+
{ from: 'checkout', to: 'store', mechanism: 'calls', file: 'src/checkout.ts',
|
|
18
|
+
purl: 'pkg:github/acme/app', symbol: 'saveCheckout', annotation: 'Persist the checkout.' },
|
|
19
|
+
] },
|
|
20
|
+
{ id: 'report', title: 'Generate the monthly account activity report and deliver it to every subscribed recipient', steps: [
|
|
21
|
+
{ from: 'report', to: 'mail', mechanism: 'calls', file: 'src/report.ts',
|
|
22
|
+
purl: 'pkg:github/acme/app', symbol: 'sendReport' },
|
|
23
|
+
] },
|
|
24
|
+
];
|
|
25
|
+
const issues: SubsystemIssue[] = [
|
|
26
|
+
{ id: 'checkout-symbol', severity: 'error', kind: 'symbol_unmatched',
|
|
27
|
+
message: 'Checkout declaration needs verification.',
|
|
28
|
+
target: { kind: 'component', id: 'checkout', label: 'Checkout' } },
|
|
29
|
+
{ id: 'checkout-step', severity: 'error', kind: 'step_span_unconfirmed',
|
|
30
|
+
message: 'Capture the checkout seam.',
|
|
31
|
+
target: { kind: 'step', id: 'checkout', stepIndex: 0, label: 'Checkout → Store' } },
|
|
32
|
+
{ id: 'report-symbol', severity: 'error', kind: 'symbol_unmatched',
|
|
33
|
+
message: 'Report declaration needs verification.',
|
|
34
|
+
target: { kind: 'component', id: 'report', label: 'Report' } },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function Demo() {
|
|
38
|
+
const [showIssues, setShowIssues] = useState(false);
|
|
39
|
+
return (
|
|
40
|
+
<div style={{ height: '100vh' }}>
|
|
41
|
+
<SubsystemComponentGraph
|
|
42
|
+
title="Trail-scoped sidebar"
|
|
43
|
+
description="Select a trail, then switch between Trails, Files, and Issues. Its header stays visible and each panel shows only that trail’s information. Back clears the trail context."
|
|
44
|
+
components={components}
|
|
45
|
+
trails={trails}
|
|
46
|
+
issues={issues}
|
|
47
|
+
showIssues={showIssues}
|
|
48
|
+
diagnostic={{ status: 'issues', issueCount: issues.length,
|
|
49
|
+
onToggle: () => setShowIssues((value) => !value) }}
|
|
50
|
+
renderFileViewer={(file) => (
|
|
51
|
+
<pre style={{ padding: 16 }}>Source preview: {file}</pre>
|
|
52
|
+
)}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const meta = {
|
|
59
|
+
title: 'Canvases/Subsystem/TrailSidebarViews',
|
|
60
|
+
component: SubsystemComponentGraph,
|
|
61
|
+
parameters: { layout: 'fullscreen' },
|
|
62
|
+
decorators: [(Story) => (
|
|
63
|
+
<ThemeProvider theme={defaultEditorTheme}><Story /></ThemeProvider>
|
|
64
|
+
)],
|
|
65
|
+
} satisfies Meta<typeof SubsystemComponentGraph>;
|
|
66
|
+
export default meta;
|
|
67
|
+
type Story = StoryObj<typeof meta>;
|
|
68
|
+
export const ScopedPanels: Story = { render: () => <Demo /> };
|
|
@@ -155,7 +155,7 @@ export function SubsystemDiagnosticToggle({
|
|
|
155
155
|
justifyContent: bordered || segmented ? 'center' : undefined,
|
|
156
156
|
gap: 4,
|
|
157
157
|
height: bordered || segmented ? 28 : 22,
|
|
158
|
-
padding:
|
|
158
|
+
padding: segmented ? '4px 16px' : bordered ? '4px 7px' : showCount ? '0 7px 0 6px' : '0 5px',
|
|
159
159
|
border: bordered ? `1px solid ${theme.colors.border}` : 'none',
|
|
160
160
|
borderRadius: 4,
|
|
161
161
|
background,
|
|
@@ -227,6 +227,7 @@ export function SubsystemDiagnosticToggle({
|
|
|
227
227
|
fontFamily: theme.fonts.monospace,
|
|
228
228
|
fontSize: theme.fontSizes[0],
|
|
229
229
|
fontWeight: 600,
|
|
230
|
+
marginLeft: segmented && labelText ? 4 : undefined,
|
|
230
231
|
lineHeight: 1,
|
|
231
232
|
}}
|
|
232
233
|
>
|
|
@@ -73,15 +73,51 @@ describe('subsystem graph sidebar', () => {
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
test('groups Trails, Files, and Issues into one switch with Trails selected by default', () => {
|
|
76
|
-
const body = renderGraph();
|
|
76
|
+
const body = renderGraph({ diagnostic: { status: 'issues', issueCount: 1 } });
|
|
77
77
|
const group = body.querySelector('header [role="group"][aria-label="Sidebar view"]');
|
|
78
78
|
expect(group?.querySelectorAll('button').length).toBe(3);
|
|
79
79
|
expect(group?.querySelectorAll('button[aria-pressed="true"]').length).toBe(1);
|
|
80
|
-
expect(group?.querySelector('button[aria-label
|
|
81
|
-
expect(group?.querySelector('button[aria-label^="
|
|
80
|
+
expect(group?.querySelector('button[aria-label^="Trails"]')?.getAttribute('aria-pressed')).toBe('true');
|
|
81
|
+
expect(group?.querySelector('button[aria-label^="Trails"]')?.textContent).toBe('Trails1');
|
|
82
|
+
expect(group?.querySelector('button[aria-label^="Issues:"]')?.textContent).toBe('Issues1');
|
|
82
83
|
expect((group as HTMLElement).style.borderStyle).toBe('solid');
|
|
84
|
+
expect((group as HTMLElement).style.gridAutoColumns).toBe('1fr');
|
|
85
|
+
for (const button of group!.querySelectorAll('button')) {
|
|
86
|
+
expect((button as HTMLElement).style.paddingLeft).toBe('16px');
|
|
87
|
+
expect((button as HTMLElement).style.paddingRight).toBe('16px');
|
|
88
|
+
expect((button.lastElementChild as HTMLElement).style.marginLeft).toBe('4px');
|
|
89
|
+
}
|
|
83
90
|
});
|
|
84
91
|
|
|
92
|
+
for (const count of [0, 1, 3]) {
|
|
93
|
+
test(`shows ${count} in the plural Trails option`, () => {
|
|
94
|
+
const body = renderGraph({
|
|
95
|
+
trails: Array.from({ length: count }, (_, index) => ({ ...trails![0], id: `trail-${index}` })),
|
|
96
|
+
graphifyRelations,
|
|
97
|
+
});
|
|
98
|
+
const button = body.querySelector('header button[aria-label^="Trails"]');
|
|
99
|
+
expect(button?.textContent).toBe(`Trails${count}`);
|
|
100
|
+
expect(button?.getAttribute('aria-label')).toBe(`Trails (${count} ${count === 1 ? 'trail' : 'trails'})`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const props of [
|
|
105
|
+
{},
|
|
106
|
+
{ diagnostic: { status: 'ok' as const, issueCount: 0 } },
|
|
107
|
+
{ issues: [], diagnostic: { status: 'issues' as const, issueCount: 3 } },
|
|
108
|
+
{ issues: [], showIssues: true },
|
|
109
|
+
]) {
|
|
110
|
+
test(`hides Issues and keeps Trails active for an empty scope: ${JSON.stringify(props)}`, () => {
|
|
111
|
+
const body = renderGraph(props);
|
|
112
|
+
const group = body.querySelector('header [aria-label="Sidebar view"]');
|
|
113
|
+
expect(group?.querySelectorAll('button').length).toBe(2);
|
|
114
|
+
expect(group?.querySelector('button[aria-label^="Issues:"]')).toBeNull();
|
|
115
|
+
expect(group?.querySelector('button[aria-label^="Trails"]')?.getAttribute('aria-pressed')).toBe('true');
|
|
116
|
+
expect(body.textContent).toContain('Request flow');
|
|
117
|
+
expect((group as HTMLElement).style.gridAutoColumns).toBe('1fr');
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
85
121
|
test('omits the Files button when no components have files', () => {
|
|
86
122
|
const body = renderGraph({
|
|
87
123
|
components: components.map((component) => ({ ...component, file: '' })),
|
|
@@ -110,7 +146,7 @@ describe('subsystem graph sidebar', () => {
|
|
|
110
146
|
expect(toggle?.getAttribute('aria-pressed')).toBe(String(active));
|
|
111
147
|
expect(header?.querySelectorAll('button').length).toBe(3);
|
|
112
148
|
expect(header?.querySelectorAll('button[aria-pressed="true"]').length).toBe(1);
|
|
113
|
-
expect(header?.querySelector('button[aria-label
|
|
149
|
+
expect(header?.querySelector('button[aria-label^="Trails"]')?.getAttribute('aria-pressed')).toBe(String(!active));
|
|
114
150
|
expect(body.querySelectorAll('button[aria-label^="Issues:"]').length).toBe(1);
|
|
115
151
|
expect(header?.nextElementSibling?.querySelector('button[aria-label^="Issues:"]')).toBeNull();
|
|
116
152
|
expect(body.textContent).toContain('Always visible description.');
|
|
@@ -188,6 +224,8 @@ describe('subsystem graph sidebar', () => {
|
|
|
188
224
|
globalThis.window = dom as unknown as typeof window;
|
|
189
225
|
try {
|
|
190
226
|
const body = renderGraph({ persistKey: 'trail-nav-test' });
|
|
227
|
+
expect(body.querySelector('header button[aria-label="Trail"]')?.textContent).toBe('Trail');
|
|
228
|
+
expect(body.querySelector('header button[aria-label^="Trails"]')).toBeNull();
|
|
191
229
|
expect(body.querySelector('[aria-hidden="false"] button[aria-label="Back to trails"]')).not.toBeNull();
|
|
192
230
|
expect(body.querySelector('button[aria-label^="Play "]')).toBeNull();
|
|
193
231
|
expect(body.querySelector('button[aria-label^="Focus layout"]')).toBeNull();
|
|
@@ -197,6 +235,66 @@ describe('subsystem graph sidebar', () => {
|
|
|
197
235
|
}
|
|
198
236
|
});
|
|
199
237
|
|
|
238
|
+
test('keeps the trail header and scoped counts in Issues after graph selection is cleared', () => {
|
|
239
|
+
const previousWindow = globalThis.window;
|
|
240
|
+
const dom = new Window();
|
|
241
|
+
dom.localStorage.setItem('principal.subsystems.viewState.scoped-sidebar-test', JSON.stringify({
|
|
242
|
+
sidebarTrailId: 'request',
|
|
243
|
+
focusedTrailId: null,
|
|
244
|
+
}));
|
|
245
|
+
globalThis.window = dom as unknown as typeof window;
|
|
246
|
+
try {
|
|
247
|
+
const body = renderGraph({
|
|
248
|
+
persistKey: 'scoped-sidebar-test',
|
|
249
|
+
showIssues: true,
|
|
250
|
+
components: [...components, {
|
|
251
|
+
alias: 'other', name: 'Other', construct: 'function', file: 'src/other.ts', purl: 'pkg:github/test/app',
|
|
252
|
+
}],
|
|
253
|
+
issues: [
|
|
254
|
+
{ id: 'related', severity: 'error', kind: 'missing_file', message: 'Related finding',
|
|
255
|
+
target: { kind: 'component', id: 'entry', label: 'Entry' } },
|
|
256
|
+
{ id: 'unrelated', severity: 'error', kind: 'missing_file', message: 'Unrelated finding',
|
|
257
|
+
target: { kind: 'component', id: 'other', label: 'Other' } },
|
|
258
|
+
],
|
|
259
|
+
diagnostic: { status: 'issues', issueCount: 2 },
|
|
260
|
+
});
|
|
261
|
+
expect(body.querySelector('[aria-hidden="false"] button[aria-label="Back to trails"]')).not.toBeNull();
|
|
262
|
+
expect(body.querySelector('[aria-hidden="false"]')?.textContent).toContain('Request flow');
|
|
263
|
+
expect(body.querySelector('header button[aria-label^="Files"]')?.textContent).toBe('Files2');
|
|
264
|
+
expect(body.querySelector('header button[aria-label^="Issues:"]')?.textContent).toBe('Issues1');
|
|
265
|
+
expect(body.querySelector('header button[aria-label="Trail"]')?.textContent).toBe('Trail');
|
|
266
|
+
expect(body.querySelector('[aria-hidden="false"]')?.textContent).not.toContain('Unrelated finding');
|
|
267
|
+
} finally {
|
|
268
|
+
globalThis.window = previousWindow;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('hides Issues for a selected trail with no findings even when other trails have issues', () => {
|
|
273
|
+
const previousWindow = globalThis.window;
|
|
274
|
+
const dom = new Window();
|
|
275
|
+
dom.localStorage.setItem('principal.subsystems.viewState.empty-trail-issues-test', JSON.stringify({
|
|
276
|
+
sidebarTrailId: 'request',
|
|
277
|
+
}));
|
|
278
|
+
globalThis.window = dom as unknown as typeof window;
|
|
279
|
+
try {
|
|
280
|
+
const body = renderGraph({
|
|
281
|
+
persistKey: 'empty-trail-issues-test',
|
|
282
|
+
showIssues: true,
|
|
283
|
+
issues: [{
|
|
284
|
+
id: 'unrelated', severity: 'error', kind: 'missing_file', message: 'Unrelated finding',
|
|
285
|
+
target: { kind: 'component', id: 'other', label: 'Other' },
|
|
286
|
+
}],
|
|
287
|
+
diagnostic: { status: 'issues', issueCount: 1 },
|
|
288
|
+
});
|
|
289
|
+
expect(body.querySelector('header button[aria-label^="Issues:"]')).toBeNull();
|
|
290
|
+
expect(body.querySelector('header button[aria-label="Trail"]')?.getAttribute('aria-pressed')).toBe('true');
|
|
291
|
+
expect(body.querySelector('[aria-hidden="false"] button[aria-label="Back to trails"]')).not.toBeNull();
|
|
292
|
+
expect(body.querySelector('[aria-hidden="false"]')?.textContent).toContain('Entry');
|
|
293
|
+
} finally {
|
|
294
|
+
globalThis.window = previousWindow;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
200
298
|
test('static graphs keep their description but do not show a file panel', () => {
|
|
201
299
|
const body = renderGraph({ trails: undefined, graphifyRelations });
|
|
202
300
|
expect(body.textContent).toContain('Always visible description.');
|
|
@@ -217,7 +315,10 @@ describe('subsystem graph sidebar', () => {
|
|
|
217
315
|
});
|
|
218
316
|
|
|
219
317
|
test('the description stays visible while diagnostics replace the trails', () => {
|
|
220
|
-
const body = renderGraph({ showIssues: true, issues: [
|
|
318
|
+
const body = renderGraph({ showIssues: true, issues: [{
|
|
319
|
+
id: 'related', severity: 'error', kind: 'missing_file', message: 'Related finding',
|
|
320
|
+
target: { kind: 'component', id: 'entry', label: 'Entry' },
|
|
321
|
+
}] });
|
|
221
322
|
expect(body.textContent).toContain('Always visible description.');
|
|
222
323
|
expect(body.textContent).not.toContain('Request flow');
|
|
223
324
|
expect(body.querySelector('[aria-label="Resize sidebar"]')).not.toBeNull();
|