WebUI: Use Map instead of Mootools Hash in Torrents table
[qBittorrent.git] / src / webui / www / private / views / log.html
blob6305fe13b8efb4e5aab6ebc3933907896c8dd610
1 <style type="text/css">
2 #logTopBar {
3 margin-top: 1em;
6 #logFilterBar {
7 margin: .5em 0;
10 #logFilterBar>label {
11 font-weight: bold;
12 margin-right: .3em;
15 #logFilterBar>button {
16 display: inline-block;
17 padding: 4px 15px;
18 margin-left: .3em;
21 #logView {
22 padding: 0 20px;
23 overflow: auto;
26 #logContentView {
27 display: block;
28 vertical-align: top;
31 #logMessageTableFixedHeaderDiv .dynamicTableHeader,
32 #logPeerTableFixedHeaderDiv .dynamicTableHeader {
33 cursor: default;
36 #filterTextInput {
37 background-image: url("../images/edit-find.svg");
38 background-repeat: no-repeat;
39 background-position: left;
40 background-size: 1.5em;
41 padding: 4px 5px 4px 2em;
42 margin-left: .3em;
43 width: 200px;
44 border: 1px solid var(--color-border-default);
45 border-radius: 3px;
48 #logFilterSummary {
49 overflow: auto;
50 margin: 1em 0 .5em;
53 #numFilteredLogs,
54 #numTotalLogs {
55 font-style: italic;
58 .logNormal {
59 color: var(--color-text-default);
62 .logInfo {
63 color: var(--color-text-blue);
66 .logWarning {
67 color: var(--color-text-orange);
70 .logCritical,
71 .peerBlocked {
72 color: var(--color-text-red);
75 .vsb-main>button {
76 padding: 4px 12px !important;
79 </style>
81 <div id="logView">
82 <div id="logTopBar">
83 <div id="logFilterBar">
84 <label for="logLevelSelect">QBT_TR(Log Levels:)QBT_TR[CONTEXT=ExecutionLogWidget]</label>
85 <select multiple size="1" id="logLevelSelect" class="logLevelSelect" onchange="window.qBittorrent.Log.logLevelChanged()">
86 <option value="1">QBT_TR(Normal Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
87 <option value="2">QBT_TR(Information Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
88 <option value="4">QBT_TR(Warning Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
89 <option value="8">QBT_TR(Critical Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
90 </select>
92 <input type="text" id="filterTextInput" onkeyup="window.qBittorrent.Log.filterTextChanged()" placeholder="QBT_TR(Filter logs)QBT_TR[CONTEXT=ExecutionLogWidget]" aria-label="QBT_TR(Filter logs)QBT_TR[CONTEXT=ExecutionLogWidget]" autocomplete="off" autocorrect="off" autocapitalize="none">
93 <button type="button" title="Clear input" onclick="javascript:document.querySelector('#filterTextInput').value='';window.qBittorrent.Log.filterTextChanged();">QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]</button>
94 </div>
96 <div id="logFilterSummary">
97 <span>QBT_TR(Results)QBT_TR[CONTEXT=ExecutionLogWidget] (QBT_TR(showing)QBT_TR[CONTEXT=ExecutionLogWidget] <span id="numFilteredLogs">0</span> QBT_TR(out of)QBT_TR[CONTEXT=ExecutionLogWidget] <span id="numTotalLogs">0</span>):</span>
98 </div>
99 </div>
101 <div id="logContentView">
102 <div id="logMessageView">
103 <div id="logMessageTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
104 <table class="dynamicTable unselectable" style="position:relative;">
105 <thead>
106 <tr class="dynamicTableHeader"></tr>
107 </thead>
108 </table>
109 </div>
110 <div id="logMessageTableDiv" class="dynamicTableDiv">
111 <table class="dynamicTable unselectable">
112 <thead>
113 <tr class="dynamicTableHeader"></tr>
114 </thead>
115 <tbody></tbody>
116 </table>
117 </div>
118 </div>
119 <div id="logPeerView" class="invisible">
120 <div id="logPeerTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
121 <table class="dynamicTable unselectable" style="position:relative;">
122 <thead>
123 <tr class="dynamicTableHeader"></tr>
124 </thead>
125 </table>
126 </div>
127 <div id="logPeerTableDiv" class="dynamicTableDiv">
128 <table class="dynamicTable unselectable">
129 <thead>
130 <tr class="dynamicTableHeader"></tr>
131 </thead>
132 <tbody></tbody>
133 </table>
134 </div>
135 </div>
136 </div>
137 </div>
139 <ul id="logTableMenu" class="contextMenu">
140 <li><a href="#" class="copyLogDataToClipboard"><img src="images/edit-copy.svg" alt="QBT_TR(Copy)QBT_TR[CONTEXT=ExecutionLogWidget]">QBT_TR(Copy)QBT_TR[CONTEXT=ExecutionLogWidget]</a></li>
141 <li><a href="#Clear"><img src="images/list-remove.svg" alt="QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]">QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]</a></li>
142 </ul>
144 <script>
145 "use strict";
147 window.qBittorrent ??= {};
148 window.qBittorrent.Log ??= (() => {
149 const exports = () => {
150 return {
151 init: init,
152 unload: unload,
153 load: load,
154 setCurrentTab: setCurrentTab,
155 getFilterText: getFilterText,
156 getSelectedLevels: getSelectedLevels,
157 logLevelChanged: logLevelChanged,
158 filterTextChanged: filterTextChanged
162 let currentSelectedTab = "main";
163 const tableInfo = {
164 main: {
165 instance: new window.qBittorrent.DynamicTable.LogMessageTable(),
166 progress: false,
167 timer: null,
168 last_id: -1
170 peer: {
171 instance: new window.qBittorrent.DynamicTable.LogPeerTable(),
172 progress: false,
173 timer: null,
174 last_id: -1
178 let customSyncLogDataInterval = null;
179 let logFilterTimer = -1;
180 let inputtedFilterText = "";
181 let selectBox;
182 let selectedLogLevels = JSON.parse(LocalPreferences.get("qbt_selected_log_levels")) || ["1", "2", "4", "8"];
184 const init = () => {
185 $("logLevelSelect").getElements("option").each((x) => {
186 if (selectedLogLevels.indexOf(x.value.toString()) !== -1)
187 x.selected = true;
188 else
189 x.selected = false;
192 selectBox = new vanillaSelectBox("#logLevelSelect", {
193 maxHeight: 200,
194 search: false,
195 translations: {
196 all: "QBT_TR(All)QBT_TR[CONTEXT=ExecutionLogWidget]",
197 item: "QBT_TR(item)QBT_TR[CONTEXT=ExecutionLogWidget]",
198 items: "QBT_TR(items)QBT_TR[CONTEXT=ExecutionLogWidget]",
199 selectAll: "QBT_TR(Select All)QBT_TR[CONTEXT=ExecutionLogWidget]",
200 clearAll: "QBT_TR(Clear All)QBT_TR[CONTEXT=ExecutionLogWidget]",
202 placeHolder: "QBT_TR(Choose a log level...)QBT_TR[CONTEXT=ExecutionLogWidget]",
203 keepInlineStyles: false
206 const logTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
207 targets: ".logTableRow",
208 menu: "logTableMenu",
209 actions: {
210 Clear: () => {
211 tableInfo[currentSelectedTab].instance.selectedRowsIds().forEach((rowId) => {
212 tableInfo[currentSelectedTab].instance.removeRow(rowId);
215 updateLabelCount();
218 offsets: {
219 x: -16,
220 y: -57
224 tableInfo["main"].instance.setup("logMessageTableDiv", "logMessageTableFixedHeaderDiv", logTableContextMenu);
225 tableInfo["peer"].instance.setup("logPeerTableDiv", "logPeerTableFixedHeaderDiv", logTableContextMenu);
227 MUI.Panels.instances.LogPanel.contentEl.style.height = "100%";
228 $("logView").style.height = "inherit";
230 load();
233 const unload = () => {
234 for (const table in tableInfo) {
235 if (!Object.hasOwn(tableInfo, table))
236 continue;
237 resetTableTimer(table);
241 const load = () => {
242 customSyncLogDataInterval = null;
243 syncLogWithInterval(100);
246 const resetTableTimer = (curTab) => {
247 if (curTab === undefined)
248 curTab = currentSelectedTab;
250 clearTimeout(tableInfo[curTab].timer);
251 tableInfo[curTab].timer = null;
254 const syncLogWithInterval = (interval) => {
255 if (!tableInfo[currentSelectedTab].progress) {
256 clearTimeout(tableInfo[currentSelectedTab].timer);
257 tableInfo[currentSelectedTab].timer = syncLogData.delay(interval, null, currentSelectedTab);
261 const getFilterText = () => {
262 return inputtedFilterText;
265 const getSelectedLevels = () => {
266 return selectedLogLevels;
269 const getSyncLogDataInterval = () => {
270 return customSyncLogDataInterval ? customSyncLogDataInterval : serverSyncMainDataInterval;
273 const logLevelChanged = () => {
274 const value = selectBox.getResult().sort();
276 if (selectedLogLevels !== value) {
277 tableInfo[currentSelectedTab].last_id = -1;
278 selectedLogLevels = value;
279 LocalPreferences.set("qbt_selected_log_levels", JSON.stringify(selectedLogLevels));
280 logFilterChanged();
284 const filterTextChanged = () => {
285 const value = $("filterTextInput").value.trim();
286 if (inputtedFilterText !== value) {
287 inputtedFilterText = value;
288 logFilterChanged();
292 const logFilterChanged = () => {
293 clearTimeout(logFilterTimer);
294 logFilterTimer = setTimeout((curTab) => {
295 logFilterTimer = -1;
297 tableInfo[curTab].instance.updateTable(false);
298 updateLabelCount(curTab);
299 }, window.qBittorrent.Misc.FILTER_INPUT_DELAY, currentSelectedTab);
302 const setCurrentTab = (tab) => {
303 if (tab === currentSelectedTab)
304 return;
306 currentSelectedTab = tab;
307 if (currentSelectedTab === "main") {
308 selectBox.enable();
309 $("logMessageView").removeClass("invisible");
310 $("logPeerView").addClass("invisible");
311 resetTableTimer("peer");
313 else {
314 selectBox.disable();
315 $("logMessageView").addClass("invisible");
316 $("logPeerView").removeClass("invisible");
317 resetTableTimer("main");
320 clearTimeout(logFilterTimer);
321 logFilterTimer = -1;
322 load();
324 if (tableInfo[currentSelectedTab].instance.filterText !== getFilterText())
325 tableInfo[currentSelectedTab].instance.updateTable();
327 updateLabelCount();
330 const updateLabelCount = (curTab) => {
331 if (curTab === undefined)
332 curTab = currentSelectedTab;
334 $("numFilteredLogs").textContent = tableInfo[curTab].instance.filteredLength();
335 $("numTotalLogs").textContent = tableInfo[curTab].instance.getRowIds().length;
338 const syncLogData = (curTab) => {
339 if (curTab === undefined)
340 curTab = currentSelectedTab;
342 let url;
343 if (curTab === "main") {
344 url = new URI("api/v2/log/main");
345 url.setData({
346 normal: selectedLogLevels.indexOf("1") !== -1,
347 info: selectedLogLevels.indexOf("2") !== -1,
348 warning: selectedLogLevels.indexOf("4") !== -1,
349 critical: selectedLogLevels.indexOf("8") !== -1
352 else {
353 url = new URI("api/v2/log/peers");
356 url.setData("last_known_id", tableInfo[curTab].last_id);
357 tableInfo[curTab].progress = true;
359 new Request.JSON({
360 url: url,
361 method: "get",
362 noCache: true,
363 onFailure: function(response) {
364 const errorDiv = $("error_div");
365 if (errorDiv)
366 errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
367 tableInfo[curTab].progress = false;
368 syncLogWithInterval(10000);
370 onSuccess: function(response) {
371 $("error_div").textContent = "";
373 if ($("logTabColumn").hasClass("invisible"))
374 return;
376 if (response.length > 0) {
377 clearTimeout(logFilterTimer);
378 logFilterTimer = -1;
380 for (let i = 0; i < response.length; ++i) {
381 let row;
382 if (curTab === "main") {
383 row = {
384 rowId: response[i].id,
385 message: response[i].message,
386 timestamp: response[i].timestamp,
387 type: response[i].type,
390 else {
391 row = {
392 rowId: response[i].id,
393 ip: response[i].ip,
394 timestamp: response[i].timestamp,
395 blocked: response[i].blocked,
396 reason: response[i].reason,
399 tableInfo[curTab].instance.updateRowData(row);
400 tableInfo[curTab].last_id = Math.max(response[i].id.toInt(), tableInfo[curTab].last_id);
403 tableInfo[curTab].instance.updateTable();
404 updateLabelCount(curTab);
407 tableInfo[curTab].progress = false;
408 syncLogWithInterval(getSyncLogDataInterval());
410 }).send();
413 new ClipboardJS(".copyLogDataToClipboard", {
414 text: function() {
415 const msg = [];
416 tableInfo[currentSelectedTab].instance.selectedRowsIds().each((rowId) => {
417 msg.push(tableInfo[currentSelectedTab].instance.rows.get(rowId).full_data[(currentSelectedTab === "main") ? "message" : "ip"]);
420 return msg.join("\n");
424 return exports();
425 })();
426 Object.freeze(window.qBittorrent.Log);
427 </script>