From 54a5b38fc61efb653f20fe5cf1bcd2b715eff139 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 18 Mar 2026 16:57:38 +0100 Subject: [PATCH] feat: add countries, devices, browsers, OS dashboard sections Add 4 new dimension queries (concurrent with existing 4, total 8) to the dashboard: countries, devices, browsers, and operating systems. All reuse the existing DimensionSection component with proportional progress bars. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../data/repository/StatsRepository.kt | 66 ++++++++++++++++++- .../implausibly/domain/model/DashboardData.kt | 6 +- .../ui/dashboard/DashboardScreen.kt | 20 ++++++ .../ui/dashboard/sections/BrowsersSection.kt | 18 +++++ .../ui/dashboard/sections/CountriesSection.kt | 18 +++++ .../ui/dashboard/sections/DevicesSection.kt | 18 +++++ .../sections/OperatingSystemsSection.kt | 18 +++++ .../data/repository/StatsRepositoryTest.kt | 22 +++++++ 8 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/BrowsersSection.kt create mode 100644 app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/CountriesSection.kt create mode 100644 app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/DevicesSection.kt create mode 100644 app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/OperatingSystemsSection.kt diff --git a/app/src/main/java/no/naiv/implausibly/data/repository/StatsRepository.kt b/app/src/main/java/no/naiv/implausibly/data/repository/StatsRepository.kt index 5842209..9e893ff 100644 --- a/app/src/main/java/no/naiv/implausibly/data/repository/StatsRepository.kt +++ b/app/src/main/java/no/naiv/implausibly/data/repository/StatsRepository.kt @@ -134,16 +134,80 @@ class StatsRepository @Inject constructor( ) } + val countriesDeferred = async { + api.query( + baseUrl, apiKey, + QueryRequest( + siteId = siteId, + metrics = listOf("visitors"), + dateRange = apiDateRange, + dimensions = listOf("visit:country_name"), + orderBy = listOf(listOf("visitors", "desc")), + pagination = Pagination(limit = 10) + ) + ) + } + + val devicesDeferred = async { + api.query( + baseUrl, apiKey, + QueryRequest( + siteId = siteId, + metrics = listOf("visitors"), + dateRange = apiDateRange, + dimensions = listOf("visit:device"), + orderBy = listOf(listOf("visitors", "desc")), + pagination = Pagination(limit = 10) + ) + ) + } + + val browsersDeferred = async { + api.query( + baseUrl, apiKey, + QueryRequest( + siteId = siteId, + metrics = listOf("visitors"), + dateRange = apiDateRange, + dimensions = listOf("visit:browser"), + orderBy = listOf(listOf("visitors", "desc")), + pagination = Pagination(limit = 10) + ) + ) + } + + val osDeferred = async { + api.query( + baseUrl, apiKey, + QueryRequest( + siteId = siteId, + metrics = listOf("visitors"), + dateRange = apiDateRange, + dimensions = listOf("visit:os"), + orderBy = listOf(listOf("visitors", "desc")), + pagination = Pagination(limit = 10) + ) + ) + } + val topStats = parseTopStats(topStatsDeferred.await()) val timeSeries = parseTimeSeries(timeSeriesDeferred.await()) val topSources = parseDimension(topSourcesDeferred.await()) val topPages = parseDimension(topPagesDeferred.await()) + val countries = parseDimension(countriesDeferred.await()) + val devices = parseDimension(devicesDeferred.await()) + val browsers = parseDimension(browsersDeferred.await()) + val operatingSystems = parseDimension(osDeferred.await()) DashboardData( topStats = topStats, timeSeries = timeSeries, topSources = topSources, - topPages = topPages + topPages = topPages, + countries = countries, + devices = devices, + browsers = browsers, + operatingSystems = operatingSystems ) } diff --git a/app/src/main/java/no/naiv/implausibly/domain/model/DashboardData.kt b/app/src/main/java/no/naiv/implausibly/domain/model/DashboardData.kt index 2a29ce8..b1064dd 100644 --- a/app/src/main/java/no/naiv/implausibly/domain/model/DashboardData.kt +++ b/app/src/main/java/no/naiv/implausibly/domain/model/DashboardData.kt @@ -12,5 +12,9 @@ data class DashboardData( val topStats: TopStatsData, val timeSeries: List, val topSources: List, - val topPages: List + val topPages: List, + val countries: List = emptyList(), + val devices: List = emptyList(), + val browsers: List = emptyList(), + val operatingSystems: List = emptyList() ) diff --git a/app/src/main/java/no/naiv/implausibly/ui/dashboard/DashboardScreen.kt b/app/src/main/java/no/naiv/implausibly/ui/dashboard/DashboardScreen.kt index a0e4b62..1fbfd70 100644 --- a/app/src/main/java/no/naiv/implausibly/ui/dashboard/DashboardScreen.kt +++ b/app/src/main/java/no/naiv/implausibly/ui/dashboard/DashboardScreen.kt @@ -32,6 +32,10 @@ import no.naiv.implausibly.ui.common.UiState import no.naiv.implausibly.ui.dashboard.components.DateRangeSelector import no.naiv.implausibly.ui.dashboard.components.StatCard import no.naiv.implausibly.ui.dashboard.components.VisitorChart +import no.naiv.implausibly.ui.dashboard.sections.BrowsersSection +import no.naiv.implausibly.ui.dashboard.sections.CountriesSection +import no.naiv.implausibly.ui.dashboard.sections.DevicesSection +import no.naiv.implausibly.ui.dashboard.sections.OperatingSystemsSection import no.naiv.implausibly.ui.dashboard.sections.TopPagesSection import no.naiv.implausibly.ui.dashboard.sections.TopSourcesSection @@ -130,6 +134,22 @@ private fun DashboardContent( TopPagesSection(entries = data.topPages) } + item { + CountriesSection(entries = data.countries) + } + + item { + DevicesSection(entries = data.devices) + } + + item { + BrowsersSection(entries = data.browsers) + } + + item { + OperatingSystemsSection(entries = data.operatingSystems) + } + item { Spacer(modifier = Modifier.height(8.dp)) } diff --git a/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/BrowsersSection.kt b/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/BrowsersSection.kt new file mode 100644 index 0000000..efc0f15 --- /dev/null +++ b/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/BrowsersSection.kt @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-only +package no.naiv.implausibly.ui.dashboard.sections + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import no.naiv.implausibly.domain.model.DimensionEntry + +@Composable +fun BrowsersSection( + entries: List, + modifier: Modifier = Modifier +) { + DimensionSection( + title = "Browsers", + entries = entries, + modifier = modifier + ) +} diff --git a/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/CountriesSection.kt b/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/CountriesSection.kt new file mode 100644 index 0000000..b0cbd3d --- /dev/null +++ b/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/CountriesSection.kt @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-only +package no.naiv.implausibly.ui.dashboard.sections + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import no.naiv.implausibly.domain.model.DimensionEntry + +@Composable +fun CountriesSection( + entries: List, + modifier: Modifier = Modifier +) { + DimensionSection( + title = "Countries", + entries = entries, + modifier = modifier + ) +} diff --git a/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/DevicesSection.kt b/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/DevicesSection.kt new file mode 100644 index 0000000..e06a21c --- /dev/null +++ b/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/DevicesSection.kt @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-only +package no.naiv.implausibly.ui.dashboard.sections + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import no.naiv.implausibly.domain.model.DimensionEntry + +@Composable +fun DevicesSection( + entries: List, + modifier: Modifier = Modifier +) { + DimensionSection( + title = "Devices", + entries = entries, + modifier = modifier + ) +} diff --git a/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/OperatingSystemsSection.kt b/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/OperatingSystemsSection.kt new file mode 100644 index 0000000..5557596 --- /dev/null +++ b/app/src/main/java/no/naiv/implausibly/ui/dashboard/sections/OperatingSystemsSection.kt @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-only +package no.naiv.implausibly.ui.dashboard.sections + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import no.naiv.implausibly.domain.model.DimensionEntry + +@Composable +fun OperatingSystemsSection( + entries: List, + modifier: Modifier = Modifier +) { + DimensionSection( + title = "Operating Systems", + entries = entries, + modifier = modifier + ) +} diff --git a/app/src/test/java/no/naiv/implausibly/data/repository/StatsRepositoryTest.kt b/app/src/test/java/no/naiv/implausibly/data/repository/StatsRepositoryTest.kt index 39143ee..2ad41f4 100644 --- a/app/src/test/java/no/naiv/implausibly/data/repository/StatsRepositoryTest.kt +++ b/app/src/test/java/no/naiv/implausibly/data/repository/StatsRepositoryTest.kt @@ -102,6 +102,24 @@ class StatsRepositoryTest { ) ) + // Mock countries, devices, browsers, OS responses + coEvery { api.query(any(), any(), match { "visit:country_name" in it.dimensions }) } returns + QueryResponse(results = listOf( + QueryResult(dimensions = listOf(JsonPrimitive("Germany")), metrics = listOf(JsonPrimitive(50))) + )) + coEvery { api.query(any(), any(), match { "visit:device" in it.dimensions }) } returns + QueryResponse(results = listOf( + QueryResult(dimensions = listOf(JsonPrimitive("Desktop")), metrics = listOf(JsonPrimitive(80))) + )) + coEvery { api.query(any(), any(), match { "visit:browser" in it.dimensions }) } returns + QueryResponse(results = listOf( + QueryResult(dimensions = listOf(JsonPrimitive("Firefox")), metrics = listOf(JsonPrimitive(60))) + )) + coEvery { api.query(any(), any(), match { "visit:os" in it.dimensions }) } returns + QueryResponse(results = listOf( + QueryResult(dimensions = listOf(JsonPrimitive("Linux")), metrics = listOf(JsonPrimitive(40))) + )) + val result = repository.getDashboardData( instance = testInstance, apiKey = "test-key", @@ -118,6 +136,10 @@ class StatsRepositoryTest { assertEquals("Google", result.topSources[0].name) assertEquals(1, result.topPages.size) assertEquals("/blog", result.topPages[0].name) + assertEquals("Germany", result.countries[0].name) + assertEquals("Desktop", result.devices[0].name) + assertEquals("Firefox", result.browsers[0].name) + assertEquals("Linux", result.operatingSystems[0].name) } @Test