android: add HealthView (#458)
Updates tailscale/tailscale#4136 This PR adds a proper health warnings viewer for the Android client, like we already do on iOS and macOS. A subtile info.circle or exclamation mark icon is displayed next to the connection status when one or more warnings are found. A detail view provides visibility into the full list. Signed-off-by: Andrea Gottardo <andrea@gottardo.me>pull/464/head
parent
403aa092c4
commit
338c13b6b5
@ -0,0 +1,99 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.tailscale.ipn.R
|
||||
import com.tailscale.ipn.ui.model.Health
|
||||
import com.tailscale.ipn.ui.theme.success
|
||||
import com.tailscale.ipn.ui.viewModel.HealthViewModel
|
||||
|
||||
@Composable
|
||||
fun HealthView(backToSettings: BackNavigation, model: HealthViewModel = viewModel()) {
|
||||
val warnings by model.warnings.collectAsState()
|
||||
|
||||
Scaffold(topBar = { Header(titleRes = R.string.health_warnings, onBack = backToSettings) }) {
|
||||
innerPadding ->
|
||||
LazyColumn(modifier = Modifier.padding(innerPadding)) {
|
||||
if (warnings.isEmpty()) {
|
||||
item("allGood") {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.Top),
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.check_circle),
|
||||
modifier = Modifier.size(48.dp),
|
||||
contentDescription = "A green checkmark",
|
||||
tint = MaterialTheme.colorScheme.success)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement =
|
||||
Arrangement.spacedBy(2.dp, alignment = Alignment.CenterVertically),
|
||||
modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_issues_found),
|
||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
||||
fontWeight = MaterialTheme.typography.titleMedium.fontWeight)
|
||||
Text(
|
||||
text = stringResource(R.string.tailscale_is_operating_normally),
|
||||
color = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(warnings) { HealthWarningView(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HealthWarningView(warning: Health.UnhealthyState) {
|
||||
Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerLow)) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
|
||||
.clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp))
|
||||
.fillMaxWidth()) {
|
||||
ListItem(
|
||||
colors = warning.Severity.listItemColors(),
|
||||
headlineContent = {
|
||||
if (warning.Title.isNotEmpty()) {
|
||||
Text(
|
||||
warning.Title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
supportingContent = {
|
||||
Text(warning.Text, style = MaterialTheme.typography.bodyMedium)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.viewModel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tailscale.ipn.App
|
||||
import com.tailscale.ipn.ui.model.Health
|
||||
import com.tailscale.ipn.ui.util.set
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HealthViewModel : ViewModel() {
|
||||
val warnings: StateFlow<List<Health.UnhealthyState>> = MutableStateFlow(listOf())
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
App.get().healthNotifier?.currentWarnings?.collect { set -> warnings.set(set.sorted()) }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M440,680L520,680L520,440L440,440L440,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M4.47,21h15.06c1.54,0 2.5,-1.67 1.73,-3L13.73,4.99c-0.77,-1.33 -2.69,-1.33 -3.46,0L2.74,18c-0.77,1.33 0.19,3 1.73,3zM12,14c-0.55,0 -1,-0.45 -1,-1v-2c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v2c0,0.55 -0.45,1 -1,1zM13,18h-2v-2h2v2z"/>
|
||||
|
||||
</vector>
|
Loading…
Reference in New Issue