@fgv/ts-res-browser 5.0.0-2 → 5.0.0-21

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.
Files changed (99) hide show
  1. package/cli.js +132 -21
  2. package/dist/bundle.js +1 -1
  3. package/dist/bundle.js.LICENSE.txt +11 -1
  4. package/package.json +42 -36
  5. package/.rush/temp/587277bfc395406ad444e989b6c309b9698f534c.tar.log +0 -15
  6. package/.rush/temp/chunked-rush-logs/ts-res-browser.build.chunks.jsonl +0 -35
  7. package/.rush/temp/operation/build/all.log +0 -35
  8. package/.rush/temp/operation/build/log-chunks.jsonl +0 -35
  9. package/.rush/temp/operation/build/state.json +0 -3
  10. package/.rush/temp/shrinkwrap-deps.json +0 -1225
  11. package/TS_RES_BROWSER_PLAN.md +0 -689
  12. package/babel.config.js +0 -17
  13. package/config/jest.config.json +0 -13
  14. package/config/rig.json +0 -16
  15. package/postcss.config.js +0 -3
  16. package/public/index.html +0 -15
  17. package/rush-logs/ts-res-browser.build.cache.log +0 -3
  18. package/rush-logs/ts-res-browser.build.log +0 -35
  19. package/src/App.tsx +0 -274
  20. package/src/cli.ts +0 -44
  21. package/src/components/common/FileImporter.tsx +0 -143
  22. package/src/components/common/NavigationWarningModal.tsx +0 -62
  23. package/src/components/layout/AppLayout.tsx +0 -46
  24. package/src/components/layout/Header.tsx +0 -22
  25. package/src/components/layout/MessagesWindow.tsx +0 -108
  26. package/src/components/layout/Sidebar.tsx +0 -99
  27. package/src/components/tools/CompiledBrowser.tsx +0 -949
  28. package/src/components/tools/ConfigurationTool.tsx +0 -1405
  29. package/src/components/tools/FilterTool.tsx +0 -1114
  30. package/src/components/tools/ImportTool.tsx +0 -249
  31. package/src/components/tools/ResolutionViewer.tsx +0 -1672
  32. package/src/components/tools/SourceBrowser.tsx +0 -496
  33. package/src/components/tools/ZipLoader.tsx +0 -302
  34. package/src/hooks/useAppState.ts +0 -111
  35. package/src/hooks/useFileImport.ts +0 -86
  36. package/src/hooks/useNavigationWarning.ts +0 -67
  37. package/src/hooks/useResourceManager.ts +0 -295
  38. package/src/hooks/useUrlParams.ts +0 -56
  39. package/src/index.css +0 -38
  40. package/src/main.tsx +0 -16
  41. package/src/types/app.ts +0 -70
  42. package/src/types/fileSystemAccess.d.ts +0 -94
  43. package/src/utils/browserFileTreeAccessors.ts +0 -245
  44. package/src/utils/fileImport.ts +0 -336
  45. package/src/utils/fileTreeConverter.ts +0 -160
  46. package/src/utils/filterResources.ts +0 -677
  47. package/src/utils/tsResIntegration.ts +0 -467
  48. package/src/utils/urlParams.ts +0 -238
  49. package/src/utils/zip/browserZipFileTreeAccessors.ts +0 -385
  50. package/tailwind.config.js +0 -21
  51. package/test-data/README.md +0 -184
  52. package/test-data/config-variations/README.md +0 -64
  53. package/test-data/config-variations/composition-terminology.json +0 -112
  54. package/test-data/config-variations/enterprise-complex.json +0 -129
  55. package/test-data/config-variations/gaming-app.json +0 -87
  56. package/test-data/config-variations/high-priority-language.json +0 -65
  57. package/test-data/config-variations/minimal-basic.json +0 -37
  58. package/test-data/config-variations/qualifier-default-values.json +0 -71
  59. package/test-data/config-variations/territory-first.json +0 -68
  60. package/test-data/config-variations/territory-hierarchy-test.json +0 -31
  61. package/test-data/config-variations/territory-hierarchy-unconstrained.json +0 -32
  62. package/test-data/default-matching-test/greeting.res.json +0 -83
  63. package/test-data/default-matching-test/only-defaults.res.json +0 -48
  64. package/test-data/sample-project/package.json +0 -27
  65. package/test-data/sample-project/src/resources/app-config.json +0 -27
  66. package/test-data/sample-project/src/resources/app.json +0 -177
  67. package/test-data/sample-project/src/resources/config/app.env=development.json +0 -21
  68. package/test-data/sample-project/src/resources/config/app.json +0 -21
  69. package/test-data/sample-project/src/resources/config/app.platform=mobile.json +0 -23
  70. package/test-data/sample-project/src/resources/feature-flags.json +0 -23
  71. package/test-data/sample-project/src/resources/home=CA/common.json +0 -16
  72. package/test-data/sample-project/src/resources/images/density=hdpi.json +0 -29
  73. package/test-data/sample-project/src/resources/images/home.json +0 -29
  74. package/test-data/sample-project/src/resources/images/logout.json +0 -14
  75. package/test-data/sample-project/src/resources/images/settings.json +0 -14
  76. package/test-data/sample-project/src/resources/images/ui-icons.json +0 -29
  77. package/test-data/sample-project/src/resources/images/user.density=hdpi.json +0 -14
  78. package/test-data/sample-project/src/resources/images/user.json +0 -14
  79. package/test-data/sample-project/src/resources/lang=en-US/common.json +0 -15
  80. package/test-data/sample-project/src/resources/language=en-CA/ui-terms.json +0 -19
  81. package/test-data/sample-project/src/resources/language=en-GB/ui-terms.json +0 -30
  82. package/test-data/sample-project/src/resources/language=fr-CA/ui-terms.json +0 -25
  83. package/test-data/sample-project/src/resources/language=nl-BE/ui-terms.json +0 -17
  84. package/test-data/sample-project/src/resources/region=europe/feature-flags.json +0 -17
  85. package/test-data/sample-project/src/resources/region=north-america/feature-flags.json +0 -18
  86. package/test-data/sample-project/src/resources/strings/dashboard.home=CA,language=fr.json +0 -20
  87. package/test-data/sample-project/src/resources/strings/dashboard.json +0 -18
  88. package/test-data/sample-project/src/resources/strings/dashboard.role=admin.json +0 -23
  89. package/test-data/sample-project/src/resources/strings/dashboard.role=user.json +0 -21
  90. package/test-data/sample-project/src/resources/strings/language=en-US.json +0 -16
  91. package/test-data/sample-project/src/resources/strings.json +0 -78
  92. package/test-data/sample-project/src/resources/territory=BE/app-config.json +0 -19
  93. package/test-data/sample-project/src/resources/territory=CA/app-config.json +0 -21
  94. package/test-data/sample-project/src/resources/territory=GB/app-config.json +0 -25
  95. package/test-data/sample-project/src/resources/territory=NL/app-config.json +0 -25
  96. package/test-data/sample-project/src/resources/ui-terms.json +0 -96
  97. package/test-data/sample-project/src/resources-config.json +0 -111
  98. package/tsconfig.json +0 -27
  99. package/webpack.config.js +0 -60
@@ -1,1672 +0,0 @@
1
- import React, { useState, useMemo, useCallback } from 'react';
2
- import { MagnifyingGlassIcon, CubeIcon, DocumentTextIcon } from '@heroicons/react/24/outline';
3
- import { UseResourceManagerReturn } from '../../hooks/useResourceManager';
4
- import { Message, FilterState } from '../../types/app';
5
- import { FilterResult } from '../../utils/filterResources';
6
- import { Runtime, Config, NoMatch } from '@fgv/ts-res';
7
- import { createSimpleContext, DEFAULT_SYSTEM_CONFIGURATION } from '../../utils/tsResIntegration';
8
-
9
- // Updated types for default resolution support
10
- type ConditionMatchType = 'match' | 'matchAsDefault' | 'noMatch';
11
-
12
- interface IConditionMatchResult {
13
- score: number;
14
- priority: number;
15
- matchType: ConditionMatchType;
16
- }
17
-
18
- type DecisionResolutionResult =
19
- | { success: false }
20
- | { success: true; instanceIndices: ReadonlyArray<number>; defaultInstanceIndices: ReadonlyArray<number> };
21
-
22
- interface ResolutionViewerProps {
23
- onMessage?: (type: Message['type'], message: string) => void;
24
- resourceManager: UseResourceManagerReturn;
25
- filterState: FilterState;
26
- filterResult?: FilterResult | null;
27
- }
28
-
29
- interface ContextState {
30
- [qualifierName: string]: string;
31
- }
32
-
33
- interface ConditionEvaluationResult {
34
- qualifierName: string;
35
- qualifierValue: any;
36
- conditionValue: any;
37
- operator: string;
38
- score: number;
39
- matched: boolean;
40
- matchType: ConditionMatchType;
41
- scoreAsDefault?: number;
42
- conditionIndex: number;
43
- }
44
-
45
- interface CandidateInfo {
46
- candidate: Runtime.IResourceCandidate;
47
- conditionSetKey: string | null;
48
- candidateIndex: number;
49
- matched: boolean;
50
- matchType: ConditionMatchType;
51
- isDefaultMatch: boolean;
52
- conditionEvaluations?: ConditionEvaluationResult[];
53
- }
54
-
55
- interface ResolutionResult {
56
- success: boolean;
57
- resource?: Runtime.IResource;
58
- bestCandidate?: Runtime.IResourceCandidate;
59
- allCandidates?: readonly Runtime.IResourceCandidate[];
60
- candidateDetails?: CandidateInfo[];
61
- composedValue?: any;
62
- error?: string;
63
- }
64
-
65
- type ViewMode = 'best' | 'all' | 'raw' | 'composed';
66
-
67
- interface TooltipProps {
68
- content: string;
69
- children: React.ReactNode;
70
- }
71
-
72
- const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
73
- const [isVisible, setIsVisible] = useState(false);
74
-
75
- return (
76
- <div
77
- className="relative inline-block"
78
- onMouseEnter={() => setIsVisible(true)}
79
- onMouseLeave={() => setIsVisible(false)}
80
- >
81
- {children}
82
- {isVisible && (
83
- <div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 z-10">
84
- <div className="bg-gray-800 text-white text-xs rounded py-1 px-2 whitespace-nowrap">
85
- {content}
86
- <div className="absolute top-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-800"></div>
87
- </div>
88
- </div>
89
- )}
90
- </div>
91
- );
92
- };
93
-
94
- interface CacheDashboardProps {
95
- cacheMetrics: Runtime.ResourceResolverCacheMetricsListener<Runtime.AggregateCacheMetrics> | null;
96
- resolver: Runtime.ResourceResolver | null;
97
- onReset: () => void;
98
- }
99
-
100
- const CacheDashboard: React.FC<CacheDashboardProps> = ({ cacheMetrics, resolver, onReset }) => {
101
- if (!cacheMetrics || !resolver) {
102
- return (
103
- <div className="bg-gray-50 border border-gray-200 rounded-lg p-3 mb-4">
104
- <div className="flex items-center justify-between">
105
- <span className="text-sm text-gray-500">📊 Cache: Not active</span>
106
- <button
107
- onClick={onReset}
108
- disabled={true}
109
- className="text-xs px-2 py-1 bg-gray-200 text-gray-400 rounded cursor-not-allowed"
110
- >
111
- Reset
112
- </button>
113
- </div>
114
- </div>
115
- );
116
- }
117
-
118
- const metrics = cacheMetrics.metrics;
119
-
120
- // Helper function to get cache display info
121
- const getCacheDisplay = (cacheType: Runtime.ResourceResolverCacheType) => {
122
- const cacheMetric = metrics[cacheType];
123
- const hitRate = cacheMetric.hitRate;
124
- const totalAccesses = cacheMetric.totalAccesses;
125
- const hits = cacheMetric.hits;
126
- const misses = cacheMetric.misses;
127
- const errors = cacheMetric.errors;
128
-
129
- // Get actual filled cache slots by looking at the cache arrays
130
- const cacheArray =
131
- cacheType === 'condition'
132
- ? resolver.conditionCache
133
- : cacheType === 'conditionSet'
134
- ? resolver.conditionSetCache
135
- : resolver.decisionCache;
136
- const filledSlots = cacheArray.filter((slot) => slot !== undefined).length;
137
-
138
- const totalSlots =
139
- cacheType === 'condition'
140
- ? resolver.conditionCacheSize
141
- : cacheType === 'conditionSet'
142
- ? resolver.conditionSetCacheSize
143
- : resolver.decisionCacheSize;
144
-
145
- // Color based on hit rate
146
- const getColor = (rate: number) => {
147
- if (rate === 0) return 'text-gray-500';
148
- if (rate >= 80) return 'text-green-600';
149
- if (rate >= 50) return 'text-yellow-600';
150
- return 'text-red-600';
151
- };
152
-
153
- return {
154
- filledSlots,
155
- totalSlots,
156
- totalAccesses,
157
- hits,
158
- misses,
159
- errors,
160
- hitRate: Math.round(hitRate),
161
- color: getColor(hitRate)
162
- };
163
- };
164
-
165
- const conditionDisplay = getCacheDisplay('condition');
166
- const conditionSetDisplay = getCacheDisplay('conditionSet');
167
- const decisionDisplay = getCacheDisplay('decision');
168
-
169
- return (
170
- <div className="bg-gray-50 border border-gray-200 rounded-lg p-3 mb-4">
171
- <div className="flex items-center justify-between">
172
- <div className="flex items-center space-x-4 text-sm">
173
- <span className="text-gray-700">📊 Cache:</span>
174
- <Tooltip
175
- content={`Condition Cache: ${conditionDisplay.filledSlots} filled out of ${conditionDisplay.totalSlots} slots, ${conditionDisplay.totalAccesses} total accesses (${conditionDisplay.hits} hits, ${conditionDisplay.misses} misses, ${conditionDisplay.errors} errors), ${conditionDisplay.hitRate}% hit rate`}
176
- >
177
- <span className={`font-mono ${conditionDisplay.color} cursor-help`}>
178
- Cond: {conditionDisplay.filledSlots}/{conditionDisplay.totalSlots} ({conditionDisplay.hitRate}%)
179
- </span>
180
- </Tooltip>
181
- <span className="text-gray-400">|</span>
182
- <Tooltip
183
- content={`ConditionSet Cache: ${conditionSetDisplay.filledSlots} filled out of ${conditionSetDisplay.totalSlots} slots, ${conditionSetDisplay.totalAccesses} total accesses (${conditionSetDisplay.hits} hits, ${conditionSetDisplay.misses} misses, ${conditionSetDisplay.errors} errors), ${conditionSetDisplay.hitRate}% hit rate`}
184
- >
185
- <span className={`font-mono ${conditionSetDisplay.color} cursor-help`}>
186
- CondSet: {conditionSetDisplay.filledSlots}/{conditionSetDisplay.totalSlots} (
187
- {conditionSetDisplay.hitRate}%)
188
- </span>
189
- </Tooltip>
190
- <span className="text-gray-400">|</span>
191
- <Tooltip
192
- content={`Decision Cache: ${decisionDisplay.filledSlots} filled out of ${decisionDisplay.totalSlots} slots, ${decisionDisplay.totalAccesses} total accesses (${decisionDisplay.hits} hits, ${decisionDisplay.misses} misses, ${decisionDisplay.errors} errors), ${decisionDisplay.hitRate}% hit rate`}
193
- >
194
- <span className={`font-mono ${decisionDisplay.color} cursor-help`}>
195
- Dec: {decisionDisplay.filledSlots}/{decisionDisplay.totalSlots} ({decisionDisplay.hitRate}%)
196
- </span>
197
- </Tooltip>
198
- </div>
199
- <Tooltip content="Clear all caches and reset metrics">
200
- <button
201
- onClick={onReset}
202
- className="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors"
203
- >
204
- Reset
205
- </button>
206
- </Tooltip>
207
- </div>
208
- </div>
209
- );
210
- };
211
-
212
- // Component to display condition evaluation results
213
- interface ConditionEvaluationDisplayProps {
214
- evaluations: ConditionEvaluationResult[];
215
- }
216
-
217
- const ConditionEvaluationDisplay: React.FC<ConditionEvaluationDisplayProps> = ({ evaluations }) => {
218
- if (!evaluations || evaluations.length === 0) {
219
- return <span className="text-xs text-gray-500">No conditions</span>;
220
- }
221
-
222
- const getMatchTypeColor = (type: ConditionMatchType) => {
223
- switch (type) {
224
- case 'match':
225
- return 'bg-green-100 text-green-800';
226
- case 'matchAsDefault':
227
- return 'bg-amber-100 text-amber-800';
228
- case 'noMatch':
229
- return 'bg-red-100 text-red-800';
230
- default:
231
- return 'bg-gray-100 text-gray-800';
232
- }
233
- };
234
-
235
- const getMatchTypeIcon = (type: ConditionMatchType) => {
236
- switch (type) {
237
- case 'match':
238
- return '✓';
239
- case 'matchAsDefault':
240
- return '≈';
241
- case 'noMatch':
242
- return '✗';
243
- default:
244
- return '?';
245
- }
246
- };
247
-
248
- return (
249
- <div className="flex flex-wrap gap-1 mt-1">
250
- {evaluations.map((evaluation, index) => {
251
- const displayText = `${evaluation.qualifierName}=${
252
- evaluation.conditionValue
253
- } (${evaluation.score.toFixed(1)})`;
254
-
255
- const tooltipContent = [
256
- `${evaluation.qualifierName}: context="${evaluation.qualifierValue}" ${evaluation.operator} condition="${evaluation.conditionValue}"`,
257
- `→ score=${evaluation.score} (${evaluation.matchType})`,
258
- evaluation.scoreAsDefault !== undefined ? `default score: ${evaluation.scoreAsDefault}` : null,
259
- `[condition index: ${evaluation.conditionIndex}]`
260
- ]
261
- .filter(Boolean)
262
- .join(' ');
263
-
264
- return (
265
- <Tooltip key={index} content={tooltipContent}>
266
- <span className={`text-xs px-2 py-1 rounded ${getMatchTypeColor(evaluation.matchType)}`}>
267
- {getMatchTypeIcon(evaluation.matchType)} {displayText}
268
- </span>
269
- </Tooltip>
270
- );
271
- })}
272
- </div>
273
- );
274
- };
275
-
276
- // Cache Contents Display Component
277
- interface CacheContentsDisplayProps {
278
- resolver: Runtime.ResourceResolver | null;
279
- }
280
-
281
- const CacheContentsDisplay: React.FC<CacheContentsDisplayProps> = ({ resolver }) => {
282
- const [expanded, setExpanded] = useState(false);
283
-
284
- if (!resolver) {
285
- return (
286
- <div className="border border-gray-200 rounded-lg bg-gray-50 p-3">
287
- <div className="flex items-center justify-between">
288
- <span className="text-sm text-gray-500">🗄️ Cache Contents</span>
289
- <span className="text-xs text-gray-400">Not available</span>
290
- </div>
291
- </div>
292
- );
293
- }
294
-
295
- const conditionCache = resolver.conditionCache;
296
- const conditionSetCache = resolver.conditionSetCache;
297
- const decisionCache = resolver.decisionCache;
298
-
299
- const conditionHits = conditionCache.filter((c) => c !== undefined).length;
300
- const conditionSetHits = conditionSetCache.filter((cs) => cs !== undefined).length;
301
- const decisionHits = decisionCache.filter((d) => d !== undefined).length;
302
-
303
- const totalCacheEntries = conditionHits + conditionSetHits + decisionHits;
304
-
305
- return (
306
- <div className="border border-gray-200 rounded-lg bg-gray-50">
307
- <div
308
- className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100"
309
- onClick={() => setExpanded(!expanded)}
310
- >
311
- <div className="flex items-center space-x-2">
312
- <span className="text-sm text-gray-700">🗄️ Cache Contents</span>
313
- <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
314
- {totalCacheEntries} cached
315
- </span>
316
- </div>
317
- <div className="flex items-center space-x-2">
318
- <span className="text-xs text-gray-500">
319
- C:{conditionHits} CS:{conditionSetHits} D:{decisionHits}
320
- </span>
321
- <span className="text-xs text-gray-400">{expanded ? '▼' : '▶'}</span>
322
- </div>
323
- </div>
324
-
325
- {expanded && (
326
- <div className="border-t border-gray-200 p-3 space-y-3 max-h-64 overflow-y-auto">
327
- {/* Condition Cache */}
328
- {conditionHits > 0 && (
329
- <div>
330
- <h4 className="text-xs font-medium text-gray-700 mb-2">Conditions ({conditionHits})</h4>
331
- <div className="space-y-1">
332
- {conditionCache.map((score, index) => {
333
- if (score === undefined) return null;
334
-
335
- try {
336
- const condition = resolver.resourceManager.conditions.getAt(index);
337
- if (condition.isFailure()) return null;
338
-
339
- const conditionObj = condition.value;
340
- const qualifierName = conditionObj.qualifier.name;
341
-
342
- // Determine match type from cached result
343
- const cachedResult = score as unknown as IConditionMatchResult;
344
- const matchType =
345
- cachedResult?.matchType || (cachedResult?.score > 0 ? 'match' : 'noMatch');
346
- const displayScore = cachedResult?.score || 0;
347
-
348
- const getMatchTypeColor = (type: ConditionMatchType) => {
349
- switch (type) {
350
- case 'match':
351
- return 'bg-green-100 text-green-800';
352
- case 'matchAsDefault':
353
- return 'bg-amber-100 text-amber-800';
354
- case 'noMatch':
355
- return 'bg-red-100 text-red-800';
356
- default:
357
- return 'bg-gray-100 text-gray-800';
358
- }
359
- };
360
-
361
- const getMatchTypeIcon = (type: ConditionMatchType) => {
362
- switch (type) {
363
- case 'match':
364
- return '✓';
365
- case 'matchAsDefault':
366
- return '≈';
367
- case 'noMatch':
368
- return '✗';
369
- default:
370
- return '?';
371
- }
372
- };
373
-
374
- return (
375
- <div key={index} className="flex items-center justify-between text-xs">
376
- <div className="text-gray-600 truncate max-w-24">
377
- <div>
378
- {index}: {qualifierName}={JSON.stringify(conditionObj.value)}
379
- </div>
380
- {conditionObj.scoreAsDefault !== undefined && (
381
- <div className="text-amber-600 font-medium text-xs">
382
- default: {conditionObj.scoreAsDefault}
383
- </div>
384
- )}
385
- </div>
386
- <div className="flex items-center space-x-1">
387
- <span className={`px-2 py-1 rounded ${getMatchTypeColor(matchType)}`}>
388
- {getMatchTypeIcon(matchType)} {matchType}
389
- </span>
390
- <span className="text-xs text-gray-500">({displayScore.toFixed(2)})</span>
391
- </div>
392
- </div>
393
- );
394
- } catch (error) {
395
- return (
396
- <div key={index} className="flex items-center justify-between text-xs">
397
- <span className="text-gray-400">
398
- {index}: condition-{index}
399
- </span>
400
- <span className="bg-gray-100 text-gray-600 px-2 py-1 rounded">
401
- condition-{index} ({score.toFixed(2)})
402
- </span>
403
- </div>
404
- );
405
- }
406
- })}
407
- </div>
408
- </div>
409
- )}
410
-
411
- {/* Condition Set Cache */}
412
- {conditionSetHits > 0 && (
413
- <div>
414
- <h4 className="text-xs font-medium text-gray-700 mb-2">Condition Sets ({conditionSetHits})</h4>
415
- <div className="space-y-1">
416
- {conditionSetCache.map((result, index) => {
417
- if (result === undefined) return null;
418
-
419
- try {
420
- const conditionSet = resolver.resourceManager.conditionSets.getAt(index);
421
- const key = conditionSet.isSuccess() ? conditionSet.value.key : `cs-${index}`;
422
-
423
- return (
424
- <div key={index} className="flex items-center justify-between text-xs">
425
- <span className="text-gray-600 truncate max-w-24">
426
- {index}: {key}
427
- </span>
428
- <span
429
- className={`px-2 py-1 rounded ${
430
- result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
431
- }`}
432
- >
433
- {key} ({result.success ? '✓' : '✗'})
434
- </span>
435
- </div>
436
- );
437
- } catch (error) {
438
- return (
439
- <div key={index} className="flex items-center justify-between text-xs">
440
- <span className="text-gray-400">
441
- {index}: cs-{index}
442
- </span>
443
- <span
444
- className={`px-2 py-1 rounded ${
445
- result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
446
- }`}
447
- >
448
- cs-{index} ({result.success ? '✓' : '✗'})
449
- </span>
450
- </div>
451
- );
452
- }
453
- })}
454
- </div>
455
- </div>
456
- )}
457
-
458
- {/* Decision Cache */}
459
- {decisionHits > 0 && (
460
- <div>
461
- <h4 className="text-xs font-medium text-gray-700 mb-2">Decisions ({decisionHits})</h4>
462
- <div className="space-y-1">
463
- {decisionCache.map((result, index) => {
464
- if (result === undefined) return null;
465
-
466
- try {
467
- const decision = resolver.resourceManager.decisions.getAt(index);
468
- const key = decision.isSuccess() ? decision.value.key : `decision-${index}`;
469
- const regularMatches = result.success ? result.instanceIndices.length : 0;
470
- const defaultMatches = result.success ? result.defaultInstanceIndices.length : 0;
471
-
472
- return (
473
- <div key={index} className="flex items-center justify-between text-xs">
474
- <span className="text-gray-600 truncate max-w-24">
475
- {index}: {key}
476
- </span>
477
- <div className="flex items-center space-x-1">
478
- <span
479
- className={`px-2 py-1 rounded ${
480
- result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
481
- }`}
482
- >
483
- {regularMatches} regular
484
- </span>
485
- {defaultMatches > 0 && (
486
- <span className="px-2 py-1 rounded bg-amber-100 text-amber-800">
487
- {defaultMatches} default
488
- </span>
489
- )}
490
- </div>
491
- </div>
492
- );
493
- } catch (error) {
494
- return (
495
- <div key={index} className="flex items-center justify-between text-xs">
496
- <span className="text-gray-400">
497
- {index}: decision-{index}
498
- </span>
499
- <span
500
- className={`px-2 py-1 rounded ${
501
- result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
502
- }`}
503
- >
504
- decision-{index} ({result.success ? result.instanceIndices.length : '0'})
505
- </span>
506
- </div>
507
- );
508
- }
509
- })}
510
- </div>
511
- </div>
512
- )}
513
-
514
- {totalCacheEntries === 0 && (
515
- <div className="text-center text-xs text-gray-500 py-4">
516
- No cache entries. Resolve a resource to populate the cache.
517
- </div>
518
- )}
519
- </div>
520
- )}
521
- </div>
522
- );
523
- };
524
-
525
- const ResolutionViewer: React.FC<ResolutionViewerProps> = ({
526
- onMessage,
527
- resourceManager,
528
- filterState,
529
- filterResult
530
- }) => {
531
- const { state: resourceState } = resourceManager;
532
-
533
- // Use filtered resources when filtering is active and successful
534
- const isFilteringActive = filterState.enabled && filterResult?.success === true;
535
- const activeProcessedResources = isFilteringActive
536
- ? filterResult?.processedResources
537
- : resourceState.processedResources;
538
-
539
- // Available qualifiers
540
- const availableQualifiers = useMemo(() => {
541
- if (activeProcessedResources?.compiledCollection.qualifiers) {
542
- // Get qualifier names from the compiled collection
543
- return activeProcessedResources.compiledCollection.qualifiers.map((q) => q.name);
544
- }
545
-
546
- // Use active configuration if available, otherwise fall back to default
547
- const config = resourceState.activeConfiguration || DEFAULT_SYSTEM_CONFIGURATION;
548
- return config.qualifiers.map((q) => q.name);
549
- }, [activeProcessedResources?.compiledCollection.qualifiers, resourceState.activeConfiguration]);
550
-
551
- // Get qualifier type information for UI decisions
552
- const qualifierTypeInfo = useMemo(() => {
553
- const info: Record<
554
- string,
555
- {
556
- type: Config.Model.ISystemConfiguration['qualifierTypes'][0];
557
- enumeratedValues?: string[];
558
- }
559
- > = {};
560
-
561
- // Get the active configuration to access qualifier types
562
- const config = resourceState.activeConfiguration || DEFAULT_SYSTEM_CONFIGURATION;
563
-
564
- // Create a map of qualifier name to qualifier type for quick lookup
565
- const qualifierTypeMap: Record<string, Config.Model.ISystemConfiguration['qualifierTypes'][0]> = {};
566
-
567
- // First, create a mapping of qualifiers to their types
568
- config.qualifiers.forEach((qualifier) => {
569
- const qualifierType = config.qualifierTypes.find((qt) => qt.name === qualifier.typeName);
570
- if (qualifierType) {
571
- qualifierTypeMap[qualifier.name] = qualifierType;
572
- }
573
- });
574
-
575
- // Build info for each qualifier
576
- availableQualifiers.forEach((qualifierName) => {
577
- const qualifierType = qualifierTypeMap[qualifierName];
578
- if (qualifierType) {
579
- info[qualifierName] = { type: qualifierType };
580
-
581
- // Extract enumerated values for literal types
582
- if (
583
- qualifierType.systemType === 'literal' &&
584
- qualifierType.configuration &&
585
- typeof qualifierType.configuration === 'object' &&
586
- 'enumeratedValues' in qualifierType.configuration &&
587
- Array.isArray(qualifierType.configuration.enumeratedValues)
588
- ) {
589
- info[qualifierName].enumeratedValues = qualifierType.configuration.enumeratedValues;
590
- }
591
- }
592
- });
593
-
594
- return info;
595
- }, [availableQualifiers, resourceState.activeConfiguration]);
596
-
597
- // Initialize context with smart default values based on qualifier types
598
- const defaultContextValues = useMemo(() => {
599
- const defaults: ContextState = {};
600
-
601
- // Compute intelligent defaults based on qualifier types
602
- availableQualifiers.forEach((qualifierName) => {
603
- const typeInfo = qualifierTypeInfo[qualifierName];
604
-
605
- if (!typeInfo) {
606
- // No type info available, use empty string
607
- defaults[qualifierName] = '';
608
- return;
609
- }
610
-
611
- const qualifierType = typeInfo.type;
612
-
613
- // Compute default based on system type
614
- switch (qualifierType.systemType) {
615
- case 'language':
616
- // Language qualifiers default to en-US
617
- defaults[qualifierName] = 'en-US';
618
- break;
619
-
620
- case 'territory':
621
- // Territory qualifiers: use first allowed territory if constrained, otherwise US
622
- if (
623
- qualifierType.configuration &&
624
- typeof qualifierType.configuration === 'object' &&
625
- 'allowedTerritories' in qualifierType.configuration &&
626
- Array.isArray(qualifierType.configuration.allowedTerritories) &&
627
- qualifierType.configuration.allowedTerritories.length > 0
628
- ) {
629
- defaults[qualifierName] = qualifierType.configuration.allowedTerritories[0];
630
- } else {
631
- defaults[qualifierName] = 'US';
632
- }
633
- break;
634
-
635
- case 'literal':
636
- // Literal qualifiers: use first enumerated value if constrained, otherwise "unknown"
637
- if (typeInfo.enumeratedValues && typeInfo.enumeratedValues.length > 0) {
638
- defaults[qualifierName] = typeInfo.enumeratedValues[0];
639
- } else {
640
- defaults[qualifierName] = 'unknown';
641
- }
642
- break;
643
-
644
- default:
645
- defaults[qualifierName] = '';
646
- }
647
- });
648
-
649
- return defaults;
650
- }, [availableQualifiers, qualifierTypeInfo]);
651
-
652
- // Context state
653
- const [contextValues, setContextValues] = useState<ContextState>({});
654
- const [pendingContextValues, setPendingContextValues] = useState<ContextState>({});
655
-
656
- // Update context state when default values change
657
- React.useEffect(() => {
658
- setContextValues(defaultContextValues);
659
- setPendingContextValues(defaultContextValues);
660
- }, [defaultContextValues]);
661
-
662
- // Selection state
663
- const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);
664
- const [viewMode, setViewMode] = useState<ViewMode>('best');
665
-
666
- // Resolution state
667
- const [currentResolver, setCurrentResolver] = useState<Runtime.ResourceResolver | null>(null);
668
- const [resolutionResult, setResolutionResult] = useState<ResolutionResult | null>(null);
669
-
670
- // Track if we've auto-applied defaults for the current configuration
671
- const [hasAutoApplied, setHasAutoApplied] = useState(false);
672
-
673
- // Cache metrics state
674
- const [cacheMetrics, setCacheMetrics] =
675
- useState<Runtime.ResourceResolverCacheMetricsListener<Runtime.AggregateCacheMetrics> | null>(null);
676
-
677
- // Available resources
678
- const availableResources = useMemo(() => {
679
- if (!activeProcessedResources?.summary?.resourceIds) {
680
- return [];
681
- }
682
- return activeProcessedResources.summary.resourceIds.sort();
683
- }, [activeProcessedResources?.summary?.resourceIds]);
684
-
685
- // Check if there are pending context changes
686
- const hasPendingChanges = useMemo(() => {
687
- return JSON.stringify(contextValues) !== JSON.stringify(pendingContextValues);
688
- }, [contextValues, pendingContextValues]);
689
-
690
- // Helper functions for condition set keys (similar to CompiledBrowser)
691
- const getConditionKey = useCallback((condition: any, compiledCollection: any): string => {
692
- try {
693
- // Use metadata if available
694
- if (condition.metadata?.key) {
695
- return condition.metadata.key;
696
- }
697
-
698
- // Fall back to manual construction
699
- const qualifier = compiledCollection.qualifiers[condition.qualifierIndex];
700
- if (!qualifier) return `unknown-qualifier`;
701
-
702
- // Create a meaningful key like "language=en-US" or "territory=US"
703
- const key = `${qualifier.name}=${condition.value}`;
704
- return key;
705
- } catch (error) {
706
- return `condition-${condition.qualifierIndex}`;
707
- }
708
- }, []);
709
-
710
- const getConditionSetKey = useCallback(
711
- (conditionSet: any, conditionSetIndex: number, compiledCollection: any): string => {
712
- try {
713
- // Use metadata if available
714
- if (conditionSet.metadata?.key) {
715
- return conditionSet.metadata.key;
716
- }
717
-
718
- if (conditionSetIndex === 0) {
719
- return 'unconditional';
720
- }
721
-
722
- // Fall back to manual construction
723
- if (!conditionSet.conditions || conditionSet.conditions.length === 0) {
724
- return `condition-set-${conditionSetIndex}`;
725
- }
726
-
727
- // Build a composite key from all conditions in the set
728
- const conditionKeys = conditionSet.conditions.map((conditionIndex: number) => {
729
- const condition = compiledCollection.conditions[conditionIndex];
730
- if (!condition) return `unknown-${conditionIndex}`;
731
- return getConditionKey(condition, compiledCollection);
732
- });
733
-
734
- return conditionKeys.join(',');
735
- } catch (error) {
736
- return `condition-set-${conditionSetIndex}`;
737
- }
738
- },
739
- [getConditionKey]
740
- );
741
-
742
- const getCandidateConditionSetKey = useCallback(
743
- (candidateIndex: number, resource: any, compiledCollection: any): string | null => {
744
- try {
745
- // Get the decision for this resource
746
- const decision = compiledCollection.decisions[resource.decision];
747
- if (!decision || !decision.conditionSets) {
748
- return null;
749
- }
750
-
751
- // Map candidate index to condition set index
752
- // Assuming candidates correspond to condition sets in order
753
- if (candidateIndex >= decision.conditionSets.length) {
754
- return null;
755
- }
756
-
757
- const conditionSetIndex = decision.conditionSets[candidateIndex];
758
- const conditionSet = compiledCollection.conditionSets[conditionSetIndex];
759
- if (!conditionSet) {
760
- return null;
761
- }
762
-
763
- return getConditionSetKey(conditionSet, conditionSetIndex, compiledCollection);
764
- } catch (error) {
765
- return null;
766
- }
767
- },
768
- [getConditionSetKey]
769
- );
770
-
771
- // Apply context changes
772
- const applyContext = useCallback(() => {
773
- if (!activeProcessedResources?.system) {
774
- onMessage?.('error', 'No resources loaded');
775
- return;
776
- }
777
-
778
- // Create new context provider
779
- const contextResult = createSimpleContext(pendingContextValues, activeProcessedResources.system);
780
- if (contextResult.isFailure()) {
781
- onMessage?.('error', `Failed to create context: ${contextResult.message}`);
782
- return;
783
- }
784
-
785
- // Reset existing cache and metrics when context changes
786
- if (currentResolver && cacheMetrics) {
787
- currentResolver.clearConditionCache();
788
- cacheMetrics.reset();
789
- }
790
-
791
- // Create cache metrics listener
792
- const metricsListener = new Runtime.ResourceResolverCacheMetricsListener(
793
- () => new Runtime.AggregateCacheMetrics()
794
- );
795
- setCacheMetrics(metricsListener);
796
-
797
- // Create new resource resolver
798
- const resolverResult = Runtime.ResourceResolver.create({
799
- resourceManager: activeProcessedResources.system.resourceManager,
800
- qualifierTypes: activeProcessedResources.system.qualifierTypes,
801
- contextQualifierProvider: contextResult.value,
802
- listener: metricsListener
803
- });
804
-
805
- if (resolverResult.isFailure()) {
806
- onMessage?.('error', `Failed to create resolver: ${resolverResult.message}`);
807
- return;
808
- }
809
-
810
- // Update state
811
- setContextValues({ ...pendingContextValues });
812
- setCurrentResolver(resolverResult.value);
813
-
814
- // If a resource is selected, resolve it with the new context
815
- if (selectedResourceId) {
816
- resolveSelectedResource(resolverResult.value, selectedResourceId);
817
- }
818
-
819
- onMessage?.('success', 'Context applied successfully');
820
- }, [pendingContextValues, activeProcessedResources?.system, selectedResourceId, onMessage]);
821
-
822
- // Helper function to evaluate conditions for a candidate
823
- const evaluateConditionsForCandidate = useCallback(
824
- (
825
- resolver: Runtime.ResourceResolver,
826
- candidateIndex: number,
827
- compiledResource: any,
828
- compiledCollection: any
829
- ): ConditionEvaluationResult[] => {
830
- try {
831
- const decision = compiledCollection.decisions[compiledResource.decision];
832
- if (!decision || !decision.conditionSets || candidateIndex >= decision.conditionSets.length) {
833
- return [];
834
- }
835
-
836
- const conditionSetIndex = decision.conditionSets[candidateIndex];
837
- const conditionSet = compiledCollection.conditionSets[conditionSetIndex];
838
- if (!conditionSet || !conditionSet.conditions) {
839
- return [];
840
- }
841
-
842
- const evaluations: ConditionEvaluationResult[] = [];
843
-
844
- for (const conditionIndex of conditionSet.conditions) {
845
- const condition = compiledCollection.conditions[conditionIndex];
846
- if (!condition) continue;
847
-
848
- const qualifier = compiledCollection.qualifiers[condition.qualifierIndex];
849
- if (!qualifier) continue;
850
-
851
- // Get the qualifier value from context
852
- const qualifierValueResult = resolver.contextQualifierProvider.get(qualifier);
853
- const qualifierValue = qualifierValueResult.isSuccess() ? qualifierValueResult.value : null;
854
-
855
- // Get the cached condition result from resolver
856
- const cachedResult = resolver.conditionCache[conditionIndex] as IConditionMatchResult;
857
- const score = cachedResult?.score || 0;
858
- const matchType = cachedResult?.matchType || 'noMatch';
859
- const matched = matchType !== 'noMatch';
860
-
861
- evaluations.push({
862
- qualifierName: qualifier.name,
863
- qualifierValue,
864
- conditionValue: condition.value,
865
- operator: condition.operator || 'eq',
866
- score,
867
- matched,
868
- matchType,
869
- scoreAsDefault: condition.scoreAsDefault,
870
- conditionIndex
871
- });
872
- }
873
-
874
- return evaluations;
875
- } catch (error) {
876
- console.warn('Error evaluating conditions for candidate:', error);
877
- return [];
878
- }
879
- },
880
- []
881
- );
882
-
883
- // Resolve the currently selected resource
884
- const resolveSelectedResource = useCallback(
885
- (resolver: Runtime.ResourceResolver, resourceId: string) => {
886
- if (
887
- !activeProcessedResources?.system.resourceManager ||
888
- !activeProcessedResources?.compiledCollection
889
- ) {
890
- setResolutionResult({ success: false, error: 'No resource manager available' });
891
- return;
892
- }
893
-
894
- const resourceResult = activeProcessedResources.system.resourceManager.getBuiltResource(resourceId);
895
- if (resourceResult.isFailure()) {
896
- setResolutionResult({
897
- success: false,
898
- error: `Failed to get resource: ${resourceResult.message}`
899
- });
900
- return;
901
- }
902
-
903
- const resource = resourceResult.value;
904
- const compiledCollection = activeProcessedResources.compiledCollection;
905
-
906
- // Find the compiled resource for condition set keys
907
- const compiledResource = compiledCollection.resources.find((r) => r.id === resourceId);
908
- if (!compiledResource) {
909
- setResolutionResult({
910
- success: false,
911
- error: 'Failed to find compiled resource'
912
- });
913
- return;
914
- }
915
-
916
- // Resolve best candidate
917
- const bestResult = resolver.resolveResource(resource);
918
-
919
- // Resolve all candidates
920
- const allResult = resolver.resolveAllResourceCandidates(resource);
921
-
922
- // Resolve composed value
923
- const composedResult = resolver.resolveComposedResourceValue(resource);
924
-
925
- // Get decision resolution result to understand which candidates are regular vs default matches
926
- const decisionResult = resolver.resolveDecision(resource.decision.baseDecision);
927
- const decision = decisionResult.isSuccess() ? decisionResult.value : null;
928
-
929
- // Build detailed candidate information, preserving priority order for matched candidates
930
- const candidateDetails: CandidateInfo[] = [];
931
- const matchedCandidates = allResult.isSuccess() ? allResult.value : [];
932
-
933
- // Create lookup sets for regular and default matches
934
- const regularMatchIndices = new Set(decision?.success ? decision.instanceIndices : []);
935
- const defaultMatchIndices = new Set(decision?.success ? decision.defaultInstanceIndices : []);
936
-
937
- // First, add matched candidates in priority order (regular matches first, then defaults)
938
- matchedCandidates.forEach((matchedCandidate) => {
939
- // Find the index by comparing candidate objects
940
- const index = resource.candidates.findIndex((candidate) => candidate === matchedCandidate);
941
- if (index !== -1) {
942
- const conditionSetKey = getCandidateConditionSetKey(index, compiledResource, compiledCollection);
943
- const conditionEvaluations = evaluateConditionsForCandidate(
944
- resolver,
945
- index,
946
- compiledResource,
947
- compiledCollection
948
- );
949
-
950
- const isDefaultMatch = defaultMatchIndices.has(index);
951
- const isRegularMatch = regularMatchIndices.has(index);
952
-
953
- // Determine overall match type for this candidate
954
- const candidateMatchType: ConditionMatchType = isRegularMatch
955
- ? 'match'
956
- : isDefaultMatch
957
- ? 'matchAsDefault'
958
- : 'noMatch';
959
-
960
- candidateDetails.push({
961
- candidate: resource.candidates[index],
962
- conditionSetKey,
963
- candidateIndex: index,
964
- matched: true,
965
- matchType: candidateMatchType,
966
- isDefaultMatch,
967
- conditionEvaluations
968
- });
969
- }
970
- });
971
-
972
- // Then, add non-matching candidates in their original order
973
- resource.candidates.forEach((candidate, index) => {
974
- const isMatched = matchedCandidates.some((mc) => mc === candidate);
975
- if (!isMatched) {
976
- const conditionSetKey = getCandidateConditionSetKey(index, compiledResource, compiledCollection);
977
- const conditionEvaluations = evaluateConditionsForCandidate(
978
- resolver,
979
- index,
980
- compiledResource,
981
- compiledCollection
982
- );
983
-
984
- candidateDetails.push({
985
- candidate,
986
- conditionSetKey,
987
- candidateIndex: index,
988
- matched: false,
989
- matchType: 'noMatch',
990
- isDefaultMatch: false,
991
- conditionEvaluations
992
- });
993
- }
994
- });
995
-
996
- if (bestResult.isFailure() && allResult.isFailure()) {
997
- setResolutionResult({
998
- success: false,
999
- resource,
1000
- candidateDetails,
1001
- error: `Resolution failed: ${bestResult.message}`
1002
- });
1003
- return;
1004
- }
1005
-
1006
- setResolutionResult({
1007
- success: true,
1008
- resource,
1009
- bestCandidate: bestResult.isSuccess() ? bestResult.value : undefined,
1010
- allCandidates: allResult.isSuccess() ? allResult.value : undefined,
1011
- candidateDetails,
1012
- composedValue: composedResult.isSuccess() ? composedResult.value : undefined,
1013
- error: bestResult.isFailure() ? bestResult.message : undefined
1014
- });
1015
- },
1016
- [
1017
- activeProcessedResources?.system.resourceManager,
1018
- activeProcessedResources?.compiledCollection,
1019
- getCandidateConditionSetKey,
1020
- evaluateConditionsForCandidate
1021
- ]
1022
- );
1023
-
1024
- // Handle resource selection
1025
- const handleResourceSelect = useCallback(
1026
- (resourceId: string) => {
1027
- setSelectedResourceId(resourceId);
1028
- setResolutionResult(null);
1029
-
1030
- if (currentResolver) {
1031
- resolveSelectedResource(currentResolver, resourceId);
1032
- }
1033
-
1034
- onMessage?.('info', `Selected resource: ${resourceId}`);
1035
- },
1036
- [currentResolver, resolveSelectedResource, onMessage]
1037
- );
1038
-
1039
- // Handle context value change
1040
- const handleContextChange = useCallback((qualifierName: string, value: string) => {
1041
- setPendingContextValues((prev) => ({
1042
- ...prev,
1043
- [qualifierName]: value
1044
- }));
1045
- }, []);
1046
-
1047
- // Handle cache reset
1048
- const handleCacheReset = useCallback(() => {
1049
- if (currentResolver && cacheMetrics) {
1050
- currentResolver.clearConditionCache();
1051
- cacheMetrics.reset();
1052
- onMessage?.('info', 'Cache cleared and metrics reset');
1053
- }
1054
- }, [currentResolver, cacheMetrics, onMessage]);
1055
-
1056
- // Reset auto-apply flag when defaults change
1057
- React.useEffect(() => {
1058
- setHasAutoApplied(false);
1059
- }, [defaultContextValues]);
1060
-
1061
- // Auto-apply default context when resources are loaded or defaults change
1062
- React.useEffect(() => {
1063
- if (activeProcessedResources?.system && Object.keys(defaultContextValues).length > 0 && !hasAutoApplied) {
1064
- // Apply the default context automatically
1065
- applyContext();
1066
- setHasAutoApplied(true);
1067
- }
1068
- }, [activeProcessedResources?.system, defaultContextValues, hasAutoApplied, applyContext]);
1069
-
1070
- if (!resourceState.processedResources) {
1071
- return (
1072
- <div className="p-6">
1073
- <div className="flex items-center space-x-3 mb-6">
1074
- <MagnifyingGlassIcon className="h-8 w-8 text-blue-600" />
1075
- <h2 className="text-2xl font-bold text-gray-900">Resolution Viewer</h2>
1076
- </div>
1077
-
1078
- <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
1079
- <div className="max-w-2xl mx-auto">
1080
- <h3 className="text-xl font-semibold text-gray-900 mb-4">No Resources Loaded</h3>
1081
- <p className="text-gray-600 mb-6">
1082
- Import resources to test resource resolution with different contexts.
1083
- </p>
1084
- <div className="bg-blue-50 rounded-lg p-4">
1085
- <p className="text-sm text-blue-800">
1086
- <strong>Tip:</strong> Use the Import Tool to load resources, then return here to test how they
1087
- resolve with different qualifier contexts.
1088
- </p>
1089
- </div>
1090
- </div>
1091
- </div>
1092
- </div>
1093
- );
1094
- }
1095
-
1096
- return (
1097
- <div className="p-6">
1098
- <div className="flex items-center space-x-3 mb-6">
1099
- <MagnifyingGlassIcon className="h-8 w-8 text-blue-600" />
1100
- <h2 className="text-2xl font-bold text-gray-900">Resolution Viewer</h2>
1101
- {isFilteringActive && (
1102
- <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
1103
- Filtered
1104
- </span>
1105
- )}
1106
- </div>
1107
-
1108
- <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
1109
- {/* Context Input Panel */}
1110
- <div className="mb-6">
1111
- <h3 className="text-lg font-semibold text-gray-900 mb-4">Context Configuration</h3>
1112
- <div className="bg-gray-50 rounded-lg p-4">
1113
- <div className="mb-4">
1114
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
1115
- {availableQualifiers.map((qualifierName) => {
1116
- const typeInfo = qualifierTypeInfo[qualifierName];
1117
- const hasEnumeratedValues =
1118
- typeInfo?.enumeratedValues && typeInfo.enumeratedValues.length > 0;
1119
-
1120
- return (
1121
- <div key={qualifierName} className="bg-white rounded border border-gray-200 p-2">
1122
- <div className="flex items-center gap-2">
1123
- <label className="text-sm font-medium text-gray-700 min-w-0 flex-shrink-0">
1124
- {qualifierName}:
1125
- </label>
1126
- {hasEnumeratedValues ? (
1127
- <select
1128
- value={pendingContextValues[qualifierName] || ''}
1129
- onChange={(e) => handleContextChange(qualifierName, e.target.value)}
1130
- className="flex-1 px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent text-sm min-w-0 bg-white"
1131
- >
1132
- <option value="" disabled>
1133
- Select {qualifierName}...
1134
- </option>
1135
- {typeInfo!.enumeratedValues!.map((value) => (
1136
- <option key={value} value={value}>
1137
- {value}
1138
- </option>
1139
- ))}
1140
- </select>
1141
- ) : (
1142
- <input
1143
- type="text"
1144
- value={pendingContextValues[qualifierName] || ''}
1145
- onChange={(e) => handleContextChange(qualifierName, e.target.value)}
1146
- className="flex-1 px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent text-sm min-w-0"
1147
- placeholder={`Enter ${qualifierName} value`}
1148
- />
1149
- )}
1150
- </div>
1151
- </div>
1152
- );
1153
- })}
1154
- </div>
1155
- </div>
1156
- <div className="flex items-center justify-between">
1157
- <div className="text-sm text-gray-600">
1158
- Current:{' '}
1159
- {Object.entries(contextValues)
1160
- .map(([key, value]) => `${key}=${value}`)
1161
- .join(', ')}
1162
- </div>
1163
- <button
1164
- onClick={applyContext}
1165
- disabled={!hasPendingChanges}
1166
- className={`px-4 py-2 rounded-md text-sm font-medium ${
1167
- hasPendingChanges
1168
- ? 'bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500'
1169
- : 'bg-gray-300 text-gray-500 cursor-not-allowed'
1170
- }`}
1171
- >
1172
- {hasPendingChanges ? 'Apply Changes' : currentResolver ? 'Context Applied' : 'Apply Context'}
1173
- </button>
1174
- </div>
1175
- </div>
1176
- </div>
1177
-
1178
- {/* Cache Dashboard */}
1179
- <CacheDashboard cacheMetrics={cacheMetrics} resolver={currentResolver} onReset={handleCacheReset} />
1180
-
1181
- {/* Main Browser/Details Layout */}
1182
- <div className="flex flex-col lg:flex-row gap-6 h-[600px]">
1183
- {/* Left side: Resource Selection and Cache Contents */}
1184
- <div className="lg:w-1/2 flex flex-col">
1185
- <div className="flex items-center justify-between mb-4">
1186
- <h3 className="text-lg font-semibold text-gray-900">Resources</h3>
1187
- <div className="text-sm text-gray-500">{availableResources.length} available</div>
1188
- </div>
1189
-
1190
- <div className="flex-1 overflow-y-auto border border-gray-200 rounded-lg bg-gray-50 mb-4">
1191
- {availableResources.map((resourceId) => (
1192
- <div
1193
- key={resourceId}
1194
- className={`flex items-center px-3 py-2 cursor-pointer hover:bg-gray-100 ${
1195
- selectedResourceId === resourceId ? 'bg-blue-50 border-r-2 border-blue-500' : ''
1196
- }`}
1197
- onClick={() => handleResourceSelect(resourceId)}
1198
- >
1199
- <DocumentTextIcon className="w-4 h-4 mr-2 text-green-500" />
1200
- <span
1201
- className={`text-sm ${
1202
- selectedResourceId === resourceId ? 'font-medium text-blue-900' : 'text-gray-700'
1203
- }`}
1204
- >
1205
- {resourceId}
1206
- </span>
1207
- </div>
1208
- ))}
1209
- </div>
1210
-
1211
- {/* Cache Contents Section */}
1212
- <CacheContentsDisplay resolver={currentResolver} />
1213
- </div>
1214
-
1215
- {/* Right side: Resolution Results */}
1216
- <div className="lg:w-1/2 flex flex-col">
1217
- <div className="flex items-center justify-between mb-4">
1218
- <h3 className="text-lg font-semibold text-gray-900">Results</h3>
1219
- {selectedResourceId && (
1220
- <div className="flex space-x-2">
1221
- <button
1222
- onClick={() => setViewMode('best')}
1223
- className={`px-3 py-1 text-xs rounded ${
1224
- viewMode === 'best' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'
1225
- }`}
1226
- >
1227
- Best
1228
- </button>
1229
- <button
1230
- onClick={() => setViewMode('all')}
1231
- className={`px-3 py-1 text-xs rounded ${
1232
- viewMode === 'all' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'
1233
- }`}
1234
- >
1235
- All
1236
- </button>
1237
- <button
1238
- onClick={() => setViewMode('composed')}
1239
- className={`px-3 py-1 text-xs rounded ${
1240
- viewMode === 'composed' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'
1241
- }`}
1242
- >
1243
- Composed
1244
- </button>
1245
- <button
1246
- onClick={() => setViewMode('raw')}
1247
- className={`px-3 py-1 text-xs rounded ${
1248
- viewMode === 'raw' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'
1249
- }`}
1250
- >
1251
- Raw
1252
- </button>
1253
- </div>
1254
- )}
1255
- </div>
1256
-
1257
- <div className="flex-1 overflow-y-auto border border-gray-200 rounded-lg p-4 bg-gray-50">
1258
- {!selectedResourceId ? (
1259
- <div className="flex items-center justify-center h-full">
1260
- <div className="text-center">
1261
- <CubeIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
1262
- <p className="text-gray-500">Select a resource to view resolution results</p>
1263
- </div>
1264
- </div>
1265
- ) : !currentResolver ? (
1266
- <div className="text-center text-gray-500">
1267
- <p>Apply a context to resolve resources</p>
1268
- </div>
1269
- ) : !resolutionResult ? (
1270
- <div className="text-center text-gray-500">
1271
- <p>Resolving...</p>
1272
- </div>
1273
- ) : (
1274
- <ResolutionResults
1275
- result={resolutionResult}
1276
- viewMode={viewMode}
1277
- contextValues={contextValues}
1278
- />
1279
- )}
1280
- </div>
1281
- </div>
1282
- </div>
1283
- </div>
1284
- </div>
1285
- );
1286
- };
1287
-
1288
- // Resolution Results Component
1289
- interface ResolutionResultsProps {
1290
- result: ResolutionResult;
1291
- viewMode: ViewMode;
1292
- contextValues: ContextState;
1293
- }
1294
-
1295
- const ResolutionResults: React.FC<ResolutionResultsProps> = ({ result, viewMode, contextValues }) => {
1296
- if (!result.success) {
1297
- return (
1298
- <div className="bg-red-50 border border-red-200 rounded-lg p-4">
1299
- <h4 className="font-medium text-red-800 mb-2">Resolution Failed</h4>
1300
- <p className="text-sm text-red-600">{result.error}</p>
1301
- </div>
1302
- );
1303
- }
1304
-
1305
- if (viewMode === 'raw') {
1306
- return (
1307
- <div className="space-y-4">
1308
- <div>
1309
- <h4 className="font-medium text-gray-800 mb-2">Raw Resolution Data</h4>
1310
- <pre className="text-xs bg-white p-3 rounded border overflow-x-auto">
1311
- {JSON.stringify(
1312
- {
1313
- context: contextValues,
1314
- resource: result.resource
1315
- ? {
1316
- id: result.resource.id,
1317
- candidateCount: result.resource.candidates.length
1318
- }
1319
- : null,
1320
- bestCandidate: result.bestCandidate?.json,
1321
- allCandidates: result.allCandidates?.map((c) => c.json),
1322
- composedValue: result.composedValue,
1323
- error: result.error
1324
- },
1325
- null,
1326
- 2
1327
- )}
1328
- </pre>
1329
- </div>
1330
- </div>
1331
- );
1332
- }
1333
-
1334
- if (viewMode === 'composed') {
1335
- return (
1336
- <div className="space-y-4">
1337
- <div>
1338
- <h4 className="font-medium text-gray-800 mb-2">Composed Resource Value</h4>
1339
- {result.composedValue ? (
1340
- <div className="bg-white p-3 rounded border">
1341
- <div className="text-sm font-medium text-gray-700 mb-2">
1342
- Composed value from all matching candidates
1343
- </div>
1344
- <pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto">
1345
- {JSON.stringify(result.composedValue, null, 2)}
1346
- </pre>
1347
- </div>
1348
- ) : (
1349
- <div className="bg-yellow-50 border border-yellow-200 rounded p-3">
1350
- <p className="text-sm text-yellow-800">No composed value available for the current context.</p>
1351
- {result.error && <p className="text-xs text-yellow-600 mt-1">{result.error}</p>}
1352
- </div>
1353
- )}
1354
- </div>
1355
-
1356
- {result.resource && (
1357
- <div>
1358
- <h4 className="font-medium text-gray-800 mb-2">Resource Info</h4>
1359
- <div className="bg-white p-3 rounded border text-sm">
1360
- <div>
1361
- <strong>ID:</strong> {result.resource.id}
1362
- </div>
1363
- <div>
1364
- <strong>Type:</strong> {result.resource.resourceType.key}
1365
- </div>
1366
- <div>
1367
- <strong>Total Candidates:</strong> {result.resource.candidates.length}
1368
- </div>
1369
- </div>
1370
- </div>
1371
- )}
1372
- </div>
1373
- );
1374
- }
1375
-
1376
- if (viewMode === 'all') {
1377
- const regularMatchingCandidates =
1378
- result.candidateDetails?.filter((c) => c.matched && !c.isDefaultMatch) || [];
1379
- const defaultMatchingCandidates =
1380
- result.candidateDetails?.filter((c) => c.matched && c.isDefaultMatch) || [];
1381
- const nonMatchingCandidates = result.candidateDetails?.filter((c) => !c.matched) || [];
1382
-
1383
- const getMatchTypeColor = (type: ConditionMatchType) => {
1384
- switch (type) {
1385
- case 'match':
1386
- return 'bg-green-100 text-green-800';
1387
- case 'matchAsDefault':
1388
- return 'bg-amber-100 text-amber-800';
1389
- case 'noMatch':
1390
- return 'bg-red-100 text-red-800';
1391
- default:
1392
- return 'bg-gray-100 text-gray-800';
1393
- }
1394
- };
1395
-
1396
- const getMatchTypeIcon = (type: ConditionMatchType) => {
1397
- switch (type) {
1398
- case 'match':
1399
- return '✓';
1400
- case 'matchAsDefault':
1401
- return '≈';
1402
- case 'noMatch':
1403
- return '✗';
1404
- default:
1405
- return '?';
1406
- }
1407
- };
1408
-
1409
- return (
1410
- <div className="space-y-4">
1411
- {/* Regular Matching Candidates */}
1412
- {regularMatchingCandidates.length > 0 && (
1413
- <div>
1414
- <h4 className="font-medium text-gray-800 mb-2">Regular Matches</h4>
1415
- <div className="space-y-2">
1416
- {regularMatchingCandidates.map((candidateInfo, index) => (
1417
- <div
1418
- key={candidateInfo.candidateIndex}
1419
- className="bg-white p-3 rounded border border-green-200"
1420
- >
1421
- <div className="flex items-center justify-between mb-2">
1422
- <div className="text-sm font-medium text-gray-700 flex items-center space-x-2">
1423
- <span>
1424
- Candidate {candidateInfo.candidateIndex + 1} {index === 0 ? '(Best Match)' : ''}
1425
- </span>
1426
- {candidateInfo.conditionSetKey && (
1427
- <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
1428
- {candidateInfo.conditionSetKey}
1429
- </span>
1430
- )}
1431
- <span
1432
- className={`px-2 py-1 rounded text-xs ${getMatchTypeColor(candidateInfo.matchType)}`}
1433
- >
1434
- {getMatchTypeIcon(candidateInfo.matchType)} {candidateInfo.matchType}
1435
- </span>
1436
- </div>
1437
- <div className="flex items-center space-x-2 text-xs">
1438
- {candidateInfo.candidate.isPartial && (
1439
- <span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Partial</span>
1440
- )}
1441
- {candidateInfo.candidate.mergeMethod && (
1442
- <span className="bg-gray-100 text-gray-700 px-2 py-1 rounded">
1443
- {candidateInfo.candidate.mergeMethod}
1444
- </span>
1445
- )}
1446
- </div>
1447
- </div>
1448
- <pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto mt-2">
1449
- {JSON.stringify(candidateInfo.candidate.json, null, 2)}
1450
- </pre>
1451
- {candidateInfo.conditionEvaluations && (
1452
- <ConditionEvaluationDisplay evaluations={candidateInfo.conditionEvaluations} />
1453
- )}
1454
- </div>
1455
- ))}
1456
- </div>
1457
- </div>
1458
- )}
1459
-
1460
- {/* Default Matching Candidates */}
1461
- {defaultMatchingCandidates.length > 0 && (
1462
- <div>
1463
- <h4 className="font-medium text-gray-800 mb-2">Default Matches</h4>
1464
- <div className="space-y-2">
1465
- {defaultMatchingCandidates.map((candidateInfo, index) => (
1466
- <div
1467
- key={candidateInfo.candidateIndex}
1468
- className="bg-white p-3 rounded border border-amber-200"
1469
- >
1470
- <div className="flex items-center justify-between mb-2">
1471
- <div className="text-sm font-medium text-gray-700 flex items-center space-x-2">
1472
- <span>Candidate {candidateInfo.candidateIndex + 1}</span>
1473
- {candidateInfo.conditionSetKey && (
1474
- <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
1475
- {candidateInfo.conditionSetKey}
1476
- </span>
1477
- )}
1478
- <span
1479
- className={`px-2 py-1 rounded text-xs ${getMatchTypeColor(candidateInfo.matchType)}`}
1480
- >
1481
- {getMatchTypeIcon(candidateInfo.matchType)} {candidateInfo.matchType}
1482
- </span>
1483
- </div>
1484
- <div className="flex items-center space-x-2 text-xs">
1485
- {candidateInfo.candidate.isPartial && (
1486
- <span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Partial</span>
1487
- )}
1488
- {candidateInfo.candidate.mergeMethod && (
1489
- <span className="bg-gray-100 text-gray-700 px-2 py-1 rounded">
1490
- {candidateInfo.candidate.mergeMethod}
1491
- </span>
1492
- )}
1493
- </div>
1494
- </div>
1495
- <pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto mt-2">
1496
- {JSON.stringify(candidateInfo.candidate.json, null, 2)}
1497
- </pre>
1498
- {candidateInfo.conditionEvaluations && (
1499
- <ConditionEvaluationDisplay evaluations={candidateInfo.conditionEvaluations} />
1500
- )}
1501
- </div>
1502
- ))}
1503
- </div>
1504
- </div>
1505
- )}
1506
-
1507
- {/* Show message when no matches */}
1508
- {regularMatchingCandidates.length === 0 && defaultMatchingCandidates.length === 0 && (
1509
- <div>
1510
- <h4 className="font-medium text-gray-800 mb-2">Matching Candidates</h4>
1511
- <p className="text-sm text-gray-600">No candidates matched the current context.</p>
1512
- </div>
1513
- )}
1514
-
1515
- {/* Non-matching Candidates */}
1516
-
1517
- {nonMatchingCandidates.length > 0 && (
1518
- <div>
1519
- <h4 className="font-medium text-gray-500 mb-2">Non-matching Candidates</h4>
1520
- <div className="space-y-2">
1521
- {nonMatchingCandidates.map((candidateInfo) => (
1522
- <div
1523
- key={candidateInfo.candidateIndex}
1524
- className="bg-gray-50 p-3 rounded border border-gray-200 opacity-75"
1525
- >
1526
- <div className="flex items-center justify-between mb-2">
1527
- <div className="text-sm font-medium text-gray-500">
1528
- Candidate {candidateInfo.candidateIndex + 1}
1529
- {candidateInfo.conditionSetKey && (
1530
- <span className="ml-2 text-xs bg-gray-200 text-gray-600 px-2 py-1 rounded">
1531
- {candidateInfo.conditionSetKey}
1532
- </span>
1533
- )}
1534
- </div>
1535
- <div className="flex items-center space-x-2 text-xs">
1536
- {candidateInfo.candidate.isPartial && (
1537
- <span className="bg-yellow-200 text-yellow-700 px-2 py-1 rounded">Partial</span>
1538
- )}
1539
- {candidateInfo.candidate.mergeMethod && (
1540
- <span className="bg-gray-200 text-gray-600 px-2 py-1 rounded">
1541
- {candidateInfo.candidate.mergeMethod}
1542
- </span>
1543
- )}
1544
- </div>
1545
- </div>
1546
- <pre className="text-xs bg-gray-100 p-2 rounded overflow-x-auto text-gray-600">
1547
- {JSON.stringify(candidateInfo.candidate.json, null, 2)}
1548
- </pre>
1549
- {candidateInfo.conditionEvaluations && (
1550
- <ConditionEvaluationDisplay evaluations={candidateInfo.conditionEvaluations} />
1551
- )}
1552
- </div>
1553
- ))}
1554
- </div>
1555
- </div>
1556
- )}
1557
- </div>
1558
- );
1559
- }
1560
-
1561
- // Best candidate view
1562
- const bestCandidateInfo = result.candidateDetails?.find((c) => c.matched);
1563
-
1564
- const getMatchTypeColor = (type: ConditionMatchType) => {
1565
- switch (type) {
1566
- case 'match':
1567
- return 'bg-green-100 text-green-800';
1568
- case 'matchAsDefault':
1569
- return 'bg-amber-100 text-amber-800';
1570
- case 'noMatch':
1571
- return 'bg-red-100 text-red-800';
1572
- default:
1573
- return 'bg-gray-100 text-gray-800';
1574
- }
1575
- };
1576
-
1577
- const getMatchTypeIcon = (type: ConditionMatchType) => {
1578
- switch (type) {
1579
- case 'match':
1580
- return '✓';
1581
- case 'matchAsDefault':
1582
- return '≈';
1583
- case 'noMatch':
1584
- return '✗';
1585
- default:
1586
- return '?';
1587
- }
1588
- };
1589
-
1590
- const getMatchDescription = (candidateInfo: CandidateInfo | undefined) => {
1591
- if (!candidateInfo) return 'Selected candidate for current context';
1592
-
1593
- if (candidateInfo.isDefaultMatch) {
1594
- return 'Selected candidate (matched via default values)';
1595
- } else {
1596
- return 'Selected candidate (direct match)';
1597
- }
1598
- };
1599
-
1600
- return (
1601
- <div className="space-y-4">
1602
- <div>
1603
- <h4 className="font-medium text-gray-800 mb-2">Best Match</h4>
1604
- {result.bestCandidate ? (
1605
- <div
1606
- className={`bg-white p-3 rounded border ${
1607
- bestCandidateInfo?.isDefaultMatch ? 'border-amber-200' : 'border-green-200'
1608
- }`}
1609
- >
1610
- <div className="flex items-center justify-between mb-2">
1611
- <div className="text-sm font-medium text-gray-700 flex items-center space-x-2">
1612
- <span>{getMatchDescription(bestCandidateInfo)}</span>
1613
- {bestCandidateInfo?.conditionSetKey && (
1614
- <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
1615
- {bestCandidateInfo.conditionSetKey}
1616
- </span>
1617
- )}
1618
- {bestCandidateInfo && (
1619
- <span
1620
- className={`px-2 py-1 rounded text-xs ${getMatchTypeColor(bestCandidateInfo.matchType)}`}
1621
- >
1622
- {getMatchTypeIcon(bestCandidateInfo.matchType)} {bestCandidateInfo.matchType}
1623
- </span>
1624
- )}
1625
- </div>
1626
- <div className="flex items-center space-x-2 text-xs">
1627
- {result.bestCandidate.isPartial && (
1628
- <span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Partial</span>
1629
- )}
1630
- {result.bestCandidate.mergeMethod && (
1631
- <span className="bg-gray-100 text-gray-700 px-2 py-1 rounded">
1632
- {result.bestCandidate.mergeMethod}
1633
- </span>
1634
- )}
1635
- </div>
1636
- </div>
1637
- <pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto">
1638
- {JSON.stringify(result.bestCandidate.json, null, 2)}
1639
- </pre>
1640
- {bestCandidateInfo?.conditionEvaluations && (
1641
- <ConditionEvaluationDisplay evaluations={bestCandidateInfo.conditionEvaluations} />
1642
- )}
1643
- </div>
1644
- ) : (
1645
- <div className="bg-yellow-50 border border-yellow-200 rounded p-3">
1646
- <p className="text-sm text-yellow-800">No best candidate found for the current context.</p>
1647
- {result.error && <p className="text-xs text-yellow-600 mt-1">{result.error}</p>}
1648
- </div>
1649
- )}
1650
- </div>
1651
-
1652
- {result.resource && (
1653
- <div>
1654
- <h4 className="font-medium text-gray-800 mb-2">Resource Info</h4>
1655
- <div className="bg-white p-3 rounded border text-sm">
1656
- <div>
1657
- <strong>ID:</strong> {result.resource.id}
1658
- </div>
1659
- <div>
1660
- <strong>Type:</strong> {result.resource.resourceType.key}
1661
- </div>
1662
- <div>
1663
- <strong>Total Candidates:</strong> {result.resource.candidates.length}
1664
- </div>
1665
- </div>
1666
- </div>
1667
- )}
1668
- </div>
1669
- );
1670
- };
1671
-
1672
- export default ResolutionViewer;