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"