diff --git a/serveradmin/common/static/icons/clock-history.svg b/serveradmin/common/static/icons/clock-history.svg new file mode 100644 index 00000000..f685e10a --- /dev/null +++ b/serveradmin/common/static/icons/clock-history.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/serveradmin/servershell/static/css/servershell.css b/serveradmin/servershell/static/css/servershell.css index e3b2d984..55376a26 100644 --- a/serveradmin/servershell/static/css/servershell.css +++ b/serveradmin/servershell/static/css/servershell.css @@ -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; } diff --git a/serveradmin/servershell/static/js/servershell/autocomplete/history.js b/serveradmin/servershell/static/js/servershell/autocomplete/history.js new file mode 100644 index 00000000..4890612e --- /dev/null +++ b/serveradmin/servershell/static/js/servershell/autocomplete/history.js @@ -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(); + } + }); +}); \ No newline at end of file diff --git a/serveradmin/servershell/static/js/servershell/autocomplete/search.js b/serveradmin/servershell/static/js/servershell/autocomplete/search.js index eabcf3d0..7f72469f 100644 --- a/serveradmin/servershell/static/js/servershell/autocomplete/search.js +++ b/serveradmin/servershell/static/js/servershell/autocomplete/search.js @@ -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; @@ -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(); }); diff --git a/serveradmin/servershell/static/js/servershell/history.js b/serveradmin/servershell/static/js/servershell/history.js new file mode 100644 index 00000000..64bf0285 --- /dev/null +++ b/serveradmin/servershell/static/js/servershell/history.js @@ -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]]; + } +} \ No newline at end of file diff --git a/serveradmin/servershell/static/js/servershell/search.js b/serveradmin/servershell/static/js/servershell/search.js index 54e7c053..d477a986 100644 --- a/serveradmin/servershell/static/js/servershell/search.js +++ b/serveradmin/servershell/static/js/servershell/search.js @@ -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) { @@ -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; diff --git a/serveradmin/servershell/templates/servershell/index.html b/serveradmin/servershell/templates/servershell/index.html index 2752c328..2e807758 100644 --- a/serveradmin/servershell/templates/servershell/index.html +++ b/serveradmin/servershell/templates/servershell/index.html @@ -30,7 +30,14 @@