@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,1114 +0,0 @@
1
- import React, { useState, useMemo, useCallback, useEffect } from 'react';
2
- import {
3
- FunnelIcon,
4
- DocumentTextIcon,
5
- ExclamationTriangleIcon,
6
- ArrowPathIcon,
7
- CheckIcon,
8
- XMarkIcon,
9
- DocumentArrowDownIcon,
10
- CodeBracketIcon,
11
- ChevronDownIcon,
12
- ChevronUpIcon
13
- } from '@heroicons/react/24/outline';
14
- import { UseResourceManagerReturn } from '../../hooks/useResourceManager';
15
- import { Message, FilterState } from '../../types/app';
16
- import { NoMatch, Config } from '@fgv/ts-res';
17
- import { DEFAULT_SYSTEM_CONFIGURATION } from '../../utils/tsResIntegration';
18
- import {
19
- createFilteredResourceManager,
20
- createFilteredResourceManagerAlternative,
21
- createFilteredResourceManagerSimple,
22
- analyzeFilteredResources,
23
- hasFilterValues,
24
- getFilterSummary,
25
- FilterResult,
26
- FilteredResource
27
- } from '../../utils/filterResources';
28
-
29
- interface FilterToolProps {
30
- onMessage?: (type: Message['type'], message: string) => void;
31
- resourceManager: UseResourceManagerReturn;
32
- filterState: FilterState;
33
- filterActions: {
34
- updateFilterEnabled: (enabled: boolean) => void;
35
- updateFilterValues: (values: Record<string, string>) => void;
36
- applyFilterValues: () => void;
37
- resetFilterValues: () => void;
38
- updateReduceQualifiers: (reduceQualifiers: boolean) => void;
39
- };
40
- onFilterResult?: (result: FilterResult | null) => void;
41
- }
42
-
43
- const FilterTool: React.FC<FilterToolProps> = ({
44
- onMessage,
45
- resourceManager,
46
- filterState,
47
- filterActions,
48
- onFilterResult
49
- }) => {
50
- const { state: resourceState } = resourceManager;
51
-
52
- // Local UI state
53
- const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);
54
- const [filterResult, setFilterResult] = useState<FilterResult | null>(null);
55
- const [isFiltering, setIsFiltering] = useState(false);
56
- const [showFilteredJsonView, setShowFilteredJsonView] = useState(false);
57
-
58
- // Available qualifiers (same logic as ResolutionViewer)
59
- const availableQualifiers = useMemo(() => {
60
- if (resourceState.processedResources?.compiledCollection.qualifiers) {
61
- return resourceState.processedResources.compiledCollection.qualifiers.map((q) => q.name);
62
- }
63
-
64
- const config = resourceState.activeConfiguration || DEFAULT_SYSTEM_CONFIGURATION;
65
- return config.qualifiers.map((q) => q.name);
66
- }, [resourceState.processedResources?.compiledCollection.qualifiers, resourceState.activeConfiguration]);
67
-
68
- // Get qualifier type information for dropdown decisions
69
- const qualifierTypeInfo = useMemo(() => {
70
- const info: Record<
71
- string,
72
- {
73
- type: Config.Model.ISystemConfiguration['qualifierTypes'][0];
74
- enumeratedValues?: string[];
75
- allowedTerritories?: string[];
76
- }
77
- > = {};
78
-
79
- // Get the active configuration to access qualifier types
80
- const config = resourceState.activeConfiguration || DEFAULT_SYSTEM_CONFIGURATION;
81
-
82
- // Create a map of qualifier name to qualifier type for quick lookup
83
- const qualifierTypeMap: Record<string, Config.Model.ISystemConfiguration['qualifierTypes'][0]> = {};
84
-
85
- // First, create a mapping of qualifiers to their types
86
- config.qualifiers.forEach((qualifier) => {
87
- const qualifierType = config.qualifierTypes.find((qt) => qt.name === qualifier.typeName);
88
- if (qualifierType) {
89
- qualifierTypeMap[qualifier.name] = qualifierType;
90
- }
91
- });
92
-
93
- // Build info for each qualifier
94
- availableQualifiers.forEach((qualifierName) => {
95
- const qualifierType = qualifierTypeMap[qualifierName];
96
- if (qualifierType) {
97
- info[qualifierName] = { type: qualifierType };
98
-
99
- // Extract enumerated values for literal types
100
- if (
101
- qualifierType.systemType === 'literal' &&
102
- qualifierType.configuration &&
103
- typeof qualifierType.configuration === 'object' &&
104
- 'enumeratedValues' in qualifierType.configuration &&
105
- Array.isArray(qualifierType.configuration.enumeratedValues)
106
- ) {
107
- info[qualifierName].enumeratedValues = qualifierType.configuration.enumeratedValues;
108
- }
109
-
110
- // Extract allowed territories for territory types
111
- if (
112
- qualifierType.systemType === 'territory' &&
113
- qualifierType.configuration &&
114
- typeof qualifierType.configuration === 'object' &&
115
- 'allowedTerritories' in qualifierType.configuration &&
116
- Array.isArray(qualifierType.configuration.allowedTerritories)
117
- ) {
118
- info[qualifierName].allowedTerritories = qualifierType.configuration.allowedTerritories;
119
- }
120
- }
121
- });
122
-
123
- return info;
124
- }, [availableQualifiers, resourceState.activeConfiguration]);
125
-
126
- // Check if we have any applied filter values set
127
- const hasAppliedFilterValues = useMemo(() => {
128
- return hasFilterValues(filterState.appliedValues);
129
- }, [filterState.appliedValues]);
130
-
131
- // Determine if filtering is active (enabled AND has applied values)
132
- const isFilteringActive = filterState.enabled && hasAppliedFilterValues;
133
-
134
- // Get filtered resource collection data
135
- const getFilteredResourceCollectionData = useCallback(() => {
136
- if (!filterResult?.processedResources?.system.resourceManager) {
137
- return null;
138
- }
139
-
140
- try {
141
- const collectionResult =
142
- filterResult.processedResources.system.resourceManager.getResourceCollectionDecl();
143
- if (collectionResult.isSuccess()) {
144
- return {
145
- ...collectionResult.value,
146
- metadata: {
147
- exportedAt: new Date().toISOString(),
148
- totalResources: filterResult.processedResources.resourceCount,
149
- type: 'ts-res-filtered-resource-collection',
150
- filterContext: filterState.appliedValues,
151
- reduceQualifiers: filterState.reduceQualifiers
152
- }
153
- };
154
- } else {
155
- onMessage?.('error', `Failed to get filtered resource collection: ${collectionResult.message}`);
156
- return null;
157
- }
158
- } catch (error) {
159
- onMessage?.(
160
- 'error',
161
- `Error getting filtered resource collection: ${
162
- error instanceof Error ? error.message : String(error)
163
- }`
164
- );
165
- return null;
166
- }
167
- }, [filterResult, onMessage, filterState.appliedValues]);
168
-
169
- // Export filtered resource collection data
170
- const handleExportFilteredData = useCallback(() => {
171
- try {
172
- const collectionData = getFilteredResourceCollectionData();
173
- if (!collectionData) {
174
- onMessage?.('error', 'No filtered collection data available to export');
175
- return;
176
- }
177
-
178
- const filterSummary = getFilterSummary(filterState.appliedValues);
179
- const sourceJson = JSON.stringify(collectionData, null, 2);
180
- const blob = new Blob([sourceJson], { type: 'application/json' });
181
- const url = URL.createObjectURL(blob);
182
-
183
- const a = document.createElement('a');
184
- a.href = url;
185
- a.download = `filtered-resource-collection-${filterSummary.replace(/[^a-z0-9]/gi, '-')}.json`;
186
- document.body.appendChild(a);
187
- a.click();
188
- document.body.removeChild(a);
189
- URL.revokeObjectURL(url);
190
-
191
- onMessage?.('success', 'Filtered resource collection exported successfully');
192
- } catch (error) {
193
- onMessage?.(
194
- 'error',
195
- `Failed to export filtered resource collection: ${
196
- error instanceof Error ? error.message : String(error)
197
- }`
198
- );
199
- }
200
- }, [getFilteredResourceCollectionData, onMessage, filterState.appliedValues]);
201
-
202
- // Apply filtering when applied filter values change
203
- useEffect(() => {
204
- if (!resourceState.processedResources || !isFilteringActive) {
205
- setFilterResult(null);
206
- onFilterResult?.(null);
207
- return;
208
- }
209
-
210
- const applyFilter = async () => {
211
- setIsFiltering(true);
212
-
213
- try {
214
- // Try the new simple filtering approach first
215
- let filteredResult = await createFilteredResourceManagerSimple(
216
- resourceState.processedResources!.system,
217
- filterState.appliedValues,
218
- {
219
- partialContextMatch: true,
220
- enableDebugLogging: false,
221
- reduceQualifiers: filterState.reduceQualifiers
222
- }
223
- );
224
-
225
- if (filteredResult.isFailure()) {
226
- onMessage?.(
227
- 'warning',
228
- `Simple filtering failed, trying legacy approach: ${filteredResult.message}`
229
- );
230
-
231
- // Fall back to the original complex implementation
232
- filteredResult = await createFilteredResourceManager(
233
- resourceState.processedResources!.system,
234
- filterState.appliedValues,
235
- {
236
- partialContextMatch: true,
237
- enableDebugLogging: false,
238
- reduceQualifiers: filterState.reduceQualifiers
239
- }
240
- );
241
-
242
- if (filteredResult.isFailure()) {
243
- onMessage?.(
244
- 'warning',
245
- `Legacy filtering failed, trying alternative approach: ${filteredResult.message}`
246
- );
247
-
248
- // Last resort: try the alternative approach
249
- filteredResult = await createFilteredResourceManagerAlternative(
250
- resourceState.processedResources!.system,
251
- filterState.appliedValues,
252
- {
253
- partialContextMatch: true,
254
- enableDebugLogging: false,
255
- reduceQualifiers: filterState.reduceQualifiers
256
- }
257
- );
258
-
259
- if (filteredResult.isFailure()) {
260
- setFilterResult({
261
- success: false,
262
- filteredResources: [],
263
- error: `All filtering approaches failed. Simple: ${filteredResult.message}`,
264
- warnings: []
265
- });
266
- onMessage?.('error', `Filtering failed: ${filteredResult.message}`);
267
- return;
268
- } else {
269
- onMessage?.('success', 'Alternative filtering approach succeeded');
270
- }
271
- } else {
272
- onMessage?.('success', 'Legacy filtering approach succeeded');
273
- }
274
- } else {
275
- onMessage?.('success', 'Simple filtering approach succeeded');
276
- }
277
-
278
- // Analyze filtered resources
279
- const originalResources = resourceState.processedResources!.summary.resourceIds || [];
280
- const analysis = analyzeFilteredResources(
281
- originalResources,
282
- filteredResult.value,
283
- resourceState.processedResources!
284
- );
285
-
286
- setFilterResult(analysis);
287
- onFilterResult?.(analysis);
288
-
289
- if (analysis.warnings.length > 0) {
290
- onMessage?.('warning', `Filtering completed with ${analysis.warnings.length} warning(s)`);
291
- } else {
292
- onMessage?.('success', `Filtering completed: ${analysis.filteredResources.length} resources`);
293
- }
294
- } catch (error) {
295
- const errorMessage = error instanceof Error ? error.message : String(error);
296
- const errorResult = {
297
- success: false,
298
- filteredResources: [],
299
- error: errorMessage,
300
- warnings: []
301
- };
302
- setFilterResult(errorResult);
303
- onFilterResult?.(errorResult);
304
- onMessage?.('error', `Filtering error: ${errorMessage}`);
305
- } finally {
306
- setIsFiltering(false);
307
- }
308
- };
309
-
310
- applyFilter();
311
- }, [filterState.appliedValues, isFilteringActive, resourceState.processedResources, onMessage]);
312
-
313
- // Get resources to display (filtered or original)
314
- const displayResources = useMemo(() => {
315
- if (!resourceState.processedResources) return [];
316
-
317
- let resources: FilteredResource[] = [];
318
-
319
- if (isFilteringActive && filterResult?.success) {
320
- resources = filterResult.filteredResources;
321
- } else {
322
- // Return original resources
323
- const originalResources = resourceState.processedResources.summary.resourceIds || [];
324
- resources = originalResources.map((id) => {
325
- const resourceResult = resourceState.processedResources!.system.resourceManager.getBuiltResource(id);
326
- const candidateCount = resourceResult.isSuccess() ? resourceResult.value.candidates.length : 0;
327
-
328
- return {
329
- id,
330
- originalCandidateCount: candidateCount,
331
- filteredCandidateCount: candidateCount,
332
- hasWarning: false
333
- } as FilteredResource;
334
- });
335
- }
336
-
337
- // Sort resources alphabetically by id
338
- return resources.sort((a, b) => a.id.localeCompare(b.id));
339
- }, [resourceState.processedResources, isFilteringActive, filterResult]);
340
-
341
- // Handle filter value changes
342
- const handleFilterChange = useCallback(
343
- (qualifierName: string, value: string) => {
344
- const newValues = { ...filterState.values, [qualifierName]: value };
345
- filterActions.updateFilterValues(newValues);
346
- },
347
- [filterState.values, filterActions]
348
- );
349
-
350
- // Handle resource selection
351
- const handleResourceSelect = useCallback((resourceId: string) => {
352
- setSelectedResourceId(resourceId);
353
- }, []);
354
-
355
- // Handle filter toggle
356
- const handleFilterToggle = useCallback(
357
- (enabled: boolean) => {
358
- filterActions.updateFilterEnabled(enabled);
359
- if (!enabled) {
360
- onMessage?.('info', 'Filtering disabled - showing all resources');
361
- } else {
362
- onMessage?.('info', 'Filtering enabled - set qualifier values and click Apply to filter resources');
363
- }
364
- },
365
- [filterActions, onMessage]
366
- );
367
-
368
- // Handle apply filter values
369
- const handleApplyFilter = useCallback(() => {
370
- filterActions.applyFilterValues();
371
- onMessage?.('info', 'Filter applied - processing resources...');
372
- }, [filterActions, onMessage]);
373
-
374
- // Handle reset filter values
375
- const handleResetFilter = useCallback(() => {
376
- filterActions.resetFilterValues();
377
- onMessage?.('info', 'Filter values reset');
378
- }, [filterActions, onMessage]);
379
-
380
- if (!resourceState.processedResources) {
381
- return (
382
- <div className="p-6">
383
- <div className="flex items-center space-x-3 mb-6">
384
- <FunnelIcon className="h-8 w-8 text-purple-600" />
385
- <h2 className="text-2xl font-bold text-gray-900">Filter Tool</h2>
386
- </div>
387
-
388
- <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
389
- <div className="max-w-2xl mx-auto">
390
- <h3 className="text-xl font-semibold text-gray-900 mb-4">No Resources Loaded</h3>
391
- <p className="text-gray-600 mb-6">
392
- Import resources first to use the filter tool for context-based resource filtering.
393
- </p>
394
- <div className="bg-purple-50 rounded-lg p-4">
395
- <p className="text-sm text-purple-800">
396
- <strong>Filter Tool:</strong> Allows you to filter resources based on partial context
397
- matching, creating focused subsets for analysis and testing.
398
- </p>
399
- </div>
400
- </div>
401
- </div>
402
- </div>
403
- );
404
- }
405
-
406
- // Get both original and filtered resources for the selected resource
407
- const selectedResourceData = useMemo(() => {
408
- if (!selectedResourceId || !resourceState.processedResources) {
409
- return null;
410
- }
411
-
412
- const originalResourceResult =
413
- resourceState.processedResources.system.resourceManager.getBuiltResource(selectedResourceId);
414
- const originalResource = originalResourceResult.isSuccess() ? originalResourceResult.value : null;
415
-
416
- let filteredResource = null;
417
- if (isFilteringActive && filterResult?.processedResources) {
418
- const filteredResourceResult =
419
- filterResult.processedResources.system.resourceManager.getBuiltResource(selectedResourceId);
420
- filteredResource = filteredResourceResult.isSuccess() ? filteredResourceResult.value : null;
421
- }
422
-
423
- return {
424
- original: originalResource,
425
- filtered: filteredResource
426
- };
427
- }, [selectedResourceId, resourceState.processedResources, filterResult, isFilteringActive]);
428
-
429
- // Categorize candidates when filtering is active
430
- const categorizedCandidates = useMemo(() => {
431
- if (!selectedResourceData?.original || !isFilteringActive) {
432
- return { matching: [], filteredOut: [] };
433
- }
434
-
435
- const originalCandidates = selectedResourceData.original.candidates;
436
- const filteredCandidates = selectedResourceData.filtered?.candidates || [];
437
-
438
- // Create a lookup for filtered candidates by comparing JSON content and conditions
439
- const filteredCandidatesSet = new Set(
440
- filteredCandidates.map((candidate) =>
441
- JSON.stringify({
442
- json: candidate.json,
443
- conditions:
444
- candidate.conditions?.conditions?.map((c) => ({
445
- qualifier: c.qualifier.name,
446
- operator: c.operator,
447
- value: c.value
448
- })) || []
449
- })
450
- )
451
- );
452
-
453
- const matching = [];
454
- const filteredOut = [];
455
-
456
- for (const candidate of originalCandidates) {
457
- const candidateKey = JSON.stringify({
458
- json: candidate.json,
459
- conditions:
460
- candidate.conditions?.conditions?.map((c) => ({
461
- qualifier: c.qualifier.name,
462
- operator: c.operator,
463
- value: c.value
464
- })) || []
465
- });
466
-
467
- if (filteredCandidatesSet.has(candidateKey)) {
468
- matching.push(candidate);
469
- } else {
470
- filteredOut.push(candidate);
471
- }
472
- }
473
-
474
- return { matching, filteredOut };
475
- }, [selectedResourceData, isFilteringActive]);
476
-
477
- return (
478
- <div className="p-6">
479
- <div className="flex items-center space-x-3 mb-6">
480
- <FunnelIcon className="h-8 w-8 text-purple-600" />
481
- <h2 className="text-2xl font-bold text-gray-900">Filter Tool</h2>
482
- </div>
483
-
484
- <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
485
- {/* Filter Controls */}
486
- <div className="mb-6">
487
- <div className="flex items-center justify-between mb-4">
488
- <h3 className="text-lg font-semibold text-gray-900">Filter Controls</h3>
489
- <div className="flex items-center justify-between">
490
- <div className="flex items-center space-x-4">
491
- <label className="flex items-center">
492
- <input
493
- type="checkbox"
494
- checked={filterState.enabled}
495
- onChange={(e) => handleFilterToggle(e.target.checked)}
496
- className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
497
- />
498
- <span className="ml-2 text-sm text-gray-700">Enable Filtering</span>
499
- </label>
500
- <label
501
- className="flex items-center"
502
- title="Remove perfectly matching qualifier conditions from filtered resources to create cleaner bundles for comparison"
503
- >
504
- <input
505
- type="checkbox"
506
- checked={filterState.reduceQualifiers}
507
- onChange={(e) => filterActions.updateReduceQualifiers(e.target.checked)}
508
- disabled={!filterState.enabled}
509
- className="rounded border-gray-300 text-purple-600 focus:ring-purple-500 disabled:text-gray-400"
510
- />
511
- <span
512
- className={`ml-2 text-sm ${!filterState.enabled ? 'text-gray-400' : 'text-gray-700'}`}
513
- >
514
- Reduce Qualifiers
515
- </span>
516
- </label>
517
- {isFilteringActive && (
518
- <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
519
- Active{filterState.reduceQualifiers ? ' + Reducing' : ''}
520
- </span>
521
- )}
522
- {filterState.hasPendingChanges && filterState.enabled && (
523
- <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">
524
- Pending Changes
525
- </span>
526
- )}
527
- </div>
528
-
529
- {filterState.enabled && (
530
- <div className="flex items-center space-x-2">
531
- <button
532
- onClick={handleResetFilter}
533
- className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
534
- >
535
- <XMarkIcon className="h-4 w-4 mr-1" />
536
- Reset
537
- </button>
538
- <button
539
- onClick={handleApplyFilter}
540
- disabled={!filterState.hasPendingChanges || isFiltering}
541
- className={`inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500 ${
542
- filterState.hasPendingChanges && !isFiltering
543
- ? 'text-white bg-purple-600 hover:bg-purple-700'
544
- : 'text-gray-400 bg-gray-300 cursor-not-allowed'
545
- }`}
546
- >
547
- {isFiltering ? (
548
- <ArrowPathIcon className="h-4 w-4 mr-1 animate-spin" />
549
- ) : (
550
- <CheckIcon className="h-4 w-4 mr-1" />
551
- )}
552
- Apply
553
- </button>
554
- </div>
555
- )}
556
- </div>
557
- </div>
558
- </div>
559
-
560
- {/* Qualifier Selection Panel */}
561
- <div className="mb-6">
562
- <h3 className="text-lg font-semibold text-gray-900 mb-4">Context Filters</h3>
563
- <div className="bg-gray-50 rounded-lg p-4">
564
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
565
- {availableQualifiers.map((qualifierName) => {
566
- const typeInfo = qualifierTypeInfo[qualifierName];
567
- const hasConstrainedValues =
568
- (typeInfo?.enumeratedValues && typeInfo.enumeratedValues.length > 0) ||
569
- (typeInfo?.allowedTerritories && typeInfo.allowedTerritories.length > 0);
570
-
571
- const constrainedValues = typeInfo?.enumeratedValues || typeInfo?.allowedTerritories || [];
572
-
573
- return (
574
- <div key={qualifierName} className="bg-white rounded border border-gray-200 p-2">
575
- <div className="flex items-center gap-2">
576
- <label className="text-sm font-medium text-gray-700 min-w-0 flex-shrink-0">
577
- {qualifierName}:
578
- </label>
579
- {hasConstrainedValues ? (
580
- <select
581
- value={filterState.values[qualifierName] || ''}
582
- onChange={(e) => handleFilterChange(qualifierName, e.target.value)}
583
- disabled={!filterState.enabled}
584
- className={`flex-1 px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent text-sm min-w-0 ${
585
- !filterState.enabled ? 'bg-gray-100 text-gray-400' : 'bg-white'
586
- }`}
587
- >
588
- <option value="">
589
- {filterState.enabled ? `All ${qualifierName} values` : 'Disabled'}
590
- </option>
591
- {constrainedValues.map((value) => (
592
- <option key={value} value={value}>
593
- {value}
594
- </option>
595
- ))}
596
- </select>
597
- ) : (
598
- <input
599
- type="text"
600
- value={filterState.values[qualifierName] || ''}
601
- onChange={(e) => handleFilterChange(qualifierName, e.target.value)}
602
- disabled={!filterState.enabled}
603
- className={`flex-1 px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent text-sm min-w-0 ${
604
- !filterState.enabled ? 'bg-gray-100 text-gray-400' : ''
605
- }`}
606
- placeholder={filterState.enabled ? `Filter by ${qualifierName}` : 'Disabled'}
607
- />
608
- )}
609
- </div>
610
- </div>
611
- );
612
- })}
613
- </div>
614
- {filterState.enabled && (
615
- <div className="mt-3 text-sm text-gray-600">
616
- <div className="flex items-center justify-between">
617
- <div className="space-y-1">
618
- <p>
619
- <strong>Pending:</strong> {getFilterSummary(filterState.values)}
620
- </p>
621
- {isFilteringActive && (
622
- <p>
623
- <strong>Applied:</strong> {getFilterSummary(filterState.appliedValues)}
624
- </p>
625
- )}
626
- </div>
627
- {isFiltering && (
628
- <div className="flex items-center text-blue-600">
629
- <ArrowPathIcon className="h-4 w-4 mr-1 animate-spin" />
630
- <span className="text-xs">Filtering...</span>
631
- </div>
632
- )}
633
- </div>
634
- {filterResult && !filterResult.success && filterResult.error && (
635
- <p className="text-red-600 text-xs mt-1">
636
- <strong>Error:</strong> {filterResult.error}
637
- </p>
638
- )}
639
- </div>
640
- )}
641
- </div>
642
- </div>
643
-
644
- {/* Filtered Resource Collection JSON View */}
645
- {isFilteringActive && filterResult?.success && (
646
- <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
647
- <div className="flex items-center justify-between mb-2">
648
- <button
649
- onClick={() => setShowFilteredJsonView(!showFilteredJsonView)}
650
- className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
651
- >
652
- <CodeBracketIcon className="h-4 w-4 mr-2" />
653
- {showFilteredJsonView ? 'Hide' : 'Show'} Filtered JSON Resource Collection
654
- {showFilteredJsonView ? (
655
- <ChevronUpIcon className="h-4 w-4 ml-2" />
656
- ) : (
657
- <ChevronDownIcon className="h-4 w-4 ml-2" />
658
- )}
659
- </button>
660
-
661
- <button
662
- onClick={handleExportFilteredData}
663
- className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
664
- >
665
- <DocumentArrowDownIcon className="h-4 w-4 mr-1" />
666
- Export Filtered JSON
667
- </button>
668
- </div>
669
-
670
- {/* JSON View */}
671
- {showFilteredJsonView && (
672
- <div className="mt-4">
673
- <div className="bg-gray-50 rounded-lg border border-gray-200 p-4">
674
- <div className="flex items-center justify-between mb-2">
675
- <h3 className="text-sm font-medium text-gray-900">
676
- Filtered Resource Collection ({getFilterSummary(filterState.appliedValues)})
677
- </h3>
678
- <button
679
- onClick={handleExportFilteredData}
680
- className="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 bg-gray-100 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
681
- >
682
- <DocumentArrowDownIcon className="h-3 w-3 mr-1" />
683
- Export
684
- </button>
685
- </div>
686
- <pre className="text-xs text-gray-800 bg-white p-3 rounded border overflow-x-auto max-h-64 overflow-y-auto">
687
- {JSON.stringify(getFilteredResourceCollectionData(), null, 2)}
688
- </pre>
689
- </div>
690
- </div>
691
- )}
692
- </div>
693
- )}
694
-
695
- {/* Main Browser/Details Layout */}
696
- <div className="flex flex-col lg:flex-row gap-6 h-[600px]">
697
- {/* Left side: Resource List */}
698
- <div className="lg:w-1/2 flex flex-col">
699
- <div className="flex items-center justify-between mb-4">
700
- <h3 className="text-lg font-semibold text-gray-900">
701
- {isFilteringActive ? 'Filtered Resources' : 'All Resources'}
702
- </h3>
703
- <div className="flex items-center space-x-2 text-sm text-gray-500">
704
- <span>{displayResources.length} resources</span>
705
- {isFilteringActive && displayResources.some((r) => r.hasWarning) && (
706
- <>
707
- <span>•</span>
708
- <span className="text-amber-600 flex items-center">
709
- <ExclamationTriangleIcon className="h-4 w-4 mr-1" />
710
- {displayResources.filter((r) => r.hasWarning).length} warnings
711
- </span>
712
- </>
713
- )}
714
- </div>
715
- </div>
716
-
717
- <div className="flex-1 overflow-y-auto border border-gray-200 rounded-lg bg-gray-50">
718
- {displayResources.map((resource) => (
719
- <div
720
- key={resource.id}
721
- className={`flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-100 border-b border-gray-100 last:border-b-0 ${
722
- selectedResourceId === resource.id ? 'bg-purple-50 border-r-2 border-purple-500' : ''
723
- }`}
724
- onClick={() => handleResourceSelect(resource.id)}
725
- >
726
- <div className="flex items-center space-x-2 flex-1 min-w-0">
727
- <DocumentTextIcon className="w-4 h-4 text-green-500 flex-shrink-0" />
728
- <span
729
- className={`text-sm truncate ${
730
- selectedResourceId === resource.id ? 'font-medium text-purple-900' : 'text-gray-700'
731
- }`}
732
- >
733
- {resource.id}
734
- </span>
735
- </div>
736
- <div className="flex items-center space-x-2 flex-shrink-0">
737
- {isFilteringActive && (
738
- <div className="flex items-center space-x-1 text-xs">
739
- <span className="text-gray-400">{resource.originalCandidateCount}</span>
740
- <span className="text-gray-400">→</span>
741
- <span
742
- className={`font-medium ${
743
- resource.filteredCandidateCount === 0
744
- ? 'text-red-600'
745
- : resource.filteredCandidateCount < resource.originalCandidateCount
746
- ? 'text-amber-600'
747
- : 'text-green-600'
748
- }`}
749
- >
750
- {resource.filteredCandidateCount}
751
- </span>
752
- </div>
753
- )}
754
- {!isFilteringActive && (
755
- <span className="text-xs text-gray-500">
756
- {resource.originalCandidateCount} candidates
757
- </span>
758
- )}
759
- {resource.hasWarning && (
760
- <ExclamationTriangleIcon
761
- className="h-4 w-4 text-amber-500"
762
- title="No matching candidates"
763
- />
764
- )}
765
- </div>
766
- </div>
767
- ))}
768
- </div>
769
- </div>
770
-
771
- {/* Right side: Resource Details */}
772
- <div className="lg:w-1/2 flex flex-col">
773
- <div className="flex items-center justify-between mb-4">
774
- <h3 className="text-lg font-semibold text-gray-900">Resource Details</h3>
775
- </div>
776
-
777
- <div className="flex-1 overflow-y-auto border border-gray-200 rounded-lg p-4 bg-gray-50">
778
- {!selectedResourceId ? (
779
- <div className="flex items-center justify-center h-full">
780
- <div className="text-center">
781
- <FunnelIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
782
- <p className="text-gray-500">Select a resource to view details</p>
783
- {isFilteringActive && (
784
- <p className="text-sm text-gray-400 mt-2">
785
- Showing resources that match your filter criteria
786
- </p>
787
- )}
788
- </div>
789
- </div>
790
- ) : selectedResourceData?.original ? (
791
- <div className="space-y-4">
792
- <div>
793
- <h4 className="font-medium text-gray-800 mb-2">Resource Information</h4>
794
- <div className="bg-white p-3 rounded border text-sm">
795
- <div className="grid grid-cols-2 gap-2">
796
- <div>
797
- <strong>ID:</strong> {selectedResourceData.original.id}
798
- </div>
799
- <div>
800
- <strong>Type:</strong> {selectedResourceData.original.resourceType.key}
801
- </div>
802
- <div>
803
- <strong>Candidates:</strong> {selectedResourceData.original.candidates.length}
804
- </div>
805
- <div>
806
- <strong>Decision:</strong> {selectedResourceData.original.decision.key}
807
- </div>
808
- </div>
809
- </div>
810
- </div>
811
-
812
- {isFilteringActive && (
813
- <div>
814
- <h4 className="font-medium text-gray-800 mb-2">Filter Impact</h4>
815
- <div className="bg-white p-3 rounded border text-sm">
816
- <div className="space-y-1">
817
- <div>Original candidates: {selectedResourceData.original.candidates.length}</div>
818
- <div className="text-purple-600">
819
- Filtered candidates:{' '}
820
- {displayResources.find((r) => r.id === selectedResourceId)
821
- ?.filteredCandidateCount || 0}
822
- </div>
823
- {displayResources.find((r) => r.id === selectedResourceId)?.hasWarning && (
824
- <div className="text-amber-600 text-xs mt-2 flex items-center">
825
- <ExclamationTriangleIcon className="h-4 w-4 mr-1" />
826
- Warning: No candidates match the current filter criteria
827
- </div>
828
- )}
829
- </div>
830
- </div>
831
- </div>
832
- )}
833
-
834
- <div>
835
- <h4 className="font-medium text-gray-800 mb-2">
836
- Candidates ({selectedResourceData.original.candidates.length})
837
- </h4>
838
- <div className="bg-white rounded border max-h-64 overflow-y-auto">
839
- {selectedResourceData.original.candidates.length > 0 ? (
840
- <div className="space-y-4 p-4">
841
- {/* Show matching candidates first when filtering is active */}
842
- {isFilteringActive && categorizedCandidates.matching.length > 0 && (
843
- <div className="space-y-4">
844
- <div className="border-b border-green-200 pb-2">
845
- <h5 className="text-sm font-semibold text-green-700 flex items-center">
846
- <CheckIcon className="h-4 w-4 mr-1" />
847
- Matching Candidates ({categorizedCandidates.matching.length})
848
- </h5>
849
- </div>
850
- {categorizedCandidates.matching.map((candidate, index) => (
851
- <div key={`matching-${index}`} className="border-l-4 border-green-500 pl-4">
852
- <div className="bg-green-50 p-4 rounded-lg border border-green-200">
853
- <div className="flex items-center justify-between mb-3">
854
- <h6 className="font-medium text-gray-800">
855
- Matching Candidate {index + 1}
856
- </h6>
857
- <div className="flex items-center space-x-2 text-xs">
858
- {candidate.isPartial && (
859
- <span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
860
- Partial
861
- </span>
862
- )}
863
- <span className="bg-green-100 text-green-700 px-2 py-1 rounded">
864
- {candidate.mergeMethod}
865
- </span>
866
- </div>
867
- </div>
868
-
869
- {/* Conditions */}
870
- {candidate.conditions &&
871
- candidate.conditions.conditions &&
872
- candidate.conditions.conditions.length > 0 ? (
873
- <div className="mb-3">
874
- <h6 className="text-sm font-medium text-gray-600 mb-2">
875
- Conditions:
876
- </h6>
877
- <div className="space-y-1">
878
- {candidate.conditions.conditions.map((condition, condIndex) => (
879
- <div
880
- key={condIndex}
881
- className="flex items-center text-xs bg-green-100 px-2 py-1 rounded"
882
- >
883
- <span className="font-medium text-green-800">
884
- {condition.qualifier.name}
885
- </span>
886
- <span className="mx-1 text-green-600">
887
- {condition.operator}
888
- </span>
889
- <span className="text-green-700">{condition.value}</span>
890
- <div className="ml-auto flex items-center space-x-2">
891
- <span className="text-green-500">
892
- priority: {condition.priority}
893
- </span>
894
- {condition.scoreAsDefault !== undefined && (
895
- <span className="text-amber-600 font-medium">
896
- default: {condition.scoreAsDefault}
897
- </span>
898
- )}
899
- </div>
900
- </div>
901
- ))}
902
- </div>
903
- </div>
904
- ) : (
905
- <div className="mb-3">
906
- <span className="text-xs text-green-600 bg-green-100 px-2 py-1 rounded">
907
- No conditions (default candidate)
908
- </span>
909
- </div>
910
- )}
911
-
912
- {/* JSON Content */}
913
- <div>
914
- <h6 className="text-sm font-medium text-gray-600 mb-2">Content:</h6>
915
- <pre className="text-xs bg-white p-3 rounded border overflow-x-auto max-h-40">
916
- {JSON.stringify(candidate.json, null, 2)}
917
- </pre>
918
- </div>
919
- </div>
920
- </div>
921
- ))}
922
- </div>
923
- )}
924
-
925
- {/* Show filtered out candidates when filtering is active */}
926
- {isFilteringActive && categorizedCandidates.filteredOut.length > 0 && (
927
- <div className="space-y-4">
928
- <div className="border-b border-gray-300 pb-2">
929
- <h5 className="text-sm font-semibold text-gray-600 flex items-center">
930
- <XMarkIcon className="h-4 w-4 mr-1" />
931
- Filtered Out Candidates ({categorizedCandidates.filteredOut.length})
932
- </h5>
933
- </div>
934
- {categorizedCandidates.filteredOut.map((candidate, index) => (
935
- <div
936
- key={`filtered-${index}`}
937
- className="border-l-4 border-gray-400 pl-4 opacity-60"
938
- >
939
- <div className="bg-gray-50 p-4 rounded-lg border border-gray-300">
940
- <div className="flex items-center justify-between mb-3">
941
- <h6 className="font-medium text-gray-600">
942
- Filtered Out Candidate {index + 1}
943
- </h6>
944
- <div className="flex items-center space-x-2 text-xs">
945
- {candidate.isPartial && (
946
- <span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
947
- Partial
948
- </span>
949
- )}
950
- <span className="bg-gray-200 text-gray-600 px-2 py-1 rounded">
951
- {candidate.mergeMethod}
952
- </span>
953
- </div>
954
- </div>
955
-
956
- {/* Conditions */}
957
- {candidate.conditions &&
958
- candidate.conditions.conditions &&
959
- candidate.conditions.conditions.length > 0 ? (
960
- <div className="mb-3">
961
- <h6 className="text-sm font-medium text-gray-500 mb-2">
962
- Conditions:
963
- </h6>
964
- <div className="space-y-1">
965
- {candidate.conditions.conditions.map((condition, condIndex) => (
966
- <div
967
- key={condIndex}
968
- className="flex items-center text-xs bg-gray-200 px-2 py-1 rounded"
969
- >
970
- <span className="font-medium text-gray-700">
971
- {condition.qualifier.name}
972
- </span>
973
- <span className="mx-1 text-gray-500">{condition.operator}</span>
974
- <span className="text-gray-600">{condition.value}</span>
975
- <div className="ml-auto flex items-center space-x-2">
976
- <span className="text-gray-400">
977
- priority: {condition.priority}
978
- </span>
979
- {condition.scoreAsDefault !== undefined && (
980
- <span className="text-amber-500 font-medium">
981
- default: {condition.scoreAsDefault}
982
- </span>
983
- )}
984
- </div>
985
- </div>
986
- ))}
987
- </div>
988
- </div>
989
- ) : (
990
- <div className="mb-3">
991
- <span className="text-xs text-gray-500 bg-gray-200 px-2 py-1 rounded">
992
- No conditions (default candidate)
993
- </span>
994
- </div>
995
- )}
996
-
997
- {/* JSON Content */}
998
- <div>
999
- <h6 className="text-sm font-medium text-gray-500 mb-2">Content:</h6>
1000
- <pre className="text-xs bg-gray-100 p-3 rounded border overflow-x-auto max-h-40">
1001
- {JSON.stringify(candidate.json, null, 2)}
1002
- </pre>
1003
- </div>
1004
- </div>
1005
- </div>
1006
- ))}
1007
- </div>
1008
- )}
1009
-
1010
- {/* Show all candidates when filtering is not active */}
1011
- {!isFilteringActive &&
1012
- selectedResourceData.original.candidates.map((candidate, index) => (
1013
- <div key={index}>
1014
- <div className="bg-white p-4 rounded-lg border">
1015
- <div className="flex items-center justify-between mb-3">
1016
- <h5 className="font-medium text-gray-800">Candidate {index + 1}</h5>
1017
- <div className="flex items-center space-x-2 text-xs">
1018
- {candidate.isPartial && (
1019
- <span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
1020
- Partial
1021
- </span>
1022
- )}
1023
- <span className="bg-gray-100 text-gray-700 px-2 py-1 rounded">
1024
- {candidate.mergeMethod}
1025
- </span>
1026
- </div>
1027
- </div>
1028
-
1029
- {/* Conditions */}
1030
- {candidate.conditions &&
1031
- candidate.conditions.conditions &&
1032
- candidate.conditions.conditions.length > 0 ? (
1033
- <div className="mb-3">
1034
- <h6 className="text-sm font-medium text-gray-600 mb-2">Conditions:</h6>
1035
- <div className="space-y-1">
1036
- {candidate.conditions.conditions.map((condition, condIndex) => (
1037
- <div
1038
- key={condIndex}
1039
- className="flex items-center text-xs bg-blue-50 px-2 py-1 rounded"
1040
- >
1041
- <span className="font-medium text-blue-800">
1042
- {condition.qualifier.name}
1043
- </span>
1044
- <span className="mx-1 text-blue-600">{condition.operator}</span>
1045
- <span className="text-blue-700">{condition.value}</span>
1046
- <div className="ml-auto flex items-center space-x-2">
1047
- <span className="text-blue-500">
1048
- priority: {condition.priority}
1049
- </span>
1050
- {condition.scoreAsDefault !== undefined && (
1051
- <span className="text-amber-600 font-medium">
1052
- default: {condition.scoreAsDefault}
1053
- </span>
1054
- )}
1055
- </div>
1056
- </div>
1057
- ))}
1058
- </div>
1059
- </div>
1060
- ) : (
1061
- <div className="mb-3">
1062
- <span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
1063
- No conditions (default candidate)
1064
- </span>
1065
- </div>
1066
- )}
1067
-
1068
- {/* JSON Content */}
1069
- <div>
1070
- <h6 className="text-sm font-medium text-gray-600 mb-2">Content:</h6>
1071
- <pre className="text-xs bg-gray-50 p-3 rounded border overflow-x-auto max-h-40">
1072
- {JSON.stringify(candidate.json, null, 2)}
1073
- </pre>
1074
- </div>
1075
- </div>
1076
- </div>
1077
- ))}
1078
- </div>
1079
- ) : (
1080
- <div className="p-4 text-center text-gray-500 text-sm">No candidates available</div>
1081
- )}
1082
- </div>
1083
- </div>
1084
-
1085
- <div>
1086
- <h4 className="font-medium text-gray-800 mb-2">Raw Resource Data</h4>
1087
- <pre className="text-xs bg-white p-3 rounded border overflow-x-auto max-h-32 overflow-y-auto">
1088
- {JSON.stringify(
1089
- {
1090
- id: selectedResourceData.original.id,
1091
- resourceType: selectedResourceData.original.resourceType.key,
1092
- decision: selectedResourceData.original.decision.key,
1093
- candidateCount: selectedResourceData.original.candidates.length
1094
- },
1095
- null,
1096
- 2
1097
- )}
1098
- </pre>
1099
- </div>
1100
- </div>
1101
- ) : (
1102
- <div className="text-center text-gray-500">
1103
- <p>Failed to load resource details</p>
1104
- </div>
1105
- )}
1106
- </div>
1107
- </div>
1108
- </div>
1109
- </div>
1110
- </div>
1111
- );
1112
- };
1113
-
1114
- export default FilterTool;