@fgv/ts-res-ui-components 5.0.0-10

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 (231) hide show
  1. package/.rush/temp/03c8b056281d9db0a97d8a6e25eea798a160d393.tar.log +271 -0
  2. package/.rush/temp/chunked-rush-logs/ts-res-ui-components.build.chunks.jsonl +9 -0
  3. package/.rush/temp/operation/build/all.log +9 -0
  4. package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
  5. package/.rush/temp/operation/build/state.json +3 -0
  6. package/.rush/temp/shrinkwrap-deps.json +1111 -0
  7. package/README.md +18 -0
  8. package/REFACTORING_PLAN.md +171 -0
  9. package/config/jest.config.json +16 -0
  10. package/config/jest.setup.js +64 -0
  11. package/config/rig.json +16 -0
  12. package/lib/components/common/QualifierContextControl.d.ts +14 -0
  13. package/lib/components/common/QualifierContextControl.d.ts.map +1 -0
  14. package/lib/components/common/QualifierContextControl.js +78 -0
  15. package/lib/components/common/QualifierContextControl.js.map +1 -0
  16. package/lib/components/common/ResourceListView.d.ts +11 -0
  17. package/lib/components/common/ResourceListView.d.ts.map +1 -0
  18. package/lib/components/common/ResourceListView.js +20 -0
  19. package/lib/components/common/ResourceListView.js.map +1 -0
  20. package/lib/components/common/ResourceTreeView.d.ts +12 -0
  21. package/lib/components/common/ResourceTreeView.d.ts.map +1 -0
  22. package/lib/components/common/ResourceTreeView.js +162 -0
  23. package/lib/components/common/ResourceTreeView.js.map +1 -0
  24. package/lib/components/forms/HierarchyEditor.d.ts +10 -0
  25. package/lib/components/forms/HierarchyEditor.d.ts.map +1 -0
  26. package/lib/components/forms/HierarchyEditor.js +106 -0
  27. package/lib/components/forms/HierarchyEditor.js.map +1 -0
  28. package/lib/components/forms/QualifierEditForm.d.ts +11 -0
  29. package/lib/components/forms/QualifierEditForm.d.ts.map +1 -0
  30. package/lib/components/forms/QualifierEditForm.js +181 -0
  31. package/lib/components/forms/QualifierEditForm.js.map +1 -0
  32. package/lib/components/forms/QualifierTypeEditForm.d.ts +10 -0
  33. package/lib/components/forms/QualifierTypeEditForm.d.ts.map +1 -0
  34. package/lib/components/forms/QualifierTypeEditForm.js +172 -0
  35. package/lib/components/forms/QualifierTypeEditForm.js.map +1 -0
  36. package/lib/components/forms/ResourceTypeEditForm.d.ts +10 -0
  37. package/lib/components/forms/ResourceTypeEditForm.d.ts.map +1 -0
  38. package/lib/components/forms/ResourceTypeEditForm.js +188 -0
  39. package/lib/components/forms/ResourceTypeEditForm.js.map +1 -0
  40. package/lib/components/forms/index.d.ts +9 -0
  41. package/lib/components/forms/index.d.ts.map +1 -0
  42. package/lib/components/forms/index.js +5 -0
  43. package/lib/components/forms/index.js.map +1 -0
  44. package/lib/components/orchestrator/ResourceOrchestrator.d.ts +14 -0
  45. package/lib/components/orchestrator/ResourceOrchestrator.d.ts.map +1 -0
  46. package/lib/components/orchestrator/ResourceOrchestrator.js +278 -0
  47. package/lib/components/orchestrator/ResourceOrchestrator.js.map +1 -0
  48. package/lib/components/views/CompiledView/index.d.ts +5 -0
  49. package/lib/components/views/CompiledView/index.d.ts.map +1 -0
  50. package/lib/components/views/CompiledView/index.js +595 -0
  51. package/lib/components/views/CompiledView/index.js.map +1 -0
  52. package/lib/components/views/ConfigurationView/index.d.ts +5 -0
  53. package/lib/components/views/ConfigurationView/index.d.ts.map +1 -0
  54. package/lib/components/views/ConfigurationView/index.js +363 -0
  55. package/lib/components/views/ConfigurationView/index.js.map +1 -0
  56. package/lib/components/views/FilterView/index.d.ts +5 -0
  57. package/lib/components/views/FilterView/index.d.ts.map +1 -0
  58. package/lib/components/views/FilterView/index.js +463 -0
  59. package/lib/components/views/FilterView/index.js.map +1 -0
  60. package/lib/components/views/ImportView/index.d.ts +5 -0
  61. package/lib/components/views/ImportView/index.d.ts.map +1 -0
  62. package/lib/components/views/ImportView/index.js +514 -0
  63. package/lib/components/views/ImportView/index.js.map +1 -0
  64. package/lib/components/views/ResolutionView/EditableJsonView.d.ts +21 -0
  65. package/lib/components/views/ResolutionView/EditableJsonView.d.ts.map +1 -0
  66. package/lib/components/views/ResolutionView/EditableJsonView.js +109 -0
  67. package/lib/components/views/ResolutionView/EditableJsonView.js.map +1 -0
  68. package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts +19 -0
  69. package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts.map +1 -0
  70. package/lib/components/views/ResolutionView/ResolutionEditControls.js +82 -0
  71. package/lib/components/views/ResolutionView/ResolutionEditControls.js.map +1 -0
  72. package/lib/components/views/ResolutionView/index.d.ts +5 -0
  73. package/lib/components/views/ResolutionView/index.d.ts.map +1 -0
  74. package/lib/components/views/ResolutionView/index.js +255 -0
  75. package/lib/components/views/ResolutionView/index.js.map +1 -0
  76. package/lib/components/views/SourceView/index.d.ts +5 -0
  77. package/lib/components/views/SourceView/index.d.ts.map +1 -0
  78. package/lib/components/views/SourceView/index.js +316 -0
  79. package/lib/components/views/SourceView/index.js.map +1 -0
  80. package/lib/components/views/ZipLoaderView/index.d.ts +5 -0
  81. package/lib/components/views/ZipLoaderView/index.d.ts.map +1 -0
  82. package/lib/components/views/ZipLoaderView/index.js +313 -0
  83. package/lib/components/views/ZipLoaderView/index.js.map +1 -0
  84. package/lib/hooks/useConfigurationState.d.ts +46 -0
  85. package/lib/hooks/useConfigurationState.d.ts.map +1 -0
  86. package/lib/hooks/useConfigurationState.js +239 -0
  87. package/lib/hooks/useConfigurationState.js.map +1 -0
  88. package/lib/hooks/useFilterState.d.ts +7 -0
  89. package/lib/hooks/useFilterState.d.ts.map +1 -0
  90. package/lib/hooks/useFilterState.js +80 -0
  91. package/lib/hooks/useFilterState.js.map +1 -0
  92. package/lib/hooks/useResolutionState.d.ts +8 -0
  93. package/lib/hooks/useResolutionState.d.ts.map +1 -0
  94. package/lib/hooks/useResolutionState.js +253 -0
  95. package/lib/hooks/useResolutionState.js.map +1 -0
  96. package/lib/hooks/useResourceData.d.ts +19 -0
  97. package/lib/hooks/useResourceData.d.ts.map +1 -0
  98. package/lib/hooks/useResourceData.js +368 -0
  99. package/lib/hooks/useResourceData.js.map +1 -0
  100. package/lib/hooks/useViewState.d.ts +10 -0
  101. package/lib/hooks/useViewState.d.ts.map +1 -0
  102. package/lib/hooks/useViewState.js +29 -0
  103. package/lib/hooks/useViewState.js.map +1 -0
  104. package/lib/index.d.ts +27 -0
  105. package/lib/index.d.ts.map +1 -0
  106. package/lib/index.js +34 -0
  107. package/lib/index.js.map +1 -0
  108. package/lib/test/helpers/testDataLoader.d.ts +37 -0
  109. package/lib/test/helpers/testDataLoader.d.ts.map +1 -0
  110. package/lib/test/helpers/testDataLoader.js +171 -0
  111. package/lib/test/helpers/testDataLoader.js.map +1 -0
  112. package/lib/test/unit/utils/configurationUtils.test.d.ts +2 -0
  113. package/lib/test/unit/utils/configurationUtils.test.d.ts.map +1 -0
  114. package/lib/test/unit/utils/configurationUtils.test.js +497 -0
  115. package/lib/test/unit/utils/configurationUtils.test.js.map +1 -0
  116. package/lib/test/unit/utils/fileProcessing.test.d.ts +2 -0
  117. package/lib/test/unit/utils/fileProcessing.test.d.ts.map +1 -0
  118. package/lib/test/unit/utils/fileProcessing.test.js +321 -0
  119. package/lib/test/unit/utils/fileProcessing.test.js.map +1 -0
  120. package/lib/test/unit/utils/filterResources.test.d.ts +2 -0
  121. package/lib/test/unit/utils/filterResources.test.d.ts.map +1 -0
  122. package/lib/test/unit/utils/filterResources.test.js +403 -0
  123. package/lib/test/unit/utils/filterResources.test.js.map +1 -0
  124. package/lib/test/unit/utils/resolutionEditing.test.d.ts +2 -0
  125. package/lib/test/unit/utils/resolutionEditing.test.d.ts.map +1 -0
  126. package/lib/test/unit/utils/resolutionEditing.test.js +439 -0
  127. package/lib/test/unit/utils/resolutionEditing.test.js.map +1 -0
  128. package/lib/test/unit/utils/resolutionUtils.test.d.ts +2 -0
  129. package/lib/test/unit/utils/resolutionUtils.test.d.ts.map +1 -0
  130. package/lib/test/unit/utils/resolutionUtils.test.js +397 -0
  131. package/lib/test/unit/utils/resolutionUtils.test.js.map +1 -0
  132. package/lib/test/unit/utils/tsResIntegration.test.d.ts +2 -0
  133. package/lib/test/unit/utils/tsResIntegration.test.d.ts.map +1 -0
  134. package/lib/test/unit/utils/tsResIntegration.test.js +376 -0
  135. package/lib/test/unit/utils/tsResIntegration.test.js.map +1 -0
  136. package/lib/types/index.d.ts +251 -0
  137. package/lib/types/index.d.ts.map +1 -0
  138. package/lib/types/index.js +2 -0
  139. package/lib/types/index.js.map +1 -0
  140. package/lib/utils/configurationUtils.d.ts +74 -0
  141. package/lib/utils/configurationUtils.d.ts.map +1 -0
  142. package/lib/utils/configurationUtils.js +359 -0
  143. package/lib/utils/configurationUtils.js.map +1 -0
  144. package/lib/utils/fileProcessing.d.ts +18 -0
  145. package/lib/utils/fileProcessing.d.ts.map +1 -0
  146. package/lib/utils/fileProcessing.js +142 -0
  147. package/lib/utils/fileProcessing.js.map +1 -0
  148. package/lib/utils/filterResources.d.ts +38 -0
  149. package/lib/utils/filterResources.d.ts.map +1 -0
  150. package/lib/utils/filterResources.js +153 -0
  151. package/lib/utils/filterResources.js.map +1 -0
  152. package/lib/utils/resolutionEditing.d.ts +58 -0
  153. package/lib/utils/resolutionEditing.d.ts.map +1 -0
  154. package/lib/utils/resolutionEditing.js +246 -0
  155. package/lib/utils/resolutionEditing.js.map +1 -0
  156. package/lib/utils/resolutionUtils.d.ts +28 -0
  157. package/lib/utils/resolutionUtils.d.ts.map +1 -0
  158. package/lib/utils/resolutionUtils.js +216 -0
  159. package/lib/utils/resolutionUtils.js.map +1 -0
  160. package/lib/utils/tsResIntegration.d.ts +71 -0
  161. package/lib/utils/tsResIntegration.d.ts.map +1 -0
  162. package/lib/utils/tsResIntegration.js +294 -0
  163. package/lib/utils/tsResIntegration.js.map +1 -0
  164. package/lib/utils/zipLoader/browserZipLoader.d.ts +48 -0
  165. package/lib/utils/zipLoader/browserZipLoader.d.ts.map +1 -0
  166. package/lib/utils/zipLoader/browserZipLoader.js +247 -0
  167. package/lib/utils/zipLoader/browserZipLoader.js.map +1 -0
  168. package/lib/utils/zipLoader/index.d.ts +8 -0
  169. package/lib/utils/zipLoader/index.d.ts.map +1 -0
  170. package/lib/utils/zipLoader/index.js +13 -0
  171. package/lib/utils/zipLoader/index.js.map +1 -0
  172. package/lib/utils/zipLoader/nodeZipBuilder.d.ts +55 -0
  173. package/lib/utils/zipLoader/nodeZipBuilder.d.ts.map +1 -0
  174. package/lib/utils/zipLoader/nodeZipBuilder.js +98 -0
  175. package/lib/utils/zipLoader/nodeZipBuilder.js.map +1 -0
  176. package/lib/utils/zipLoader/types.d.ts +139 -0
  177. package/lib/utils/zipLoader/types.d.ts.map +1 -0
  178. package/lib/utils/zipLoader/types.js +2 -0
  179. package/lib/utils/zipLoader/types.js.map +1 -0
  180. package/lib/utils/zipLoader/zipUtils.d.ts +53 -0
  181. package/lib/utils/zipLoader/zipUtils.d.ts.map +1 -0
  182. package/lib/utils/zipLoader/zipUtils.js +229 -0
  183. package/lib/utils/zipLoader/zipUtils.js.map +1 -0
  184. package/package.json +69 -0
  185. package/rush-logs/ts-res-ui-components.build.cache.log +3 -0
  186. package/rush-logs/ts-res-ui-components.build.log +9 -0
  187. package/src/components/common/QualifierContextControl.tsx +151 -0
  188. package/src/components/common/ResourceListView.tsx +63 -0
  189. package/src/components/common/ResourceTreeView.tsx +271 -0
  190. package/src/components/forms/HierarchyEditor.tsx +204 -0
  191. package/src/components/forms/QualifierEditForm.tsx +355 -0
  192. package/src/components/forms/QualifierTypeEditForm.tsx +347 -0
  193. package/src/components/forms/ResourceTypeEditForm.tsx +331 -0
  194. package/src/components/forms/index.ts +11 -0
  195. package/src/components/orchestrator/ResourceOrchestrator.tsx +372 -0
  196. package/src/components/views/CompiledView/index.tsx +922 -0
  197. package/src/components/views/ConfigurationView/index.tsx +800 -0
  198. package/src/components/views/FilterView/index.tsx +825 -0
  199. package/src/components/views/ImportView/index.tsx +717 -0
  200. package/src/components/views/ResolutionView/EditableJsonView.tsx +214 -0
  201. package/src/components/views/ResolutionView/ResolutionEditControls.tsx +170 -0
  202. package/src/components/views/ResolutionView/index.tsx +591 -0
  203. package/src/components/views/SourceView/index.tsx +536 -0
  204. package/src/components/views/ZipLoaderView/index.tsx +485 -0
  205. package/src/hooks/useConfigurationState.ts +374 -0
  206. package/src/hooks/useFilterState.ts +97 -0
  207. package/src/hooks/useResolutionState.ts +355 -0
  208. package/src/hooks/useResourceData.ts +467 -0
  209. package/src/hooks/useViewState.ts +44 -0
  210. package/src/index.ts +45 -0
  211. package/src/test/helpers/testDataLoader.ts +195 -0
  212. package/src/test/unit/utils/configurationUtils.test.ts +630 -0
  213. package/src/test/unit/utils/fileProcessing.test.ts +391 -0
  214. package/src/test/unit/utils/filterResources.test.ts +574 -0
  215. package/src/test/unit/utils/resolutionEditing.test.ts +556 -0
  216. package/src/test/unit/utils/resolutionUtils.test.ts +521 -0
  217. package/src/test/unit/utils/tsResIntegration.test.ts +433 -0
  218. package/src/types/index.ts +322 -0
  219. package/src/utils/configurationUtils.ts +424 -0
  220. package/src/utils/fileProcessing.ts +160 -0
  221. package/src/utils/filterResources.ts +206 -0
  222. package/src/utils/resolutionEditing.ts +319 -0
  223. package/src/utils/resolutionUtils.ts +289 -0
  224. package/src/utils/tsResIntegration.ts +440 -0
  225. package/src/utils/zipLoader/browserZipLoader.ts +319 -0
  226. package/src/utils/zipLoader/index.ts +26 -0
  227. package/src/utils/zipLoader/nodeZipBuilder.ts +153 -0
  228. package/src/utils/zipLoader/types.ts +175 -0
  229. package/src/utils/zipLoader/zipUtils.ts +266 -0
  230. package/temp/build/typescript/ts_gZid87Hu.json +1 -0
  231. package/tsconfig.json +15 -0
@@ -0,0 +1,825 @@
1
+ import React, { useState, useMemo, useCallback } 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
+ ListBulletIcon,
14
+ FolderIcon
15
+ } from '@heroicons/react/24/outline';
16
+ import { FilterViewProps } from '../../../types';
17
+ import { Config } from '@fgv/ts-res';
18
+ import { QualifierContextControl } from '../../common/QualifierContextControl';
19
+ import { ResourceTreeView } from '../../common/ResourceTreeView';
20
+ import { ResourceListView } from '../../common/ResourceListView';
21
+
22
+ // Import FilteredResource type from the utils
23
+ interface FilteredResource {
24
+ id: string;
25
+ originalCandidateCount: number;
26
+ filteredCandidateCount: number;
27
+ hasWarning: boolean;
28
+ }
29
+
30
+ export const FilterView: React.FC<FilterViewProps> = ({
31
+ resources,
32
+ filterState,
33
+ filterActions,
34
+ filterResult,
35
+ onFilterResult,
36
+ onMessage,
37
+ className = ''
38
+ }) => {
39
+ // Local UI state
40
+ const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);
41
+ const [showFilteredJsonView, setShowFilteredJsonView] = useState(false);
42
+ const [viewMode, setViewMode] = useState<'tree' | 'list'>('list');
43
+
44
+ // Available qualifiers from system configuration or compiled collection
45
+ const availableQualifiers = useMemo(() => {
46
+ if (resources?.compiledCollection.qualifiers) {
47
+ return resources.compiledCollection.qualifiers.map((q) => q.name);
48
+ }
49
+
50
+ // Fallback to default qualifiers if no compiled collection
51
+ return ['language', 'territory', 'currentTerritory', 'role', 'env'];
52
+ }, [resources?.compiledCollection.qualifiers]);
53
+
54
+ // Handle filter value changes using the shared component's callback pattern
55
+ const handleQualifierChange = useCallback(
56
+ (qualifierName: string, value: string | undefined) => {
57
+ const newValues = { ...filterState.values, [qualifierName]: value };
58
+ filterActions.updateFilterValues(newValues);
59
+ },
60
+ [filterState.values, filterActions]
61
+ );
62
+
63
+ // Check if we have any applied filter values set
64
+ const hasAppliedFilterValues = useMemo(() => {
65
+ if (!filterState.appliedValues) return false;
66
+ return Object.values(filterState.appliedValues).some((value) => value !== undefined && value !== '');
67
+ }, [filterState.appliedValues]);
68
+
69
+ // Determine if filtering is active (enabled AND has applied values)
70
+ const isFilteringActive = filterState.enabled && hasAppliedFilterValues;
71
+
72
+ // Simplified filter summary
73
+ const getFilterSummary = useCallback((values: Record<string, string | undefined>) => {
74
+ const activeFilters = Object.entries(values)
75
+ .filter(([, value]) => value !== undefined && value !== '')
76
+ .map(([key, value]) => `${key}=${value}`);
77
+ return activeFilters.length > 0 ? activeFilters.join(', ') : 'No filters';
78
+ }, []);
79
+
80
+ // Get filtered resource collection data (simplified)
81
+ const getFilteredResourceCollectionData = useCallback(() => {
82
+ if (!filterResult?.processedResources?.system.resourceManager) {
83
+ return null;
84
+ }
85
+
86
+ const resourceManager = filterResult.processedResources.system.resourceManager;
87
+
88
+ // Check if this is a ResourceManagerBuilder (has getResourceCollectionDecl method)
89
+ if ('getResourceCollectionDecl' in resourceManager) {
90
+ const collectionResult = (resourceManager as any).getResourceCollectionDecl();
91
+ if (collectionResult.isSuccess()) {
92
+ return {
93
+ ...collectionResult.value,
94
+ metadata: {
95
+ exportedAt: new Date().toISOString(),
96
+ totalResources: filterResult.processedResources.resourceCount,
97
+ type: 'ts-res-filtered-resource-collection',
98
+ filterContext: filterState.appliedValues,
99
+ reduceQualifiers: filterState.reduceQualifiers
100
+ }
101
+ };
102
+ } else {
103
+ onMessage?.('error', `Failed to get filtered resource collection: ${collectionResult.message}`);
104
+ return null;
105
+ }
106
+ } else if (filterResult.processedResources.compiledCollection) {
107
+ // For IResourceManager from bundles, use the compiled collection directly
108
+ return {
109
+ resources: filterResult.processedResources.compiledCollection.resources || [],
110
+ metadata: {
111
+ exportedAt: new Date().toISOString(),
112
+ totalResources: filterResult.processedResources.resourceCount,
113
+ type: 'ts-res-filtered-resource-collection',
114
+ filterContext: filterState.appliedValues,
115
+ reduceQualifiers: filterState.reduceQualifiers
116
+ }
117
+ };
118
+ } else {
119
+ onMessage?.('error', 'Filtered resource collection data not available');
120
+ return null;
121
+ }
122
+ }, [filterResult, onMessage, filterState.appliedValues, filterState.reduceQualifiers]);
123
+
124
+ // Export filtered resource collection data
125
+ const handleExportFilteredData = useCallback(() => {
126
+ try {
127
+ const collectionData = getFilteredResourceCollectionData();
128
+ if (!collectionData) {
129
+ onMessage?.('error', 'No filtered collection data available to export');
130
+ return;
131
+ }
132
+
133
+ const filterSummary = getFilterSummary(filterState.appliedValues);
134
+ // Use onExport callback instead of direct file download for flexibility
135
+ // onExport?.(collectionData, 'json');
136
+ onMessage?.('success', 'Filtered resource collection exported successfully');
137
+ } catch (error) {
138
+ onMessage?.(
139
+ 'error',
140
+ `Failed to export filtered resource collection: ${
141
+ error instanceof Error ? error.message : String(error)
142
+ }`
143
+ );
144
+ }
145
+ }, [getFilteredResourceCollectionData, onMessage, filterState.appliedValues, getFilterSummary]);
146
+
147
+ // Get resources to display (filtered or original) - now uses orchestrator's filterResult
148
+ const displayResources = useMemo(() => {
149
+ console.log('FilterView displayResources calculation:', {
150
+ hasResources: !!resources,
151
+ isFilteringActive,
152
+ filterResultExists: !!filterResult,
153
+ filterResultSuccess: filterResult?.success,
154
+ filterResultCount: filterResult?.filteredResources?.length,
155
+ appliedValues: filterState.appliedValues,
156
+ hasAppliedFilterValues
157
+ });
158
+
159
+ if (!resources) return [];
160
+
161
+ let resourceList: FilteredResource[] = [];
162
+
163
+ if (isFilteringActive && filterResult?.success && filterResult.filteredResources) {
164
+ console.log('Using filtered resources:', filterResult.filteredResources.length);
165
+ console.log(
166
+ 'Filtered resource details:',
167
+ filterResult.filteredResources.map((r) => ({
168
+ id: r.id,
169
+ original: r.originalCandidateCount,
170
+ filtered: r.filteredCandidateCount,
171
+ hasWarning: r.hasWarning
172
+ }))
173
+ );
174
+ resourceList = filterResult.filteredResources;
175
+ } else {
176
+ // Return original resources
177
+ console.log('Using original resources');
178
+ const originalResources = resources.summary.resourceIds || [];
179
+ resourceList = originalResources.map((id) => {
180
+ const resourceResult = resources.system.resourceManager.getBuiltResource(id);
181
+ const candidateCount = resourceResult.isSuccess() ? resourceResult.value.candidates.length : 0;
182
+
183
+ return {
184
+ id,
185
+ originalCandidateCount: candidateCount,
186
+ filteredCandidateCount: candidateCount,
187
+ hasWarning: false
188
+ };
189
+ });
190
+ }
191
+
192
+ // Sort resources alphabetically by id
193
+ return resourceList.sort((a, b) => a.id.localeCompare(b.id));
194
+ }, [resources, isFilteringActive, filterResult]);
195
+
196
+ // Handle filter value changes
197
+ const handleFilterChange = useCallback(
198
+ (qualifierName: string, value: string | undefined) => {
199
+ const newValues = { ...filterState.values, [qualifierName]: value };
200
+ filterActions.updateFilterValues(newValues);
201
+ },
202
+ [filterState.values, filterActions]
203
+ );
204
+
205
+ // Handle resource selection
206
+ const handleResourceSelect = useCallback((resourceId: string) => {
207
+ setSelectedResourceId(resourceId);
208
+ }, []);
209
+
210
+ // Handle filter toggle
211
+ const handleFilterToggle = useCallback(
212
+ (enabled: boolean) => {
213
+ filterActions.updateFilterEnabled(enabled);
214
+ if (!enabled) {
215
+ onMessage?.('info', 'Filtering disabled - showing all resources');
216
+ } else {
217
+ onMessage?.('info', 'Filtering enabled - set qualifier values and click Apply to filter resources');
218
+ }
219
+ },
220
+ [filterActions, onMessage]
221
+ );
222
+
223
+ // Handle apply filter values
224
+ const handleApplyFilter = useCallback(() => {
225
+ filterActions.applyFilterValues();
226
+ onMessage?.('info', 'Filter applied - processing resources...');
227
+ }, [filterActions, onMessage]);
228
+
229
+ // Handle reset filter values
230
+ const handleResetFilter = useCallback(() => {
231
+ filterActions.resetFilterValues();
232
+ onMessage?.('info', 'Filter values reset');
233
+ }, [filterActions, onMessage]);
234
+
235
+ if (!resources) {
236
+ return (
237
+ <div className={`p-6 ${className}`}>
238
+ <div className="flex items-center space-x-3 mb-6">
239
+ <FunnelIcon className="h-8 w-8 text-purple-600" />
240
+ <h2 className="text-2xl font-bold text-gray-900">Filter Tool</h2>
241
+ </div>
242
+
243
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
244
+ <div className="max-w-2xl mx-auto">
245
+ <h3 className="text-xl font-semibold text-gray-900 mb-4">No Resources Loaded</h3>
246
+ <p className="text-gray-600 mb-6">
247
+ Import resources first to use the filter tool for context-based resource filtering.
248
+ </p>
249
+ <div className="bg-purple-50 rounded-lg p-4">
250
+ <p className="text-sm text-purple-800">
251
+ <strong>Filter Tool:</strong> Allows you to filter resources based on partial context
252
+ matching, creating focused subsets for analysis and testing.
253
+ </p>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ return (
262
+ <div className={`p-6 ${className}`}>
263
+ <div className="flex items-center space-x-3 mb-6">
264
+ <FunnelIcon className="h-8 w-8 text-purple-600" />
265
+ <h2 className="text-2xl font-bold text-gray-900">Filter Tool</h2>
266
+ </div>
267
+
268
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
269
+ {/* Filter Controls */}
270
+ <div className="mb-6">
271
+ <div className="flex items-center justify-between mb-4">
272
+ <h3 className="text-lg font-semibold text-gray-900">Filter Controls</h3>
273
+ <div className="flex items-center justify-between">
274
+ <div className="flex items-center space-x-4">
275
+ <label className="flex items-center">
276
+ <input
277
+ type="checkbox"
278
+ checked={filterState.enabled}
279
+ onChange={(e) => handleFilterToggle(e.target.checked)}
280
+ className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
281
+ />
282
+ <span className="ml-2 text-sm text-gray-700">Enable Filtering</span>
283
+ </label>
284
+ <label
285
+ className="flex items-center"
286
+ title="Remove perfectly matching qualifier conditions from filtered resources to create cleaner bundles for comparison"
287
+ >
288
+ <input
289
+ type="checkbox"
290
+ checked={filterState.reduceQualifiers}
291
+ onChange={(e) => filterActions.updateReduceQualifiers(e.target.checked)}
292
+ disabled={!filterState.enabled}
293
+ className="rounded border-gray-300 text-purple-600 focus:ring-purple-500 disabled:text-gray-400"
294
+ />
295
+ <span
296
+ className={`ml-2 text-sm ${!filterState.enabled ? 'text-gray-400' : 'text-gray-700'}`}
297
+ >
298
+ Reduce Qualifiers
299
+ </span>
300
+ </label>
301
+ {isFilteringActive && (
302
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
303
+ Active{filterState.reduceQualifiers ? ' + Reducing' : ''}
304
+ </span>
305
+ )}
306
+ {filterState.hasPendingChanges && filterState.enabled && (
307
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">
308
+ Pending Changes
309
+ </span>
310
+ )}
311
+ </div>
312
+
313
+ {filterState.enabled && (
314
+ <div className="flex items-center space-x-2">
315
+ <button
316
+ onClick={handleResetFilter}
317
+ 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"
318
+ >
319
+ <XMarkIcon className="h-4 w-4 mr-1" />
320
+ Reset
321
+ </button>
322
+ <button
323
+ onClick={handleApplyFilter}
324
+ disabled={!filterState.hasPendingChanges}
325
+ 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 ${
326
+ filterState.hasPendingChanges
327
+ ? 'text-white bg-purple-600 hover:bg-purple-700'
328
+ : 'text-gray-400 bg-gray-300 cursor-not-allowed'
329
+ }`}
330
+ >
331
+ <CheckIcon className="h-4 w-4 mr-1" />
332
+ Apply
333
+ </button>
334
+ </div>
335
+ )}
336
+ </div>
337
+ </div>
338
+ </div>
339
+
340
+ {/* Qualifier Selection Panel */}
341
+ <div className="mb-6">
342
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">Context Filters</h3>
343
+ <div className="bg-gray-50 rounded-lg p-4">
344
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
345
+ {availableQualifiers.map((qualifierName) => (
346
+ <QualifierContextControl
347
+ key={qualifierName}
348
+ qualifierName={qualifierName}
349
+ value={filterState.values[qualifierName]}
350
+ onChange={handleQualifierChange}
351
+ disabled={!filterState.enabled}
352
+ placeholder={`Filter by ${qualifierName}`}
353
+ resources={resources}
354
+ />
355
+ ))}
356
+ </div>
357
+ {filterState.enabled && (
358
+ <div className="mt-3 text-sm text-gray-600">
359
+ <div className="flex items-center justify-between">
360
+ <div className="space-y-1">
361
+ <p>
362
+ <strong>Pending:</strong> {getFilterSummary(filterState.values)}
363
+ </p>
364
+ {isFilteringActive && (
365
+ <p>
366
+ <strong>Applied:</strong> {getFilterSummary(filterState.appliedValues)}
367
+ </p>
368
+ )}
369
+ </div>
370
+ </div>
371
+ {filterResult && !filterResult.success && filterResult.error && (
372
+ <p className="text-red-600 text-xs mt-1">
373
+ <strong>Error:</strong> {filterResult.error}
374
+ </p>
375
+ )}
376
+ </div>
377
+ )}
378
+ </div>
379
+ </div>
380
+
381
+ {/* Main Browser/Details Layout */}
382
+ <div className="flex flex-col md:flex-row gap-6 h-[600px]">
383
+ {/* Left side: Resource List */}
384
+ <div className="md:w-1/2 flex flex-col">
385
+ <div className="flex items-center justify-between mb-4">
386
+ <div>
387
+ <h3 className="text-lg font-semibold text-gray-900">
388
+ {isFilteringActive ? 'Filtered Resources' : 'All Resources'}
389
+ </h3>
390
+ <div className="flex items-center space-x-2 text-sm text-gray-500 mt-1">
391
+ <span>{displayResources.length} resources</span>
392
+ {isFilteringActive && displayResources.some((r) => r.hasWarning) && (
393
+ <>
394
+ <span>•</span>
395
+ <span className="text-amber-600 flex items-center">
396
+ <ExclamationTriangleIcon className="h-4 w-4 mr-1" />
397
+ {displayResources.filter((r) => r.hasWarning).length} warnings
398
+ </span>
399
+ </>
400
+ )}
401
+ </div>
402
+ </div>
403
+ {/* View Mode Toggle */}
404
+ <div className="flex items-center space-x-1 bg-gray-100 rounded-lg p-1">
405
+ <button
406
+ onClick={() => setViewMode('list')}
407
+ className={`flex items-center px-2 py-1 text-xs font-medium rounded ${
408
+ viewMode === 'list'
409
+ ? 'bg-white text-gray-900 shadow-sm'
410
+ : 'text-gray-600 hover:text-gray-900'
411
+ }`}
412
+ title="List View"
413
+ >
414
+ <ListBulletIcon className="h-4 w-4" />
415
+ <span className="ml-1">List</span>
416
+ </button>
417
+ <button
418
+ onClick={() => setViewMode('tree')}
419
+ className={`flex items-center px-2 py-1 text-xs font-medium rounded ${
420
+ viewMode === 'tree'
421
+ ? 'bg-white text-gray-900 shadow-sm'
422
+ : 'text-gray-600 hover:text-gray-900'
423
+ }`}
424
+ title="Tree View"
425
+ >
426
+ <FolderIcon className="h-4 w-4" />
427
+ <span className="ml-1">Tree</span>
428
+ </button>
429
+ </div>
430
+ </div>
431
+
432
+ <div className="flex-1 overflow-y-auto border border-gray-200 rounded-lg bg-gray-50">
433
+ {viewMode === 'tree' && resources?.system.resourceManager ? (
434
+ <ResourceTreeView
435
+ resources={resources.system.resourceManager}
436
+ selectedResourceId={selectedResourceId}
437
+ onResourceSelect={handleResourceSelect}
438
+ searchTerm=""
439
+ className=""
440
+ />
441
+ ) : (
442
+ displayResources.map((resource) => (
443
+ <div
444
+ key={resource.id}
445
+ 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 ${
446
+ selectedResourceId === resource.id ? 'bg-purple-50 border-r-2 border-purple-500' : ''
447
+ }`}
448
+ onClick={() => handleResourceSelect(resource.id)}
449
+ >
450
+ <div className="flex items-center space-x-2 flex-1 min-w-0">
451
+ <DocumentTextIcon className="w-4 h-4 text-green-500 flex-shrink-0" />
452
+ <span
453
+ className={`text-sm truncate ${
454
+ selectedResourceId === resource.id ? 'font-medium text-purple-900' : 'text-gray-700'
455
+ }`}
456
+ >
457
+ {resource.id}
458
+ </span>
459
+ </div>
460
+ <div className="flex items-center space-x-2 flex-shrink-0">
461
+ {isFilteringActive && (
462
+ <div className="flex items-center space-x-1 text-xs">
463
+ <span className="text-gray-400">{resource.originalCandidateCount}</span>
464
+ <span className="text-gray-400">→</span>
465
+ <span
466
+ className={`font-medium ${
467
+ resource.filteredCandidateCount === 0
468
+ ? 'text-red-600'
469
+ : resource.filteredCandidateCount < resource.originalCandidateCount
470
+ ? 'text-amber-600'
471
+ : 'text-green-600'
472
+ }`}
473
+ >
474
+ {resource.filteredCandidateCount}
475
+ </span>
476
+ </div>
477
+ )}
478
+ {!isFilteringActive && (
479
+ <span className="text-xs text-gray-500">
480
+ {resource.originalCandidateCount} candidates
481
+ </span>
482
+ )}
483
+ {resource.hasWarning && (
484
+ <ExclamationTriangleIcon
485
+ className="h-4 w-4 text-amber-500"
486
+ title="No matching candidates"
487
+ />
488
+ )}
489
+ </div>
490
+ </div>
491
+ ))
492
+ )}
493
+ </div>
494
+ </div>
495
+
496
+ {/* Right side: Resource Details */}
497
+ <div className="md:w-1/2 flex flex-col">
498
+ <div className="flex items-center justify-between mb-4">
499
+ <h3 className="text-lg font-semibold text-gray-900">Resource Details</h3>
500
+ </div>
501
+
502
+ <div className="flex-1 overflow-y-auto border border-gray-200 rounded-lg p-4 bg-gray-50">
503
+ {!selectedResourceId ? (
504
+ <div className="flex items-center justify-center h-full">
505
+ <div className="text-center">
506
+ <FunnelIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
507
+ <p className="text-gray-500">Select a resource to view details</p>
508
+ {isFilteringActive && (
509
+ <p className="text-sm text-gray-400 mt-2">
510
+ Showing resources that match your filter criteria
511
+ </p>
512
+ )}
513
+ </div>
514
+ </div>
515
+ ) : (
516
+ <FilteredResourceDetail
517
+ resourceId={selectedResourceId}
518
+ processedResources={resources}
519
+ filterResult={filterResult}
520
+ isFilteringActive={isFilteringActive}
521
+ filterContext={filterState.appliedValues}
522
+ onMessage={onMessage}
523
+ />
524
+ )}
525
+ </div>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ );
531
+ };
532
+
533
+ interface FilteredResourceDetailProps {
534
+ resourceId: string;
535
+ processedResources: any;
536
+ filterResult: any;
537
+ isFilteringActive: boolean;
538
+ filterContext: Record<string, string | undefined>;
539
+ onMessage?: (type: 'info' | 'warning' | 'error' | 'success', message: string) => void;
540
+ }
541
+
542
+ const FilteredResourceDetail: React.FC<FilteredResourceDetailProps> = ({
543
+ resourceId,
544
+ processedResources,
545
+ filterResult,
546
+ isFilteringActive,
547
+ filterContext,
548
+ onMessage
549
+ }) => {
550
+ const [resourceDetail, setResourceDetail] = useState<any>(null);
551
+ const [filteredResourceDetail, setFilteredResourceDetail] = useState<any>(null);
552
+ const [isLoading, setIsLoading] = useState(false);
553
+ const [error, setError] = useState<string | null>(null);
554
+ const [showFilteredView, setShowFilteredView] = useState(true);
555
+
556
+ React.useEffect(() => {
557
+ const loadResourceDetails = () => {
558
+ setIsLoading(true);
559
+ setError(null);
560
+
561
+ try {
562
+ // Load original resource details
563
+ const resourceManager = processedResources.system.resourceManager;
564
+ const resourceResult = resourceManager.getBuiltResource(resourceId);
565
+
566
+ if (resourceResult.isSuccess()) {
567
+ const resource = resourceResult.value;
568
+ const originalDetail = {
569
+ id: resource.id,
570
+ resourceType: resource.resourceType.key,
571
+ candidateCount: resource.candidates.length,
572
+ candidates: resource.candidates.map((candidate: any) => ({
573
+ json: candidate.json,
574
+ conditions: candidate.conditions.conditions.map((condition: any) => ({
575
+ qualifier: condition.qualifier.name,
576
+ operator: condition.operator,
577
+ value: condition.value,
578
+ priority: condition.priority,
579
+ scoreAsDefault: condition.scoreAsDefault
580
+ })),
581
+ isPartial: candidate.isPartial,
582
+ mergeMethod: candidate.mergeMethod
583
+ }))
584
+ };
585
+ setResourceDetail(originalDetail);
586
+
587
+ // Load filtered resource details if filtering is active
588
+ if (isFilteringActive && filterResult?.processedResources) {
589
+ const filteredResourceManager = filterResult.processedResources.system.resourceManager;
590
+ const filteredResourceResult = filteredResourceManager.getBuiltResource(resourceId);
591
+
592
+ if (filteredResourceResult.isSuccess()) {
593
+ const filteredResource = filteredResourceResult.value;
594
+ const filteredDetail = {
595
+ id: filteredResource.id,
596
+ resourceType: filteredResource.resourceType.key,
597
+ candidateCount: filteredResource.candidates.length,
598
+ candidates: filteredResource.candidates.map((candidate: any) => ({
599
+ json: candidate.json,
600
+ conditions: candidate.conditions.conditions.map((condition: any) => ({
601
+ qualifier: condition.qualifier.name,
602
+ operator: condition.operator,
603
+ value: condition.value,
604
+ priority: condition.priority,
605
+ scoreAsDefault: condition.scoreAsDefault
606
+ })),
607
+ isPartial: candidate.isPartial,
608
+ mergeMethod: candidate.mergeMethod
609
+ }))
610
+ };
611
+ setFilteredResourceDetail(filteredDetail);
612
+ }
613
+ }
614
+
615
+ onMessage?.('info', `Loaded details for resource: ${resourceId}`);
616
+ } else {
617
+ setError(`Failed to load resource details: ${resourceResult.message}`);
618
+ onMessage?.('error', `Failed to load resource details: ${resourceResult.message}`);
619
+ }
620
+ } catch (err) {
621
+ const errorMsg = `Error loading resource details: ${
622
+ err instanceof Error ? err.message : String(err)
623
+ }`;
624
+ setError(errorMsg);
625
+ onMessage?.('error', errorMsg);
626
+ } finally {
627
+ setIsLoading(false);
628
+ }
629
+ };
630
+
631
+ loadResourceDetails();
632
+ }, [resourceId, processedResources, filterResult, isFilteringActive, onMessage]);
633
+
634
+ if (isLoading) {
635
+ return (
636
+ <div className="bg-white rounded-lg border border-gray-200 p-4">
637
+ <h4 className="font-medium text-gray-900 mb-2">Resource Details</h4>
638
+ <div className="flex items-center justify-center py-8">
639
+ <div className="text-center">
640
+ <div className="animate-spin h-6 w-6 border-2 border-purple-600 border-t-transparent rounded-full mx-auto mb-2"></div>
641
+ <p className="text-sm text-gray-500">Loading resource details...</p>
642
+ </div>
643
+ </div>
644
+ </div>
645
+ );
646
+ }
647
+
648
+ if (error) {
649
+ return (
650
+ <div className="bg-white rounded-lg border border-red-200 p-4">
651
+ <h4 className="font-medium text-red-900 mb-2">Resource Details</h4>
652
+ <div className="bg-red-50 p-3 rounded">
653
+ <p className="text-sm text-red-600">{error}</p>
654
+ </div>
655
+ </div>
656
+ );
657
+ }
658
+
659
+ if (!resourceDetail) {
660
+ return (
661
+ <div className="bg-white rounded-lg border border-gray-200 p-4">
662
+ <h4 className="font-medium text-gray-900 mb-2">Resource Details</h4>
663
+ <p className="text-sm text-gray-500">No resource details available</p>
664
+ </div>
665
+ );
666
+ }
667
+
668
+ const currentDetail = showFilteredView && filteredResourceDetail ? filteredResourceDetail : resourceDetail;
669
+ const isShowingFiltered = showFilteredView && filteredResourceDetail;
670
+
671
+ return (
672
+ <div className="bg-white rounded-lg border border-gray-200 p-4">
673
+ <div className="flex items-center justify-between mb-4">
674
+ <h4 className="font-medium text-gray-900">Resource Details</h4>
675
+ {isFilteringActive && filteredResourceDetail && (
676
+ <div className="flex items-center space-x-2">
677
+ <span className="text-xs text-gray-500">View:</span>
678
+ <button
679
+ onClick={() => setShowFilteredView(false)}
680
+ className={`px-2 py-1 text-xs rounded ${
681
+ !showFilteredView
682
+ ? 'bg-blue-100 text-blue-800 font-medium'
683
+ : 'text-gray-600 hover:bg-gray-100'
684
+ }`}
685
+ >
686
+ Original ({resourceDetail.candidateCount})
687
+ </button>
688
+ <button
689
+ onClick={() => setShowFilteredView(true)}
690
+ className={`px-2 py-1 text-xs rounded ${
691
+ showFilteredView
692
+ ? 'bg-purple-100 text-purple-800 font-medium'
693
+ : 'text-gray-600 hover:bg-gray-100'
694
+ }`}
695
+ >
696
+ Filtered ({filteredResourceDetail.candidateCount})
697
+ </button>
698
+ </div>
699
+ )}
700
+ </div>
701
+
702
+ <div className="space-y-4">
703
+ {/* Resource Overview */}
704
+ <div>
705
+ <div className="bg-gray-50 p-3 rounded border space-y-2">
706
+ <div className="flex items-center space-x-2">
707
+ <span className="text-sm font-medium text-gray-600">Resource ID:</span>
708
+ <code className="text-sm bg-white px-2 py-1 rounded border break-all">{currentDetail.id}</code>
709
+ </div>
710
+ <div className="flex items-center space-x-2">
711
+ <span className="text-sm font-medium text-gray-600">Type:</span>
712
+ <span className="text-sm">{currentDetail.resourceType}</span>
713
+ </div>
714
+ <div className="flex items-center space-x-2">
715
+ <span className="text-sm font-medium text-gray-600">Candidates:</span>
716
+ <span
717
+ className={`text-sm font-medium ${
718
+ isShowingFiltered && currentDetail.candidateCount === 0
719
+ ? 'text-red-600'
720
+ : isShowingFiltered && currentDetail.candidateCount < resourceDetail.candidateCount
721
+ ? 'text-amber-600'
722
+ : 'text-green-600'
723
+ }`}
724
+ >
725
+ {currentDetail.candidateCount}
726
+ {isShowingFiltered && currentDetail.candidateCount !== resourceDetail.candidateCount && (
727
+ <span className="text-gray-400 ml-1">(was {resourceDetail.candidateCount})</span>
728
+ )}
729
+ </span>
730
+ </div>
731
+ {isFilteringActive && (
732
+ <div className="flex items-start space-x-2">
733
+ <span className="text-sm font-medium text-gray-600">Filter:</span>
734
+ <span className="text-sm text-purple-700">
735
+ {Object.entries(filterContext)
736
+ .filter(([, value]) => value !== undefined && value !== '')
737
+ .map(([key, value]) => `${key}=${value}`)
738
+ .join(', ') || 'No filters'}
739
+ </span>
740
+ </div>
741
+ )}
742
+ </div>
743
+ </div>
744
+
745
+ {/* Candidates */}
746
+ {currentDetail.candidates.length > 0 ? (
747
+ <div>
748
+ <h5 className="font-medium text-gray-700 mb-2">
749
+ {isShowingFiltered ? 'Filtered ' : ''}Candidates ({currentDetail.candidates.length})
750
+ </h5>
751
+ <div className="space-y-3 max-h-96 overflow-y-auto">
752
+ {currentDetail.candidates.map((candidate: any, index: number) => (
753
+ <div key={index} className="bg-gray-50 p-3 rounded border">
754
+ <div className="flex items-center justify-between mb-2">
755
+ <h6 className="font-medium text-gray-800 text-sm">Candidate {index + 1}</h6>
756
+ <div className="flex items-center space-x-2 text-xs">
757
+ {candidate.isPartial && (
758
+ <span className="bg-yellow-100 text-yellow-800 px-2 py-0.5 rounded">Partial</span>
759
+ )}
760
+ <span className="bg-gray-200 text-gray-700 px-2 py-0.5 rounded">
761
+ {candidate.mergeMethod}
762
+ </span>
763
+ </div>
764
+ </div>
765
+
766
+ {/* Conditions */}
767
+ {candidate.conditions.length > 0 ? (
768
+ <div className="mb-2">
769
+ <h6 className="text-xs font-medium text-gray-600 mb-1">Conditions:</h6>
770
+ <div className="space-y-1">
771
+ {candidate.conditions.map((condition: any, condIndex: number) => (
772
+ <div
773
+ key={condIndex}
774
+ className="flex items-center justify-between text-xs bg-blue-50 px-2 py-1 rounded"
775
+ >
776
+ <div className="flex items-center space-x-1">
777
+ <span className="font-medium text-blue-800">{condition.qualifier}</span>
778
+ <span className="text-blue-600">{condition.operator}</span>
779
+ <span className="text-blue-700">{condition.value}</span>
780
+ </div>
781
+ <div className="flex items-center space-x-2 text-xs">
782
+ <span className="text-blue-500">p:{condition.priority}</span>
783
+ {condition.scoreAsDefault !== undefined && (
784
+ <span className="text-amber-600 font-medium">
785
+ d:{condition.scoreAsDefault}
786
+ </span>
787
+ )}
788
+ </div>
789
+ </div>
790
+ ))}
791
+ </div>
792
+ </div>
793
+ ) : (
794
+ <div className="mb-2">
795
+ <span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded">
796
+ No conditions (default)
797
+ </span>
798
+ </div>
799
+ )}
800
+
801
+ {/* JSON Content */}
802
+ <div>
803
+ <h6 className="text-xs font-medium text-gray-600 mb-1">Content:</h6>
804
+ <pre className="text-xs bg-white p-2 rounded border overflow-x-auto max-h-32 overflow-y-auto">
805
+ {JSON.stringify(candidate.json, null, 2)}
806
+ </pre>
807
+ </div>
808
+ </div>
809
+ ))}
810
+ </div>
811
+ </div>
812
+ ) : (
813
+ <div className="bg-red-50 border border-red-200 p-3 rounded">
814
+ <p className="text-sm text-red-700 font-medium">No candidates match the filter</p>
815
+ <p className="text-xs text-red-600 mt-1">
816
+ This resource has been completely filtered out. Consider adjusting your filter criteria.
817
+ </p>
818
+ </div>
819
+ )}
820
+ </div>
821
+ </div>
822
+ );
823
+ };
824
+
825
+ export default FilterView;