mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 17:44:50 +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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user