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