Skip to content

Commit 7bfd73f

Browse files
authored
Implement Query history (#434)
Provide a history in the Servershell for recently typed queries.
1 parent dbf6d35 commit 7bfd73f

8 files changed

Lines changed: 195 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Loading

serveradmin/servershell/static/css/servershell.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ th:hover .attr-headericons {
3737
height: 29px;
3838
}
3939

40+
#history-toggle img {
41+
width: 16px;
42+
height: 16px;
43+
}
44+
45+
#history-toggle {
46+
background-color: #fff;
47+
background-clip: padding-box;
48+
border: 1px solid #ced4da;
49+
}
50+
51+
#history-toggle.active {
52+
background: var(--background-primary);
53+
}
54+
55+
4056
.input-controls > div:last-of-type {
4157
padding-left: 0;
4258
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Autocomplete History - Copyright (c) 2026 InnoGames GmbH
3+
*
4+
* This module ads auto complete while searching the query history
5+
*/
6+
7+
servershell.autocomplete_history_enabled = false;
8+
9+
servershell.close_history_autocomplete = function () {
10+
const autocomplete_search_input = $('#term');
11+
autocomplete_search_input.autocomplete('destroy');
12+
servershell.autocomplete_history_enabled = false;
13+
servershell.enable_search_autocomplete();
14+
$('#history-toggle').removeClass('active');
15+
}
16+
17+
servershell.open_history_autocomplete = function () {
18+
const autocomplete_search_input = $('#term');
19+
autocomplete_search_input.autocomplete('destroy');
20+
autocomplete_search_input.autocomplete({
21+
source: function (request, response) {
22+
const displayLimit = 20;
23+
const search = request.term;
24+
25+
const history = servershell.history.get()
26+
const possibleChoices = history.filter((entry) => entry.term.toLowerCase().includes(search.toLowerCase()))
27+
.map((entry) => entry.term);
28+
response(possibleChoices.slice(0, Math.min(displayLimit, possibleChoices.length)));
29+
},
30+
31+
select: function (_, ui) {
32+
const term = ui.item.value;
33+
const [, entry] = servershell.history.findMatchingEntry(term);
34+
35+
servershell.term = term;
36+
37+
const manageAttributes = $('#history_attributes')[0].checked;
38+
if (manageAttributes && entry) {
39+
servershell.shown_attributes = entry.shown_attributes;
40+
} else {
41+
servershell.submit_search();
42+
}
43+
servershell.close_history_autocomplete();
44+
}
45+
});
46+
autocomplete_search_input.autocomplete('enable');
47+
autocomplete_search_input.autocomplete('option', 'autoFocus', $('#autoselect')[0].checked);
48+
autocomplete_search_input.autocomplete('option', 'minLength', 0);
49+
autocomplete_search_input.autocomplete('option', 'delay', $('#autocomplete_delay_search')[0].value);
50+
51+
// When history is opened show all item, regardless of the current input text
52+
autocomplete_search_input.autocomplete('search', "");
53+
autocomplete_search_input.focus();
54+
servershell.autocomplete_history_enabled = true;
55+
$('#history-toggle').addClass('active');
56+
}
57+
58+
$(document).ready(function () {
59+
$(document).keydown(function (event) {
60+
if (event.shiftKey && event.ctrlKey) {
61+
if (event.key !== 'F') {
62+
return;
63+
}
64+
if (servershell.autocomplete_history_enabled) {
65+
servershell.close_history_autocomplete();
66+
return;
67+
}
68+
servershell.open_history_autocomplete();
69+
}
70+
});
71+
});

serveradmin/servershell/static/js/servershell/autocomplete/search.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
* autocomplete for hostnames, attributes, attribute values and filters by
66
* now.
77
*/
8-
$(document).ready(function () {
8+
9+
servershell.enable_search_autocomplete = function () {
910
let _build_value = function (full_term, cur_term, attribute, value) {
1011
let cur_term_index = full_term.lastIndexOf(cur_term);
1112
let result = full_term.substring(0, cur_term_index) + attribute;
@@ -117,4 +118,8 @@ $(document).ready(function () {
117118
autocomplete_search_input.autocomplete($('#autocomplete')[0].checked ? 'enable' : 'disable');
118119
autocomplete_search_input.autocomplete('option', 'autoFocus', $('#autoselect')[0].checked);
119120
autocomplete_search_input.autocomplete('option', 'delay', $('#autocomplete_delay_search')[0].value);
121+
}
122+
123+
$(document).ready(function () {
124+
servershell.enable_search_autocomplete();
120125
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* History - Copyright (c) 2026 InnoGames GmbH
3+
*
4+
* This module implements interactions with localstorage to serve the history array.
5+
*/
6+
7+
const historyStorageKey = "servershell_history"
8+
9+
servershell.history = {
10+
get: function () {
11+
const history = localStorage.getItem(historyStorageKey);
12+
if (!history) {
13+
return [];
14+
}
15+
16+
return JSON.parse(history);
17+
},
18+
19+
storeEntry: function (entry) {
20+
const history = servershell.history.get();
21+
22+
const [matching] = servershell.history.findMatchingEntry(entry.term);
23+
if (matching !== -1) {
24+
history.splice(matching, 1);
25+
}
26+
27+
const maxSize = parseInt($('#history_size').val());
28+
while (history.length >= maxSize) {
29+
history.pop();
30+
}
31+
32+
history.unshift(entry);
33+
34+
localStorage.setItem(historyStorageKey, JSON.stringify(history));
35+
},
36+
37+
clear: function () {
38+
localStorage.setItem(historyStorageKey, "[]");
39+
},
40+
41+
findMatchingEntry: function (term) {
42+
const history = servershell.history.get();
43+
const index = history.findIndex((i) => term === i.term)
44+
if (index === -1) {
45+
return [-1, undefined]
46+
}
47+
48+
return [index, history[index]];
49+
}
50+
}

serveradmin/servershell/static/js/servershell/search.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ servershell.submit_search = function(focus_command_input = false) {
100100
servershell.num_servers = data.num_servers;
101101
servershell.status = data.status;
102102
servershell.understood = data.understood;
103+
104+
if (servershell.term) {
105+
const storeEmpty = $('#history_store_empty')[0].checked
106+
if (!storeEmpty && data.servers.length === 0) {
107+
return
108+
}
109+
110+
servershell.history.storeEntry({
111+
term: servershell.term,
112+
shown_attributes: servershell.shown_attributes,
113+
});
114+
}
103115
}
104116
})
105117
.catch(function(xhr) {
@@ -166,6 +178,9 @@ $(document).ready(function() {
166178
'autocomplete_delay_commands': $('#autocomplete_delay_commands').val(),
167179
'autoselect': $('#autoselect')[0].checked,
168180
'save_attributes': $('#save_attributes')[0].checked,
181+
'history_attributes': $('#history_attributes')[0].checked,
182+
'history_size': $('#history_size').val(),
183+
'history_store_empty': $('#history_store_empty')[0].checked,
169184
'timeout': 5000,
170185
}).done(function(data) {
171186
servershell.search_settings = data;

serveradmin/servershell/templates/servershell/index.html

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,14 @@
3030
<div class="col-sm-8">
3131
<form method="post" action="{% url 'servershell_results' %}" id="search_form" autocomplete="off">
3232
{% csrf_token %}
33-
<input tabindex="1" class="form-control form-control-sm autocomplete" type="text" id="term" data-servershell-property-bind="term" data-servershell-autocomplete-url="{% url 'servershell_autocomplete' %}" />
33+
<div class="input-group input-group-sm">
34+
<input tabindex="1" class="form-control form-control-sm autocomplete" type="text" id="term" data-servershell-property-bind="term" data-servershell-autocomplete-url="{% url 'servershell_autocomplete' %}" />
35+
<div class="input-group-append">
36+
<button id="history-toggle" class="btn btn-sm" type="button" title="Search history (Ctrl+Shift+F)" onclick="servershell.autocomplete_history_enabled ? servershell.close_history_autocomplete() : servershell.open_history_autocomplete()">
37+
<img src="{{ STATIC_URL }}icons/clock-history.svg" alt="Search history"/>
38+
</button>
39+
</div>
40+
</div>
3441
</form>
3542
</div>
3643
<div class="col-sm-3">
@@ -47,7 +54,7 @@
4754
<!-- Advanced search options (hidden by default) -->
4855
<div class="collapse" id="search-options">
4956
<div class="card card-body">
50-
<b>Your search settings are bound to your user and persistent.</b>
57+
<b>Your settings are bound to your user and persistent.</b>
5158
<div class="form-check">
5259
<input type="checkbox" class="form-check-input" id="autocomplete" {% if search_settings.autocomplete %}checked="checked"{% endif %} onchange="this.checked ? $('.autocomplete').autocomplete('enable') : $('.autocomplete').autocomplete('disable');">
5360
<label class="form-check-label" for="autocomplete">Autocomplete for Search & Command</label>
@@ -69,6 +76,23 @@
6976
<label class="form-input-label" for="autocomplete_delay_commands">Delay in milliseconds before auto completion for commands kicks in.</label>
7077
</div>
7178
<hr>
79+
<b>Search History</b>
80+
<div class="form-check">
81+
<input type="checkbox" class="form-check-input" id="history_attributes" {% if search_settings.history_attributes %}checked="checked"{% endif %}>
82+
<label class="form-check-label" for="history_attributes">When selecting a history entry also change displayed attributes</label>
83+
</div>
84+
<div class="form-check">
85+
<input type="checkbox" class="form-check-input" id="history_store_empty" {% if search_settings.history_store_empty %}checked="checked"{% endif %}>
86+
<label class="form-check-label" for="history_store_empty">Whether searches that yield no results should be stored</label>
87+
</div>
88+
<div class="form-check">
89+
<input type="number" class="form-input" id="history_size" value="{{ search_settings.history_size }}" min="20">
90+
<label class="form-input-label" for="history_size">The maximum size of your history</label>
91+
</div>
92+
<div>
93+
<input value="Clear History" type="button" onclick="servershell.history.clear()" class="btn btn-sm btn-danger" style="margin-top: 5px;">
94+
</div>
95+
<hr>
7296
<b>Keyboard Shortcuts</b>
7397
<ul>
7498
<li><kbd>TAB</kbd> Goto next input</li>
@@ -78,6 +102,7 @@
78102
<li><kbd>CTRL + &#8592;</kbd> Jump a word backward</li>
79103
<li><kbd>CTRL + &#8594;</kbd> Jump a word forward</li>
80104
<li><kbd>ALT + BACKSPACE</kbd> Delete one word (reverse)</li>
105+
<li><kbd>CTRL + SHIFT + F</kbd> Toggle search history</li>
81106
</ul>
82107
</div>
83108
</div>
@@ -193,9 +218,11 @@
193218
<script src="{{ STATIC_URL }}js/servershell/result.js"></script>
194219
<script src="{{ STATIC_URL }}js/servershell/command.js"></script>
195220
<script src="{{ STATIC_URL }}js/servershell/validate.js"></script>
221+
<script src="{{ STATIC_URL }}js/servershell/history.js"></script>
196222
<script src="{{ STATIC_URL }}{{ choose_ip_address.js }}"></script>
197223
<script src="{{ STATIC_URL }}js/servershell/autocomplete/search.js"></script>
198224
<script src="{{ STATIC_URL }}js/servershell/autocomplete/command.js"></script>
225+
<script src="{{ STATIC_URL }}js/servershell/autocomplete/history.js"></script>
199226
<script>
200227
$(document).ready(function() {
201228
// We want to allow the user to abort some queries such as search

serveradmin/servershell/views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
'autocomplete_delay_commands': 10,
5757
'autoselect': True,
5858
'save_attributes': False,
59+
'history_attributes': True,
60+
'history_size': 20,
61+
'history_store_empty': True,
5962
}
6063

6164

0 commit comments

Comments
 (0)