Skip to content

Commit 9e72bb9

Browse files
authored
Merge pull request #1615 from nextcloud/backport/1505/stable30
2 parents 8ae0ee0 + 4e2fb37 commit 9e72bb9

File tree

9 files changed

+207
-90
lines changed

9 files changed

+207
-90
lines changed

css/logreader-main.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
/* extracted by css-entry-points-plugin */
2-
@import './main-CK8tpAlq.chunk.css';
2+
@import './main-CwtsoKIz.chunk.css';

css/main-CwtsoKIz.chunk.css

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/logreader-main.mjs

Lines changed: 46 additions & 46 deletions
Large diffs are not rendered by default.

js/logreader-main.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/table/LogTable.vue

Lines changed: 128 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
:open.sync="isModalOpen"
99
:current-entry.sync="currentRow"
1010
:log-entries="sortedRows" />
11-
<table class="log-table__table">
12-
<thead>
11+
<table ref="tableRoot" class="log-table__table">
12+
<thead role="rowgroup" class="log-table__header">
1313
<tr>
1414
<LogTableHeader :name="t('logreader', 'Level')"
1515
:sorted.sync="sortedByLevel" />
@@ -23,9 +23,9 @@
2323
<th><span class="hidden-visually">{{ t('logreader', 'Log entry actions') }}</span></th>
2424
</tr>
2525
</thead>
26-
<tbody ref="tableBody">
27-
<tr v-if="sortedByTime === 'ascending'">
28-
<td colspan="5" class="log-table__load-more">
26+
<tbody ref="tableBody" :style="tbodyStyle" class="log-table__body">
27+
<tr v-if="sortedByTime === 'ascending'" class="log-table__load-more">
28+
<td>
2929
<IntersectionObserver v-if="logStore.hasRemainingEntries" @intersection="loadMore">
3030
{{ t('logreader', 'Loading older log entries') }}
3131
</IntersectionObserver>
@@ -35,14 +35,15 @@
3535
</td>
3636
</tr>
3737

38-
<LogTableRow v-for="row, rowNumber in sortedRows"
39-
:key="rowNumber"
38+
<LogTableRow v-for="row in renderedItems"
39+
:key="row.id"
4040
:row="row"
41+
class="log-table__row"
4142
@show-details="showDetailsForRow" />
4243
</tbody>
43-
<tfoot>
44-
<tr v-if="sortedByTime !== 'ascending'">
45-
<td colspan="5" class="log-table__load-more">
44+
<tfoot role="rowgroup" class="log-table__footer">
45+
<tr v-if="sortedByTime !== 'ascending'" class="log-table__load-more">
46+
<td>
4647
<IntersectionObserver v-if="logStore.hasRemainingEntries" @intersection="loadMore">
4748
{{ t('logreader', 'Loading older log entries') }}
4849
</IntersectionObserver>
@@ -59,17 +60,22 @@
5960
<script setup lang="ts">
6061
import type { ILogEntry, ISortingOptions } from '../../interfaces'
6162
62-
import { computed, nextTick, ref } from 'vue'
63+
import { computed, nextTick, onMounted, onBeforeUnmount, ref } from 'vue'
6364
import { translate as t } from '@nextcloud/l10n'
6465
import { useSettingsStore } from '../../store/settings'
6566
import { useLogStore } from '../../store/logging'
67+
import { debounce } from '../../utils/debounce'
68+
import { logger } from '../../utils/logger'
6669
6770
import IntersectionObserver from '../IntersectionObserver.vue'
6871
import LogDetailsModal from '../LogDetailsModal.vue'
6972
import LogTableHeader from './LogTableHeader.vue'
7073
import LogTableRow from './LogTableRow.vue'
7174
import LogSearch from '../LogSearch.vue'
7275
76+
// Items to render before and after the visible area
77+
const bufferItems = 3
78+
7379
const settingsStore = useSettingsStore()
7480
const logStore = useLogStore()
7581
@@ -110,8 +116,9 @@ const showDetailsForRow = (row: ILogEntry) => {
110116
}
111117
112118
/**
113-
* Reference to the table body, used for keeping scroll position on loading more entries
119+
* Reference to the table elements, used for keeping scroll position on loading more entries
114120
*/
121+
const tableRoot = ref<HTMLElement>()
115122
const tableBody = ref<HTMLElement>()
116123
117124
/**
@@ -146,49 +153,131 @@ const sortedRows = computed(() => {
146153
sorted.sort((a, b) => order(byLevel, sortedByLevel.value, a, b) || order(byApp, sortedByApp.value, a, b) || order(byTime, sortedByTime.value, a, b))
147154
return sorted
148155
})
156+
157+
/**
158+
* Virtual scrolling logic
159+
*/
160+
const resizeObserver = ref<ResizeObserver | null>(null)
161+
162+
const firstVisibleRowIndex = ref(0)
163+
const startIndex = computed(() => Math.max(0, firstVisibleRowIndex.value - bufferItems))
164+
165+
const tableRootHeight = ref(0)
166+
const tableHeadHeight = ref(44)
167+
const tableRowHeight = ref(42)
168+
const itemsInViewport = computed(() => Math.ceil((tableRootHeight.value - tableHeadHeight.value) / tableRowHeight.value) + bufferItems * 2)
169+
170+
const renderedItems = computed(() => sortedRows.value.slice(startIndex.value, startIndex.value + itemsInViewport.value))
171+
172+
const tbodyStyle = computed(() => {
173+
const isOverScrolled = startIndex.value + itemsInViewport.value > sortedRows.value.length
174+
const lastIndex = sortedRows.value.length - startIndex.value - itemsInViewport.value
175+
const hiddenAfterItems = Math.min(sortedRows.value.length - startIndex.value, lastIndex)
176+
177+
return {
178+
paddingTop: `${startIndex.value * tableRowHeight.value}px`,
179+
paddingBottom: isOverScrolled ? 0 : `${hiddenAfterItems * tableRowHeight.value}px`,
180+
}
181+
})
182+
183+
onMounted(() => {
184+
resizeObserver.value = new ResizeObserver(debounce(() => {
185+
tableRootHeight.value = tableRoot.value?.clientHeight ?? 0
186+
tableHeadHeight.value = tableRoot.value?.querySelector('thead.log-table__header')?.clientHeight ?? 44
187+
tableRowHeight.value = tableRoot.value?.querySelector('tr.log-table__row:not(.expanded)')?.clientHeight ?? 42
188+
logger.debug('ResizeObserver for virtual list updated', { rendered: renderedItems.value.length, total: filteredRows.value.length })
189+
onScroll()
190+
}, 100))
191+
192+
resizeObserver.value.observe(tableRoot.value!)
193+
tableRoot.value!.addEventListener('scroll', onScroll)
194+
})
195+
196+
onBeforeUnmount(() => {
197+
if (resizeObserver.value) {
198+
resizeObserver.value.disconnect()
199+
}
200+
})
201+
202+
/**
203+
* Update the first visible row index on scroll (max 0 to prevent negative index)
204+
*/
205+
function onScroll() {
206+
firstVisibleRowIndex.value = Math.max(0, Math.round(tableRoot.value!.scrollTop / tableRowHeight.value))
207+
}
149208
</script>
150209

151210
<style lang="scss" scoped>
152211
.log-table {
153212
width: 100%;
154213
height: 100%;
155-
overflow: scroll;
214+
overflow: hidden;
156215
157216
&__table {
158217
width: calc(100% - 12px);
159218
margin-inline: 6px;
160219
table-layout: fixed;
220+
221+
// Necessary for virtual scroll optimized rendering
222+
display: block;
223+
overflow: auto;
224+
height: 100%;
225+
will-change: scroll-position;
161226
}
162227
163228
&__load-more {
164-
text-align: center;
165-
padding-block: 4px;
166-
}
229+
display: flex;
167230
168-
th, td {
169-
// level column
170-
&:nth-child(1) {
171-
width: 108px;
172-
}
173-
// app column
174-
&:nth-child(2) {
175-
width: 168px;
231+
:deep(td) {
232+
flex-basis: 100%;
233+
text-align: center;
234+
padding-block: 4px;
176235
}
177-
// message column
178-
&:nth-child(3) {
179-
width: 418px;
180-
}
181-
// time column
182-
&:nth-child(4) {
183-
width: 168px;
236+
}
237+
238+
&__header,
239+
&__body,
240+
&__footer {
241+
display: flex;
242+
flex-direction: column;
243+
width: 100%;
244+
245+
:deep(tr) {
246+
display: flex;
184247
}
185-
// actions column
186-
&:last-child {
187-
width: 62px; // 44px button + 18px padding
248+
249+
:deep(th),
250+
:deep(td) {
251+
flex-shrink: 0;
252+
253+
// level column
254+
&:nth-child(1) {
255+
width: 108px;
256+
}
257+
// app column
258+
&:nth-child(2) {
259+
width: 168px;
260+
}
261+
// message column
262+
&:nth-child(3) {
263+
width: 418px;
264+
flex-grow: 1;
265+
}
266+
// time column
267+
&:nth-child(4) {
268+
width: 25ch; // "Mar 10, 2025, 12:00:00 PM" length
269+
}
270+
// actions column
271+
&:last-child {
272+
width: 62px; // 44px button + 18px padding
273+
}
188274
}
189275
}
190276
191-
thead {
277+
&__header {
278+
position: sticky;
279+
top: 0;
280+
z-index: 1;
192281
min-height: 44px;
193282
194283
:deep(th) {
@@ -200,7 +289,7 @@ const sortedRows = computed(() => {
200289
}
201290
}
202291
203-
tbody {
292+
&__body {
204293
// Some spacing for first row
205294
&:before {
206295
content: '\200c';
@@ -210,5 +299,8 @@ const sortedRows = computed(() => {
210299
}
211300
}
212301
302+
&__row {
303+
min-height: 42px;
304+
}
213305
}
214306
</style>

src/components/table/LogTableRow.vue

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
<div class="row-message__container" :class="{ 'row-message__container--expanded': isExpanded }">
1515
<div class="row-message__text">
1616
<LogException v-if="row.exception" :exception="row.exception" />
17-
<!-- Show log message if either there is no exception or a custom message was added -->
18-
<div v-if="!row.exception || row.message !== row.exception.Message" class="row-message__text_message" :title="row.message">
17+
<div v-if="showLogMessage" class="row-message__text_message" :title="row.message">
1918
{{ row.message }}
2019
</div>
2120
</div>
@@ -117,6 +116,13 @@ const timestamp = computed(() => Date.parse(props.row.time))
117116
*/
118117
const isExpanded = ref(false)
119118
119+
/**
120+
* Show log message if either there is no exception or a custom message was added (at expanded view)
121+
*/
122+
const showLogMessage = computed(() => {
123+
return !props.row.exception || (props.row.message !== props.row.exception.Message && isExpanded.value)
124+
})
125+
120126
/**
121127
* Human readable and localized level name
122128
*/
@@ -175,10 +181,10 @@ watch(isExpanded, () => resizeTabeRow)
175181

176182
<style lang="scss" scoped>
177183
td {
178-
display: table-cell;
184+
display: block;
179185
overflow: hidden;
180186
text-overflow: ellipsis;
181-
vertical-align: top;
187+
min-height: 42px;
182188
padding-block-start: 4px;
183189
padding-inline: 18px 0;
184190
}
@@ -215,7 +221,7 @@ td {
215221
}
216222
217223
tr {
218-
display: table-row;
224+
display: flex;
219225
&.expanded {
220226
white-space: normal;
221227

src/interfaces/ILogEntry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export type IRawLogEntry = INextcloud14LogEntry | INextcloud22LogEntry
102102
* Fixed version of the log entry where the exception has its own field of type IException
103103
*/
104104
export interface ILogEntry extends Omit<INextcloud22LogEntry, 'exception'> {
105+
/** Unique ID, appended to each iterator element (see LogController#poll) */
106+
id: string
105107
/** Full exception with trace (if applicable) */
106108
exception?: IException
107109
}

src/store/logging.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const mocks = vi.hoisted(() => {
2727
})
2828

2929
vi.mock('@nextcloud/dialogs', () => ({
30-
showError: mocks.showError
30+
showError: mocks.showError,
3131
}))
3232

3333
vi.mock('../utils/logfile.ts', () => {

src/store/logging.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export const useLogStore = defineStore('logreader-logs', () => {
103103

104104
/**
105105
* Load entries from string
106+
* @param text clipboard text content
106107
*/
107108
async function loadText(text: string) {
108109
// Skip if aborted

0 commit comments

Comments
 (0)