Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ import app.getarcane.android.core.runSuspendCatching
import app.getarcane.android.nav.AppTab
import app.getarcane.android.ui.screens.activities.ActivitiesTab
import app.getarcane.android.ui.screens.settings.FormErrorRow
import app.getarcane.android.ui.screens.settings.FormSuccessRow
import app.getarcane.android.ui.screens.settings.LabeledPicker
import app.getarcane.android.ui.screens.settings.LabeledTextField
import app.getarcane.android.ui.screens.settings.SettingsSectionHeader
Expand Down Expand Up @@ -172,6 +171,8 @@ fun DashboardScreen(
val manager = LocalArcaneManager.current
val client = manager.client
val envId = manager.activeEnvironmentId
val scope = rememberCoroutineScope()
val snackbar = remember { SnackbarHostState() }

val supportsActivities = manager.capabilities.supportsActivities
val isAdmin = manager.currentUser?.isGlobalAdmin ?: false
Expand Down Expand Up @@ -356,6 +357,7 @@ fun DashboardScreen(
?: totals?.updates

Scaffold(
snackbarHost = { SnackbarHost(snackbar) },
topBar = {
TopAppBar(
title = { Text("Dashboard") },
Expand Down Expand Up @@ -538,12 +540,16 @@ fun DashboardScreen(
}

pruneEnvironmentId?.let { pruneEnvId ->
SystemPruneSheet(
envId = pruneEnvId,
onDismiss = { pruneEnvironmentId = null },
onComplete = { refreshKey++ },
)
}
SystemPruneSheet(
envId = pruneEnvId,
onDismiss = { pruneEnvironmentId = null },
onComplete = { message ->
pruneEnvironmentId = null
refreshKey++
scope.launch { snackbar.showSnackbar(message) }
},
)
}
}

private suspend fun loadDashboardImageUpdateCount(
Expand Down Expand Up @@ -959,7 +965,7 @@ private fun DashboardTile(

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SystemPruneSheet(envId: EnvironmentId, onDismiss: () -> Unit, onComplete: () -> Unit) {
private fun SystemPruneSheet(envId: EnvironmentId, onDismiss: () -> Unit, onComplete: (String) -> Unit) {
val manager = LocalArcaneManager.current
val scope = rememberCoroutineScope()

Expand All @@ -972,10 +978,29 @@ private fun SystemPruneSheet(envId: EnvironmentId, onDismiss: () -> Unit, onComp
var networkUntil by remember { mutableStateOf("") }
var buildCacheMode by remember { mutableStateOf(PruneBuildCacheMode.NONE) }
var buildCacheUntil by remember { mutableStateOf("") }
var loadingDefaults by remember { mutableStateOf(false) }
var submitting by remember { mutableStateOf(false) }
var resultMessage by remember { mutableStateOf<String?>(null) }
var errorMessage by remember { mutableStateOf<String?>(null) }

LaunchedEffect(envId.rawValue) {
val client = manager.client ?: return@LaunchedEffect
loadingDefaults = true
try {
val defaults = client.settings.getSettings(envId).associate { it.key to it.value }
parsePruneContainerMode(defaults["pruneContainerMode"])?.let { containerMode = it }
defaults["pruneContainerUntil"]?.takeIf { it.isNotBlank() }?.let { containerUntil = it }
parsePruneImageMode(defaults["pruneImageMode"])?.let { imageMode = it }
defaults["pruneImageUntil"]?.takeIf { it.isNotBlank() }?.let { imageUntil = it }
parsePruneVolumeMode(defaults["pruneVolumeMode"])?.let { volumeMode = it }
parsePruneNetworkMode(defaults["pruneNetworkMode"])?.let { networkMode = it }
defaults["pruneNetworkUntil"]?.takeIf { it.isNotBlank() }?.let { networkUntil = it }
parsePruneBuildCacheMode(defaults["pruneBuildCacheMode"])?.let { buildCacheMode = it }
defaults["pruneBuildCacheUntil"]?.takeIf { it.isNotBlank() }?.let { buildCacheUntil = it }
} finally {
loadingDefaults = false
}
}

val selectedCount = listOf(
containerMode != PruneContainerMode.NONE,
imageMode != PruneImageMode.NONE,
Expand Down Expand Up @@ -1004,10 +1029,8 @@ private fun SystemPruneSheet(envId: EnvironmentId, onDismiss: () -> Unit, onComp
scope.launch {
submitting = true
errorMessage = null
resultMessage = null
try {
resultMessage = formatPruneResult(client.system.prune(request, envId))
onComplete()
onComplete(formatSystemPruneResult(client.system.prune(request, envId)))
} catch (e: Throwable) {
errorMessage = friendlyErrorMessage(e)
} finally {
Expand All @@ -1028,8 +1051,8 @@ private fun SystemPruneSheet(envId: EnvironmentId, onDismiss: () -> Unit, onComp
}
},
actions = {
TextButton(onClick = { runPrune() }, enabled = selectedCount > 0 && !submitting) {
if (submitting) {
TextButton(onClick = { runPrune() }, enabled = selectedCount > 0 && !submitting && !loadingDefaults) {
if (submitting || loadingDefaults) {
CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(16.dp))
} else {
Text(if (selectedCount > 0) "Prune ($selectedCount)" else "Prune")
Expand Down Expand Up @@ -1084,19 +1107,20 @@ private fun SystemPruneSheet(envId: EnvironmentId, onDismiss: () -> Unit, onComp
item { LabeledTextField("Older than", buildCacheUntil, { buildCacheUntil = it }, placeholder = "24h") }
}

resultMessage?.let { msg ->
item { FormSuccessRow(msg) }
}
errorMessage?.let { msg ->
item { FormErrorRow(msg) }
}
item {
Button(
onClick = { runPrune() },
enabled = selectedCount > 0 && !submitting,
enabled = selectedCount > 0 && !submitting && !loadingDefaults,
modifier = Modifier.fillMaxWidth().padding(16.dp),
) {
Text(if (selectedCount > 0) "Prune ($selectedCount)" else "Prune")
if (submitting || loadingDefaults) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text(if (selectedCount > 0) "Prune ($selectedCount)" else "Prune")
}
}
}
}
Expand All @@ -1105,6 +1129,21 @@ private fun SystemPruneSheet(envId: EnvironmentId, onDismiss: () -> Unit, onComp
}
}

private fun parsePruneContainerMode(value: String?): PruneContainerMode? =
PruneContainerMode.entries.firstOrNull { it.wire == value }

private fun parsePruneImageMode(value: String?): PruneImageMode? =
PruneImageMode.entries.firstOrNull { it.wire == value }

private fun parsePruneVolumeMode(value: String?): PruneVolumeMode? =
PruneVolumeMode.entries.firstOrNull { it.wire == value }

private fun parsePruneNetworkMode(value: String?): PruneNetworkMode? =
PruneNetworkMode.entries.firstOrNull { it.wire == value }

private fun parsePruneBuildCacheMode(value: String?): PruneBuildCacheMode? =
PruneBuildCacheMode.entries.firstOrNull { it.wire == value }

private val PruneContainerMode.label: String
get() = when (this) {
PruneContainerMode.NONE -> "None"
Expand Down Expand Up @@ -1142,15 +1181,27 @@ private val PruneBuildCacheMode.label: String
PruneBuildCacheMode.OLDER_THAN -> "Older than..."
}

private fun formatPruneResult(result: PruneAllResult): String {
internal fun formatSystemPruneResult(result: PruneAllResult): String {
val parts = buildList {
result.containersPruned?.size?.takeIf { it > 0 }?.let { add("$it container${if (it == 1) "" else "s"}") }
result.imagesDeleted?.size?.takeIf { it > 0 }?.let { add("$it image${if (it == 1) "" else "s"}") }
result.volumesDeleted?.size?.takeIf { it > 0 }?.let { add("$it volume${if (it == 1) "" else "s"}") }
result.networksDeleted?.size?.takeIf { it > 0 }?.let { add("$it network${if (it == 1) "" else "s"}") }
result.buildCacheSpaceReclaimed?.takeIf { it > 0 }?.let { add("build cache") }
}
val reclaimedBytes = result.spaceReclaimed.takeIf { it > 0 }
?: listOfNotNull(
result.containerSpaceReclaimed,
result.imageSpaceReclaimed,
result.volumeSpaceReclaimed,
result.buildCacheSpaceReclaimed,
).sum().takeIf { it > 0 }
val summary = when {
parts.isNotEmpty() -> "Pruned ${parts.joinToString(", ")}."
reclaimedBytes != null -> "Pruned resources."
else -> "No resources pruned."
}
val summary = if (parts.isEmpty()) "No resources pruned." else "Pruned ${parts.joinToString(", ")}."
val reclaimed = result.spaceReclaimed.takeIf { it > 0 }?.let { " Freed ${formatBytes(it)}." }.orEmpty()
val reclaimed = reclaimedBytes?.let { " Freed ${formatBytes(it)}." }.orEmpty()
val errors = result.errors?.takeIf { it.isNotEmpty() }?.joinToString(prefix = " Errors: ")
return summary + reclaimed + (errors ?: "")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package app.getarcane.android.ui.screens

import app.getarcane.sdk.models.system.PruneAllResult
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test

class DashboardScreenTest {
@Test
fun systemPruneResultIncludesBuildCacheWhenOnlyBuildCacheWasPruned() {
val message = formatSystemPruneResult(
PruneAllResult(
buildCacheSpaceReclaimed = 1024,
spaceReclaimed = 1024,
success = true,
),
)

assertEquals("Pruned build cache. Freed 1.0 KB.", message)
assertFalse(message.contains("No resources pruned"))
}

@Test
fun systemPruneResultUsesPositiveSummaryWhenOnlyReclaimedBytesAreReported() {
val message = formatSystemPruneResult(
PruneAllResult(
spaceReclaimed = 1024,
success = true,
),
)

assertEquals("Pruned resources. Freed 1.0 KB.", message)
}

@Test
fun systemPruneResultKeepsEmptyResultCopyWhenNothingWasPruned() {
val message = formatSystemPruneResult(PruneAllResult(success = true))

assertEquals("No resources pruned.", message)
}
}
Loading