diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue index c5c4d3112a..6dd495c9c9 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue @@ -25,7 +25,7 @@ class="inline-flex items-center" > + + + + + + {{ label }} + + + + + + + + diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts index 1f5f414d02..21cae3cd79 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts @@ -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[] { @@ -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 diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-csv-export.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-csv-export.ts index ee0cab3efd..1bd6f21ef8 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-csv-export.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-csv-export.ts @@ -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': diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts index 04c7d0a12e..a67abece6a 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts @@ -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,6 +44,9 @@ type BuildAnalyticsTableRowsOptions = { timeSlices: Labrinth.Analytics.v3.TimeSlice[] selectedBreakdowns: readonly AnalyticsTableBreakdownPreset[] selectedProjectIds: ReadonlySet + selectedFilters: AnalyticsSelectedFilters + dependentProjectTypesById: ReadonlyMap + includeDependentProjectTooltipContext: boolean relevantStats: ReadonlySet projectNamesById: ReadonlyMap getVersionDisplayName: (versionId: string) => string @@ -51,6 +62,9 @@ export function buildAnalyticsTableRows({ timeSlices, selectedBreakdowns, selectedProjectIds, + selectedFilters, + dependentProjectTypesById, + includeDependentProjectTooltipContext, relevantStats, projectNamesById, getVersionDisplayName, @@ -72,6 +86,7 @@ export function buildAnalyticsTableRows({ const projectDisplayValues = new Map() const nextRows = new Map() const bucketLabelsBySliceIndex = new Map() + const normalizedFilters = normalizeAnalyticsSelectedFilters(selectedFilters) function getBreakdownDisplayValue( breakdownValue: string, @@ -111,6 +126,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 +142,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 +194,7 @@ export function buildAnalyticsTableRows({ function createRow( rowId: string, breakdownValues: readonly string[], + dependentOnProjectId?: string, bucketLabel?: { date: string; dateMs: number }, ) { const breakdownKey = @@ -169,6 +207,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 +234,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 +267,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 +291,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) } }) @@ -295,7 +369,16 @@ function formatAnalyticsTableBreakdownDisplayValue( 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) diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-search-filtering.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-search-filtering.ts index 406c3c8c3a..a70e24d5df 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-search-filtering.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-search-filtering.ts @@ -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(['date', 'project']) +const SEARCHABLE_COLUMN_KEYS = new Set(['date', 'project', 'dependent_on']) export function getAnalyticsTableSearchableColumns( columns: TableColumn[], @@ -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: diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-sorting.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-sorting.ts index d55f8ef267..0b47d7accb 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-sorting.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-sorting.ts @@ -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( diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-types.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-types.ts index da343e7239..167b5996fa 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-types.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-types.ts @@ -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 diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/index.vue b/apps/frontend/src/components/analytics-dashboard/analytics-table/index.vue index ac0dcdb1f5..1b3734728b 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/index.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/index.vue @@ -55,32 +55,66 @@ {{ value }} - - {{ value }} + + - {{ value }} + {{ value }} - {{ value }} + {{ value }} - {{ value }} + {{ value }} - {{ value }} + {{ value }} - - {{ value }} + + + + + + {{ value }} + - {{ value }} + {{ value }} - {{ value }} - - - {{ value }} + {{ value }} {{ formatInteger(row.views) }} @@ -163,6 +197,10 @@ import { analyticsTableMessages, } from '../analytics-messages.ts' import AnalyticsLoadingBar from '../AnalyticsLoadingBar.vue' +import { + isNoDependentAnalyticsBreakdownValue, + isUnknownAnalyticsBreakdownValue, +} from '../breakdown.ts' import { buildAnalyticsTableColumns, getAnalyticsTableBreakdownColumnLabel, @@ -194,8 +232,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 +261,13 @@ const { getRelevantAnalyticsDashboardStats, isLoading, versionNumbersById, + versionProjectIdsById, versionProjectNamesById, + projectNamesById, + projectIconUrlsById, + projectOrganizationIdsById, + projectOrganizationNamesById, + dependentProjectTypesById, getVersionDisplayName, getVersionProjectName, } = injectAnalyticsDashboardContext() @@ -263,11 +309,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 +359,6 @@ const csvExportOptions = computed(() => { }, ] }) -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,6 +402,9 @@ 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, getVersionDisplayName, @@ -379,12 +424,69 @@ 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 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 + ? `Dependent on ${dependencyProjectNames.join(', ')}` + : undefined +} + watch( activeColumns, (nextColumns) => { @@ -445,6 +547,8 @@ watch( selectedBreakdowns, selectedFilters, projects, + dependentProjectTypesById, + projectNamesById, versionNumbersById, versionProjectNamesById, ], diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/use-analytics-table-graph-selection.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/use-analytics-table-graph-selection.ts index bfb609ddf7..1fec06b030 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/use-analytics-table-graph-selection.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/use-analytics-table-graph-selection.ts @@ -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({ 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 { + return new Set( + rows + .filter((row) => + Object.values(row.breakdownValues).some( + (value) => + isUnknownAnalyticsBreakdownValue(value) || + isNoDependentAnalyticsBreakdownValue(value), + ), + ) + .map((row) => row.graphDatasetId), + ) + } + return { filteredSelectableGraphDatasetIds, tableSelectedGraphDatasetIds, diff --git a/apps/frontend/src/components/analytics-dashboard/breakdown.ts b/apps/frontend/src/components/analytics-dashboard/breakdown.ts index 9de2e97452..ff6b00bc6a 100644 --- a/apps/frontend/src/components/analytics-dashboard/breakdown.ts +++ b/apps/frontend/src/components/analytics-dashboard/breakdown.ts @@ -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,17 @@ export function getAnalyticsBreakdownValue( 'reason' in point ? point.reason : undefined, UNKNOWN_BREAKDOWN_VALUE, ) + 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 +106,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 diff --git a/apps/frontend/src/components/analytics-dashboard/query-builder/QueryFilter.vue b/apps/frontend/src/components/analytics-dashboard/query-builder/QueryFilter.vue index 2be3b92704..b7faad0271 100644 --- a/apps/frontend/src/components/analytics-dashboard/query-builder/QueryFilter.vue +++ b/apps/frontend/src/components/analytics-dashboard/query-builder/QueryFilter.vue @@ -25,10 +25,11 @@ - + @@ -42,6 +43,8 @@ @@ -181,14 +184,22 @@