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) <noreply@anthropic.com>
This commit is contained in:
parent
d9e4b18a52
commit
54a5b38fc6
8 changed files with 184 additions and 2 deletions
|
|
@ -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 topStats = parseTopStats(topStatsDeferred.await())
|
||||||
val timeSeries = parseTimeSeries(timeSeriesDeferred.await())
|
val timeSeries = parseTimeSeries(timeSeriesDeferred.await())
|
||||||
val topSources = parseDimension(topSourcesDeferred.await())
|
val topSources = parseDimension(topSourcesDeferred.await())
|
||||||
val topPages = parseDimension(topPagesDeferred.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(
|
DashboardData(
|
||||||
topStats = topStats,
|
topStats = topStats,
|
||||||
timeSeries = timeSeries,
|
timeSeries = timeSeries,
|
||||||
topSources = topSources,
|
topSources = topSources,
|
||||||
topPages = topPages
|
topPages = topPages,
|
||||||
|
countries = countries,
|
||||||
|
devices = devices,
|
||||||
|
browsers = browsers,
|
||||||
|
operatingSystems = operatingSystems
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,9 @@ data class DashboardData(
|
||||||
val topStats: TopStatsData,
|
val topStats: TopStatsData,
|
||||||
val timeSeries: List<TimeSeriesPoint>,
|
val timeSeries: List<TimeSeriesPoint>,
|
||||||
val topSources: List<DimensionEntry>,
|
val topSources: List<DimensionEntry>,
|
||||||
val topPages: List<DimensionEntry>
|
val topPages: List<DimensionEntry>,
|
||||||
|
val countries: List<DimensionEntry> = emptyList(),
|
||||||
|
val devices: List<DimensionEntry> = emptyList(),
|
||||||
|
val browsers: List<DimensionEntry> = emptyList(),
|
||||||
|
val operatingSystems: List<DimensionEntry> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.DateRangeSelector
|
||||||
import no.naiv.implausibly.ui.dashboard.components.StatCard
|
import no.naiv.implausibly.ui.dashboard.components.StatCard
|
||||||
import no.naiv.implausibly.ui.dashboard.components.VisitorChart
|
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.TopPagesSection
|
||||||
import no.naiv.implausibly.ui.dashboard.sections.TopSourcesSection
|
import no.naiv.implausibly.ui.dashboard.sections.TopSourcesSection
|
||||||
|
|
||||||
|
|
@ -130,6 +134,22 @@ private fun DashboardContent(
|
||||||
TopPagesSection(entries = data.topPages)
|
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 {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<DimensionEntry>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
DimensionSection(
|
||||||
|
title = "Browsers",
|
||||||
|
entries = entries,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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<DimensionEntry>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
DimensionSection(
|
||||||
|
title = "Countries",
|
||||||
|
entries = entries,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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<DimensionEntry>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
DimensionSection(
|
||||||
|
title = "Devices",
|
||||||
|
entries = entries,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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<DimensionEntry>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
DimensionSection(
|
||||||
|
title = "Operating Systems",
|
||||||
|
entries = entries,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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(
|
val result = repository.getDashboardData(
|
||||||
instance = testInstance,
|
instance = testInstance,
|
||||||
apiKey = "test-key",
|
apiKey = "test-key",
|
||||||
|
|
@ -118,6 +136,10 @@ class StatsRepositoryTest {
|
||||||
assertEquals("Google", result.topSources[0].name)
|
assertEquals("Google", result.topSources[0].name)
|
||||||
assertEquals(1, result.topPages.size)
|
assertEquals(1, result.topPages.size)
|
||||||
assertEquals("/blog", result.topPages[0].name)
|
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
|
@Test
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue