Skip to content

Commit 705e13d

Browse files
Prototype of X-Model Search (#1175)
* Working state * Prototype * Black formatting * Prettier * Prettier for translate * Prettier for paging-filter.js * Prettier for search-export.js * Component-based translation feature * Checkpoint * Grouping checkpoint * Querying checkpoint * Prototype of cross-model search * Fix migration, prioritize tiles, sort alphabetically * Fix bugs, handle pagination, update UI/UX, improve performance in some areas * Fix bugs, handle pagination, update UI/UX, improve performance in some areas * Fix performance, update maximum terms * Reduce code, more performant, better support for search cases * Update migration names and references * Reformat w/ prettier * Disable custom ES mappings for x-model search clarity (#1198) * Update documentation * Remove paging-filter.js and search-export.js * Fix qualifier search, short-circuit condition * Fix black format issue --------- Co-authored-by: brett <brett@qedsystems.ca>
1 parent 2746929 commit 705e13d

17 files changed

+6109
-88
lines changed

bcap/media/js/views/components/search/cross-model-advanced-search.js

Lines changed: 996 additions & 0 deletions
Large diffs are not rendered by default.

bcap/media/js/views/components/search/search-results.js

Lines changed: 399 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import arches from 'arches';
2+
import ko from 'knockout';
3+
import BaseFilter from 'views/components/search/base-filter';
4+
import translateToResourceTypeFilterTemplate from 'templates/views/components/search/translate-to-resource-type-filter.htm';
5+
6+
var componentName = 'translate-to-resource-type-filter';
7+
8+
var viewModel = BaseFilter.extend({
9+
initialize: async function (options) {
10+
options.name = 'Translate to Resource Type Filter';
11+
BaseFilter.prototype.initialize.call(this, options);
12+
13+
this.available_resource_types = ko.observableArray([]);
14+
this.is_loading_types = ko.observable(true);
15+
this.is_translating = ko.observable(false);
16+
this.original_filters = null;
17+
this.is_original_filters_stale = false;
18+
this.translation_error = ko.observable(null);
19+
this.translation_tag = ko.observable(null);
20+
this.is_query_set = false;
21+
this.is_updating_tag = false;
22+
23+
var self = this;
24+
25+
this.query.subscribe(function (new_query) {
26+
var has_ids = !!new_query['ids'];
27+
28+
if (!self.is_query_set) {
29+
if (has_ids) {
30+
self.is_original_filters_stale = true;
31+
} else {
32+
self.original_filters = null;
33+
self.is_original_filters_stale = false;
34+
self.translation_error(null);
35+
if (self.translation_tag()) {
36+
self.is_updating_tag = true;
37+
self.getFilterByType('term-filter-type').removeTag(
38+
self.translation_tag(),
39+
);
40+
self.is_updating_tag = false;
41+
self.translation_tag(null);
42+
}
43+
window.translation_source_mapping = {};
44+
window.get_translation_source = null;
45+
}
46+
}
47+
self.is_query_set = false;
48+
}, this);
49+
50+
var response = await fetch(
51+
arches.urls.root + 'api/translatable-resource-types',
52+
{
53+
method: 'GET',
54+
headers: { 'Content-Type': 'application/json' },
55+
},
56+
);
57+
58+
if (response.ok) {
59+
var data = await response.json();
60+
self.is_loading_types(false);
61+
if (data.status === 'success' && data.resource_types) {
62+
self.available_resource_types(data.resource_types);
63+
}
64+
} else {
65+
self.is_loading_types(false);
66+
console.error('Failed to fetch resource types');
67+
}
68+
69+
this.searchFilterVms[componentName](this);
70+
},
71+
72+
get_csrf_token: function () {
73+
var cookie_value = null;
74+
if (document.cookie && document.cookie !== '') {
75+
var cookies = document.cookie.split(';');
76+
for (var i = 0; i < cookies.length; i++) {
77+
var cookie = cookies[i].trim();
78+
if (cookie.substring(0, 10) === 'csrftoken=') {
79+
cookie_value = decodeURIComponent(cookie.substring(10));
80+
break;
81+
}
82+
}
83+
}
84+
return cookie_value;
85+
},
86+
87+
capture_filters_from_query: function (query) {
88+
var excluded_keys = [
89+
'paging-filter',
90+
'ids',
91+
'tiles',
92+
'csrfmiddlewaretoken',
93+
];
94+
var filters = {};
95+
96+
for (var key in query) {
97+
if (
98+
query.hasOwnProperty(key) &&
99+
excluded_keys.indexOf(key) === -1
100+
) {
101+
filters[key] = query[key];
102+
}
103+
}
104+
105+
return filters;
106+
},
107+
108+
translate_to_resource_type_by_id: function (graph_id) {
109+
var self = this;
110+
111+
if (!graph_id) {
112+
self.translation_error('Please select a resource type.');
113+
return;
114+
}
115+
116+
self.is_translating(true);
117+
self.translation_error(null);
118+
119+
var current_query = self.query();
120+
var filters_to_use;
121+
122+
if (self.original_filters !== null && !self.is_original_filters_stale) {
123+
filters_to_use = self.original_filters;
124+
} else {
125+
filters_to_use = self.capture_filters_from_query(current_query);
126+
}
127+
128+
var search_params = new URLSearchParams();
129+
search_params.set('paging-filter', '1');
130+
search_params.set('target_graph_id', graph_id);
131+
132+
for (var key in filters_to_use) {
133+
if (filters_to_use.hasOwnProperty(key) && filters_to_use[key]) {
134+
search_params.set(key, filters_to_use[key]);
135+
}
136+
}
137+
138+
fetch(arches.urls.root + 'api/translate-to-resource-type', {
139+
method: 'POST',
140+
headers: {
141+
'Content-Type': 'application/x-www-form-urlencoded',
142+
'X-CSRFToken': self.get_csrf_token(),
143+
},
144+
body: search_params.toString(),
145+
})
146+
.then(function (response) {
147+
return response.json();
148+
})
149+
.then(function (data) {
150+
self.is_translating(false);
151+
152+
if (data.status === 'error') {
153+
self.translation_error(data.message);
154+
return;
155+
}
156+
157+
if (data.resource_ids && data.resource_ids.length > 0) {
158+
self.original_filters = filters_to_use;
159+
self.is_original_filters_stale = false;
160+
self.apply_resource_filter(
161+
data.resource_ids,
162+
data.source_mapping,
163+
data.source_resource_type_name,
164+
data.target_resource_type_name,
165+
);
166+
} else {
167+
self.translation_error(
168+
'No related resources found for the selected type.',
169+
);
170+
}
171+
})
172+
.catch(function (error) {
173+
self.is_translating(false);
174+
self.translation_error('An error occurred during translation.');
175+
console.error('Translation error:', error);
176+
});
177+
},
178+
179+
apply_resource_filter: function (
180+
resource_ids,
181+
source_mapping,
182+
source_name,
183+
target_name,
184+
) {
185+
var self = this;
186+
187+
if (self.translation_tag()) {
188+
self.is_updating_tag = true;
189+
self.getFilterByType('term-filter-type').removeTag(
190+
self.translation_tag(),
191+
);
192+
self.is_updating_tag = false;
193+
}
194+
195+
var tag_text = source_name + ' \u2192 ' + target_name;
196+
self.translation_tag(tag_text);
197+
self.getFilterByType('term-filter-type').addTag(
198+
tag_text,
199+
self.name,
200+
ko.observable(false),
201+
);
202+
203+
var queryObj = {};
204+
queryObj['paging-filter'] = '1';
205+
queryObj['ids'] = JSON.stringify(resource_ids);
206+
207+
window.translation_source_mapping = source_mapping || {};
208+
window.get_translation_source = function (resourceinstanceid) {
209+
var sources = window.translation_source_mapping[resourceinstanceid];
210+
if (sources && sources.length > 0) {
211+
return sources;
212+
}
213+
return null;
214+
};
215+
216+
self.is_query_set = true;
217+
self.query(queryObj);
218+
},
219+
220+
clear: function () {
221+
var self = this;
222+
223+
if (self.is_updating_tag) {
224+
return;
225+
}
226+
227+
if (self.translation_tag()) {
228+
self.is_updating_tag = true;
229+
self.getFilterByType('term-filter-type').removeTag(
230+
self.translation_tag(),
231+
);
232+
self.is_updating_tag = false;
233+
self.translation_tag(null);
234+
}
235+
236+
window.translation_source_mapping = {};
237+
window.get_translation_source = null;
238+
self.translation_error(null);
239+
240+
var queryObj = {};
241+
queryObj['paging-filter'] = '1';
242+
243+
if (self.original_filters && !self.is_original_filters_stale) {
244+
for (var key in self.original_filters) {
245+
if (
246+
self.original_filters.hasOwnProperty(key) &&
247+
self.original_filters[key]
248+
) {
249+
queryObj[key] = self.original_filters[key];
250+
}
251+
}
252+
}
253+
254+
self.original_filters = null;
255+
self.is_original_filters_stale = false;
256+
self.is_query_set = true;
257+
self.query(queryObj);
258+
},
259+
});
260+
261+
export default ko.components.register(componentName, {
262+
viewModel: viewModel,
263+
template: translateToResourceTypeFilterTemplate,
264+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from django.db import migrations
2+
3+
4+
class Migration(migrations.Migration):
5+
dependencies = [
6+
("bcap", "1100_show_etl_plugin_by_default"),
7+
]
8+
9+
operations = [
10+
migrations.RunSQL(
11+
sql="""
12+
INSERT INTO search_component (
13+
searchcomponentid,
14+
name,
15+
icon,
16+
modulename,
17+
classname,
18+
type,
19+
componentpath,
20+
componentname,
21+
config
22+
) VALUES (
23+
'b2c3d4e5-f6a7-8901-bcde-f12345678901',
24+
'Translate to Resource Type',
25+
'fa fa-exchange',
26+
'translate_to_resource_type_filter.py',
27+
'TranslateToResourceTypeFilter',
28+
'filter',
29+
'views/components/search/translate-to-resource-type-filter',
30+
'translate-to-resource-type-filter',
31+
'{}'::jsonb
32+
)
33+
ON CONFLICT (searchcomponentid) DO NOTHING;
34+
35+
UPDATE search_component
36+
SET config = jsonb_set(
37+
config,
38+
'{linkedSearchFilters}',
39+
config->'linkedSearchFilters' || '[{
40+
"componentname": "translate-to-resource-type-filter",
41+
"searchcomponentid": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
42+
"layoutSortorder": 15
43+
}]'::jsonb
44+
)
45+
WHERE componentname = 'standard-search-view';
46+
""",
47+
reverse_sql="""
48+
UPDATE search_component
49+
SET config = jsonb_set(
50+
config,
51+
'{linkedSearchFilters}',
52+
(
53+
SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
54+
FROM jsonb_array_elements(config->'linkedSearchFilters') elem
55+
WHERE elem->>'componentname' != 'translate-to-resource-type-filter'
56+
)
57+
)
58+
WHERE componentname = 'standard-search-view';
59+
60+
DELETE FROM search_component
61+
WHERE componentname = 'translate-to-resource-type-filter';
62+
""",
63+
),
64+
]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.db import migrations
2+
3+
4+
class Migration(migrations.Migration):
5+
dependencies = [
6+
("bcap", "1182a_add_translate_to_resource_type"),
7+
]
8+
9+
operations = [
10+
migrations.RunSQL(
11+
sql="""
12+
INSERT INTO search_component (
13+
searchcomponentid,
14+
name,
15+
icon,
16+
modulename,
17+
classname,
18+
type,
19+
componentpath,
20+
componentname,
21+
config
22+
) VALUES (
23+
'f1856bfb-c3c4-4d67-8f23-0aa3eef3a160',
24+
'ResourceIds Filter',
25+
'',
26+
'ids.py',
27+
'ResourceIdsFilter',
28+
'ids-filter-type',
29+
NULL,
30+
'ids',
31+
'{}'::jsonb
32+
)
33+
ON CONFLICT (searchcomponentid) DO NOTHING;
34+
""",
35+
reverse_sql="""
36+
DELETE FROM search_component
37+
WHERE componentname = 'ids';
38+
""",
39+
),
40+
]

0 commit comments

Comments
 (0)