Skip to content

Commit

Permalink
Merge pull request #216 from amosproj/add_description_to_features
Browse files Browse the repository at this point in the history
[UI] fix scrolling on configuration screen, feature descriptions on configuration screen, add autoscroll + toggle to visualization
  • Loading branch information
fhilgers authored Jan 21, 2025
2 parents 860ea40 + 76f3aec commit 925e0fe
Show file tree
Hide file tree
Showing 32 changed files with 454 additions and 210 deletions.
2 changes: 2 additions & 0 deletions frontend/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ dependencies {
implementation(libs.arrow.core)
implementation(libs.arrow.fx.coroutines)
detektPlugins(libs.detekt.compose.rules)
implementation(libs.kotlinx.collections.immutable)

}

tasks.cyclonedxBom {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
* @param clientFactory the client factory for backend communication
*/
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("TooGenericException") // all exceptions should be forwarded for display
class ConfigurationManager(clientFactory: ClientFactory) :
FlowReduxStateMachine<ConfigurationState, ConfigurationAction>(
initialState = ConfigurationState.Uninitialized(clientFactory = clientFactory)
Expand All @@ -59,9 +60,13 @@ class ConfigurationManager(clientFactory: ClientFactory) :
spec {
inState<ConfigurationState.Uninitialized> {
onEnter { state ->
val client = state.snapshot.clientFactory.connect()
val configuration = client.initializeConfiguration()
state.override { configuration.synchronizedOrErrorState(client) }
try {
val client = state.snapshot.clientFactory.connect()
val configuration = client.initializeConfiguration()
state.override { configuration.synchronizedOrErrorState(client) }
} catch (e: Exception) {
state.override { ConfigurationState.Error(e) }
}
}
}

Expand Down Expand Up @@ -142,6 +147,10 @@ class ConfigurationManager(clientFactory: ClientFactory) :
this.getConfiguration()
}

/**
* Try to get the configuration and if it fails, obtain set an empty configuration
* (configuration has possibly never been written)
*/
private suspend fun Client.initializeConfiguration() =
try {
Either.Right(this.getConfiguration())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import de.amosproj3.ziofa.api.processes.InstalledPackageInfo

/** Provides information about the installed packages on the system and caches this information. */
class PackageInformationProvider(private val packageManager: PackageManager) {

private val installedPackagesCache: Map<String, InstalledPackageInfo> by lazy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber

/**
* Provides an updating list of running components, with components being apps (groups of processes)
* or standalone processes like native processes based on backend data and package info.
*/
class RunningComponentsProvider(
private val clientFactory: ClientFactory,
private val packageInformationProvider: PackageInformationProvider,
Expand All @@ -43,6 +47,10 @@ class RunningComponentsProvider(
}
}

/**
* Start polling the backend process list and update the [processesList] every
* [PROCESS_LIST_REFRESH_INTERVAL_MS] milliseconds.
*/
private suspend fun startPollingProcessList() {
while (true) {
delay(PROCESS_LIST_REFRESH_INTERVAL_MS)
Expand All @@ -51,11 +59,17 @@ class RunningComponentsProvider(
}
}

/** Group processes based on the [Process.cmd]. */
private fun Flow<List<Process>>.groupByProcessName() =
this.map { processList ->
processList.groupBy { process -> process.cmd.toReadableString() }
}

/**
* Separate grouped processes into apps and standalone processes like native processes. All
* processes where [Process.cmd] is a package name will be treated as
* [RunningComponent.Application].
*/
private fun Flow<Map<String, List<Process>>>.splitIntoAppsAndStandaloneProcesses() =
this.map { packageProcessMap ->
packageProcessMap.entries.map { (packageOrProcessName, processList) ->
Expand Down
8 changes: 7 additions & 1 deletion frontend/app/src/main/java/de/amosproj3/ziofa/ui/ZiofaApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import de.amosproj3.ziofa.ui.shared.deserializePIDs
import de.amosproj3.ziofa.ui.shared.validPIDsOrNull
import de.amosproj3.ziofa.ui.symbols.SymbolsScreen
import de.amosproj3.ziofa.ui.visualization.VisualizationScreen
import kotlinx.collections.immutable.toImmutableList

val GLOBAL_CONFIGURATION_ROUTE =
"${Routes.IndividualConfiguration.name}?displayName=${Uri.encode("all processes")}?pids=-1"
Expand Down Expand Up @@ -100,7 +101,12 @@ fun ZIOFAApp() {
) {
ConfigurationScreen(
Modifier.padding(innerPadding),
pids = it.arguments?.getString("pids")?.deserializePIDs()?.validPIDsOrNull(),
pids =
it.arguments
?.getString("pids")
?.deserializePIDs()
?.validPIDsOrNull()
?.toImmutableList(),
onAddUprobeSelected = {
navController.navigate(it.arguments.copyToSymbolsRoute())
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
package de.amosproj3.ziofa.ui.configuration

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
Expand All @@ -20,14 +20,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import de.amosproj3.ziofa.ui.configuration.composables.EbpfIOFeatureOptions
import de.amosproj3.ziofa.ui.configuration.composables.EbpfUprobeFeatureOptions
import de.amosproj3.ziofa.ui.configuration.composables.ErrorScreen
import de.amosproj3.ziofa.ui.configuration.composables.SectionTitleRow
import de.amosproj3.ziofa.ui.configuration.composables.PresetFeatureOptionsGroup
import de.amosproj3.ziofa.ui.configuration.composables.SubmitFab
import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions
import de.amosproj3.ziofa.ui.configuration.data.ConfigurationScreenState
import de.amosproj3.ziofa.ui.configuration.data.FeatureType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf

Expand All @@ -37,7 +39,7 @@ import org.koin.core.parameter.parametersOf
fun ConfigurationScreen(
modifier: Modifier = Modifier,
onAddUprobeSelected: () -> Unit = {},
pids: List<UInt>? = listOf(),
pids: ImmutableList<UInt>? = persistentListOf(),
) {

val viewModel: ConfigurationViewModel = koinViewModel(parameters = { parametersOf(pids) })
Expand All @@ -47,34 +49,40 @@ fun ConfigurationScreen(
val configurationChangedByUser by remember { viewModel.changed }.collectAsState()
when (val state = screenState) { // needed for immutability
is ConfigurationScreenState.Valid -> {
// Render list of options
LazyColumn(Modifier.fillMaxWidth()) {
item {
PresetFeatureOptionsGroup(
options = state.options,
type = FeatureType.IO,
onOptionChanged = { option, newState ->
viewModel.optionChanged(option, newState)
},
)
}

Column(Modifier.fillMaxWidth()) {
// Render list of options
SectionTitleRow(FeatureType.IO.displayName)
EbpfIOFeatureOptions(
options = state.options.filter { it.featureType == FeatureType.IO },
onOptionChanged = { option, newState ->
viewModel.optionChanged(option, newState)
},
)

SectionTitleRow(FeatureType.SIGNALS.displayName)
EbpfIOFeatureOptions(
options = state.options.filter { it.featureType == FeatureType.SIGNALS },
onOptionChanged = { option, newState ->
viewModel.optionChanged(option, newState)
},
)
item {
PresetFeatureOptionsGroup(
options = state.options,
type = FeatureType.SIGNALS,
onOptionChanged = { option, newState ->
viewModel.optionChanged(option, newState)
},
)
}

SectionTitleRow(FeatureType.UPROBES.displayName)
EbpfUprobeFeatureOptions(
options =
state.options.mapNotNull { it as? BackendFeatureOptions.UprobeOption },
onOptionDeleted = { option ->
viewModel.optionChanged(option, active = false)
},
onAddUprobeSelected = onAddUprobeSelected,
)
item {
EbpfUprobeFeatureOptions(
options =
state.options
.mapNotNull { it as? BackendFeatureOptions.UprobeOption }
.toImmutableList(),
onOptionDeleted = { option ->
viewModel.optionChanged(option, active = false)
},
onAddUprobeSelected = onAddUprobeSelected,
)
}
}

// Show the submit button if the user changed settings
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2024 Felix Hilgers <[email protected]>
// SPDX-FileCopyrightText: 2024 Luca Bretting <[email protected]>
//
// SPDX-License-Identifier: MIT

package de.amosproj3.ziofa.ui.configuration.composables

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions
import de.amosproj3.ziofa.ui.configuration.data.FeatureType
import kotlinx.collections.immutable.ImmutableList

/**
* Displays a group of options along with a title. All [options] must be of the specified [type] and
* only those will be shown.
*/
@Composable
fun PresetFeatureOptionsGroup(
options: ImmutableList<BackendFeatureOptions>,
type: FeatureType,
onOptionChanged: (BackendFeatureOptions, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.padding(horizontal = 20.dp, vertical = 15.dp).fillMaxWidth()) {
SectionTitleRow(type.displayName)
Spacer(Modifier.height(15.dp))
options
.filter { it.type == type }
.forEach { option ->
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 15.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "",
modifier = Modifier.padding(end = 10.dp),
)
Column {
Text(option.name, fontWeight = FontWeight.Bold)
Text(option.description, fontStyle = FontStyle.Italic)
}
}
Checkbox(
checked = option.active,
onCheckedChange = { onOptionChanged(option, it) },
)
}
}
}
}
Loading

0 comments on commit 925e0fe

Please sign in to comment.