@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.
- package/cli.js +132 -21
- package/dist/bundle.js +1 -1
- package/dist/bundle.js.LICENSE.txt +11 -1
- package/package.json +42 -36
- package/.rush/temp/587277bfc395406ad444e989b6c309b9698f534c.tar.log +0 -15
- package/.rush/temp/chunked-rush-logs/ts-res-browser.build.chunks.jsonl +0 -35
- package/.rush/temp/operation/build/all.log +0 -35
- package/.rush/temp/operation/build/log-chunks.jsonl +0 -35
- package/.rush/temp/operation/build/state.json +0 -3
- package/.rush/temp/shrinkwrap-deps.json +0 -1225
- package/TS_RES_BROWSER_PLAN.md +0 -689
- package/babel.config.js +0 -17
- package/config/jest.config.json +0 -13
- package/config/rig.json +0 -16
- package/postcss.config.js +0 -3
- package/public/index.html +0 -15
- package/rush-logs/ts-res-browser.build.cache.log +0 -3
- package/rush-logs/ts-res-browser.build.log +0 -35
- package/src/App.tsx +0 -274
- package/src/cli.ts +0 -44
- package/src/components/common/FileImporter.tsx +0 -143
- package/src/components/common/NavigationWarningModal.tsx +0 -62
- package/src/components/layout/AppLayout.tsx +0 -46
- package/src/components/layout/Header.tsx +0 -22
- package/src/components/layout/MessagesWindow.tsx +0 -108
- package/src/components/layout/Sidebar.tsx +0 -99
- package/src/components/tools/CompiledBrowser.tsx +0 -949
- package/src/components/tools/ConfigurationTool.tsx +0 -1405
- package/src/components/tools/FilterTool.tsx +0 -1114
- package/src/components/tools/ImportTool.tsx +0 -249
- package/src/components/tools/ResolutionViewer.tsx +0 -1672
- package/src/components/tools/SourceBrowser.tsx +0 -496
- package/src/components/tools/ZipLoader.tsx +0 -302
- package/src/hooks/useAppState.ts +0 -111
- package/src/hooks/useFileImport.ts +0 -86
- package/src/hooks/useNavigationWarning.ts +0 -67
- package/src/hooks/useResourceManager.ts +0 -295
- package/src/hooks/useUrlParams.ts +0 -56
- package/src/index.css +0 -38
- package/src/main.tsx +0 -16
- package/src/types/app.ts +0 -70
- package/src/types/fileSystemAccess.d.ts +0 -94
- package/src/utils/browserFileTreeAccessors.ts +0 -245
- package/src/utils/fileImport.ts +0 -336
- package/src/utils/fileTreeConverter.ts +0 -160
- package/src/utils/filterResources.ts +0 -677
- package/src/utils/tsResIntegration.ts +0 -467
- package/src/utils/urlParams.ts +0 -238
- package/src/utils/zip/browserZipFileTreeAccessors.ts +0 -385
- package/tailwind.config.js +0 -21
- package/test-data/README.md +0 -184
- package/test-data/config-variations/README.md +0 -64
- package/test-data/config-variations/composition-terminology.json +0 -112
- package/test-data/config-variations/enterprise-complex.json +0 -129
- package/test-data/config-variations/gaming-app.json +0 -87
- package/test-data/config-variations/high-priority-language.json +0 -65
- package/test-data/config-variations/minimal-basic.json +0 -37
- package/test-data/config-variations/qualifier-default-values.json +0 -71
- package/test-data/config-variations/territory-first.json +0 -68
- package/test-data/config-variations/territory-hierarchy-test.json +0 -31
- package/test-data/config-variations/territory-hierarchy-unconstrained.json +0 -32
- package/test-data/default-matching-test/greeting.res.json +0 -83
- package/test-data/default-matching-test/only-defaults.res.json +0 -48
- package/test-data/sample-project/package.json +0 -27
- package/test-data/sample-project/src/resources/app-config.json +0 -27
- package/test-data/sample-project/src/resources/app.json +0 -177
- package/test-data/sample-project/src/resources/config/app.env=development.json +0 -21
- package/test-data/sample-project/src/resources/config/app.json +0 -21
- package/test-data/sample-project/src/resources/config/app.platform=mobile.json +0 -23
- package/test-data/sample-project/src/resources/feature-flags.json +0 -23
- package/test-data/sample-project/src/resources/home=CA/common.json +0 -16
- package/test-data/sample-project/src/resources/images/density=hdpi.json +0 -29
- package/test-data/sample-project/src/resources/images/home.json +0 -29
- package/test-data/sample-project/src/resources/images/logout.json +0 -14
- package/test-data/sample-project/src/resources/images/settings.json +0 -14
- package/test-data/sample-project/src/resources/images/ui-icons.json +0 -29
- package/test-data/sample-project/src/resources/images/user.density=hdpi.json +0 -14
- package/test-data/sample-project/src/resources/images/user.json +0 -14
- package/test-data/sample-project/src/resources/lang=en-US/common.json +0 -15
- package/test-data/sample-project/src/resources/language=en-CA/ui-terms.json +0 -19
- package/test-data/sample-project/src/resources/language=en-GB/ui-terms.json +0 -30
- package/test-data/sample-project/src/resources/language=fr-CA/ui-terms.json +0 -25
- package/test-data/sample-project/src/resources/language=nl-BE/ui-terms.json +0 -17
- package/test-data/sample-project/src/resources/region=europe/feature-flags.json +0 -17
- package/test-data/sample-project/src/resources/region=north-america/feature-flags.json +0 -18
- package/test-data/sample-project/src/resources/strings/dashboard.home=CA,language=fr.json +0 -20
- package/test-data/sample-project/src/resources/strings/dashboard.json +0 -18
- package/test-data/sample-project/src/resources/strings/dashboard.role=admin.json +0 -23
- package/test-data/sample-project/src/resources/strings/dashboard.role=user.json +0 -21
- package/test-data/sample-project/src/resources/strings/language=en-US.json +0 -16
- package/test-data/sample-project/src/resources/strings.json +0 -78
- package/test-data/sample-project/src/resources/territory=BE/app-config.json +0 -19
- package/test-data/sample-project/src/resources/territory=CA/app-config.json +0 -21
- package/test-data/sample-project/src/resources/territory=GB/app-config.json +0 -25
- package/test-data/sample-project/src/resources/territory=NL/app-config.json +0 -25
- package/test-data/sample-project/src/resources/ui-terms.json +0 -96
- package/test-data/sample-project/src/resources-config.json +0 -111
- package/tsconfig.json +0 -27
- 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;
|