WebUI: Add ability to toggle alternating row colors in tables
[qBittorrent.git] / src / webui / www / private / views / log.html
blobd5ced42816283edde079d566cdc8c5149104e992
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 .contextMenu>li>a>img {
80 margin-right: 0.5em;
83 </style>
85 <div id="logView">
86 <div id="logTopBar">
87 <div id="logFilterBar">
88 <label for="logLevelSelect">QBT_TR(Log Levels:)QBT_TR[CONTEXT=ExecutionLogWidget]</label>
89 <select multiple size="1" id="logLevelSelect" class="logLevelSelect" onchange="window.qBittorrent.Log.logLevelChanged()">
90 <option value="1">QBT_TR(Normal Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
91 <option value="2">QBT_TR(Information Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
92 <option value="4">QBT_TR(Warning Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
93 <option value="8">QBT_TR(Critical Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
94 </select>
96 <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">
97 <button type="button" title="Clear input" onclick="javascript:document.querySelector('#filterTextInput').value='';window.qBittorrent.Log.filterTextChanged();">QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]</button>
98 </div>
100 <div id="logFilterSummary">
101 <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>
102 </div>
103 </div>
105 <div id="logContentView">
106 <div id="logMessageView">
107 <div id="logMessageTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
108 <table class="dynamicTable unselectable" style="position:relative;">
109 <thead>
110 <tr class="dynamicTableHeader"></tr>
111 </thead>
112 </table>
113 </div>
114 <div id="logMessageTableDiv" class="dynamicTableDiv">
115 <table class="dynamicTable unselectable">
116 <thead>
117 <tr class="dynamicTableHeader"></tr>
118 </thead>
119 <tbody></tbody>
120 </table>
121 </div>
122 </div>
123 <div id="logPeerView" class="invisible">
124 <div id="logPeerTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
125 <table class="dynamicTable unselectable" style="position:relative;">
126 <thead>
127 <tr class="dynamicTableHeader"></tr>
128 </thead>
129 </table>
130 </div>
131 <div id="logPeerTableDiv" class="dynamicTableDiv">
132 <table class="dynamicTable unselectable">
133 <thead>
134 <tr class="dynamicTableHeader"></tr>
135 </thead>
136 <tbody></tbody>
137 </table>
138 </div>
139 </div>
140 </div>
141 </div>
143 <ul id="logTableMenu" class="contextMenu">
144 <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>
145 <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>
146 </ul>
148 <script>
149 "use strict";
151 window.qBittorrent ??= {};
152 window.qBittorrent.Log ??= (() => {
153 const exports = () => {
154 return {
155 init: init,
156 unload: unload,
157 load: load,
158 setCurrentTab: setCurrentTab,
159 getFilterText: getFilterText,
160 getSelectedLevels: getSelectedLevels,
161 logLevelChanged: logLevelChanged,
162 filterTextChanged: filterTextChanged
166 let currentSelectedTab = "main";
167 const tableInfo = {
168 main: {
169 instance: new window.qBittorrent.DynamicTable.LogMessageTable(),
170 progress: false,
171 timer: null,
172 last_id: -1
174 peer: {
175 instance: new window.qBittorrent.DynamicTable.LogPeerTable(),
176 progress: false,
177 timer: null,
178 last_id: -1
182 let customSyncLogDataInterval = null;
183 let logFilterTimer = -1;
184 let inputtedFilterText = "";
185 let selectBox;
186 let selectedLogLevels = JSON.parse(LocalPreferences.get("qbt_selected_log_levels")) || ["1", "2", "4", "8"];
188 const init = () => {
189 $("logLevelSelect").getElements("option").each((x) => {
190 if (selectedLogLevels.indexOf(x.value.toString()) !== -1)
191 x.selected = true;
192 else
193 x.selected = false;
196 selectBox = new vanillaSelectBox("#logLevelSelect", {
197 maxHeight: 200,
198 search: false,
199 translations: {
200 all: "QBT_TR(All)QBT_TR[CONTEXT=ExecutionLogWidget]",
201 item: "QBT_TR(item)QBT_TR[CONTEXT=ExecutionLogWidget]",
202 items: "QBT_TR(items)QBT_TR[CONTEXT=ExecutionLogWidget]",
203 selectAll: "QBT_TR(Select All)QBT_TR[CONTEXT=ExecutionLogWidget]",
204 clearAll: "QBT_TR(Clear All)QBT_TR[CONTEXT=ExecutionLogWidget]",
206 placeHolder: "QBT_TR(Choose a log level...)QBT_TR[CONTEXT=ExecutionLogWidget]",
207 keepInlineStyles: false
210 const logTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
211 targets: ".logTableRow",
212 menu: "logTableMenu",
213 actions: {
214 Clear: () => {
215 tableInfo[currentSelectedTab].instance.selectedRowsIds().forEach((rowId) => {
216 tableInfo[currentSelectedTab].instance.removeRow(rowId);
219 updateLabelCount();
222 offsets: {
223 x: -16,
224 y: -57
228 tableInfo["main"].instance.setup("logMessageTableDiv", "logMessageTableFixedHeaderDiv", logTableContextMenu);
229 tableInfo["peer"].instance.setup("logPeerTableDiv", "logPeerTableFixedHeaderDiv", logTableContextMenu);
231 MUI.Panels.instances.LogPanel.contentEl.setStyle("height", "100%");
232 $("logView").setStyle("height", "inherit");
234 load();
237 const unload = () => {
238 for (const table in tableInfo) {
239 if (!Object.hasOwn(tableInfo, table))
240 continue;
241 resetTableTimer(table);
245 const load = () => {
246 customSyncLogDataInterval = null;
247 syncLogWithInterval(100);
250 const resetTableTimer = (curTab) => {
251 if (curTab === undefined)
252 curTab = currentSelectedTab;
254 clearTimeout(tableInfo[curTab].timer);
255 tableInfo[curTab].timer = null;
258 const syncLogWithInterval = (interval) => {
259 if (!tableInfo[currentSelectedTab].progress) {
260 clearTimeout(tableInfo[currentSelectedTab].timer);
261 tableInfo[currentSelectedTab].timer = syncLogData.delay(interval, null, currentSelectedTab);
265 const getFilterText = () => {
266 return inputtedFilterText;
269 const getSelectedLevels = () => {
270 return selectedLogLevels;
273 const getSyncLogDataInterval = () => {
274 return customSyncLogDataInterval ? customSyncLogDataInterval : serverSyncMainDataInterval;
277 const logLevelChanged = () => {
278 const value = selectBox.getResult().sort();
280 if (selectedLogLevels !== value) {
281 tableInfo[currentSelectedTab].last_id = -1;
282 selectedLogLevels = value;
283 LocalPreferences.set("qbt_selected_log_levels", JSON.stringify(selectedLogLevels));
284 logFilterChanged();
288 const filterTextChanged = () => {
289 const value = $("filterTextInput").value.trim();
290 if (inputtedFilterText !== value) {
291 inputtedFilterText = value;
292 logFilterChanged();
296 const logFilterChanged = () => {
297 clearTimeout(logFilterTimer);
298 logFilterTimer = setTimeout((curTab) => {
299 logFilterTimer = -1;
301 tableInfo[curTab].instance.updateTable(false);
302 updateLabelCount(curTab);
303 }, window.qBittorrent.Misc.FILTER_INPUT_DELAY, currentSelectedTab);
306 const setCurrentTab = (tab) => {
307 if (tab === currentSelectedTab)
308 return;
310 currentSelectedTab = tab;
311 if (currentSelectedTab === "main") {
312 selectBox.enable();
313 $("logMessageView").removeClass("invisible");
314 $("logPeerView").addClass("invisible");
315 resetTableTimer("peer");
317 else {
318 selectBox.disable();
319 $("logMessageView").addClass("invisible");
320 $("logPeerView").removeClass("invisible");
321 resetTableTimer("main");
324 clearTimeout(logFilterTimer);
325 logFilterTimer = -1;
326 load();
328 if (tableInfo[currentSelectedTab].instance.filterText !== getFilterText())
329 tableInfo[currentSelectedTab].instance.updateTable();
331 updateLabelCount();
334 const updateLabelCount = (curTab) => {
335 if (curTab === undefined)
336 curTab = currentSelectedTab;
338 $("numFilteredLogs").textContent = tableInfo[curTab].instance.filteredLength();
339 $("numTotalLogs").textContent = tableInfo[curTab].instance.getRowIds().length;
342 const syncLogData = (curTab) => {
343 if (curTab === undefined)
344 curTab = currentSelectedTab;
346 let url;
347 if (curTab === "main") {
348 url = new URI("api/v2/log/main");
349 url.setData({
350 normal: selectedLogLevels.indexOf("1") !== -1,
351 info: selectedLogLevels.indexOf("2") !== -1,
352 warning: selectedLogLevels.indexOf("4") !== -1,
353 critical: selectedLogLevels.indexOf("8") !== -1
356 else {
357 url = new URI("api/v2/log/peers");
360 url.setData("last_known_id", tableInfo[curTab].last_id);
361 tableInfo[curTab].progress = true;
363 new Request.JSON({
364 url: url,
365 method: "get",
366 noCache: true,
367 onFailure: function(response) {
368 const errorDiv = $("error_div");
369 if (errorDiv)
370 errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
371 tableInfo[curTab].progress = false;
372 syncLogWithInterval(10000);
374 onSuccess: function(response) {
375 $("error_div").textContent = "";
377 if ($("logTabColumn").hasClass("invisible"))
378 return;
380 if (response.length > 0) {
381 clearTimeout(logFilterTimer);
382 logFilterTimer = -1;
384 for (let i = 0; i < response.length; ++i) {
385 let row;
386 if (curTab === "main") {
387 row = {
388 rowId: response[i].id,
389 message: response[i].message,
390 timestamp: response[i].timestamp,
391 type: response[i].type,
394 else {
395 row = {
396 rowId: response[i].id,
397 ip: response[i].ip,
398 timestamp: response[i].timestamp,
399 blocked: response[i].blocked,
400 reason: response[i].reason,
403 tableInfo[curTab].instance.updateRowData(row);
404 tableInfo[curTab].last_id = Math.max(response[i].id.toInt(), tableInfo[curTab].last_id);
407 tableInfo[curTab].instance.updateTable();
408 updateLabelCount(curTab);
411 tableInfo[curTab].progress = false;
412 syncLogWithInterval(getSyncLogDataInterval());
414 }).send();
417 new ClipboardJS(".copyLogDataToClipboard", {
418 text: function() {
419 const msg = [];
420 tableInfo[currentSelectedTab].instance.selectedRowsIds().each((rowId) => {
421 msg.push(tableInfo[currentSelectedTab].instance.rows.get(rowId).full_data[(currentSelectedTab === "main") ? "message" : "ip"]);
424 return msg.join("\n");
428 return exports();
429 })();
430 Object.freeze(window.qBittorrent.Log);
431 </script>