Cloudron 9: UI Issues in the system and email eventlog
-
I ran into two issues with the eventlog pages (
/system-eventlogand/email-eventlog).When search results don't fill the result element enough to create a scrollbar, infinite scroll breaks. If you search for something that returns only a few results, there's no scrollbar and no way to easily trigger loading more entries. You can technically work around this by expanding detail rows until a scrollbar appears and then scroll down to trigger the process of loading the next entries.
The second issue is a pagination bug in the email-eventlog. If you've scrolled down before searching, the new search keeps the old page number instead of resetting to page 1. You end up missing earlier entries, and the reload button also doesn't fix it. Looks like this is just a simple case of a missing reset of the page value as it is there in system-eventlog but missing in email-eventlog.
As far as I can tell, this only affects the system-eventlog and email-eventlog pages, there might be other pages I missed though.
I've put together two patch proposals that fix the problems. The first idea automatically fetches pages until a scrollbar shows up and the other one adds a "Load More" button users can click to fetch more entries. Both solutions add the reset of the page value in the email-eventlog.
eventlog_auto_load.patch
diff --git a/dashboard/src/views/EmailEventlogView.vue b/dashboard/src/views/EmailEventlogView.vue index 172e7d383..b404f87e0 100644 --- a/dashboard/src/views/EmailEventlogView.vue +++ b/dashboard/src/views/EmailEventlogView.vue @@ -1,6 +1,6 @@ <script setup> -import { ref, reactive, onMounted, watch, useTemplateRef } from 'vue'; +import { ref, reactive, onMounted, watch, useTemplateRef, nextTick } from 'vue'; import { Button, TextInput, MultiSelect } from '@cloudron/pankow'; import { useDebouncedRef, prettyEmailAddresses, prettyLongDate } from '@cloudron/pankow/utils'; import MailModel from '../models/MailModel.js'; @@ -29,6 +29,7 @@ const eventlogContainer = useTemplateRef('eventlogContainer'); async function onRefresh() { refreshBusy.value = true; + page.value = 1; const [error, result] = await mailModel.eventlog(types.join(','), search.value, page.value, perPage.value); if (error) return console.error(error); @@ -36,15 +37,28 @@ async function onRefresh() { eventlogs.value = result; refreshBusy.value = false; + + await nextTick(); + while (eventlogContainer.value && eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) { + const hasMore = await fetchMore(); + if (!hasMore) break; + await nextTick(); + } } async function fetchMore() { page.value++; const [error, result] = await mailModel.eventlog(types.join(','), search.value, page.value, perPage.value); - if (error) return console.error(error); + if (error) { + console.error(error); + return false; + } + + if (!result || result.length === 0) return false; eventlogs.value = eventlogs.value.concat(result); + return true; } async function onScroll(event) { @@ -57,10 +71,6 @@ watch(search, onRefresh); onMounted(async () => { await onRefresh(); - - while (eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) { - await fetchMore(); - } }); </script> diff --git a/dashboard/src/views/EventlogView.vue b/dashboard/src/views/EventlogView.vue index f30712ca9..32c88a616 100644 --- a/dashboard/src/views/EventlogView.vue +++ b/dashboard/src/views/EventlogView.vue @@ -1,6 +1,6 @@ <script setup> -import { ref, reactive, onMounted, onUnmounted, watch, useTemplateRef } from 'vue'; +import { ref, reactive, onMounted, onUnmounted, watch, useTemplateRef, nextTick } from 'vue'; import { Button, TextInput, MultiSelect } from '@cloudron/pankow'; import { useDebouncedRef, copyToClipboard, prettyLongDate, prettyShortDate } from '@cloudron/pankow/utils'; import AppsModel from '../models/AppsModel.js'; @@ -120,12 +120,24 @@ async function onRefresh() { }); refreshBusy.value = false; + + await nextTick(); + while (eventlogContainer.value && eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) { + const hasMore = await fetchMore(); + if (!hasMore) break; + await nextTick(); + } } async function fetchMore() { page.value++; const [error, result] = await eventlogsModel.search(actions.join(','), search.value, page.value, perPage.value); - if (error) return console.error(error); + if (error) { + console.error(error); + return false; + } + + if (!result || result.length === 0) return false; eventlogs.value = eventlogs.value.concat(result.map(e => { return { @@ -135,6 +147,8 @@ async function fetchMore() { source: eventlogSource(e, e.appId ? getApp(e.appId) : null) }; })); + + return true; } async function onScroll(event) { @@ -163,10 +177,6 @@ onMounted(async () => { onHashChange(); if (!search.value) { onRefresh(); - - while (eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) { - await fetchMore(); - } } });eventlog_button_load.patch
diff --git a/dashboard/public/translation/da.json b/dashboard/public/translation/da.json index d0e748654..ceeb78690 100644 --- a/dashboard/public/translation/da.json +++ b/dashboard/public/translation/da.json @@ -52,7 +52,9 @@ "users": "Brugere" }, "statusEnabled": "Aktiveret", - "loadingPlaceholder": "Indlæsning" + "loadingPlaceholder": "Indlæsning", + "loadMore": "Indlæs mere", + "noMoreResults": "Ikke flere resultater" }, "appstore": { "category": { diff --git a/dashboard/public/translation/de.json b/dashboard/public/translation/de.json index 77915f281..a10a89975 100644 --- a/dashboard/public/translation/de.json +++ b/dashboard/public/translation/de.json @@ -61,7 +61,9 @@ "users": "User", "groups": "Gruppen" }, - "loadingPlaceholder": "Laden" + "loadingPlaceholder": "Laden", + "loadMore": "Mehr laden", + "noMoreResults": "Keine weiteren Ergebnisse" }, "network": { "title": "Netzwerk", diff --git a/dashboard/public/translation/en.json b/dashboard/public/translation/en.json index 177baa39d..77f08d8bb 100644 --- a/dashboard/public/translation/en.json +++ b/dashboard/public/translation/en.json @@ -65,6 +65,8 @@ }, "statusEnabled": "Enabled", "loadingPlaceholder": "Loading", + "loadMore": "Load More", + "noMoreResults": "No more results", "platform": { "startupFailed": "Platform startup failed" } diff --git a/dashboard/public/translation/es.json b/dashboard/public/translation/es.json index 600bc0bd1..1bbdbb51c 100644 --- a/dashboard/public/translation/es.json +++ b/dashboard/public/translation/es.json @@ -76,7 +76,9 @@ "groups": "Grupos" }, "statusEnabled": "Habilitado", - "loadingPlaceholder": "Cargando" + "loadingPlaceholder": "Cargando", + "loadMore": "Cargar más", + "noMoreResults": "No hay más resultados" }, "apps": { "searchPlaceholder": "Busca Aplicaciones", diff --git a/dashboard/public/translation/fr.json b/dashboard/public/translation/fr.json index 1bc5d7c99..6922c3bf2 100644 --- a/dashboard/public/translation/fr.json +++ b/dashboard/public/translation/fr.json @@ -52,7 +52,9 @@ "navbar": { "users": "Utilisateurs" }, - "loadingPlaceholder": "Chargement" + "loadingPlaceholder": "Chargement", + "loadMore": "Charger plus", + "noMoreResults": "Plus de résultats" }, "users": { "users": { diff --git a/dashboard/public/translation/id.json b/dashboard/public/translation/id.json index d619b088e..3d67a3e0e 100644 --- a/dashboard/public/translation/id.json +++ b/dashboard/public/translation/id.json @@ -11,6 +11,8 @@ }, "table": { "date": "Tanggal" - } + }, + "loadMore": "Load More", + "noMoreResults": "No more results" } } diff --git a/dashboard/public/translation/it.json b/dashboard/public/translation/it.json index 343972b13..f80e9c7f3 100644 --- a/dashboard/public/translation/it.json +++ b/dashboard/public/translation/it.json @@ -24,7 +24,9 @@ "description": "Usa questo per applicare gli aggiornamenti di sicurezza o se hai riscontrato comportamenti inaspettati. Tutte le app e i servizi attivi attualmente su questo Cloudron saranno automaticamente riavviati quando il riavvio sarà completato.", "title": "Vuoi davvero riavviare il server?" }, - "searchPlaceholder": "Cerca" + "searchPlaceholder": "Cerca", + "loadMore": "Carica altro", + "noMoreResults": "Nessun altro risultato" }, "apps": { "searchPlaceholder": "Cerca una App", diff --git a/dashboard/public/translation/ja.json b/dashboard/public/translation/ja.json index b05674b59..5826c28fb 100644 --- a/dashboard/public/translation/ja.json +++ b/dashboard/public/translation/ja.json @@ -20,7 +20,9 @@ "cancel": "キャンセル" }, "logout": "ログアウト", - "offline": "Cloudronはオフラインです。再接続中…" + "offline": "Cloudronはオフラインです。再接続中…", + "loadMore": "さらに読み込む", + "noMoreResults": "これ以上の結果はありません" }, "apps": { "searchPlaceholder": "アプリを探す", diff --git a/dashboard/public/translation/nl.json b/dashboard/public/translation/nl.json index 9bb891d25..c418e4110 100644 --- a/dashboard/public/translation/nl.json +++ b/dashboard/public/translation/nl.json @@ -65,6 +65,8 @@ }, "statusEnabled": "Ingeschakeld", "loadingPlaceholder": "Laden", + "loadMore": "Meer laden", + "noMoreResults": "Geen resultaten meer", "platform": { "startupFailed": "Platformstart mislukt" } diff --git a/dashboard/public/translation/pl.json b/dashboard/public/translation/pl.json index e8e4c5876..42055f742 100644 --- a/dashboard/public/translation/pl.json +++ b/dashboard/public/translation/pl.json @@ -50,7 +50,9 @@ "cancel": "Anuluj" }, "logout": "Wyloguj", - "offline": "Cloudron jest niedostępny. Odnawiam połączenie…" + "offline": "Cloudron jest niedostępny. Odnawiam połączenie…", + "loadMore": "Załaduj więcej", + "noMoreResults": "Brak więcej wyników" }, "apps": { "searchPlaceholder": "Szukaj Aplikacji", diff --git a/dashboard/public/translation/pt.json b/dashboard/public/translation/pt.json index 3d5181bc4..72d43367d 100644 --- a/dashboard/public/translation/pt.json +++ b/dashboard/public/translation/pt.json @@ -62,7 +62,9 @@ "groups": "Grupos" }, "statusEnabled": "Ativado", - "loadingPlaceholder": "A carregar" + "loadingPlaceholder": "A carregar", + "loadMore": "Carregar mais", + "noMoreResults": "Não há mais resultados" }, "appstore": { "category": { diff --git a/dashboard/public/translation/ru.json b/dashboard/public/translation/ru.json index 2d07f0528..c10a1aeb0 100644 --- a/dashboard/public/translation/ru.json +++ b/dashboard/public/translation/ru.json @@ -63,7 +63,9 @@ "groups": "Группы" }, "statusEnabled": "Включено", - "loadingPlaceholder": "Загрузка" + "loadingPlaceholder": "Загрузка", + "loadMore": "Загрузить ещё", + "noMoreResults": "Больше нет результатов" }, "appstore": { "category": { diff --git a/dashboard/public/translation/vi.json b/dashboard/public/translation/vi.json index 17023bca1..5b4a374f9 100644 --- a/dashboard/public/translation/vi.json +++ b/dashboard/public/translation/vi.json @@ -54,7 +54,9 @@ "navbar": { "users": "Người dùng" }, - "loadingPlaceholder": "Đang tải" + "loadingPlaceholder": "Đang tải", + "loadMore": "Tải thêm", + "noMoreResults": "Không còn kết quả" }, "appstore": { "title": "Cửa hàng App", diff --git a/dashboard/public/translation/zh_Hans.json b/dashboard/public/translation/zh_Hans.json index 66bd4d730..ee7f34ef4 100644 --- a/dashboard/public/translation/zh_Hans.json +++ b/dashboard/public/translation/zh_Hans.json @@ -204,7 +204,9 @@ "statusEnabled": "已启用", "navbar": { "users": "用户" - } + }, + "loadMore": "加载更多", + "noMoreResults": "没有更多结果" }, "appstore": { "title": "App Store", diff --git a/dashboard/src/views/EmailEventlogView.vue b/dashboard/src/views/EmailEventlogView.vue index 172e7d383..1387b3c65 100644 --- a/dashboard/src/views/EmailEventlogView.vue +++ b/dashboard/src/views/EmailEventlogView.vue @@ -20,6 +20,8 @@ const availableTypes = [ ]; const refreshBusy = ref(false); +const loadMoreBusy = ref(false); +const hasMoreResults = ref(true); const eventlogs = ref([]); const search = useDebouncedRef(''); const page = ref(1); @@ -29,22 +31,40 @@ const eventlogContainer = useTemplateRef('eventlogContainer'); async function onRefresh() { refreshBusy.value = true; + page.value = 1; + hasMoreResults.value = true; const [error, result] = await mailModel.eventlog(types.join(','), search.value, page.value, perPage.value); if (error) return console.error(error); eventlogs.value = result; - + hasMoreResults.value = result.length === perPage.value; refreshBusy.value = false; } async function fetchMore() { + if (!hasMoreResults.value) return; + + loadMoreBusy.value = true; page.value++; const [error, result] = await mailModel.eventlog(types.join(','), search.value, page.value, perPage.value); - if (error) return console.error(error); + + if (error) { + console.error(error); + loadMoreBusy.value = false; + return; + } + + if (!result || result.length === 0) { + hasMoreResults.value = false; + loadMoreBusy.value = false; + return; + } eventlogs.value = eventlogs.value.concat(result); + hasMoreResults.value = result.length === perPage.value; + loadMoreBusy.value = false; } async function onScroll(event) { @@ -57,10 +77,6 @@ watch(search, onRefresh); onMounted(async () => { await onRefresh(); - - while (eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) { - await fetchMore(); - } }); </script> @@ -134,6 +150,11 @@ onMounted(async () => { </tbody> </table> </div> + <div style="display: flex; justify-content: center; padding: 20px;"> + <Button @click="fetchMore" :loading="loadMoreBusy" :disabled="!hasMoreResults"> + {{ hasMoreResults ? $t('main.loadMore') : $t('main.noMoreResults') }} + </Button> + </div> </div> </div> </template> diff --git a/dashboard/src/views/EventlogView.vue b/dashboard/src/views/EventlogView.vue index f30712ca9..b1588600f 100644 --- a/dashboard/src/views/EventlogView.vue +++ b/dashboard/src/views/EventlogView.vue @@ -95,6 +95,8 @@ const availableActions = [ ]; const refreshBusy = ref(false); +const loadMoreBusy = ref(false); +const hasMoreResults = ref(true); const apps = ref([]); const eventlogs = ref([]); const search = useDebouncedRef(''); @@ -106,6 +108,7 @@ const eventlogContainer = useTemplateRef('eventlogContainer'); async function onRefresh() { refreshBusy.value = true; page.value = 1; + hasMoreResults.value = true; const [error, result] = await eventlogsModel.search(actions.join(','), search.value, page.value, perPage.value); if (error) return console.error(error); @@ -119,22 +122,40 @@ async function onRefresh() { }; }); + hasMoreResults.value = result.length === perPage.value; refreshBusy.value = false; } async function fetchMore() { + if (!hasMoreResults.value) return; + + loadMoreBusy.value = true; page.value++; const [error, result] = await eventlogsModel.search(actions.join(','), search.value, page.value, perPage.value); - if (error) return console.error(error); + + if (error) { + console.error(error); + loadMoreBusy.value = false; + return; + } + + if (!result || result.length === 0) { + hasMoreResults.value = false; + loadMoreBusy.value = false; + return; + } eventlogs.value = eventlogs.value.concat(result.map(e => { return { id: Symbol(), raw: e, - details: eventlogDetails(e, e.appId ? getApp(e.appId) : null), - source: eventlogSource(e, e.appId ? getApp(e.appId) : null) + details: eventlogDetails(e, e.data?.appId ? getApp(e.data.appId) : null), + source: eventlogSource(e, e.data?.appId ? getApp(e.data.appId) : null) }; })); + + hasMoreResults.value = result.length === perPage.value; + loadMoreBusy.value = false; } async function onScroll(event) { @@ -163,10 +184,6 @@ onMounted(async () => { onHashChange(); if (!search.value) { onRefresh(); - - while (eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) { - await fetchMore(); - } } }); @@ -203,6 +220,11 @@ onUnmounted(() => { </div> </div> </div> + <div style="display: flex; justify-content: center; padding: 20px;"> + <Button @click="fetchMore" :loading="loadMoreBusy" :disabled="!hasMoreResults"> + {{ hasMoreResults ? $t('main.loadMore') : $t('main.noMoreResults') }} + </Button> + </div> </div> </div> </template> -
Thanks for the detailed description. The page reset on reload was an oversight, good to get that fixed.
Still working on the refresh, when search changes and results do not fill up the view. Overall we have to rework eventlogs (both system and mail). Currently the pagination does not work well with the fetchMore(), especially when one tries to track down an issue with a combination of search and filters. It is very hard to get context around those events, as the pagination works with the query not the time of the events

-
Thanks for the detailed description. The page reset on reload was an oversight, good to get that fixed.
Still working on the refresh, when search changes and results do not fill up the view. Overall we have to rework eventlogs (both system and mail). Currently the pagination does not work well with the fetchMore(), especially when one tries to track down an issue with a combination of search and filters. It is very hard to get context around those events, as the pagination works with the query not the time of the events

@nebulon said in Cloudron 9: UI Issues in the system and email eventlog:
Still working on the refresh, when search changes and results do not fill up the view. Overall we have to rework eventlogs (both system and mail). Currently the pagination does not work well with the fetchMore(), especially when one tries to track down an issue with a combination of search and filters. It is very hard to get context around those events, as the pagination works with the query not the time of the events

I might be misunderstanding you, but the issue happens even without using a combination of search or filters. The email-eventlog has
per_pageset to10, which isn't enough to create a scrollbar on a modern monitor with the browser being maximized, so infinite scroll can basically never work when you search for anything. The system-eventlog has the same problem on larger displays despite usingper_pageof40. Maybe providing the user with a button to manually trigger the loading of more items is the way to go here? Or maybe you could just load more items "per page"? -
The code actually would fetch more pages until it starts scrolling to fill up the screen, but only of course if there are enough results to show: https://git.cloudron.io/platform/box/-/commit/b53da61e7cec3c919ac11ebd65a8ccce03f7f611
Maybe there is some other bug you are hitting, here on this screen if I change page size to 5 it fetches about 20 pages