Merge lp:~ben-hutchings/ensoft-sextant/autocomplete-fix into lp:ensoft-sextant

Proposed by Ben Hutchings
Status: Superseded
Proposed branch: lp:~ben-hutchings/ensoft-sextant/autocomplete-fix
Merge into: lp:ensoft-sextant
Diff against target: 344 lines (+112/-69)
4 files modified
resources/sextant/web/interface.html (+6/-5)
resources/sextant/web/queryjavascript.js (+78/-54)
src/sextant/db_api.py (+8/-3)
src/sextant/web/server.py (+20/-7)
To merge this branch: bzr merge lp:~ben-hutchings/ensoft-sextant/autocomplete-fix
Reviewer Review Type Date Requested Status
Robert Pending
Review via email: mp+242190@code.launchpad.net

This proposal supersedes a proposal from 2014-11-13.

This proposal has been superseded by a proposal from 2014-11-21.

Description of the change

Web gui now has a hard limit on how many functions it is willing to get for the autocomplete menu. If there are more than this number of functions in the program, the empty list will be returned. Maximum number of functions is set to 75 at present - more than this leads to drop down lists not displaying properly sometimes.

Substring search in function names. Debouncing (one second timer at present).

Fixed bug that was completely stopping the autocomplete from being called!

To post a comment you must log in.
Revision history for this message
Ben Hutchings (ben-hutchings) wrote : Posted in a previous version of this proposal

Also the limit is hardcoded in web/server.py, not db_api.py.

Revision history for this message
Robert (rjwills) : Posted in a previous version of this proposal
review: Approve
37. By Ben Hutchings

whitespace changes, added missing var

38. By Ben Hutchings

indentation fix

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'resources/sextant/web/interface.html'
2--- resources/sextant/web/interface.html 2014-09-04 09:46:18 +0000
3+++ resources/sextant/web/interface.html 2014-11-21 12:32:15 +0000
4@@ -17,8 +17,8 @@
5 <div class="toolbar">
6 <div>
7 Program:
8- <input list="program_names" id="program_name" class="textbox"
9- onblur="get_names_for_autocomplete('funcs')" style="width: 150px" />
10+ <input list="program_names" id="program_name" class="textbox" style="width: 150px" />
11+ <!-- onblur="get_names_for_autocomplete('funcs')" -->
12 <datalist id="program_names"></datalist>
13
14 <select id="query_list" class="dropdown" onchange="display_when();">
15@@ -44,13 +44,14 @@
16 </div>
17 <div id="toolbar-row2" style="margin-left: 234px;">
18 <!--list to populate arguments. Updates when program name is specified-->
19- <datalist id="function_names"></datalist>
20+ <datalist id="function_names_1"></datalist>
21+ <datalist id="function_names_2"></datalist>
22
23 <span id="argument_1" style="size:20;"></span>
24- <input list="function_names" id="function_1" class="textbox" style="size:20;"></input>
25+ <input list="function_names_1" autocomplete="off" id="function_1" class="textbox" style="size:20;" onkeyup="get_names_for_autocomplete('funcs_with_timeout_1')"></input>
26
27 <span id="argument_2" style="size:20;"></span>
28- <input list="function_names" id="function_2" class="textbox" style="size:20;"></input>
29+ <input list="function_names_2" autocomplete="off" id="function_2" class="textbox" style="size:20;" onkeyup="get_names_for_autocomplete('funcs_with_timeout_2')"></input>
30 </div>
31 </div>
32
33
34=== modified file 'resources/sextant/web/queryjavascript.js'
35--- resources/sextant/web/queryjavascript.js 2014-09-29 21:08:33 +0000
36+++ resources/sextant/web/queryjavascript.js 2014-11-21 12:32:15 +0000
37@@ -6,54 +6,84 @@
38 //server or function names from a specific program.
39
40
41-function get_names_for_autocomplete(info_needed){
42+var timeout = null;
43+var do_timeout = false;
44+
45+
46+function get_names_for_autocomplete(info_needed, search) {
47 //Function queries to database to create a list
48 //which is used to populate the auto-complete text boxes.
49+ if (typeof search == 'undefined') {
50+ search = "";
51+ }
52+
53 var xmlhttp = new XMLHttpRequest();
54- xmlhttp.onreadystatechange = function(){
55- if (xmlhttp.status = 200){
56+ xmlhttp.onreadystatechange = function() {
57+ if (xmlhttp.status = 200) {
58 var values_list = xmlhttp.responseText;
59 if (values_list != "") {
60 values_list = JSON.parse(values_list);
61- if (info_needed =='programs'){
62+ if (info_needed =='programs') {
63 //We need to populate the program names list
64 add_options("program_names", values_list);
65- }
66- if (info_needed =='funcs'){
67- //We need to populate the functions list for the arguments
68- add_options("function_names", values_list);
69- }
70+ do_timeout = false;
71+ }
72+ if (info_needed == 'funcs_with_timeout_1') {
73+ add_options("function_names_1", values_list);
74+ do_timeout = true;
75+ }
76+ if (info_needed == 'funcs_with_timeout_2') {
77+ add_options("function_names_2", values_list);
78+ do_timeout = true;
79+ }
80 }
81 }
82 }
83- if (info_needed == 'programs'){
84+
85+ if (info_needed == 'programs') {
86 var string = "/database_properties?query=" + info_needed + "&program_name=";
87- }
88- else{
89- var string = "/database_properties?query=" + "functions" +
90- "&program_name=" + document.getElementById("program_name").value;
91- if (info_needed == 'programs'){
92- var string = "/database_properties?query=" +
93- info_needed + "&program_name=" + prog_name;
94- }
95+ xmlhttp.open("GET", string, true);
96+ xmlhttp.send();
97+ } else {
98+ var target = null;
99+
100+ if (info_needed == 'funcs_with_timeout_1') {
101+ target = 'function_1';
102+ } else {
103+ target = 'function_2';
104+ }
105+
106+ var timeoutfn = function() {
107+ var string = "/database_properties?query=" + "functions" +
108+ "&program_name=" + document.getElementById("program_name").value +
109+ "&search=" + document.getElementById(target).value;
110+ xmlhttp.open("GET", string, true);
111+ xmlhttp.send();
112+ timeout = null;
113+ };
114+
115+ if (do_timeout == true && timeout === null) {
116+ timeout = window.setTimeout(timeoutfn, 1000);
117+ } else if (do_timeout == true) {
118+ window.clearTimeout(timeout);
119+ timeout = window.setTimeout(timeoutfn, 1000);
120+ } else {
121+ timeoutfn();
122+ }
123+
124 //"GET" information from the specified url (string)
125- xmlhttp.open("GET", string, true);
126- xmlhttp.send();
127 }
128- xmlhttp.open("GET", string, true);
129- xmlhttp.send();
130 }
131
132
133-function add_options(selectedlist, values_list){
134+function add_options(selectedlist, values_list) {
135 //Adds all the options obtained from the list of program
136 //names or function names to an auto complete drop-down box
137 var options = ''
138- if (values_list.length == 1 || values_list.length ==0){
139+ if (values_list.length == 1 || values_list.length ==0) {
140 options += '<option value="'+values_list+'"/>';
141- }
142- else{
143- for (var i=0; i < values_list.length;++i){
144+ } else {
145+ for (var i=0; i < values_list.length;++i) {
146 options += '<option value="'+values_list[i]+'"/>';
147 }
148 }
149@@ -84,48 +114,47 @@
150 }
151
152
153-function display_when(){
154+function display_when() {
155 //For each query specifies when auto-complete text boxes should be made
156 //visible or invisible and makes them read only
157 var query_list = document.getElementById("query_list");
158
159 var no_functions = new Array();
160 var prog_name = document.getElementById("program_name").value;
161- if (query_list.options[query_list.selectedIndex].value == "functions_calling"){
162+ if (query_list.options[query_list.selectedIndex].value == "functions_calling") {
163 set_arguments("Function being called", "");
164 }
165- if (query_list.options[query_list.selectedIndex].value == "functions_called_by"){
166+ if (query_list.options[query_list.selectedIndex].value == "functions_called_by") {
167 set_arguments("Function calling", "");
168 }
169- if (query_list.options[query_list.selectedIndex].value == "all_call_paths"){
170+ if (query_list.options[query_list.selectedIndex].value == "all_call_paths") {
171 set_arguments("Function calling", "Function being called");
172 }
173- if (query_list.options[query_list.selectedIndex].value == "shortest_call_path"){
174+ if (query_list.options[query_list.selectedIndex].value == "shortest_call_path") {
175 set_arguments("Function calling", "Function being called");
176 }
177 if (query_list.options[query_list.selectedIndex].value == "whole_program") {
178 set_arguments("", "");
179 }
180- if (query_list.options[query_list.selectedIndex].value == "function_names"){
181+ if (query_list.options[query_list.selectedIndex].value == "function_names") {
182 set_arguments("", "");
183 }
184 }
185
186
187
188-function execute_query(){
189+function execute_query() {
190 document.getElementById("output_image").src = "";
191
192 //Returns error in alert window if query not executed properly,
193 //otherwise performs the query and outputs it
194 show_item("please-wait");
195 var query_id = document.getElementById("query_list").value;
196- if (query_id == "function_names"){
197+ if (query_id == "function_names") {
198 //url for page containing all function names
199 var string = "/database_properties?program_name=" +
200 document.getElementById("program_name").value + "&query=functions";
201- }
202- else{
203+ } else {
204 //If not function names we will want a graph as an output;
205 //url returns svg file of graph.
206 // We use a random number argument to prevent caching.
207@@ -141,11 +170,11 @@
208 var xmlhttp = new XMLHttpRequest();
209 xmlhttp.open("GET", string, true);
210 xmlhttp.send();
211- xmlhttp.onreadystatechange = function(){
212- if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
213+ xmlhttp.onreadystatechange = function() {
214+ if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
215 //readyState == 4 means query has finished executing.
216 //status == 200 means "GET"ing has been successful.
217- if (query_id == "function_names"){
218+ if (query_id == "function_names") {
219 //Text output displayed in paragraph.
220 document.getElementById("function_names_output").innerHTML =
221 xmlhttp.responseText;
222@@ -158,24 +187,19 @@
223 var dataUrl = "data:image/svg+xml;base64," + btoa(xmlhttp.responseText);
224 document.getElementById("output_image").src = dataUrl;
225 }
226- }
227- else if (xmlhttp.readyState == 4 && xmlhttp.status == 400){
228- //Error occurred during query; display response.
229- show_error(xmlhttp.responseText);
230- }
231- else if(xmlhttp.readyState == 4 && xmlhttp.status == 404){
232- //Error occurred during query; display response.
233- show_error(xmlhttp.responseText);
234- }
235- else if(xmlhttp.readyState ==4 && xmlhttp.status == 204){
236+ } else if (xmlhttp.readyState == 4 && xmlhttp.status == 400) {
237+ //Error occurred during query; display response.
238+ show_error(xmlhttp.responseText);
239+ } else if(xmlhttp.readyState == 4 && xmlhttp.status == 404) {
240+ //Error occurred during query; display response.
241+ show_error(xmlhttp.responseText);
242+ } else if(xmlhttp.readyState ==4 && xmlhttp.status == 204) {
243 //Query executed correctly but graph returned is empty
244 show_error("Graph returned was empty");
245- }
246- else if (xmlhttp.readyState == 4 && xmlhttp.status == 502) {
247+ } else if (xmlhttp.readyState == 4 && xmlhttp.status == 502) {
248 //Error occurs if Neo4j isn't running
249 show_error("Bad Gateway received - are you sure the database server is running?");
250- }
251- else if(xmlhttp.readyState ==4){
252+ } else if(xmlhttp.readyState ==4) {
253 //query executed correctly
254 show_error("An unknown error occurred");
255 }
256
257=== modified file 'src/sextant/db_api.py'
258--- src/sextant/db_api.py 2014-10-23 11:15:48 +0000
259+++ src/sextant/db_api.py 2014-11-21 12:32:15 +0000
260@@ -832,7 +832,7 @@
261 result = self._db.query(q, returns=neo4jrestclient.Node)
262 return bool(result)
263
264- def get_function_names(self, program_name):
265+ def get_function_names(self, program_name, search, max_funcs):
266 """
267 Execute query to retrieve a list of all functions in the program.
268 Any of the output names can be used verbatim in any SextantConnection
269@@ -845,8 +845,13 @@
270 if not validate_query(program_name):
271 return set()
272
273- q = (' MATCH (:program {{name: "{}"}})-[:subject]->(f:func)'
274- ' RETURN f.name').format(program_name)
275+ if not search:
276+ q = (' MATCH (:program {{name: "{}"}})-[:subject]->(f:func)'
277+ ' RETURN f.name LIMIT {}').format(program_name, max_funcs)
278+ else:
279+ q = (' MATCH (:program {{name: "{}"}})-[:subject]->(f:func)'
280+ ' WHERE f.name =~ ".*{}.*" RETURN f.name LIMIT {}'
281+ .format(program_name, search, max_funcs))
282 return {func[0] for func in self._db.query(q)}
283
284 def get_all_functions_called(self, program_name, function_calling):
285
286=== modified file 'src/sextant/web/server.py'
287--- src/sextant/web/server.py 2014-10-13 15:09:08 +0000
288+++ src/sextant/web/server.py 2014-11-21 12:32:15 +0000
289@@ -29,6 +29,10 @@
290 # global SextantConnection object which deals with the port forwarding
291 CONNECTION = None
292
293+# The maximum number of matches to display for autocomplete. If more than
294+# this are matched, nothing will be returned.
295+AUTOCOMPLETE_NAMES_LIMIT = 75
296+
297 RESPONSE_CODE_OK = 200
298 RESPONSE_CODE_BAD_REQUEST = 400
299 RESPONSE_CODE_NOT_FOUND = 404
300@@ -218,8 +222,15 @@
301 return CONNECTION.get_program_names()
302
303 @staticmethod
304- def _get_function_names(program_name):
305- return CONNECTION.get_function_names(program_name)
306+ def _get_function_names(program_name, search=""):
307+ # For the returned data, we must allow one more name than
308+ # the maximum so that we know if the maximum was exceeded rather
309+ # than only met.
310+ max_funcs = AUTOCOMPLETE_NAMES_LIMIT + 1
311+ programs = CONNECTION.programs_with_metadata()
312+ result = CONNECTION.get_function_names(program_name, search, max_funcs)
313+ return result if len(result) < max_funcs else set()
314+
315
316 @defer.inlineCallbacks
317 def _render_GET(self, request):
318@@ -247,19 +258,21 @@
319 request.finish()
320 defer.returnValue(None)
321 program_name = request.args['program_name'][0]
322+ search = request.args.get('search', [''])[0]
323
324- funcnames = yield deferToThread(self._get_function_names, program_name)
325+ funcnames = yield deferToThread(self._get_function_names, program_name, search)
326 if funcnames is None:
327 request.setResponseCode(404)
328 request.setHeader("content-type", "text/plain")
329 request.write("No program with name %s was found in the Sextant." % escape(program_name))
330 request.finish()
331 defer.returnValue(None)
332+ else:
333+ request.setHeader("content-type", "application/json")
334+ request.write(json.dumps(list(funcnames)))
335
336- request.setHeader("content-type", "application/json")
337- request.write(json.dumps(list(funcnames)))
338- request.finish()
339- defer.returnValue(None)
340+ request.finish()
341+ defer.returnValue(None)
342
343 else:
344 request.setResponseCode(400)

Subscribers

People subscribed via source and target branches