android: add tailnet deletion dialog (#682)

Add dialog for deleting tailnet in user switcher view.

Fixes tailscale/corp#31024

Signed-off-by: kari-ts <kari@tailscale.com>
pull/675/head^2
kari-ts 4 months ago committed by GitHub
parent b3626fc342
commit 7aab785be0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -15,7 +15,8 @@ class Netmap {
var Domain: String,
var UserProfiles: Map<String, Tailcfg.UserProfile>,
var TKAEnabled: Boolean,
var DNS: Tailcfg.DNSConfig? = null
var DNS: Tailcfg.DNSConfig? = null,
var AllCaps: List<String> = emptyList()
) {
// Keys are tailcfg.UserIDs thet get stringified
// Helpers
@ -51,5 +52,9 @@ class Netmap {
UserProfiles == other.UserProfiles &&
TKAEnabled == other.TKAEnabled
}
fun hasCap(capability: String): Boolean {
return AllCaps.contains(capability)
}
}
}

@ -3,6 +3,8 @@
package com.tailscale.ipn.ui.view
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -10,8 +12,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@ -20,13 +24,19 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@ -46,10 +56,14 @@ data class UserSwitcherNav(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UserSwitcherView(nav: UserSwitcherNav, viewModel: UserSwitcherViewModel = viewModel()) {
val users by viewModel.loginProfiles.collectAsState()
val currentUser by viewModel.loggedInUser.collectAsState()
val showHeaderMenu by viewModel.showHeaderMenu.collectAsState()
var showDeleteDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
val netmapState by viewModel.netmap.collectAsState()
val capabilityIsOwner = "https://tailscale.com/cap/is-owner"
val isOwner = netmapState?.hasCap(capabilityIsOwner) == true
Scaffold(
topBar = {
@ -138,10 +152,47 @@ fun UserSwitcherView(nav: UserSwitcherNav, viewModel: UserSwitcherViewModel = vi
}
})
}
Lists.SectionDivider()
Setting.Text(R.string.delete_tailnet, destructive = true) {
showDeleteDialog = true
}
}
}
}
}
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text(text = stringResource(R.string.delete_tailnet)) },
text = {
if (isOwner) {
OwnerDeleteDialogText {
val uri = Uri.parse("https://login.tailscale.com/admin/settings/general")
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
}
} else {
Text(stringResource(R.string.request_deletion_nonowner))
}
},
confirmButton = {
TextButton(
onClick = {
val intent =
Intent(Intent.ACTION_VIEW, Uri.parse("https://tailscale.com/contact/support"))
context.startActivity(intent)
showDeleteDialog = false
}) {
Text(text = stringResource(R.string.contact_support))
}
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) {
Text(text = stringResource(R.string.cancel))
}
})
}
}
@Composable
@ -171,6 +222,41 @@ fun FusMenu(
}
}
@Composable
fun OwnerDeleteDialogText(onSettingsClick: () -> Unit) {
val part1 = stringResource(R.string.request_deletion_owner_part1)
val part2a = stringResource(R.string.request_deletion_owner_part2a)
val part2b = stringResource(R.string.request_deletion_owner_part2b)
val annotatedText = buildAnnotatedString {
append(part1 + " ")
pushStringAnnotation(
tag = "settings", annotation = "https://login.tailscale.com/admin/settings/general")
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
append("Settings > General")
}
pop()
append(" $part2a\n\n") // newline after "Delete tailnet."
append(part2b)
}
val context = LocalContext.current
ClickableText(
text = annotatedText,
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
onClick = { offset ->
annotatedText
.getStringAnnotations(tag = "settings", start = offset, end = offset)
.firstOrNull()
?.let { annotation ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(annotation.item))
context.startActivity(intent)
}
})
}
@Composable
fun MenuItem(text: String, onClick: () -> Unit) {
DropdownMenuItem(

@ -13,7 +13,7 @@
<string name="not_connected">Not connected</string>
<string name="empty" translatable="false"> </string>
<string name="template" translatable="false">%s</string>
<string name="selected">Selected</string>
<string name="selected">Selected</string>
<string name="offline">Offline</string>
<string name="ok">OK</string>
<string name="_continue">Continue</string>
@ -137,6 +137,20 @@
<string name="custom_control_url_title">Custom control server URL</string>
<string name="auth_key_input_title">Auth key</string>
<string name="delete_tailnet">Delete tailnet</string>
<string name="contact_support">Contact support</string>
<string name="request_deletion_nonowner">All requests related to the removal or deletion of data are handled by our Support team. To open a request, tap the Contact Support button below to be taken to our contact form in the browser. Complete the form, and a Customer Support Engineer will work with you directly to assist.</string>
<string name="request_deletion_owner_part1">
As the owner of this tailnet, to remove yourself from the tailnet you can either reassign ownership and contact our Support team, or delete the whole tailnet through the admin console. To do the latter, go to
</string>
<string name="request_deletion_owner_part2a">
and look for “Delete tailnet”.
</string>
<string name="request_deletion_owner_part2b">
All requests related to the removal or deletion of data are handled by our Support team. To open a request, tap the Contact Support button below to be taken to our contact form in the browser. Complete the form, and a Customer Support Engineer will work with you directly to assist.
</string>
<!-- Strings for ExitNode picker -->
<string name="choose_exit_node">Choose exit node</string>
<string name="choose_mullvad_exit_node">Mullvad exit nodes</string>

Loading…
Cancel
Save