Skip to content
Merged
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 @@ -15,7 +15,6 @@ import app.getarcane.sdk.models.activity.Activity
import app.getarcane.sdk.models.activity.ActivityMessage
import app.getarcane.sdk.models.activity.ActivityStreamEvent
import app.getarcane.sdk.models.activity.ActivityStreamEventType
import app.getarcane.sdk.models.base.SearchPaginationSort
import app.getarcane.sdk.models.base.SortOrder
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -319,11 +318,7 @@ class ActivityCenterStore(private val scope: CoroutineScope) {
values.filter { it.isNotEmpty() }.toSortedSet(String.CASE_INSENSITIVE_ORDER).toList()

private suspend fun resolveEnvironments(client: ArcaneClient): List<ActivityEnvironment> {
val items = runCatching {
client.environments.list(
query = SearchPaginationSort(start = 0, limit = 100, sortOrder = SortOrder.ASCENDING),
).data
}.getOrNull().orEmpty()
val items = loadCompleteEnvironments { query -> client.environments.list(query) }
return items.map { environment ->
ActivityEnvironment(
id = EnvironmentId(environment.id),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package app.getarcane.android.core

import app.getarcane.sdk.models.base.SearchPaginationSort
import app.getarcane.sdk.models.base.SortOrder
import app.getarcane.sdk.models.environment.Environment
import app.getarcane.sdk.pagination.PaginatedResponse

internal const val COMPLETE_LIST_LIMIT = -1

internal data class CompleteListResponse<T>(
val items: List<T>,
val totalItems: Long,
val success: Boolean = true,
)

internal class IncompleteCollectionException(message: String) : IllegalStateException(message)

internal fun completeListQuery(
sortBy: String? = null,
sortOrder: SortOrder? = null,
): SearchPaginationSort =
SearchPaginationSort(
start = 0,
limit = COMPLETE_LIST_LIMIT,
sortBy = sortBy,
sortOrder = sortOrder,
)

/**
* Normalizes one endpoint-supported show-all response without ever publishing a partial result.
* Duplicate identities are collapsed in server order. The server may count either raw rows or
* unique identities, but any other reported total means the response is incomplete or malformed.
*/
internal fun <T, K> normalizeCompleteCollection(
resourceName: String,
response: CompleteListResponse<T>,
idOf: (T) -> K,
): List<T> {
if (!response.success) {
throw IncompleteCollectionException("$resourceName response was unsuccessful")
}
val itemsById = linkedMapOf<K, T>()
response.items.forEach { item -> itemsById.putIfAbsent(idOf(item), item) }

val rawItemCount = response.items.size.toLong()
val uniqueItemCount = itemsById.size.toLong()
if (response.totalItems != rawItemCount &&
response.totalItems != uniqueItemCount
) {
throw IncompleteCollectionException(
"$resourceName response counts: raw=$rawItemCount, unique=$uniqueItemCount; " +
"server reported ${response.totalItems}",
)
}
return itemsById.values.toList()
}

internal suspend fun <T, K> loadCompleteCollection(
resourceName: String,
idOf: (T) -> K,
loadAll: suspend () -> CompleteListResponse<T>,
): List<T> = normalizeCompleteCollection(resourceName, loadAll(), idOf)

internal suspend fun <T, K> loadCompletePaginatedCollection(
resourceName: String,
idOf: (T) -> K,
loadAll: suspend () -> PaginatedResponse<T>,
): List<T> {
return loadCompleteCollection(
resourceName = resourceName,
idOf = idOf,
loadAll = {
val response = loadAll()
CompleteListResponse(response.data, response.pagination.totalItems, response.success)
},
)
}

/** All visible environments, using the server's documented finite show-all request. */
internal suspend fun loadCompleteEnvironments(
loadAll: suspend (SearchPaginationSort) -> PaginatedResponse<Environment>,
): List<Environment> =
loadCompletePaginatedCollection(
resourceName = "Environment",
idOf = Environment::id,
loadAll = {
loadAll(
completeListQuery(sortBy = "id", sortOrder = SortOrder.ASCENDING),
)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import app.getarcane.sdk.models.base.arrayValue
import app.getarcane.sdk.models.base.boolValue
import app.getarcane.sdk.models.base.objectValue
import app.getarcane.sdk.models.base.stringValue
import app.getarcane.sdk.models.dashboard.ActionItemKind
import app.getarcane.sdk.models.dashboard.ActionItems
import app.getarcane.sdk.models.dashboard.DashboardEnvironmentOverview
import app.getarcane.sdk.models.dashboard.DashboardEnvironmentsOverview
import app.getarcane.sdk.models.dashboard.EnvironmentSnapshotState
import app.getarcane.sdk.models.environment.Environment

data class DashboardEnvironmentCardOverviewCounts(
Expand All @@ -31,6 +34,19 @@ internal fun DashboardEnvironmentsOverview.toDashTotals(
stopped = summary.containers.stoppedContainers,
)

internal fun ActionItems.imageUpdateActionCount(): Int =
items.firstOrNull { it.kind == ActionItemKind.IMAGE_UPDATES }
?.count
?.coerceAtLeast(0)
?: 0

internal fun DashboardEnvironmentsOverview.imageUpdateActionCount(): Int? {
if (environments.any { it.snapshotState == EnvironmentSnapshotState.ERROR }) return null
return environments
.filter { it.snapshotState == EnvironmentSnapshotState.READY }
.sumOf { it.actionItems.imageUpdateActionCount() }
}

internal fun DashboardEnvironmentOverview.cardOverviewCounts(): DashboardEnvironmentCardOverviewCounts =
DashboardEnvironmentCardOverviewCounts(
running = containers.runningContainers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ import androidx.compose.ui.unit.dp
import app.getarcane.android.core.LocalArcaneManager
import app.getarcane.android.core.LocalPinnedStore
import app.getarcane.android.core.PinnedItemsStore
import app.getarcane.android.core.CompleteListResponse
import app.getarcane.android.core.completeListQuery
import app.getarcane.android.core.displayName
import app.getarcane.android.core.friendlyErrorMessage
import app.getarcane.android.core.isRunning
import app.getarcane.android.core.loadCompleteCollection
import app.getarcane.android.core.loadCompletePaginatedCollection
import app.getarcane.android.ui.components.ResourceStatusBadge
import app.getarcane.android.ui.theme.ArcaneBlue
import app.getarcane.android.ui.theme.ArcaneGreen
Expand All @@ -60,10 +64,10 @@ import app.getarcane.android.ui.theme.ArcaneRed
import app.getarcane.android.ui.theme.ArcaneTeal
import app.getarcane.android.ui.theme.StatusRunning
import app.getarcane.android.ui.theme.StatusUnknown
import app.getarcane.sdk.models.base.SearchPaginationSort
import app.getarcane.sdk.models.container.ContainerSummary
import app.getarcane.sdk.models.project.ProjectDetails
import app.getarcane.sdk.models.volume.Volume as SdkVolume
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch

@Composable
Expand Down Expand Up @@ -101,28 +105,42 @@ fun DashboardPinnedSection(

loading = containers.isEmpty() && projects.isEmpty() && volumes.isEmpty()
containers = if (pinnedContainers.isNotEmpty()) {
runCatching { client.containers.list(envId = envId).data.filter { it.id in pinnedContainers } }
.getOrDefault(emptyList())
try {
loadCompleteCollection("Container", ContainerSummary::id) {
val response = client.containers.list(envId = envId, query = completeListQuery())
CompleteListResponse(response.data, response.pagination.totalItems, response.success)
}.filter { it.id in pinnedContainers }
} catch (e: CancellationException) {
throw e
} catch (_: Throwable) {
emptyList()
}
} else {
emptyList()
}
projects = if (pinnedProjects.isNotEmpty()) {
runCatching {
client.projects.list(
envId = envId,
query = SearchPaginationSort(start = 0, limit = 500),
).data.filter { it.id in pinnedProjects }
}.getOrDefault(emptyList())
try {
loadCompletePaginatedCollection("Project", ProjectDetails::id) {
client.projects.list(envId = envId, query = completeListQuery())
}.filter { it.id in pinnedProjects }
} catch (e: CancellationException) {
throw e
} catch (_: Throwable) {
emptyList()
}
} else {
emptyList()
}
volumes = if (pinnedVolumes.isNotEmpty()) {
runCatching {
client.volumes.list(
envId = envId,
query = SearchPaginationSort(start = 0, limit = 500),
).data.filter { it.id in pinnedVolumes }
}.getOrDefault(emptyList())
try {
loadCompletePaginatedCollection("Volume", SdkVolume::id) {
client.volumes.list(envId = envId, query = completeListQuery())
}.filter { it.id in pinnedVolumes }
} catch (e: CancellationException) {
throw e
} catch (_: Throwable) {
emptyList()
}
} else {
emptyList()
}
Expand Down
Loading
Loading