Skip to content

Commit 756168c

Browse files
authored
Merge branch 'main' into feat/npmx-connector-add-debug-and-new-otp-case
2 parents 9c6398a + 146c677 commit 756168c

File tree

3 files changed

+182
-12
lines changed

3 files changed

+182
-12
lines changed

app/components/CollapsibleSection.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ useHead({
125125

126126
<div
127127
:id="contentId"
128-
class="grid ms-6 transition-[grid-template-rows] duration-200 ease-in-out collapsible-content overflow-hidden"
128+
class="grid ms-6 grid-rows-[1fr] transition-[grid-template-rows] duration-200 ease-in-out collapsible-content overflow-hidden"
129129
:inert="!isOpen"
130130
>
131-
<div class="min-h-0 min-w-0 p-1">
131+
<div class="min-h-0 min-w-0">
132132
<slot />
133133
</div>
134134
</div>

app/components/Package/DownloadAnalytics.vue

Lines changed: 179 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ const props = defineProps<{
2525
createdIso?: string | null
2626
}>()
2727
28-
const shouldFetch = computed(() => true)
29-
3028
const { locale } = useI18n()
3129
const { accentColors, selectedAccentColor } = useAccentColor()
3230
const colorMode = useColorMode()
@@ -140,6 +138,29 @@ function isYearlyDataset(data: unknown): data is YearlyDownloadPoint[] {
140138
)
141139
}
142140
141+
/**
142+
* Formats a single evolution dataset into the structure expected by `VueUiXy`
143+
* for single-series charts.
144+
*
145+
* The dataset is interpreted based on the selected time granularity:
146+
* - **daily** → uses `timestamp`
147+
* - **weekly** → uses `timestampEnd`
148+
* - **monthly** → uses `timestamp`
149+
* - **yearly** → uses `timestamp`
150+
*
151+
* Only datasets matching the expected shape for the given granularity are
152+
* accepted. If the dataset does not match, an empty result is returned.
153+
*
154+
* The returned structure includes:
155+
* - a single line-series dataset with a consistent color
156+
* - a list of timestamps used as the x-axis values
157+
*
158+
* @param selectedGranularity - Active chart time granularity
159+
* @param dataset - Raw evolution dataset to format
160+
* @param seriesName - Display name for the resulting series
161+
* @returns An object containing a formatted dataset and its associated dates,
162+
* or `{ dataset: null, dates: [] }` when the input is incompatible
163+
*/
143164
function formatXyDataset(
144165
selectedGranularity: ChartTimeGranularity,
145166
dataset: EvolutionData,
@@ -200,6 +221,30 @@ function formatXyDataset(
200221
return { dataset: null, dates: [] }
201222
}
202223
224+
/**
225+
* Extracts normalized time-series points from an evolution dataset based on
226+
* the selected time granularity.
227+
*
228+
* Each returned point contains:
229+
* - `timestamp`: the numeric time value used for x-axis alignment
230+
* - `downloads`: the corresponding value at that time
231+
*
232+
* The timestamp field is selected according to granularity:
233+
* - **daily** → `timestamp`
234+
* - **weekly** → `timestampEnd`
235+
* - **monthly** → `timestamp`
236+
* - **yearly** → `timestamp`
237+
*
238+
* If the dataset does not match the expected shape for the given granularity,
239+
* an empty array is returned.
240+
*
241+
* This helper is primarily used in multi-package mode to align multiple
242+
* datasets on a shared time axis.
243+
*
244+
* @param selectedGranularity - Active chart time granularity
245+
* @param dataset - Raw evolution dataset to extract points from
246+
* @returns An array of normalized `{ timestamp, downloads }` points
247+
*/
203248
function extractSeriesPoints(
204249
selectedGranularity: ChartTimeGranularity,
205250
dataset: EvolutionData,
@@ -263,6 +308,22 @@ const startDate = shallowRef<string>('') // YYYY-MM-DD
263308
const endDate = shallowRef<string>('') // YYYY-MM-DD
264309
const hasUserEditedDates = shallowRef(false)
265310
311+
/**
312+
* Initializes the date range from the provided weeklyDownloads dataset.
313+
*
314+
* The range is inferred directly from the dataset boundaries:
315+
* - `startDate` is set from the `weekStart` of the first entry
316+
* - `endDate` is set from the `weekEnd` of the last entry
317+
*
318+
* Dates are normalized to `YYYY-MM-DD` and validated before assignment.
319+
*
320+
* This function is a no-op when:
321+
* - the user has already edited the date range
322+
* - no weekly download data is available
323+
*
324+
* The inferred range takes precedence over client-side fallbacks but does not
325+
* override user-defined dates.
326+
*/
266327
function initDateRangeFromWeekly() {
267328
if (hasUserEditedDates.value) return
268329
if (!props.weeklyDownloads?.length) return
@@ -275,6 +336,20 @@ function initDateRangeFromWeekly() {
275336
if (isValidIsoDateOnly(end)) endDate.value = end
276337
}
277338
339+
/**
340+
* Initializes a default date range on the client when no explicit dates
341+
* have been provided and the user has not manually edited the range, typically
342+
* when weeklyDownloads is not provided.
343+
*
344+
* The range is computed in UTC to avoid timezone-related off-by-one errors:
345+
* - `endDate` is set to yesterday (UTC)
346+
* - `startDate` is set to 29 days before yesterday (UTC), yielding a 30-day range
347+
*
348+
* This function is a no-op when:
349+
* - the user has already edited the date range
350+
* - the code is running on the server
351+
* - both `startDate` and `endDate` are already defined
352+
*/
278353
function initDateRangeFallbackClient() {
279354
if (hasUserEditedDates.value) return
280355
if (!import.meta.client) return
@@ -297,11 +372,31 @@ function initDateRangeFallbackClient() {
297372
function toUtcDateOnly(date: Date): string {
298373
return date.toISOString().slice(0, 10)
299374
}
375+
300376
function addUtcDays(date: Date, days: number): Date {
301377
const next = new Date(date)
302378
next.setUTCDate(next.getUTCDate() + days)
303379
return next
304380
}
381+
382+
/**
383+
* Initializes a default date range for multi-package mode using a fixed
384+
* 52-week rolling window.
385+
*
386+
* The range is computed in UTC to ensure consistent boundaries across
387+
* timezones:
388+
* - `endDate` is set to yesterday (UTC)
389+
* - `startDate` is set to the first day of the 52-week window ending yesterday
390+
*
391+
* This function is intended for multi-package comparisons where no explicit
392+
* date range or dataset-derived range is available.
393+
*
394+
* This function is a no-op when:
395+
* - the user has already edited the date range
396+
* - the code is running on the server
397+
* - the component is not in multi-package mode
398+
* - both `startDate` and `endDate` are already defined
399+
*/
305400
function initDateRangeForMultiPackageWeekly52() {
306401
if (hasUserEditedDates.value) return
307402
if (!import.meta.client) return
@@ -364,6 +459,24 @@ const options = shallowRef<
364459
| { granularity: 'year'; startDate?: string; endDate?: string }
365460
>({ granularity: 'week', weeks: 52 })
366461
462+
/**
463+
* Applies the current date range (`startDate` / `endDate`) to a base options
464+
* object, returning a new object augmented with validated date fields.
465+
*
466+
* Dates are normalized to `YYYY-MM-DD`, validated, and ordered to ensure
467+
* logical consistency:
468+
* - When both dates are valid, the earliest is assigned to `startDate` and
469+
* the latest to `endDate`
470+
* - When only one valid date is present, only that boundary is applied
471+
* - Invalid or empty dates are omitted from the result
472+
*
473+
* The input object is not mutated.
474+
*
475+
* @typeParam T - Base options type to extend with date range fields
476+
* @param base - Base options object to which the date range should be applied
477+
* @returns A new options object including the applicable `startDate` and/or
478+
* `endDate` fields
479+
*/
367480
function applyDateRange<T extends Record<string, unknown>>(base: T): T & DateRangeFields {
368481
const next: T & DateRangeFields = { ...base }
369482
@@ -396,6 +509,16 @@ const pending = shallowRef(false)
396509
const isMounted = shallowRef(false)
397510
let requestToken = 0
398511
512+
// Watches granularity and date inputs to keep request options in sync and
513+
// manage the loading state.
514+
//
515+
// This watcher does NOT perform the fetch itself. Its responsibilities are:
516+
// - derive the correct API options from the selected granularity
517+
// - apply the current validated date range to those options
518+
// - determine whether a loading indicator should be shown
519+
//
520+
// Fetching is debounced separately to avoid excessive
521+
// network requests while the user is interacting with controls.
399522
watch(
400523
[selectedGranularity, startDate, endDate],
401524
([granularityValue]) => {
@@ -410,7 +533,7 @@ watch(
410533
if (!isMounted.value) return
411534
412535
const packageNames = effectivePackageNames.value
413-
if (!import.meta.client || !shouldFetch.value || !packageNames.length) {
536+
if (!import.meta.client || !packageNames.length) {
414537
pending.value = false
415538
return
416539
}
@@ -434,9 +557,27 @@ watch(
434557
{ immediate: true },
435558
)
436559
560+
/**
561+
* Fetches download evolution data based on the current granularity,
562+
* date range, and package selection.
563+
*
564+
* This function:
565+
* - runs only on the client
566+
* - supports both single-package and multi-package modes
567+
* - applies request de-duplication via a request token to avoid race conditions
568+
* - updates the appropriate reactive stores with fetched data
569+
* - manages the `pending` loading state
570+
*
571+
* Behavior details:
572+
* - In multi-package mode, all packages are fetched in parallel and partial
573+
* failures are tolerated using `Promise.allSettled`
574+
* - In single-package mode, weekly data is reused from `weeklyDownloads`
575+
* when available and no explicit date range is requested
576+
* - Outdated responses are discarded when a newer request supersedes them
577+
*
578+
*/
437579
async function loadNow() {
438580
if (!import.meta.client) return
439-
if (!shouldFetch.value) return
440581
441582
const packageNames = effectivePackageNames.value
442583
if (!packageNames.length) return
@@ -498,6 +639,13 @@ async function loadNow() {
498639
}
499640
}
500641
642+
// Debounced wrapper around `loadNow` to avoid triggering a network request
643+
// on every intermediate state change while the user is interacting with inputs
644+
//
645+
// This 'arbitrary' 1000 ms delay:
646+
// - gives enough time for the user to finish changing granularity or dates
647+
// - prevents unnecessary API load and visual flicker of the loading state
648+
//
501649
const debouncedLoadNow = useDebounceFn(() => {
502650
loadNow()
503651
}, 1000)
@@ -506,7 +654,6 @@ const fetchTriggerKey = computed(() => {
506654
const names = effectivePackageNames.value.join(',')
507655
const o = options.value as any
508656
return [
509-
shouldFetch.value ? '1' : '0',
510657
isMultiPackageMode.value ? 'M' : 'S',
511658
names,
512659
String(props.createdIso ?? ''),
@@ -536,6 +683,28 @@ const effectiveDataSingle = computed<EvolutionData>(() => {
536683
return evolution.value
537684
})
538685
686+
/**
687+
* Normalized chart data derived from the fetched evolution datasets.
688+
*
689+
* This computed value adapts its behavior based on the current mode:
690+
*
691+
* - **Single-package mode**
692+
* - Delegates formatting to `formatXyDataset`
693+
* - Produces a single series with its corresponding timestamps
694+
*
695+
* - **Multi-package mode**
696+
* - Merges multiple package datasets into a shared time axis
697+
* - Aligns all series on the same sorted list of timestamps
698+
* - Fills missing datapoints with `0` to keep series lengths consistent
699+
* - Assigns framework-specific colors when applicable
700+
*
701+
* The returned structure matches the expectations of `VueUiXy`:
702+
* - `dataset`: array of series definitions, or `null` when no data is available
703+
* - `dates`: sorted list of timestamps used as the x-axis reference
704+
*
705+
* Returning `dataset: null` explicitly signals the absence of data and allows
706+
* the template to handle empty states without ambiguity.
707+
*/
539708
const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: number[] }>(() => {
540709
if (!isMultiPackageMode.value) {
541710
const pkg = effectivePackageNames.value[0] ?? props.packageName ?? ''
@@ -558,7 +727,7 @@ const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: number
558727
const dates = Array.from(timestampSet).sort((a, b) => a - b)
559728
if (!dates.length) return { dataset: null, dates: [] }
560729
561-
const dataset: VueUiXyDatasetItem[] = names.map((pkg, index) => {
730+
const dataset: VueUiXyDatasetItem[] = names.map(pkg => {
562731
const points = pointsByPackage.get(pkg) ?? []
563732
const map = new Map<number, number>()
564733
for (const p of points) map.set(p.timestamp, p.downloads)
@@ -616,7 +785,8 @@ function buildExportFilename(extension: string): string {
616785
return `${sanitise(label ?? '')}-${g}_${range}.${extension}`
617786
}
618787
619-
const config = computed(() => {
788+
// VueUiXy chart component configuration
789+
const chartConfig = computed(() => {
620790
return {
621791
theme: isDarkMode.value ? 'dark' : 'default',
622792
chart: {
@@ -860,7 +1030,7 @@ const config = computed(() => {
8601030
<div role="region" aria-labelledby="download-analytics-title">
8611031
<ClientOnly v-if="chartData.dataset">
8621032
<div>
863-
<VueUiXy :dataset="chartData.dataset" :config="config" class="[direction:ltr]">
1033+
<VueUiXy :dataset="chartData.dataset" :config="chartConfig" class="[direction:ltr]">
8641034
<!-- Custom legend for multiple series -->
8651035
<template v-if="isMultiPackageMode" #legend="{ legend }">
8661036
<div class="flex gap-4 flex-wrap justify-center">
@@ -970,7 +1140,7 @@ const config = computed(() => {
9701140
</div>
9711141

9721142
<div
973-
v-if="shouldFetch && !chartData.dataset && !pending"
1143+
v-if="!chartData.dataset && !pending"
9741144
class="min-h-[260px] flex items-center justify-center text-fg-subtle font-mono text-sm"
9751145
>
9761146
{{ $t('package.downloads.no_data') }}

app/pages/package/[...package].vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1343,7 +1343,7 @@ onKeyStroke(
13431343
13441344
.sidebar-scroll:hover::-webkit-scrollbar-thumb,
13451345
.sidebar-scroll:focus-within::-webkit-scrollbar-thumb {
1346-
background-color: #cecece;
1346+
background-color: var(--border);
13471347
border-radius: 9999px;
13481348
}
13491349

0 commit comments

Comments
 (0)