diff --git a/assets/js/api.js b/assets/js/api.js
index 45859733f..cd7fbd4d9 100644
--- a/assets/js/api.js
+++ b/assets/js/api.js
@@ -5,11 +5,27 @@ const { protocol, hostname, port, pathname } = window.location;
const base = protocol + "//" + hostname + (port ? ":" + port : "") + pathname;
+// override the way axios serializes arrays in query parameters (a=1&a=2&a=3 instead of a[]=1&a[]=2&a[]=3)
+function customParamsSerializer(params) {
+ const queryString = Object.keys(params)
+ .filter((key) => params[key] !== null)
+ .map((key) => {
+ const value = params[key];
+ if (Array.isArray(value)) {
+ return value.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join("&");
+ }
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
+ })
+ .join("&");
+ return queryString;
+}
+
const api = axios.create({
baseURL: base + "api/",
headers: {
Accept: "application/json",
},
+ paramsSerializer: customParamsSerializer,
});
// global error handling
diff --git a/assets/js/components/MultiSelect.vue b/assets/js/components/MultiSelect.vue
new file mode 100644
index 000000000..e7a312427
--- /dev/null
+++ b/assets/js/components/MultiSelect.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
diff --git a/assets/js/router.js b/assets/js/router.js
index b0b993f66..2bdaaab6e 100644
--- a/assets/js/router.js
+++ b/assets/js/router.js
@@ -56,6 +56,13 @@ export default function setupRouter(i18n) {
path: "/log",
component: () => import("./views/Log.vue"),
beforeEnter: ensureAuth,
+ props: (route) => {
+ const { areas, level } = route.query;
+ return {
+ areas: areas ? areas.split(",") : undefined,
+ level,
+ };
+ },
},
],
});
diff --git a/assets/js/views/Log.vue b/assets/js/views/Log.vue
index a22632570..5e7a911a8 100644
--- a/assets/js/views/Log.vue
+++ b/assets/js/views/Log.vue
@@ -47,29 +47,26 @@
-
+ {{ areasLabel }}
+
@@ -112,14 +109,15 @@ import "@h2d2/shopicons/es/regular/download";
import TopHeader from "../components/TopHeader.vue";
import Play from "../components/MaterialIcon/Play.vue";
import Record from "../components/MaterialIcon/Record.vue";
+import MultiSelect from "../components/MultiSelect.vue";
import api from "../api";
import store from "../store";
-const LEVELS = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
-const DEFAULT_LEVEL = "DEBUG";
+const LEVELS = ["fatal", "error", "warn", "info", "debug", "trace"];
+const DEFAULT_LEVEL = "debug";
const DEFAULT_COUNT = 1000;
-const levelMatcher = new RegExp(`\\[.*?\\] (${LEVELS.join("|")})`);
+const levelMatcher = new RegExp(`\\[.*?\\] (${LEVELS.map((l) => l.toUpperCase()).join("|")})`);
export default {
name: "Log",
@@ -127,14 +125,17 @@ export default {
TopHeader,
Play,
Record,
+ MultiSelect,
+ },
+ props: {
+ areas: { type: Array, default: () => [] },
+ level: { type: String, default: DEFAULT_LEVEL },
},
data() {
return {
lines: [],
- areas: [],
+ availableAreas: [],
search: "",
- level: DEFAULT_LEVEL,
- area: "",
timeout: null,
levels: LEVELS,
busy: false,
@@ -168,6 +169,18 @@ export default {
return { key, className, line };
});
},
+ areaOptions() {
+ return this.availableAreas.map((area) => ({ name: area, value: area }));
+ },
+ areasLabel() {
+ if (this.areas.length === 0) {
+ return this.$t("log.areas");
+ } else if (this.areas.length === 1) {
+ return this.areas[0];
+ } else {
+ return this.$t("log.nAreas", { count: this.areas.length });
+ }
+ },
showMoreButton() {
return this.lines.length === DEFAULT_COUNT;
},
@@ -179,9 +192,9 @@ export default {
if (this.level) {
params.append("level", this.level);
}
- if (this.area) {
- params.append("area", this.area);
- }
+ this.areas.forEach((area) => {
+ params.append("area", area);
+ });
params.append("format", "txt");
return `./api/system/log?${params.toString()}`;
},
@@ -189,6 +202,14 @@ export default {
return this.timeout !== null;
},
},
+ watch: {
+ selectedAreas() {
+ this.updateLogs();
+ },
+ level() {
+ this.updateLogs();
+ },
+ },
methods: {
async updateLogs(showAll) {
// prevent concurrent requests
@@ -198,8 +219,8 @@ export default {
this.busy = true;
const response = await api.get("/system/log", {
params: {
- level: this.level?.toLocaleLowerCase() || null,
- area: this.area || null,
+ level: this.level || null,
+ area: this.areas.length ? this.areas : null,
count: showAll ? null : DEFAULT_COUNT,
},
});
@@ -232,7 +253,7 @@ export default {
async updateAreas() {
try {
const response = await api.get("/system/log/areas");
- this.areas = response.data?.result || [];
+ this.availableAreas = response.data?.result || [];
} catch (e) {
console.error(e);
}
@@ -260,6 +281,22 @@ export default {
this.startInterval();
}
},
+ updateQuery({ level, areas }) {
+ let newLevel = level || this.level;
+ let newAreas = areas || this.areas;
+ // reset to default level
+ if (newLevel === DEFAULT_LEVEL) newLevel = undefined;
+ newAreas = newAreas.length ? newAreas.join(",") : undefined;
+ this.$router.push({
+ query: { level: newLevel, areas: newAreas },
+ });
+ },
+ changeLevel(event) {
+ this.updateQuery({ level: event.target.value });
+ },
+ changeAreas(areas) {
+ this.updateQuery({ areas });
+ },
},
};
diff --git a/i18n/de.toml b/i18n/de.toml
index 3af110a2b..62b187c7b 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -348,8 +348,10 @@ areaLabel = "Nach Bereich filtern"
areas = "Alle Bereiche"
download = "Komplettes Log herunterladen"
levelLabel = "Nach Log-Level filtern"
+nAreas = "{count} Bereiche"
noResults = "Keine passenden Einträge gefunden."
search = "Suchen"
+selectAll = "alle wählen"
showAll = "Alle Einträge anzeigen"
title = "Logs"
update = "Aktualisieren"
diff --git a/i18n/en.toml b/i18n/en.toml
index 655bbf33f..5ca4eb109 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -350,8 +350,10 @@ areaLabel = "Filter by area"
areas = "All areas"
download = "Download complete log"
levelLabel = "Filter by log level"
+nAreas = "{count} areas"
noResults = "No matching log entries."
search = "Search"
+selectAll = "select all"
showAll = "Show all entries"
title = "Logs"
update = "Auto update"