Update UI according to feedback (#37423)

This commit is contained in:
Dante Catalfamo
2025-12-18 10:30:25 -05:00
committed by GitHub
parent cfc944e49a
commit cb0fb18fef
5 changed files with 76 additions and 46 deletions
@@ -51,7 +51,7 @@ class AgentApplication : Application() {
ApiClient.setEnrollmentCredentials(
enrollSecret = enrollSecret,
hardwareUUID = hostUUID,
baseUrl = serverURL,
serverUrl = serverURL,
computerName = "${Build.BRAND} ${Build.MODEL}",
)
@@ -30,7 +30,7 @@ object ApiClient {
private lateinit var dataStore: DataStore<Preferences>
private val API_KEY = stringPreferencesKey("api_key")
private val BASE_URL_KEY = stringPreferencesKey("base_url")
private val SERVER_URL_KEY = stringPreferencesKey("server_url")
private val ENROLL_SECRET = stringPreferencesKey("enroll_secret")
private val HARDWARE_UUID = stringPreferencesKey("hardware_uuid")
private val COMPUTER_NAME = stringPreferencesKey("computer_name")
@@ -50,27 +50,9 @@ object ApiClient {
}
}
suspend fun setBaseUrl(url: String) {
dataStore.edit { preferences ->
preferences[BASE_URL_KEY] = url
}
}
val apiKeyDebugFlow: Flow<String?>
get() = dataStore.data.map { preferences ->
preferences[API_KEY]?.let { encrypted ->
try {
"****" + KeystoreManager.decrypt(encrypted).takeLast(4)
} catch (e: Exception) {
Log.e("ApiClient", "Failed to decrypt API key", e)
null
}
}
}
val baseUrlFlow: Flow<String?>
get() = dataStore.data.map { preferences ->
preferences[BASE_URL_KEY]
preferences[SERVER_URL_KEY]
}
suspend fun getApiKey(): String? {
@@ -83,7 +65,7 @@ object ApiClient {
}
}
suspend fun getBaseUrl(): String? = dataStore.data.first()[BASE_URL_KEY]
suspend fun getBaseUrl(): String? = dataStore.data.first()[SERVER_URL_KEY]
private suspend fun <R, T> makeRequest(
endpoint: String,
@@ -205,12 +187,12 @@ object ApiClient {
)
}
suspend fun setEnrollmentCredentials(enrollSecret: String, hardwareUUID: String, computerName: String, baseUrl: String) {
suspend fun setEnrollmentCredentials(enrollSecret: String, hardwareUUID: String, computerName: String, serverUrl: String) {
dataStore.edit { preferences ->
preferences[ENROLL_SECRET] = enrollSecret
preferences[HARDWARE_UUID] = hardwareUUID
preferences[COMPUTER_NAME] = computerName
preferences[BASE_URL_KEY] = baseUrl
preferences[SERVER_URL_KEY] = serverUrl
}
}
@@ -275,7 +257,7 @@ object ApiClient {
val enrollSecret = prefs[ENROLL_SECRET]
val hardwareUUID = prefs[HARDWARE_UUID]
val computerName = prefs[COMPUTER_NAME]
val baseUrl = prefs[BASE_URL_KEY]
val baseUrl = prefs[SERVER_URL_KEY]
if (enrollSecret == null || hardwareUUID == null || computerName == null || baseUrl == null) {
return null
@@ -60,6 +60,8 @@ object CertificateOrchestrator {
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
// Treat a missing field like a null field for optional types
explicitNulls = false
}
// Mutex to protect concurrent access to certificate storage
@@ -293,6 +295,13 @@ object CertificateOrchestrator {
Log.d(TAG, "Successfully fetched certificate template: ${template.name}")
if (template.status != "delivered") {
// The certificate template hasn't failed on the device, but isn't ready to be processed yet.
// Retry next time we fetch but don't mark as failed locally
Log.i(TAG, "Certificate template ${template.name} does not have status \"delivered\": status \"${template.status}\"")
return CertificateEnrollmentHandler.EnrollmentResult.Success(template.name)
}
// Step 3: Create certificate installer (use provided or create default)
val installer = certificateInstaller ?: AndroidCertificateInstaller(context)
@@ -21,6 +21,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
@@ -30,6 +31,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -43,6 +45,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -59,7 +62,6 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.fleetdm.agent.ui.theme.FleetTextDark
import com.fleetdm.agent.ui.theme.MyApplicationTheme
import kotlinx.coroutines.flow.map
import kotlinx.serialization.Serializable
const val CLICKS_TO_DEBUG = 8
@@ -136,7 +138,6 @@ fun MainScreen(onNavigateToDebug: () -> Unit) {
}
HorizontalDivider()
CertificateList(certificates = installedCerts)
HorizontalDivider()
AppVersion {
if (++versionClicks >= CLICKS_TO_DEBUG) {
onNavigateToDebug()
@@ -159,9 +160,7 @@ fun DebugScreen(onNavigateBack: () -> Unit) {
val appRestrictions = restrictionsManager.applicationRestrictions
val dpm = context.getSystemService(DEVICE_POLICY_SERVICE) as DevicePolicyManager
val enrollSecret = remember { appRestrictions.getString("enroll_secret")?.let { "****" + it.takeLast(4) } }
val delegatedScopes = remember { dpm.getDelegatedScopes(null, context.packageName).toList() }
val delegatedCertScope = remember { delegatedScopes.contains(DevicePolicyManager.DELEGATION_CERT_INSTALL) }
val enrollmentSpecificID = remember { appRestrictions.getString("host_uuid")?.let { "****" + it.takeLast(4) } }
val certIds = remember { CertificateOrchestrator.getCertificateIDs(context) }
val permissionsList = remember {
@@ -181,7 +180,6 @@ fun DebugScreen(onNavigateBack: () -> Unit) {
grantedPermissions.toList()
}
val fleetBaseUrl = remember { appRestrictions.getString("server_url") }
val apiKey by ApiClient.apiKeyDebugFlow.collectAsState(initial = null)
val baseUrl by ApiClient.baseUrlFlow.collectAsState(initial = null)
val installedCerts by CertificateOrchestrator.installedCertsFlow(context).collectAsState(initial = emptyMap())
@@ -207,17 +205,14 @@ fun DebugScreen(onNavigateBack: () -> Unit) {
.verticalScroll(rememberScrollState()),
) {
KeyValue("packageName", context.packageName)
KeyValue("versionName", context.packageManager.getPackageInfo(context.packageName, 0).versionName)
KeyValue("longVersionCode", context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode.toString())
KeyValue("enroll_secret", enrollSecret)
KeyValue("versionName", BuildConfig.VERSION_NAME)
KeyValue("longVersionCode", BuildConfig.VERSION_CODE.toString())
KeyValue("delegatedScopes", delegatedScopes.toString())
KeyValue("delegated cert scope", delegatedCertScope.toString())
KeyValue("host_uuid (MC)", enrollmentSpecificID)
KeyValue("server_url (MC)", fleetBaseUrl)
KeyValue("orbit_node_key (datastore)", apiKey)
KeyValue("base_url (datastore)", baseUrl)
KeyValue("server_url (DS)", baseUrl)
KeyValue("certificate_templates->id", certIds.toString())
KeyValue("certs_installed", installedCerts.toString())
DebugCertificateList(certificates = installedCerts)
PermissionList(
permissionsList = permissionsList,
)
@@ -226,6 +221,28 @@ fun DebugScreen(onNavigateBack: () -> Unit) {
)
}
@Composable
fun DebugCertificateList(certificates: CertStatusMap) {
Column {
Text("certificate status:", fontWeight = FontWeight.Bold)
certificates.forEach { (key, value) ->
Row(modifier = Modifier.padding(bottom = 5.dp, start = 10.dp)) {
Text(
text = key.toString(),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 5.dp),
)
Column {
Text(text = "alias: ${value.alias}")
Text(text = "status: ${value.status}")
Text(text = "retries: ${value.retries}")
}
}
}
HorizontalDivider()
}
}
@Composable
fun PermissionList(modifier: Modifier = Modifier, permissionsList: List<String>) {
Column(modifier = modifier) {
@@ -258,14 +275,20 @@ fun AboutFleet(modifier: Modifier = Modifier, onLearnClick: () -> Unit = {}) {
Text(
text = stringResource(R.string.app_description),
)
Text(
text = stringResource(R.string.learn_about_fleet),
fontWeight = FontWeight.Bold,
color = FleetTextDark,
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.padding(top = 10.dp)
.clickable(onClick = onLearnClick),
)
) {
Text(
text = stringResource(R.string.learn_about_fleet),
fontWeight = FontWeight.Bold,
color = FleetTextDark,
)
Icon(imageVector = Icons.AutoMirrored.Default.ArrowForward, contentDescription = "forward arrow")
}
}
}
@@ -280,12 +303,15 @@ fun LogoHeader(modifier: Modifier = Modifier) {
@Composable
fun CertificateList(modifier: Modifier = Modifier, certificates: CertStatusMap) {
Column(modifier = modifier.padding(20.dp)) {
Column(modifier = modifier.padding(all = 20.dp)) {
Text(
text = stringResource(R.string.certificate_list_title),
color = FleetTextDark,
fontWeight = FontWeight.Bold,
)
certificates.ifEmpty {
Text(text = stringResource(R.string.certificate_list_no_certificates))
}
certificates.forEach { (_, value) ->
if (value.status == CertificateInstallStatus.INSTALLED) {
Text(text = value.alias)
@@ -303,7 +329,7 @@ fun AppVersion(onClick: () -> Unit = {}) {
) {
Column(
modifier = Modifier
.padding(20.dp),
.padding(horizontal = 20.dp),
) {
Text(
text = stringResource(R.string.app_version_title),
@@ -330,12 +356,24 @@ fun FleetScreenPreview() {
2 to CertificateInstallInfo(alias = "VPN-3", status = CertificateInstallStatus.FAILED),
),
)
HorizontalDivider()
AppVersion(onClick = {})
}
}
}
@Preview(showBackground = true)
@Composable
fun DebugCertificateListPreview() {
MyApplicationTheme {
DebugCertificateList(
certificates = mapOf(
1 to CertificateInstallInfo(alias = "WIFI-1", status = CertificateInstallStatus.INSTALLED),
2 to CertificateInstallInfo(alias = "VPN-3", status = CertificateInstallStatus.FAILED),
),
)
}
}
@Preview(showBackground = true)
@Composable
fun AboutFleetPreview() {
+2 -1
View File
@@ -2,9 +2,10 @@
<string name="app_name">Fleet</string>
<string name="app_description">The app is installed by your IT administrator to install the certificates needed to connect to corporate networks.</string>
<string name="learn_about_fleet">About Fleet</string>
<string name="learn_about_fleet">About Fleet</string>
<string name="fleet_logo">Fleet logo</string>
<string name="certificate_list_title">Installed certificates</string>
<string name="certificate_list_no_certificates">No certificates installed</string>
<string name="app_version_title">App Version</string>
<!-- Managed Configuration -->