Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions src/components/tables/DeploymentTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import { formatDate } from '~/services/date'
import { getTimeWindowPageRange } from '~/services/dateRange'
import { getLogDocUrl } from '~/services/logDocLinks'
import { defaultApiHost, useSupabase } from '~/services/supabase'

Expand Down Expand Up @@ -46,15 +47,8 @@ const paginatedRange = computed(() => {
const rangeStart = range.value ? range.value[0].getTime() : undefined
const rangeEnd = range.value ? range.value[1].getTime() : undefined

if (rangeStart && rangeEnd) {
const timeDifference = rangeEnd - rangeStart
const pageTimeOffset = timeDifference * (currentPage.value - 1)

return {
rangeStart: rangeStart + pageTimeOffset,
rangeEnd: rangeEnd + pageTimeOffset,
}
}
if (rangeStart !== undefined && rangeEnd !== undefined)
return getTimeWindowPageRange(rangeStart, rangeEnd, currentPage.value)

return {
rangeStart,
Expand Down Expand Up @@ -169,10 +163,9 @@ columns.value = [
},
]

async function reload() {
// TableLog emits `reload` only from "Load older" after decrementing currentPage.
async function loadOlder() {
try {
currentPage.value = 1
elements.value.length = 0
await getData()
}
catch (error) {
Expand Down Expand Up @@ -230,7 +223,7 @@ watch(range, async () => {
:auto-reload="false"
:app-id="props.appId ?? ''"
:search-placeholder="t('search-by-version')"
@reload="reload()" @reset="refreshData()"
@reload="loadOlder()" @reset="refreshData()"
/>
</div>
</template>
84 changes: 61 additions & 23 deletions src/components/tables/DeviceTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { toast } from 'vue-sonner'
import IconSmartphone from '~icons/lucide/smartphone'
import DateRangePicker from '~/components/DateRangePicker.vue'
import { formatDate } from '~/services/date'
import { getDateRangeForPreset, TABLE_DATE_RANGE_DEFAULT } from '~/services/dateRange'
import {
getDateRangeForPreset,
getTableDateRangeSignature,
shouldRecountOnTableReload,
TABLE_DATE_RANGE_DEFAULT,
} from '~/services/dateRange'
import { defaultApiHost, useSupabase } from '~/services/supabase'

const props = defineProps<{
Expand Down Expand Up @@ -38,6 +43,7 @@ const search = ref('')
const elements = ref<Device[]>([])
const isLoading = ref(true)
const currentPage = ref(1)
const previousPage = ref(1)
const nextCursor = ref<string | undefined>(undefined)
const hasMore = ref(false)
const pageStartCursor = ref<Map<number, string | null | undefined>>(new Map([[1, undefined]]))
Expand Down Expand Up @@ -82,6 +88,13 @@ function openDateRangePicker(event: MouseEvent) {
dateRangePickerRef.value?.togglePicker(event.currentTarget as HTMLElement)
}

function onDateRangeApply(payload: { start: Date, end: Date, mode: DateRangePreset }) {
// Apply payload first so refresh does not race v-model flush and keep the old window.
dateRangeMode.value = payload.mode
dateRange.value = [payload.start, payload.end]
void refreshData()
}

function clearDeviceViewFilters(clearFilters: () => void) {
cancelScheduledReload()
clearFilters()
Expand Down Expand Up @@ -143,13 +156,8 @@ function getSearchTerm() {
}

function getDateRangePayload() {
if (dateRangeMode.value !== 'custom') {
const rolling = getDateRangeForPreset(dateRangeMode.value)
return {
updated_at_gt: rolling.start.toISOString(),
updated_at_lte: rolling.end.toISOString(),
}
}
// Always use the frozen session bounds. Recomputing rolling presets from
// `now` on each page fetch desyncs API filters from cached cursors.
if (!dateRange.value)
return {}
return {
Expand All @@ -158,6 +166,13 @@ function getDateRangePayload() {
}
}

function snapRollingDateRangeBounds() {
if (dateRangeMode.value === 'custom')
return
const rolling = getDateRangeForPreset(dateRangeMode.value)
dateRange.value = [rolling.start, rolling.end]
}

function getVersionNameFilter() {
const selected = selectedVersionName.value.trim()
return selected || undefined
Expand All @@ -177,7 +192,8 @@ function getQuerySignature() {
override: filters.value.Override,
customIdMode: filters.value.CustomId,
ids: props.ids ? [...props.ids].sort().join(',') : '',
dateRange: getDateRangePayload(),
// Stable mode identity — not rolling ISO bounds that move every millisecond.
dateRange: getTableDateRangeSignature(dateRangeMode.value, dateRange.value),
Comment thread
cursor[bot] marked this conversation as resolved.
})
}

Expand Down Expand Up @@ -288,24 +304,47 @@ function clearPaginationState() {
hasMore.value = false
}

function resetTablePagination(options: { snapRolling?: boolean } = {}) {
if (options.snapRolling)
snapRollingDateRangeBounds()
currentPage.value = 1
previousPage.value = 1
clearPaginationState()
elements.value.length = 0
lastQuerySignature.value = getQuerySignature()
}

async function reload() {
const loadId = ++activeLoadId.value
isLoading.value = true
try {
const requestedPage = currentPage.value
const querySignature = getQuerySignature()
if (lastQuerySignature.value !== querySignature) {
lastQuerySignature.value = querySignature
currentPage.value = 1
clearPaginationState()
elements.value.length = 0
const filtersChanged = lastQuerySignature.value !== querySignature
const shouldRecount = shouldRecountOnTableReload({
filtersChanged,
previousPage: previousPage.value,
requestedPage,
})
if (filtersChanged) {
// Keep frozen date bounds; only drop cursors / page for the new filters.
resetTablePagination()
}

const newTotal = await countDevices()
if (loadId !== activeLoadId.value)
return
if (shouldRecount) {
// Toolbar reload (not a page change): snap rolling bounds and drop
// cursors so the new window cannot reuse stale page offsets.
if (!filtersChanged)
resetTablePagination({ snapRolling: true })
const newTotal = await countDevices()
if (loadId !== activeLoadId.value)
return
total.value = newTotal
}

total.value = newTotal
await getData(loadId)
if (loadId === activeLoadId.value)
previousPage.value = currentPage.value
}
catch (error) {
console.error(error)
Expand All @@ -321,16 +360,15 @@ async function refreshData() {
const loadId = ++activeLoadId.value
isLoading.value = true
try {
currentPage.value = 1
lastQuerySignature.value = getQuerySignature()
clearPaginationState()
elements.value.length = 0
resetTablePagination({ snapRolling: true })
const newTotal = await countDevices()
if (loadId !== activeLoadId.value)
return

total.value = newTotal
await getData(loadId)
if (loadId === activeLoadId.value)
previousPage.value = currentPage.value
}
catch (error) {
console.error(error)
Expand Down Expand Up @@ -587,7 +625,7 @@ watch([selectedPlatform, selectedVersionName], () => {
v-model="dateRange"
v-model:mode="dateRangeMode"
compact
@apply="refreshData()"
@apply="onDateRangeApply"
/>
</template>
<template #empty-state="{ clearFilters, hasActiveFilters }">
Expand Down
27 changes: 11 additions & 16 deletions src/components/tables/LogTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import type { Ref } from 'vue'
import type { TableColumn } from '../comp_def'
import { useDebounceFn } from '@vueuse/core'
import dayjs from 'dayjs'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import { formatDate } from '~/services/date'
import { getDateRangeForPreset, getTimeWindowPageRange, TABLE_DATE_RANGE_DEFAULT } from '~/services/dateRange'
import { getLogDocUrl } from '~/services/logDocLinks'
import { actionToFilter, createActionFilterState, failureActionFilterKeys, filterToAction, observeActionFilterKeys, updateActionFilterKeys } from '~/services/statsActions'
import { defaultApiHost, useSupabase } from '~/services/supabase'
Expand Down Expand Up @@ -67,7 +67,7 @@ const isLoading = ref(false)
const isExporting = ref(false)
const currentPage = ref(1)

// Initialize date range from query parameters if provided, otherwise default to last hour
// Initialize date range from query parameters if provided, otherwise table default.
function initializeDateRange(): [Date, Date] {
const startParam = route.query.start
const endParam = route.query.end
Expand All @@ -87,7 +87,8 @@ function initializeDateRange(): [Date, Date] {
}
}

return [dayjs().subtract(1, 'hour').toDate(), new Date()]
const initial = getDateRangeForPreset(TABLE_DATE_RANGE_DEFAULT)
return [initial.start, initial.end]
}

const range = ref<[Date, Date]>(initializeDateRange())
Expand Down Expand Up @@ -181,15 +182,8 @@ const paginatedRange = computed(() => {
const rangeStart = range.value ? range.value[0].getTime() : undefined
const rangeEnd = range.value ? range.value[1].getTime() : undefined

if (rangeStart && rangeEnd) {
const timeDifference = rangeEnd - rangeStart
const pageTimeOffset = timeDifference * (currentPage.value - 1)

return {
rangeStart: rangeStart + pageTimeOffset,
rangeEnd: rangeEnd + pageTimeOffset,
}
}
if (rangeStart !== undefined && rangeEnd !== undefined)
return getTimeWindowPageRange(rangeStart, rangeEnd, currentPage.value)

return {
rangeStart,
Expand Down Expand Up @@ -389,10 +383,11 @@ columns.value = [
},
]

async function reload() {
// TableLog emits `reload` only from "Load older" after decrementing currentPage.
// Do not reset the page here — that pinned logs on the first window forever.
async function loadOlder() {
try {
currentPage.value = 1
await getData({ append: false })
await getData({ append: true })
}
catch (error) {
console.error(error)
Expand Down Expand Up @@ -475,7 +470,7 @@ watch(range, async () => {
:auto-reload="false"
:app-id="props.appId ?? ''"
:search-placeholder="deviceId ? t('search-by-device-id-0') : t('search-by-device-id-')"
@reload="reload()" @reset="refreshData()" @export="exportCsv()"
@reload="loadOlder()" @reset="refreshData()" @export="exportCsv()"
/>
</div>
</template>
51 changes: 51 additions & 0 deletions src/services/dateRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,54 @@ export function serializeDateRangeQuery(
return { range: DEFAULT_DATE_RANGE_PRESET }
return { range: mode }
}

/**
* Stable filter identity for table reloads.
* Rolling presets must NOT embed `now`-based timestamps — those change every
* call and would reset cursor pagination back to page 1 on each next click.
*/
export function getTableDateRangeSignature(
mode: DateRangePreset,
customRange?: [Date, Date] | null,
): { mode: DateRangePreset, start?: string, end?: string } {
if (mode !== 'custom')
return { mode }
if (!customRange?.[0] || !customRange?.[1])
return { mode: 'custom' }
return {
mode: 'custom',
start: customRange[0].toISOString(),
end: customRange[1].toISOString(),
}
}

/**
* Whether a devices/logs table reload should re-run the expensive count query.
* Page-only navigation keeps the cached total so 100k+ device apps stay usable.
*/
export function shouldRecountOnTableReload(options: {
filtersChanged: boolean
previousPage: number
requestedPage: number
}): boolean {
if (options.filtersChanged)
return true
return options.previousPage === options.requestedPage
}

/**
* Time-window pagination used by logs/deployments "Load older".
* Page 1 is the selected range; page 0 / -1 / … shift one full window backward.
*/
export function getTimeWindowPageRange(
rangeStartMs: number,
rangeEndMs: number,
page: number,
): { rangeStart: number, rangeEnd: number } {
const timeDifference = rangeEndMs - rangeStartMs
const pageTimeOffset = timeDifference * (page - 1)
return {
rangeStart: rangeStartMs + pageTimeOffset,
rangeEnd: rangeEndMs + pageTimeOffset,
}
}
Loading
Loading