Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions serveradmin/common/static/icons/clock-history.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions serveradmin/servershell/static/css/servershell.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ th:hover .attr-headericons {
height: 29px;
}

#history-toggle img {
width: 16px;
height: 16px;
}

#history-toggle {
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
}

#history-toggle.active {
background: var(--background-primary);
}


.input-controls > div:last-of-type {
padding-left: 0;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Autocomplete History - Copyright (c) 2026 InnoGames GmbH
*
* This module ads auto complete while searching the query history
*/

servershell.autocomplete_history_enabled = false;

servershell.close_history_autocomplete = function () {
const autocomplete_search_input = $('#term');
autocomplete_search_input.autocomplete('destroy');
servershell.autocomplete_history_enabled = false;
servershell.enable_search_autocomplete();
$('#history-toggle').removeClass('active');
}

servershell.open_history_autocomplete = function () {
const autocomplete_search_input = $('#term');
autocomplete_search_input.autocomplete('destroy');
autocomplete_search_input.autocomplete({
source: function (request, response) {
const displayLimit = 20;
const search = request.term;

const history = servershell.history.get()
const possibleChoices = history.filter((entry) => entry.term.toLowerCase().includes(search.toLowerCase()))
.map((entry) => entry.term);
response(possibleChoices.slice(0, Math.min(displayLimit, possibleChoices.length)));
},

select: function (_, ui) {
const term = ui.item.value;
const [, entry] = servershell.history.findMatchingEntry(term);

servershell.term = term;

const manageAttributes = $('#history_attributes')[0].checked;
if (manageAttributes && entry) {
servershell.shown_attributes = entry.shown_attributes;
} else {
servershell.submit_search();
}
servershell.close_history_autocomplete();
}
});
autocomplete_search_input.autocomplete('enable');
autocomplete_search_input.autocomplete('option', 'autoFocus', $('#autoselect')[0].checked);
autocomplete_search_input.autocomplete('option', 'minLength', 0);
autocomplete_search_input.autocomplete('option', 'delay', $('#autocomplete_delay_search')[0].value);

// When history is opened show all item, regardless of the current input text
autocomplete_search_input.autocomplete('search', "");
autocomplete_search_input.focus();
servershell.autocomplete_history_enabled = true;
$('#history-toggle').addClass('active');
}

$(document).ready(function () {
$(document).keydown(function (event) {
if (event.shiftKey && event.ctrlKey) {
if (event.key !== 'F') {
return;
}
if (servershell.autocomplete_history_enabled) {
servershell.close_history_autocomplete();
return;
}
servershell.open_history_autocomplete();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* autocomplete for hostnames, attributes, attribute values and filters by
* now.
*/
$(document).ready(function () {

servershell.enable_search_autocomplete = function () {
let _build_value = function (full_term, cur_term, attribute, value) {
let cur_term_index = full_term.lastIndexOf(cur_term);
let result = full_term.substring(0, cur_term_index) + attribute;
Expand Down Expand Up @@ -117,4 +118,8 @@ $(document).ready(function () {
autocomplete_search_input.autocomplete($('#autocomplete')[0].checked ? 'enable' : 'disable');
autocomplete_search_input.autocomplete('option', 'autoFocus', $('#autoselect')[0].checked);
autocomplete_search_input.autocomplete('option', 'delay', $('#autocomplete_delay_search')[0].value);
}

$(document).ready(function () {
servershell.enable_search_autocomplete();
});
50 changes: 50 additions & 0 deletions serveradmin/servershell/static/js/servershell/history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* History - Copyright (c) 2026 InnoGames GmbH
*
* This module implements interactions with localstorage to serve the history array.
*/

const historyStorageKey = "servershell_history"

servershell.history = {
get: function () {
const history = localStorage.getItem(historyStorageKey);
if (!history) {
return [];
}

return JSON.parse(history);
},

storeEntry: function (entry) {
const history = servershell.history.get();

const [matching] = servershell.history.findMatchingEntry(entry.term);
if (matching !== -1) {
history.splice(matching, 1);
}

const maxSize = parseInt($('#history_size').val());
while (history.length >= maxSize) {
history.pop();
}

history.unshift(entry);

localStorage.setItem(historyStorageKey, JSON.stringify(history));
},

clear: function () {
localStorage.setItem(historyStorageKey, "[]");
},

findMatchingEntry: function (term) {
const history = servershell.history.get();
const index = history.findIndex((i) => term === i.term)
if (index === -1) {
return [-1, undefined]
}

return [index, history[index]];
}
}
15 changes: 15 additions & 0 deletions serveradmin/servershell/static/js/servershell/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ servershell.submit_search = function(focus_command_input = false) {
servershell.num_servers = data.num_servers;
servershell.status = data.status;
servershell.understood = data.understood;

if (servershell.term) {
const storeEmpty = $('#history_store_empty')[0].checked
if (!storeEmpty && data.servers.length === 0) {
return
}

servershell.history.storeEntry({
term: servershell.term,
shown_attributes: servershell.shown_attributes,
});
}
}
})
.catch(function(xhr) {
Expand Down Expand Up @@ -166,6 +178,9 @@ $(document).ready(function() {
'autocomplete_delay_commands': $('#autocomplete_delay_commands').val(),
'autoselect': $('#autoselect')[0].checked,
'save_attributes': $('#save_attributes')[0].checked,
'history_attributes': $('#history_attributes')[0].checked,
'history_size': $('#history_size').val(),
'history_store_empty': $('#history_store_empty')[0].checked,
'timeout': 5000,
}).done(function(data) {
servershell.search_settings = data;
Expand Down
31 changes: 29 additions & 2 deletions serveradmin/servershell/templates/servershell/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@
<div class="col-sm-8">
<form method="post" action="{% url 'servershell_results' %}" id="search_form" autocomplete="off">
{% csrf_token %}
<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' %}" />
<div class="input-group input-group-sm">
<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' %}" />
<div class="input-group-append">
<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()">
<img src="{{ STATIC_URL }}icons/clock-history.svg" alt="Search history"/>
</button>
</div>
</div>
</form>
</div>
<div class="col-sm-3">
Expand All @@ -47,7 +54,7 @@
<!-- Advanced search options (hidden by default) -->
<div class="collapse" id="search-options">
<div class="card card-body">
<b>Your search settings are bound to your user and persistent.</b>
<b>Your settings are bound to your user and persistent.</b>
<div class="form-check">
<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');">
<label class="form-check-label" for="autocomplete">Autocomplete for Search & Command</label>
Expand All @@ -69,6 +76,23 @@
<label class="form-input-label" for="autocomplete_delay_commands">Delay in milliseconds before auto completion for commands kicks in.</label>
</div>
<hr>
<b>Search History</b>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="history_attributes" {% if search_settings.history_attributes %}checked="checked"{% endif %}>
<label class="form-check-label" for="history_attributes">When selecting a history entry also change displayed attributes</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="history_store_empty" {% if search_settings.history_store_empty %}checked="checked"{% endif %}>
<label class="form-check-label" for="history_store_empty">Whether searches that yield no results should be stored</label>
</div>
<div class="form-check">
<input type="number" class="form-input" id="history_size" value="{{ search_settings.history_size }}" min="20">
<label class="form-input-label" for="history_size">The maximum size of your history</label>
</div>
<div>
<input value="Clear History" type="button" onclick="servershell.history.clear()" class="btn btn-sm btn-danger" style="margin-top: 5px;">
</div>
<hr>
<b>Keyboard Shortcuts</b>
<ul>
<li><kbd>TAB</kbd> Goto next input</li>
Expand All @@ -78,6 +102,7 @@
<li><kbd>CTRL + &#8592;</kbd> Jump a word backward</li>
<li><kbd>CTRL + &#8594;</kbd> Jump a word forward</li>
<li><kbd>ALT + BACKSPACE</kbd> Delete one word (reverse)</li>
<li><kbd>CTRL + SHIFT + F</kbd> Toggle search history</li>
</ul>
</div>
</div>
Expand Down Expand Up @@ -193,9 +218,11 @@
<script src="{{ STATIC_URL }}js/servershell/result.js"></script>
<script src="{{ STATIC_URL }}js/servershell/command.js"></script>
<script src="{{ STATIC_URL }}js/servershell/validate.js"></script>
<script src="{{ STATIC_URL }}js/servershell/history.js"></script>
<script src="{{ STATIC_URL }}{{ choose_ip_address.js }}"></script>
<script src="{{ STATIC_URL }}js/servershell/autocomplete/search.js"></script>
<script src="{{ STATIC_URL }}js/servershell/autocomplete/command.js"></script>
<script src="{{ STATIC_URL }}js/servershell/autocomplete/history.js"></script>
<script>
$(document).ready(function() {
// We want to allow the user to abort some queries such as search
Expand Down
3 changes: 3 additions & 0 deletions serveradmin/servershell/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
'autocomplete_delay_commands': 10,
'autoselect': True,
'save_attributes': False,
'history_attributes': True,
'history_size': 20,
'history_store_empty': True,
}


Expand Down
Loading