mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
feat: implement dependent projects + members breakdown/filter (#6395)
* feat: implement dependent project breakdown * implement dependent project type filter * feat: show dependent on column if there are multiple projects selected * feat: different tooltip for when both project version and dependent breakdowns are on * feat: implement dependents query filter * feat: add project icon to query filter * fix: dont show dependent on column when project breakdown is selected * feat: add org icon * remove: dependent on project column and project version project column * refactor: pnpm prepr * fix: all project selection gets unselected when reload page * pnpm prepr * fix: still using old search formatter * feat: implement projects in table click to link to project page * feat: handle analytics values that dont have a dependent project as a "No dependent" row * pnpm prepr * feat: remove org icon for dependents projects and change unknown label * feat: do not include unknown rows for showing top 8 * feat: separate out no dependents based on download reason * fix: use compatible dependencies for query filter options * feat: implement members breakdown/filter frontend (#6470) * feat: add link to user in table * fix: do not allow breakdowns to occur without shared stats * pnpm prepr * fix: i18n and passing params for formatter
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<button
|
||||
v-tooltip="legendEntry.projectName ?? ''"
|
||||
v-tooltip="legendEntry.tooltip ?? legendEntry.projectName ?? ''"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1.5 px-2 py-0.5 text-sm !outline-0 transition-all focus-within:!outline-0 focus:!outline-0 focus-visible:!outline-0"
|
||||
:class="[
|
||||
|
||||
+4
-1
@@ -64,6 +64,7 @@ export function useAnalyticsChartLegend({
|
||||
id: dataset.projectId,
|
||||
name: dataset.label,
|
||||
projectName: dataset.projectName,
|
||||
tooltip: dataset.tooltip,
|
||||
color: dataset.borderColor,
|
||||
totalValue: getChartDatasetTotal(dataset),
|
||||
hidden: hiddenDatasetIds.value.has(dataset.projectId),
|
||||
@@ -118,6 +119,7 @@ export function useAnalyticsChartLegend({
|
||||
id: getPreviousPeriodDatasetId(entry.id),
|
||||
name: formatMessage(analyticsChartMessages.previousPeriodSuffix, { name: entry.name }),
|
||||
projectName: entry.projectName,
|
||||
tooltip: entry.tooltip,
|
||||
color: entry.color,
|
||||
totalValue: previousDataset ? getChartDatasetTotal(previousDataset) : 0,
|
||||
hidden: hiddenDatasetIds.value.has(getPreviousPeriodDatasetId(entry.id)),
|
||||
@@ -153,6 +155,7 @@ export function useAnalyticsChartLegend({
|
||||
name: dataset.label,
|
||||
}),
|
||||
projectName: dataset.projectName,
|
||||
tooltip: dataset.tooltip,
|
||||
data: previousData,
|
||||
borderColor: dataset.borderColor,
|
||||
backgroundColor: dataset.backgroundColor,
|
||||
@@ -296,7 +299,7 @@ export function useAnalyticsChartLegend({
|
||||
}
|
||||
|
||||
function getLegendEntryTooltip(legendEntry: AnalyticsChartLegendEntry) {
|
||||
return legendEntry.projectName ?? ''
|
||||
return legendEntry.tooltip ?? legendEntry.projectName ?? ''
|
||||
}
|
||||
|
||||
function isUnmonetizedLegendEntry(legendEntry: AnalyticsChartLegendEntry) {
|
||||
|
||||
+2
-1
@@ -97,7 +97,7 @@
|
||||
"
|
||||
/>
|
||||
<span
|
||||
v-tooltip="entry.projectName ?? ''"
|
||||
v-tooltip="entry.tooltip ?? entry.projectName ?? ''"
|
||||
class="min-w-0 truncate"
|
||||
:class="{
|
||||
'line-through': entry.hidden,
|
||||
@@ -145,6 +145,7 @@ export type AnalyticsChartTooltipEntry = {
|
||||
projectId: string
|
||||
name: string
|
||||
projectName?: string
|
||||
tooltip?: string
|
||||
color: string
|
||||
formattedValue: string
|
||||
hidden: boolean
|
||||
|
||||
+1
@@ -208,6 +208,7 @@ const hoverEntries = computed(() => {
|
||||
projectId: legendEntry.id,
|
||||
name: legendEntry.name,
|
||||
projectName: legendEntry.projectName,
|
||||
tooltip: legendEntry.tooltip,
|
||||
color: legendEntry.color,
|
||||
formattedValue: props.isRatioMode
|
||||
? `${ratioValue.toFixed(1)}%`
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ export type AnalyticsChartLegendEntry = {
|
||||
id: string
|
||||
name: string
|
||||
projectName?: string
|
||||
tooltip?: string
|
||||
color: string
|
||||
totalValue: number
|
||||
hidden: boolean
|
||||
|
||||
+192
-26
@@ -1,20 +1,24 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
import type {
|
||||
AnalyticsBreakdownPreset,
|
||||
AnalyticsDashboardProject,
|
||||
AnalyticsDashboardStat,
|
||||
AnalyticsGroupByPreset,
|
||||
import {
|
||||
type AnalyticsBreakdownPreset,
|
||||
type AnalyticsDashboardProject,
|
||||
type AnalyticsDashboardStat,
|
||||
type AnalyticsGroupByPreset,
|
||||
type AnalyticsSelectedFilters,
|
||||
doesAnalyticsPointMatchNormalizedFilters,
|
||||
normalizeAnalyticsSelectedFilters,
|
||||
} from '~/providers/analytics/analytics'
|
||||
|
||||
import type { FormatMessage } from '../analytics-messages'
|
||||
import {
|
||||
analyticsChartMessages,
|
||||
analyticsMessages,
|
||||
analyticsStatCardMessages,
|
||||
formatAnalyticsDependentProjectFallbackLabel,
|
||||
formatAnalyticsDownloadReasonLabel,
|
||||
formatAnalyticsLoaderLabel,
|
||||
formatAnalyticsMonetizationLabel,
|
||||
type FormatMessage,
|
||||
} from '../analytics-messages'
|
||||
import {
|
||||
ALL_BREAKDOWN_VALUE,
|
||||
@@ -22,6 +26,8 @@ import {
|
||||
getAnalyticsBreakdownDatasetId,
|
||||
getAnalyticsBreakdownKey,
|
||||
getAnalyticsBreakdownValues,
|
||||
isNoDependentAnalyticsBreakdownValue,
|
||||
isUnknownAnalyticsBreakdownValue,
|
||||
UNKNOWN_BREAKDOWN_VALUE,
|
||||
} from '../breakdown'
|
||||
import { PREVIOUS_PERIOD_DATASET_ID_PREFIX } from './analytics-chart-constants'
|
||||
@@ -30,6 +36,7 @@ export type ChartDataset = {
|
||||
projectId: string
|
||||
label: string
|
||||
projectName?: string
|
||||
tooltip?: string
|
||||
data: number[]
|
||||
borderColor: string
|
||||
backgroundColor: string
|
||||
@@ -136,6 +143,7 @@ export function formatBreakdownLabel(
|
||||
breakdownValue: string,
|
||||
selectedBreakdown: AnalyticsBreakdownPreset,
|
||||
getVersionDisplayName: ((versionId: string) => string) | undefined,
|
||||
userNamesById: ReadonlyMap<string, string> | undefined,
|
||||
formatMessage: FormatMessage,
|
||||
): string {
|
||||
const normalizedValue = breakdownValue.trim()
|
||||
@@ -160,6 +168,15 @@ export function formatBreakdownLabel(
|
||||
if (selectedBreakdown === 'download_reason') {
|
||||
return formatAnalyticsDownloadReasonLabel(normalizedLowercaseValue, formatMessage)
|
||||
}
|
||||
if (selectedBreakdown === 'dependent_project_download') {
|
||||
if (isNoDependentAnalyticsBreakdownValue(breakdownValue)) {
|
||||
return formatMessage(analyticsMessages.noDependent)
|
||||
}
|
||||
return breakdownValue
|
||||
}
|
||||
if (selectedBreakdown === 'user_id') {
|
||||
return userNamesById?.get(breakdownValue) ?? breakdownValue
|
||||
}
|
||||
if (selectedBreakdown === 'version_id') {
|
||||
return getVersionDisplayName?.(breakdownValue) ?? breakdownValue
|
||||
}
|
||||
@@ -174,19 +191,34 @@ export function formatBreakdownLabels(
|
||||
breakdownValues: readonly string[],
|
||||
selectedBreakdowns: readonly AnalyticsBreakdownPreset[],
|
||||
getVersionDisplayName: ((versionId: string) => string) | undefined,
|
||||
userNamesById: ReadonlyMap<string, string> | undefined,
|
||||
formatMessage: FormatMessage,
|
||||
): string {
|
||||
const normalizedBreakdowns = selectedBreakdowns.filter((breakdown) => breakdown !== 'none')
|
||||
const downloadReasonBreakdownIndex = normalizedBreakdowns.indexOf('download_reason')
|
||||
|
||||
return collapseRepeatedUnknownBreakdownLabels(
|
||||
selectedBreakdowns
|
||||
.filter((breakdown) => breakdown !== 'none')
|
||||
.map((breakdown, index) =>
|
||||
formatBreakdownLabel(
|
||||
breakdownValues[index] ?? '',
|
||||
breakdown,
|
||||
getVersionDisplayName,
|
||||
normalizedBreakdowns.map((breakdown, index) => {
|
||||
const breakdownValue = breakdownValues[index] ?? ''
|
||||
if (
|
||||
breakdown === 'dependent_project_download' &&
|
||||
downloadReasonBreakdownIndex !== -1 &&
|
||||
isUnknownAnalyticsBreakdownValue(breakdownValue)
|
||||
) {
|
||||
return formatAnalyticsDependentProjectFallbackLabel(
|
||||
breakdownValues[downloadReasonBreakdownIndex],
|
||||
formatMessage,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return formatBreakdownLabel(
|
||||
breakdownValue,
|
||||
breakdown,
|
||||
getVersionDisplayName,
|
||||
userNamesById,
|
||||
formatMessage,
|
||||
)
|
||||
}),
|
||||
formatMessage,
|
||||
).join(COMBINED_BREAKDOWN_LABEL_SEPARATOR)
|
||||
}
|
||||
@@ -258,6 +290,33 @@ type PaletteRankEntry = {
|
||||
total: number
|
||||
}
|
||||
|
||||
function formatDatasetTooltip(projectName: string | undefined): string | undefined {
|
||||
return projectName
|
||||
}
|
||||
|
||||
function formatDependentProjectDatasetTooltip(
|
||||
versionName: string | undefined,
|
||||
dependentProjectName: string | undefined,
|
||||
dependencyProjectNames: readonly string[],
|
||||
formatMessage: FormatMessage,
|
||||
): string | undefined {
|
||||
if (dependencyProjectNames.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (versionName && dependentProjectName) {
|
||||
return formatMessage(analyticsChartMessages.dependentProjectVersionTooltip, {
|
||||
dependentProject: dependentProjectName,
|
||||
dependencyProject: dependencyProjectNames.join(', '),
|
||||
version: versionName,
|
||||
})
|
||||
}
|
||||
|
||||
return formatMessage(analyticsChartMessages.dependentOnProjectTooltip, {
|
||||
project: dependencyProjectNames.join(', '),
|
||||
})
|
||||
}
|
||||
|
||||
function getPaletteColorForIndex(index: number, palette: string[]): string {
|
||||
if (palette.length === 0) return ''
|
||||
|
||||
@@ -320,6 +379,10 @@ export function buildChartDatasets(
|
||||
activeStat: AnalyticsDashboardStat,
|
||||
palette: string[],
|
||||
selectedBreakdowns: readonly AnalyticsBreakdownPreset[],
|
||||
selectedFilters: AnalyticsSelectedFilters,
|
||||
dependentProjectTypesById: ReadonlyMap<string, readonly string[]>,
|
||||
projectNamesById: ReadonlyMap<string, string>,
|
||||
userNamesById: ReadonlyMap<string, string>,
|
||||
getVersionDisplayName: ((versionId: string) => string) | undefined,
|
||||
getVersionProjectName: ((versionId: string) => string | undefined) | undefined,
|
||||
formatMessage: FormatMessage,
|
||||
@@ -332,17 +395,43 @@ export function buildChartDatasets(
|
||||
|
||||
const dataLength = Math.max(sliceCount, timeSlices.length)
|
||||
const normalizedBreakdowns = selectedBreakdowns.filter((breakdown) => breakdown !== 'none')
|
||||
const projectNamesById = new Map(selectedProjects.map((project) => [project.id, project.name]))
|
||||
const normalizedFilters = normalizeAnalyticsSelectedFilters(selectedFilters)
|
||||
|
||||
function formatChartBreakdownLabels(breakdownValues: readonly string[]): string {
|
||||
const downloadReasonBreakdownIndex = normalizedBreakdowns.indexOf('download_reason')
|
||||
|
||||
return collapseRepeatedUnknownBreakdownLabels(
|
||||
normalizedBreakdowns.map((breakdown, index) => {
|
||||
const breakdownValue = breakdownValues[index] ?? ''
|
||||
if (breakdown === 'project') {
|
||||
if (breakdown === 'project' || breakdown === 'dependent_project_download') {
|
||||
if (
|
||||
breakdown === 'dependent_project_download' &&
|
||||
isNoDependentAnalyticsBreakdownValue(breakdownValue)
|
||||
) {
|
||||
return formatMessage(analyticsMessages.noDependent)
|
||||
}
|
||||
if (
|
||||
breakdown === 'dependent_project_download' &&
|
||||
isUnknownAnalyticsBreakdownValue(breakdownValue)
|
||||
) {
|
||||
return downloadReasonBreakdownIndex === -1
|
||||
? formatMessage(analyticsMessages.unknown)
|
||||
: formatAnalyticsDependentProjectFallbackLabel(
|
||||
breakdownValues[downloadReasonBreakdownIndex],
|
||||
formatMessage,
|
||||
)
|
||||
}
|
||||
|
||||
return projectNamesById.get(breakdownValue) ?? breakdownValue
|
||||
}
|
||||
|
||||
return formatBreakdownLabel(breakdownValue, breakdown, getVersionDisplayName, formatMessage)
|
||||
return formatBreakdownLabel(
|
||||
breakdownValue,
|
||||
breakdown,
|
||||
getVersionDisplayName,
|
||||
userNamesById,
|
||||
formatMessage,
|
||||
)
|
||||
}),
|
||||
formatMessage,
|
||||
).join(COMBINED_BREAKDOWN_LABEL_SEPARATOR)
|
||||
@@ -352,13 +441,27 @@ export function buildChartDatasets(
|
||||
normalizedBreakdowns.length > 0 &&
|
||||
!(normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'project')
|
||||
) {
|
||||
const hasVersionBreakdown = normalizedBreakdowns.includes('version_id')
|
||||
const hasDependentProjectBreakdown = normalizedBreakdowns.includes('dependent_project_download')
|
||||
const shouldShowDependentProjectTooltip =
|
||||
hasDependentProjectBreakdown && (selectedProjects.length > 1 || hasVersionBreakdown)
|
||||
const dataByBreakdown = new Map<string, number[]>()
|
||||
const breakdownValuesByKey = new Map<string, string[]>()
|
||||
const downloadTotalsByBreakdown = new Map<string, number>()
|
||||
const dependentOnProjectIdsByBreakdown = new Map<string, Set<string>>()
|
||||
|
||||
timeSlices.forEach((slice, sliceIndex) => {
|
||||
for (const point of slice) {
|
||||
if (!isProjectAnalyticsPointInSelectedProjects(point, selectedProjectIds)) continue
|
||||
if (
|
||||
!doesAnalyticsPointMatchNormalizedFilters(
|
||||
point,
|
||||
normalizedFilters,
|
||||
dependentProjectTypesById,
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const breakdownValues = getAnalyticsBreakdownValues(
|
||||
point,
|
||||
@@ -374,6 +477,11 @@ export function buildChartDatasets(
|
||||
dataByBreakdown.set(breakdownKey, new Array(dataLength).fill(0))
|
||||
breakdownValuesByKey.set(breakdownKey, breakdownValues)
|
||||
}
|
||||
if (shouldShowDependentProjectTooltip && point.metric_kind === 'downloads') {
|
||||
const projectIds = dependentOnProjectIdsByBreakdown.get(breakdownKey) ?? new Set<string>()
|
||||
projectIds.add(point.source_project)
|
||||
dependentOnProjectIdsByBreakdown.set(breakdownKey, projectIds)
|
||||
}
|
||||
|
||||
if (point.metric_kind === 'downloads') {
|
||||
downloadTotalsByBreakdown.set(
|
||||
@@ -402,6 +510,42 @@ export function buildChartDatasets(
|
||||
return Array.from(dataByBreakdown.entries()).map(([breakdownKey, data]) => {
|
||||
const breakdownValues = breakdownValuesByKey.get(breakdownKey) ?? []
|
||||
const fallbackColor = colorsByBreakdown.get(breakdownKey) ?? ''
|
||||
const versionBreakdownIndex = normalizedBreakdowns.indexOf('version_id')
|
||||
const dependentProjectBreakdownIndex = normalizedBreakdowns.indexOf(
|
||||
'dependent_project_download',
|
||||
)
|
||||
const versionName =
|
||||
hasVersionBreakdown && versionBreakdownIndex !== -1
|
||||
? getVersionDisplayName?.(breakdownValues[versionBreakdownIndex] ?? '')
|
||||
: undefined
|
||||
const dependentProjectId =
|
||||
dependentProjectBreakdownIndex !== -1
|
||||
? breakdownValues[dependentProjectBreakdownIndex]
|
||||
: undefined
|
||||
const dependentProjectName = dependentProjectId
|
||||
? isMissingDependentProjectValue(dependentProjectId)
|
||||
? undefined
|
||||
: (projectNamesById.get(dependentProjectId) ?? dependentProjectId)
|
||||
: undefined
|
||||
const versionProjectName =
|
||||
normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'version_id'
|
||||
? getVersionProjectName?.(breakdownValues[0] ?? '')
|
||||
: undefined
|
||||
const dependencyProjectNames = [...(dependentOnProjectIdsByBreakdown.get(breakdownKey) ?? [])]
|
||||
.map((projectId) => projectNamesById.get(projectId) ?? projectId)
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
const dependentProjectTooltip = dependentProjectId
|
||||
? isNoDependentAnalyticsBreakdownValue(dependentProjectId)
|
||||
? formatMessage(analyticsMessages.noDependentTooltip)
|
||||
: isUnknownAnalyticsBreakdownValue(dependentProjectId)
|
||||
? formatMessage(analyticsMessages.unknown)
|
||||
: formatDependentProjectDatasetTooltip(
|
||||
versionName,
|
||||
dependentProjectName,
|
||||
dependencyProjectNames,
|
||||
formatMessage,
|
||||
)
|
||||
: undefined
|
||||
const color =
|
||||
normalizedBreakdowns.length === 1
|
||||
? getBreakdownColor(
|
||||
@@ -414,10 +558,8 @@ export function buildChartDatasets(
|
||||
return {
|
||||
projectId: getAnalyticsBreakdownDatasetId(breakdownValues, normalizedBreakdowns),
|
||||
label: formatChartBreakdownLabels(breakdownValues),
|
||||
projectName:
|
||||
normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'version_id'
|
||||
? getVersionProjectName?.(breakdownValues[0] ?? '')
|
||||
: undefined,
|
||||
projectName: versionProjectName,
|
||||
tooltip: dependentProjectTooltip ?? formatDatasetTooltip(versionProjectName),
|
||||
data,
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
@@ -432,6 +574,15 @@ export function buildChartDatasets(
|
||||
timeSlices.forEach((slice, sliceIndex) => {
|
||||
for (const point of slice) {
|
||||
if (!isProjectAnalyticsPointInSelectedProjects(point, selectedProjectIds)) continue
|
||||
if (
|
||||
!doesAnalyticsPointMatchNormalizedFilters(
|
||||
point,
|
||||
normalizedFilters,
|
||||
dependentProjectTypesById,
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (point.metric_kind === 'downloads') {
|
||||
downloadTotal += getMetricValue(point, 'downloads')
|
||||
@@ -481,6 +632,15 @@ export function buildChartDatasets(
|
||||
timeSlices.forEach((slice, sliceIndex) => {
|
||||
for (const point of slice) {
|
||||
if (!isProjectAnalyticsPointInSelectedProjects(point, selectedProjectIds)) continue
|
||||
if (
|
||||
!doesAnalyticsPointMatchNormalizedFilters(
|
||||
point,
|
||||
normalizedFilters,
|
||||
dependentProjectTypesById,
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const breakdownValues = getAnalyticsBreakdownValues(
|
||||
point,
|
||||
@@ -526,6 +686,10 @@ export function buildChartDatasets(
|
||||
return Array.from(dataByProjectBreakdown.entries()).map(([breakdownKey, data]) => {
|
||||
const breakdownValues = breakdownValuesByKey.get(breakdownKey) ?? []
|
||||
const fallbackColor = colorsByBreakdown.get(breakdownKey) ?? ''
|
||||
const versionProjectName =
|
||||
normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'version_id'
|
||||
? getVersionProjectName?.(breakdownValues[0] ?? '')
|
||||
: undefined
|
||||
const color =
|
||||
normalizedBreakdowns.length === 1
|
||||
? getBreakdownColor(
|
||||
@@ -538,10 +702,8 @@ export function buildChartDatasets(
|
||||
return {
|
||||
projectId: getAnalyticsBreakdownDatasetId(breakdownValues, normalizedBreakdowns),
|
||||
label: formatChartBreakdownLabels(breakdownValues),
|
||||
projectName:
|
||||
normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'version_id'
|
||||
? getVersionProjectName?.(breakdownValues[0] ?? '')
|
||||
: undefined,
|
||||
projectName: versionProjectName,
|
||||
tooltip: formatDatasetTooltip(versionProjectName),
|
||||
data,
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
@@ -549,6 +711,10 @@ export function buildChartDatasets(
|
||||
})
|
||||
}
|
||||
|
||||
function isMissingDependentProjectValue(value: string | undefined): boolean {
|
||||
return isUnknownAnalyticsBreakdownValue(value) || isNoDependentAnalyticsBreakdownValue(value)
|
||||
}
|
||||
|
||||
export function getSliceCount(
|
||||
timeRange: Labrinth.Analytics.v3.TimeRange,
|
||||
fallback: number,
|
||||
|
||||
+27
-4
@@ -1,12 +1,15 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { useVIntl } from '@modrinth/ui'
|
||||
import { computed, type ComputedRef, ref, watch } from 'vue'
|
||||
|
||||
import { useTheme } from '~/composables/nuxt-accessors'
|
||||
import { isDarkTheme } from '~/plugins/theme/index.ts'
|
||||
import type {
|
||||
AnalyticsBreakdownPreset,
|
||||
AnalyticsDashboardContextValue,
|
||||
AnalyticsDashboardProject,
|
||||
AnalyticsDashboardStat,
|
||||
AnalyticsSelectedFilters,
|
||||
} from '~/providers/analytics/analytics'
|
||||
|
||||
import {
|
||||
@@ -47,12 +50,16 @@ export function useAnalyticsChartDatasets(
|
||||
| 'displayedPreviousTimeSlices'
|
||||
| 'displayedSelectedGroupBy'
|
||||
| 'displayedSelectedBreakdowns'
|
||||
| 'displayedSelectedFilters'
|
||||
| 'hiddenGraphDatasetIds'
|
||||
| 'hasExplicitGraphDatasetSelection'
|
||||
| 'isGraphDatasetSelectionActive'
|
||||
| 'selectedGraphDatasetIds'
|
||||
| 'defaultGraphDatasetIds'
|
||||
| 'topGraphDatasetIds'
|
||||
| 'projectNamesById'
|
||||
| 'userNamesById'
|
||||
| 'dependentProjectTypesById'
|
||||
| 'getVersionDisplayName'
|
||||
| 'getVersionProjectName'
|
||||
>,
|
||||
@@ -160,6 +167,10 @@ export function useAnalyticsChartDatasets(
|
||||
selectedProjects.value,
|
||||
legendPalette.value,
|
||||
context.displayedSelectedBreakdowns.value,
|
||||
context.displayedSelectedFilters.value,
|
||||
context.dependentProjectTypesById.value,
|
||||
context.projectNamesById.value,
|
||||
context.userNamesById.value,
|
||||
context.getVersionDisplayName,
|
||||
showProjectVersionNames.value ? context.getVersionProjectName : undefined,
|
||||
formatMessage,
|
||||
@@ -172,6 +183,10 @@ export function useAnalyticsChartDatasets(
|
||||
selectedProjects.value,
|
||||
legendPalette.value,
|
||||
context.displayedSelectedBreakdowns.value,
|
||||
context.displayedSelectedFilters.value,
|
||||
context.dependentProjectTypesById.value,
|
||||
context.projectNamesById.value,
|
||||
context.userNamesById.value,
|
||||
context.getVersionDisplayName,
|
||||
showProjectVersionNames.value ? context.getVersionProjectName : undefined,
|
||||
formatMessage,
|
||||
@@ -357,12 +372,16 @@ export function useAnalyticsChartDatasets(
|
||||
}
|
||||
|
||||
function buildDatasetsByStat(
|
||||
timeSlices: Parameters<typeof buildChartDatasets>[0],
|
||||
timeSlices: Labrinth.Analytics.v3.TimeSlice[],
|
||||
selectedProjects: AnalyticsDashboardProject[],
|
||||
palette: string[],
|
||||
selectedBreakdowns: Parameters<typeof buildChartDatasets>[4],
|
||||
getVersionDisplayName: Parameters<typeof buildChartDatasets>[5],
|
||||
getVersionProjectName: Parameters<typeof buildChartDatasets>[6],
|
||||
selectedBreakdowns: readonly AnalyticsBreakdownPreset[],
|
||||
selectedFilters: AnalyticsSelectedFilters,
|
||||
dependentProjectTypesById: ReadonlyMap<string, readonly string[]>,
|
||||
projectNamesById: ReadonlyMap<string, string>,
|
||||
userNamesById: ReadonlyMap<string, string>,
|
||||
getVersionDisplayName: (versionId: string) => string,
|
||||
getVersionProjectName: ((versionId: string) => string | undefined) | undefined,
|
||||
formatMessage: FormatMessage,
|
||||
sliceCount: number,
|
||||
) {
|
||||
@@ -374,6 +393,10 @@ function buildDatasetsByStat(
|
||||
stat,
|
||||
palette,
|
||||
selectedBreakdowns,
|
||||
selectedFilters,
|
||||
dependentProjectTypesById,
|
||||
projectNamesById,
|
||||
userNamesById,
|
||||
getVersionDisplayName,
|
||||
getVersionProjectName,
|
||||
formatMessage,
|
||||
|
||||
@@ -13,6 +13,7 @@ export type AnalyticsBreakdownItemType =
|
||||
| 'downloadSource'
|
||||
| 'gameVersion'
|
||||
| 'loader'
|
||||
| 'member'
|
||||
| 'monetization'
|
||||
| 'project'
|
||||
| 'projectVersion'
|
||||
@@ -83,6 +84,14 @@ export const analyticsMessages = defineMessages({
|
||||
id: 'analytics.value.unknown',
|
||||
defaultMessage: 'Unknown',
|
||||
},
|
||||
noDependent: {
|
||||
id: 'analytics.value.no-dependent',
|
||||
defaultMessage: 'No dependents',
|
||||
},
|
||||
noDependentTooltip: {
|
||||
id: 'analytics.value.no-dependent-tooltip',
|
||||
defaultMessage: 'Downloaded for reasons other than being a dependency',
|
||||
},
|
||||
other: {
|
||||
id: 'analytics.value.other',
|
||||
defaultMessage: 'Other',
|
||||
@@ -175,6 +184,14 @@ export const analyticsMessages = defineMessages({
|
||||
id: 'analytics.filter.search.project-versions',
|
||||
defaultMessage: 'Search project versions...',
|
||||
},
|
||||
searchDependentProjectsPlaceholder: {
|
||||
id: 'analytics.filter.search.dependent-projects',
|
||||
defaultMessage: 'Search projects...',
|
||||
},
|
||||
searchMembersPlaceholder: {
|
||||
id: 'analytics.filter.search.members',
|
||||
defaultMessage: 'Search members...',
|
||||
},
|
||||
searchVersionsPlaceholder: {
|
||||
id: 'analytics.filter.search.versions',
|
||||
defaultMessage: 'Search versions...',
|
||||
@@ -351,6 +368,22 @@ export const analyticsBreakdownMessages = defineMessages({
|
||||
id: 'analytics.breakdown.download-reason',
|
||||
defaultMessage: 'Download reason',
|
||||
},
|
||||
members: {
|
||||
id: 'analytics.breakdown.members',
|
||||
defaultMessage: 'Members',
|
||||
},
|
||||
dependentProjectDownload: {
|
||||
id: 'analytics.breakdown.dependent-project-download',
|
||||
defaultMessage: 'Dependent project',
|
||||
},
|
||||
dependentProjectType: {
|
||||
id: 'analytics.breakdown.dependent-project-type',
|
||||
defaultMessage: 'Dependent project type',
|
||||
},
|
||||
dependentOn: {
|
||||
id: 'analytics.breakdown.dependent-on',
|
||||
defaultMessage: 'Dependent on',
|
||||
},
|
||||
versionId: {
|
||||
id: 'analytics.breakdown.project-version',
|
||||
defaultMessage: 'Project version',
|
||||
@@ -517,25 +550,33 @@ export const analyticsChartMessages = defineMessages({
|
||||
id: 'analytics.chart.action.show-top-eight',
|
||||
defaultMessage: 'Show top 8',
|
||||
},
|
||||
dependentOnProjectTooltip: {
|
||||
id: 'analytics.chart.tooltip.dependent-on-project',
|
||||
defaultMessage: 'Dependent on {project}',
|
||||
},
|
||||
dependentProjectVersionTooltip: {
|
||||
id: 'analytics.chart.tooltip.dependent-project-version',
|
||||
defaultMessage: '{dependentProject} dependent on {dependencyProject}, {version}',
|
||||
},
|
||||
tableSelectionLimited: {
|
||||
id: 'analytics.chart.table-selection.limited',
|
||||
defaultMessage:
|
||||
'Showing {limit} {itemType, select, project {{limit, plural, one {project} other {projects}}} country {{limit, plural, one {country} other {countries}}} monetization {{limit, plural, one {monetization value} other {monetization values}}} downloadSource {{limit, plural, one {download source} other {download sources}}} downloadReason {{limit, plural, one {download reason} other {download reasons}}} projectVersion {{limit, plural, one {project version} other {project versions}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {game version} other {game versions}}} other {{limit, plural, one {item} other {items}}}} from table',
|
||||
'Showing {limit} {itemType, select, project {{limit, plural, one {project} other {projects}}} country {{limit, plural, one {country} other {countries}}} monetization {{limit, plural, one {monetization value} other {monetization values}}} downloadSource {{limit, plural, one {download source} other {download sources}}} downloadReason {{limit, plural, one {download reason} other {download reasons}}} member {{limit, plural, one {member} other {members}}} projectVersion {{limit, plural, one {project version} other {project versions}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {game version} other {game versions}}} other {{limit, plural, one {item} other {items}}}} from table',
|
||||
},
|
||||
tableSelectionAll: {
|
||||
id: 'analytics.chart.table-selection.all',
|
||||
defaultMessage:
|
||||
'Showing all {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table',
|
||||
'Showing all {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} member {{count, plural, one {member} other {members}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table',
|
||||
},
|
||||
tableSelectionTop: {
|
||||
id: 'analytics.chart.table-selection.top',
|
||||
defaultMessage:
|
||||
'Showing top {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table',
|
||||
'Showing top {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} member {{count, plural, one {member} other {members}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table',
|
||||
},
|
||||
tableSelectionCount: {
|
||||
id: 'analytics.chart.table-selection.count',
|
||||
defaultMessage:
|
||||
'Showing {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table',
|
||||
'Showing {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} member {{count, plural, one {member} other {members}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table',
|
||||
},
|
||||
lineView: {
|
||||
id: 'analytics.chart.view.line',
|
||||
@@ -781,6 +822,10 @@ export function formatAnalyticsBreakdownLabel(
|
||||
return formatMessage(analyticsBreakdownMessages.userAgent)
|
||||
case 'download_reason':
|
||||
return formatMessage(analyticsBreakdownMessages.downloadReason)
|
||||
case 'user_id':
|
||||
return formatMessage(analyticsBreakdownMessages.members)
|
||||
case 'dependent_project_download':
|
||||
return formatMessage(analyticsBreakdownMessages.dependentProjectDownload)
|
||||
case 'version_id':
|
||||
return formatMessage(analyticsBreakdownMessages.versionId)
|
||||
case 'loader':
|
||||
@@ -810,6 +855,10 @@ export function getAnalyticsBreakdownItemType(
|
||||
return 'downloadSource'
|
||||
case 'download_reason':
|
||||
return 'downloadReason'
|
||||
case 'user_id':
|
||||
return 'member'
|
||||
case 'dependent_project_download':
|
||||
return 'project'
|
||||
case 'version_id':
|
||||
return 'projectVersion'
|
||||
case 'loader':
|
||||
@@ -853,6 +902,18 @@ export function formatAnalyticsDownloadReasonLabel(
|
||||
}
|
||||
}
|
||||
|
||||
export function formatAnalyticsDependentProjectFallbackLabel(
|
||||
downloadReason: string | undefined,
|
||||
formatMessage: FormatMessage,
|
||||
): string {
|
||||
const normalizedReason = downloadReason?.trim().toLowerCase()
|
||||
if (normalizedReason === 'standalone' || normalizedReason === 'update') {
|
||||
return formatMessage(analyticsMessages.noDependent)
|
||||
}
|
||||
|
||||
return formatMessage(analyticsMessages.unknown)
|
||||
}
|
||||
|
||||
export function formatAnalyticsDownloadSourceLabel(
|
||||
source: string,
|
||||
formatMessage: FormatMessage,
|
||||
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
MutableRouteQuery,
|
||||
} from '~/providers/analytics/analytics-types'
|
||||
|
||||
import { getAnalyticsBreakdownsWithSharedStats } from './query-builder/query-filter-utils'
|
||||
|
||||
export const DEFAULT_TIMEFRAME_PRESET: AnalyticsTimeframePreset = 'last_30_days'
|
||||
export const DEFAULT_TIMEFRAME_MODE: AnalyticsTimeframeMode = 'preset'
|
||||
export const DEFAULT_LAST_TIMEFRAME_AMOUNT = 1
|
||||
@@ -73,9 +75,11 @@ const BREAKDOWN_PRESET_VALUES: AnalyticsBreakdownPreset[] = [
|
||||
'monetization',
|
||||
'user_agent',
|
||||
'download_reason',
|
||||
'user_id',
|
||||
'version_id',
|
||||
'loader',
|
||||
'game_version',
|
||||
'dependent_project_download',
|
||||
]
|
||||
|
||||
const ANALYTICS_DASHBOARD_STAT_VALUES: AnalyticsDashboardStat[] = [
|
||||
@@ -89,15 +93,18 @@ const ANALYTICS_GRAPH_VIEW_MODE_VALUES: AnalyticsGraphViewMode[] = ['line', 'are
|
||||
const ANALYTICS_TABLE_SORT_COLUMN_VALUES: AnalyticsTableSortColumn[] = [
|
||||
'date',
|
||||
'project',
|
||||
'dependent_on',
|
||||
'breakdown',
|
||||
'breakdown_project',
|
||||
'breakdown_country',
|
||||
'breakdown_monetization',
|
||||
'breakdown_user_agent',
|
||||
'breakdown_download_reason',
|
||||
'breakdown_user_id',
|
||||
'breakdown_version_id',
|
||||
'breakdown_loader',
|
||||
'breakdown_game_version',
|
||||
'breakdown_dependent_project_download',
|
||||
'views',
|
||||
'downloads',
|
||||
'revenue',
|
||||
@@ -131,9 +138,12 @@ const QUERY_KEY_FILTER_MONETIZATION = 'a_monetization'
|
||||
const QUERY_KEY_FILTER_USER_AGENT = 'a_user_agent'
|
||||
const QUERY_KEY_FILTER_LEGACY_DOWNLOAD_SOURCE = 'a_download_source'
|
||||
const QUERY_KEY_FILTER_DOWNLOAD_REASON = 'a_download_reason'
|
||||
const QUERY_KEY_FILTER_USER_ID = 'a_user_id'
|
||||
const QUERY_KEY_FILTER_VERSION_ID = 'a_version_id'
|
||||
const QUERY_KEY_FILTER_GAME_VERSION = 'a_game_version'
|
||||
const QUERY_KEY_FILTER_LOADER_TYPE = 'a_loader_type'
|
||||
const QUERY_KEY_FILTER_DEPENDENT_PROJECT_ID = 'a_dependent_project_id'
|
||||
const QUERY_KEY_FILTER_DEPENDENT_PROJECT_TYPE = 'a_dependent_project_type'
|
||||
const QUERY_KEY_STAT = 'a_stat'
|
||||
const QUERY_KEY_GRAPH_VIEW_MODE = 'a_chart'
|
||||
const QUERY_KEY_GRAPH_RATIO_MODE = 'a_ratio'
|
||||
@@ -154,9 +164,12 @@ const URL_FILTER_CATEGORIES: Exclude<AnalyticsQueryFilterCategory, 'project'>[]
|
||||
'monetization',
|
||||
'user_agent',
|
||||
'download_reason',
|
||||
'user_id',
|
||||
'version_id',
|
||||
'game_version',
|
||||
'loader_type',
|
||||
'dependent_project_id',
|
||||
'dependent_project_type',
|
||||
]
|
||||
|
||||
const FILTER_QUERY_KEY_BY_CATEGORY: Record<
|
||||
@@ -168,9 +181,12 @@ const FILTER_QUERY_KEY_BY_CATEGORY: Record<
|
||||
monetization: QUERY_KEY_FILTER_MONETIZATION,
|
||||
user_agent: QUERY_KEY_FILTER_USER_AGENT,
|
||||
download_reason: QUERY_KEY_FILTER_DOWNLOAD_REASON,
|
||||
user_id: QUERY_KEY_FILTER_USER_ID,
|
||||
version_id: QUERY_KEY_FILTER_VERSION_ID,
|
||||
game_version: QUERY_KEY_FILTER_GAME_VERSION,
|
||||
loader_type: QUERY_KEY_FILTER_LOADER_TYPE,
|
||||
dependent_project_id: QUERY_KEY_FILTER_DEPENDENT_PROJECT_ID,
|
||||
dependent_project_type: QUERY_KEY_FILTER_DEPENDENT_PROJECT_TYPE,
|
||||
}
|
||||
|
||||
const ANALYTICS_QUERY_KEYS = [
|
||||
@@ -189,9 +205,12 @@ const ANALYTICS_QUERY_KEYS = [
|
||||
QUERY_KEY_FILTER_USER_AGENT,
|
||||
QUERY_KEY_FILTER_LEGACY_DOWNLOAD_SOURCE,
|
||||
QUERY_KEY_FILTER_DOWNLOAD_REASON,
|
||||
QUERY_KEY_FILTER_USER_ID,
|
||||
QUERY_KEY_FILTER_VERSION_ID,
|
||||
QUERY_KEY_FILTER_GAME_VERSION,
|
||||
QUERY_KEY_FILTER_LOADER_TYPE,
|
||||
QUERY_KEY_FILTER_DEPENDENT_PROJECT_ID,
|
||||
QUERY_KEY_FILTER_DEPENDENT_PROJECT_TYPE,
|
||||
QUERY_KEY_STAT,
|
||||
QUERY_KEY_GRAPH_VIEW_MODE,
|
||||
QUERY_KEY_GRAPH_RATIO_MODE,
|
||||
@@ -212,9 +231,12 @@ export function buildEmptySelectedFilters(): AnalyticsSelectedFilters {
|
||||
monetization: [],
|
||||
user_agent: [],
|
||||
download_reason: [],
|
||||
user_id: [],
|
||||
version_id: [],
|
||||
game_version: [],
|
||||
loader_type: [],
|
||||
dependent_project_id: [],
|
||||
dependent_project_type: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +277,7 @@ function normalizeFilterQueryValues(
|
||||
.filter((value) => PROJECT_STATUS_FILTER_VALUES.includes(value))
|
||||
}
|
||||
|
||||
if (category !== 'loader_type') {
|
||||
if (category !== 'loader_type' && category !== 'dependent_project_type') {
|
||||
return values
|
||||
}
|
||||
|
||||
@@ -456,7 +478,7 @@ export function getAnalyticsBreakdownPresetsForProjectSelection(
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedBreakdowns
|
||||
return getAnalyticsBreakdownsWithSharedStats(normalizedBreakdowns)
|
||||
}
|
||||
|
||||
export function getAnalyticsBreakdownPresetForProjectSelection(
|
||||
@@ -776,6 +798,12 @@ export function hasAnalyticsProjectSelectionQuery(query: LocationQuery): boolean
|
||||
return parseListQueryValue(query[QUERY_KEY_PROJECT_IDS]).length > 0
|
||||
}
|
||||
|
||||
export function hasAnalyticsAllProjectSelectionQuery(query: LocationQuery): boolean {
|
||||
return parseListQueryValue(query[QUERY_KEY_PROJECT_IDS]).includes(
|
||||
PROJECT_SELECTION_ALL_QUERY_VALUE,
|
||||
)
|
||||
}
|
||||
|
||||
export function hasAnalyticsGraphProjectEventsVisibilityQuery(query: LocationQuery): boolean {
|
||||
return query[QUERY_KEY_GRAPH_PROJECT_EVENTS_VISIBILITY] !== undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="mr-2.5 flex min-w-0 items-center gap-2">
|
||||
<span
|
||||
v-if="!hideIcon"
|
||||
v-tooltip="iconTooltip"
|
||||
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded text-primary"
|
||||
>
|
||||
<img
|
||||
v-if="iconUrl"
|
||||
:src="iconUrl"
|
||||
:alt="formatMessage(analyticsMessages.projectIconAlt, { name: label })"
|
||||
class="h-6 w-6 rounded object-cover"
|
||||
/>
|
||||
<BoxIcon v-else class="h-full w-full" />
|
||||
</span>
|
||||
<component
|
||||
:is="labelHref ? 'a' : 'span'"
|
||||
v-tooltip="labelTooltip"
|
||||
:href="labelHref"
|
||||
:target="labelHref ? '_blank' : undefined"
|
||||
:rel="labelHref ? 'noopener noreferrer' : undefined"
|
||||
class="line-clamp-2 min-w-0 truncate text-wrap font-semibold leading-tight text-primary"
|
||||
:class="{ 'hover:underline': labelHref }"
|
||||
:title="label"
|
||||
>
|
||||
{{ label }}
|
||||
</component>
|
||||
<component
|
||||
:is="organizationHref ? 'a' : 'span'"
|
||||
v-if="organizationTooltip"
|
||||
v-tooltip="organizationTooltip"
|
||||
:href="organizationHref"
|
||||
:target="organizationHref ? '_blank' : undefined"
|
||||
:rel="organizationHref ? 'noopener noreferrer' : undefined"
|
||||
:aria-label="organizationTooltip"
|
||||
class="flex size-4 shrink-0 items-center text-primary"
|
||||
:class="{ 'hover:underline': organizationHref }"
|
||||
>
|
||||
<OrganizationIcon class="size-4" />
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BoxIcon, OrganizationIcon } from '@modrinth/assets'
|
||||
import { useVIntl } from '@modrinth/ui'
|
||||
|
||||
import { analyticsMessages } from '../analytics-messages.ts'
|
||||
|
||||
defineProps<{
|
||||
label: string
|
||||
iconUrl?: string
|
||||
iconTooltip?: string
|
||||
hideIcon?: boolean
|
||||
labelHref?: string
|
||||
labelTooltip?: string
|
||||
organizationHref?: string
|
||||
organizationTooltip?: string
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
</script>
|
||||
+5
-12
@@ -23,7 +23,6 @@ type BuildAnalyticsTableColumnsOptions = {
|
||||
selectedBreakdowns: readonly AnalyticsTableBreakdownPreset[]
|
||||
selectedFilters: AnalyticsSelectedFilters
|
||||
showBreakdownColumn: boolean
|
||||
showProjectVersionProjectColumn: boolean
|
||||
formatMessage: FormatMessage
|
||||
getRelevantAnalyticsDashboardStats: (
|
||||
breakdowns: readonly AnalyticsBreakdownPreset[],
|
||||
@@ -43,7 +42,6 @@ export function buildAnalyticsTableColumns({
|
||||
selectedBreakdowns,
|
||||
selectedFilters,
|
||||
showBreakdownColumn,
|
||||
showProjectVersionProjectColumn,
|
||||
formatMessage,
|
||||
getRelevantAnalyticsDashboardStats,
|
||||
}: BuildAnalyticsTableColumnsOptions): TableColumn<AnalyticsTableColumnKey>[] {
|
||||
@@ -66,20 +64,11 @@ export function buildAnalyticsTableColumns({
|
||||
key: getAnalyticsTableBreakdownColumnKey(breakdown),
|
||||
label: getAnalyticsTableBreakdownColumnLabel(breakdown, formatMessage),
|
||||
enableSorting: true,
|
||||
width: breakdown === 'project' ? '25%' : undefined,
|
||||
width: breakdown === 'project' && selectedBreakdowns.length === 1 ? '45%' : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (showProjectVersionProjectColumn) {
|
||||
nextColumns.push({
|
||||
key: 'project',
|
||||
label: formatAnalyticsBreakdownLabel('project', formatMessage),
|
||||
enableSorting: true,
|
||||
width: '25%',
|
||||
})
|
||||
}
|
||||
|
||||
for (const stat of stats) {
|
||||
const column = getAnalyticsTableMetricColumn(stat, formatMessage)
|
||||
if (column) {
|
||||
@@ -102,6 +91,7 @@ export function getAnalyticsTableMetricColumn(
|
||||
enableSorting: true,
|
||||
defaultSortDirection: 'desc',
|
||||
align: 'right',
|
||||
width: '20%',
|
||||
}
|
||||
case 'downloads':
|
||||
return {
|
||||
@@ -110,6 +100,7 @@ export function getAnalyticsTableMetricColumn(
|
||||
enableSorting: true,
|
||||
defaultSortDirection: 'desc',
|
||||
align: 'right',
|
||||
width: '20%',
|
||||
}
|
||||
case 'revenue':
|
||||
return {
|
||||
@@ -118,6 +109,7 @@ export function getAnalyticsTableMetricColumn(
|
||||
enableSorting: true,
|
||||
defaultSortDirection: 'desc',
|
||||
align: 'right',
|
||||
width: '20%',
|
||||
}
|
||||
case 'playtime':
|
||||
return {
|
||||
@@ -126,6 +118,7 @@ export function getAnalyticsTableMetricColumn(
|
||||
enableSorting: true,
|
||||
defaultSortDirection: 'desc',
|
||||
align: 'right',
|
||||
width: '20%',
|
||||
}
|
||||
default:
|
||||
return null
|
||||
|
||||
+2
@@ -67,6 +67,8 @@ function getAnalyticsTableCsvCellValue(
|
||||
return row.date
|
||||
case 'project':
|
||||
return row.project
|
||||
case 'dependent_on':
|
||||
return row.dependent_on
|
||||
case 'breakdown':
|
||||
return row.breakdownDisplay
|
||||
case 'views':
|
||||
|
||||
+94
-7
@@ -1,8 +1,11 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
import type {
|
||||
AnalyticsBreakdownPreset,
|
||||
AnalyticsDashboardStat,
|
||||
import {
|
||||
type AnalyticsBreakdownPreset,
|
||||
type AnalyticsDashboardStat,
|
||||
type AnalyticsSelectedFilters,
|
||||
doesAnalyticsPointMatchNormalizedFilters,
|
||||
normalizeAnalyticsSelectedFilters,
|
||||
} from '~/providers/analytics/analytics'
|
||||
|
||||
import {
|
||||
@@ -12,13 +15,18 @@ import {
|
||||
getSliceCount,
|
||||
} from '../analytics-chart/analytics-chart-utils'
|
||||
import type { FormatMessage } from '../analytics-messages'
|
||||
import { analyticsMessages } from '../analytics-messages'
|
||||
import {
|
||||
analyticsMessages,
|
||||
formatAnalyticsDependentProjectFallbackLabel,
|
||||
} from '../analytics-messages'
|
||||
import {
|
||||
ALL_BREAKDOWN_VALUE,
|
||||
COMBINED_BREAKDOWN_LABEL_SEPARATOR,
|
||||
getAnalyticsBreakdownDatasetId,
|
||||
getAnalyticsBreakdownKey,
|
||||
getAnalyticsBreakdownValues,
|
||||
isNoDependentAnalyticsBreakdownValue,
|
||||
isUnknownAnalyticsBreakdownValue,
|
||||
} from '../breakdown'
|
||||
import { getAnalyticsTableBreakdownColumnKey } from './analytics-table-columns'
|
||||
import type {
|
||||
@@ -36,8 +44,12 @@ type BuildAnalyticsTableRowsOptions = {
|
||||
timeSlices: Labrinth.Analytics.v3.TimeSlice[]
|
||||
selectedBreakdowns: readonly AnalyticsTableBreakdownPreset[]
|
||||
selectedProjectIds: ReadonlySet<string>
|
||||
selectedFilters: AnalyticsSelectedFilters
|
||||
dependentProjectTypesById: ReadonlyMap<string, readonly string[]>
|
||||
includeDependentProjectTooltipContext: boolean
|
||||
relevantStats: ReadonlySet<AnalyticsDashboardStat>
|
||||
projectNamesById: ReadonlyMap<string, string>
|
||||
userNamesById: ReadonlyMap<string, string>
|
||||
getVersionDisplayName: (versionId: string) => string
|
||||
getVersionProjectName: (versionId: string) => string | undefined
|
||||
showTimeInBucketLabel: boolean
|
||||
@@ -51,8 +63,12 @@ export function buildAnalyticsTableRows({
|
||||
timeSlices,
|
||||
selectedBreakdowns,
|
||||
selectedProjectIds,
|
||||
selectedFilters,
|
||||
dependentProjectTypesById,
|
||||
includeDependentProjectTooltipContext,
|
||||
relevantStats,
|
||||
projectNamesById,
|
||||
userNamesById,
|
||||
getVersionDisplayName,
|
||||
getVersionProjectName,
|
||||
showTimeInBucketLabel,
|
||||
@@ -72,6 +88,7 @@ export function buildAnalyticsTableRows({
|
||||
const projectDisplayValues = new Map<string, string>()
|
||||
const nextRows = new Map<string, AnalyticsTableRow>()
|
||||
const bucketLabelsBySliceIndex = new Map<number, { date: string; dateMs: number }>()
|
||||
const normalizedFilters = normalizeAnalyticsSelectedFilters(selectedFilters)
|
||||
|
||||
function getBreakdownDisplayValue(
|
||||
breakdownValue: string,
|
||||
@@ -84,6 +101,7 @@ export function buildAnalyticsTableRows({
|
||||
breakdownValue,
|
||||
breakdown,
|
||||
projectNamesById,
|
||||
userNamesById,
|
||||
getVersionDisplayName,
|
||||
formatMessage,
|
||||
)
|
||||
@@ -111,6 +129,15 @@ export function buildAnalyticsTableRows({
|
||||
return displayValue
|
||||
}
|
||||
|
||||
function getProjectVersionIdForBreakdownValues(breakdownValues: readonly string[]) {
|
||||
const versionBreakdownIndex = selectedBreakdowns.indexOf('version_id')
|
||||
if (versionBreakdownIndex === -1) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return breakdownValues[versionBreakdownIndex] ?? ''
|
||||
}
|
||||
|
||||
function getBreakdownDisplays(breakdownValues: readonly string[]) {
|
||||
const displays: AnalyticsTableBreakdownDisplayValues = {}
|
||||
|
||||
@@ -118,6 +145,19 @@ export function buildAnalyticsTableRows({
|
||||
displays[breakdown] = getBreakdownDisplayValue(breakdownValues[index] ?? '', breakdown)
|
||||
})
|
||||
|
||||
const dependentProjectBreakdownIndex = selectedBreakdowns.indexOf('dependent_project_download')
|
||||
const downloadReasonBreakdownIndex = selectedBreakdowns.indexOf('download_reason')
|
||||
if (
|
||||
dependentProjectBreakdownIndex !== -1 &&
|
||||
downloadReasonBreakdownIndex !== -1 &&
|
||||
isUnknownAnalyticsBreakdownValue(breakdownValues[dependentProjectBreakdownIndex])
|
||||
) {
|
||||
displays.dependent_project_download = formatAnalyticsDependentProjectFallbackLabel(
|
||||
breakdownValues[downloadReasonBreakdownIndex],
|
||||
formatMessage,
|
||||
)
|
||||
}
|
||||
|
||||
return displays
|
||||
}
|
||||
|
||||
@@ -157,6 +197,7 @@ export function buildAnalyticsTableRows({
|
||||
function createRow(
|
||||
rowId: string,
|
||||
breakdownValues: readonly string[],
|
||||
dependentOnProjectId?: string,
|
||||
bucketLabel?: { date: string; dateMs: number },
|
||||
) {
|
||||
const breakdownKey =
|
||||
@@ -169,6 +210,12 @@ export function buildAnalyticsTableRows({
|
||||
date: bucketLabel?.date ?? '',
|
||||
dateMs: bucketLabel?.dateMs ?? 0,
|
||||
project: getProjectDisplayValueForBreakdownValues(breakdownValues),
|
||||
projectVersionId: getProjectVersionIdForBreakdownValues(breakdownValues),
|
||||
dependent_on: dependentOnProjectId
|
||||
? (projectNamesById.get(dependentOnProjectId) ?? dependentOnProjectId)
|
||||
: '',
|
||||
dependentOnProjectId: dependentOnProjectId ?? '',
|
||||
dependentOnProjectIds: dependentOnProjectId ? [dependentOnProjectId] : [],
|
||||
breakdown: breakdownKey,
|
||||
breakdownValues: Object.fromEntries(
|
||||
selectedBreakdowns.map((breakdown, index) => [breakdown, breakdownValues[index] ?? '']),
|
||||
@@ -190,6 +237,18 @@ export function buildAnalyticsTableRows({
|
||||
return row
|
||||
}
|
||||
|
||||
function addDependentOnProjectIdToRow(row: AnalyticsTableRow, projectId: string | undefined) {
|
||||
if (!projectId || row.dependentOnProjectIds.includes(projectId)) {
|
||||
return
|
||||
}
|
||||
|
||||
row.dependentOnProjectIds.push(projectId)
|
||||
if (!row.dependentOnProjectId) {
|
||||
row.dependentOnProjectId = projectId
|
||||
row.dependent_on = projectNamesById.get(projectId) ?? projectId
|
||||
}
|
||||
}
|
||||
|
||||
if (!includeDate && selectedBreakdowns.length === 0) {
|
||||
createRow(ALL_PROJECTS_BREAKDOWN_VALUE, [])
|
||||
}
|
||||
@@ -211,6 +270,15 @@ export function buildAnalyticsTableRows({
|
||||
if (!selectedProjectIds.has(point.source_project)) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
!doesAnalyticsPointMatchNormalizedFilters(
|
||||
point,
|
||||
normalizedFilters,
|
||||
dependentProjectTypesById,
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pointStat = getAnalyticsTableStatForMetric(point.metric_kind)
|
||||
if (!pointStat || !relevantStats.has(pointStat)) {
|
||||
@@ -226,12 +294,21 @@ export function buildAnalyticsTableRows({
|
||||
}
|
||||
|
||||
const nextBucketLabel = includeDate ? (bucketLabel ?? getBucketLabel(sliceIndex)) : undefined
|
||||
const dependentOnProjectId = includeDependentProjectTooltipContext
|
||||
? point.source_project
|
||||
: undefined
|
||||
const dependentTooltipProjectId = selectedBreakdowns.includes('dependent_project_download')
|
||||
? point.source_project
|
||||
: undefined
|
||||
const breakdownKey =
|
||||
breakdownValues.length === 0
|
||||
? ALL_PROJECTS_BREAKDOWN_VALUE
|
||||
: getAnalyticsBreakdownKey(breakdownValues)
|
||||
const rowId = includeDate ? `${nextBucketLabel?.dateMs ?? 0}::${breakdownKey}` : breakdownKey
|
||||
const row = nextRows.get(rowId) ?? createRow(rowId, breakdownValues, nextBucketLabel)
|
||||
const row =
|
||||
nextRows.get(rowId) ??
|
||||
createRow(rowId, breakdownValues, dependentOnProjectId, nextBucketLabel)
|
||||
addDependentOnProjectIdToRow(row, dependentTooltipProjectId)
|
||||
addAnalyticsMetricToTableRow(row, point)
|
||||
}
|
||||
})
|
||||
@@ -292,11 +369,21 @@ function formatAnalyticsTableBreakdownDisplayValue(
|
||||
value: string,
|
||||
breakdown: AnalyticsTableBreakdownPreset,
|
||||
projectNamesById: ReadonlyMap<string, string>,
|
||||
userNamesById: ReadonlyMap<string, string>,
|
||||
getVersionDisplayName: (versionId: string) => string,
|
||||
formatMessage: FormatMessage,
|
||||
): string {
|
||||
if (breakdown === 'project') {
|
||||
if (breakdown === 'project' || breakdown === 'dependent_project_download') {
|
||||
if (breakdown === 'dependent_project_download') {
|
||||
if (isNoDependentAnalyticsBreakdownValue(value)) {
|
||||
return formatMessage(analyticsMessages.noDependent)
|
||||
}
|
||||
if (isUnknownAnalyticsBreakdownValue(value)) {
|
||||
return formatMessage(analyticsMessages.unknown)
|
||||
}
|
||||
}
|
||||
|
||||
return projectNamesById.get(value) ?? value
|
||||
}
|
||||
return formatBreakdownLabel(value, breakdown, getVersionDisplayName, formatMessage)
|
||||
return formatBreakdownLabel(value, breakdown, getVersionDisplayName, userNamesById, formatMessage)
|
||||
}
|
||||
|
||||
+3
-1
@@ -3,7 +3,7 @@ import type { TableColumn } from '@modrinth/ui'
|
||||
import { isAnalyticsTableBreakdownColumnKey } from './analytics-table-columns'
|
||||
import type { AnalyticsTableColumnKey, AnalyticsTableRow } from './analytics-table-types'
|
||||
|
||||
const SEARCHABLE_COLUMN_KEYS = new Set<AnalyticsTableColumnKey>(['date', 'project'])
|
||||
const SEARCHABLE_COLUMN_KEYS = new Set<AnalyticsTableColumnKey>(['date', 'project', 'dependent_on'])
|
||||
|
||||
export function getAnalyticsTableSearchableColumns(
|
||||
columns: TableColumn<AnalyticsTableColumnKey>[],
|
||||
@@ -39,6 +39,8 @@ function getAnalyticsTableSearchableCellValue(
|
||||
return row.date
|
||||
case 'project':
|
||||
return row.project
|
||||
case 'dependent_on':
|
||||
return row.dependent_on
|
||||
case 'breakdown':
|
||||
return row.breakdownDisplay
|
||||
default:
|
||||
|
||||
+9
@@ -129,6 +129,15 @@ function getAnalyticsTableRowComparator(
|
||||
directionFactor,
|
||||
sortCollator,
|
||||
)
|
||||
case 'dependent_on':
|
||||
return (left, right) =>
|
||||
compareAnalyticsTableRows(
|
||||
left,
|
||||
right,
|
||||
sortCollator.compare(left.dependent_on, right.dependent_on),
|
||||
directionFactor,
|
||||
sortCollator,
|
||||
)
|
||||
case 'breakdown':
|
||||
return (left, right) =>
|
||||
compareAnalyticsTableRows(
|
||||
|
||||
+5
-1
@@ -18,11 +18,15 @@ export type AnalyticsTableSortState = {
|
||||
export type AnalyticsTableSortDirectionValue = AnalyticsTableSortDirection
|
||||
|
||||
export type AnalyticsTableRow = {
|
||||
[key: string]: string | number | AnalyticsTableBreakdownDisplayValues
|
||||
[key: string]: string | number | string[] | AnalyticsTableBreakdownDisplayValues
|
||||
id: string
|
||||
date: string
|
||||
dateMs: number
|
||||
project: string
|
||||
projectVersionId: string
|
||||
dependent_on: string
|
||||
dependentOnProjectId: string
|
||||
dependentOnProjectIds: string[]
|
||||
breakdown: string
|
||||
breakdownValues: AnalyticsTableBreakdownDisplayValues
|
||||
breakdownDisplays: AnalyticsTableBreakdownDisplayValues
|
||||
|
||||
@@ -55,32 +55,92 @@
|
||||
<template #cell-date="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-breakdown_project="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
<template #cell-breakdown_project="{ row, value }">
|
||||
<ProjectCell
|
||||
:label="getProjectCellLabel(value)"
|
||||
:icon-url="getProjectIconUrl(row.breakdownValues.project)"
|
||||
:icon-tooltip="getProjectCellLabel(value)"
|
||||
:label-href="getProjectPageHref(row.breakdownValues.project)"
|
||||
:organization-href="getProjectOrganizationPageHref(row.breakdownValues.project)"
|
||||
:organization-tooltip="getProjectOrganizationName(row.breakdownValues.project)"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-breakdown_country="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
<span class="mr-2.5 text-primary">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-breakdown_monetization="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
<span class="mr-2.5 text-primary">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-breakdown_user_agent="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
<span class="mr-2.5 text-primary">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-breakdown_download_reason="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
<span class="mr-2.5 text-primary">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-breakdown_version_id="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
<template #cell-breakdown_user_id="{ row, value }">
|
||||
<component
|
||||
:is="getUserPageHref(row.breakdownValues.user_id) ? 'a' : 'span'"
|
||||
:href="getUserPageHref(row.breakdownValues.user_id)"
|
||||
:target="getUserPageHref(row.breakdownValues.user_id) ? '_blank' : undefined"
|
||||
:rel="getUserPageHref(row.breakdownValues.user_id) ? 'noopener noreferrer' : undefined"
|
||||
class="mr-2.5 flex min-w-0 items-center gap-2 text-primary"
|
||||
:class="{ 'hover:underline': getUserPageHref(row.breakdownValues.user_id) }"
|
||||
>
|
||||
<span
|
||||
v-tooltip="getUserCellLabel(value)"
|
||||
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full text-primary"
|
||||
>
|
||||
<img
|
||||
v-if="getUserAvatarUrl(row.breakdownValues.user_id)"
|
||||
:src="getUserAvatarUrl(row.breakdownValues.user_id)"
|
||||
:alt="getUserCellLabel(value)"
|
||||
class="h-6 w-6 rounded-full object-cover"
|
||||
/>
|
||||
<UserIcon v-else class="h-full w-full" />
|
||||
</span>
|
||||
<span class="min-w-0 truncate font-semibold leading-tight text-primary">
|
||||
{{ value }}
|
||||
</span>
|
||||
</component>
|
||||
</template>
|
||||
<template #cell-breakdown_dependent_project_download="{ row, value }">
|
||||
<ProjectCell
|
||||
:label="getProjectCellLabel(value)"
|
||||
:icon-url="
|
||||
isMissingDependentProjectValue(row.breakdownValues.dependent_project_download)
|
||||
? undefined
|
||||
: getProjectIconUrl(row.breakdownValues.dependent_project_download)
|
||||
"
|
||||
:icon-tooltip="getProjectCellLabel(value)"
|
||||
:hide-icon="
|
||||
isMissingDependentProjectValue(row.breakdownValues.dependent_project_download)
|
||||
"
|
||||
:label-href="
|
||||
isMissingDependentProjectValue(row.breakdownValues.dependent_project_download)
|
||||
? undefined
|
||||
: getProjectPageHref(row.breakdownValues.dependent_project_download)
|
||||
"
|
||||
:label-tooltip="getDependentProjectTooltip(row)"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-breakdown_version_id="{ row, value }">
|
||||
<component
|
||||
:is="getVersionPageHref(row.projectVersionId) ? 'a' : 'span'"
|
||||
v-tooltip="getVersionProjectName(row.projectVersionId)"
|
||||
:href="getVersionPageHref(row.projectVersionId)"
|
||||
:target="getVersionPageHref(row.projectVersionId) ? '_blank' : undefined"
|
||||
:rel="getVersionPageHref(row.projectVersionId) ? 'noopener noreferrer' : undefined"
|
||||
class="mr-2.5 text-primary"
|
||||
:class="{ 'hover:underline': getVersionPageHref(row.projectVersionId) }"
|
||||
>
|
||||
{{ value }}
|
||||
</component>
|
||||
</template>
|
||||
<template #cell-breakdown_loader="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
<span class="mr-2.5 text-primary">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-breakdown_game_version="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-project="{ value }">
|
||||
<span class="text-primary">{{ value }}</span>
|
||||
<span class="mr-2.5 text-primary">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-views="{ row }">
|
||||
<span>{{ formatInteger(row.views) }}</span>
|
||||
@@ -130,7 +190,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, DropdownIcon, SearchIcon } from '@modrinth/assets'
|
||||
import { DownloadIcon, DropdownIcon, SearchIcon, UserIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
@@ -159,10 +219,15 @@ import {
|
||||
} from '../analytics-chart/analytics-chart-utils.ts'
|
||||
import {
|
||||
analyticsBreakdownMessages,
|
||||
analyticsChartMessages,
|
||||
analyticsMessages,
|
||||
analyticsTableMessages,
|
||||
} from '../analytics-messages.ts'
|
||||
import AnalyticsLoadingBar from '../AnalyticsLoadingBar.vue'
|
||||
import {
|
||||
isNoDependentAnalyticsBreakdownValue,
|
||||
isUnknownAnalyticsBreakdownValue,
|
||||
} from '../breakdown.ts'
|
||||
import {
|
||||
buildAnalyticsTableColumns,
|
||||
getAnalyticsTableBreakdownColumnLabel,
|
||||
@@ -194,8 +259,10 @@ import { sortAnalyticsTableRows } from './analytics-table-sorting.ts'
|
||||
import type {
|
||||
AnalyticsTableColumnKey,
|
||||
AnalyticsTableMode,
|
||||
AnalyticsTableRow,
|
||||
AnalyticsTableSortDirectionValue,
|
||||
} from './analytics-table-types.ts'
|
||||
import ProjectCell from './ProjectCell.vue'
|
||||
import { useAnalyticsTableGraphSelection } from './use-analytics-table-graph-selection.ts'
|
||||
import { useAnalyticsTablePagination } from './use-analytics-table-pagination.ts'
|
||||
import { useAnalyticsTableRowCache } from './use-analytics-table-row-cache.ts'
|
||||
@@ -221,7 +288,15 @@ const {
|
||||
getRelevantAnalyticsDashboardStats,
|
||||
isLoading,
|
||||
versionNumbersById,
|
||||
versionProjectIdsById,
|
||||
versionProjectNamesById,
|
||||
projectNamesById,
|
||||
projectIconUrlsById,
|
||||
projectOrganizationIdsById,
|
||||
projectOrganizationNamesById,
|
||||
userNamesById,
|
||||
userAvatarUrlsById,
|
||||
dependentProjectTypesById,
|
||||
getVersionDisplayName,
|
||||
getVersionProjectName,
|
||||
} = injectAnalyticsDashboardContext()
|
||||
@@ -263,11 +338,10 @@ const showGraphDatasetSelection = computed(() =>
|
||||
? selectedProjectIdSet.value.size > 1
|
||||
: selectedBreakdowns.value.length > 0,
|
||||
)
|
||||
const showProjectVersionProjectColumn = computed(
|
||||
const includeDependentProjectTooltipContext = computed(
|
||||
() =>
|
||||
selectedBreakdownSet.value.has('version_id') &&
|
||||
!selectedBreakdownSet.value.has('project') &&
|
||||
selectedProjectIdSet.value.size > 1,
|
||||
selectedBreakdownSet.value.has('dependent_project_download') &&
|
||||
!selectedBreakdownSet.value.has('project'),
|
||||
)
|
||||
const includeDateColumn = computed(
|
||||
() =>
|
||||
@@ -314,9 +388,6 @@ const csvExportOptions = computed<OverflowMenuOption[]>(() => {
|
||||
},
|
||||
]
|
||||
})
|
||||
const projectNamesById = computed(
|
||||
() => new Map(projects.value.map((project) => [project.id, project.name])),
|
||||
)
|
||||
const hasAvailableProjects = computed(() => projects.value.length > 0)
|
||||
const analyticsPointCount = computed(() =>
|
||||
timeSlices.value.reduce((sum, slice) => sum + slice.length, 0),
|
||||
@@ -360,8 +431,12 @@ function buildTableRows(mode: AnalyticsTableMode) {
|
||||
timeSlices: timeSlices.value,
|
||||
selectedBreakdowns: selectedBreakdowns.value,
|
||||
selectedProjectIds: selectedProjectIdSet.value,
|
||||
selectedFilters: selectedFilters.value,
|
||||
dependentProjectTypesById: dependentProjectTypesById.value,
|
||||
includeDependentProjectTooltipContext: includeDependentProjectTooltipContext.value,
|
||||
relevantStats: relevantStats.value,
|
||||
projectNamesById: projectNamesById.value,
|
||||
userNamesById: userNamesById.value,
|
||||
getVersionDisplayName,
|
||||
getVersionProjectName,
|
||||
showTimeInBucketLabel: showTimeInBucketLabel.value,
|
||||
@@ -379,12 +454,86 @@ function buildColumns(includeDate: boolean) {
|
||||
selectedBreakdowns: selectedBreakdowns.value,
|
||||
selectedFilters: selectedFilters.value,
|
||||
showBreakdownColumn: showBreakdownColumn.value,
|
||||
showProjectVersionProjectColumn: showProjectVersionProjectColumn.value,
|
||||
formatMessage,
|
||||
getRelevantAnalyticsDashboardStats,
|
||||
})
|
||||
}
|
||||
|
||||
function getProjectIconUrl(projectId: string | undefined) {
|
||||
return projectId ? projectIconUrlsById.value.get(projectId) : undefined
|
||||
}
|
||||
|
||||
function getProjectOrganizationName(projectId: string | undefined) {
|
||||
return projectId ? projectOrganizationNamesById.value.get(projectId) : undefined
|
||||
}
|
||||
|
||||
function getProjectCellLabel(value: unknown) {
|
||||
return typeof value === 'string' ? value : String(value ?? '')
|
||||
}
|
||||
|
||||
function getUserAvatarUrl(userId: string | undefined) {
|
||||
return userId ? userAvatarUrlsById.value.get(userId) : undefined
|
||||
}
|
||||
|
||||
function getUserCellLabel(value: unknown) {
|
||||
return typeof value === 'string' ? value : String(value ?? '')
|
||||
}
|
||||
|
||||
function getUserPageHref(userId: string | undefined) {
|
||||
if (!userId) return undefined
|
||||
const username = userNamesById.value.get(userId) ?? userId
|
||||
|
||||
return `/user/${encodeURIComponent(username)}`
|
||||
}
|
||||
|
||||
function getProjectPageHref(projectId: string | undefined) {
|
||||
return projectId ? `/project/${encodeURIComponent(projectId)}` : undefined
|
||||
}
|
||||
|
||||
function getProjectOrganizationPageHref(projectId: string | undefined) {
|
||||
if (!projectId) return undefined
|
||||
const organizationId = projectOrganizationIdsById.value.get(projectId)
|
||||
if (!organizationId) return undefined
|
||||
|
||||
return `/organization/${encodeURIComponent(organizationId)}`
|
||||
}
|
||||
|
||||
function getVersionPageHref(versionId: string | undefined) {
|
||||
if (!versionId) return undefined
|
||||
const projectId = versionProjectIdsById.value.get(versionId)
|
||||
if (!projectId) return undefined
|
||||
|
||||
return `/project/${encodeURIComponent(projectId)}/version/${encodeURIComponent(versionId)}`
|
||||
}
|
||||
|
||||
function isMissingDependentProjectValue(value: string | undefined) {
|
||||
return isUnknownAnalyticsBreakdownValue(value) || isNoDependentAnalyticsBreakdownValue(value)
|
||||
}
|
||||
|
||||
function getDependentProjectTooltip(row: AnalyticsTableRow) {
|
||||
if (isNoDependentAnalyticsBreakdownValue(row.breakdownValues.dependent_project_download)) {
|
||||
return formatMessage(analyticsMessages.noDependentTooltip)
|
||||
}
|
||||
if (isUnknownAnalyticsBreakdownValue(row.breakdownValues.dependent_project_download)) {
|
||||
return formatMessage(analyticsMessages.unknown)
|
||||
}
|
||||
|
||||
const dependencyProjectIds = new Set(row.dependentOnProjectIds)
|
||||
if (row.dependentOnProjectId) {
|
||||
dependencyProjectIds.add(row.dependentOnProjectId)
|
||||
}
|
||||
|
||||
const dependencyProjectNames = [...dependencyProjectIds]
|
||||
.map((projectId) => projectNamesById.value.get(projectId) ?? projectId)
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
|
||||
return dependencyProjectNames.length > 0
|
||||
? formatMessage(analyticsChartMessages.dependentOnProjectTooltip, {
|
||||
project: dependencyProjectNames.join(', '),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
|
||||
watch(
|
||||
activeColumns,
|
||||
(nextColumns) => {
|
||||
@@ -445,6 +594,9 @@ watch(
|
||||
selectedBreakdowns,
|
||||
selectedFilters,
|
||||
projects,
|
||||
dependentProjectTypesById,
|
||||
projectNamesById,
|
||||
userNamesById,
|
||||
versionNumbersById,
|
||||
versionProjectNamesById,
|
||||
],
|
||||
|
||||
+27
-2
@@ -7,6 +7,10 @@ import type {
|
||||
AnalyticsSelectedBreakdowns,
|
||||
} from '~/providers/analytics/analytics'
|
||||
|
||||
import {
|
||||
isNoDependentAnalyticsBreakdownValue,
|
||||
isUnknownAnalyticsBreakdownValue,
|
||||
} from '../breakdown'
|
||||
import { getAnalyticsTableMetricSortedGraphDatasetIds } from './analytics-table-sorting'
|
||||
import type { AnalyticsTableColumnKey, AnalyticsTableRow } from './analytics-table-types'
|
||||
|
||||
@@ -58,6 +62,9 @@ export function useAnalyticsTableGraphSelection({
|
||||
const filteredSelectableGraphDatasetIds = computed(() =>
|
||||
getAnalyticsTableSelectableGraphDatasetIds(filteredRows.value),
|
||||
)
|
||||
const excludedGraphDatasetIds = computed(() =>
|
||||
getAnalyticsTableExcludedGraphDatasetIds(sortedRows.value),
|
||||
)
|
||||
const sortedMetricGraphDatasetIds = computed(() =>
|
||||
getAnalyticsTableMetricSortedGraphDatasetIds(sortedRows.value, sortColumn.value, sortCollator),
|
||||
)
|
||||
@@ -65,7 +72,9 @@ export function useAnalyticsTableGraphSelection({
|
||||
const sortedMetricIds = sortedMetricGraphDatasetIds.value
|
||||
const defaultIds =
|
||||
sortedMetricIds.length > 0 ? sortedMetricIds : selectableGraphDatasetIds.value
|
||||
return defaultIds.slice(0, graphDatasetSelectionLimit)
|
||||
return defaultIds
|
||||
.filter((id) => !excludedGraphDatasetIds.value.has(id))
|
||||
.slice(0, graphDatasetSelectionLimit)
|
||||
})
|
||||
const tableSelectedGraphDatasetIds = computed<unknown[]>({
|
||||
get: () => selectedGraphDatasetIds.value,
|
||||
@@ -149,7 +158,9 @@ export function useAnalyticsTableGraphSelection({
|
||||
defaultGraphDatasetIds.value = nextShowGraphDatasetSelection
|
||||
? [...nextDefaultGraphDatasetIds]
|
||||
: []
|
||||
topGraphDatasetIds.value = nextShowGraphDatasetSelection ? [...nextTopGraphDatasetIds] : []
|
||||
topGraphDatasetIds.value = nextShowGraphDatasetSelection
|
||||
? nextTopGraphDatasetIds.filter((id) => !excludedGraphDatasetIds.value.has(id))
|
||||
: []
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
@@ -184,6 +195,20 @@ export function useAnalyticsTableGraphSelection({
|
||||
return Array.from(new Set(rows.map((row) => row.graphDatasetId)))
|
||||
}
|
||||
|
||||
function getAnalyticsTableExcludedGraphDatasetIds(rows: AnalyticsTableRow[]): Set<string> {
|
||||
return new Set(
|
||||
rows
|
||||
.filter((row) =>
|
||||
Object.values(row.breakdownValues).some(
|
||||
(value) =>
|
||||
isUnknownAnalyticsBreakdownValue(value) ||
|
||||
isNoDependentAnalyticsBreakdownValue(value),
|
||||
),
|
||||
)
|
||||
.map((row) => row.graphDatasetId),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
filteredSelectableGraphDatasetIds,
|
||||
tableSelectedGraphDatasetIds,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { formatAnalyticsDownloadSourceLabel, type FormatMessage } from './analyt
|
||||
|
||||
export const ALL_BREAKDOWN_VALUE = '__all__'
|
||||
export const UNKNOWN_BREAKDOWN_VALUE = '__unknown__'
|
||||
export const NO_DEPENDENT_BREAKDOWN_VALUE = '__no_dependent__'
|
||||
export const COMBINED_BREAKDOWN_LABEL_SEPARATOR = ' + '
|
||||
export const COMBINED_BREAKDOWN_DATASET_ID_PREFIX = 'breakdowns:'
|
||||
|
||||
@@ -41,6 +42,19 @@ export function getAnalyticsBreakdownValue(
|
||||
'reason' in point ? point.reason : undefined,
|
||||
UNKNOWN_BREAKDOWN_VALUE,
|
||||
)
|
||||
case 'user_id':
|
||||
return normalizeBreakdownValue('user_id' in point ? point.user_id : undefined)
|
||||
case 'dependent_project_download': {
|
||||
const dependentProjectId = normalizeBreakdownValue(
|
||||
'dependent_project_id' in point ? point.dependent_project_id : undefined,
|
||||
UNKNOWN_BREAKDOWN_VALUE,
|
||||
)
|
||||
const downloadReason = 'reason' in point ? point.reason?.trim().toLowerCase() : undefined
|
||||
return dependentProjectId === UNKNOWN_BREAKDOWN_VALUE &&
|
||||
(downloadReason === 'standalone' || downloadReason === 'update')
|
||||
? NO_DEPENDENT_BREAKDOWN_VALUE
|
||||
: dependentProjectId
|
||||
}
|
||||
case 'version_id':
|
||||
return normalizeBreakdownValue('version_id' in point ? point.version_id : undefined)
|
||||
case 'loader':
|
||||
@@ -94,16 +108,30 @@ export function getDownloadSourceLabel(value: string, formatMessage: FormatMessa
|
||||
return formatAnalyticsDownloadSourceLabel(value, formatMessage)
|
||||
}
|
||||
|
||||
export function isUnknownAnalyticsBreakdownValue(value: string | null | undefined): boolean {
|
||||
const normalized = value?.trim()
|
||||
if (!normalized) {
|
||||
return false
|
||||
}
|
||||
|
||||
const normalizedLowercase = normalized.toLowerCase()
|
||||
return (
|
||||
normalized === UNKNOWN_BREAKDOWN_VALUE ||
|
||||
normalizedLowercase === 'unknown' ||
|
||||
normalizedLowercase === 'other'
|
||||
)
|
||||
}
|
||||
|
||||
export function isNoDependentAnalyticsBreakdownValue(value: string | null | undefined): boolean {
|
||||
return value?.trim() === NO_DEPENDENT_BREAKDOWN_VALUE
|
||||
}
|
||||
|
||||
function normalizeBreakdownValue(
|
||||
value: string | undefined,
|
||||
fallback = ALL_BREAKDOWN_VALUE,
|
||||
): string {
|
||||
const normalized = value?.trim()
|
||||
const normalizedLowercase = normalized?.toLowerCase()
|
||||
if (
|
||||
fallback === UNKNOWN_BREAKDOWN_VALUE &&
|
||||
(normalizedLowercase === 'unknown' || normalizedLowercase === 'other')
|
||||
) {
|
||||
if (fallback === UNKNOWN_BREAKDOWN_VALUE && isUnknownAnalyticsBreakdownValue(normalized)) {
|
||||
return fallback
|
||||
}
|
||||
return normalized && normalized.length > 0 ? normalized : fallback
|
||||
|
||||
@@ -25,10 +25,25 @@
|
||||
|
||||
<template #option="{ category, option, selected }">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<template v-if="category.key === 'version_id'">
|
||||
<span
|
||||
v-if="category.key === 'user_id'"
|
||||
v-tooltip="option.label"
|
||||
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full text-primary"
|
||||
:class="selected ? 'text-contrast' : 'text-primary'"
|
||||
>
|
||||
<img
|
||||
v-if="getUserAvatarUrl(option.value)"
|
||||
:src="getUserAvatarUrl(option.value)"
|
||||
:alt="option.label"
|
||||
class="h-6 w-6 rounded-full object-cover"
|
||||
/>
|
||||
<UserIcon v-else class="h-full w-full" />
|
||||
</span>
|
||||
<template
|
||||
v-for="metadata in getFilterOptionProjectMetadata(category.key, option.value)"
|
||||
:key="`${category.key}-${option.value}-${metadata.name}`"
|
||||
>
|
||||
<span
|
||||
v-for="metadata in getProjectVersionOptionProjectMetadata(option.value)"
|
||||
:key="`${option.value}-${metadata.name}`"
|
||||
v-tooltip="metadata.name"
|
||||
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded text-primary"
|
||||
>
|
||||
@@ -42,6 +57,8 @@
|
||||
</span>
|
||||
</template>
|
||||
<span
|
||||
:ref="(element) => setFilterOptionLabelRef(category.key, option.value, element)"
|
||||
v-tooltip="getFilterOptionLabelTooltip(category.key, option.value, option.label)"
|
||||
class="min-w-0 truncate font-semibold leading-tight"
|
||||
:class="selected ? 'text-contrast' : 'text-primary'"
|
||||
>
|
||||
@@ -179,16 +196,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BoxIcon } from '@modrinth/assets'
|
||||
import { BoxIcon, UserIcon } from '@modrinth/assets'
|
||||
import {
|
||||
buildDependentsSearchFilters,
|
||||
DropdownFilterBar,
|
||||
type DropdownFilterBarCategory,
|
||||
type DropdownFilterBarOption,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
type ProjectType,
|
||||
Tabs,
|
||||
type TabsTab,
|
||||
type TabsValue,
|
||||
truncatedTooltip,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
|
||||
import { useFormattedCountries } from '@/composables/country.ts'
|
||||
import {
|
||||
@@ -258,6 +283,8 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const {
|
||||
hasProjectContext,
|
||||
projects,
|
||||
@@ -277,6 +304,9 @@ const {
|
||||
versionPublishedDatesById,
|
||||
versionProjectNamesById,
|
||||
versionProjectIconUrlsById,
|
||||
projectNamesById,
|
||||
userNamesById,
|
||||
userAvatarUrlsById,
|
||||
getVersionDisplayName,
|
||||
} = injectAnalyticsDashboardContext()
|
||||
const formattedCountries = useFormattedCountries()
|
||||
@@ -290,6 +320,17 @@ const gameVersionTypeTabs = computed<TabsTab[]>(() => [
|
||||
{ value: 'release', label: formatMessage(analyticsMessages.releaseTab) },
|
||||
{ value: 'all', label: formatMessage(analyticsMessages.allTab) },
|
||||
])
|
||||
const dependentProjectSearchProjectTypes: readonly ProjectType[] = [
|
||||
'mod',
|
||||
'modpack',
|
||||
'resourcepack',
|
||||
'shader',
|
||||
'datapack',
|
||||
'plugin',
|
||||
]
|
||||
const dependentProjectInitialSearchLimit = 100
|
||||
const dependentProjectQuerySearchLimit = 500
|
||||
const dependentProjectSearchDebounceMs = 250
|
||||
const resolvedAddLabel = computed(
|
||||
() => props.addLabel ?? formatMessage(analyticsMessages.addButton),
|
||||
)
|
||||
@@ -306,14 +347,16 @@ const projectStatusFilterOptions = computed<DropdownFilterBarOption[]>(() =>
|
||||
})),
|
||||
)
|
||||
const selectedProjectIdSet = computed(() => new Set(selectedProjectIds.value))
|
||||
const effectiveSelectedProjectCount = computed(
|
||||
() =>
|
||||
projects.value.filter(
|
||||
const effectiveSelectedProjectIds = computed(() =>
|
||||
projects.value
|
||||
.filter(
|
||||
(project) =>
|
||||
selectedProjectIdSet.value.has(project.id) &&
|
||||
doesProjectStatusMatchFilters(project.status, selectedFilters.value),
|
||||
).length,
|
||||
)
|
||||
.map((project) => project.id),
|
||||
)
|
||||
const effectiveSelectedProjectCount = computed(() => effectiveSelectedProjectIds.value.length)
|
||||
const showProjectVersionProjectIcons = computed(() => effectiveSelectedProjectCount.value > 1)
|
||||
const defaultSelectedBreakdown = computed(() =>
|
||||
getDefaultAnalyticsBreakdownPresets(selectedProjectIds.value),
|
||||
@@ -330,12 +373,72 @@ const projectVersionFilterOptions = shallowRef<ProjectVersionFilterOption[]>([])
|
||||
const projectVersionFilterOptionProjectMetadataById = shallowRef(
|
||||
new Map<string, ProjectVersionFilterOptionProjectMetadata[]>(),
|
||||
)
|
||||
const dependentProjectSearchInput = ref('')
|
||||
const dependentProjectSearchQuery = ref('')
|
||||
const draftSelectedFilters = ref<AnalyticsSelectedFilters>(
|
||||
cloneSelectedFilters(selectedFilters.value),
|
||||
)
|
||||
let selectedFiltersCommitRequestId = 0
|
||||
let projectVersionFilterOptionsCacheKey = ''
|
||||
let projectVersionFilterOptionProjectMetadataCacheKey = ''
|
||||
let dependentProjectSearchDebounceTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
const filterOptionLabelElements = new Map<string, HTMLElement>()
|
||||
const filterOptionLabelRefUpdateToken = ref(0)
|
||||
|
||||
const dependentProjectSearchFilters = computed(() =>
|
||||
buildDependentsSearchFilters(
|
||||
dependentProjectSearchProjectTypes,
|
||||
effectiveSelectedProjectIds.value,
|
||||
),
|
||||
)
|
||||
const dependentProjectSearchQueryValue = computed(() => dependentProjectSearchQuery.value.trim())
|
||||
const dependentProjectSearchResultLimit = computed(() =>
|
||||
dependentProjectSearchQueryValue.value.length > 0
|
||||
? dependentProjectQuerySearchLimit
|
||||
: dependentProjectInitialSearchLimit,
|
||||
)
|
||||
const selectedDependentProjectIds = computed(() => [
|
||||
...new Set([
|
||||
...selectedFilters.value.dependent_project_id,
|
||||
...draftSelectedFilters.value.dependent_project_id,
|
||||
]),
|
||||
])
|
||||
const { data: dependentProjectSearchResults, error: dependentProjectSearchError } = useQuery({
|
||||
queryKey: computed(() => [
|
||||
'analytics',
|
||||
'query-filter',
|
||||
'dependent-project-search',
|
||||
dependentProjectSearchFilters.value,
|
||||
dependentProjectSearchQueryValue.value,
|
||||
dependentProjectSearchResultLimit.value,
|
||||
]),
|
||||
queryFn: () =>
|
||||
client.labrinth.projects_v2.search({
|
||||
query: dependentProjectSearchQueryValue.value || undefined,
|
||||
new_filters: dependentProjectSearchFilters.value,
|
||||
limit: dependentProjectSearchResultLimit.value,
|
||||
index: 'downloads',
|
||||
}),
|
||||
enabled: computed(
|
||||
() =>
|
||||
effectiveSelectedProjectIds.value.length > 0 &&
|
||||
dependentProjectSearchFilters.value.length > 0,
|
||||
),
|
||||
placeholderData: (previousData) => previousData,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
const { data: selectedDependentProjects } = useQuery({
|
||||
queryKey: computed(() => [
|
||||
'analytics',
|
||||
'query-filter',
|
||||
'selected-dependent-projects',
|
||||
selectedDependentProjectIds.value,
|
||||
]),
|
||||
queryFn: () => client.labrinth.projects_v2.getMultiple(selectedDependentProjectIds.value),
|
||||
enabled: computed(() => selectedDependentProjectIds.value.length > 0),
|
||||
placeholderData: [],
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const selectedFilterValue = computed<Record<string, string[]>>({
|
||||
get: () => getSelectedFilterBarValue(),
|
||||
@@ -374,6 +477,27 @@ watch(queryResetToken, () => {
|
||||
clearDownloadsThresholds()
|
||||
})
|
||||
|
||||
watch(dependentProjectSearchInput, (query) => {
|
||||
if (dependentProjectSearchDebounceTimeout) {
|
||||
clearTimeout(dependentProjectSearchDebounceTimeout)
|
||||
}
|
||||
|
||||
dependentProjectSearchDebounceTimeout = setTimeout(() => {
|
||||
dependentProjectSearchQuery.value = query.trim()
|
||||
dependentProjectSearchDebounceTimeout = null
|
||||
}, dependentProjectSearchDebounceMs)
|
||||
})
|
||||
|
||||
watch(dependentProjectSearchError, (error) => {
|
||||
if (!error) return
|
||||
|
||||
addNotification({
|
||||
title: 'Dependent projects failed to load',
|
||||
text: getDependentProjectSearchErrorMessage(error),
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
selectedFilters,
|
||||
(nextFilters, previousFilters) => {
|
||||
@@ -475,6 +599,12 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (dependentProjectSearchDebounceTimeout) {
|
||||
clearTimeout(dependentProjectSearchDebounceTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
async function scheduleSelectedFiltersCommit() {
|
||||
const requestId = ++selectedFiltersCommitRequestId
|
||||
const nextFilters = cloneSelectedFilters(draftSelectedFilters.value)
|
||||
@@ -577,6 +707,40 @@ const filterCategories = computed<DropdownFilterBarCategory[]>(() => {
|
||||
label: formatMessage(analyticsBreakdownMessages.loader),
|
||||
options: withSelectedOptions('loader_type', loaderTypeFilterOptions.value),
|
||||
},
|
||||
{
|
||||
key: 'dependent_project_id',
|
||||
label: formatMessage(analyticsBreakdownMessages.dependentProjectDownload),
|
||||
searchable: true,
|
||||
disableLocalOptionsFilter: true,
|
||||
searchPlaceholder: formatMessage(analyticsMessages.searchDependentProjectsPlaceholder),
|
||||
emptyOptionsLabel: analyticsFilterOptionsEmptyLabel.value,
|
||||
emptySearchLabel: analyticsFilterOptionsEmptyLabel.value,
|
||||
onSearchQueryChange: setDependentProjectSearchInput,
|
||||
options: withSelectedOptions('dependent_project_id', dependentProjectFilterOptions.value),
|
||||
submenuClass: 'w-fit min-w-[18rem]',
|
||||
previewDropdownWidth: 'fit-content',
|
||||
previewDropdownMinWidth: '18rem',
|
||||
},
|
||||
{
|
||||
key: 'dependent_project_type',
|
||||
label: formatMessage(analyticsBreakdownMessages.dependentProjectType),
|
||||
options: withSelectedOptions(
|
||||
'dependent_project_type',
|
||||
dependentProjectTypeFilterOptions.value,
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'user_id',
|
||||
label: formatMessage(analyticsBreakdownMessages.members),
|
||||
searchable: memberFilterOptions.value.length > 6,
|
||||
searchPlaceholder: formatMessage(analyticsMessages.searchMembersPlaceholder),
|
||||
emptyOptionsLabel: analyticsFilterOptionsEmptyLabel.value,
|
||||
emptySearchLabel: analyticsFilterOptionsEmptyLabel.value,
|
||||
options: withSelectedOptions('user_id', memberFilterOptions.value),
|
||||
submenuClass: 'w-fit min-w-[14rem]',
|
||||
previewDropdownWidth: 'fit-content',
|
||||
previewDropdownMinWidth: '14rem',
|
||||
},
|
||||
)
|
||||
|
||||
return categories.filter((category) =>
|
||||
@@ -636,6 +800,16 @@ const downloadReasonFilterOptions = computed<DropdownFilterBarOption[]>(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
const memberFilterOptions = computed<DropdownFilterBarOption[]>(() =>
|
||||
filterOptions.value.userIds
|
||||
.map((userId) => ({
|
||||
value: userId,
|
||||
label: getUserFilterOptionLabel(userId),
|
||||
searchTerms: [userId],
|
||||
}))
|
||||
.sort((left, right) => left.label.localeCompare(right.label)),
|
||||
)
|
||||
|
||||
const gameVersionFilterOptions = computed<DropdownFilterBarOption[]>(() =>
|
||||
filterOptions.value.gameVersions
|
||||
.filter((gameVersion) => {
|
||||
@@ -668,6 +842,45 @@ const loaderTypeFilterOptions = computed<DropdownFilterBarOption[]>(() =>
|
||||
.sort((left, right) => left.label.localeCompare(right.label)),
|
||||
)
|
||||
|
||||
const dependentProjectSearchResultsById = computed(
|
||||
() =>
|
||||
new Map(
|
||||
(dependentProjectSearchResults.value?.hits ?? []).map((project) => [
|
||||
project.project_id,
|
||||
project,
|
||||
]),
|
||||
),
|
||||
)
|
||||
const selectedDependentProjectsById = computed(
|
||||
() => new Map((selectedDependentProjects.value ?? []).map((project) => [project.id, project])),
|
||||
)
|
||||
|
||||
const dependentProjectFilterOptions = computed<DropdownFilterBarOption[]>(() => {
|
||||
const optionsById = new Map<string, DropdownFilterBarOption>()
|
||||
|
||||
for (const project of dependentProjectSearchResults.value?.hits ?? []) {
|
||||
optionsById.set(project.project_id, {
|
||||
value: project.project_id,
|
||||
label: project.title,
|
||||
searchTerms: [project.project_id, project.slug, project.author].filter(
|
||||
(term): term is string => Boolean(term),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return [...optionsById.values()]
|
||||
})
|
||||
|
||||
const dependentProjectTypeFilterOptions = computed<DropdownFilterBarOption[]>(() =>
|
||||
filterOptions.value.dependentProjectTypes
|
||||
.map((projectType) => ({
|
||||
value: projectType,
|
||||
label: getProjectTypeFilterOptionLabel(projectType),
|
||||
searchTerms: [projectType],
|
||||
}))
|
||||
.sort((left, right) => left.label.localeCompare(right.label)),
|
||||
)
|
||||
|
||||
function isAnalyticsFilterValueCategory(
|
||||
categoryKey: string,
|
||||
): categoryKey is AnalyticsFilterValueCategory {
|
||||
@@ -697,15 +910,77 @@ function getMissingSelectedOptionLabel(
|
||||
if (categoryKey === 'download_reason') {
|
||||
return getDownloadReasonFilterOptionLabel
|
||||
}
|
||||
if (categoryKey === 'user_id') {
|
||||
return getUserFilterOptionLabel
|
||||
}
|
||||
if (categoryKey === 'user_agent') {
|
||||
return (value) => getDownloadSourceLabel(value, formatMessage)
|
||||
}
|
||||
if (categoryKey === 'loader_type') {
|
||||
return getLoaderTypeFilterOptionLabel
|
||||
}
|
||||
if (categoryKey === 'dependent_project_id') {
|
||||
return getDependentProjectFilterOptionLabel
|
||||
}
|
||||
if (categoryKey === 'dependent_project_type') {
|
||||
return getProjectTypeFilterOptionLabel
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getUserAvatarUrl(userId: string): string | undefined {
|
||||
return userAvatarUrlsById.value.get(userId)
|
||||
}
|
||||
|
||||
function setFilterOptionLabelRef(
|
||||
categoryKey: string,
|
||||
optionValue: string,
|
||||
element: Element | ComponentPublicInstance | null,
|
||||
) {
|
||||
const key = getFilterOptionLabelElementKey(categoryKey, optionValue)
|
||||
if (element instanceof HTMLElement) {
|
||||
if (filterOptionLabelElements.get(key) === element) {
|
||||
return
|
||||
}
|
||||
|
||||
filterOptionLabelElements.set(key, element)
|
||||
filterOptionLabelRefUpdateToken.value++
|
||||
return
|
||||
}
|
||||
|
||||
if (filterOptionLabelElements.delete(key)) {
|
||||
filterOptionLabelRefUpdateToken.value++
|
||||
}
|
||||
}
|
||||
|
||||
function getFilterOptionLabelTooltip(
|
||||
categoryKey: string,
|
||||
optionValue: string,
|
||||
label: string,
|
||||
): string | undefined {
|
||||
void filterOptionLabelRefUpdateToken.value
|
||||
|
||||
return truncatedTooltip(
|
||||
filterOptionLabelElements.get(getFilterOptionLabelElementKey(categoryKey, optionValue)),
|
||||
label,
|
||||
)
|
||||
}
|
||||
|
||||
function getFilterOptionLabelElementKey(categoryKey: string, optionValue: string) {
|
||||
return `${categoryKey}\x1f${optionValue}`
|
||||
}
|
||||
|
||||
function getFilterOptionProjectMetadata(categoryKey: string, optionValue: string) {
|
||||
if (categoryKey === 'version_id') {
|
||||
return getProjectVersionOptionProjectMetadata(optionValue)
|
||||
}
|
||||
if (categoryKey === 'dependent_project_id') {
|
||||
return getDependentProjectOptionProjectMetadata(optionValue)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function getProjectVersionOptionProjectMetadata(versionId: string) {
|
||||
if (!showProjectVersionProjectIcons.value) {
|
||||
return []
|
||||
@@ -714,6 +989,24 @@ function getProjectVersionOptionProjectMetadata(versionId: string) {
|
||||
return projectVersionFilterOptionProjectMetadataById.value.get(versionId) ?? []
|
||||
}
|
||||
|
||||
function getDependentProjectOptionProjectMetadata(projectId: string) {
|
||||
const searchResult = dependentProjectSearchResultsById.value.get(projectId)
|
||||
const selectedProject = selectedDependentProjectsById.value.get(projectId)
|
||||
const name =
|
||||
searchResult?.title ??
|
||||
selectedProject?.title ??
|
||||
projectNamesById.value.get(projectId) ??
|
||||
projectId
|
||||
const iconUrl = searchResult?.icon_url ?? selectedProject?.icon_url
|
||||
|
||||
return [
|
||||
{
|
||||
name,
|
||||
...(iconUrl ? { iconUrl } : {}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function getCountryFilterOptionLabel(countryCode: string): string {
|
||||
const normalizedCode = countryCode.trim().toUpperCase()
|
||||
if (normalizedCode === 'XX') {
|
||||
@@ -731,10 +1024,51 @@ function getLoaderTypeFilterOptionLabel(loaderType: string): string {
|
||||
return formatAnalyticsLoaderLabel(loaderType, formatMessage)
|
||||
}
|
||||
|
||||
function getProjectTypeFilterOptionLabel(projectType: string): string {
|
||||
return formatProjectType(projectType)
|
||||
}
|
||||
|
||||
function getDependentProjectFilterOptionLabel(projectId: string): string {
|
||||
return (
|
||||
dependentProjectSearchResultsById.value.get(projectId)?.title ??
|
||||
selectedDependentProjectsById.value.get(projectId)?.title ??
|
||||
projectNamesById.value.get(projectId) ??
|
||||
projectId
|
||||
)
|
||||
}
|
||||
|
||||
function getDownloadReasonFilterOptionLabel(reason: string): string {
|
||||
return formatAnalyticsDownloadReasonLabel(reason, formatMessage)
|
||||
}
|
||||
|
||||
function getUserFilterOptionLabel(userId: string): string {
|
||||
return userNamesById.value.get(userId) ?? userId
|
||||
}
|
||||
|
||||
function setDependentProjectSearchInput(query: string) {
|
||||
dependentProjectSearchInput.value = query
|
||||
}
|
||||
|
||||
function getDependentProjectSearchErrorMessage(error: unknown): string {
|
||||
if (error && typeof error === 'object') {
|
||||
const dataDescription = (error as { data?: { description?: unknown } }).data?.description
|
||||
if (typeof dataDescription === 'string' && dataDescription.length > 0) {
|
||||
return dataDescription
|
||||
}
|
||||
|
||||
const message = (error as { message?: unknown }).message
|
||||
if (typeof message === 'string' && message.length > 0) {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof error === 'string' && error.length > 0) {
|
||||
return error
|
||||
}
|
||||
|
||||
return 'Please try searching again or changing the selected projects.'
|
||||
}
|
||||
|
||||
function getDateTimestamp(date: string | undefined): number | undefined {
|
||||
if (!date) {
|
||||
return undefined
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
@keydown.enter.stop
|
||||
@keydown.space.stop
|
||||
>
|
||||
<LayersIcon
|
||||
<UserIcon
|
||||
class="h-5 w-5 shrink-0 text-primary"
|
||||
:class="isUserProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
|
||||
/>
|
||||
@@ -290,7 +290,7 @@
|
||||
@keydown.enter.stop
|
||||
@keydown.space.stop
|
||||
>
|
||||
<LayersIcon
|
||||
<UserIcon
|
||||
class="h-5 w-5 shrink-0 text-primary"
|
||||
:class="isUserProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
|
||||
/>
|
||||
@@ -449,6 +449,7 @@ import {
|
||||
ClockIcon,
|
||||
FolderOpenIcon,
|
||||
LayersIcon,
|
||||
UserIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
@@ -499,7 +500,7 @@ import {
|
||||
import TimeFramePicker from './TimeframePicker.vue'
|
||||
|
||||
const QUERY_BUILDER_DROPDOWN_MAX_HEIGHT = 500
|
||||
const QUERY_BUILDER_DROPDOWN_MIN_WIDTH = '12rem'
|
||||
const QUERY_BUILDER_DROPDOWN_MIN_WIDTH = '14rem'
|
||||
const analyticsQueryChipTriggerClass = 'h-10 '
|
||||
const analyticsQueryAddFilterButtonClass = '!h-10 max-w-full !w-max !px-3.5 flex !gap-2'
|
||||
const projectOptionCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
|
||||
@@ -1008,15 +1009,34 @@ const breakdownOptions = computed<MultiSelectOption<Exclude<AnalyticsBreakdownPr
|
||||
value: 'game_version',
|
||||
label: formatAnalyticsBreakdownLabel('game_version', formatMessage),
|
||||
},
|
||||
{
|
||||
value: 'dependent_project_download',
|
||||
label: formatAnalyticsBreakdownLabel('dependent_project_download', formatMessage),
|
||||
},
|
||||
{
|
||||
value: 'user_id',
|
||||
label: formatAnalyticsBreakdownLabel('user_id', formatMessage),
|
||||
},
|
||||
)
|
||||
|
||||
return options.map((option) => ({
|
||||
...option,
|
||||
disabled: hasReachedBreakdownLimit && !selectedBreakdownSet.has(option.value),
|
||||
}))
|
||||
return options.map((option) => {
|
||||
const isSelected = selectedBreakdownSet.has(option.value)
|
||||
return {
|
||||
...option,
|
||||
disabled:
|
||||
!isSelected && (hasReachedBreakdownLimit || !canSelectBreakdownOption(option.value)),
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
function canSelectBreakdownOption(breakdown: Exclude<AnalyticsBreakdownPreset, 'none'>): boolean {
|
||||
return getAnalyticsBreakdownPresetsForProjectSelection(
|
||||
[...selectedBreakdownValue.value, breakdown],
|
||||
selectedProjectIds.value,
|
||||
).includes(breakdown)
|
||||
}
|
||||
|
||||
function getBreakdownOptionLabel(breakdown: Exclude<AnalyticsBreakdownPreset, 'none'>): string {
|
||||
return (
|
||||
breakdownOptions.value.find((option) => option.value === breakdown)?.label ??
|
||||
@@ -1136,6 +1156,17 @@ function withBreakdownFields(
|
||||
downloads.push('reason')
|
||||
}
|
||||
break
|
||||
case 'user_id':
|
||||
if (includesStat(breakdownStats, 'revenue') && includesStat(enabledStats, 'revenue')) {
|
||||
revenue.push('user_id')
|
||||
}
|
||||
break
|
||||
case 'dependent_project_download':
|
||||
if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
|
||||
downloads.push('dependent_project_id')
|
||||
downloads.push('reason')
|
||||
}
|
||||
break
|
||||
case 'version_id':
|
||||
if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
|
||||
downloads.push('version_id')
|
||||
@@ -1264,6 +1295,12 @@ function buildMetricFilters(
|
||||
filters.game_version,
|
||||
),
|
||||
loader: getFilterValuesForStat('loader_type', 'downloads', enabledStats, filters.loader_type),
|
||||
dependent_project_id: getFilterValuesForStat(
|
||||
'dependent_project_id',
|
||||
'downloads',
|
||||
enabledStats,
|
||||
filters.dependent_project_id,
|
||||
),
|
||||
},
|
||||
playtime: {
|
||||
country: getFilterValuesForStat('country', 'playtime', enabledStats, filters.country),
|
||||
@@ -1281,7 +1318,9 @@ function buildMetricFilters(
|
||||
),
|
||||
loader: getFilterValuesForStat('loader_type', 'playtime', enabledStats, filters.loader_type),
|
||||
},
|
||||
revenue: {},
|
||||
revenue: {
|
||||
user_id: getFilterValuesForStat('user_id', 'revenue', enabledStats, filters.user_id),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1295,6 +1334,15 @@ const fetchRequest = computed<Labrinth.Analytics.v3.FetchRequest>(() => {
|
||||
|
||||
const bucketBy = withBreakdownFields(selectedBreakdowns.value, selectedFilters.value)
|
||||
const filterBy = buildMetricFilters(selectedBreakdowns.value, selectedFilters.value)
|
||||
if (
|
||||
includesStat(
|
||||
getEnabledAnalyticsStatsForState(selectedBreakdowns.value, selectedFilters.value),
|
||||
'revenue',
|
||||
) &&
|
||||
!bucketBy.revenue.includes('user_id')
|
||||
) {
|
||||
bucketBy.revenue.push('user_id')
|
||||
}
|
||||
const filteredProjectIds = getProjectIdsMatchingStatusFilter(
|
||||
selectedProjectIds.value,
|
||||
projectStatusById.value,
|
||||
|
||||
+68
-6
@@ -2,6 +2,7 @@ import type {
|
||||
AnalyticsBreakdownPreset,
|
||||
AnalyticsDashboardStat,
|
||||
AnalyticsQueryFilterCategory,
|
||||
AnalyticsSelectedBreakdowns,
|
||||
AnalyticsSelectedFilters,
|
||||
} from '~/providers/analytics/analytics'
|
||||
|
||||
@@ -13,6 +14,10 @@ export type AnalyticsDashboardDimension =
|
||||
| 'monetization'
|
||||
| 'user_agent'
|
||||
| 'download_reason'
|
||||
| 'user_id'
|
||||
| 'dependent_project_download'
|
||||
| 'dependent_project_id'
|
||||
| 'dependent_project_type'
|
||||
| 'game_version'
|
||||
| 'loader_type'
|
||||
|
||||
@@ -23,9 +28,12 @@ export const FILTER_VALUE_CATEGORIES: Exclude<AnalyticsQueryFilterCategory, 'pro
|
||||
'monetization',
|
||||
'user_agent',
|
||||
'download_reason',
|
||||
'user_id',
|
||||
'version_id',
|
||||
'game_version',
|
||||
'loader_type',
|
||||
'dependent_project_id',
|
||||
'dependent_project_type',
|
||||
]
|
||||
|
||||
const ANALYTICS_DASHBOARD_STAT_ORDER: AnalyticsDashboardStat[] = [
|
||||
@@ -45,8 +53,12 @@ const ANALYTICS_STATS_BY_DIMENSION: Record<
|
||||
monetization: ['views', 'downloads'],
|
||||
user_agent: ['downloads'],
|
||||
download_reason: ['downloads'],
|
||||
user_id: ['revenue'],
|
||||
dependent_project_download: ['downloads'],
|
||||
dependent_project_type: ['downloads'],
|
||||
game_version: ['downloads', 'playtime'],
|
||||
loader_type: ['downloads', 'playtime'],
|
||||
dependent_project_id: ['downloads'],
|
||||
project_status: ANALYTICS_DASHBOARD_STAT_ORDER,
|
||||
}
|
||||
|
||||
@@ -60,6 +72,8 @@ const ANALYTICS_DIMENSION_BY_BREAKDOWN: Record<
|
||||
monetization: 'monetization',
|
||||
user_agent: 'user_agent',
|
||||
download_reason: 'download_reason',
|
||||
user_id: 'user_id',
|
||||
dependent_project_download: 'dependent_project_download',
|
||||
version_id: 'version_id',
|
||||
loader: 'loader_type',
|
||||
game_version: 'game_version',
|
||||
@@ -74,9 +88,12 @@ const ANALYTICS_DIMENSION_BY_FILTER_CATEGORY: Record<
|
||||
monetization: 'monetization',
|
||||
user_agent: 'user_agent',
|
||||
download_reason: 'download_reason',
|
||||
user_id: 'user_id',
|
||||
version_id: 'version_id',
|
||||
game_version: 'game_version',
|
||||
loader_type: 'loader_type',
|
||||
dependent_project_id: 'dependent_project_id',
|
||||
dependent_project_type: 'dependent_project_type',
|
||||
}
|
||||
|
||||
const ANALYTICS_FILTER_CATEGORY_BY_BREAKDOWN: Record<
|
||||
@@ -89,6 +106,8 @@ const ANALYTICS_FILTER_CATEGORY_BY_BREAKDOWN: Record<
|
||||
monetization: 'monetization',
|
||||
user_agent: 'user_agent',
|
||||
download_reason: 'download_reason',
|
||||
user_id: 'user_id',
|
||||
dependent_project_download: null,
|
||||
version_id: 'version_id',
|
||||
loader: 'loader_type',
|
||||
game_version: 'game_version',
|
||||
@@ -286,6 +305,33 @@ export function getAnalyticsStatsForBreakdowns(
|
||||
return stats
|
||||
}
|
||||
|
||||
export function getAnalyticsBreakdownsWithSharedStats(
|
||||
breakdowns: AnalyticsBreakdownInput,
|
||||
): AnalyticsSelectedBreakdowns {
|
||||
const normalizedBreakdowns = normalizeAnalyticsBreakdowns(breakdowns)
|
||||
const compatibleBreakdowns: AnalyticsSelectedBreakdowns = []
|
||||
let sharedStats: readonly AnalyticsDashboardStat[] | null = null
|
||||
|
||||
for (const breakdown of normalizedBreakdowns) {
|
||||
const breakdownStats = getAnalyticsStatsForBreakdown(breakdown)
|
||||
if (sharedStats === null) {
|
||||
compatibleBreakdowns.push(breakdown)
|
||||
sharedStats = breakdownStats
|
||||
continue
|
||||
}
|
||||
|
||||
const nextSharedStats = intersectAnalyticsStats(sharedStats, breakdownStats)
|
||||
if (nextSharedStats.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
compatibleBreakdowns.push(breakdown)
|
||||
sharedStats = nextSharedStats
|
||||
}
|
||||
|
||||
return compatibleBreakdowns
|
||||
}
|
||||
|
||||
export function getAnalyticsStatsForFilterCategory(
|
||||
category: AnalyticsQueryFilterCategory,
|
||||
): readonly AnalyticsDashboardStat[] {
|
||||
@@ -331,12 +377,20 @@ export function getVisibleAnalyticsFilterCategoriesForState(
|
||||
breakdowns: AnalyticsBreakdownInput,
|
||||
filters: AnalyticsSelectedFilters,
|
||||
): readonly Exclude<AnalyticsQueryFilterCategory, 'project'>[] {
|
||||
return FILTER_VALUE_CATEGORIES.filter((category) =>
|
||||
haveAnalyticsStatOverlap(
|
||||
const normalizedBreakdowns = normalizeAnalyticsBreakdowns(breakdowns)
|
||||
return FILTER_VALUE_CATEGORIES.filter((category) => {
|
||||
if (
|
||||
category === 'dependent_project_type' &&
|
||||
!normalizedBreakdowns.includes('dependent_project_download')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return haveAnalyticsStatOverlap(
|
||||
getAnalyticsStatsForFilterScope(breakdowns, filters, category),
|
||||
getAnalyticsStatsForFilterCategory(category),
|
||||
),
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function sanitizeAnalyticsSelectedFilters(
|
||||
@@ -345,6 +399,7 @@ export function sanitizeAnalyticsSelectedFilters(
|
||||
): AnalyticsSelectedFilters {
|
||||
const nextFilters = cloneSelectedFilters(filters)
|
||||
let availableStats = [...getAnalyticsStatsForBreakdowns(breakdowns)]
|
||||
const normalizedBreakdowns = normalizeAnalyticsBreakdowns(breakdowns)
|
||||
|
||||
for (const category of FILTER_VALUE_CATEGORIES) {
|
||||
if (filters[category].length === 0) {
|
||||
@@ -352,7 +407,11 @@ export function sanitizeAnalyticsSelectedFilters(
|
||||
}
|
||||
|
||||
const categoryStats = getAnalyticsStatsForFilterCategory(category)
|
||||
if (!haveAnalyticsStatOverlap(availableStats, categoryStats)) {
|
||||
if (
|
||||
!haveAnalyticsStatOverlap(availableStats, categoryStats) ||
|
||||
(category === 'dependent_project_type' &&
|
||||
!normalizedBreakdowns.includes('dependent_project_download'))
|
||||
) {
|
||||
nextFilters[category] = []
|
||||
continue
|
||||
}
|
||||
@@ -371,9 +430,12 @@ export function cloneSelectedFilters(filters: AnalyticsSelectedFilters): Analyti
|
||||
monetization: [...filters.monetization],
|
||||
user_agent: [...filters.user_agent],
|
||||
download_reason: [...filters.download_reason],
|
||||
user_id: [...filters.user_id],
|
||||
version_id: [...filters.version_id],
|
||||
game_version: [...filters.game_version],
|
||||
loader_type: [...filters.loader_type],
|
||||
dependent_project_id: [...filters.dependent_project_id],
|
||||
dependent_project_type: [...filters.dependent_project_type],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +518,7 @@ export function normalizeSelectedValues(
|
||||
.map((value) => value.trim().toLowerCase())
|
||||
.filter(isProjectStatusFilterValue)
|
||||
}
|
||||
if (categoryKey === 'loader_type') {
|
||||
if (categoryKey === 'loader_type' || categoryKey === 'dependent_project_type') {
|
||||
return Array.from(
|
||||
new Set(
|
||||
selectedValues
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
areStringArraysEqual,
|
||||
buildAnalyticsQueryBuilderRouteQuery,
|
||||
getAnalyticsBreakdownPresetsForProjectSelection,
|
||||
hasAnalyticsAllProjectSelectionQuery,
|
||||
hasAnalyticsQueryBuilderRouteChange,
|
||||
readAnalyticsGraphState,
|
||||
readAnalyticsQueryBuilderState,
|
||||
@@ -54,6 +55,7 @@ export interface UseAnalyticsRouteSyncOptions {
|
||||
graph: AnalyticsGraphRefs
|
||||
availableProjectIds: Ref<string[]>
|
||||
defaultProjectIds: Ref<string[]>
|
||||
areProjectsLoaded: Ref<boolean>
|
||||
sanitizeSelectedFilters: (
|
||||
breakdowns: readonly AnalyticsBreakdownPreset[],
|
||||
filters: AnalyticsSelectedFilters,
|
||||
@@ -61,8 +63,14 @@ export interface UseAnalyticsRouteSyncOptions {
|
||||
}
|
||||
|
||||
export function useAnalyticsRouteSync(options: UseAnalyticsRouteSyncOptions) {
|
||||
const { queryBuilder, graph, availableProjectIds, defaultProjectIds, sanitizeSelectedFilters } =
|
||||
options
|
||||
const {
|
||||
queryBuilder,
|
||||
graph,
|
||||
availableProjectIds,
|
||||
defaultProjectIds,
|
||||
areProjectsLoaded,
|
||||
sanitizeSelectedFilters,
|
||||
} = options
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -113,6 +121,10 @@ export function useAnalyticsRouteSync(options: UseAnalyticsRouteSyncOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hasAnalyticsAllProjectSelectionQuery(route.query) && !areProjectsLoaded.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextRouteQuery = buildAnalyticsQueryBuilderRouteQuery(
|
||||
route.query,
|
||||
getSelectedAnalyticsQueryBuilderState(),
|
||||
@@ -147,6 +159,10 @@ export function useAnalyticsRouteSync(options: UseAnalyticsRouteSyncOptions) {
|
||||
}
|
||||
|
||||
function applyRouteQueryToState(nextQuery: LocationQuery) {
|
||||
if (hasAnalyticsAllProjectSelectionQuery(nextQuery) && !areProjectsLoaded.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextQueryState = readAnalyticsQueryBuilderState(
|
||||
nextQuery,
|
||||
availableProjectIds.value,
|
||||
|
||||
@@ -134,18 +134,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Alle {count} Linien im Diagramm anzeigen?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Zeige alle {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Download-Quelle} other {Download-Quellen}}} downloadReason {{count, plural, one {Download-Grund} other {Download-Gründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Element} other {Elemente}}}} aus der Tabelle an"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "Zeige {count} {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Download-Quelle} other {Download-Quellen}}} downloadReason {{count, plural, one {Download-Grund} other {Download-Gründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Element} other {Elemente}}}} aus der Tabelle an"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Zeige {limit} {itemType, select, project {{limit, plural, one {Projekt} other {Projekte}}} country {{limit, plural, one {Land} other {Länder}}} monetization {{limit, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{limit, plural, one {Download-Quelle} other {Download-Quellen}}} downloadReason {{limit, plural, one {Download-Grund} other {Download-Gründe}}} projectVersion {{limit, plural, one {Projektversion} other {Projektversionen}}} loader {{limit, plural, one {Loader} other {Loader}}} gameVersion {{limit, plural, one {Spielversion} other {Spielversionen}}} other {{limit, plural, one {Element} other {Elemente}}}} aus der Tabelle an"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Zeige die Top {count} {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Download-Quelle} other {Download-Quellen}}} downloadReason {{count, plural, one {Download-Grund} other {Download-Gründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Element} other {Elemente}}}} aus der Tabelle an"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# Tag} other {# Tage}}"
|
||||
},
|
||||
|
||||
@@ -134,18 +134,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Alle {count} Zeilen im Graphen anzeigen?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Zeige alle {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Downloadquelle} other {Downloadquellen}}} downloadReason {{count, plural, one {Downloadgrund} other {Downloadgründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Artikel} other {Artikel}}}} aus der Tabelle"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "Zeige {count} {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Downloadquelle} other {Downloadquellen}}} downloadReason {{count, plural, one {Downloadgrund} other {Downloadgründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Artikel} other {Artikel}}}} aus der Tabelle"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Zeige {limit} {itemType, select, project {{limit, plural, one {Projekt} other {Projekte}}} country {{limit, plural, one {Land} other {Länder}}} monetization {{limit, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{limit, plural, one {Downloadquelle} other {Downloadquellen}}} downloadReason {{limit, plural, one {Downloadgrund} other {Downloadgründe}}} projectVersion {{limit, plural, one {Projektversion} other {Projektversionen}}} loader {{limit, plural, one {Loader} other {Loader}}} gameVersion {{limit, plural, one {Spielversion} other {Spielversionen}}} other {{limit, plural, one {Artikel} other {Artikel}}}} aus der Tabelle"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Zeige die top {count} {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Downloadquelle} other {Downloadquellen}}} downloadReason {{count, plural, one {Downloadgrund} other {Downloadgründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Artikel} other {Artikel}}}} aus der Tabelle"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# Tag} other {# Tage}}"
|
||||
},
|
||||
|
||||
@@ -23,6 +23,15 @@
|
||||
"analytics.breakdown.country": {
|
||||
"message": "Country"
|
||||
},
|
||||
"analytics.breakdown.dependent-on": {
|
||||
"message": "Dependent on"
|
||||
},
|
||||
"analytics.breakdown.dependent-project-download": {
|
||||
"message": "Dependent project"
|
||||
},
|
||||
"analytics.breakdown.dependent-project-type": {
|
||||
"message": "Dependent project type"
|
||||
},
|
||||
"analytics.breakdown.download-reason": {
|
||||
"message": "Download reason"
|
||||
},
|
||||
@@ -38,6 +47,9 @@
|
||||
"analytics.breakdown.loader": {
|
||||
"message": "Loader"
|
||||
},
|
||||
"analytics.breakdown.members": {
|
||||
"message": "Members"
|
||||
},
|
||||
"analytics.breakdown.monetization": {
|
||||
"message": "Monetization"
|
||||
},
|
||||
@@ -135,16 +147,22 @@
|
||||
"message": "Show all {count} lines in graph?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Showing all {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table"
|
||||
"message": "Showing all {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} member {{count, plural, one {member} other {members}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "Showing {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table"
|
||||
"message": "Showing {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} member {{count, plural, one {member} other {members}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Showing {limit} {itemType, select, project {{limit, plural, one {project} other {projects}}} country {{limit, plural, one {country} other {countries}}} monetization {{limit, plural, one {monetization value} other {monetization values}}} downloadSource {{limit, plural, one {download source} other {download sources}}} downloadReason {{limit, plural, one {download reason} other {download reasons}}} projectVersion {{limit, plural, one {project version} other {project versions}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {game version} other {game versions}}} other {{limit, plural, one {item} other {items}}}} from table"
|
||||
"message": "Showing {limit} {itemType, select, project {{limit, plural, one {project} other {projects}}} country {{limit, plural, one {country} other {countries}}} monetization {{limit, plural, one {monetization value} other {monetization values}}} downloadSource {{limit, plural, one {download source} other {download sources}}} downloadReason {{limit, plural, one {download reason} other {download reasons}}} member {{limit, plural, one {member} other {members}}} projectVersion {{limit, plural, one {project version} other {project versions}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {game version} other {game versions}}} other {{limit, plural, one {item} other {items}}}} from table"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Showing top {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table"
|
||||
"message": "Showing top {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} member {{count, plural, one {member} other {members}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table"
|
||||
},
|
||||
"analytics.chart.tooltip.dependent-on-project": {
|
||||
"message": "Dependent on {project}"
|
||||
},
|
||||
"analytics.chart.tooltip.dependent-project-version": {
|
||||
"message": "{dependentProject} dependent on {dependencyProject}, {version}"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# day} other {# days}}"
|
||||
@@ -230,9 +248,15 @@
|
||||
"analytics.filter.search.countries": {
|
||||
"message": "Search countries..."
|
||||
},
|
||||
"analytics.filter.search.dependent-projects": {
|
||||
"message": "Search projects..."
|
||||
},
|
||||
"analytics.filter.search.download-sources": {
|
||||
"message": "Search download sources..."
|
||||
},
|
||||
"analytics.filter.search.members": {
|
||||
"message": "Search members..."
|
||||
},
|
||||
"analytics.filter.search.project-versions": {
|
||||
"message": "Search project versions..."
|
||||
},
|
||||
@@ -479,6 +503,12 @@
|
||||
"analytics.value.monetized": {
|
||||
"message": "Monetized"
|
||||
},
|
||||
"analytics.value.no-dependent": {
|
||||
"message": "No dependents"
|
||||
},
|
||||
"analytics.value.no-dependent-tooltip": {
|
||||
"message": "Downloaded for reasons other than being a dependency"
|
||||
},
|
||||
"analytics.value.none": {
|
||||
"message": "None"
|
||||
},
|
||||
|
||||
@@ -134,15 +134,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "¿Mostrar todas las {count} líneas en el gráfico?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Mostrando {itemType, select, project {{count, plural, one {el proyecto} other {todos los # proyectos}}} country {{count, plural, one {el país} other {todos los # países}}} monetization {{count, plural, one {el valor de monetización} other {todos los valores de monetización}}} downloadSource {{count, plural, one {la fuente de descarga} other {todas las # fuentes de descarga}}} downloadReason {{count, plural, one {la razón de descarga} other {todas las # razones de descarga}}} projectVersion {{count, plural, one {la versión del proyecto} other {todas las # versiones del proyecto}}} loader {{count, plural, one {el loader} other {todos los # loaders}}} gameVersion {{count, plural, one {la versión del juego} other {todas las # versiones del juego}}} other {{count, plural, one {el ítem} other {todos los # ítems}}}} de la tabla"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Mostrando {limit} {itemType, select, project {{limit, plural, one {proyecto} other {proyectos}}} country {{limit, plural, one {país} other {países}}} monetization {{limit, plural, one {valor de monetización} other {valores de monetización}}} downloadSource {{limit, plural, one {fuente de descarga} other {fuentes de descarga}}} downloadReason {{limit, plural, one {razón de descarga} other {razones de descarga}}} projectVersion {{limit, plural, one {versión del proyecto} other {versiones del proyecto}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {version de juego} other {versiones de juego}}} other {{limit, plural, one {ítem} other {ítems}}}} de la tabla"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Mostrando el top {count} {itemType, select, project {{count, plural, one {del proyecto} other {de todos los # proyectos}}} country {{count, plural, one {del país} other {de todos los # países}}} monetization {{count, plural, one {del valor de monetización} other {de todos los valores de monetización}}} downloadSource {{count, plural, one {de la fuente de descarga} other {de todas las # fuentes de descarga}}} downloadReason {{count, plural, one {de la razón de descarga} other {de todas las # razones de descarga}}} projectVersion {{count, plural, one {de la versión del proyecto} other {de todas las # versiones del proyecto}}} loader {{count, plural, one {del loader} other {de todos los # loaders}}} gameVersion {{count, plural, one {de la versión del juego} other {de todas las # versiones del juego}}} other {{count, plural, one {del ítem} other {de todos los # ítems}}}} de la tabla"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# día} other {# días}}"
|
||||
},
|
||||
|
||||
@@ -134,15 +134,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "¿Mostrar todas las {count} líneas en el gráfico?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Mostrando {itemType, select, project {{count, plural, one {el proyecto} other {todos los # proyectos}}} country {{count, plural, one {el país} other {todos los # países}}} monetization {{count, plural, one {el valor de monetización} other {todos los valores de monetización}}} downloadSource {{count, plural, one {la fuente de descarga} other {todas las # fuentes de descarga}}} downloadReason {{count, plural, one {la razón de descarga} other {todas las # razones de descarga}}} projectVersion {{count, plural, one {la versión del proyecto} other {todas las # versiones del proyecto}}} loader {{count, plural, one {el loader} other {todos los # loaders}}} gameVersion {{count, plural, one {la versión del juego} other {todas las # versiones del juego}}} other {{count, plural, one {el ítem} other {todos los # ítems}}}} de la tabla"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Mostrando {limit} {itemType, select, project {{limit, plural, one {proyecto} other {proyectos}}} country {{limit, plural, one {país} other {países}}} monetization {{limit, plural, one {valor de monetización} other {valores de monetización}}} downloadSource {{limit, plural, one {fuente de descarga} other {fuentes de descarga}}} downloadReason {{limit, plural, one {razón de descarga} other {razones de descarga}}} projectVersion {{limit, plural, one {versión del proyecto} other {versiones del proyecto}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {version de juego} other {versiones de juego}}} other {{limit, plural, one {ítem} other {ítems}}}} de la tabla"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Mostrando el top {count} {itemType, select, project {{count, plural, one {del proyecto} other {de todos los # proyectos}}} country {{count, plural, one {del país} other {de todos los # países}}} monetization {{count, plural, one {del valor de monetización} other {de todos los valores de monetización}}} downloadSource {{count, plural, one {de la fuente de descarga} other {de todas las # fuentes de descarga}}} downloadReason {{count, plural, one {de la razón de descarga} other {de todas las # razones de descarga}}} projectVersion {{count, plural, one {de la versión del proyecto} other {de todas las # versiones del proyecto}}} loader {{count, plural, one {del loader} other {de todos los # loaders}}} gameVersion {{count, plural, one {de la versión del juego} other {de todas las # versiones del juego}}} other {{count, plural, one {del ítem} other {de todos los # ítems}}}} de la tabla"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# día} other {# días}}"
|
||||
},
|
||||
|
||||
@@ -134,12 +134,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Afficher les {count} lignes sur le graphique ?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Montrer {itemType, select, project {{count, plural, one {1 projet} other {tous les projets}}} country {{count, plural, one {1 pays} other {tous les pays}}} monetization {{count, plural, one {1 valeur de monétisation} other {toutes les valeurs de monétisation}}} downloadSource {{count, plural, one {1 source de téléchargement} other {toutes les sources de téléchargement}}} downloadReason {{count, plural, one {1 raison de téléchargement} other {toutes les raisons de téléchargement}}} projectVersion {{count, plural, one {1 version de projet} other {toutes les versions de projets}}} loader {{count, plural, one {1 loader} other {tous les loaders}}} gameVersion {{count, plural, one {1 version du jeu} other {toutes les version versions du jeu}}} other {{count, plural, one {l'objet} other {tous les objets}}}} du tableau"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "Affichage de {count} {itemType, select, project {{count, plural, one {projet} other {projets}}} country {{count, plural, one {pays} other {pays}}} monetization {{count, plural, one {valeur de monétisation} other {valeurs de monétisation}}} downloadSource {{count, plural, one {source de téléchargement} other {sources de téléchargement}}} downloadReason {{count, plural, one {raison de téléchargement} other {raisons de téléchargement}}} projectVersion {{count, plural, one {version de projet} other {versions de projet}}} loader {{count, plural, one {chargeur} other {chargeurs}}} gameVersion {{count, plural, one {version de jeu} other {versions de jeu}}} other {{count, plural, one {élément} other {éléments}}}} du tableau"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# jour} other {# jours}}"
|
||||
},
|
||||
|
||||
@@ -134,12 +134,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Megjelenítse a grafikonon mind a {count} sort?"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Mutatás {limit} {itemType, select, project {{limit, plural, one {project} other {projects}}} country {{limit, plural, one {country} other {countries}}} monetization {{limit, plural, one {monetization value} other {monetization values}}} downloadSource {{limit, plural, one {download source} other {download sources}}} downloadReason {{limit, plural, one {download reason} other {download reasons}}} projectVersion {{limit, plural, one {project version} other {project versions}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {game version} other {game versions}}} other {{limit, plural, one {item} other {items}}}} from table"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Mutatás top {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} from table"
|
||||
},
|
||||
"analytics.chart.tooltip.hide-entry": {
|
||||
"message": "{name} elrejtése a grafikonon"
|
||||
},
|
||||
|
||||
@@ -128,18 +128,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Mostrare tutte le {count} linee nel grafico?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Mostrando {itemType, select, project {{count, plural, one {l'unico progetto} other {tutti i progetti}}} country {{count, plural, one {l'unico paese} other {tutti i paesi}}} monetization {{count, plural, one {l'unico valore} other {tutti i valori}} di monetizzazione} downloadSource {{count, plural, one {l'unica fonte} other {tutte le fonti}}} downloadReason {{count, plural, one {l'unico motivo} other {tutti i motivi}} di download} projectVersion {{count, plural, one {l'unica versione} other {tutte le versioni}} del progetto} loader {{count, plural, one {l'unico loader} other {tutti i loader}}} gameVersion {{count, plural, one {l'unica versione} other {tutte le versioni}} del gioco} other {{count, plural, one {l'unico elemento} other {tutti gli elementi}}}} dalla tabella"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "Mostrando {count} {itemType, select, project {{count, plural, one {progetto} other {progetti}}} country {{count, plural, one {paese} other {paesi}}} monetization {{count, plural, one {valore di monetizzazione} other {valori di monetizzazione}}} downloadSource {{count, plural, one {fonte} other {fonti}}} downloadReason {{count, plural, one {motivo} other {motivi}}} projectVersion {{count, plural, one {versione del progetto} other {versioni del progetto}}} loader {loader} gameVersion {{count, plural, one {versione di gioco} other {versioni di gioco}}} other {{count, plural, one {elemento} other {elementi}}}} dalla tabella"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Mostrando {limit} {itemType, select, project {{limit, plural, one {progetto} other {progetti}}} country {{limit, plural, one {paese} other {paesi}}} monetization {{limit, plural, one {valore di monetizzazione} other {valori di monetizzazione}}} downloadSource {{limit, plural, one {fonte} other {fonti}}} downloadReason {{limit, plural, one {motivo} other {motivi}}} projectVersion {{limit, plural, one {versione del progetto} other {versioni del progetto}}} loader {loader} gameVersion {{limit, plural, one {versione di gioco} other {versioni di gioco}}} other {{limit, plural, one {elemento} other {elementi}}}} dalla tabella"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Mostrando {itemType, select, project {{count, plural, one {il primo progetto} other {i primi # progetti}}} country {{count, plural, one {il primo paese} other {i primi # paesi}}} monetization {{count, plural, one {il primo valore di monetizzazione} other {i primi # valori di monetizzazione}}} downloadSource {{count, plural, one {la prima fonte} other {le prime # fonti}}} downloadReason {{count, plural, one {il primo motivo} other {i primi # motivi}}} projectVersion {{count, plural, one {la prima versione del progetto} other {le prime # versioni del progetto}}} loader {{count, plural, one {il primo loader} other {i primi # loader}}} gameVersion {{count, plural, one {la prima versione di gioco} other {le prime # versioni di gioco}}} other {{count, plural, one {il primo elemento} other {i primi {count} elementi}}}} dalla tabella"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# giorno} other {# giorni}}"
|
||||
},
|
||||
|
||||
@@ -125,9 +125,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Pokazać wszystkie {count} linii na wykresie?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Pokazano {itemType, select, project {{count, plural, one {projekt} other {wszystkie projekty}}} country {{count, plural, one {kraj} other {wszystkie kraje}}} monetization {{count, plural, one {wartość z monetyzacji} other {wszystkie wartości z monetyzacji}}} downloadSource {{count, plural, one {żródło pobrań} other {wszystkie źródła pobrań}}} downloadReason {{count, plural, one {powód pobrań} other {wszystkie powody pobrań}}} projectVersion {{count, plural, one {wersję projektu} other {wszystkie wersje projektu}}} loader {{count, plural, one {loader} other {wszystkie loadery}}} gameVersion {{count, plural, one {wersję gry} other {wszystkie wersje gry}}} other {{count, plural, one {pozycję} other {wszystkie pozycje}}}} z tabeli"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# dzień} other {# dni}}"
|
||||
},
|
||||
|
||||
@@ -134,18 +134,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Exibir todas as {count} linhas no gráfico?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Exibindo {itemType, select,project {{count, plural, one {todo o projeto} other {todos os projetos}}}country {{count, plural, one {todo o país} other {todos os países}}}monetization {{count, plural, one {todo o valor de monetização} other {todos os valores de monetização}}}downloadSource {{count, plural, one {toda a fonte de download} other {todas as fontes de download}}} downloadReason {{count, plural, one {todo o motivo de download} other {todos os motivos de download}}} projectVersion {{count, plural, one {toda a versão do projeto} other {todas as versões do projeto}}} loader {{count, plural, one {todo loader} other {todos os loaders}}} gameVersion {{count, plural, one {toda a versão do jogo} other {todas as versões do jogo}}} other {{count, plural, one {todo o item} other {todos os itens}}}\n} da tabela"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "Exibindo {count} {itemType, select, project {{count, plural, one {projeto} other {projetos}}} country {{count, plural, one {país} other {países}}} monetization {{count, plural, one {valor de monetização} other {valores de monetização}}} downloadSource {{count, plural, one {fonte de download} other {fontes de download}}} downloadReason {{count, plural, one {motivo de download} other {motivos de download}}} projectVersion {{count, plural, one {versão do projeto} other {versões do projeto}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {versão do jogo} other {versões do jogo}}} other {{count, plural, one {item} other {itens}}}} da tabela"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Exibindo {limit} {itemType, select, project {{limit, plural, one {projeto} other {projetos}}} country {{limit, plural, one {país} other {países}}} monetization {{limit, plural, one {valor de monetização} other {valores de monetização}}} downloadSource {{limit, plural, one {fonte de download} other {fontes de download}}} downloadReason {{limit, plural, one {motivo de download} other {motivos de download}}} projectVersion {{limit, plural, one {versão do projeto} other {versões do projeto}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {versão do jogo} other {versões do jogo}}} other {{limit, plural, one {item} other {itens}}}} da tabela"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Exibindo os {count} {itemType, select,\nproject {{count, plural, one {projeto principal} other {principais projetos}}}\ncountry {{count, plural, one {país principal} other {principais países}}}\nmonetization {{count, plural, one {valor de monetização principal} other {principais valores de monetização}}}\ndownloadSource {{count, plural, one {fonte de download principal} other {principais fontes de download}}}\ndownloadReason {{count, plural, one {motivo de download principal} other {principais motivos de download}}}\nprojectVersion {{count, plural, one {versão principal do projeto} other {principais versões do projeto}}}\nloader {{count, plural, one {loader principal} other {loaders principais}}}\ngameVersion {{count, plural, one {versão principal do jogo} other {principais versões do jogo}}}\nother {{count, plural, one {item principal} other {principais itens}}}\n} da tabela"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# dia} other {# dias}}"
|
||||
},
|
||||
|
||||
@@ -101,15 +101,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Visa alla {count} linjer i diagram?"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "Visar {count} {itemType, select, project {projekt} country {{count, plural, one {land} other {länder}}} monetization {{count, plural, one {intäktvärde} other {intäktvärden}}} downloadSource {{count, plural, one {nedladdningskälla} other {nedladdningskällor}}} downloadReason {{count, plural, one {nedladdningsanledning} other {nedladdningsanledningar}}} projectVersion {{count, plural, one {projektversion} other {projektversioner}}} loader {laddare} gameVersion {{count, plural, one {spelversion} other {spelversioner}}} other {föremål}} från tabell"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Visar {limit} {itemType, select, project {projekt} country {{limit, plural, one {land} other {länder}}} monetization {{limit, plural, one {intäktvärde} other {intäktvärden}}} downloadSource {{limit, plural, one {nedladdningskälla} other {nedladdningskällor}}} downloadReason {{limit, plural, one {nedladdningsanledning} other {nedladdningsanledningar}}} projectVersion {{limit, plural, one {projektversion} other {projektversioner}}} loader {laddare} gameVersion {{limit, plural, one {spelversion} other {spelversioner}}} other {föremål}} från tabell"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Visar topp {count} {itemType, select, project {projekt} country {{count, plural, one {land} other {länder}}} monetization {{count, plural, one {intäktvärde} other {intäktvärden}}} downloadSource {{count, plural, one {nedladdningskälla} other {nedladdningskällor}}} downloadReason {{count, plural, one {nedladdningsanledning} other {nedladdningsanledningar}}} projectVersion {{count, plural, one {projektversion} other {projektversioner}}}loader {laddare} gameVersion {{count, plural, one {spelversion} other {spelversioner}}} other {föremål}} från tabell"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# dag} other {# dagar}}"
|
||||
},
|
||||
|
||||
@@ -134,18 +134,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Grafikteki {count} satırın tümünü göstermek ister misiniz?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Tablodaki tüm {itemType, select, project {{count, plural, one {proje} other {projeler}}} country {{count, plural, one {ülke} other {ülkeler}}} monetization {{count, plural, one {gelir elde etme değeri} other {gelir elde etme değerleri}}} downloadSource {{count, plural, one {indirme kaynağı} other {indirme kaynakları}}} downloadReason {{count, plural, one {indirme nedeni} other {indirme nedenleri}}} projectVersion {{count, plural, one {proje sürümü} other {proje sürümleri}}} loader {{count, plural, one {yükleyici} other {yükleyiciler}}} gameVersion {{count, plural, one {oyun sürümü} other {oyun sürümleri}}} other {{count, plural, one {öğe} other {öğeler}}}} gösteriliyor"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "Tablodan {count} {itemType, select, project {{count, plural, one {proje} other {projeler}}} country {{count, plural, one {ülke} other {ülkeler}}} monetization {{count, plural, one {gelir elde etme değeri} other {gelir elde etme değerleri}}} downloadSource {{count, plural, one {indirme kaynağı} other {indirme kaynakları}}} downloadReason {{count, plural, one {indirme nedeni} other {indirme nedenleri}}} projectVersion {{count, plural, one {proje sürümü} other {proje sürümleri}}} loader {{count, plural, one {yükleyici} other {yükleyiciler}}} gameVersion {{count, plural, one {oyun sürümü} other {oyun sürümleri}}} other {{count, plural, one {öğe} other {öğeler}}}} seçildi"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Tablodan {limit} {itemType, select, project {{limit, plural, one {proje} other {projeler}}} country {{limit, plural, one {ülke} other {ülkeler}}} monetization {{limit, plural, one {gelir elde etme değeri} other {gelir elde etme değerleri}}} downloadSource {{limit, plural, one {indirme kaynağı} other {indirme kaynakları}}} downloadReason {{limit, plural, one {indirme nedeni} other {indirme nedenleri}}} projectVersion {{limit, plural, one {proje sürümü} other {proje sürümleri}}} loader {{limit, plural, one {yükleyici} other {yükleyiciler}}} gameVersion {{limit, plural, one {oyun sürümü} other {oyun sürümleri}}} other {{limit, plural, one {öğe} other {öğeler}}}} gösteriliyor"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Tablodan en iyi {count} {itemType, select, project {{count, plural, one {proje} other {projeler}}} country {{count, plural, one {ülke} other {ülkeler}}} monetization {{count, plural, one {gelir elde etme değeri} other {gelir elde etme değerleri}}} downloadSource {{count, plural, one {indirme kaynağı} other {indirme kaynakları}}} downloadReason {{count, plural, one {indirme nedeni} other {indirme nedenleri}}} projectVersion {{count, plural, one {proje sürümü} other {proje sürümleri}}} loader {{count, plural, one {yükleyici} other {yükleyiciler}}} gameVersion {{count, plural, one {oyun sürümü} other {oyun sürümleri}}} other {{count, plural, one {öğe} other {öğeler}}}} gösteriliyor"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# gün} other {# gün}}"
|
||||
},
|
||||
|
||||
@@ -131,9 +131,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Hiện thị tất cả {count} đường trong biểu đồ?"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "Đang hiển thị {limit} {itemType, select, project {{limit, plural, one {dự án} other {dự án}}} country {{limit, plural, one {quốc gia} other {quốc gia}}} monetization {{limit, plural, one {doanh thu} other {doanh thu}}} downloadSource {{limit, plural, one {nơi tải gốc} other {nơi tải gốc}}} downloadReason {{limit, plural, one {lý do tải} other {lý do tải}}} projectVersion {{limit, plural, one {phiên bản dự án} other {phiên bản dự án}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {phiên bản game} other {phiên bản game}}} other {{limit, plural, one {mục} other {mục}}}} từ bảng"
|
||||
},
|
||||
"analytics.chart.tooltip.hide-entry": {
|
||||
"message": "Che {name} trong biểu đồ"
|
||||
},
|
||||
|
||||
@@ -131,18 +131,6 @@
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "是否在图中显示全部 {count} 行?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "展示全部 {itemType, select, project {{count, plural, one {项目} other {项目}}} country {{count, plural, one {国家} other {国家}}} monetization {{count, plural, one {收益项} other {收益项}}} downloadSource {{count, plural, one {下载来源} other {下载来源}}} downloadReason {{count, plural, one {下载原因} other {下载原因}}} projectVersion {{count, plural, one {项目版本} other {项目版本}}} loader {{count, plural, one {加载器} other {加载器}}} gameVersion {{count, plural, one {游戏版本} other {游戏版本}}} other {{count, plural, one {条目} other {条目}}}} 表格数据"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "正在展示{count}{itemType, select,project {{count, plural,one {项目}other {项目}}}country {{count, plural,one {国家}other {国家}}} monetization {{count, plural,one {变现收益}other {变现收益}}} downloadSource {{count, plural,one {下载来源}other {下载来源}}} downloadReason {{count, plural,one {下载原因}other {下载原因}}} projectVersion {{count, plural,one {项目版本}other {项目版本}}} loader {{count, plural,one {启动器}other {启动器}}} gameVersion {{count, plural,one {游戏版本}other {游戏版本}}} other {{count, plural,one {项目}other {项目}}}}"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "正在展示{limit}{itemType, select,project {{limit, plural,one {项目}other {项目}}}country {{limit, plural,one {国家}other {国家}}} monetization {{limit, plural,one {变现收益}other {变现收益}}} downloadSource {{limit, plural,one {下载来源}other {下载来源}}} downloadReason {{limit, plural,one {下载原因}other {下载原因}}} projectVersion {{limit, plural,one {项目版本}other {项目版本}}} loader {{limit, plural,one {启动器}other {启动器}}} gameVersion {{limit, plural,one {游戏版本}other {游戏版本}}}other {{limit, plural,one {项目}other {项目}}}}"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "从表格中展示前{count}\n{itemType, select,\nproject {{count, plural, one {个项目} other {个项目}}}\ncountry {{count, plural, one {个国家} other {个国家}}}\nmonetization {{count, plural, one {项变现金额} other {项变现金额}}}\ndownloadSource {{count, plural, one {个下载来源} other {个下载来源}}}\ndownloadReason {{count, plural, one {条下载原因} other {条下载原因}}}\nprojectVersion {{count, plural, one {个项目版本} other {个项目版本}}}\nloader {{count, plural, one {个加载器} other {个加载器}}}\ngameVersion {{count, plural, one {个游戏版本} other {个游戏版本}}}\nother {{count, plural, one {条数据} other {条数据}}}\n}"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# 天} other {# 天}}"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,10 @@ import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
import type { ProjectStatusFilterValue } from '~/components/analytics-dashboard/query-builder/query-filter-utils'
|
||||
|
||||
import {
|
||||
doesAnalyticsPointMatchNormalizedFilters,
|
||||
normalizeAnalyticsSelectedFilters,
|
||||
} from './analytics-filter-utils'
|
||||
import { getProjectIdsMatchingStatusFilter } from './analytics-project-utils'
|
||||
import type {
|
||||
AnalyticsDashboardTotals,
|
||||
@@ -203,6 +207,30 @@ function mergeAnalyticsProjectEvents(
|
||||
})
|
||||
}
|
||||
|
||||
function mergeAnalyticsProjects(
|
||||
projectGroups: Record<string, Labrinth.Projects.v3.Project>[],
|
||||
): Record<string, Labrinth.Projects.v3.Project> {
|
||||
const projects: Record<string, Labrinth.Projects.v3.Project> = {}
|
||||
|
||||
for (const projectGroup of projectGroups) {
|
||||
Object.assign(projects, projectGroup)
|
||||
}
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
function mergeAnalyticsUsers(
|
||||
userGroups: Record<string, Labrinth.Users.v3.User>[],
|
||||
): Record<string, Labrinth.Users.v3.User> {
|
||||
const users: Record<string, Labrinth.Users.v3.User> = {}
|
||||
|
||||
for (const userGroup of userGroups) {
|
||||
Object.assign(users, userGroup)
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
function waitForAnalyticsFetchBatchDelay(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ANALYTICS_PROJECT_IDS_FETCH_BATCH_DELAY_MS))
|
||||
}
|
||||
@@ -215,6 +243,8 @@ export async function fetchAnalyticsData(
|
||||
): Promise<AnalyticsFetchData> {
|
||||
const fetchRequests = buildAnalyticsFetchRequestBatches(fetchRequest)
|
||||
const timeSliceGroups: Labrinth.Analytics.v3.TimeSlice[][] = []
|
||||
const projectGroups: Record<string, Labrinth.Projects.v3.Project>[] = []
|
||||
const userGroups: Record<string, Labrinth.Users.v3.User>[] = []
|
||||
const projectEventGroups: Labrinth.Analytics.v3.ProjectAnalyticsEvent[][] = []
|
||||
|
||||
for (let index = 0; index < fetchRequests.length; index++) {
|
||||
@@ -224,11 +254,15 @@ export async function fetchAnalyticsData(
|
||||
|
||||
const response = await fetchAnalytics(fetchRequests[index])
|
||||
timeSliceGroups.push(response.metrics)
|
||||
projectGroups.push(response.projects ?? {})
|
||||
userGroups.push(response.users ?? {})
|
||||
projectEventGroups.push(response.project_events ?? [])
|
||||
}
|
||||
|
||||
return {
|
||||
metrics: mergeAnalyticsTimeSlices(timeSliceGroups),
|
||||
projects: mergeAnalyticsProjects(projectGroups),
|
||||
users: mergeAnalyticsUsers(userGroups),
|
||||
project_events: mergeAnalyticsProjectEvents(projectEventGroups),
|
||||
}
|
||||
}
|
||||
@@ -433,6 +467,7 @@ export function computeTotals(
|
||||
availableProjectIds: Set<string>,
|
||||
projectStatusById: Map<string, ProjectStatusFilterValue>,
|
||||
filters: AnalyticsSelectedFilters,
|
||||
dependentProjectTypesById?: ReadonlyMap<string, readonly string[]>,
|
||||
): AnalyticsDashboardTotals {
|
||||
const totals: AnalyticsDashboardTotals = {
|
||||
views: 0,
|
||||
@@ -452,6 +487,7 @@ export function computeTotals(
|
||||
if (filteredProjectIds.size === 0) {
|
||||
return totals
|
||||
}
|
||||
const normalizedFilters = normalizeAnalyticsSelectedFilters(filters)
|
||||
|
||||
for (const timeSlice of timeSlices) {
|
||||
for (const dataPoint of timeSlice) {
|
||||
@@ -462,6 +498,15 @@ export function computeTotals(
|
||||
if (!filteredProjectIds.has(dataPoint.source_project)) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
!doesAnalyticsPointMatchNormalizedFilters(
|
||||
dataPoint,
|
||||
normalizedFilters,
|
||||
dependentProjectTypesById,
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch (dataPoint.metric_kind) {
|
||||
case 'views':
|
||||
|
||||
@@ -119,6 +119,7 @@ export function sanitizeAnalyticsSelectedFiltersForAvailableOptions(
|
||||
filters.download_reason,
|
||||
filterOptions.downloadReasons,
|
||||
),
|
||||
user_id: retainAvailableSelectedFilterValues(filters.user_id, filterOptions.userIds),
|
||||
game_version: retainAvailableSelectedFilterValues(
|
||||
filters.game_version,
|
||||
filterOptions.gameVersions,
|
||||
@@ -140,9 +141,12 @@ export function cloneAnalyticsSelectedFilters(
|
||||
monetization: [...filters.monetization],
|
||||
user_agent: [...filters.user_agent],
|
||||
download_reason: [...filters.download_reason],
|
||||
user_id: [...filters.user_id],
|
||||
version_id: [...filters.version_id],
|
||||
game_version: [...filters.game_version],
|
||||
loader_type: [...filters.loader_type],
|
||||
dependent_project_id: [...filters.dependent_project_id],
|
||||
dependent_project_type: [...filters.dependent_project_type],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,8 +157,10 @@ export function cloneAnalyticsFilterOptions(
|
||||
countries: [...filterOptions.countries],
|
||||
downloadSources: [...filterOptions.downloadSources],
|
||||
downloadReasons: [...filterOptions.downloadReasons],
|
||||
userIds: [...filterOptions.userIds],
|
||||
gameVersions: [...filterOptions.gameVersions],
|
||||
loaderTypes: [...filterOptions.loaderTypes],
|
||||
dependentProjectTypes: [...filterOptions.dependentProjectTypes],
|
||||
versionIds: [...filterOptions.versionIds],
|
||||
}
|
||||
}
|
||||
@@ -164,8 +170,10 @@ function getEmptyAnalyticsFacetsFilterOptionSummary(): AnalyticsFacetsFilterOpti
|
||||
countries: [],
|
||||
downloadSources: [],
|
||||
downloadReasons: [],
|
||||
userIds: [],
|
||||
gameVersions: [],
|
||||
loaderTypes: [],
|
||||
dependentProjectTypes: [],
|
||||
versionIds: [],
|
||||
projectDownloadsById: new Map(),
|
||||
projectVersionDownloadsById: new Map(),
|
||||
@@ -215,12 +223,14 @@ export function getAnalyticsFacetsFilterOptionSummary(
|
||||
),
|
||||
downloadSources: sortStringValues(getAnalyticsFacetValues(projectDownloadFacets?.user_agent)),
|
||||
downloadReasons: sortStringValues(getAnalyticsFacetValues(projectDownloadFacets?.reason)),
|
||||
userIds: [],
|
||||
gameVersions: sortStringValues(
|
||||
[...gameVersions]
|
||||
.map((gameVersion) => gameVersion.trim())
|
||||
.filter((gameVersion) => gameVersion.length > 0),
|
||||
),
|
||||
loaderTypes: sortStringValues([...loaderTypes]),
|
||||
dependentProjectTypes: [],
|
||||
versionIds: sortStringValues([...new Set([...downloadVersionIds, ...playtimeVersionIds])]),
|
||||
projectDownloadsById: new Map(),
|
||||
projectVersionDownloadsById: new Map(),
|
||||
@@ -232,10 +242,12 @@ export function getAnalyticsFacetsFilterOptionSummary(
|
||||
export function doesAnalyticsPointMatchFilters(
|
||||
dataPoint: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
filters: AnalyticsSelectedFilters,
|
||||
dependentProjectTypesById?: ReadonlyMap<string, readonly string[]>,
|
||||
): boolean {
|
||||
return doesAnalyticsPointMatchNormalizedFilters(
|
||||
dataPoint,
|
||||
normalizeAnalyticsSelectedFilters(filters),
|
||||
dependentProjectTypesById,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -247,9 +259,12 @@ export function normalizeAnalyticsSelectedFilters(
|
||||
monetization: normalizeAnalyticsFilterValues(filters.monetization),
|
||||
userAgent: normalizeAnalyticsFilterValues(filters.user_agent),
|
||||
downloadReason: normalizeAnalyticsFilterValues(filters.download_reason),
|
||||
userId: normalizeAnalyticsFilterValues(filters.user_id),
|
||||
versionId: normalizeAnalyticsFilterValues(filters.version_id),
|
||||
gameVersion: normalizeAnalyticsFilterValues(filters.game_version),
|
||||
loaderType: normalizeAnalyticsFilterValues(filters.loader_type),
|
||||
dependentProjectId: normalizeAnalyticsFilterValues(filters.dependent_project_id),
|
||||
dependentProjectType: normalizeAnalyticsFilterValues(filters.dependent_project_type),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +282,7 @@ function normalizeAnalyticsFilterValues(values: string[]): ReadonlySet<string> {
|
||||
export function doesAnalyticsPointMatchNormalizedFilters(
|
||||
dataPoint: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
filters: NormalizedAnalyticsSelectedFilters,
|
||||
dependentProjectTypesById?: ReadonlyMap<string, readonly string[]>,
|
||||
): boolean {
|
||||
switch (dataPoint.metric_kind) {
|
||||
case 'views':
|
||||
@@ -314,7 +330,21 @@ export function doesAnalyticsPointMatchNormalizedFilters(
|
||||
filters.gameVersion,
|
||||
getGameVersionFilterValue,
|
||||
) &&
|
||||
doesAnalyticsPointMatchNormalizedFilter(dataPoint, filters.loaderType, getLoaderFilterValue)
|
||||
doesAnalyticsPointMatchNormalizedFilter(
|
||||
dataPoint,
|
||||
filters.loaderType,
|
||||
getLoaderFilterValue,
|
||||
) &&
|
||||
doesAnalyticsPointMatchNormalizedFilter(
|
||||
dataPoint,
|
||||
filters.dependentProjectId,
|
||||
getDependentProjectIdFilterValue,
|
||||
) &&
|
||||
doesAnalyticsDownloadPointMatchDependentProjectTypeFilter(
|
||||
dataPoint,
|
||||
filters.dependentProjectType,
|
||||
dependentProjectTypesById,
|
||||
)
|
||||
)
|
||||
case 'playtime':
|
||||
return (
|
||||
@@ -336,7 +366,11 @@ export function doesAnalyticsPointMatchNormalizedFilters(
|
||||
doesAnalyticsPointMatchNormalizedFilter(dataPoint, filters.loaderType, getLoaderFilterValue)
|
||||
)
|
||||
case 'revenue':
|
||||
return true
|
||||
return doesAnalyticsPointMatchNormalizedFilter(
|
||||
dataPoint,
|
||||
filters.userId,
|
||||
getUserIdFilterValue,
|
||||
)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
@@ -363,6 +397,28 @@ function doesAnalyticsPointMatchNormalizedFilter(
|
||||
return filterValues.has(normalizedPointValue)
|
||||
}
|
||||
|
||||
function doesAnalyticsDownloadPointMatchDependentProjectTypeFilter(
|
||||
dataPoint: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
filterValues: ReadonlySet<string>,
|
||||
dependentProjectTypesById: ReadonlyMap<string, readonly string[]> | undefined,
|
||||
): boolean {
|
||||
if (filterValues.size === 0) {
|
||||
return true
|
||||
}
|
||||
if (dataPoint.metric_kind !== 'downloads') {
|
||||
return true
|
||||
}
|
||||
|
||||
const dependentProjectId =
|
||||
'dependent_project_id' in dataPoint ? dataPoint.dependent_project_id?.trim() : undefined
|
||||
if (!dependentProjectId) {
|
||||
return false
|
||||
}
|
||||
|
||||
const projectTypes = dependentProjectTypesById?.get(dependentProjectId) ?? []
|
||||
return projectTypes.some((projectType) => filterValues.has(projectType.trim().toLowerCase()))
|
||||
}
|
||||
|
||||
function getCountryFilterValue(
|
||||
dataPoint: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
): string | null | undefined {
|
||||
@@ -390,6 +446,16 @@ function getMonetizationFilterValue(
|
||||
return dataPoint.monetized ? 'monetized' : 'unmonetized'
|
||||
}
|
||||
|
||||
function getDependentProjectIdFilterValue(
|
||||
dataPoint: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
): string | null | undefined {
|
||||
if (dataPoint.metric_kind !== 'downloads') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return 'dependent_project_id' in dataPoint ? (dataPoint.dependent_project_id ?? null) : undefined
|
||||
}
|
||||
|
||||
function getDownloadSourceFilterValue(
|
||||
dataPoint: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
): string | null | undefined {
|
||||
@@ -410,6 +476,16 @@ function getDownloadReasonFilterValue(
|
||||
return dataPoint.reason ?? null
|
||||
}
|
||||
|
||||
function getUserIdFilterValue(
|
||||
dataPoint: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
): string | null | undefined {
|
||||
if (dataPoint.metric_kind !== 'revenue') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return dataPoint.user_id ?? null
|
||||
}
|
||||
|
||||
function getVersionFilterValue(
|
||||
dataPoint: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
): string | null | undefined {
|
||||
|
||||
@@ -15,7 +15,7 @@ const PLUGIN_PROJECT_TYPE = 'plugin'
|
||||
|
||||
export const UNKNOWN_ORGANIZATION_NAME = 'Organization'
|
||||
|
||||
function getProjectTypes(project: ProjectTypeMetadata): string[] {
|
||||
export function getProjectTypes(project: ProjectTypeMetadata): string[] {
|
||||
const projectTypes = new Set<string>()
|
||||
const projectType = project.project_type?.trim()
|
||||
if (projectType) {
|
||||
@@ -69,6 +69,7 @@ export function toAnalyticsDashboardProject(
|
||||
id: project.id,
|
||||
name: project.name ?? project.title ?? project.id,
|
||||
iconUrl: project.icon_url ?? undefined,
|
||||
organizationId: getProjectOrganizationId(project),
|
||||
downloads: project.downloads ?? 0,
|
||||
status: getProjectStatusFilterValue(project.status),
|
||||
publishedAt: project.published ?? undefined,
|
||||
|
||||
@@ -10,9 +10,12 @@ export type AnalyticsQueryFilterCategory =
|
||||
| 'monetization'
|
||||
| 'user_agent'
|
||||
| 'download_reason'
|
||||
| 'user_id'
|
||||
| 'version_id'
|
||||
| 'game_version'
|
||||
| 'loader_type'
|
||||
| 'dependent_project_id'
|
||||
| 'dependent_project_type'
|
||||
|
||||
export type AnalyticsTimeframePreset =
|
||||
| 'today'
|
||||
@@ -37,9 +40,11 @@ export type AnalyticsBreakdownPreset =
|
||||
| 'monetization'
|
||||
| 'user_agent'
|
||||
| 'download_reason'
|
||||
| 'user_id'
|
||||
| 'version_id'
|
||||
| 'loader'
|
||||
| 'game_version'
|
||||
| 'dependent_project_download'
|
||||
|
||||
export type AnalyticsSelectedBreakdowns = Exclude<AnalyticsBreakdownPreset, 'none'>[]
|
||||
export type AnalyticsDashboardStat = 'views' | 'downloads' | 'revenue' | 'playtime'
|
||||
@@ -47,6 +52,7 @@ export type AnalyticsGraphViewMode = 'line' | 'area' | 'bar'
|
||||
export type AnalyticsTableSortColumn =
|
||||
| 'date'
|
||||
| 'project'
|
||||
| 'dependent_on'
|
||||
| 'breakdown'
|
||||
| `breakdown_${Exclude<AnalyticsBreakdownPreset, 'none'>}`
|
||||
| 'views'
|
||||
@@ -121,6 +127,7 @@ export interface AnalyticsDashboardProject {
|
||||
id: string
|
||||
name: string
|
||||
iconUrl?: string
|
||||
organizationId?: string
|
||||
downloads: number
|
||||
status: ProjectStatusFilterValue
|
||||
publishedAt?: string
|
||||
@@ -151,8 +158,10 @@ export interface AnalyticsDashboardFilterOptions {
|
||||
countries: string[]
|
||||
downloadSources: string[]
|
||||
downloadReasons: string[]
|
||||
userIds: string[]
|
||||
gameVersions: string[]
|
||||
loaderTypes: string[]
|
||||
dependentProjectTypes: string[]
|
||||
versionIds: string[]
|
||||
}
|
||||
|
||||
@@ -161,17 +170,22 @@ export interface NormalizedAnalyticsSelectedFilters {
|
||||
monetization: ReadonlySet<string>
|
||||
userAgent: ReadonlySet<string>
|
||||
downloadReason: ReadonlySet<string>
|
||||
userId: ReadonlySet<string>
|
||||
versionId: ReadonlySet<string>
|
||||
gameVersion: ReadonlySet<string>
|
||||
loaderType: ReadonlySet<string>
|
||||
dependentProjectId: ReadonlySet<string>
|
||||
dependentProjectType: ReadonlySet<string>
|
||||
}
|
||||
|
||||
export interface AnalyticsFacetsFilterOptionSummary {
|
||||
countries: string[]
|
||||
downloadSources: string[]
|
||||
downloadReasons: string[]
|
||||
userIds: string[]
|
||||
gameVersions: string[]
|
||||
loaderTypes: string[]
|
||||
dependentProjectTypes: string[]
|
||||
versionIds: string[]
|
||||
projectDownloadsById: Map<string, number>
|
||||
projectVersionDownloadsById: Map<string, number>
|
||||
@@ -202,5 +216,7 @@ export type AnalyticsTimeSliceSplit = {
|
||||
|
||||
export type AnalyticsFetchData = {
|
||||
metrics: Labrinth.Analytics.v3.TimeSlice[]
|
||||
projects: Record<string, Labrinth.Projects.v3.Project>
|
||||
project_events: Labrinth.Analytics.v3.ProjectAnalyticsEvent[]
|
||||
users: Record<string, Labrinth.Users.v3.User>
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
import {
|
||||
getProjectIdsMatchingStatusFilter,
|
||||
getProjectOrganizationId,
|
||||
getProjectTypes,
|
||||
getSingleQueryValue,
|
||||
getUniqueAnalyticsDashboardProjects,
|
||||
isAnalyticsEligibleProject,
|
||||
@@ -200,8 +201,17 @@ export interface AnalyticsDashboardContextValue {
|
||||
isAnalyticsFilterOptionsLoading: ComputedRef<boolean>
|
||||
versionNumbersById: ComputedRef<Map<string, string>>
|
||||
versionPublishedDatesById: ComputedRef<Map<string, string>>
|
||||
versionProjectIdsById: ComputedRef<Map<string, string>>
|
||||
versionProjectNamesById: ComputedRef<Map<string, string>>
|
||||
versionProjectIconUrlsById: ComputedRef<Map<string, string>>
|
||||
versionProjectOrganizationNamesById: ComputedRef<Map<string, string>>
|
||||
projectNamesById: ComputedRef<Map<string, string>>
|
||||
projectIconUrlsById: ComputedRef<Map<string, string>>
|
||||
projectOrganizationIdsById: ComputedRef<Map<string, string>>
|
||||
projectOrganizationNamesById: ComputedRef<Map<string, string>>
|
||||
userNamesById: ComputedRef<Map<string, string>>
|
||||
userAvatarUrlsById: ComputedRef<Map<string, string>>
|
||||
dependentProjectTypesById: ComputedRef<Map<string, string[]>>
|
||||
projectStatusById: ComputedRef<Map<string, ProjectStatusFilterValue>>
|
||||
availableProjectStatuses: ComputedRef<ProjectStatusFilterValue[]>
|
||||
availableProjectDownloadsById: ComputedRef<Map<string, number>>
|
||||
@@ -250,6 +260,7 @@ export interface AnalyticsDashboardContextValue {
|
||||
getVersionPublishedDate: (versionId: string) => string | undefined
|
||||
getVersionProjectName: (versionId: string) => string | undefined
|
||||
getVersionProjectIconUrl: (versionId: string) => string | undefined
|
||||
getVersionProjectOrganizationName: (versionId: string) => string | undefined
|
||||
setFetchRequest: (fetchRequest: Labrinth.Analytics.v3.FetchRequest) => void
|
||||
setActiveStat: (stat: AnalyticsDashboardStat) => void
|
||||
}
|
||||
@@ -565,17 +576,6 @@ export function createAnalyticsDashboardContext(
|
||||
? dashboardUserProjectIds.value
|
||||
: availableProjectIds.value,
|
||||
)
|
||||
const projectNamesById = computed(
|
||||
() => new Map(projects.value.map((project) => [project.id, project.name])),
|
||||
)
|
||||
const projectIconUrlsById = computed(
|
||||
() =>
|
||||
new Map(
|
||||
projects.value
|
||||
.filter((project) => project.iconUrl)
|
||||
.map((project) => [project.id, project.iconUrl as string]),
|
||||
),
|
||||
)
|
||||
const projectStatusById = computed(
|
||||
() => new Map(projects.value.map((project) => [project.id, project.status])),
|
||||
)
|
||||
@@ -808,6 +808,7 @@ export function createAnalyticsDashboardContext(
|
||||
},
|
||||
availableProjectIds,
|
||||
defaultProjectIds,
|
||||
areProjectsLoaded,
|
||||
sanitizeSelectedFilters: sanitizeAnalyticsSelectedFiltersForContext,
|
||||
})
|
||||
|
||||
@@ -996,6 +997,7 @@ export function createAnalyticsDashboardContext(
|
||||
selectedFilters,
|
||||
availableProjectIds,
|
||||
defaultProjectIds,
|
||||
areProjectsLoaded,
|
||||
],
|
||||
() => {
|
||||
syncQueryBuilderRouteQuery()
|
||||
@@ -1056,6 +1058,8 @@ export function createAnalyticsDashboardContext(
|
||||
if (!isAnalyticsFetchRequestReady(nextFetchRequest)) {
|
||||
return {
|
||||
metrics: [],
|
||||
projects: {},
|
||||
users: {},
|
||||
project_events: [],
|
||||
}
|
||||
}
|
||||
@@ -1307,10 +1311,185 @@ export function createAnalyticsDashboardContext(
|
||||
const projectVersionFilterOptionSummary = computed(() =>
|
||||
getProjectVersionFilterOptionSummary(filterOptionProjectVersions.value ?? []),
|
||||
)
|
||||
const timeSlices = shallowRef<Labrinth.Analytics.v3.TimeSlice[]>([])
|
||||
const previousTimeSlices = shallowRef<Labrinth.Analytics.v3.TimeSlice[]>([])
|
||||
const analyticsProjects = shallowRef<Record<string, Labrinth.Projects.v3.Project>>({})
|
||||
const analyticsUsers = shallowRef<Record<string, Labrinth.Users.v3.User>>({})
|
||||
const baseOrganizationNamesById = computed(() => {
|
||||
const organizationNames = new Map<string, string>()
|
||||
const organization = options.organizationContext?.organization.value
|
||||
if (organization) {
|
||||
organizationNames.set(organization.id, organization.name)
|
||||
}
|
||||
|
||||
for (const [organizationId, organization] of Object.entries(
|
||||
dashboardAllProjects.value?.organizations ?? {},
|
||||
)) {
|
||||
organizationNames.set(organizationId, organization.name)
|
||||
}
|
||||
|
||||
return organizationNames
|
||||
})
|
||||
const missingOrganizationIds = computed(() => {
|
||||
const knownOrganizationNames = baseOrganizationNamesById.value
|
||||
const organizationIds = new Set<string>()
|
||||
|
||||
for (const project of projects.value) {
|
||||
if (project.organizationId && !knownOrganizationNames.has(project.organizationId)) {
|
||||
organizationIds.add(project.organizationId)
|
||||
}
|
||||
}
|
||||
for (const project of Object.values(analyticsProjects.value)) {
|
||||
const organizationId = getProjectOrganizationId(project)
|
||||
if (organizationId && !knownOrganizationNames.has(organizationId)) {
|
||||
organizationIds.add(organizationId)
|
||||
}
|
||||
}
|
||||
|
||||
return sortStringValues([...organizationIds])
|
||||
})
|
||||
const { data: missingOrganizations } = useQuery({
|
||||
queryKey: computed(() => [
|
||||
'analytics',
|
||||
'dashboard',
|
||||
'missing-organizations',
|
||||
missingOrganizationIds.value,
|
||||
]),
|
||||
queryFn: () => client.labrinth.organizations_v3.getMultiple(missingOrganizationIds.value),
|
||||
enabled: computed(() => missingOrganizationIds.value.length > 0),
|
||||
placeholderData: [],
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
const organizationNamesById = computed(() => {
|
||||
const organizationNames = new Map(baseOrganizationNamesById.value)
|
||||
for (const organization of missingOrganizations.value ?? []) {
|
||||
organizationNames.set(organization.id, organization.name)
|
||||
}
|
||||
return organizationNames
|
||||
})
|
||||
const projectIconUrlsById = computed(() => {
|
||||
const projectIconUrls = new Map(
|
||||
projects.value
|
||||
.filter((project) => project.iconUrl)
|
||||
.map((project) => [project.id, project.iconUrl as string]),
|
||||
)
|
||||
for (const [projectId, project] of Object.entries(analyticsProjects.value)) {
|
||||
if (project.icon_url) {
|
||||
projectIconUrls.set(projectId, project.icon_url)
|
||||
}
|
||||
}
|
||||
return projectIconUrls
|
||||
})
|
||||
const projectOrganizationIdsById = computed(() => {
|
||||
const contextOrganizationId = hasOrganizationContext.value
|
||||
? options.organizationContext?.organization.value?.id
|
||||
: undefined
|
||||
const projectOrganizationIds = new Map<string, string>()
|
||||
for (const project of projects.value) {
|
||||
if (project.organizationId) {
|
||||
projectOrganizationIds.set(project.id, project.organizationId)
|
||||
} else if (contextOrganizationId) {
|
||||
projectOrganizationIds.set(project.id, contextOrganizationId)
|
||||
}
|
||||
}
|
||||
for (const [projectId, project] of Object.entries(analyticsProjects.value)) {
|
||||
const organizationId = getProjectOrganizationId(project)
|
||||
if (organizationId) {
|
||||
projectOrganizationIds.set(projectId, organizationId)
|
||||
}
|
||||
}
|
||||
return projectOrganizationIds
|
||||
})
|
||||
const projectOrganizationNamesById = computed(() => {
|
||||
const organizationNames = organizationNamesById.value
|
||||
const contextOrganizationName = hasOrganizationContext.value
|
||||
? options.organizationContext?.organization.value?.name
|
||||
: undefined
|
||||
const projectOrganizationNames = new Map<string, string>()
|
||||
for (const project of projects.value) {
|
||||
if (project.organizationId) {
|
||||
const organizationName = organizationNames.get(project.organizationId)
|
||||
if (organizationName) {
|
||||
projectOrganizationNames.set(project.id, organizationName)
|
||||
}
|
||||
} else if (contextOrganizationName) {
|
||||
projectOrganizationNames.set(project.id, contextOrganizationName)
|
||||
}
|
||||
}
|
||||
for (const [projectId, project] of Object.entries(analyticsProjects.value)) {
|
||||
const organizationId = getProjectOrganizationId(project)
|
||||
if (organizationId) {
|
||||
const organizationName = organizationNames.get(organizationId)
|
||||
if (organizationName) {
|
||||
projectOrganizationNames.set(projectId, organizationName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return projectOrganizationNames
|
||||
})
|
||||
const dependentProjectTypesById = computed(() => {
|
||||
const projectTypesById = new Map<string, string[]>()
|
||||
for (const project of projects.value) {
|
||||
projectTypesById.set(project.id, project.projectTypes)
|
||||
}
|
||||
for (const [projectId, project] of Object.entries(analyticsProjects.value)) {
|
||||
projectTypesById.set(projectId, getProjectTypes(project))
|
||||
}
|
||||
return projectTypesById
|
||||
})
|
||||
const dependentProjectTypeFilterOptions = computed(() => {
|
||||
const projectTypes = new Set<string>()
|
||||
const dependentProjectIds = new Set<string>()
|
||||
for (const timeSlice of [...timeSlices.value, ...previousTimeSlices.value]) {
|
||||
for (const dataPoint of timeSlice) {
|
||||
const dependentProjectId =
|
||||
'dependent_project_id' in dataPoint ? dataPoint.dependent_project_id?.trim() : undefined
|
||||
if (dependentProjectId) {
|
||||
dependentProjectIds.add(dependentProjectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const projectId of dependentProjectIds) {
|
||||
const types = dependentProjectTypesById.value.get(projectId) ?? []
|
||||
for (const type of types) {
|
||||
const normalizedType = type.trim().toLowerCase()
|
||||
if (normalizedType.length > 0) {
|
||||
projectTypes.add(normalizedType)
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortStringValues([...projectTypes])
|
||||
})
|
||||
const userIdFilterOptions = computed(() => {
|
||||
const userIds = new Set<string>()
|
||||
for (const userId of selectedFilters.value.user_id) {
|
||||
const normalizedUserId = userId.trim()
|
||||
if (normalizedUserId.length > 0) {
|
||||
userIds.add(normalizedUserId)
|
||||
}
|
||||
}
|
||||
for (const userId of Object.keys(analyticsUsers.value)) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
for (const timeSlice of timeSlices.value) {
|
||||
for (const dataPoint of timeSlice) {
|
||||
const userId =
|
||||
dataPoint.metric_kind === 'revenue' && 'user_id' in dataPoint
|
||||
? dataPoint.user_id?.trim()
|
||||
: undefined
|
||||
if (userId) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortStringValues([...userIds])
|
||||
})
|
||||
const filterOptions = computed<AnalyticsDashboardFilterOptions>(() => ({
|
||||
countries: analyticsFacetsFilterOptionSummary.value.countries,
|
||||
downloadSources: analyticsFacetsFilterOptionSummary.value.downloadSources,
|
||||
downloadReasons: analyticsFacetsFilterOptionSummary.value.downloadReasons,
|
||||
userIds: userIdFilterOptions.value,
|
||||
gameVersions: sortStringValues([
|
||||
...new Set([
|
||||
...projectVersionFilterOptionSummary.value.gameVersions,
|
||||
@@ -1323,6 +1502,7 @@ export function createAnalyticsDashboardContext(
|
||||
...analyticsFacetsFilterOptionSummary.value.loaderTypes,
|
||||
]),
|
||||
]),
|
||||
dependentProjectTypes: dependentProjectTypeFilterOptions.value,
|
||||
versionIds: sortStringValues([
|
||||
...new Set([
|
||||
...projectVersionFilterOptionSummary.value.versionIds,
|
||||
@@ -1362,8 +1542,6 @@ export function createAnalyticsDashboardContext(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const timeSlices = shallowRef<Labrinth.Analytics.v3.TimeSlice[]>([])
|
||||
const previousTimeSlices = shallowRef<Labrinth.Analytics.v3.TimeSlice[]>([])
|
||||
const projectEvents = shallowRef<Labrinth.Analytics.v3.ProjectAnalyticsEvent[]>([])
|
||||
const displayedSelectedProjectIds = ref<string[]>([...selectedProjectIds.value])
|
||||
const displayedSelectedGroupBy = ref<AnalyticsGroupByPreset>(selectedGroupBy.value)
|
||||
@@ -1408,6 +1586,8 @@ export function createAnalyticsDashboardContext(
|
||||
)
|
||||
timeSlices.value = splitTimeSlices.currentTimeSlices
|
||||
previousTimeSlices.value = splitTimeSlices.previousTimeSlices
|
||||
analyticsProjects.value = nextAnalyticsData.projects
|
||||
analyticsUsers.value = nextAnalyticsData.users
|
||||
projectEvents.value = getAnalyticsProjectEventsInTimeRange(
|
||||
nextAnalyticsData.project_events,
|
||||
fetchRequest.value,
|
||||
@@ -1423,9 +1603,38 @@ export function createAnalyticsDashboardContext(
|
||||
}
|
||||
timeSlices.value = []
|
||||
previousTimeSlices.value = []
|
||||
analyticsProjects.value = {}
|
||||
analyticsUsers.value = {}
|
||||
projectEvents.value = []
|
||||
})
|
||||
|
||||
const projectNamesById = computed(() => {
|
||||
const projectNames = new Map(projects.value.map((project) => [project.id, project.name]))
|
||||
for (const [projectId, project] of Object.entries(analyticsProjects.value)) {
|
||||
projectNames.set(projectId, project.name ?? projectNames.get(projectId) ?? projectId)
|
||||
}
|
||||
return projectNames
|
||||
})
|
||||
const userNamesById = computed(
|
||||
() =>
|
||||
new Map(
|
||||
Object.entries(analyticsUsers.value).map(([userId, user]) => [
|
||||
userId,
|
||||
user.username ?? userId,
|
||||
]),
|
||||
),
|
||||
)
|
||||
const userAvatarUrlsById = computed(
|
||||
() =>
|
||||
new Map(
|
||||
Object.entries(analyticsUsers.value)
|
||||
.filter((entry): entry is [string, Labrinth.Users.v3.User & { avatar_url: string }] =>
|
||||
Boolean(entry[1].avatar_url),
|
||||
)
|
||||
.map(([userId, user]) => [userId, user.avatar_url]),
|
||||
),
|
||||
)
|
||||
|
||||
const analyticsVersionIds = computed(() => {
|
||||
const versionIds = new Set<string>()
|
||||
for (const versionId of selectedFilters.value.version_id) {
|
||||
@@ -1481,6 +1690,26 @@ export function createAnalyticsDashboardContext(
|
||||
const versionPublishedDatesById = computed(
|
||||
() => new Map(allVersionMetadata.value.map((version) => [version.id, version.datePublished])),
|
||||
)
|
||||
const versionProjectIdsById = computed(() => {
|
||||
const versionProjectIds = new Map(
|
||||
allVersionMetadata.value.map((version) => [version.id, version.projectId]),
|
||||
)
|
||||
for (const timeSlice of [...timeSlices.value, ...previousTimeSlices.value]) {
|
||||
for (const dataPoint of timeSlice) {
|
||||
if (
|
||||
'source_project' in dataPoint &&
|
||||
(dataPoint.metric_kind === 'downloads' || dataPoint.metric_kind === 'playtime') &&
|
||||
dataPoint.version_id
|
||||
) {
|
||||
const versionId = dataPoint.version_id.trim()
|
||||
if (versionId.length > 0 && !versionProjectIds.has(versionId)) {
|
||||
versionProjectIds.set(versionId, dataPoint.source_project)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return versionProjectIds
|
||||
})
|
||||
const versionProjectNamesById = computed(() => {
|
||||
const projectNames = projectNamesById.value
|
||||
const versionProjectNames = new Map<string, string>()
|
||||
@@ -1509,6 +1738,17 @@ export function createAnalyticsDashboardContext(
|
||||
}
|
||||
return versionProjectIconUrls
|
||||
})
|
||||
const versionProjectOrganizationNamesById = computed(() => {
|
||||
const projectOrganizationNames = projectOrganizationNamesById.value
|
||||
const versionProjectOrganizationNames = new Map<string, string>()
|
||||
for (const version of allVersionMetadata.value) {
|
||||
const organizationName = projectOrganizationNames.get(version.projectId)
|
||||
if (organizationName) {
|
||||
versionProjectOrganizationNames.set(version.id, organizationName)
|
||||
}
|
||||
}
|
||||
return versionProjectOrganizationNames
|
||||
})
|
||||
const downloadCountTimeSlices = computed(() => {
|
||||
const countTimeSlices = analyticsDownloadCountTimeSlices.value ?? []
|
||||
return countTimeSlices.length > 0 ? countTimeSlices : timeSlices.value
|
||||
@@ -1539,6 +1779,7 @@ export function createAnalyticsDashboardContext(
|
||||
availableProjectIdSet.value,
|
||||
projectStatusById.value,
|
||||
selectedFilters.value,
|
||||
dependentProjectTypesById.value,
|
||||
),
|
||||
)
|
||||
const previousTotals = computed<AnalyticsDashboardTotals>(() =>
|
||||
@@ -1548,6 +1789,7 @@ export function createAnalyticsDashboardContext(
|
||||
availableProjectIdSet.value,
|
||||
projectStatusById.value,
|
||||
selectedFilters.value,
|
||||
dependentProjectTypesById.value,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1665,6 +1907,10 @@ export function createAnalyticsDashboardContext(
|
||||
return versionProjectIconUrlsById.value.get(versionId)
|
||||
}
|
||||
|
||||
function getVersionProjectOrganizationName(versionId: string): string | undefined {
|
||||
return versionProjectOrganizationNamesById.value.get(versionId)
|
||||
}
|
||||
|
||||
function setActiveStat(nextStat: AnalyticsDashboardStat) {
|
||||
if (
|
||||
!isAnalyticsDashboardStatRelevant(nextStat, selectedBreakdowns.value, selectedFilters.value)
|
||||
@@ -1709,8 +1955,17 @@ export function createAnalyticsDashboardContext(
|
||||
isAnalyticsFilterOptionsLoading,
|
||||
versionNumbersById,
|
||||
versionPublishedDatesById,
|
||||
versionProjectIdsById,
|
||||
versionProjectNamesById,
|
||||
versionProjectIconUrlsById,
|
||||
versionProjectOrganizationNamesById,
|
||||
projectNamesById,
|
||||
projectIconUrlsById,
|
||||
projectOrganizationIdsById,
|
||||
projectOrganizationNamesById,
|
||||
userNamesById,
|
||||
userAvatarUrlsById,
|
||||
dependentProjectTypesById,
|
||||
projectStatusById,
|
||||
availableProjectStatuses,
|
||||
availableProjectDownloadsById,
|
||||
@@ -1752,6 +2007,7 @@ export function createAnalyticsDashboardContext(
|
||||
getVersionPublishedDate,
|
||||
getVersionProjectName,
|
||||
getVersionProjectIconUrl,
|
||||
getVersionProjectOrganizationName,
|
||||
setFetchRequest,
|
||||
setActiveStat,
|
||||
}
|
||||
|
||||
@@ -313,6 +313,7 @@ export namespace Labrinth {
|
||||
export type ProjectDownloadsField =
|
||||
| 'project_id'
|
||||
| 'version_id'
|
||||
| 'dependent_project_id'
|
||||
| 'user_agent'
|
||||
| 'domain'
|
||||
| 'country'
|
||||
@@ -328,7 +329,7 @@ export namespace Labrinth {
|
||||
| 'game_version'
|
||||
| 'country'
|
||||
|
||||
export type ProjectRevenueField = 'project_id'
|
||||
export type ProjectRevenueField = 'project_id' | 'user_id'
|
||||
|
||||
export type DownloadReason = 'standalone' | 'dependency' | 'modpack' | 'update'
|
||||
|
||||
@@ -347,6 +348,7 @@ export namespace Labrinth {
|
||||
|
||||
export type ProjectDownloadsFilters = {
|
||||
version_id?: string[]
|
||||
dependent_project_id?: string[]
|
||||
domain?: string[]
|
||||
user_agent?: string[]
|
||||
monetized?: boolean[]
|
||||
@@ -363,7 +365,9 @@ export namespace Labrinth {
|
||||
country?: string[]
|
||||
}
|
||||
|
||||
export type ProjectRevenueFilters = Record<string, never>
|
||||
export type ProjectRevenueFilters = {
|
||||
user_id?: string[]
|
||||
}
|
||||
|
||||
export type AffiliateCodeClicksFilters = {
|
||||
affiliate_code_id?: string[]
|
||||
@@ -379,6 +383,8 @@ export namespace Labrinth {
|
||||
|
||||
export type FetchResponse = {
|
||||
metrics: TimeSlice[]
|
||||
projects: Record<string, Projects.v3.Project>
|
||||
users: Record<string, Users.v3.User>
|
||||
project_events: ProjectAnalyticsEvent[]
|
||||
}
|
||||
|
||||
@@ -462,6 +468,7 @@ export namespace Labrinth {
|
||||
user_agent?: string
|
||||
domain?: string
|
||||
version_id?: string
|
||||
dependent_project_id?: string
|
||||
country?: string
|
||||
monetized?: boolean
|
||||
reason?: DownloadReason
|
||||
@@ -479,6 +486,7 @@ export namespace Labrinth {
|
||||
}
|
||||
|
||||
export type ProjectRevenue = {
|
||||
user_id?: string
|
||||
revenue: string
|
||||
}
|
||||
|
||||
|
||||
@@ -414,9 +414,11 @@ export type DropdownFilterBarCategory = {
|
||||
options: DropdownFilterBarItem[]
|
||||
syntheticOptions?: DropdownFilterBarOption[]
|
||||
searchable?: boolean
|
||||
disableLocalOptionsFilter?: boolean
|
||||
searchPlaceholder?: string
|
||||
emptyOptionsLabel?: string
|
||||
emptySearchLabel?: string
|
||||
onSearchQueryChange?: (query: string) => void
|
||||
submenuClass?: string
|
||||
previewDropdownWidth?: string | number
|
||||
previewDropdownMinWidth?: string | number
|
||||
@@ -618,7 +620,7 @@ const filteredActiveCategoryOptions = computed(() => {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!activeCategory.value.searchable) {
|
||||
if (!activeCategory.value.searchable || activeCategory.value.disableLocalOptionsFilter) {
|
||||
return activeCategory.value.options
|
||||
}
|
||||
|
||||
@@ -1808,6 +1810,7 @@ watch(isAddMenuOpen, (isOpen) => {
|
||||
})
|
||||
|
||||
watch(categorySearchQuery, () => {
|
||||
activeCategory.value?.onSearchQueryChange?.(categorySearchQuery.value)
|
||||
resetActiveCategoryOptionsScrollState()
|
||||
initializeActiveCategoryOptionsOverlayScrollbars()
|
||||
scheduleSubmenuPositionUpdate()
|
||||
@@ -1816,6 +1819,7 @@ watch(categorySearchQuery, () => {
|
||||
watch(activeCategoryKey, (categoryKey) => {
|
||||
resetActiveCategoryOptionsScrollState()
|
||||
if (categoryKey) {
|
||||
activeCategory.value?.onSearchQueryChange?.(categorySearchQuery.value)
|
||||
initializeActiveCategoryOptionsOverlayScrollbars()
|
||||
} else {
|
||||
destroyActiveCategoryOptionsOverlayScrollbars()
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<span
|
||||
v-if="column.label || column.enableSorting"
|
||||
class="inline-flex min-w-0 max-w-full items-center gap-1 font-semibold"
|
||||
:class="`${sortColumn === column.key ? 'text-contrast' : ''}`"
|
||||
:class="`${sortColumn === column.key ? 'text-contrast -mr-1' : ''}`"
|
||||
>
|
||||
<span class="min-w-0 truncate">{{ column.label ?? '' }}</span>
|
||||
<template v-if="column.enableSorting">
|
||||
|
||||
@@ -823,3 +823,32 @@ function getParamValuesAsArray(x: LocationQueryValue | LocationQueryValue[]): st
|
||||
return x.filter((x) => x !== null)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDependentsSearchFilters(
|
||||
projectTypes: readonly ProjectType[],
|
||||
dependencyProjectIds: readonly string[],
|
||||
): string {
|
||||
const parts: string[] = []
|
||||
const mappedProjectTypes = projectTypes.map(mapProjectTypeToSearch)
|
||||
|
||||
if (mappedProjectTypes.length === 1) {
|
||||
parts.push(`project_types = ${formatSearchFilterValue(mappedProjectTypes[0])}`)
|
||||
} else if (mappedProjectTypes.length > 1) {
|
||||
const quoted = mappedProjectTypes.map(formatSearchFilterValue).join(', ')
|
||||
parts.push(`project_types IN [${quoted}]`)
|
||||
}
|
||||
|
||||
const normalizedProjectIds = Array.from(
|
||||
new Set(dependencyProjectIds.map((projectId) => projectId.trim()).filter(Boolean)),
|
||||
)
|
||||
if (normalizedProjectIds.length === 1) {
|
||||
parts.push(
|
||||
`compatible_dependency_project_ids = ${formatSearchFilterValue(normalizedProjectIds[0])}`,
|
||||
)
|
||||
} else if (normalizedProjectIds.length > 1) {
|
||||
const quoted = normalizedProjectIds.map(formatSearchFilterValue).join(', ')
|
||||
parts.push(`compatible_dependency_project_ids IN [${quoted}]`)
|
||||
}
|
||||
|
||||
return parts.join(' AND ')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user