From 6ebece1b1e2ac674cef0651faecae7adf479aa25 Mon Sep 17 00:00:00 2001 From: David Perez Date: Tue, 8 Apr 2025 14:11:05 -0500 Subject: [PATCH] PM-19978: Build out flight recorder UI (#5009) --- .../model/FlightRecorderDuration.kt | 13 ++ .../components/text/BitwardenHyperTextLink.kt | 6 +- .../flightrecorder/FlightRecorderScreen.kt | 153 +++++++++++++++++- .../flightrecorder/FlightRecorderViewModel.kt | 48 +++++- .../util/FlightRecorderDurationExtensions.kt | 20 +++ app/src/main/res/values/strings.xml | 10 ++ .../FlightRecorderScreenTest.kt | 84 +++++++++- .../FlightRecorderViewModelTest.kt | 47 +++++- 8 files changed, 374 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/FlightRecorderDuration.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/util/FlightRecorderDurationExtensions.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/FlightRecorderDuration.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/FlightRecorderDuration.kt new file mode 100644 index 0000000000..18c3cbaa76 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/FlightRecorderDuration.kt @@ -0,0 +1,13 @@ +package com.x8bit.bitwarden.data.platform.repository.model + +/** + * The selectable durations allowed for the flight recorder. + */ +enum class FlightRecorderDuration( + val milliseconds: Long, +) { + ONE_HOUR(milliseconds = 3_600_000L), + EIGHT_HOURS(milliseconds = 28_800_000L), + TWENTY_FOUR_HOURS(milliseconds = 86_400_000L), + ONE_WEEK(milliseconds = 604_800_000L), +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenHyperTextLink.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenHyperTextLink.kt index 90747447af..1e55bc4b47 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenHyperTextLink.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenHyperTextLink.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +import com.x8bit.bitwarden.ui.platform.util.spanStyleOf /** * Uses an annotated string resource to create a string with clickable text. @@ -30,7 +31,10 @@ fun BitwardenHyperTextLink( color: Color = BitwardenTheme.colorScheme.text.secondary, ) { Text( - text = annotatedResId.toAnnotatedString(args = args) { key -> + text = annotatedResId.toAnnotatedString( + args = args, + style = spanStyleOf(color = color, textStyle = style), + ) { key -> when (key) { annotationKey -> onClick() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreen.kt index a835f4f4b1..3eda3dfa93 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreen.kt @@ -1,19 +1,45 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton +import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.text.BitwardenHyperTextLink import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter +import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager +import com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder.util.displayText +import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +import kotlinx.collections.immutable.toImmutableList /** * Displays the flight recorder configuration screen. @@ -23,10 +49,15 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter fun FlightRecorderScreen( onNavigateBack: () -> Unit, viewModel: FlightRecorderViewModel = hiltViewModel(), + intentManager: IntentManager = LocalIntentManager.current, ) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() EventsEffect(viewModel) { event -> when (event) { FlightRecorderEvent.NavigateBack -> onNavigateBack() + FlightRecorderEvent.NavigateToHelpCenter -> { + intentManager.launchUri(uri = "https://bitwarden.com/help".toUri()) + } } } val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) @@ -40,10 +71,130 @@ fun FlightRecorderScreen( { viewModel.trySendAction(FlightRecorderAction.BackClick) } }, scrollBehavior = scrollBehavior, + actions = { + BitwardenTextButton( + label = stringResource(id = R.string.save), + onClick = remember(viewModel) { + { viewModel.trySendAction(FlightRecorderAction.SaveClick) } + }, + modifier = Modifier.testTag("SaveButton"), + ) + }, ) }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { - // TODO: PM-19592 Create the flight recorder UI. + FlightRecorderContent( + state = state, + onDurationSelected = remember(viewModel) { + { viewModel.trySendAction(FlightRecorderAction.DurationSelect(it)) } + }, + onHelpCenterClick = remember(viewModel) { + { viewModel.trySendAction(FlightRecorderAction.HelpCenterClick) } + }, + ) } } + +@Suppress("LongMethod") +@Composable +private fun FlightRecorderContent( + state: FlightRecorderState, + onDurationSelected: (FlightRecorderDuration) -> Unit, + onHelpCenterClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .verticalScroll(state = rememberScrollState()), + ) { + Spacer(modifier = Modifier.height(height = 24.dp)) + Text( + text = stringResource(id = R.string.experiencing_an_issue), + color = BitwardenTheme.colorScheme.text.primary, + style = BitwardenTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 12.dp)) + Text( + text = stringResource( + id = R.string.enable_temporary_logging_to_collect_and_inspect_logs_locally, + ), + color = BitwardenTheme.colorScheme.text.primary, + style = BitwardenTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 12.dp)) + Text( + text = stringResource(id = R.string.to_get_started_set_a_logging_duration), + color = BitwardenTheme.colorScheme.text.primary, + style = BitwardenTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 24.dp)) + DurationSelectButton( + selectedOption = state.selectedDuration, + onOptionSelected = onDurationSelected, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 24.dp)) + Text( + text = stringResource(id = R.string.logs_will_be_automatically_deleted_after_30_days), + color = BitwardenTheme.colorScheme.text.secondary, + style = BitwardenTheme.typography.bodySmall, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + BitwardenHyperTextLink( + annotatedResId = R.string.for_details_on_what_is_and_isnt_logged, + annotationKey = "helpCenter", + accessibilityString = stringResource(id = R.string.bitwarden_help_center), + onClick = onHelpCenterClick, + color = BitwardenTheme.colorScheme.text.secondary, + style = BitwardenTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(height = 16.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) + } +} + +@Composable +private fun DurationSelectButton( + selectedOption: FlightRecorderDuration, + onOptionSelected: (FlightRecorderDuration) -> Unit, + modifier: Modifier = Modifier, +) { + val resources = LocalContext.current.resources + val options = FlightRecorderDuration.entries.map { it.displayText() }.toImmutableList() + BitwardenMultiSelectButton( + label = stringResource(id = R.string.logging_duration), + options = options, + selectedOption = selectedOption.displayText(), + onOptionSelected = { selectedOption -> + onOptionSelected( + FlightRecorderDuration + .entries + .first { selectedOption == it.displayText.toString(resources) }, + ) + }, + cardStyle = CardStyle.Full, + modifier = modifier, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderViewModel.kt index 85151ae7c9..9a2e14d541 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderViewModel.kt @@ -1,8 +1,10 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder import androidx.lifecycle.SavedStateHandle +import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.update import javax.inject.Inject private const val KEY_STATE = "state" @@ -15,23 +17,43 @@ class FlightRecorderViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. - initialState = savedStateHandle[KEY_STATE] ?: FlightRecorderState, + initialState = savedStateHandle[KEY_STATE] + ?: FlightRecorderState( + selectedDuration = FlightRecorderDuration.ONE_HOUR, + ), ) { override fun handleAction(action: FlightRecorderAction) { when (action) { FlightRecorderAction.BackClick -> handleBackClick() + is FlightRecorderAction.DurationSelect -> handleOnDurationSelect(action) + FlightRecorderAction.HelpCenterClick -> handleHelpCenterClick() + FlightRecorderAction.SaveClick -> handleSaveClick() } } private fun handleBackClick() { sendEvent(FlightRecorderEvent.NavigateBack) } + + private fun handleOnDurationSelect(action: FlightRecorderAction.DurationSelect) { + mutableStateFlow.update { it.copy(selectedDuration = action.duration) } + } + + private fun handleHelpCenterClick() { + sendEvent(FlightRecorderEvent.NavigateToHelpCenter) + } + + private fun handleSaveClick() { + // TODO: PM-19592 Persist the flight recorder state. + } } /** * Models the UI state for the flight recorder screen. */ -data object FlightRecorderState +data class FlightRecorderState( + val selectedDuration: FlightRecorderDuration, +) /** * Models events for the flight recorder screen. @@ -41,6 +63,11 @@ sealed class FlightRecorderEvent { * Navigates back. */ data object NavigateBack : FlightRecorderEvent() + + /** + * Launches the the help center link. + */ + data object NavigateToHelpCenter : FlightRecorderEvent() } /** @@ -51,4 +78,21 @@ sealed class FlightRecorderAction { * Indicates that the user clicked the close button. */ data object BackClick : FlightRecorderAction() + + /** + * Indicates that the user clicked the help center link. + */ + data object HelpCenterClick : FlightRecorderAction() + + /** + * Indicates that the user clicked the save button. + */ + data object SaveClick : FlightRecorderAction() + + /** + * Indicates that the user selected a duration. + */ + data class DurationSelect( + val duration: FlightRecorderDuration, + ) : FlightRecorderAction() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/util/FlightRecorderDurationExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/util/FlightRecorderDurationExtensions.kt new file mode 100644 index 0000000000..87d9e8dba3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/util/FlightRecorderDurationExtensions.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder.util + +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration +import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.asText + +/** + * A helper function to map the [FlightRecorderDuration] to a displayable label. + */ +val FlightRecorderDuration.displayText: Text + get() = when (this) { + FlightRecorderDuration.ONE_HOUR -> R.string.flight_recorder_one_hour.asText() + FlightRecorderDuration.EIGHT_HOURS -> R.string.flight_recorder_eight_hours.asText() + FlightRecorderDuration.TWENTY_FOUR_HOURS -> { + R.string.flight_recorder_twenty_four_hours.asText() + } + + FlightRecorderDuration.ONE_WEEK -> R.string.flight_recorder_one_week.asText() + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7375b743e8..52e66d8368 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1226,4 +1226,14 @@ Do you want to switch to this account? Enable flight recorder Recorded logs No logs recorded + Experiencing an issue? + Enable temporary logging to collect and inspect logs locally. When enabled, application states and network calls may be logged—never sensitive vault information. + To get started, set a logging duration. Logging 
will automatically turn off after this period, giving you time to capture activity while reproducing 
any issues. + Logs will be automatically deleted after 30 days. Bitwarden is only able to access your log data when you share it. + For details on what is and isn’t logged, visit the\nBitwarden help center. + Logging duration + 1 hour + 8 hours + 24 hours + 1 week diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt index da8fdd7a2d..c35dc7b9f3 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt @@ -1,11 +1,24 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.isDialog +import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.core.net.toUri import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow +import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest +import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager +import com.x8bit.bitwarden.ui.util.assertNoDialogExists +import com.x8bit.bitwarden.ui.util.performCustomAccessibilityAction import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.runs import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Assert.assertTrue @@ -22,9 +35,15 @@ class FlightRecorderScreenTest : BaseComposeTest() { every { stateFlow } returns mutableStateFlow } + private val intentManager = mockk { + every { launchUri(uri = any()) } just runs + } + @Before fun setUp() { - setContent { + setContent( + intentManager = intentManager, + ) { FlightRecorderScreen( onNavigateBack = { onNavigateBackCalled = true }, viewModel = viewModel, @@ -42,11 +61,72 @@ class FlightRecorderScreenTest : BaseComposeTest() { } } + @Test + fun `on save click should emit SaveClick action`() { + composeTestRule.onNodeWithText(text = "Save").performClick() + verify(exactly = 1) { + viewModel.trySendAction(FlightRecorderAction.SaveClick) + } + } + + @Test + fun `on help center click should emit HelpCenterClick action`() { + composeTestRule + .onNodeWithText(text = "Bitwarden help center", substring = true) + .performScrollTo() + .performCustomAccessibilityAction(label = "Bitwarden help center") + verify(exactly = 1) { + viewModel.trySendAction(FlightRecorderAction.HelpCenterClick) + } + } + + @Test + fun `on logging duration click should display select dialog`() { + composeTestRule.assertNoDialogExists() + composeTestRule + .onNodeWithContentDescription(label = "1 hour. Logging duration") + .performScrollTo() + .performClick() + composeTestRule + .onNodeWithText(text = "Logging duration") + .assert(hasAnyAncestor(isDialog())) + .isDisplayed() + } + + @Test + fun `on selection of new duration should emit action`() { + composeTestRule.assertNoDialogExists() + composeTestRule + .onNodeWithContentDescription(label = "1 hour. Logging duration") + .performScrollTo() + .performClick() + composeTestRule + .onNodeWithText(text = "24 hours") + .performScrollTo() + .performClick() + verify(exactly = 1) { + viewModel.trySendAction( + FlightRecorderAction.DurationSelect(FlightRecorderDuration.TWENTY_FOUR_HOURS), + ) + } + } + @Test fun `on NavigateBack event should invoke onNavigateBack`() { mutableEventFlow.tryEmit(FlightRecorderEvent.NavigateBack) assertTrue(onNavigateBackCalled) } + + @Test + fun `on NavigateToHelpCenter event should launch intent for help center`() { + mutableEventFlow.tryEmit(FlightRecorderEvent.NavigateToHelpCenter) + verify(exactly = 1) { + intentManager.launchUri(uri = "https://bitwarden.com/help".toUri()) + } + } } -private val DEFAULT_STATE: FlightRecorderState = FlightRecorderState +private val DEFAULT_STATE: FlightRecorderState = + FlightRecorderState( + selectedDuration = FlightRecorderDuration.ONE_HOUR, + ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderViewModelTest.kt index 9f3c9a79b1..20bb8268c0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderViewModelTest.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test +import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -24,6 +25,47 @@ class FlightRecorderViewModelTest : BaseViewModelTest() { } } + @Test + fun `on SaveClick action should do nothing`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(FlightRecorderAction.SaveClick) + expectNoEvents() + } + } + + @Test + fun `on HelpCenterClick action should send the NavigateToHelpCenter event`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(FlightRecorderAction.HelpCenterClick) + assertEquals(FlightRecorderEvent.NavigateToHelpCenter, awaitItem()) + } + } + + @Test + fun `on DurationSelect action should update the selectedDuration state`() = runTest { + val viewModel = createViewModel() + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + viewModel.trySendAction( + FlightRecorderAction.DurationSelect(duration = FlightRecorderDuration.ONE_WEEK), + ) + assertEquals( + DEFAULT_STATE.copy(selectedDuration = FlightRecorderDuration.ONE_WEEK), + awaitItem(), + ) + + viewModel.trySendAction( + FlightRecorderAction.DurationSelect(duration = FlightRecorderDuration.EIGHT_HOURS), + ) + assertEquals( + DEFAULT_STATE.copy(selectedDuration = FlightRecorderDuration.EIGHT_HOURS), + awaitItem(), + ) + } + } + private fun createViewModel( state: FlightRecorderState? = null, ): FlightRecorderViewModel = @@ -34,4 +76,7 @@ class FlightRecorderViewModelTest : BaseViewModelTest() { ) } -private val DEFAULT_STATE: FlightRecorderState = FlightRecorderState +private val DEFAULT_STATE: FlightRecorderState = + FlightRecorderState( + selectedDuration = FlightRecorderDuration.ONE_HOUR, + )