WebUI: migrate to fetch API
[qBittorrent.git] / src / webui / www / private / views / log.html
blobca98f3dda390411c1caa4aab896017e34bcfdce3
1 <style type="text/css">
2 #logTopBar {
3 flex-shrink: 0;
4 margin-top: 1em;
7 #logFilterBar {
8 align-items: center;
9 display: flex;
10 flex-wrap: wrap;
11 gap: 2px;
12 height: 24px;
15 #logFilterBar>label {
16 font-weight: bold;
17 margin-right: .3em;
20 #logFilterBar>button {
21 display: inline-block;
22 padding: 2px 12px;
25 #logView {
26 display: flex;
27 flex-direction: column;
28 height: 100%;
29 padding: 0 20px;
30 overflow: auto;
33 #logContentView {
34 flex-grow: 1;
35 overflow: auto;
38 #logMessageTableFixedHeaderDiv .dynamicTableHeader,
39 #logPeerTableFixedHeaderDiv .dynamicTableHeader {
40 cursor: default;
43 #filterTextInput {
44 background-image: url("../images/edit-find.svg");
45 background-repeat: no-repeat;
46 background-position: 2px;
47 background-size: 1.5em;
48 padding: 2px 2px 2px 2em;
49 margin-left: .3em;
50 width: 237px;
51 border: 1px solid var(--color-border-default);
52 border-radius: 3px;
55 #logFilterSummary {
56 overflow: auto;
57 margin: 1em 0 .5em;
60 #numFilteredLogs,
61 #numTotalLogs {
62 font-style: italic;
65 .logTableRowlogNormal {
66 color: var(--color-text-default);
69 .logTableRowlogInfo {
70 color: var(--color-text-blue);
73 .logTableRowlogWarning {
74 color: var(--color-text-orange);
77 .logTableRowlogCritical,
78 .logTableRowpeerBlocked {
79 color: var(--color-text-red);
82 .vsb-main>button {
83 padding: 2px 12px !important;
86 </style>
88 <div id="logView">
89 <div id="logTopBar">
90 <div id="logFilterBar">
91 <label for="logLevelSelect">QBT_TR(Log Levels:)QBT_TR[CONTEXT=ExecutionLogWidget]</label>
92 <select multiple size="1" id="logLevelSelect" class="logLevelSelect" onchange="window.qBittorrent.Log.logLevelChanged()">
93 <option value="1">QBT_TR(Normal Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
94 <option value="2">QBT_TR(Information Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
95 <option value="4">QBT_TR(Warning Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
96 <option value="8">QBT_TR(Critical Messages)QBT_TR[CONTEXT=ExecutionLogWidget]</option>
97 </select>
99 <input type="search" id="filterTextInput" oninput="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">
100 <button type="button" title="Clear input" onclick="javascript:document.getElementById('filterTextInput').value='';window.qBittorrent.Log.filterTextChanged();">QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]</button>
101 </div>
103 <div id="logFilterSummary">
104 <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>
105 </div>
106 </div>
108 <div id="logContentView">
109 <div id="logMessageView">
110 <div id="logMessageTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
111 <table class="dynamicTable unselectable" style="position:relative;">
112 <thead>
113 <tr class="dynamicTableHeader"></tr>
114 </thead>
115 </table>
116 </div>
117 <div id="logMessageTableDiv" class="dynamicTableDiv">
118 <table class="dynamicTable unselectable">
119 <thead>
120 <tr class="dynamicTableHeader"></tr>
121 </thead>
122 <tbody></tbody>
123 </table>
124 </div>
125 </div>
126 <div id="logPeerView" class="invisible">
127 <div id="logPeerTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
128 <table class="dynamicTable unselectable" style="position:relative;">
129 <thead>
130 <tr class="dynamicTableHeader"></tr>
131 </thead>
132 </table>
133 </div>
134 <div id="logPeerTableDiv" class="dynamicTableDiv">
135 <table class="dynamicTable unselectable">
136 <thead>
137 <tr class="dynamicTableHeader"></tr>
138 </thead>
139 <tbody></tbody>
140 </table>
141 </div>
142 </div>
143 </div>
144 </div>
146 <ul id="logTableMenu" class="contextMenu">
147 <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>
148 <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>
149 </ul>
151 <script>
152 "use strict";
154 window.qBittorrent ??= {};
155 window.qBittorrent.Log ??= (() => {
156 const exports = () => {
157 return {
158 init: init,
159 unload: unload,
160 load: load,
161 setCurrentTab: setCurrentTab,
162 getFilterText: getFilterText,
163 getSelectedLevels: getSelectedLevels,
164 logLevelChanged: logLevelChanged,
165 filterTextChanged: filterTextChanged
169 let currentSelectedTab = "main";
170 const tableInfo = {
171 main: {
172 instance: new window.qBittorrent.DynamicTable.LogMessageTable(),
173 progress: false,
174 timer: null,
175 last_id: -1
177 peer: {
178 instance: new window.qBittorrent.DynamicTable.LogPeerTable(),
179 progress: false,
180 timer: null,
181 last_id: -1
185 let logFilterTimer = -1;
186 let inputtedFilterText = "";
187 let selectBox;
188 let selectedLogLevels = JSON.parse(LocalPreferences.get("qbt_selected_log_levels")) || ["1", "2", "4", "8"];
190 const init = () => {
191 for (const option of $("logLevelSelect").options)
192 option.toggleAttribute("selected", selectedLogLevels.includes(option.value));
194 selectBox = new vanillaSelectBox("#logLevelSelect", {
195 maxHeight: 200,
196 search: false,
197 translations: {
198 all: "QBT_TR(All)QBT_TR[CONTEXT=ExecutionLogWidget]",
199 item: "QBT_TR(item)QBT_TR[CONTEXT=ExecutionLogWidget]",
200 items: "QBT_TR(items)QBT_TR[CONTEXT=ExecutionLogWidget]",
201 selectAll: "QBT_TR(Select All)QBT_TR[CONTEXT=ExecutionLogWidget]",
202 clearAll: "QBT_TR(Clear All)QBT_TR[CONTEXT=ExecutionLogWidget]",
204 placeHolder: "QBT_TR(Choose a log level...)QBT_TR[CONTEXT=ExecutionLogWidget]",
205 keepInlineStyles: false
208 const logTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
209 targets: ":is(#logMessageView, #logPeerView) tr",
210 menu: "logTableMenu",
211 actions: {
212 Clear: () => {
213 tableInfo[currentSelectedTab].instance.selectedRowsIds().forEach((rowId) => {
214 tableInfo[currentSelectedTab].instance.removeRow(rowId);
217 updateLabelCount();
220 offsets: {
221 x: 3,
222 y: -90
226 tableInfo["main"].instance.setup("logMessageTableDiv", "logMessageTableFixedHeaderDiv", logTableContextMenu);
227 tableInfo["peer"].instance.setup("logPeerTableDiv", "logPeerTableFixedHeaderDiv", logTableContextMenu);
229 MUI.Panels.instances.LogPanel.contentEl.style.height = "100%";
231 load();
234 const unload = () => {
235 for (const table in tableInfo) {
236 if (!Object.hasOwn(tableInfo, table))
237 continue;
238 resetTableTimer(table);
242 const load = () => {
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 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").classList.remove("invisible");
310 $("logPeerView").classList.add("invisible");
311 resetTableTimer("peer");
313 else {
314 selectBox.disable();
315 $("logMessageView").classList.add("invisible");
316 $("logPeerView").classList.remove("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.getRowSize();
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.includes("1"),
347 info: selectedLogLevels.includes("2"),
348 warning: selectedLogLevels.includes("4"),
349 critical: selectedLogLevels.includes("8")
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 fetch(url, {
360 method: "GET",
361 cache: "no-store"
363 .then(async (response) => {
364 if (!response.ok) {
365 const errorDiv = $("error_div");
366 if (errorDiv)
367 errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
368 tableInfo[curTab].progress = false;
369 syncLogWithInterval(10000);
370 return;
373 $("error_div").textContent = "";
375 if ($("logTabColumn").classList.contains("invisible"))
376 return;
378 const responseJSON = await response.json();
379 if (responseJSON.length > 0) {
380 clearTimeout(logFilterTimer);
381 logFilterTimer = -1;
383 for (let i = 0; i < responseJSON.length; ++i) {
384 let row;
385 if (curTab === "main") {
386 row = {
387 rowId: responseJSON[i].id,
388 message: responseJSON[i].message,
389 timestamp: responseJSON[i].timestamp,
390 type: responseJSON[i].type,
393 else {
394 row = {
395 rowId: responseJSON[i].id,
396 ip: responseJSON[i].ip,
397 timestamp: responseJSON[i].timestamp,
398 blocked: responseJSON[i].blocked,
399 reason: responseJSON[i].reason,
402 tableInfo[curTab].instance.updateRowData(row);
403 tableInfo[curTab].last_id = Math.max(Number(responseJSON[i].id), tableInfo[curTab].last_id);
406 tableInfo[curTab].instance.updateTable();
407 updateLabelCount(curTab);
410 tableInfo[curTab].progress = false;
411 syncLogWithInterval(getSyncLogDataInterval());
415 new ClipboardJS(".copyLogDataToClipboard", {
416 text: () => {
417 const msg = [];
418 tableInfo[currentSelectedTab].instance.selectedRowsIds().forEach((rowId) => {
419 msg.push(tableInfo[currentSelectedTab].instance.getRow(rowId).full_data[(currentSelectedTab === "main") ? "message" : "ip"]);
422 return msg.join("\n");
426 return exports();
427 })();
428 Object.freeze(window.qBittorrent.Log);
429 </script>