Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloud address translation #830

Merged
merged 2 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 109 additions & 17 deletions module/connection/ServerDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.ui.awt.ComposeDialog
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp
import com.vaticle.typedb.driver.api.TypeDBCredential
import com.vaticle.typedb.studio.framework.common.theme.Theme
import com.vaticle.typedb.studio.framework.material.ActionableList
import com.vaticle.typedb.studio.framework.material.Dialog
Expand All @@ -46,12 +47,13 @@ import com.vaticle.typedb.studio.framework.material.Tooltip
import com.vaticle.typedb.studio.service.Service
import com.vaticle.typedb.studio.service.common.util.Label
import com.vaticle.typedb.studio.service.common.util.Property
import com.vaticle.typedb.studio.service.common.util.Property.Server.TYPEDB_CORE
import com.vaticle.typedb.studio.service.common.util.Property.Server.TYPEDB_CLOUD
import com.vaticle.typedb.studio.service.common.util.Property.Server.TYPEDB_CORE
import com.vaticle.typedb.studio.service.common.util.Sentence
import com.vaticle.typedb.studio.service.connection.DriverState.Status.CONNECTED
import com.vaticle.typedb.studio.service.connection.DriverState.Status.CONNECTING
import com.vaticle.typedb.studio.service.connection.DriverState.Status.DISCONNECTED
import java.nio.file.Path

object ServerDialog {

Expand All @@ -69,6 +71,10 @@ object ServerDialog {
var cloudAddresses: MutableList<String> = mutableStateListOf<String>().also {
appData.cloudAddresses?.let { saved -> it.addAll(saved) }
}
var cloudAddressTranslation: MutableList<Pair<String, String>> = mutableStateListOf<Pair<String, String>>().also {
appData.cloudAddressTranslation?.let { saved -> it.addAll(saved) }
}
var useCloudAddressTranslation: Boolean by mutableStateOf(appData.useCloudAddressTranslation ?: false)
var username: String by mutableStateOf(appData.username ?: "")
var password: String by mutableStateOf("")
var tlsEnabled: Boolean by mutableStateOf(appData.tlsEnabled ?: true)
Expand All @@ -77,37 +83,68 @@ object ServerDialog {
override fun cancel() = Service.driver.connectServerDialog.close()
override fun isValid(): Boolean = when (server) {
TYPEDB_CORE -> coreAddress.isNotBlank() && addressFormatIsValid(coreAddress)
TYPEDB_CLOUD -> !(cloudAddresses.isEmpty() || username.isBlank() || password.isBlank())
TYPEDB_CLOUD -> username.isNotBlank() && password.isNotBlank() && if (useCloudAddressTranslation) {
cloudAddressTranslation.isNotEmpty()
} else {
cloudAddresses.isNotEmpty()
}
}

override fun submit() {
when (server) {
TYPEDB_CORE -> Service.driver.tryConnectToTypeDBCoreAsync(coreAddress) {
Service.driver.connectServerDialog.close()
}
TYPEDB_CLOUD -> Service.driver.tryConnectToTypeDBCloudAsync(
cloudAddresses.toSet(), username, password, tlsEnabled, caCertificate
) { Service.driver.connectServerDialog.close() }
TYPEDB_CLOUD -> {
val credentials = if (caCertificate.isBlank()) TypeDBCredential(username, password, tlsEnabled)
else TypeDBCredential(username, password, Path.of(caCertificate))
val onSuccess = { Service.driver.connectServerDialog.close() }
if (useCloudAddressTranslation) {
val firstAddress = cloudAddressTranslation.first().first
Service.driver.tryConnectToTypeDBCloudAsync("$username@$firstAddress", cloudAddressTranslation.associate { it }, credentials, onSuccess)
} else {
val firstAddress = cloudAddresses.first()
Service.driver.tryConnectToTypeDBCloudAsync("$username@$firstAddress", cloudAddresses.toSet(), credentials, onSuccess)
}
}
}
password = ""
appData.server = server
appData.coreAddress = coreAddress
appData.cloudAddresses = cloudAddresses
appData.cloudAddressTranslation = cloudAddressTranslation
appData.useCloudAddressTranslation = useCloudAddressTranslation
appData.username = username
appData.tlsEnabled = tlsEnabled
appData.caCertificate = caCertificate
}
}

private object AddAddressForm : Form.State() {
var value: String by mutableStateOf("")
var server: String by mutableStateOf("")
override fun cancel() = Service.driver.manageAddressesDialog.close()
override fun isValid() = server.isNotBlank() && addressFormatIsValid(server) && !state.cloudAddresses.contains(server)

override fun submit() {
assert(isValid())
state.cloudAddresses.add(server)
server = ""
}
}

private object AddAddressTranslationForm : Form.State() {
var server: String by mutableStateOf("")
var translation: String by mutableStateOf("")
override fun cancel() = Service.driver.manageAddressesDialog.close()
override fun isValid() = value.isNotBlank() && addressFormatIsValid(value) && !state.cloudAddresses.contains(value)
override fun isValid() = serverIsValid() && translationIsValid()
fun serverIsValid() = server.isNotBlank() && addressFormatIsValid(server) && !state.cloudAddressTranslation.any { it.first == server }
fun translationIsValid() = translation.isNotBlank() && addressFormatIsValid(translation) && !state.cloudAddressTranslation.any { it.second == translation }

override fun submit() {
assert(isValid())
state.cloudAddresses.add(value)
value = ""
state.cloudAddressTranslation.add(Pair(server, translation))
server = ""
translation = ""
}
}

Expand Down Expand Up @@ -187,8 +224,9 @@ object ServerDialog {
private fun ManageCloudAddressesButton(state: ConnectServerForm, shouldFocus: Boolean) {
val focusReq = if (shouldFocus) remember { FocusRequester() } else null
Field(label = Label.ADDRESSES) {
val numAddresses = if (state.useCloudAddressTranslation) state.cloudAddressTranslation.size else state.cloudAddresses.size
TextButton(
text = Label.MANAGE_CLOUD_ADDRESSES + " (${state.cloudAddresses.size})",
text = Label.MANAGE_CLOUD_ADDRESSES + " ($numAddresses)",
focusReq = focusReq, leadingIcon = Form.IconArg(Icon.CONNECT_TO_TYPEDB),
enabled = Service.driver.isDisconnected
) {
Expand All @@ -205,11 +243,21 @@ object ServerDialog {
Column(Modifier.fillMaxSize()) {
Text(value = Sentence.MANAGE_ADDRESSES_MESSAGE, softWrap = true)
Spacer(Modifier.height(Dialog.DIALOG_SPACING))
CloudAddressList(Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(Dialog.DIALOG_SPACING))
AddCloudAddressForm()
if (state.useCloudAddressTranslation) {
CloudAddressTranslationList(Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(Dialog.DIALOG_SPACING))
AddCloudAddressTranslationForm()
} else {
CloudAddressList(Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(Dialog.DIALOG_SPACING))
AddCloudAddressForm()
}
Spacer(Modifier.height(Dialog.DIALOG_SPACING * 2))
Row(verticalAlignment = Alignment.Bottom) {
Text(value = Label.TRANSLATE_ADDRESSES)
RowSpacer()
Checkbox(value = state.useCloudAddressTranslation) { state.useCloudAddressTranslation = it }
RowSpacer()
Spacer(modifier = Modifier.weight(1f))
RowSpacer()
TextButton(text = Label.CLOSE) { dialogState.close() }
Expand All @@ -224,12 +272,12 @@ object ServerDialog {
Submission(AddAddressForm, modifier = Modifier.height(Form.FIELD_HEIGHT), showButtons = false) {
Row {
TextInputValidated(
value = AddAddressForm.value,
value = AddAddressForm.server,
placeholder = Label.DEFAULT_SERVER_ADDRESS,
onValueChange = { AddAddressForm.value = it },
onValueChange = { AddAddressForm.server = it },
modifier = Modifier.weight(1f).focusRequester(focusReq),
invalidWarning = Label.ADDRESS_PORT_WARNING,
validator = { AddAddressForm.value.isBlank() || addressFormatIsValid(AddAddressForm.value) }
validator = { AddAddressForm.server.isBlank() || addressFormatIsValid(AddAddressForm.server) }
)
RowSpacer()
TextButton(text = Label.ADD, enabled = AddAddressForm.isValid()) { AddAddressForm.submit() }
Expand All @@ -238,9 +286,38 @@ object ServerDialog {
LaunchedEffect(focusReq) { focusReq.requestFocus() }
}

@Composable
private fun AddCloudAddressTranslationForm() {
val focusReq = remember { FocusRequester() }
Submission(AddAddressTranslationForm, modifier = Modifier.height(Form.FIELD_HEIGHT), showButtons = false) {
Row {
TextInputValidated(
value = AddAddressTranslationForm.server,
placeholder = Label.DEFAULT_SERVER_ADDRESS,
onValueChange = { AddAddressTranslationForm.server = it },
modifier = Modifier.weight(1f).focusRequester(focusReq),
invalidWarning = Label.ADDRESS_PORT_WARNING,
validator = { AddAddressTranslationForm.server.isBlank() || addressFormatIsValid(AddAddressTranslationForm.server) }
)
RowSpacer()
TextInputValidated(
value = AddAddressTranslationForm.translation,
placeholder = Label.DEFAULT_SERVER_ADDRESS,
onValueChange = { AddAddressTranslationForm.translation = it },
modifier = Modifier.weight(1f).focusRequester(focusReq),
invalidWarning = Label.ADDRESS_PORT_WARNING,
validator = { AddAddressTranslationForm.translation.isBlank() || addressFormatIsValid(AddAddressTranslationForm.translation) }
)
RowSpacer()
TextButton(text = Label.ADD, enabled = AddAddressTranslationForm.isValid()) { AddAddressTranslationForm.submit() }
}
}
LaunchedEffect(focusReq) { focusReq.requestFocus() }
}

@Composable
private fun CloudAddressList(modifier: Modifier) = ActionableList.SingleButtonLayout(
items = state.cloudAddresses.toMutableList(),
items = state.cloudAddresses,
modifier = modifier.border(1.dp, Theme.studio.border),
buttonSide = ActionableList.Side.RIGHT,
buttonFn = { address ->
Expand All @@ -252,6 +329,21 @@ object ServerDialog {
}
)

@Composable
private fun CloudAddressTranslationList(modifier: Modifier) = ActionableList.SingleButtonLayout(
items = state.cloudAddressTranslation.map { "${it.first} ⇒ ${it.second}" },
modifier = modifier.border(1.dp, Theme.studio.border),
buttonSide = ActionableList.Side.RIGHT,
buttonFn = { address ->
val parts = address.split(" ⇒ ", limit = 2)
Form.IconButtonArg(
icon = Icon.REMOVE,
color = { Theme.studio.errorStroke },
onClick = { state.cloudAddressTranslation.remove(parts[0] to parts[1]) }
)
}
)

@Composable
private fun UsernameFormField(state: ConnectServerForm) = Field(label = Label.USERNAME) {
TextInput(
Expand Down
13 changes: 11 additions & 2 deletions module/user/UpdateDefaultPasswordDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,17 @@ object UpdateDefaultPasswordDialog {
var newPassword: String by mutableStateOf("")
var repeatPassword: String by mutableStateOf("")

override fun cancel() = Service.driver.updateDefaultPasswordDialog.cancel()
override fun submit() = Service.driver.updateDefaultPasswordDialog.submit(oldPassword, newPassword)
override fun cancel() {
Service.driver.updateDefaultPasswordDialog.cancel()
oldPassword = ""
newPassword = ""
}
override fun submit() {
assert(isValid())
Service.driver.updateDefaultPasswordDialog.submit(oldPassword, newPassword)
oldPassword = ""
newPassword = ""
}
Comment on lines -35 to +45
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by: discard password field contents on submit / cancel.

override fun isValid() = oldPassword.isNotEmpty() && newPassword.isNotEmpty()
&& oldPassword != newPassword && repeatPassword == newPassword
}
Expand Down
14 changes: 12 additions & 2 deletions service/common/DataService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class DataService {
private val CONNECTION_SERVER = "connection.server"
private val CONNECTION_CORE_ADDRESS = "connection.core_address"
private val CONNECTION_CLOUD_ADDRESSES = "connection.cloud_addresses"
private val CONNECTION_CLOUD_ADDRESS_TRANSLATION = "connection.cloud_address_translation"
private val CONNECTION_USE_CLOUD_ADDRESS_TRANSLATION = "connection.use_cloud_address_translation"
private val CONNECTION_USERNAME = "connection.username"
private val CONNECTION_TLS_ENABLED = "connection.tls_enabled"
private val CONNECTION_CA_CERTIFICATE = "connection.ca_certificate"
Expand All @@ -71,10 +73,18 @@ class DataService {
var coreAddress: String?
get() = properties?.getProperty(CONNECTION_CORE_ADDRESS)
set(value) = value?.let { setProperty(CONNECTION_CORE_ADDRESS, it) } ?: Unit
var cloudAddresses: MutableList<String>?
var cloudAddresses: List<String>?
get() = properties?.getProperty(CONNECTION_CLOUD_ADDRESSES)
?.split(",")?.filter { it.isNotBlank() }?.toMutableList()
?.split(",")?.filter { it.isNotBlank() }
set(value) = value?.let { setProperty(CONNECTION_CLOUD_ADDRESSES, it.joinToString(",")) } ?: Unit
var cloudAddressTranslation: List<Pair<String, String>>?
get() = properties?.getProperty(CONNECTION_CLOUD_ADDRESS_TRANSLATION)
?.split(",")?.filter { it.contains("=") }?.map { it.split("=", limit = 2) }?.map{ it[0] to it[1] }
set(value) = value
?.let { setProperty(CONNECTION_CLOUD_ADDRESS_TRANSLATION, it.map { pair -> "${pair.first}=${pair.second}" } .joinToString(",")) } ?: Unit
var useCloudAddressTranslation: Boolean?
get() = properties?.getProperty(CONNECTION_USE_CLOUD_ADDRESS_TRANSLATION)?.toBooleanStrictOrNull()
set(value) = value?.let { setProperty(CONNECTION_USE_CLOUD_ADDRESS_TRANSLATION, it.toString()) } ?: Unit
var username: String?
get() = properties?.getProperty(CONNECTION_USERNAME)
set(value) = value?.let { setProperty(CONNECTION_USERNAME, it) } ?: Unit
Expand Down
1 change: 1 addition & 0 deletions service/common/util/Label.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ object Label {
const val TRANSACTION_STATUS = "Transaction Status"
const val TRANSACTION_TIMEOUT_MINS = "Transaction Timeout (mins)"
const val TRANSACTION_TYPE = "Transaction Type"
const val TRANSLATE_ADDRESSES = "Translate addresses"
const val TYPE = "Type"
const val TYPEDB_STUDIO = "TypeDB Studio"
const val TYPEDB_STUDIO_APPLICATION_ERROR = "TypeDB Studio Application Error"
Expand Down
40 changes: 24 additions & 16 deletions service/connection/DriverState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class DriverState(

companion object {
private const val DATABASE_LIST_REFRESH_RATE_MS = 100
private val PASSWORD_EXPIRY_WARN_DURATION = Duration.ofDays(7);
private val PASSWORD_EXPIRY_WARN_DURATION = Duration.ofDays(7)
private val LOGGER = KotlinLogging.logger {}
}

Expand Down Expand Up @@ -114,19 +114,25 @@ class DriverState(
) = tryConnectAsync(newConnectionName = address, onSuccess = onSuccess) { TypeDB.coreDriver(address) }

fun tryConnectToTypeDBCloudAsync(
addresses: Set<String>, username: String, password: String,
tlsEnabled: Boolean, caPath: String, onSuccess: (() -> Unit)? = null
connectionName: String, addresses: Set<String>, credentials: TypeDBCredential, onSuccess: (() -> Unit)? = null
) {
val credentials = if (caPath.isBlank()) TypeDBCredential(username, password, tlsEnabled)
else TypeDBCredential(username, password, Path.of(caPath))
val postLoginFn = {
onSuccess?.invoke()
if (needsToChangeDefaultPassword()) forcePasswordUpdate()
else mayWarnPasswordExpiry()
}
tryConnectAsync(newConnectionName = "$username@${addresses.first()}", postLoginFn) {
TypeDB.cloudDriver(addresses, credentials)
tryConnectAsync(newConnectionName = connectionName, postLoginFn) { TypeDB.cloudDriver(addresses, credentials) }
}

fun tryConnectToTypeDBCloudAsync(
connectionName: String, addressTranslation: Map<String, String>, credentials: TypeDBCredential, onSuccess: (() -> Unit)? = null
) {
val postLoginFn = {
onSuccess?.invoke()
if (needsToChangeDefaultPassword()) forcePasswordUpdate()
else mayWarnPasswordExpiry()
}
tryConnectAsync(newConnectionName = connectionName, postLoginFn) { TypeDB.cloudDriver(addressTranslation, credentials) }
}

private fun forcePasswordUpdate() = updateDefaultPasswordDialog.open(
Expand All @@ -138,15 +144,17 @@ class DriverState(
tryUpdateUserPassword(old, new) {
updateDefaultPasswordDialog.close()
close()
tryConnectToTypeDBCloudAsync(
addresses = dataSrv.connection.cloudAddresses!!.toSet(),
username = dataSrv.connection.username!!,
password = new,
tlsEnabled = dataSrv.connection.tlsEnabled!!,
caPath = dataSrv.connection.caCertificate!!
) {
notificationSrv.info(LOGGER, Message.Connection.RECONNECTED_WITH_NEW_PASSWORD_SUCCESSFULLY)
}

val username = dataSrv.connection.username!!
val password = new
val credentials = if (dataSrv.connection.caCertificate!!.isBlank()) TypeDBCredential(username, password, dataSrv.connection.tlsEnabled!!)
else TypeDBCredential(username, password, Path.of(dataSrv.connection.caCertificate!!))
val onSuccess = { notificationSrv.info(LOGGER, Message.Connection.RECONNECTED_WITH_NEW_PASSWORD_SUCCESSFULLY) }

if (dataSrv.connection.useCloudAddressTranslation == true)
tryConnectToTypeDBCloudAsync(connectionName!!, dataSrv.connection.cloudAddressTranslation!!.associate { it }, credentials, onSuccess)
else
tryConnectToTypeDBCloudAsync(connectionName!!, dataSrv.connection.cloudAddresses!!.toSet(), credentials, onSuccess)
}
}

Expand Down