Skip to content

Commit 93d66a0

Browse files
grolupetersutter
andauthored
Improved usage of VDataTableVirtual (#2291)
* Improved usage of VDataVirtual - Pass ref to ensure correct row height measurement - Introduced GScrollContainer Component to indicate more content is available - Optimized worker, ticket labels and access restrictions layout on cluster list - Use VDataVirtual for Secret & Member Page - Introduced useTwoTableLayout composable for optimized two table page layouting * Small changes * fixed typo in css class name * PR Feedback * write arrow function as expression function, group computed and functions --------- Co-authored-by: petersutter <peter.sutter@sap.com>
1 parent 4072966 commit 93d66a0

16 files changed

Lines changed: 356 additions & 549 deletions

frontend/__tests__/components/Vuetify.spec.js

Lines changed: 21 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -127,84 +127,38 @@ describe('components', () => {
127127
})
128128

129129
describe('v-data-table-virtual', () => {
130-
// These tests basically test that nothing changes in the way vuetify renders the virtual table
131-
// as the way it behaves in case no item-height is defined is kind of undeterministic
132-
const TestTableRow = {
133-
name: 'TestTableRow',
134-
props: {
135-
item: {
136-
type: Object,
137-
required: true,
138-
},
139-
},
140-
template: '<tr><td>{{ item.name }}</td></tr>',
141-
}
142-
143-
function mountDataTableVirtual ({ itemHeight, itemsCount = 50 } = {}) {
144-
const items = Array.from({ length: itemsCount }).map((_, i) => ({
145-
id: i,
146-
name: `Item ${i}`,
147-
}))
148-
const columns = [
149-
{ key: 'name', title: 'Name' },
150-
]
151-
130+
it('should be able to find v-table__wrapper element', () => {
152131
const Component = {
153-
name: 'TestVirtualTable',
154-
components: {
155-
TestTableRow,
156-
},
157-
template: `
158-
<v-app>
159-
<v-main>
160-
<v-data-table-virtual
161-
:items="items"
162-
:columns="columns"
163-
:item-height="itemHeight"
164-
:height="400"
165-
>
166-
<template #item="{ item }">
167-
<TestTableRow :item="item" />
168-
</template>
169-
</v-data-table-virtual>
170-
</v-main>
171-
</v-app>
172-
`,
173-
data () {
174-
return {
175-
items,
176-
columns,
177-
itemHeight,
178-
}
179-
},
132+
template: '<v-data-table-virtual />',
180133
}
181-
182-
return mount(Component, {
134+
const wrapper = mount(Component, {
183135
global: {
184136
plugins: [
185137
createVuetifyPlugin(),
186138
],
187139
},
188140
})
189-
}
190-
191-
it('should render default number of rows if item-height is not defined', async () => {
192-
const wrapper = mountDataTableVirtual({ itemsCount: 50 })
193-
await nextTick()
194-
195-
const rows = wrapper.findAllComponents(TestTableRow)
196-
expect(rows).toHaveLength(25) // by default, the table expects 16px tall rows
141+
const footer = wrapper.find('.v-table__wrapper')
142+
expect(footer.exists()).toBe(true)
197143
})
144+
})
198145

199-
it('should only render the visible rows if item-height is defined', async () => {
200-
const wrapper = mountDataTableVirtual({
201-
itemsCount: 50,
202-
itemHeight: 40,
146+
describe('v-data-table', () => {
147+
it('should be able to find v-data-table-footer classes', () => {
148+
const Component = {
149+
template: '<v-data-table />',
150+
}
151+
const wrapper = mount(Component, {
152+
global: {
153+
plugins: [
154+
createVuetifyPlugin(),
155+
],
156+
},
203157
})
204-
await nextTick()
205-
206-
const rows = wrapper.findAllComponents(TestTableRow)
207-
expect(rows).toHaveLength(10) // 400px / 40px = 10 rows
158+
const footer = wrapper.find('.v-data-table-footer')
159+
expect(footer.exists()).toBe(true)
160+
const footerInfo = wrapper.find('.v-data-table-footer__info')
161+
expect(footerInfo.exists()).toBe(true)
208162
})
209163
})
210164

@@ -240,7 +194,6 @@ describe('components', () => {
240194
})
241195
const iconItem = wrapper.find('.v-icon')
242196
expect(iconItem.exists()).toBe(true)
243-
expect(iconItem.classes()).toContain('v-icon')
244197
})
245198
})
246199
})

frontend/src/components/GDataTableFooter.vue

Lines changed: 6 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -7,122 +7,27 @@ SPDX-License-Identifier: Apache-2.0
77
<template>
88
<v-divider />
99
<div class="v-data-table-footer">
10-
<template v-if="pageCount">
11-
<div class="v-data-table-footer__items-per-page">
12-
<span>Rows per page:</span>
13-
<v-select
14-
:items="itemsPerPageOptions"
15-
:model-value="itemsPerPage"
16-
density="compact"
17-
variant="solo"
18-
flat
19-
single-line
20-
hide-details
21-
width="105px"
22-
@update:model-value="value => setItemsPerPage(Number(value))"
23-
/>
24-
</div>
25-
<div class="v-data-table-footer__info">
26-
{{ !itemsLength ? 0 : startIndex + 1 }}-{{ stopIndex }} of {{ itemsLength }}
27-
</div>
28-
<div class="v-data-table-footer__pagination">
29-
<v-btn
30-
icon="mdi-page-first"
31-
variant="plain"
32-
:disabled="page === 1"
33-
aria-label="First page"
34-
@click="setPage(1)"
35-
/>
36-
<v-btn
37-
icon="mdi-chevron-left"
38-
variant="plain"
39-
:disabled="page === 1"
40-
aria-label="Previous page"
41-
@click="setPage(Math.max(1, page - 1))"
42-
/>
43-
<v-btn
44-
icon="mdi-chevron-right"
45-
variant="plain"
46-
:disabled="page === pageCount"
47-
aria-label="Next page"
48-
@click="setPage(Math.min(pageCount, page + 1))"
49-
/>
50-
<v-btn
51-
icon="mdi-page-last"
52-
variant="plain"
53-
:disabled="page === pageCount"
54-
aria-label="Last page"
55-
@click="setPage(pageCount)"
56-
/>
57-
</div>
58-
</template>
5910
<div
60-
v-else
6111
class="v-data-table-footer__info"
6212
>
63-
{{ itemsLength }} Rows
13+
{{ itemsLength }} {{ itemsLabel }}
6414
</div>
6515
</div>
6616
</template>
6717

6818
<script setup>
69-
import {
70-
computed,
71-
toRefs,
72-
} from 'vue'
19+
import { toRefs } from 'vue'
7320
7421
const props = defineProps({
7522
itemsLength: {
7623
type: Number,
7724
required: true,
7825
},
79-
itemsPerPage: {
80-
type: Number,
81-
required: false,
82-
},
83-
itemsPerPageOptions: {
84-
type: Array,
85-
default: () => ([
86-
{ value: 10, title: '10' },
87-
{ value: 25, title: '25' },
88-
{ value: 50, title: '50' },
89-
{ value: 100, title: '100' },
90-
]),
91-
},
92-
page: {
93-
type: Number,
94-
required: false,
95-
},
96-
pageCount: {
97-
type: Number,
98-
required: false,
26+
itemsLabel: {
27+
type: String,
28+
default: 'Rows',
9929
},
10030
})
10131
102-
const { page, pageCount, itemsLength, itemsPerPage, itemsPerPageOptions } = toRefs(props)
103-
104-
const startIndex = computed(() => {
105-
if (itemsPerPage.value === -1) return 0
106-
107-
return itemsPerPage.value * (page.value - 1)
108-
})
109-
const stopIndex = computed(() => {
110-
if (itemsPerPage.value === -1) return itemsLength.value
111-
112-
return Math.min(itemsLength.value, startIndex.value + itemsPerPage.value)
113-
})
114-
115-
const emit = defineEmits([
116-
'update:page',
117-
'update:itemsPerPage',
118-
])
119-
120-
function setItemsPerPage (value) {
121-
emit('update:itemsPerPage', value)
122-
}
123-
124-
function setPage (value) {
125-
emit('update:page', value)
126-
}
127-
32+
const { itemsLength, itemsLabel } = toRefs(props)
12833
</script>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
-->
6+
7+
<template>
8+
<div
9+
ref="scrollRef"
10+
class="scrollable-container"
11+
:style="{ '--fadeOpacity': fadeOpacity, '--fadeHeight': `${fadeHeight}px` }"
12+
>
13+
<slot />
14+
</div>
15+
</template>
16+
17+
<script setup>
18+
import {
19+
ref,
20+
computed,
21+
} from 'vue'
22+
import {
23+
useScroll,
24+
useElementSize,
25+
} from '@vueuse/core'
26+
27+
const scrollRef = ref(null)
28+
const { y } = useScroll(scrollRef)
29+
const { height } = useElementSize(scrollRef)
30+
31+
const fadeHeight = Math.max(40, height.value / 2)
32+
33+
const fadeOpacity = computed(() => {
34+
const el = scrollRef.value
35+
if (!el) {
36+
return false
37+
}
38+
39+
const remainingY = el.scrollHeight - (y.value + height.value)
40+
if (remainingY >= fadeHeight) {
41+
return 1
42+
}
43+
44+
return remainingY / fadeHeight
45+
})
46+
47+
</script>
48+
49+
<style scoped>
50+
.scrollable-container {
51+
overflow-y: auto;
52+
}
53+
54+
.scrollable-container::after {
55+
content: "";
56+
position: absolute;
57+
left: 0;
58+
bottom: 0;
59+
width: 100%;
60+
height: var(--fadeHeight);
61+
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(var(--v-theme-surface)));
62+
pointer-events: none;
63+
opacity: var(--fadeOpacity);
64+
}
65+
66+
</style>

0 commit comments

Comments
 (0)