mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-165434 Provide ability to sync with additional providers in Android Studio
(cherry picked from commit 656ed2562377be73a7b49b965bfb42c187ce69ca) Signed-off-by: Sergey Pak <sergey.pak@jetbrains.com> GitOrigin-RevId: 6dba33081f0acb3ca5ba1fc89bb4ed08b624c280
This commit is contained in:
committed by
intellij-monorepo-bot
parent
de367a30ba
commit
5b7c2d071b
Generated
+1
@@ -919,6 +919,7 @@
|
||||
<module fileurl="file://$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.iml" filepath="$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.tests.iml" filepath="$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.tests.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/plugins/settings-sync/intellij.settingsSync.iml" filepath="$PROJECT_DIR$/plugins/settings-sync/intellij.settingsSync.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/plugins/settings-sync/fileSystem/intellij.settingsSync.fileSystem.iml" filepath="$PROJECT_DIR$/plugins/settings-sync/fileSystem/intellij.settingsSync.fileSystem.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/plugins/settings-sync/git/intellij.settingsSync.git.iml" filepath="$PROJECT_DIR$/plugins/settings-sync/git/intellij.settingsSync.git.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/plugins/settings-sync/jba/intellij.settingsSync.jba.iml" filepath="$PROJECT_DIR$/plugins/settings-sync/jba/intellij.settingsSync.jba.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/plugins/sh/intellij.sh.iml" filepath="$PROJECT_DIR$/plugins/sh/intellij.sh.iml" />
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
<orderEntry type="module" module-name="intellij.settingsSync" scope="RUNTIME" />
|
||||
<orderEntry type="module" module-name="intellij.settingsSync.git" scope="RUNTIME" />
|
||||
<orderEntry type="module" module-name="intellij.settingsSync.jba" scope="RUNTIME" />
|
||||
<orderEntry type="module" module-name="intellij.settingsSync.fileSystem" scope="RUNTIME" />
|
||||
<orderEntry type="module" module-name="intellij.java.featuresTrainer" scope="RUNTIME" />
|
||||
<orderEntry type="module" module-name="intellij.idea.community.build.tasks" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.junit.v5.rt.tests" scope="TEST" />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="kotlin-stdlib" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.settingsSync" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core.ui" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<idea-plugin package="intellij.settingsSync.fileSystem">
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<settingsSync.communicatorProvider implementation="intellij.settingsSync.fileSystem.FSCommunicatorProvider"/>
|
||||
</extensions>
|
||||
<actions resource-bundle="messages.BackupNSyncFSBundle">
|
||||
<action class="intellij.settingsSync.fileSystem.EnableBackupNSyncRemotely" id="settingsSync.enableRemotely"
|
||||
icon="AllIcons.General.Settings"/>
|
||||
</actions>
|
||||
</idea-plugin>
|
||||
@@ -0,0 +1 @@
|
||||
action.settingsSync.enableRemotely.text = Enable Backup Sync (from FS)
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package intellij.settingsSync.fileSystem
|
||||
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.fileChooser.FileChooserFactory
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.settingsSync.SettingsSyncLocalSettings
|
||||
import com.intellij.settingsSync.SettingsSyncSettings
|
||||
import com.intellij.settingsSync.UpdateResult.*
|
||||
import com.intellij.settingsSync.config.SettingsSyncEnabler
|
||||
import com.intellij.util.containers.toMutableSmartList
|
||||
|
||||
class EnableBackupNSyncRemotely : DumbAwareAction() {
|
||||
override fun getActionUpdateThread() = ActionUpdateThread.BGT
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
val folderDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
|
||||
.withTitle("Select root folder")
|
||||
val chooser = FileChooserFactory.getInstance().createPathChooser(folderDescriptor, null, null)
|
||||
chooser.choose(null) {
|
||||
val path = it.single().path
|
||||
val availableAccounts = (PropertiesComponent.getInstance().getList("FSAuthServiceAccounts") ?: emptyList()).toMutableSmartList()
|
||||
availableAccounts.add(path)
|
||||
SettingsSyncLocalSettings.getInstance().userId = path
|
||||
SettingsSyncLocalSettings.getInstance().providerCode = "fs"
|
||||
PropertiesComponent.getInstance().setList("FSAuthServiceAccounts", availableAccounts)
|
||||
}
|
||||
SettingsSyncSettings.getInstance().syncEnabled = true
|
||||
val enabler = SettingsSyncEnabler()
|
||||
val serverState = enabler.getServerState()
|
||||
when (serverState) {
|
||||
is NoFileOnServer, FileDeletedFromServer -> {
|
||||
enabler.pushSettingsToServer()
|
||||
}
|
||||
is Success -> {
|
||||
enabler.getSettingsFromServer(null)
|
||||
}
|
||||
is Error -> {
|
||||
logger<EnableBackupNSyncRemotely>().error(serverState.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package intellij.settingsSync.fileSystem
|
||||
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.fileChooser.FileChooserFactory
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
import com.intellij.platform.ide.progress.withModalProgress
|
||||
import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.settingsSync.communicator.SettingsSyncUserData
|
||||
import com.intellij.util.containers.mapSmart
|
||||
import com.intellij.util.containers.toMutableSmartList
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.Component
|
||||
import java.nio.file.Path
|
||||
import javax.swing.Icon
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.name
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
|
||||
internal class FSAuthService : SettingsSyncAuthService {
|
||||
override val providerCode: String
|
||||
get() = "fs"
|
||||
override val providerName: String
|
||||
get() = "File System"
|
||||
|
||||
override val icon: Icon?
|
||||
get() = com.intellij.icons.AllIcons.Actions.ModuleDirectory
|
||||
|
||||
override suspend fun login(parentComponent: Component?): SettingsSyncUserData? {
|
||||
val modalTaskOwner = if (parentComponent != null)
|
||||
ModalTaskOwner.component(parentComponent)
|
||||
else
|
||||
ModalTaskOwner.guess()
|
||||
return withModalProgress(modalTaskOwner, "Getting data", TaskCancellation.cancellable(), ) {
|
||||
val folderDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
|
||||
.withTitle("Select root folder")
|
||||
withContext(Dispatchers.EDT) {
|
||||
val chooser = FileChooserFactory.getInstance().createPathChooser(folderDescriptor, null, null)
|
||||
suspendCancellableCoroutine<SettingsSyncUserData?> { cont ->
|
||||
cont.invokeOnCancellation {
|
||||
cont.resume(null)
|
||||
}
|
||||
chooser.choose(null) {
|
||||
val path = it.single().path
|
||||
val userData = userDataFromPath(path)
|
||||
val availableAccounts = (PropertiesComponent.getInstance().getList("FSAuthServiceAccounts") ?: emptyList()).toMutableSmartList()
|
||||
if (!availableAccounts.contains(path)) {
|
||||
availableAccounts.add(path)
|
||||
PropertiesComponent.getInstance().setList("FSAuthServiceAccounts", availableAccounts)
|
||||
}
|
||||
cont.resume(userData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getUserData(userId: String): SettingsSyncUserData? {
|
||||
return getAvailableUserAccounts().find { it.id == userId }
|
||||
}
|
||||
|
||||
override fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
|
||||
return PropertiesComponent.getInstance().getList("FSAuthServiceAccounts")?.mapSmart { userDataFromPath(it) } ?: emptyList()
|
||||
}
|
||||
|
||||
private fun userDataFromPath(pathStr: String) : SettingsSyncUserData {
|
||||
val path = Path.of(pathStr)
|
||||
return SettingsSyncUserData(
|
||||
path.absolutePathString(),
|
||||
providerCode,
|
||||
path.name,
|
||||
"noname@email.com"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package intellij.settingsSync.fileSystem
|
||||
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.settingsSync.*
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.getLastModifiedTime
|
||||
|
||||
internal class FSCommunicator(override val userId: String) : AbstractServerCommunicator() {
|
||||
|
||||
private val basePath: Path = Path.of(userId)
|
||||
|
||||
companion object {
|
||||
private val LOG = logger<FSCommunicator>()
|
||||
}
|
||||
|
||||
override fun requestSuccessful() {
|
||||
LOG.info("requestSuccessful")
|
||||
}
|
||||
|
||||
override fun handleRemoteError(e: Throwable): String {
|
||||
LOG.warn("remote error occurred", e)
|
||||
return e.message ?: "Remote error occurred: ${e.javaClass.name}"
|
||||
}
|
||||
|
||||
override fun readFileInternal(filePath: String): Pair<InputStream?, String?> {
|
||||
val path = basePath.resolve(filePath)
|
||||
val file = path.toFile()
|
||||
if (file.exists()) {
|
||||
return Pair(file.inputStream(), path.getLastModifiedTime().toString())
|
||||
}
|
||||
return Pair(null, null)
|
||||
}
|
||||
|
||||
override fun writeFileInternal(filePath: String, versionId: String?, content: InputStream): String? {
|
||||
val path = basePath.resolve(filePath)
|
||||
if (path.toFile().exists() && path.getLastModifiedTime().toString() != versionId) {
|
||||
throw InvalidVersionIdException("Expected versionId is $versionId, but actual is ${path.getLastModifiedTime()}")
|
||||
}
|
||||
if (!path.parent.toFile().exists()) {
|
||||
path.parent.toFile().mkdirs()
|
||||
}
|
||||
path.toFile().outputStream().use { content.copyTo(it) }
|
||||
return path.getLastModifiedTime().toString()
|
||||
}
|
||||
|
||||
override fun getLatestVersion(filePath: String): String? {
|
||||
val path = basePath.resolve(filePath)
|
||||
if (!path.toFile().exists())
|
||||
return null
|
||||
return path.getLastModifiedTime().toString()
|
||||
}
|
||||
|
||||
override fun deleteFileInternal(filePath: String) {
|
||||
val path = basePath.resolve(filePath)
|
||||
path.toFile().delete()
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package intellij.settingsSync.fileSystem
|
||||
|
||||
import com.intellij.settingsSync.SettingsSyncRemoteCommunicator
|
||||
import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.settingsSync.communicator.SettingsSyncCommunicatorProvider
|
||||
|
||||
class FSCommunicatorProvider : SettingsSyncCommunicatorProvider {
|
||||
private val authServiceLazy = lazy<FSAuthService> { FSAuthService() }
|
||||
|
||||
override val providerCode: String
|
||||
get() = "fs"
|
||||
|
||||
override val authService: SettingsSyncAuthService
|
||||
get() = authServiceLazy.value
|
||||
|
||||
override fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator? {
|
||||
return FSCommunicator(userId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -56,5 +56,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.performanceTesting" />
|
||||
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.observable" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -20,5 +20,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.statistics" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
|
||||
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.progress" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -1,4 +1,5 @@
|
||||
action.settingsSync.troubleShoot.text=Backup and Sync Troubleshooting
|
||||
login.manual.helper.text=In case of troubles please log in via Help->Register Plugins
|
||||
|
||||
troubleshooting.loading.info.progress.dialog.title=Loading information about Backup and Sync\u2026
|
||||
troubleshooting.dialog.title=Backup and Sync Troubleshooting
|
||||
|
||||
+5
-2
@@ -56,8 +56,8 @@ internal open class CloudConfigServerCommunicator(serverUrl: String? = null,
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun readFileInternal(snapshotFilePath: String): Pair<InputStream?, String?> {
|
||||
return clientVersionContext.doWithVersion(snapshotFilePath, null) { filePath ->
|
||||
override fun readFileInternal(filePath: String): Pair<InputStream?, String?> {
|
||||
return clientVersionContext.doWithVersion(filePath, null) { filePath ->
|
||||
try {
|
||||
val stream = client.read(filePath)
|
||||
|
||||
@@ -122,6 +122,9 @@ internal open class CloudConfigServerCommunicator(serverUrl: String? = null,
|
||||
client.delete(filePath)
|
||||
}
|
||||
|
||||
override val userId: String
|
||||
get() = "jba"
|
||||
|
||||
|
||||
override fun writeFileInternal(filePath: String, versionId: String?, content: InputStream) : String? {
|
||||
return clientVersionContext.doWithVersion(filePath, versionId) { filePath ->
|
||||
|
||||
+2
-1
@@ -12,12 +12,13 @@ class JbaCommunicatorProvider : SettingsSyncCommunicatorProvider, Disposable {
|
||||
|
||||
override val providerCode: String
|
||||
get() = "jba"
|
||||
|
||||
override val authService: SettingsSyncAuthService
|
||||
get() {
|
||||
return authServiceLazy.value
|
||||
}
|
||||
|
||||
override fun createCommunicator(): SettingsSyncRemoteCommunicator? = lazy<CloudConfigServerCommunicator> {
|
||||
override fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator = lazy<CloudConfigServerCommunicator> {
|
||||
CloudConfigServerCommunicator(null, authServiceLazy.value)
|
||||
}.value
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.intellij.settingsSync.jba
|
||||
|
||||
import com.intellij.DynamicBundle
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
|
||||
private const val PATH_TO_BUNDLE = "messages.SettingsSyncJbaBundle"
|
||||
|
||||
@ApiStatus.Internal
|
||||
object SettingsSyncJbaBundle {
|
||||
private val bundle = DynamicBundle(SettingsSyncJbaBundle::class.java, PATH_TO_BUNDLE)
|
||||
fun message(@PropertyKey(resourceBundle = PATH_TO_BUNDLE) key: String, vararg params: Any) : @Nls String {
|
||||
return bundle.getMessage(key, *params)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -79,7 +79,7 @@ class SettingsSyncPromotion : SettingsDialogListener {
|
||||
|
||||
SettingsSyncEvents.Companion.getInstance().addListener(object : SettingsSyncEventListener {
|
||||
override fun loginStateChanged() {
|
||||
if (RemoteCommunicatorHolder.getAuthService().isLoggedIn()) {
|
||||
if (RemoteCommunicatorHolder.getCurrentUserData() != null) {
|
||||
SettingsSyncEventsStatistics.PROMOTION_IN_SETTINGS.log(SettingsSyncEventsStatistics.PromotionInSettingsEvent.LOGGED_IN)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -64,7 +64,12 @@ internal class SettingsSyncTroubleshootingAction : DumbAwareAction() {
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator()
|
||||
val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run {
|
||||
Messages.showErrorDialog(e.project,
|
||||
"No remote communicator available",
|
||||
SettingsSyncBundle.message("troubleshooting.dialog.title"))
|
||||
return
|
||||
}
|
||||
if (remoteCommunicator !is CloudConfigServerCommunicator) {
|
||||
Messages.showErrorDialog(e.project,
|
||||
SettingsSyncBundle.message("troubleshooting.dialog.error.wrong.configuration", remoteCommunicator::class),
|
||||
@@ -147,7 +152,7 @@ internal class SettingsSyncTroubleshootingAction : DumbAwareAction() {
|
||||
val remoteCommunicator: CloudConfigServerCommunicator,
|
||||
val rootNode: TreeNode.Branch) : DialogWrapper(project, true) {
|
||||
|
||||
val userData = RemoteCommunicatorHolder.getAuthService().getUserData()
|
||||
val userData = RemoteCommunicatorHolder.getCurrentUserData()
|
||||
|
||||
init {
|
||||
title = SettingsSyncBundle.message("troubleshooting.dialog.title")
|
||||
@@ -273,7 +278,7 @@ internal class SettingsSyncTroubleshootingAction : DumbAwareAction() {
|
||||
if (showHistoryButton) {
|
||||
actionButton(object : DumbAwareAction(AllIcons.Vcs.History) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
showHistoryDialog(project, remoteCommunicator, version.filePath, userData.name!!)
|
||||
showHistoryDialog(project, remoteCommunicator, version.filePath, userData?.name ?: "Unknown")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+9
-1
@@ -22,7 +22,15 @@ object DummyJBAccountInfoService : JBAccountInfoService {
|
||||
}
|
||||
|
||||
override fun startLoginSession(loginMode: JBAccountInfoService.LoginMode, authProviderId: String?, clientMetadata: Map<String, String>): JBAccountInfoService.LoginSession {
|
||||
TODO("Not yet implemented")
|
||||
return object : JBAccountInfoService.LoginSession {
|
||||
override fun close() {
|
||||
|
||||
}
|
||||
|
||||
override fun onCompleted(): CompletableFuture<JBAccountInfoService.LoginResult> {
|
||||
return CompletableFuture.completedFuture(JBAccountInfoService.LoginResult.LoginSuccessful(dummyUserData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAvailableLicenses(productCode: String): CompletableFuture<JBAccountInfoService.LicenseListResult> {
|
||||
|
||||
+76
-21
@@ -1,32 +1,38 @@
|
||||
package com.intellij.settingsSync.jba.auth
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.application.ex.ApplicationManagerEx
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
import com.intellij.platform.ide.progress.withModalProgress
|
||||
import com.intellij.settingsSync.SettingsSyncEvents
|
||||
import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.settingsSync.communicator.SettingsSyncUserData
|
||||
import com.intellij.settingsSync.jba.SettingsSyncJbaBundle
|
||||
import com.intellij.settingsSync.jba.SettingsSyncPromotion
|
||||
import com.intellij.ui.JBAccountInfoService
|
||||
import java.util.function.Consumer
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.Component
|
||||
import java.util.concurrent.CancellationException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
internal class JBAAuthService : SettingsSyncAuthService {
|
||||
|
||||
companion object {
|
||||
private val LOG = logger<JBAAuthService>()
|
||||
private const val JBA_USER_ID = "jba"
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var invalidatedIdToken: String? = null
|
||||
|
||||
override fun isLoggedIn(): Boolean {
|
||||
return isTokenValid(getAccountInfoService()?.idToken)
|
||||
}
|
||||
|
||||
private fun isTokenValid(token: String?): Boolean {
|
||||
return token != null && token != invalidatedIdToken
|
||||
}
|
||||
|
||||
override fun getUserData() = fromJBAData(
|
||||
override fun getUserData(userId: String) = fromJBAData(
|
||||
if (ApplicationManagerEx.isInIntegrationTest()) {
|
||||
DummyJBAccountInfoService.userData
|
||||
} else {
|
||||
@@ -34,12 +40,23 @@ internal class JBAAuthService : SettingsSyncAuthService {
|
||||
}
|
||||
)
|
||||
|
||||
private fun fromJBAData(jbaData: JBAccountInfoService.JBAData?) : SettingsSyncUserData {
|
||||
override fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
|
||||
val userData = getUserData("")
|
||||
if (userData != null) {
|
||||
return listOf(userData)
|
||||
} else {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fromJBAData(jbaData: JBAccountInfoService.JBAData?) : SettingsSyncUserData? {
|
||||
if (jbaData == null) {
|
||||
return SettingsSyncUserData.EMPTY
|
||||
return null
|
||||
} else {
|
||||
return SettingsSyncUserData(
|
||||
jbaData.loginName,
|
||||
JBA_USER_ID,
|
||||
providerCode,
|
||||
jbaData.email,
|
||||
jbaData.email,
|
||||
)
|
||||
}
|
||||
@@ -53,9 +70,17 @@ internal class JBAAuthService : SettingsSyncAuthService {
|
||||
}
|
||||
override val providerCode: String
|
||||
get() = "jba"
|
||||
override val providerName: String
|
||||
get() = "JetBrains"
|
||||
|
||||
override fun login() {
|
||||
if (!isLoggedIn()) {
|
||||
override val icon = AllIcons.Ultimate.IdeaUltimatePromo
|
||||
|
||||
override suspend fun login(parentComponent: Component?): SettingsSyncUserData? {
|
||||
val modalTaskOwner = if (parentComponent != null)
|
||||
ModalTaskOwner.component(parentComponent)
|
||||
else
|
||||
ModalTaskOwner.guess()
|
||||
return withModalProgress(modalTaskOwner, SettingsSyncJbaBundle.message("login.manual.helper.text"), TaskCancellation.cancellable()) {
|
||||
val accountInfoService = getAccountInfoService()
|
||||
val loginMetadata = hashMapOf(
|
||||
"from.settings.sync" to "true"
|
||||
@@ -63,25 +88,55 @@ internal class JBAAuthService : SettingsSyncAuthService {
|
||||
if (SettingsSyncPromotion.promotionShownThisSession) {
|
||||
loginMetadata["from.settings.sync.promotion"] = "true"
|
||||
}
|
||||
if (accountInfoService != null) {
|
||||
if (accountInfoService == null) {
|
||||
LOG.error("JBA auth service is not available!")
|
||||
return@withModalProgress null
|
||||
}
|
||||
if (isTokenValid(accountInfoService.idToken)) {
|
||||
return@withModalProgress fromJBAData(accountInfoService.userData)
|
||||
}
|
||||
suspendCancellableCoroutine<SettingsSyncUserData?> { cont ->
|
||||
try {
|
||||
val loginSession: JBAccountInfoService.LoginSession? = accountInfoService.startLoginSession(
|
||||
JBAccountInfoService.LoginMode.AUTO, null, loginMetadata)
|
||||
|
||||
loginSession!!.onCompleted().thenAccept(Consumer<JBAccountInfoService.LoginResult> {
|
||||
SettingsSyncEvents.getInstance().fireLoginStateChanged()
|
||||
})
|
||||
cont.invokeOnCancellation {
|
||||
cont.resume(null)
|
||||
}
|
||||
accountInfoService
|
||||
.startLoginSession(JBAccountInfoService.LoginMode.AUTO, null, loginMetadata)
|
||||
.onCompleted()
|
||||
.exceptionally { exc ->
|
||||
if (exc is CancellationException) {
|
||||
LOG.warn("Login cancelled")
|
||||
}
|
||||
else {
|
||||
LOG.warn("Login failed", exc)
|
||||
}
|
||||
cont.resume(null)
|
||||
null
|
||||
}.thenApply { loginResult ->
|
||||
val result: SettingsSyncUserData? = when (loginResult) {
|
||||
is JBAccountInfoService.LoginResult.LoginSuccessful -> {
|
||||
fromJBAData(loginResult.jbaUser)
|
||||
}
|
||||
is JBAccountInfoService.LoginResult.LoginFailed -> {
|
||||
LOG.warn("Login failed: ${loginResult.errorMessage}")
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
LOG.warn("Unknown login result: $loginResult")
|
||||
null
|
||||
}
|
||||
}
|
||||
cont.resume(result)
|
||||
}
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
LOG.error(e)
|
||||
SettingsSyncEvents.getInstance().fireLoginStateChanged()
|
||||
cont.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isLoginAvailable(): Boolean = getAccountInfoService() != null
|
||||
|
||||
fun invalidateJBA(idToken: String) {
|
||||
if (invalidatedIdToken == idToken) return
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.intellij.settingsSync
|
||||
|
||||
import com.intellij.idea.TestFor
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.progress.runBlockingCancellable
|
||||
import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.settingsSync.jba.CloudConfigServerCommunicator
|
||||
import com.intellij.settingsSync.jba.CloudConfigVersionContext
|
||||
@@ -72,11 +73,14 @@ internal class SettingsSyncAuthTest : BasePlatformTestCase() {
|
||||
|
||||
communicator.checkServerState()
|
||||
verify(authServiceSpy, times(1)).invalidateJBA("OLD-ID-TOKEN")
|
||||
assertFalse(authServiceSpy.isLoggedIn())
|
||||
|
||||
TODO("assertFalse(authServiceSpy.isLoggedIn())")
|
||||
|
||||
// User updates JBA details
|
||||
`when`(accountInfoService.idToken).thenReturn("NEW-ID-TOKEN")
|
||||
authServiceSpy.login()
|
||||
runBlockingCancellable {
|
||||
authServiceSpy.login(null)
|
||||
}
|
||||
|
||||
// Check that userId was updated in communicator
|
||||
communicator.checkServerState()
|
||||
@@ -115,7 +119,7 @@ internal class SettingsSyncAuthTest : BasePlatformTestCase() {
|
||||
}
|
||||
|
||||
communicator.checkServerState()
|
||||
assertFalse(authServiceSpy.isLoggedIn())
|
||||
TODO("assertFalse(authServiceSpy.isLoggedIn())")
|
||||
assertFalse(SettingsSyncSettings.getInstance().syncEnabled)
|
||||
}
|
||||
}
|
||||
+15
-9
@@ -4,13 +4,14 @@ import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.settingsSync.communicator.SettingsSyncUserData
|
||||
import com.intellij.settingsSync.jba.auth.DummyJBAccountInfoService
|
||||
import com.intellij.ui.JBAccountInfoService
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import java.awt.Component
|
||||
import javax.swing.Icon
|
||||
|
||||
internal class SettingsSyncTestAuthService : SettingsSyncAuthService {
|
||||
override fun isLoggedIn(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getUserData(): SettingsSyncUserData {
|
||||
override fun getUserData(userId: String): SettingsSyncUserData {
|
||||
val id = System.getenv("SETTINGS_SYNC_TEST_ID")
|
||||
val loginName = "testLogin"
|
||||
val email = "testEmail@example.com"
|
||||
@@ -18,6 +19,10 @@ internal class SettingsSyncTestAuthService : SettingsSyncAuthService {
|
||||
return SettingsSyncUserData(loginName, email)
|
||||
}
|
||||
|
||||
override fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
fun getAccountInfoService(): JBAccountInfoService {
|
||||
return DummyJBAccountInfoService
|
||||
}
|
||||
@@ -27,11 +32,12 @@ internal class SettingsSyncTestAuthService : SettingsSyncAuthService {
|
||||
|
||||
override val providerCode: String
|
||||
get() = TODO("Not yet implemented")
|
||||
override val providerName: String
|
||||
get() = TODO("Not yet implemented")
|
||||
override val icon: Icon?
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override fun login() {
|
||||
}
|
||||
|
||||
override fun isLoginAvailable(): Boolean {
|
||||
return false
|
||||
override suspend fun login(parentComponent: Component?) : SettingsSyncUserData? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<content>
|
||||
<module name="intellij.settingsSync.git"/>
|
||||
<module name="intellij.settingsSync.jba"/>
|
||||
<!--<module name="intellij.settingsSync.fileSystem"/>-->
|
||||
</content>
|
||||
|
||||
<depends>com.intellij.modules.platform</depends>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="2" fill="#818594"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="2" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -41,16 +41,28 @@ config.button.login=Log in with JetBrains Account\u2026
|
||||
# config.button.logout=Log Out...
|
||||
config.button.enable=Enable Backup and Sync\u2026
|
||||
config.button.disable=Disable Backup and Sync\u2026
|
||||
enable.dialog.select.what.to.sync=Select what settings to sync:
|
||||
enable.dialog.select.what.to.sync=Settings to sync
|
||||
enable.dialog.enable.sync.action=Enable Backup and Sync
|
||||
enable.dialog.source.option.title=Choose Settings Source
|
||||
enable.dialog.source.option.text=There already are existing settings on this account. Please choose which settings you would like to use as the basis for sync going forward.
|
||||
enable.dialog.sync.local.settings=Push Settings to Account
|
||||
enable.dialog.sync.local.settings.option=Use the local settings and upload them to your account storage
|
||||
enable.dialog.sync.local.settings.text=Settings saved on your account will be overwritten by the local ones
|
||||
enable.dialog.get.settings.from.account=Get Settings from Account
|
||||
enable.sync.check.server.data.progress=Checking Server Data\u2026
|
||||
enable.dialog.get.settings.from.account.option=Use the settings from your account storage
|
||||
enable.dialog.get.settings.from.account.text=Your local settings will be overwritten by the settings on your account
|
||||
enable.dialog.change=Change
|
||||
enable.sync.check.server.data.progress=Checking server data\u2026
|
||||
enable.sync.get.from.server.progress=Getting Settings from Server\u2026
|
||||
enable.sync.push.to.server.progress=Pushing Settings to Server\u2026
|
||||
disable.dialog.title=Disable Backup and Sync?
|
||||
disable.dialog.text=The settings will not be synced anymore
|
||||
disable.dialog.remove.data.box=Remove data from JB account and disable for all IDEs
|
||||
enable.sync.add.account=Add Account
|
||||
enable.sync.choose.data.provider.title=Choose Provider
|
||||
enable.sync.choose.data.provider.text=Please select a provider that will store and process your data
|
||||
disable.dialog.title=Confirm disabling Settings Sync
|
||||
# {0} - account type, i.e. JetBrains or Google
|
||||
disable.dialog.text=This will disable Settings Sync for this installation,\nleaving the data stored on the {0} account intact.\nThis change will take effect immediately.
|
||||
# {0} - account type, i.e. JetBrains or Google
|
||||
disable.dialog.remove.data.box=Remove data from {0} account and disable for all IDEs
|
||||
disable.dialog.disable.button=Disable
|
||||
disable.remove.data.title=Removing Server Data\u2026
|
||||
disable.remove.data.failure=Unable to remove data from server
|
||||
@@ -66,6 +78,7 @@ subcategory.config.link=Configure
|
||||
|
||||
settings.category.ui.editor.font=Editor font
|
||||
settings.sync.info.message=Sync UI, Code and System settings, Keymaps, Plugins, and Tools.
|
||||
settings.sync.select.provider.message=To enable Backup and Sync, select a provider that will store and process your data
|
||||
sync.restart.notification.title=Restart required after syncing settings
|
||||
|
||||
# {0} - count of plugins, {1} - coma separated list of plugins (max two plugins)
|
||||
@@ -83,12 +96,14 @@ sync.notification.restart.message.list.entry.plugin.disable=Disable {0,choice,1#
|
||||
sync.restart.notification.submessage.plugins={0} plugin(s): {1}\u2026
|
||||
# {0} - IDE name, i.e. Android Studio, MPS, etc.
|
||||
sync.restart.notification.action=Restart {0}
|
||||
sync.status.enabled=Backup and Sync enabled
|
||||
sync.status.enabled=Backup and Sync enabled for
|
||||
sync.status.disabled=Backup and Sync disabled
|
||||
sync.status.failed=Sync failed.
|
||||
sync.status.login.message=Login to enable Backup and Sync
|
||||
# {0} is last sync time (human-readable), {1} is a user name, for example: "Last synced 5 minutes ago for John.Doe"
|
||||
sync.status.last.sync.message=Last synced {0} for {1}.
|
||||
# {0} is last sync time (human-readable), after (in a different component) goes a user-name, for example: "Last synced 5 minutes ago for John.Doe"
|
||||
sync.status.last.sync.message=Synced {0} for
|
||||
sync.status.disabled.message=Disabled for
|
||||
sync.login.message=Logging in\u2026
|
||||
# The full (original) message is "..., install JetBrains Marketplace Licensing Support"
|
||||
# but the plugin name is rendered in a separate component and doesn't need to be translated
|
||||
sync.status.login.not.available=To enable Backup and Sync, install
|
||||
|
||||
@@ -15,13 +15,75 @@ abstract class AbstractServerCommunicator() : SettingsSyncRemoteCommunicator {
|
||||
private val LOG = logger<AbstractServerCommunicator>()
|
||||
}
|
||||
|
||||
private var myTemporary = false
|
||||
|
||||
override fun setTemporary(isTemporary: Boolean) {
|
||||
myTemporary = isTemporary
|
||||
}
|
||||
|
||||
/**
|
||||
* called when a request is successful, as an opposite to handleRemoteError
|
||||
*/
|
||||
protected open fun requestSuccessful() {}
|
||||
|
||||
/**
|
||||
* Handles errors that occur during remote operations by mapping the given `Throwable` to a meaningful error message.
|
||||
*
|
||||
* @param e The exception or error that occurred during a remote operation.
|
||||
* @return A string describing the error in a human-readable format, suitable for logging or display.
|
||||
*/
|
||||
protected abstract fun handleRemoteError(e: Throwable): String
|
||||
|
||||
/**
|
||||
* Reads the content of a file from the given file path.
|
||||
*
|
||||
* @param filePath The path of the file to be read.
|
||||
* @return A pair containing:
|
||||
* - An InputStream of the file content if the operation is successful, otherwise null.
|
||||
* - A server version identifier associated with the file, or null if unavailable.
|
||||
* @throws IOException If an I/O error occurs during file reading.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun readFileInternal(filePath: String): Pair<InputStream?, String?>
|
||||
|
||||
/**
|
||||
* Writes the content to a specified file, potentially associated with a particular version ID.
|
||||
*
|
||||
* @param filePath The path to the file where the content will be written.
|
||||
* @param versionId An optional version identifier for the file. If provided, the version ID must match
|
||||
* the version on server; otherwise, an InvalidVersionIdException may occur.
|
||||
* @param content The content to be written to the file, provided as an InputStream.
|
||||
* @return The version ID of the file after the content is written, or null if the operation does not result
|
||||
* in an identifiable version.
|
||||
* @throws IOException If an I/O error occurs during the writing process.
|
||||
* @throws InvalidVersionIdException If the provided(expected) version ID doesn't match the actual remote one.
|
||||
*/
|
||||
@Throws(IOException::class, InvalidVersionIdException::class)
|
||||
protected abstract fun writeFileInternal(filePath: String, versionId: String?, content: InputStream): String?
|
||||
|
||||
/**
|
||||
* Fetches the latest version identifier for the file at the specified file path.
|
||||
*
|
||||
* This version is compared against SettingsSyncLocalSettings.getKnownAndAppliedServerId
|
||||
* to check whether settings version on server is different from the local one.
|
||||
*
|
||||
* @param filePath The path to the file whose latest version is to be retrieved.
|
||||
* @return The latest version identifier of the file, or null if no version information is available.
|
||||
* @throws IOException If an I/O error occurs while attempting to retrieve the version information.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun getLatestVersion(filePath: String) : String?
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun deleteFileInternal(filePath: String)
|
||||
|
||||
|
||||
@VisibleForTesting
|
||||
@Throws(IOException::class, SecurityException::class)
|
||||
protected fun currentSnapshotFilePath(): Pair<String, Boolean>? {
|
||||
try {
|
||||
val crossIdeSyncEnabled = isFileExists(CROSS_IDE_SYNC_MARKER_FILE)
|
||||
if (crossIdeSyncEnabled != SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled) {
|
||||
if (!myTemporary && crossIdeSyncEnabled != SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled) {
|
||||
LOG.info("Cross-IDE sync status on server is: ${enabledOrDisabled(crossIdeSyncEnabled)}. Updating local settings with it.")
|
||||
SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled = crossIdeSyncEnabled
|
||||
}
|
||||
@@ -63,7 +125,7 @@ abstract class AbstractServerCommunicator() : SettingsSyncRemoteCommunicator {
|
||||
if (force) {
|
||||
// get the latest server version: pushing with it will overwrite the file in any case
|
||||
versionToPush = getLatestVersion(snapshotFilePath)
|
||||
writeFileInternal(snapshotFilePath, null, inputStream)
|
||||
writeFileInternal(snapshotFilePath, versionToPush, inputStream)
|
||||
}
|
||||
else {
|
||||
if (knownServerVersion != null) {
|
||||
@@ -172,22 +234,6 @@ abstract class AbstractServerCommunicator() : SettingsSyncRemoteCommunicator {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun requestSuccessful()
|
||||
|
||||
protected abstract fun handleRemoteError(e: Throwable): String
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun readFileInternal(snapshotFilePath: String): Pair<InputStream?, String?>
|
||||
|
||||
@Throws(IOException::class, InvalidVersionIdException::class)
|
||||
protected abstract fun writeFileInternal(filePath: String, versionId: String?, content: InputStream): String?
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun getLatestVersion(filePath: String) : String?
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun deleteFileInternal(filePath: String)
|
||||
|
||||
override fun createFile(filePath: String, content: String) {
|
||||
writeFileInternal(filePath, null, content.byteInputStream())
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class SettingsSyncBridge(
|
||||
private val pendingEvents = ContainerUtil.createConcurrentList<SyncSettingsEvent.StandardEvent>()
|
||||
|
||||
private val remoteCommunicator: SettingsSyncRemoteCommunicator
|
||||
get() = RemoteCommunicatorHolder.getRemoteCommunicator()
|
||||
get() = RemoteCommunicatorHolder.getRemoteCommunicator() ?: DummyCommunicator
|
||||
|
||||
@Volatile
|
||||
private var queueJob: Job? = null
|
||||
@@ -73,6 +73,7 @@ class SettingsSyncBridge(
|
||||
}
|
||||
}
|
||||
while (!locked)
|
||||
LOG.info("Lock obtained for exclusive event")
|
||||
processExclusiveEvent(event)
|
||||
}
|
||||
catch (th: Throwable) {
|
||||
@@ -83,6 +84,7 @@ class SettingsSyncBridge(
|
||||
if (!eventsLock.compareAndSet(true, false)) {
|
||||
LOG.error("eventsLock already unlocked by someone else!!!")
|
||||
}
|
||||
LOG.info("Lock released for exclusive event")
|
||||
}
|
||||
pendingExclusiveEvents.remove(event)
|
||||
}
|
||||
@@ -563,11 +565,54 @@ class SettingsSyncBridge(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@TestOnly
|
||||
internal fun stop() {
|
||||
stopSyncingAndRollback(null, null)
|
||||
}
|
||||
|
||||
private object DummyCommunicator : SettingsSyncRemoteCommunicator {
|
||||
override val userId: String
|
||||
get() = ""
|
||||
|
||||
override fun setTemporary(isTemporary: Boolean) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun checkServerState(): ServerState {
|
||||
val errorMsg = "Cannot check server state - no communicator provided"
|
||||
LOG.info(errorMsg)
|
||||
return ServerState.Error(errorMsg)
|
||||
}
|
||||
|
||||
override fun receiveUpdates(): UpdateResult {
|
||||
val errorMsg = "Cannot received updates - no communicator provided"
|
||||
LOG.info(errorMsg)
|
||||
return UpdateResult.Error(errorMsg)
|
||||
}
|
||||
|
||||
override fun push(snapshot: SettingsSnapshot, force: Boolean, expectedServerVersionId: String?): SettingsSyncPushResult {
|
||||
val errorMsg = "Cannot push - no communicator provided"
|
||||
LOG.info(errorMsg)
|
||||
return SettingsSyncPushResult.Error(errorMsg)
|
||||
}
|
||||
|
||||
override fun createFile(filePath: String, content: String) {
|
||||
LOG.info("Cannot create file '$filePath' - no communicator provided")
|
||||
}
|
||||
|
||||
override fun deleteFile(filePath: String) {
|
||||
LOG.info("Cannot delete file '$filePath' - no communicator provided")
|
||||
}
|
||||
|
||||
override fun isFileExists(filePath: String): Boolean {
|
||||
LOG.info("Cannot check if file '$filePath' exists - no communicator provided")
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private val LOG = logger<SettingsSyncBridge>()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ interface SettingsSyncLocalState {
|
||||
val applicationId: UUID
|
||||
var knownAndAppliedServerId: String?
|
||||
var isCrossIdeSyncEnabled: Boolean
|
||||
var userId: String?
|
||||
var providerCode: String?
|
||||
}
|
||||
|
||||
@State(name = "SettingsSyncLocalSettings", storages = [Storage("settingsSyncLocal.xml", roamingType = RoamingType.DISABLED)])
|
||||
@@ -23,12 +25,16 @@ class SettingsSyncLocalSettings : SimplePersistentStateComponent<SettingsSyncLoc
|
||||
var applicationId: String? by string(UUID.randomUUID().toString())
|
||||
var knownAndAppliedServerId: String? by string(null)
|
||||
var crossIdeSyncEnabled by property(false)
|
||||
var userId by string(null)
|
||||
var providerCode by string(null)
|
||||
|
||||
@TestOnly
|
||||
internal fun reset() {
|
||||
applicationId = UUID.randomUUID().toString()
|
||||
knownAndAppliedServerId = null
|
||||
crossIdeSyncEnabled = false
|
||||
userId = null
|
||||
providerCode = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +42,8 @@ class SettingsSyncLocalSettings : SimplePersistentStateComponent<SettingsSyncLoc
|
||||
applicationId = newState.applicationId
|
||||
knownAndAppliedServerId = newState.knownAndAppliedServerId
|
||||
isCrossIdeSyncEnabled = newState.isCrossIdeSyncEnabled
|
||||
userId = newState.userId
|
||||
providerCode = newState.providerCode
|
||||
}
|
||||
|
||||
override var applicationId: UUID
|
||||
@@ -55,6 +63,18 @@ class SettingsSyncLocalSettings : SimplePersistentStateComponent<SettingsSyncLoc
|
||||
set(value) {
|
||||
state.crossIdeSyncEnabled = value
|
||||
}
|
||||
|
||||
override var userId: String?
|
||||
get() = state.userId
|
||||
set(value) {
|
||||
state.userId = value
|
||||
}
|
||||
|
||||
override var providerCode: String?
|
||||
get() = state.providerCode
|
||||
set(value) {
|
||||
state.providerCode = value
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary non-persistent form state akin to `SettingsSyncSettings`'s `SettingsSyncStateHolder`
|
||||
@@ -83,4 +103,17 @@ class SettingsSyncLocalStateHolder(
|
||||
set(value) {
|
||||
state.crossIdeSyncEnabled = value
|
||||
}
|
||||
|
||||
override var userId: String?
|
||||
get() = state.userId
|
||||
set(value) {
|
||||
state.userId = value
|
||||
}
|
||||
|
||||
override var providerCode: String?
|
||||
get() = state.providerCode
|
||||
set(value) {
|
||||
state.providerCode = value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -65,7 +65,10 @@ class SettingsSyncMain(coroutineScope: CoroutineScope) : Disposable {
|
||||
ideMediator: SettingsSyncIdeMediator,
|
||||
): SettingsSyncControls {
|
||||
val settingsLog = GitSettingsLog(settingsSyncStorage, appConfigPath, parentDisposable,
|
||||
RemoteCommunicatorHolder.getAuthService()::getUserData,
|
||||
{
|
||||
val userId = RemoteCommunicatorHolder.getRemoteCommunicator()?.userId ?: return@GitSettingsLog null
|
||||
RemoteCommunicatorHolder.getAuthService()?.getUserData(userId)
|
||||
},
|
||||
initialSnapshotProvider = { currentSnapshot ->
|
||||
ideMediator.getInitialSnapshot(appConfigPath, currentSnapshot)
|
||||
})
|
||||
|
||||
@@ -18,6 +18,19 @@ const val SETTINGS_SYNC_SNAPSHOT_ZIP = "$SETTINGS_SYNC_SNAPSHOT.zip"
|
||||
@ApiStatus.Internal
|
||||
interface SettingsSyncRemoteCommunicator {
|
||||
|
||||
/**
|
||||
* userId used within the communicator
|
||||
*/
|
||||
val userId: String
|
||||
|
||||
|
||||
/**
|
||||
* Indicates whether the communicator is temporary (i.e. used to check a remote state before enabling)
|
||||
* and shouldn't update configuration values
|
||||
*/
|
||||
fun setTemporary(isTemporary: Boolean)
|
||||
|
||||
|
||||
/**
|
||||
* checks the current state of the user's data in the cloud.
|
||||
*/
|
||||
|
||||
@@ -60,6 +60,12 @@ class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateCompo
|
||||
override val disabledSubcategories: Map<SettingsCategory, List<String>>
|
||||
get() = state.disabledSubcategories
|
||||
|
||||
fun updateCategories(disabledCategories: List<SettingsCategory>, disabledSubcategories: Map<SettingsCategory, List<String>>) {
|
||||
updateState {
|
||||
it.withDisabledCategories(disabledCategories).withDisabledSubcategories(disabledSubcategories)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyFromState(state: SettingsSyncState) {
|
||||
updateState {
|
||||
State(state.disabledCategories, state.disabledSubcategories, state.migrationFromOldStorageChecked, state.syncEnabled)
|
||||
@@ -78,7 +84,7 @@ class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateCompo
|
||||
return State(disabledCategories, disabledSubcategories, checked, syncEnabled)
|
||||
}
|
||||
|
||||
private fun withDisabledCategories(newCategories: List<SettingsCategory>): State {
|
||||
internal fun withDisabledCategories(newCategories: List<SettingsCategory>): State {
|
||||
return State(newCategories, disabledSubcategories, migrationFromOldStorageChecked, syncEnabled)
|
||||
}
|
||||
|
||||
@@ -95,7 +101,7 @@ class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateCompo
|
||||
}
|
||||
|
||||
|
||||
private fun withDisabledSubcategories(newSubcategoriesMap: Map<SettingsCategory, List<String>>): State {
|
||||
internal fun withDisabledSubcategories(newSubcategoriesMap: Map<SettingsCategory, List<String>>): State {
|
||||
return State(disabledCategories, newSubcategoriesMap, migrationFromOldStorageChecked, syncEnabled)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@ class SettingsSyncUpdateChecker() {
|
||||
|
||||
@RequiresBackgroundThread
|
||||
fun scheduleUpdateFromServer() : UpdateResult {
|
||||
val updateResult = RemoteCommunicatorHolder.getRemoteCommunicator().receiveUpdates()
|
||||
val updateResult = RemoteCommunicatorHolder.getRemoteCommunicator()?.receiveUpdates() ?: run {
|
||||
val errorMsg = "Cannot get update from server - no communicator provider"
|
||||
LOG.info(errorMsg)
|
||||
return UpdateResult.Error(errorMsg)
|
||||
}
|
||||
when(updateResult) {
|
||||
is UpdateResult.Success -> {
|
||||
val snapshot = updateResult.settingsSnapshot
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.progress.blockingContext
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.wm.IdeFrame
|
||||
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
|
||||
import com.intellij.settingsSync.migration.migrateIfNeeded
|
||||
import com.intellij.settingsSync.statistics.SettingsSyncEventsStatistics
|
||||
import com.intellij.util.concurrency.AppExecutorUtil
|
||||
@@ -50,6 +51,9 @@ private class SettingsSynchronizerApplicationInitializedListener : ApplicationAc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setProviderCodeAndUserId()
|
||||
|
||||
serviceAsync<SettingsSyncEvents>().addListener(settingsSyncEventListener)
|
||||
|
||||
if (isSettingsSyncEnabledInSettings()) {
|
||||
@@ -75,17 +79,28 @@ private class SettingsSynchronizerApplicationInitializedListener : ApplicationAc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initializeSyncing(initMode: SettingsSyncBridge.InitMode, settingsSyncEventListener: SettingsSyncEventListener) {
|
||||
LOG.info("Initializing settings sync. Mode: $initMode")
|
||||
val settingsSyncMain = serviceAsync<SettingsSyncMain>()
|
||||
blockingContext {
|
||||
settingsSyncMain.controls.bridge.initialize(initMode)
|
||||
val settingsSyncEvents = SettingsSyncEvents.getInstance()
|
||||
settingsSyncEvents.addListener(settingsSyncEventListener)
|
||||
settingsSyncEvents.fireSettingsChanged(SyncSettingsEvent.SyncRequest)
|
||||
LocalHostNameProvider.initialize()
|
||||
private suspend fun initializeSyncing(initMode: SettingsSyncBridge.InitMode, settingsSyncEventListener: SettingsSyncEventListener) {
|
||||
LOG.info("Initializing settings sync. Mode: $initMode")
|
||||
val settingsSyncMain = serviceAsync<SettingsSyncMain>()
|
||||
blockingContext {
|
||||
settingsSyncMain.controls.bridge.initialize(initMode)
|
||||
val settingsSyncEvents = SettingsSyncEvents.getInstance()
|
||||
settingsSyncEvents.addListener(settingsSyncEventListener)
|
||||
settingsSyncEvents.fireSettingsChanged(SyncSettingsEvent.SyncRequest)
|
||||
LocalHostNameProvider.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setProviderCodeAndUserId() {
|
||||
if (!SettingsSyncSettings.getInstance().syncEnabled)
|
||||
return
|
||||
if (SettingsSyncLocalSettings.getInstance().providerCode.isNullOrBlank()) {
|
||||
SettingsSyncLocalSettings.getInstance().providerCode = RemoteCommunicatorHolder.DEFAULT_PROVIDER_CODE
|
||||
}
|
||||
if (SettingsSyncLocalSettings.getInstance().userId.isNullOrBlank()) {
|
||||
SettingsSyncLocalSettings.getInstance().userId = RemoteCommunicatorHolder.DEFAULT_USER_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-9
@@ -1,6 +1,9 @@
|
||||
package com.intellij.settingsSync.auth
|
||||
|
||||
import com.intellij.settingsSync.communicator.SettingsSyncUserData
|
||||
import kotlinx.coroutines.Deferred
|
||||
import java.awt.Component
|
||||
import javax.swing.Icon
|
||||
|
||||
interface SettingsSyncAuthService {
|
||||
/**
|
||||
@@ -10,22 +13,25 @@ interface SettingsSyncAuthService {
|
||||
val providerCode: String
|
||||
|
||||
/**
|
||||
* Starts the login procedure
|
||||
* Free-form name of the provider
|
||||
*/
|
||||
fun login()
|
||||
val providerName: String
|
||||
|
||||
/**
|
||||
* Whether the user has logged in
|
||||
* provider icon
|
||||
*/
|
||||
fun isLoggedIn(): Boolean
|
||||
val icon: Icon?
|
||||
|
||||
/**
|
||||
* Starts the login procedure (if necessary) and returns the Deferred of the logged-in user
|
||||
*/
|
||||
suspend fun login(parentComponent: Component?) : SettingsSyncUserData?
|
||||
|
||||
/**
|
||||
* Data of the current user. If there's no user, return null
|
||||
* This data is used for in the local git repo as well as UI (if necessary)
|
||||
*/
|
||||
fun getUserData(): SettingsSyncUserData
|
||||
fun getUserData(userId: String): SettingsSyncUserData?
|
||||
|
||||
/**
|
||||
* Indicates if it's currently possible to log in (i.e. all necessary plugins are present and enabled, etc), given the current state of IDE.
|
||||
*/
|
||||
fun isLoginAvailable(): Boolean
|
||||
fun getAvailableUserAccounts(): List<SettingsSyncUserData>
|
||||
}
|
||||
+68
-73
@@ -1,95 +1,90 @@
|
||||
package com.intellij.settingsSync.communicator
|
||||
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.settingsSync.ServerState
|
||||
import com.intellij.settingsSync.SettingsSnapshot
|
||||
import com.intellij.settingsSync.SettingsSyncPushResult
|
||||
import com.intellij.settingsSync.SettingsSyncRemoteCommunicator
|
||||
import com.intellij.settingsSync.UpdateResult
|
||||
import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.util.concurrency.SynchronizedClearableLazy
|
||||
import com.intellij.settingsSync.*
|
||||
import com.intellij.util.resettableLazy
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
object RemoteCommunicatorHolder {
|
||||
@ApiStatus.Internal
|
||||
object RemoteCommunicatorHolder : SettingsSyncEventListener {
|
||||
private val logger = logger<RemoteCommunicatorHolder>()
|
||||
const val DEFAULT_PROVIDER_CODE = "jba"
|
||||
const val DEFAULT_USER_ID = "jba"
|
||||
|
||||
private val communicatorInternalLazy = SynchronizedClearableLazy<SettingsSyncRemoteCommunicator> {
|
||||
getCurrentCommunicator().also {
|
||||
logger<RemoteCommunicatorHolder>().warn("Initializing remote communicator: $it")
|
||||
// pair userId:remoteCommunicator
|
||||
private val communicatorLazy = resettableLazy {
|
||||
createRemoteCommunicator()
|
||||
}
|
||||
|
||||
fun getRemoteCommunicator(): SettingsSyncRemoteCommunicator? = communicatorLazy.value ?: createRemoteCommunicator()
|
||||
|
||||
fun getAuthService() = getCurrentProvider()?.authService
|
||||
|
||||
fun isAvailable() = communicatorLazy.value != null
|
||||
|
||||
fun invalidateCommunicator() = communicatorLazy.reset().also {
|
||||
logger.warn("Invalidating remote communicator")
|
||||
}
|
||||
|
||||
fun getCurrentUserData() : SettingsSyncUserData? {
|
||||
val userId = SettingsSyncLocalSettings.getInstance().userId ?: run {
|
||||
logger.warn("No current userId. Returning null user data")
|
||||
return null
|
||||
}
|
||||
val provider: SettingsSyncCommunicatorProvider = getCurrentProvider() ?: run {
|
||||
logger.warn("No active provider. Returning null user data")
|
||||
return null
|
||||
}
|
||||
return provider.authService.getUserData(userId)
|
||||
}
|
||||
|
||||
fun getRemoteCommunicator(): SettingsSyncRemoteCommunicator = communicatorInternalLazy.value
|
||||
fun getAuthService() = getProvider().authService
|
||||
fun isAvailable() = communicatorInternalLazy.isInitialized()
|
||||
fun invalidateCommunicator() = communicatorInternalLazy.drop().also {
|
||||
logger<RemoteCommunicatorHolder>().warn("Invalidating remote communicator: $it")
|
||||
override fun loginStateChanged() {
|
||||
}
|
||||
|
||||
fun createRemoteCommunicator(provider: SettingsSyncCommunicatorProvider, userId: String): SettingsSyncRemoteCommunicator? {
|
||||
return provider.createCommunicator(userId)
|
||||
}
|
||||
|
||||
private fun getProvider() : SettingsSyncCommunicatorProvider {
|
||||
private fun createRemoteCommunicator(): SettingsSyncRemoteCommunicator? {
|
||||
val provider: SettingsSyncCommunicatorProvider = getCurrentProvider() ?: run {
|
||||
logger.warn("Attempting to create remote communicator without active provider")
|
||||
return null
|
||||
}
|
||||
val userId = SettingsSyncLocalSettings.getInstance().userId ?: run {
|
||||
logger.warn("Empty current userId. Communicator will not be created.")
|
||||
return null
|
||||
}
|
||||
|
||||
val currentUserData = provider.authService.getUserData(userId) ?: run {
|
||||
logger.warn("Empty current user data. Communicator will not be created.")
|
||||
return null
|
||||
}
|
||||
val communicator: SettingsSyncRemoteCommunicator = provider.createCommunicator(currentUserData.id) ?: run {
|
||||
logger.warn("Provider '${provider.providerCode}' returned empty communicator")
|
||||
return null
|
||||
}
|
||||
return communicator
|
||||
}
|
||||
|
||||
fun getAvailableProviders(): List<SettingsSyncCommunicatorProvider> {
|
||||
val extensionList = SettingsSyncCommunicatorProvider.PROVIDER_EP.extensionList
|
||||
return extensionList.firstOrNull() ?: DummyProvider
|
||||
return extensionList
|
||||
}
|
||||
|
||||
private fun getCurrentCommunicator() : SettingsSyncRemoteCommunicator {
|
||||
return getProvider().createCommunicator() ?: DummyCommunicator
|
||||
fun getDefaultProvider(): SettingsSyncCommunicatorProvider {
|
||||
return getProvider(DEFAULT_PROVIDER_CODE)!!
|
||||
}
|
||||
|
||||
internal object DummyProvider: SettingsSyncCommunicatorProvider {
|
||||
override val providerCode: String
|
||||
get() = "dummy"
|
||||
override val authService: SettingsSyncAuthService
|
||||
get() = DummyAuthProvider
|
||||
|
||||
override fun createCommunicator(): SettingsSyncRemoteCommunicator? {
|
||||
return DummyCommunicator
|
||||
}
|
||||
|
||||
fun getProvider(providerCode: String): SettingsSyncCommunicatorProvider? {
|
||||
return getAvailableProviders().find { it.providerCode == providerCode }
|
||||
}
|
||||
|
||||
internal object DummyAuthProvider: SettingsSyncAuthService {
|
||||
override val providerCode: String
|
||||
get() = "dummy"
|
||||
|
||||
override fun login() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isLoggedIn(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getUserData(): SettingsSyncUserData {
|
||||
return SettingsSyncUserData.EMPTY
|
||||
}
|
||||
|
||||
override fun isLoginAvailable(): Boolean {
|
||||
return true
|
||||
}
|
||||
fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
|
||||
return getAvailableProviders().flatMap { it.authService.getAvailableUserAccounts() }
|
||||
}
|
||||
|
||||
internal object DummyCommunicator: SettingsSyncRemoteCommunicator {
|
||||
override fun checkServerState(): ServerState {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun receiveUpdates(): UpdateResult {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun push(snapshot: SettingsSnapshot, force: Boolean, expectedServerVersionId: String?): SettingsSyncPushResult {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun createFile(filePath: String, content: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun deleteFile(filePath: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isFileExists(filePath: String): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
private fun getCurrentProvider(): SettingsSyncCommunicatorProvider? {
|
||||
val providerCode = SettingsSyncLocalSettings.getInstance().providerCode ?: return null
|
||||
return getProvider(providerCode)
|
||||
}
|
||||
}
|
||||
+7
-8
@@ -19,7 +19,7 @@ interface SettingsSyncCommunicatorProvider {
|
||||
/**
|
||||
* Creates a communicator (using the login data from authService)
|
||||
*/
|
||||
fun createCommunicator(): SettingsSyncRemoteCommunicator?
|
||||
fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator?
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
@@ -28,10 +28,9 @@ interface SettingsSyncCommunicatorProvider {
|
||||
}
|
||||
|
||||
data class SettingsSyncUserData(
|
||||
val name: String?,
|
||||
val email: String?,
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = SettingsSyncUserData(null, null)
|
||||
}
|
||||
}
|
||||
val id: String,
|
||||
val providerCode: String,
|
||||
val name: String? = null,
|
||||
val email: String? = null,
|
||||
val printableName: String? = null
|
||||
)
|
||||
+501
-376
@@ -1,451 +1,576 @@
|
||||
package com.intellij.settingsSync.config
|
||||
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.ide.plugins.InstalledPluginsState
|
||||
import com.intellij.ide.plugins.PluginManagerConfigurable
|
||||
import com.intellij.ide.plugins.PluginStateManager
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ApplicationNamesInfo
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.ex.ApplicationEx
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.components.impl.stores.stateStore
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.application.*
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
|
||||
import com.intellij.openapi.observable.properties.AtomicProperty
|
||||
import com.intellij.openapi.observable.util.and
|
||||
import com.intellij.openapi.observable.util.not
|
||||
import com.intellij.openapi.options.BoundConfigurable
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.options.ConfigurableProvider
|
||||
import com.intellij.openapi.options.ex.Settings
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.ui.*
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.platform.ide.progress.withModalProgress
|
||||
import com.intellij.settingsSync.*
|
||||
import com.intellij.settingsSync.SettingsSyncBundle.message
|
||||
import com.intellij.settingsSync.UpdateResult.*
|
||||
import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
|
||||
//import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.settingsSync.communicator.SettingsSyncCommunicatorProvider
|
||||
import com.intellij.settingsSync.communicator.SettingsSyncUserData
|
||||
import com.intellij.settingsSync.config.SettingsSyncEnabler.State
|
||||
import com.intellij.settingsSync.statistics.SettingsSyncEventsStatistics
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.dsl.builder.BottomGap
|
||||
import com.intellij.ui.dsl.builder.Cell
|
||||
import com.intellij.ui.dsl.builder.RightGap
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.ui.layout.ComponentPredicate
|
||||
import com.intellij.ui.layout.and
|
||||
import com.intellij.ui.layout.not
|
||||
import com.intellij.ui.components.DropDownLink
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.ui.dsl.listCellRenderer.groupedTextListCellRenderer
|
||||
import com.intellij.util.text.DateFormatUtil
|
||||
import org.jetbrains.annotations.Nls
|
||||
import com.intellij.util.ui.JBUI
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.event.ItemEvent
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JCheckBox
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.*
|
||||
|
||||
internal class SettingsSyncConfigurable : BoundConfigurable(message("title.settings.sync")),
|
||||
SettingsSyncEnabler.Listener,
|
||||
SettingsSyncStatusTracker.Listener {
|
||||
internal class SettingsSyncConfigurable(private val coroutineScope: CoroutineScope) : BoundConfigurable(message("title.settings.sync")),
|
||||
SettingsSyncEnabler.Listener,
|
||||
SettingsSyncStatusTracker.Listener {
|
||||
companion object {
|
||||
private val LOG = logger<SettingsSyncConfigurable>()
|
||||
}
|
||||
|
||||
private lateinit var configPanel: DialogPanel
|
||||
private lateinit var enableButton: Cell<JButton>
|
||||
private lateinit var enableButton: JButton
|
||||
private lateinit var statusLabel: JLabel
|
||||
|
||||
@Volatile
|
||||
private var marketplacePluginInstalled = false
|
||||
private lateinit var userDropDownLink: DropDownLink<UserProviderHolder?>
|
||||
private lateinit var syncTypeLabel: JEditorPane
|
||||
|
||||
private val syncEnabler = SettingsSyncEnabler()
|
||||
private val MARKETPLACE_PLUGIN_ID = PluginId.getId("com.intellij.marketplace")
|
||||
|
||||
private val enabledStatus = AtomicBooleanProperty(false)
|
||||
private val enableSyncOption = AtomicProperty<InitSyncType>(InitSyncType.GET_FROM_SERVER)
|
||||
private val disableSyncOption = AtomicProperty<Int>(DisableSyncType.DISABLE)
|
||||
private val remoteSettingsExist = AtomicBooleanProperty(false)
|
||||
private val wasUsedBefore = AtomicBooleanProperty(SettingsSyncLocalSettings.getInstance().userId != null)
|
||||
private val userAccountsList = arrayListOf<UserProviderHolder>()
|
||||
private val syncPanelHolder = SettingsSyncPanelHolder()
|
||||
private val hasMultipleProviders = AtomicBooleanProperty(RemoteCommunicatorHolder.getAvailableProviders().size > 1)
|
||||
|
||||
init {
|
||||
syncEnabler.addListener(this)
|
||||
SettingsSyncStatusTracker.getInstance().addListener(this)
|
||||
}
|
||||
|
||||
inner class LoggedInPredicate : ComponentPredicate() {
|
||||
override fun addListener(listener: (Boolean) -> Unit) =
|
||||
SettingsSyncEvents.getInstance().addListener(
|
||||
object : SettingsSyncEventListener {
|
||||
override fun loginStateChanged() {
|
||||
listener(invoke())
|
||||
}
|
||||
},
|
||||
disposable!!)
|
||||
|
||||
override fun invoke() = RemoteCommunicatorHolder.getAuthService().isLoggedIn()
|
||||
}
|
||||
|
||||
inner class EnabledPredicate : ComponentPredicate() {
|
||||
override fun addListener(listener: (Boolean) -> Unit) {
|
||||
SettingsSyncEvents.getInstance().addListener(object : SettingsSyncEventListener {
|
||||
override fun enabledStateChanged(syncEnabled: Boolean) {
|
||||
listener(invoke())
|
||||
configPanel.reset()
|
||||
}
|
||||
}, disposable!!)
|
||||
}
|
||||
|
||||
override fun invoke() = SettingsSyncSettings.getInstance().syncEnabled
|
||||
|
||||
}
|
||||
|
||||
inner class SyncEnablerRunning : ComponentPredicate() {
|
||||
private var isRunning = false
|
||||
|
||||
override fun addListener(listener: (Boolean) -> Unit) {
|
||||
syncEnabler.addListener(object : SettingsSyncEnabler.Listener {
|
||||
override fun serverRequestStarted() {
|
||||
updateRunning(listener, true)
|
||||
}
|
||||
|
||||
override fun serverRequestFinished() {
|
||||
updateRunning(listener, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateRunning(listener: (Boolean) -> Unit, isRunning: Boolean) {
|
||||
this.isRunning = isRunning
|
||||
listener(invoke())
|
||||
}
|
||||
|
||||
override fun invoke(): Boolean = isRunning
|
||||
}
|
||||
|
||||
inner class AuthServiceRestartPredicate : ComponentPredicate() {
|
||||
init {
|
||||
marketplacePluginInstalled = InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID)
|
||||
}
|
||||
|
||||
override fun addListener(listener: (Boolean) -> Unit) {
|
||||
PluginStateManager.addStateListener { descriptor ->
|
||||
if (descriptor.pluginId == MARKETPLACE_PLUGIN_ID) {
|
||||
// InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID) is still false at that time,
|
||||
// so we just cache the value
|
||||
marketplacePluginInstalled = true
|
||||
listener(marketplacePluginInstalled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun invoke(): Boolean {
|
||||
return marketplacePluginInstalled
|
||||
}
|
||||
}
|
||||
|
||||
override fun createPanel(): DialogPanel {
|
||||
val syncConfigPanel = SettingsSyncPanelFactory.createCombinedSyncSettingsPanel(
|
||||
message("configurable.what.to.sync.label"),
|
||||
SettingsSyncSettings.getInstance(),
|
||||
SettingsSyncLocalSettings.getInstance(),
|
||||
)
|
||||
val authService = RemoteCommunicatorHolder.getAuthService()
|
||||
val authAvailable = authService.isLoginAvailable()
|
||||
val syncConfigPanel = syncPanelHolder.createCombinedSyncSettingsPanel(message("configurable.what.to.sync.label"),
|
||||
SettingsSyncSettings.getInstance(),
|
||||
SettingsSyncLocalSettings.getInstance())
|
||||
|
||||
configPanel = panel {
|
||||
val isSyncEnabled = LoggedInPredicate().and(EnabledPredicate())
|
||||
if (settingsRepositoryIsEnabled()) {
|
||||
row {
|
||||
label(message("settings.warning.sync.cannot.be.enabled.label")).applyToComponent {
|
||||
icon = AllIcons.General.Warning
|
||||
enabledStatus.set(SettingsSyncSettings.getInstance().syncEnabled)
|
||||
var userProviderHolder: UserProviderHolder? = null
|
||||
if (SettingsSyncLocalSettings.getInstance().userId != null && SettingsSyncLocalSettings.getInstance().providerCode != null) {
|
||||
val authService = RemoteCommunicatorHolder.getProvider(SettingsSyncLocalSettings.getInstance().providerCode!!)?.authService
|
||||
if (authService != null) {
|
||||
authService.getAvailableUserAccounts().find {
|
||||
it.id == SettingsSyncLocalSettings.getInstance().userId
|
||||
}?.apply {
|
||||
userProviderHolder = toUserProviderHolder(authService.providerName)
|
||||
}
|
||||
bottomGap(BottomGap.MEDIUM)
|
||||
}
|
||||
}
|
||||
|
||||
// authService is not available without restart
|
||||
if (authAvailable) {
|
||||
row {
|
||||
val statusCell = label("")
|
||||
statusCell
|
||||
.visibleIf(LoggedInPredicate())
|
||||
.enabled(!settingsRepositoryIsEnabled())
|
||||
statusLabel = statusCell.component
|
||||
updateStatusInfo()
|
||||
label(message("sync.status.login.message"))
|
||||
.visibleIf(LoggedInPredicate().not())
|
||||
.enabled(!settingsRepositoryIsEnabled())
|
||||
}
|
||||
row {
|
||||
button(message("config.button.login")) {
|
||||
authService.login()
|
||||
}.visibleIf(LoggedInPredicate().not())
|
||||
.enabled(!settingsRepositoryIsEnabled())
|
||||
enableButton = button(message("config.button.enable")) {
|
||||
syncEnabler.checkServerState()
|
||||
}.visibleIf(LoggedInPredicate().and(EnabledPredicate().not()))
|
||||
.enabledIf(SyncEnablerRunning().not())
|
||||
.enabled(!settingsRepositoryIsEnabled())
|
||||
|
||||
button(message("config.button.disable")) {
|
||||
LoggedInPredicate().and(EnabledPredicate())
|
||||
disableSync()
|
||||
}.visibleIf(isSyncEnabled)
|
||||
bottomGap(BottomGap.MEDIUM)
|
||||
}
|
||||
updateUserAccountsList()
|
||||
enabledStatus.afterChange {
|
||||
syncStatusChanged()
|
||||
}
|
||||
else {
|
||||
val authServiceRestartPredicate = AuthServiceRestartPredicate()
|
||||
row {
|
||||
label(message("sync.status.login.not.available")).gap(RightGap.SMALL)
|
||||
@Suppress("DialogTitleCapitalization", "HardCodedStringLiteral")
|
||||
link("JetBrains Marketplace Licensing Support") {
|
||||
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(it.source as ActionLink))
|
||||
val pluginManager = settings?.find("preferences.pluginManager")
|
||||
if (pluginManager is PluginManagerConfigurable) {
|
||||
settings.select(pluginManager).doWhenDone {
|
||||
pluginManager.openMarketplaceTab("/organization:JetBrains Marketplace Licensing")
|
||||
|
||||
row {
|
||||
label(message("settings.sync.info.message"))
|
||||
}.visibleIf(enabledStatus.not())
|
||||
|
||||
row {
|
||||
label(message("settings.sync.select.provider.message"))
|
||||
}.visibleIf(enabledStatus.not())
|
||||
|
||||
row {
|
||||
val availableProviders = RemoteCommunicatorHolder.getAvailableProviders()
|
||||
availableProviders.forEach { provider ->
|
||||
button(provider.authService.providerName) {
|
||||
login(provider, syncConfigPanel)
|
||||
}.applyToComponent {
|
||||
icon = provider.authService.icon
|
||||
}
|
||||
}
|
||||
}.visibleIf(wasUsedBefore.not().and(hasMultipleProviders))
|
||||
|
||||
row {
|
||||
val defaultProvider = RemoteCommunicatorHolder.getDefaultProvider()
|
||||
button(message("config.button.login")) {
|
||||
login(defaultProvider, syncConfigPanel)
|
||||
}
|
||||
}.visibleIf(wasUsedBefore.not().and(hasMultipleProviders.not()))
|
||||
|
||||
row {
|
||||
val label = label("").applyToComponent {
|
||||
iconTextGap = 6
|
||||
}.gap(RightGap.SMALL)
|
||||
statusLabel = label.component
|
||||
cell(object: DropDownLink<UserProviderHolder?>(userProviderHolder, userAccountsList) {
|
||||
override fun createRenderer(): ListCellRenderer<in UserProviderHolder?> {
|
||||
return groupedTextListCellRenderer({
|
||||
if (it == UserProviderHolder.addAccount) {
|
||||
message("enable.sync.add.account")
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
|
||||
}, {
|
||||
it?.separatorString
|
||||
})
|
||||
}
|
||||
}).onChangedContext { component, context ->
|
||||
val event = context.event
|
||||
if (event is ItemEvent && event.item == UserProviderHolder.addAccount) {
|
||||
val syncTypeDialog = AddAccountDialog(configPanel)
|
||||
if (syncTypeDialog.showAndGet()) {
|
||||
val providerCode = syncTypeDialog.providerCode
|
||||
val provider = RemoteCommunicatorHolder.getProvider(providerCode) ?: return@onChangedContext
|
||||
component.selectedItem = null
|
||||
component.text = ""
|
||||
login(provider, syncConfigPanel)
|
||||
}
|
||||
} else {
|
||||
component.text = component.selectedItem.toString()
|
||||
}
|
||||
}.apply {
|
||||
userDropDownLink = this.component
|
||||
}
|
||||
|
||||
}.visibleIf(wasUsedBefore)
|
||||
|
||||
row {
|
||||
val enableButtonCell = button(message("config.button.enable")) {
|
||||
if (!enabledStatus.get()) {
|
||||
runWithModalProgressBlocking(ModalTaskOwner.component(configPanel), message("enable.sync.check.server.data.progress")) {
|
||||
val (userId, userData, providerCode, providerName) = userDropDownLink.selectedItem ?: run {
|
||||
LOG.warn("No selected user")
|
||||
return@runWithModalProgressBlocking
|
||||
}
|
||||
val provider = RemoteCommunicatorHolder.getProvider(providerCode) ?: run {
|
||||
LOG.warn("Provider '$providerName' ($providerCode) is not available")
|
||||
return@runWithModalProgressBlocking
|
||||
}
|
||||
val remoteCommunicator = RemoteCommunicatorHolder.createRemoteCommunicator(provider, userId) ?: run {
|
||||
LOG.warn("Cannot create remote communicator of type '$providerName' ($providerCode)")
|
||||
return@runWithModalProgressBlocking
|
||||
}
|
||||
if (checkServerState(syncPanelHolder, remoteCommunicator)) {
|
||||
enabledStatus.set(true)
|
||||
syncStatusChanged()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val syncDisableOption = showDisableSyncDialog()
|
||||
if (syncDisableOption != DisableSyncType.DONT_DISABLE) {
|
||||
enabledStatus.set(false)
|
||||
disableSyncOption.set(syncDisableOption)
|
||||
syncStatusChanged()
|
||||
}
|
||||
}
|
||||
}.visibleIf(authServiceRestartPredicate.not())
|
||||
row {
|
||||
label(message("sync.status.restart.required", ApplicationNamesInfo.getInstance().fullProductName))
|
||||
}.visibleIf(authServiceRestartPredicate)
|
||||
row {
|
||||
button(message("sync.status.restart.ide.button")) {
|
||||
val app = ApplicationManager.getApplication() as ApplicationEx
|
||||
app.restart(true)
|
||||
}
|
||||
}.visibleIf(authServiceRestartPredicate)
|
||||
}
|
||||
row {
|
||||
comment(message("settings.sync.info.message"), 80)
|
||||
.visibleIf(isSyncEnabled.not())
|
||||
}
|
||||
|
||||
row {
|
||||
cell(syncConfigPanel)
|
||||
.visibleIf(LoggedInPredicate().and(EnabledPredicate()))
|
||||
.onApply {
|
||||
syncConfigPanel.apply()
|
||||
|
||||
SettingsSyncEvents.getInstance().fireCategoriesChanged()
|
||||
SettingsSyncEvents.getInstance().fireSettingsChanged(
|
||||
SyncSettingsEvent.CrossIdeSyncStateChanged(SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled))
|
||||
}
|
||||
.onReset(syncConfigPanel::reset)
|
||||
.onIsModified(syncConfigPanel::isModified)
|
||||
}
|
||||
}
|
||||
SettingsSyncEvents.getInstance().addListener(
|
||||
object : SettingsSyncEventListener {
|
||||
override fun loginStateChanged() {
|
||||
if (RemoteCommunicatorHolder.getAuthService().isLoggedIn() && !SettingsSyncSettings.getInstance().syncEnabled) {
|
||||
syncEnabler.checkServerState()
|
||||
}
|
||||
reset()
|
||||
}
|
||||
},
|
||||
disposable!!
|
||||
)
|
||||
enableButton = enableButtonCell.component
|
||||
}.visibleIf(wasUsedBefore)
|
||||
|
||||
|
||||
// settings to sync
|
||||
group(message("enable.dialog.select.what.to.sync")) {
|
||||
row {
|
||||
icon(AllIcons.General.BalloonWarning).applyToComponent {
|
||||
isOpaque = true
|
||||
background = JBUI.CurrentTheme.Banner.WARNING_BACKGROUND
|
||||
border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(JBUI.CurrentTheme.Banner.WARNING_BORDER_COLOR, 1, 1, 1, 0),
|
||||
JBUI.Borders.empty(8)
|
||||
)
|
||||
verticalAlignment = SwingConstants.TOP
|
||||
}.align(AlignY.FILL)
|
||||
text("",
|
||||
action = {
|
||||
val syncTypeDialog = ChangeSyncTypeDialog(configPanel, enableSyncOption.get())
|
||||
if (syncTypeDialog.showAndGet()) {
|
||||
enableSyncOption.set(syncTypeDialog.option)
|
||||
}
|
||||
}).applyToComponent {
|
||||
isOpaque = true
|
||||
background = JBUI.CurrentTheme.Banner.WARNING_BACKGROUND
|
||||
border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(JBUI.CurrentTheme.Banner.WARNING_BORDER_COLOR, 1, 0, 1, 1),
|
||||
JBUI.Borders.empty(8)
|
||||
)
|
||||
}.align(AlignX.FILL).resizableColumn().also {
|
||||
syncTypeLabel = it.component
|
||||
enableSyncOption.afterChange {
|
||||
updateSyncOptionText()
|
||||
}
|
||||
}
|
||||
cell()
|
||||
}.layout(RowLayout.PARENT_GRID).topGap(TopGap.SMALL)
|
||||
.visibleIf(remoteSettingsExist)
|
||||
|
||||
row {
|
||||
cell(syncConfigPanel)
|
||||
.onReset(syncConfigPanel::reset)
|
||||
.onIsModified{
|
||||
enabledStatus.get() != SettingsSyncSettings.getInstance().syncEnabled || syncConfigPanel.isModified()
|
||||
}
|
||||
.onApply {
|
||||
with(SettingsSyncLocalSettings.getInstance()) {
|
||||
userId = userDropDownLink.selectedItem?.userId
|
||||
providerCode = userDropDownLink.selectedItem?.providerCode
|
||||
}
|
||||
if (enabledStatus.get()) {
|
||||
syncConfigPanel.apply()
|
||||
}
|
||||
if (SettingsSyncSettings.getInstance().syncEnabled != enabledStatus.get()) {
|
||||
if (enabledStatus.get()) {
|
||||
SettingsSyncSettings.getInstance().syncEnabled = enabledStatus.get()
|
||||
if (enableSyncOption.get() == InitSyncType.GET_FROM_SERVER) {
|
||||
syncEnabler.getSettingsFromServer()
|
||||
}
|
||||
else {
|
||||
syncEnabler.pushSettingsToServer()
|
||||
}
|
||||
} else {
|
||||
when (disableSyncOption.get()) {
|
||||
DisableSyncType.DISABLE_AND_REMOVE_DATA -> {
|
||||
disableAndRemoveData()
|
||||
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(
|
||||
SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_AND_REMOVED_DATA_FROM_SERVER)
|
||||
}
|
||||
DisableSyncType.DISABLE -> {
|
||||
SettingsSyncSettings.getInstance().syncEnabled = false
|
||||
syncStatusChanged()
|
||||
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY)
|
||||
}
|
||||
else -> {
|
||||
SettingsSyncSettings.getInstance().syncEnabled = false
|
||||
syncStatusChanged()
|
||||
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY)
|
||||
}
|
||||
}
|
||||
SettingsSyncSettings.getInstance().syncEnabled = enabledStatus.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.topGap(TopGap.SMALL)
|
||||
}.visibleIf(enabledStatus)
|
||||
|
||||
// apply necessary changes
|
||||
}
|
||||
syncStatusChanged()
|
||||
return configPanel
|
||||
}
|
||||
|
||||
private fun settingsRepositoryIsEnabled(): Boolean {
|
||||
return !SettingsSyncSettings.getInstance().syncEnabled &&
|
||||
(ApplicationManager.getApplication().stateStore.storageManager).streamProvider.let { it.enabled && it.isExclusive }
|
||||
}
|
||||
|
||||
override fun serverStateCheckFinished(state: UpdateResult) {
|
||||
when (state) {
|
||||
NoFileOnServer, FileDeletedFromServer -> showEnableSyncDialog(null, null)
|
||||
is Success -> showEnableSyncDialog(
|
||||
state.settingsSnapshot.getState(),
|
||||
SettingsSyncLocalStateHolder(state.isCrossIdeSyncEnabled),
|
||||
)
|
||||
is Error -> {
|
||||
if (state != SettingsSyncEnabler.State.CANCELLED) {
|
||||
showError(message("notification.title.update.error"), state.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateFromServerFinished(result: UpdateResult) {
|
||||
when (result) {
|
||||
is Success -> {
|
||||
reset()
|
||||
SettingsSyncSettings.getInstance().syncEnabled = true
|
||||
}
|
||||
NoFileOnServer, FileDeletedFromServer -> {
|
||||
showError(message("notification.title.update.error"), message("notification.title.update.no.such.file"))
|
||||
}
|
||||
is Error -> {
|
||||
showError(message("notification.title.update.error"), result.message)
|
||||
}
|
||||
}
|
||||
updateStatusInfo()
|
||||
}
|
||||
|
||||
private fun showEnableSyncDialog(remoteSettings: SettingsSyncState?, remoteSyncScopeSettings: SettingsSyncLocalStateHolder?) {
|
||||
val dialog = EnableSettingsSyncDialog(configPanel, remoteSettings, remoteSyncScopeSettings)
|
||||
|
||||
dialog.show()
|
||||
|
||||
val dialogResult = dialog.getResult()
|
||||
|
||||
if (dialogResult != null) {
|
||||
when (dialogResult) {
|
||||
EnableSettingsSyncDialog.Result.GET_FROM_SERVER -> {
|
||||
syncEnabler.getSettingsFromServer(dialog.syncSettings)
|
||||
|
||||
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.GET_FROM_SERVER)
|
||||
}
|
||||
|
||||
EnableSettingsSyncDialog.Result.PUSH_LOCAL -> {
|
||||
SettingsSyncSettings.getInstance().syncEnabled = true
|
||||
|
||||
syncEnabler.pushSettingsToServer()
|
||||
|
||||
if (remoteSettings != null) {
|
||||
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL)
|
||||
}
|
||||
else {
|
||||
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL_WAS_ONLY_WAY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.CANCELED)
|
||||
}
|
||||
|
||||
reset()
|
||||
configPanel.reset()
|
||||
}
|
||||
|
||||
companion object DisableResult {
|
||||
const val RESULT_CANCEL = 0
|
||||
const val RESULT_REMOVE_DATA_AND_DISABLE = 1
|
||||
const val RESULT_DISABLE = 2
|
||||
}
|
||||
|
||||
private fun disableSync() {
|
||||
private fun showDisableSyncDialog(): Int {
|
||||
@Suppress("DialogTitleCapitalization")
|
||||
val result = Messages.showCheckboxMessageDialog( // TODO<rv>: Use AlertMessage instead
|
||||
message("disable.dialog.text"),
|
||||
val providerName = userDropDownLink.selectedItem?.providerName ?: ""
|
||||
return Messages.showCheckboxMessageDialog( // TODO<rv>: Use AlertMessage instead
|
||||
message("disable.dialog.text", providerName),
|
||||
message("disable.dialog.title"),
|
||||
arrayOf(Messages.getCancelButton(), message("disable.dialog.disable.button")),
|
||||
message("disable.dialog.remove.data.box"),
|
||||
message("disable.dialog.remove.data.box", providerName),
|
||||
false,
|
||||
1,
|
||||
1,
|
||||
Messages.getInformationIcon()
|
||||
) { index: Int, checkbox: JCheckBox ->
|
||||
if (index == 1) {
|
||||
if (checkbox.isSelected) RESULT_REMOVE_DATA_AND_DISABLE else RESULT_DISABLE
|
||||
if (checkbox.isSelected) DisableSyncType.DISABLE_AND_REMOVE_DATA else DisableSyncType.DISABLE
|
||||
}
|
||||
else {
|
||||
RESULT_CANCEL
|
||||
}
|
||||
}
|
||||
|
||||
when (result) {
|
||||
RESULT_DISABLE -> {
|
||||
SettingsSyncSettings.getInstance().syncEnabled = false
|
||||
updateStatusInfo()
|
||||
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY)
|
||||
}
|
||||
RESULT_REMOVE_DATA_AND_DISABLE -> {
|
||||
disableAndRemoveData()
|
||||
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(
|
||||
SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_AND_REMOVED_DATA_FROM_SERVER)
|
||||
}
|
||||
RESULT_CANCEL -> {
|
||||
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.CANCEL)
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableAndRemoveData() {
|
||||
val modality = ModalityState.current();
|
||||
runWithModalProgressBlocking(ModalTaskOwner.component(configPanel), message("disable.remove.data.title"), TaskCancellation.cancellable()) {
|
||||
val cdl = CountDownLatch(1)
|
||||
SettingsSyncEvents.getInstance().fireSettingsChanged(SyncSettingsEvent.DeleteServerData { result ->
|
||||
cdl.countDown()
|
||||
|
||||
object : Task.Modal(null, message("disable.remove.data.title"), false) {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
val cdl = CountDownLatch(1)
|
||||
SettingsSyncEvents.getInstance().fireSettingsChanged(SyncSettingsEvent.DeleteServerData { result ->
|
||||
cdl.countDown()
|
||||
|
||||
when (result) {
|
||||
is DeleteServerDataResult.Error -> {
|
||||
runInEdt {
|
||||
showError(message("disable.remove.data.failure"), result.error)
|
||||
}
|
||||
}
|
||||
DeleteServerDataResult.Success -> {
|
||||
runInEdt(modality) {
|
||||
updateStatusInfo()
|
||||
}
|
||||
}
|
||||
when (result) {
|
||||
is DeleteServerDataResult.Error -> {
|
||||
val messageBuilder = StringBuilder()
|
||||
messageBuilder.append(message("sync.status.failed"))
|
||||
statusLabel.icon = AllIcons.General.Error
|
||||
messageBuilder.append(' ').append("${message("disable.remove.data.failure")}: ${result.error}")
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
statusLabel.text = messageBuilder.toString()
|
||||
}
|
||||
})
|
||||
|
||||
cdl.await(1, TimeUnit.MINUTES)
|
||||
}
|
||||
}.queue()
|
||||
}
|
||||
|
||||
private fun showError(message: @Nls String, details: @Nls String) {
|
||||
val messageBuilder = StringBuilder()
|
||||
messageBuilder.append(message("sync.status.failed"))
|
||||
statusLabel.icon = AllIcons.General.Error
|
||||
messageBuilder.append(' ').append("$message: $details")
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
statusLabel.text = messageBuilder.toString()
|
||||
}
|
||||
|
||||
private fun updateStatusInfo() {
|
||||
if (::statusLabel.isInitialized) {
|
||||
val messageBuilder = StringBuilder()
|
||||
statusLabel.icon = null
|
||||
if (SettingsSyncSettings.getInstance().syncEnabled) {
|
||||
val statusTracker = SettingsSyncStatusTracker.getInstance()
|
||||
if (statusTracker.isSyncSuccessful()) {
|
||||
messageBuilder
|
||||
.append(message("sync.status.enabled"))
|
||||
if (statusTracker.isSynced()) {
|
||||
messageBuilder
|
||||
.append(". ")
|
||||
.append(message("sync.status.last.sync.message", getReadableSyncTime(), getUserName()))
|
||||
DeleteServerDataResult.Success -> {
|
||||
syncStatusChanged()
|
||||
}
|
||||
}
|
||||
else {
|
||||
messageBuilder.append(message("sync.status.failed"))
|
||||
statusLabel.icon = AllIcons.General.Error
|
||||
messageBuilder.append(' ').append(statusTracker.getErrorMessage())
|
||||
}
|
||||
}
|
||||
else {
|
||||
messageBuilder.append(message("sync.status.disabled"))
|
||||
}
|
||||
@Suppress("HardCodedStringLiteral") // The above strings are localized
|
||||
statusLabel.text = messageBuilder.toString()
|
||||
})
|
||||
cdl.await(1, TimeUnit.MINUTES)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReadableSyncTime(): String {
|
||||
return DateFormatUtil.formatPrettyDateTime(SettingsSyncStatusTracker.getInstance().getLastSyncTime()).lowercase()
|
||||
private fun updateSyncOptionText() {
|
||||
val message = if (enableSyncOption.get() == InitSyncType.GET_FROM_SERVER) {
|
||||
message("enable.dialog.get.settings.from.account.text")
|
||||
} else if (enableSyncOption.get() == InitSyncType.PUSH_LOCAL) {
|
||||
message("enable.dialog.sync.local.settings.text")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
syncTypeLabel.text ="<div>$message</div> <div style='margin-top: 5px'><a>${message("enable.dialog.change")}</a></div>"
|
||||
}
|
||||
|
||||
private fun getUserName(): String {
|
||||
return RemoteCommunicatorHolder.getAuthService().getUserData().name ?: "?"
|
||||
|
||||
private fun updateUserAccountsList() {
|
||||
userAccountsList.clear()
|
||||
val providersMap = RemoteCommunicatorHolder.getAvailableProviders().map { it.providerCode to it }.toMap()
|
||||
providersMap.forEach { providerId, communicator ->
|
||||
val authService = communicator.authService
|
||||
val providerName = authService.providerName
|
||||
authService.getAvailableUserAccounts().forEachIndexed { idx, account ->
|
||||
val separatorString = if (idx == 0)
|
||||
providerName
|
||||
else
|
||||
null
|
||||
userAccountsList.add(account.toUserProviderHolder(providerName, separatorString))
|
||||
}
|
||||
}
|
||||
if (hasMultipleProviders.get()) {
|
||||
userAccountsList.add(UserProviderHolder.addAccount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun login(
|
||||
provider: SettingsSyncCommunicatorProvider,
|
||||
syncConfigPanel: DialogPanel,
|
||||
) {
|
||||
coroutineScope.launch(ModalityState.current().asContextElement()) {
|
||||
try {
|
||||
val userData = provider.authService.login(syncConfigPanel)
|
||||
if (userData != null) {
|
||||
withContext(Dispatchers.EDT) {
|
||||
updateUserAccountsList()
|
||||
val remoteCommunicator = RemoteCommunicatorHolder.createRemoteCommunicator(provider, userData.id) ?: return@withContext
|
||||
if (checkServerState(syncPanelHolder, remoteCommunicator)) {
|
||||
SettingsSyncEvents.getInstance().fireLoginStateChanged()
|
||||
userDropDownLink.selectedItem = UserProviderHolder(userData.id, userData, provider.authService.providerCode,
|
||||
provider.authService.providerName, null)
|
||||
userDropDownLink.text
|
||||
enabledStatus.set(true)
|
||||
wasUsedBefore.set(true)
|
||||
syncStatusChanged()
|
||||
syncConfigPanel.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOG.info("Received empty user data from login")
|
||||
}
|
||||
}
|
||||
catch (ex: CancellationException) {
|
||||
LOG.info("Login procedure was cancelled")
|
||||
if (LOG.isDebugEnabled) {
|
||||
LOG.info("Login procedure was cancelled", ex)
|
||||
}
|
||||
}
|
||||
catch (ex: Throwable) {
|
||||
LOG.warn("Error during login", ex)
|
||||
}
|
||||
syncConfigPanel.requestFocusInWindow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun SettingsSyncUserData.toUserProviderHolder(providerName: String, separatorString: String? = null) =
|
||||
UserProviderHolder(id, this, providerCode, providerName, separatorString)
|
||||
|
||||
override fun syncStatusChanged() {
|
||||
updateStatusInfo()
|
||||
if (::statusLabel.isInitialized) {
|
||||
if (enabledStatus.get()) {
|
||||
val messageBuilder = StringBuilder()
|
||||
if (SettingsSyncSettings.getInstance().syncEnabled) {
|
||||
val statusTracker = SettingsSyncStatusTracker.getInstance()
|
||||
if (statusTracker.isSyncSuccessful()) {
|
||||
statusLabel.icon = icons.SettingsSyncIcons.StatusEnabled
|
||||
if (statusTracker.isSynced()) {
|
||||
messageBuilder.append(message("sync.status.last.sync.message", getReadableSyncTime()))
|
||||
} else {
|
||||
messageBuilder.append(message("sync.status.enabled"))
|
||||
}
|
||||
}
|
||||
else {
|
||||
messageBuilder.append(message("sync.status.failed"))
|
||||
statusLabel.icon = AllIcons.General.Error
|
||||
messageBuilder.append(' ').append(statusTracker.getErrorMessage())
|
||||
}
|
||||
}
|
||||
else {
|
||||
statusLabel.icon = icons.SettingsSyncIcons.StatusNotRun
|
||||
messageBuilder.append(message("sync.status.enabled"))
|
||||
}
|
||||
@Suppress("HardCodedStringLiteral") // The above strings are localized
|
||||
statusLabel.text = messageBuilder.toString()
|
||||
enableButton.text = message("config.button.disable")
|
||||
}
|
||||
else {
|
||||
statusLabel.icon = icons.SettingsSyncIcons.StatusDisabled
|
||||
statusLabel.text = message("sync.status.disabled.message")
|
||||
enableButton.text = message("config.button.enable")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun disposeUIResources() {
|
||||
super.disposeUIResources()
|
||||
SettingsSyncStatusTracker.getInstance().removeListener(this)
|
||||
private fun getReadableSyncTime(): String =
|
||||
DateFormatUtil.formatPrettyDateTime(SettingsSyncStatusTracker.getInstance().getLastSyncTime())
|
||||
|
||||
|
||||
private fun checkServerState(syncPanelHolder: SettingsSyncPanelHolder, communicator: SettingsSyncRemoteCommunicator) : Boolean {
|
||||
communicator.setTemporary(true)
|
||||
val updateResult = try {
|
||||
communicator.receiveUpdates()
|
||||
}
|
||||
catch (ex: Exception) {
|
||||
LOG.warn(ex.message)
|
||||
State.CANCELLED
|
||||
}
|
||||
when (updateResult) {
|
||||
NoFileOnServer, FileDeletedFromServer -> {
|
||||
syncPanelHolder.setSyncSettings(null)
|
||||
syncPanelHolder.setSyncScopeSettings(null)
|
||||
enableSyncOption.set(InitSyncType.PUSH_LOCAL)
|
||||
remoteSettingsExist.set(false)
|
||||
return true
|
||||
}
|
||||
is Success -> {
|
||||
syncPanelHolder.setSyncSettings(updateResult.settingsSnapshot.getState())
|
||||
syncPanelHolder.setSyncScopeSettings(SettingsSyncLocalStateHolder(updateResult.isCrossIdeSyncEnabled))
|
||||
enableSyncOption.set(InitSyncType.GET_FROM_SERVER)
|
||||
remoteSettingsExist.set(true)
|
||||
return true
|
||||
}
|
||||
is Error -> {
|
||||
if (updateResult != SettingsSyncEnabler.State.CANCELLED) {
|
||||
//showError(message("notification.title.update.error"), state.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getHelpTopic(): String = "cloud-config.plugin-dialog"
|
||||
private data class UserProviderHolder(
|
||||
val userId: String,
|
||||
val userData: SettingsSyncUserData,
|
||||
val providerCode: String,
|
||||
val providerName: String,
|
||||
val separatorString: String?, // separator value, set only for the first account in the list
|
||||
) {
|
||||
companion object{
|
||||
val addAccount = UserProviderHolder(
|
||||
"<ADDACCOUNT>", SettingsSyncUserData("<ADDACCOUNT>", ""), "",
|
||||
"", "")
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return userData.printableName ?: userData.email ?: userData.name ?: userData.id
|
||||
}
|
||||
}
|
||||
|
||||
private enum class InitSyncType {
|
||||
PUSH_LOCAL,
|
||||
GET_FROM_SERVER
|
||||
}
|
||||
|
||||
private sealed class DisableSyncType{
|
||||
companion object{
|
||||
const val DISABLE = 1
|
||||
const val DISABLE_AND_REMOVE_DATA = 2
|
||||
const val DONT_DISABLE = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class ChangeSyncTypeDialog(parent: JComponent, var option: InitSyncType) : DialogWrapper(parent, false) {
|
||||
|
||||
init {
|
||||
title = message("title.settings.sync")
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return panel {
|
||||
row {
|
||||
icon(AllIcons.General.QuestionDialog).align(AlignY.TOP)
|
||||
panel {
|
||||
row {
|
||||
text(message("enable.dialog.source.option.title")).bold()
|
||||
}
|
||||
row {
|
||||
text(message("enable.dialog.source.option.text"), 50)
|
||||
}
|
||||
buttonsGroup ("", false) {
|
||||
row {
|
||||
radioButton(message("enable.dialog.get.settings.from.account.option"), InitSyncType.GET_FROM_SERVER)
|
||||
}
|
||||
row {
|
||||
radioButton(message("enable.dialog.sync.local.settings.option"), InitSyncType.PUSH_LOCAL)
|
||||
}
|
||||
}.bind(::option)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createActions(): Array<Action> =
|
||||
arrayOf(cancelAction, okAction)
|
||||
}
|
||||
|
||||
private class AddAccountDialog(parent: JComponent) : DialogWrapper(parent, false) {
|
||||
|
||||
var providerCode: String = ""
|
||||
|
||||
init {
|
||||
title = message("title.settings.sync")
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return panel {
|
||||
row {
|
||||
icon(AllIcons.General.QuestionDialog).align(AlignY.TOP)
|
||||
panel {
|
||||
row {
|
||||
text(message("enable.sync.choose.data.provider.title")).bold()
|
||||
}
|
||||
buttonsGroup (message("enable.sync.choose.data.provider.text"), false) {
|
||||
val availableProviders = RemoteCommunicatorHolder.getAvailableProviders()
|
||||
row {
|
||||
for (provider in availableProviders) {
|
||||
radioButton(provider.authService.providerName, provider.providerCode)
|
||||
}
|
||||
}
|
||||
}.bind(::providerCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsSyncConfigurableProvider : ConfigurableProvider() {
|
||||
override fun createConfigurable(): Configurable = SettingsSyncConfigurable()
|
||||
|
||||
override fun canCreateConfigurable() = isSettingsSyncEnabledByKey()
|
||||
class SettingsSyncConfigurableProvider(private val coroutineScope: CoroutineScope) : ConfigurableProvider() {
|
||||
override fun createConfigurable(): Configurable = SettingsSyncConfigurable(coroutineScope)
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
package com.intellij.settingsSync.config
|
||||
|
||||
import com.intellij.configurationStore.saveSettings
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.progress.runBlockingCancellable
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.platform.util.progress.withProgressText
|
||||
import com.intellij.settingsSync.*
|
||||
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
|
||||
import com.intellij.util.EventDispatcher
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
|
||||
internal class SettingsSyncEnabler {
|
||||
class SettingsSyncEnabler {
|
||||
companion object {
|
||||
private val logger = logger<SettingsSyncEnabler>()
|
||||
}
|
||||
private val eventDispatcher = EventDispatcher.create(Listener::class.java)
|
||||
|
||||
|
||||
object State {
|
||||
val CANCELLED = UpdateResult.Error("Cancelled")
|
||||
}
|
||||
|
||||
fun checkServerState() {
|
||||
fun checkServerStateAsync() {
|
||||
eventDispatcher.multicaster.serverStateCheckStarted()
|
||||
val communicator = RemoteCommunicatorHolder.getRemoteCommunicator()
|
||||
val communicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run {
|
||||
logger.info("communicator doesn't exist, skipping check")
|
||||
return
|
||||
}
|
||||
object : Task.Modal(null, SettingsSyncBundle.message("enable.sync.check.server.data.progress"), true) {
|
||||
private lateinit var updateResult: UpdateResult
|
||||
|
||||
@@ -42,6 +42,14 @@ internal class SettingsSyncEnabler {
|
||||
}.queue()
|
||||
}
|
||||
|
||||
fun getServerState() : UpdateResult {
|
||||
val communicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run {
|
||||
logger.info("communicator doesn't exist, skipping check")
|
||||
return State.CANCELLED
|
||||
}
|
||||
return communicator.receiveUpdates()
|
||||
}
|
||||
|
||||
|
||||
fun getSettingsFromServer(syncSettings: SettingsSyncState? = null) {
|
||||
eventDispatcher.multicaster.updateFromServerStarted()
|
||||
@@ -50,7 +58,12 @@ internal class SettingsSyncEnabler {
|
||||
private lateinit var updateResult: UpdateResult
|
||||
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
val result = RemoteCommunicatorHolder.getRemoteCommunicator().receiveUpdates()
|
||||
val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run {
|
||||
logger.info("communicator doesn't exist, cannot get settings from server")
|
||||
updateResult = UpdateResult.Error("No remote communicator")
|
||||
return
|
||||
}
|
||||
val result = remoteCommunicator.receiveUpdates()
|
||||
updateResult = result
|
||||
if (result is UpdateResult.Success) {
|
||||
val cloudEvent = SyncSettingsEvent.CloudChange(result.settingsSnapshot, result.serverVersionId, syncSettings)
|
||||
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
package com.intellij.settingsSync.config
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.ide.plugins.InstalledPluginsState
|
||||
import com.intellij.ide.plugins.PluginManagerConfigurable
|
||||
import com.intellij.ide.plugins.PluginStateManager
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ApplicationNamesInfo
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.ex.ApplicationEx
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.components.impl.stores.stateStore
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.options.BoundConfigurable
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.options.ConfigurableProvider
|
||||
import com.intellij.openapi.options.ex.Settings
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.progress.runBlockingCancellable
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.settingsSync.*
|
||||
import com.intellij.settingsSync.SettingsSyncBundle.message
|
||||
import com.intellij.settingsSync.UpdateResult.*
|
||||
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
|
||||
//import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.settingsSync.statistics.SettingsSyncEventsStatistics
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.dsl.builder.BottomGap
|
||||
import com.intellij.ui.dsl.builder.Cell
|
||||
import com.intellij.ui.dsl.builder.RightGap
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.ui.layout.ComponentPredicate
|
||||
import com.intellij.ui.layout.and
|
||||
import com.intellij.ui.layout.not
|
||||
import com.intellij.util.text.DateFormatUtil
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JCheckBox
|
||||
import javax.swing.JLabel
|
||||
|
||||
internal class SettingsSyncOldConfigurable : BoundConfigurable(message("title.settings.sync")),
|
||||
SettingsSyncEnabler.Listener,
|
||||
SettingsSyncStatusTracker.Listener {
|
||||
|
||||
private lateinit var configPanel: DialogPanel
|
||||
private lateinit var enableButton: Cell<JButton>
|
||||
private lateinit var statusLabel: JLabel
|
||||
|
||||
@Volatile
|
||||
private var marketplacePluginInstalled = false
|
||||
|
||||
private val syncEnabler = SettingsSyncEnabler()
|
||||
private val MARKETPLACE_PLUGIN_ID = PluginId.getId("com.intellij.marketplace")
|
||||
|
||||
|
||||
init {
|
||||
syncEnabler.addListener(this)
|
||||
SettingsSyncStatusTracker.getInstance().addListener(this)
|
||||
}
|
||||
|
||||
inner class LoggedInPredicate : ComponentPredicate() {
|
||||
override fun addListener(listener: (Boolean) -> Unit) =
|
||||
SettingsSyncEvents.getInstance().addListener(
|
||||
object : SettingsSyncEventListener {
|
||||
override fun loginStateChanged() {
|
||||
listener(invoke())
|
||||
}
|
||||
},
|
||||
disposable!!)
|
||||
|
||||
override fun invoke() = RemoteCommunicatorHolder.getCurrentUserData() != null
|
||||
}
|
||||
|
||||
inner class EnabledPredicate : ComponentPredicate() {
|
||||
override fun addListener(listener: (Boolean) -> Unit) {
|
||||
SettingsSyncEvents.getInstance().addListener(object : SettingsSyncEventListener {
|
||||
override fun enabledStateChanged(syncEnabled: Boolean) {
|
||||
listener(invoke())
|
||||
configPanel.reset()
|
||||
}
|
||||
}, disposable!!)
|
||||
}
|
||||
|
||||
override fun invoke() = SettingsSyncSettings.getInstance().syncEnabled
|
||||
|
||||
}
|
||||
|
||||
inner class SyncEnablerRunning : ComponentPredicate() {
|
||||
private var isRunning = false
|
||||
|
||||
override fun addListener(listener: (Boolean) -> Unit) {
|
||||
syncEnabler.addListener(object : SettingsSyncEnabler.Listener {
|
||||
override fun serverRequestStarted() {
|
||||
updateRunning(listener, true)
|
||||
}
|
||||
|
||||
override fun serverRequestFinished() {
|
||||
updateRunning(listener, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateRunning(listener: (Boolean) -> Unit, isRunning: Boolean) {
|
||||
this.isRunning = isRunning
|
||||
listener(invoke())
|
||||
}
|
||||
|
||||
override fun invoke(): Boolean = isRunning
|
||||
}
|
||||
|
||||
inner class AuthServiceRestartPredicate : ComponentPredicate() {
|
||||
init {
|
||||
marketplacePluginInstalled = InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID)
|
||||
}
|
||||
|
||||
override fun addListener(listener: (Boolean) -> Unit) {
|
||||
PluginStateManager.addStateListener { descriptor ->
|
||||
if (descriptor.pluginId == MARKETPLACE_PLUGIN_ID) {
|
||||
// InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID) is still false at that time,
|
||||
// so we just cache the value
|
||||
marketplacePluginInstalled = true
|
||||
listener(marketplacePluginInstalled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun invoke(): Boolean {
|
||||
return marketplacePluginInstalled
|
||||
}
|
||||
}
|
||||
|
||||
override fun createPanel(): DialogPanel {
|
||||
val syncConfigPanel = SettingsSyncPanelFactory.createCombinedSyncSettingsPanel(
|
||||
message("configurable.what.to.sync.label"),
|
||||
SettingsSyncSettings.getInstance(),
|
||||
SettingsSyncLocalSettings.getInstance(),
|
||||
)
|
||||
val authService = RemoteCommunicatorHolder.getAuthService()
|
||||
val authAvailable = true
|
||||
configPanel = panel {
|
||||
val isSyncEnabled = LoggedInPredicate().and(EnabledPredicate())
|
||||
if (settingsRepositoryIsEnabled()) {
|
||||
row {
|
||||
label(message("settings.warning.sync.cannot.be.enabled.label")).applyToComponent {
|
||||
icon = AllIcons.General.Warning
|
||||
}
|
||||
bottomGap(BottomGap.MEDIUM)
|
||||
}
|
||||
}
|
||||
|
||||
// authService is not available without restart
|
||||
if (authAvailable) {
|
||||
row {
|
||||
val statusCell = label("")
|
||||
statusCell
|
||||
.visibleIf(LoggedInPredicate())
|
||||
.enabled(!settingsRepositoryIsEnabled())
|
||||
statusLabel = statusCell.component
|
||||
updateStatusInfo()
|
||||
label(message("sync.status.login.message"))
|
||||
.visibleIf(LoggedInPredicate().not())
|
||||
.enabled(!settingsRepositoryIsEnabled())
|
||||
}
|
||||
row {
|
||||
button(message("config.button.login")) {
|
||||
runBlockingCancellable {
|
||||
authService?.login(configPanel)
|
||||
}
|
||||
}.visibleIf(LoggedInPredicate().not())
|
||||
.enabled(!settingsRepositoryIsEnabled())
|
||||
enableButton = button(message("config.button.enable")) {
|
||||
syncEnabler.checkServerStateAsync()
|
||||
}.visibleIf(LoggedInPredicate().and(EnabledPredicate().not()))
|
||||
.enabledIf(SyncEnablerRunning().not())
|
||||
.enabled(!settingsRepositoryIsEnabled())
|
||||
|
||||
button(message("config.button.disable")) {
|
||||
LoggedInPredicate().and(EnabledPredicate())
|
||||
disableSync()
|
||||
}.visibleIf(isSyncEnabled)
|
||||
bottomGap(BottomGap.MEDIUM)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val authServiceRestartPredicate = AuthServiceRestartPredicate()
|
||||
row {
|
||||
label(message("sync.status.login.not.available")).gap(RightGap.SMALL)
|
||||
@Suppress("DialogTitleCapitalization", "HardCodedStringLiteral")
|
||||
link("JetBrains Marketplace Licensing Support") {
|
||||
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(it.source as ActionLink))
|
||||
val pluginManager = settings?.find("preferences.pluginManager")
|
||||
if (pluginManager is PluginManagerConfigurable) {
|
||||
settings.select(pluginManager).doWhenDone {
|
||||
pluginManager.openMarketplaceTab("/organization:JetBrains Marketplace Licensing")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.visibleIf(authServiceRestartPredicate.not())
|
||||
row {
|
||||
label(message("sync.status.restart.required", ApplicationNamesInfo.getInstance().fullProductName))
|
||||
}.visibleIf(authServiceRestartPredicate)
|
||||
row {
|
||||
button(message("sync.status.restart.ide.button")) {
|
||||
val app = ApplicationManager.getApplication() as ApplicationEx
|
||||
app.restart(true)
|
||||
}
|
||||
}.visibleIf(authServiceRestartPredicate)
|
||||
}
|
||||
row {
|
||||
comment(message("settings.sync.info.message"), 80)
|
||||
.visibleIf(isSyncEnabled.not())
|
||||
}
|
||||
|
||||
row {
|
||||
cell(syncConfigPanel)
|
||||
.visibleIf(LoggedInPredicate().and(EnabledPredicate()))
|
||||
.onApply {
|
||||
syncConfigPanel.apply()
|
||||
|
||||
SettingsSyncEvents.getInstance().fireCategoriesChanged()
|
||||
SettingsSyncEvents.getInstance().fireSettingsChanged(
|
||||
SyncSettingsEvent.CrossIdeSyncStateChanged(SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled))
|
||||
}
|
||||
.onReset(syncConfigPanel::reset)
|
||||
.onIsModified(syncConfigPanel::isModified)
|
||||
}
|
||||
}
|
||||
SettingsSyncEvents.getInstance().addListener(
|
||||
object : SettingsSyncEventListener {
|
||||
override fun loginStateChanged() {
|
||||
if (RemoteCommunicatorHolder.getCurrentUserData() != null
|
||||
&& !SettingsSyncSettings.getInstance().syncEnabled) {
|
||||
syncEnabler.checkServerStateAsync()
|
||||
}
|
||||
reset()
|
||||
}
|
||||
},
|
||||
disposable!!
|
||||
)
|
||||
return configPanel
|
||||
}
|
||||
|
||||
private fun settingsRepositoryIsEnabled(): Boolean {
|
||||
return !SettingsSyncSettings.getInstance().syncEnabled &&
|
||||
(ApplicationManager.getApplication().stateStore.storageManager).streamProvider.let { it.enabled && it.isExclusive }
|
||||
}
|
||||
|
||||
override fun serverStateCheckFinished(state: UpdateResult) {
|
||||
when (state) {
|
||||
NoFileOnServer, FileDeletedFromServer -> showEnableSyncDialog(null, null)
|
||||
is Success -> showEnableSyncDialog(
|
||||
state.settingsSnapshot.getState(),
|
||||
SettingsSyncLocalStateHolder(state.isCrossIdeSyncEnabled),
|
||||
)
|
||||
is Error -> {
|
||||
if (state != SettingsSyncEnabler.State.CANCELLED) {
|
||||
showError(message("notification.title.update.error"), state.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateFromServerFinished(result: UpdateResult) {
|
||||
when (result) {
|
||||
is Success -> {
|
||||
reset()
|
||||
SettingsSyncSettings.getInstance().syncEnabled = true
|
||||
}
|
||||
NoFileOnServer, FileDeletedFromServer -> {
|
||||
showError(message("notification.title.update.error"), message("notification.title.update.no.such.file"))
|
||||
}
|
||||
is Error -> {
|
||||
showError(message("notification.title.update.error"), result.message)
|
||||
}
|
||||
}
|
||||
updateStatusInfo()
|
||||
}
|
||||
|
||||
private fun showEnableSyncDialog(remoteSettings: SettingsSyncState?, remoteSyncScopeSettings: SettingsSyncLocalStateHolder?) {
|
||||
val dialog = EnableSettingsSyncDialog(configPanel, remoteSettings, remoteSyncScopeSettings)
|
||||
|
||||
dialog.show()
|
||||
|
||||
val dialogResult = dialog.getResult()
|
||||
|
||||
if (dialogResult != null) {
|
||||
when (dialogResult) {
|
||||
EnableSettingsSyncDialog.Result.GET_FROM_SERVER -> {
|
||||
syncEnabler.getSettingsFromServer(dialog.syncSettings)
|
||||
|
||||
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.GET_FROM_SERVER)
|
||||
}
|
||||
|
||||
EnableSettingsSyncDialog.Result.PUSH_LOCAL -> {
|
||||
SettingsSyncSettings.getInstance().syncEnabled = true
|
||||
|
||||
syncEnabler.pushSettingsToServer()
|
||||
|
||||
if (remoteSettings != null) {
|
||||
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL)
|
||||
}
|
||||
else {
|
||||
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL_WAS_ONLY_WAY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.CANCELED)
|
||||
}
|
||||
|
||||
reset()
|
||||
configPanel.reset()
|
||||
}
|
||||
|
||||
companion object DisableResult {
|
||||
const val RESULT_CANCEL = 0
|
||||
const val RESULT_REMOVE_DATA_AND_DISABLE = 1
|
||||
const val RESULT_DISABLE = 2
|
||||
}
|
||||
|
||||
private fun disableSync() {
|
||||
@Suppress("DialogTitleCapitalization")
|
||||
val result = Messages.showCheckboxMessageDialog( // TODO<rv>: Use AlertMessage instead
|
||||
message("disable.dialog.text"),
|
||||
message("disable.dialog.title"),
|
||||
arrayOf(Messages.getCancelButton(), message("disable.dialog.disable.button")),
|
||||
message("disable.dialog.remove.data.box"),
|
||||
false,
|
||||
1,
|
||||
1,
|
||||
Messages.getInformationIcon()
|
||||
) { index: Int, checkbox: JCheckBox ->
|
||||
if (index == 1) {
|
||||
if (checkbox.isSelected) RESULT_REMOVE_DATA_AND_DISABLE else RESULT_DISABLE
|
||||
}
|
||||
else {
|
||||
RESULT_CANCEL
|
||||
}
|
||||
}
|
||||
|
||||
when (result) {
|
||||
RESULT_DISABLE -> {
|
||||
SettingsSyncSettings.getInstance().syncEnabled = false
|
||||
updateStatusInfo()
|
||||
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY)
|
||||
}
|
||||
RESULT_REMOVE_DATA_AND_DISABLE -> {
|
||||
disableAndRemoveData()
|
||||
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(
|
||||
SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_AND_REMOVED_DATA_FROM_SERVER)
|
||||
}
|
||||
RESULT_CANCEL -> {
|
||||
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.CANCEL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableAndRemoveData() {
|
||||
val modality = ModalityState.current();
|
||||
|
||||
object : Task.Modal(null, message("disable.remove.data.title"), false) {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
val cdl = CountDownLatch(1)
|
||||
SettingsSyncEvents.getInstance().fireSettingsChanged(SyncSettingsEvent.DeleteServerData { result ->
|
||||
cdl.countDown()
|
||||
|
||||
when (result) {
|
||||
is DeleteServerDataResult.Error -> {
|
||||
runInEdt {
|
||||
showError(message("disable.remove.data.failure"), result.error)
|
||||
}
|
||||
}
|
||||
DeleteServerDataResult.Success -> {
|
||||
runInEdt(modality) {
|
||||
updateStatusInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
cdl.await(1, TimeUnit.MINUTES)
|
||||
}
|
||||
}.queue()
|
||||
}
|
||||
|
||||
private fun showError(message: @Nls String, details: @Nls String) {
|
||||
val messageBuilder = StringBuilder()
|
||||
messageBuilder.append(message("sync.status.failed"))
|
||||
statusLabel.icon = AllIcons.General.Error
|
||||
messageBuilder.append(' ').append("$message: $details")
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
statusLabel.text = messageBuilder.toString()
|
||||
}
|
||||
|
||||
private fun updateStatusInfo() {
|
||||
if (::statusLabel.isInitialized) {
|
||||
val messageBuilder = StringBuilder()
|
||||
statusLabel.icon = null
|
||||
if (SettingsSyncSettings.getInstance().syncEnabled) {
|
||||
val statusTracker = SettingsSyncStatusTracker.getInstance()
|
||||
if (statusTracker.isSyncSuccessful()) {
|
||||
messageBuilder
|
||||
.append(message("sync.status.enabled"))
|
||||
if (statusTracker.isSynced()) {
|
||||
messageBuilder
|
||||
.append(". ")
|
||||
.append(message("sync.status.last.sync.message", getReadableSyncTime()))
|
||||
}
|
||||
}
|
||||
else {
|
||||
messageBuilder.append(message("sync.status.failed"))
|
||||
statusLabel.icon = AllIcons.General.Error
|
||||
messageBuilder.append(' ').append(statusTracker.getErrorMessage())
|
||||
}
|
||||
}
|
||||
else {
|
||||
messageBuilder.append(message("sync.status.disabled"))
|
||||
}
|
||||
@Suppress("HardCodedStringLiteral") // The above strings are localized
|
||||
statusLabel.text = messageBuilder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReadableSyncTime(): String {
|
||||
return DateFormatUtil.formatPrettyDateTime(SettingsSyncStatusTracker.getInstance().getLastSyncTime()).lowercase()
|
||||
}
|
||||
|
||||
private fun getUserName(): String {
|
||||
return RemoteCommunicatorHolder.getCurrentUserData()?.name ?: "?"
|
||||
}
|
||||
|
||||
override fun syncStatusChanged() {
|
||||
updateStatusInfo()
|
||||
}
|
||||
|
||||
override fun disposeUIResources() {
|
||||
super.disposeUIResources()
|
||||
SettingsSyncStatusTracker.getInstance().removeListener(this)
|
||||
}
|
||||
|
||||
override fun getHelpTopic(): String = "cloud-config.plugin-dialog"
|
||||
}
|
||||
+71
-18
@@ -1,6 +1,8 @@
|
||||
package com.intellij.settingsSync.config
|
||||
|
||||
import com.intellij.openapi.application.ApplicationNamesInfo
|
||||
import com.intellij.openapi.components.SettingsCategory
|
||||
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.settingsSync.*
|
||||
@@ -30,17 +32,41 @@ internal object SettingsSyncPanelFactory {
|
||||
syncSettings: SettingsSyncState,
|
||||
syncScopeSettings: SettingsSyncLocalState,
|
||||
): DialogPanel {
|
||||
val categoriesPanel = createSyncCategoriesPanel(syncLabel, syncSettings)
|
||||
val syncScopePanel = createSyncScopePanel(syncScopeSettings)
|
||||
return SettingsSyncPanelHolder().createCombinedSyncSettingsPanel(syncLabel, syncSettings, syncScopeSettings)
|
||||
}
|
||||
}
|
||||
internal class SettingsSyncPanelHolder() {
|
||||
private lateinit var panel : DialogPanel
|
||||
private var isCrossIdeSyncEnabled = false
|
||||
|
||||
return panel {
|
||||
fun setSyncSettings(syncSettings: SettingsSyncState?) {
|
||||
val notNullState = syncSettings ?: SettingsSyncStateHolder()
|
||||
SyncCategoryHolder.updateState(notNullState)
|
||||
}
|
||||
|
||||
fun setSyncScopeSettings(settings: SettingsSyncLocalState?) {
|
||||
isCrossIdeSyncEnabled = settings?.isCrossIdeSyncEnabled ?: false
|
||||
}
|
||||
|
||||
fun createCombinedSyncSettingsPanel(
|
||||
syncLabel: @Nls String,
|
||||
syncSettings: SettingsSyncState?,
|
||||
syncScopeSettings: SettingsSyncLocalState?,
|
||||
): DialogPanel {
|
||||
setSyncSettings(syncSettings)
|
||||
setSyncScopeSettings(syncScopeSettings)
|
||||
val categoriesPanel = createSyncCategoriesPanel(syncLabel)
|
||||
val syncScopePanel = createSyncScopePanel()
|
||||
|
||||
panel = panel {
|
||||
row {
|
||||
cell(categoriesPanel)
|
||||
.onApply(categoriesPanel::apply)
|
||||
.onReset(categoriesPanel::reset)
|
||||
.onIsModified(categoriesPanel::isModified)
|
||||
.onIsModified {
|
||||
categoriesPanel.isModified()
|
||||
}
|
||||
}
|
||||
|
||||
row {
|
||||
cell(syncScopePanel)
|
||||
.onApply(syncScopePanel::apply)
|
||||
@@ -48,14 +74,23 @@ internal object SettingsSyncPanelFactory {
|
||||
.onIsModified(syncScopePanel::isModified)
|
||||
}
|
||||
onApply {
|
||||
SettingsSyncLocalSettings.getInstance().applyFromState(syncScopeSettings)
|
||||
SettingsSyncSettings.getInstance().applyFromState(syncSettings)
|
||||
// do nothing, handled by descendants
|
||||
}
|
||||
onIsModified {
|
||||
categoriesPanel.isModified() || syncScopePanel.isModified()
|
||||
}
|
||||
|
||||
}
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun createSyncScopePanel(state: SettingsSyncLocalState): DialogPanel {
|
||||
private fun createSyncScopePanel(): DialogPanel {
|
||||
return panel {
|
||||
onApply {
|
||||
SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled = isCrossIdeSyncEnabled
|
||||
SettingsSyncEvents.getInstance().fireSettingsChanged(
|
||||
SyncSettingsEvent.CrossIdeSyncStateChanged(SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled))
|
||||
}
|
||||
row {
|
||||
topGap(TopGap.MEDIUM)
|
||||
label(message("settings.cross.product.sync"))
|
||||
@@ -68,27 +103,42 @@ internal object SettingsSyncPanelFactory {
|
||||
row {
|
||||
radioButton(message("settings.cross.product.sync.choice.all.products"), true)
|
||||
}
|
||||
}.bind(state::isCrossIdeSyncEnabled)
|
||||
}.bind(::isCrossIdeSyncEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSyncCategoriesPanel(syncLabel: @Nls String, state: SettingsSyncState): DialogPanel {
|
||||
private fun createSyncCategoriesPanel(syncLabel: @Nls String): DialogPanel {
|
||||
return panel {
|
||||
onApply {
|
||||
SettingsSyncSettings.getInstance().updateCategories(
|
||||
SyncCategoryHolder.disabledCategories,
|
||||
SyncCategoryHolder.disabledSubcategories
|
||||
)
|
||||
SettingsSyncEvents.getInstance().fireCategoriesChanged()
|
||||
}
|
||||
row {
|
||||
label(syncLabel)
|
||||
}
|
||||
val categoryHolders = SyncCategoryHolder.createAllForState(state)
|
||||
for (holder in categoryHolders) {
|
||||
for (holder in SyncCategoryHolder.allHolders) {
|
||||
indent {
|
||||
row {
|
||||
if (holder.secondaryGroup == null) {
|
||||
checkBox(
|
||||
val checkBox = checkBox(
|
||||
holder.name
|
||||
)
|
||||
checkBox
|
||||
.bindSelected(holder::isSynchronized)
|
||||
.onReset { holder.reset() }
|
||||
.onApply { holder.apply() }
|
||||
.onIsModified { holder.isModified() }
|
||||
.onReset {
|
||||
holder.reset()
|
||||
checkBox.component.isSelected = holder.isSynchronized
|
||||
}
|
||||
.onApply {
|
||||
holder.apply()
|
||||
}
|
||||
.onIsModified {
|
||||
holder.isModified()
|
||||
}
|
||||
.enabled(isModifiable(holder))
|
||||
comment(holder.description)
|
||||
}
|
||||
else {
|
||||
@@ -109,8 +159,12 @@ internal object SettingsSyncPanelFactory {
|
||||
topCheckBox.state = getGroupState(holder)
|
||||
holder.isSynchronized = topCheckBox.state != State.NOT_SELECTED
|
||||
}
|
||||
cell(subcategoryLink)
|
||||
val subcategoryLinkCell = cell(subcategoryLink)
|
||||
subcategoryLinkCell
|
||||
.visible(holder.secondaryGroup!!.getDescriptors().size > 1 || !holder.secondaryGroup!!.isComplete())
|
||||
.onReset {
|
||||
subcategoryLinkCell.visible(holder.secondaryGroup!!.getDescriptors().size > 1 || !holder.secondaryGroup!!.isComplete())
|
||||
}
|
||||
topCheckBox.addActionListener {
|
||||
holder.isSynchronized = topCheckBox.state != State.NOT_SELECTED
|
||||
holder.secondaryGroup!!.getDescriptors().forEach {
|
||||
@@ -119,7 +173,6 @@ internal object SettingsSyncPanelFactory {
|
||||
subcategoryLink.isEnabled = holder.secondaryGroup!!.isComplete() || holder.isSynchronized
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ private enum class SyncStatus {ON, OFF, FAILED}
|
||||
|
||||
private fun getStatus() : SyncStatus {
|
||||
if (SettingsSyncSettings.getInstance().syncEnabled &&
|
||||
RemoteCommunicatorHolder.getAuthService().isLoggedIn()) {
|
||||
RemoteCommunicatorHolder.getCurrentUserData() != null) {
|
||||
return if (SettingsSyncStatusTracker.getInstance().isSyncSuccessful()) SyncStatus.ON
|
||||
else SyncStatus.FAILED
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import com.intellij.settingsSync.SettingsSyncBundle.message
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.util.*
|
||||
|
||||
internal class SyncCategoryHolder(
|
||||
val descriptor: Category,
|
||||
private val state: SettingsSyncState
|
||||
) {
|
||||
var isSynchronized: Boolean = state.isCategoryEnabled(descriptor.category)
|
||||
internal class SyncCategoryHolder(val descriptor: Category) {
|
||||
|
||||
private var state: SettingsSyncState? = null
|
||||
|
||||
var isSynchronized: Boolean = state?.isCategoryEnabled(descriptor.category) ?: false
|
||||
|
||||
val name: @Nls String
|
||||
get() = descriptor.name
|
||||
@@ -24,10 +24,10 @@ internal class SyncCategoryHolder(
|
||||
|
||||
fun reset() {
|
||||
with(descriptor) {
|
||||
isSynchronized = state.isCategoryEnabled(category)
|
||||
isSynchronized = state?.isCategoryEnabled(category) ?: false
|
||||
if (secondaryGroup != null) {
|
||||
secondaryGroup.getDescriptors().forEach {
|
||||
it.isSelected = isSynchronized && state.isSubcategoryEnabled(category, it.id)
|
||||
it.isSelected = isSynchronized && state?.isSubcategoryEnabled(category, it.id) ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,38 +38,68 @@ internal class SyncCategoryHolder(
|
||||
if (secondaryGroup != null) {
|
||||
secondaryGroup.getDescriptors().forEach {
|
||||
// !isSynchronized not store disabled states individually
|
||||
state.setSubcategoryEnabled(category, it.id, !isSynchronized || it.isSelected)
|
||||
state?.setSubcategoryEnabled(category, it.id, !isSynchronized || it.isSelected)
|
||||
}
|
||||
}
|
||||
state.setCategoryEnabled(category, isSynchronized)
|
||||
state?.setCategoryEnabled(category, isSynchronized)
|
||||
}
|
||||
}
|
||||
|
||||
fun isModified(): Boolean {
|
||||
with(descriptor) {
|
||||
if (isSynchronized != state.isCategoryEnabled(category)) return true
|
||||
if (isSynchronized != state?.isCategoryEnabled(category)) return true
|
||||
if (secondaryGroup != null && isSynchronized) {
|
||||
secondaryGroup.getDescriptors().forEach {
|
||||
if (it.isSelected != state.isSubcategoryEnabled(category, it.id)) return true
|
||||
if (it.isSelected != state?.isSubcategoryEnabled(category, it.id)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "SyncCategoryHolder(name='$name', isSynchronized=$isSynchronized, isModified=${isModified()})"
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
fun createAllForState(state: SettingsSyncState): List<SyncCategoryHolder> {
|
||||
val retval = arrayListOf<SyncCategoryHolder>()
|
||||
val allHolders: List<SyncCategoryHolder> = arrayListOf<SyncCategoryHolder>().apply {
|
||||
for (descriptor in Category.DESCRIPTORS) {
|
||||
retval.add(SyncCategoryHolder(descriptor, state))
|
||||
add(SyncCategoryHolder(descriptor))
|
||||
}
|
||||
return retval
|
||||
}
|
||||
|
||||
fun updateState(state: SettingsSyncState) {
|
||||
allHolders.forEach {
|
||||
it.state = state
|
||||
}
|
||||
}
|
||||
|
||||
val disabledCategories: List<SettingsCategory>
|
||||
get() = arrayListOf<SettingsCategory>().apply {
|
||||
for (holder in allHolders) {
|
||||
if (!holder.isSynchronized) {
|
||||
add(holder.descriptor.category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val disabledSubcategories: Map<SettingsCategory, List<String>>
|
||||
get() = hashMapOf<SettingsCategory, MutableList<String>>().apply {
|
||||
for (holder in allHolders) {
|
||||
val descriptors = holder.secondaryGroup?.getDescriptors() ?: continue
|
||||
for (descriptor in descriptors) {
|
||||
if (!descriptor.isSelected) {
|
||||
computeIfAbsent(holder.descriptor.category) { arrayListOf() }.add(descriptor.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class Category(
|
||||
val category: SettingsCategory,
|
||||
val secondaryGroup: SyncSubcategoryGroup? = null
|
||||
val secondaryGroup: SyncSubcategoryGroup? = null,
|
||||
) {
|
||||
|
||||
val name: @Nls String
|
||||
|
||||
+4
-5
@@ -6,7 +6,6 @@ import com.intellij.openapi.ui.playback.commands.PlaybackCommandCoroutineAdapter
|
||||
import com.intellij.settingsSync.*
|
||||
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
|
||||
import com.intellij.settingsSync.config.SettingsSyncEnabler
|
||||
import com.jetbrains.performancePlugin.commands.Waiter
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -64,7 +63,7 @@ class EnableSettingsSyncCommand(text: @NonNls String, line: Int) : PlaybackComma
|
||||
serverRespondedOnCheck.complete(true)
|
||||
}
|
||||
})
|
||||
settingsSyncEnabler.checkServerState()
|
||||
settingsSyncEnabler.checkServerStateAsync()
|
||||
serverRespondedOnCheck.await()
|
||||
|
||||
var startTime = System.currentTimeMillis()
|
||||
@@ -81,11 +80,11 @@ class EnableSettingsSyncCommand(text: @NonNls String, line: Int) : PlaybackComma
|
||||
}
|
||||
//there is no event that cross-ide sync was enabled, so we need to check that file appears and wait a bit :(
|
||||
startTime = System.currentTimeMillis()
|
||||
val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator()
|
||||
while (remoteCommunicator.isFileExists(CROSS_IDE_SYNC_MARKER_FILE) != isCrossIdeSync) {
|
||||
while (RemoteCommunicatorHolder.getRemoteCommunicator() != null &&
|
||||
RemoteCommunicatorHolder.getRemoteCommunicator()?.isFileExists(CROSS_IDE_SYNC_MARKER_FILE) != isCrossIdeSync) {
|
||||
delay(500)
|
||||
if (System.currentTimeMillis() - startTime > TimeUnit.SECONDS.toMillis(5)) {
|
||||
val fileExists = remoteCommunicator.isFileExists(CROSS_IDE_SYNC_MARKER_FILE)
|
||||
val fileExists = RemoteCommunicatorHolder.getRemoteCommunicator()?.isFileExists(CROSS_IDE_SYNC_MARKER_FILE)
|
||||
throw Exception("Cross-IDE sync marker file was not updated in 5 seconds. File exists=$fileExists, expected=$isCrossIdeSync")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import javax.swing.*;
|
||||
* DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead
|
||||
*/
|
||||
public final class SettingsSyncIcons {
|
||||
private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) {
|
||||
return IconManager.getInstance().loadRasterizedIcon(path, SettingsSyncIcons.class.getClassLoader(), cacheKey, flags);
|
||||
}
|
||||
private static @NotNull Icon load(@NotNull String expUIPath, @NotNull String path, int cacheKey, int flags) {
|
||||
return IconManager.getInstance().loadRasterizedIcon(path, expUIPath, SettingsSyncIcons.class.getClassLoader(), cacheKey, flags);
|
||||
}
|
||||
@@ -18,4 +21,5 @@ public final class SettingsSyncIcons {
|
||||
/** 16x16 */ public static final @NotNull Icon RemoteChanges = load("icons/expui/remoteChanges.svg", "icons/remoteChanges.svg", 287889654, 0);
|
||||
/** 16x16 */ public static final @NotNull Icon StatusDisabled = load("icons/expui/statusDisabled.svg", "icons/statusDisabled.svg", -1854758088, 2);
|
||||
/** 16x16 */ public static final @NotNull Icon StatusEnabled = load("icons/expui/statusEnabled.svg", "icons/statusEnabled.svg", 1900690067, 0);
|
||||
/** 16x16 */ public static final @NotNull Icon StatusNotRun = load("icons/statusNotRun.svg", 1430629267, 2);
|
||||
}
|
||||
|
||||
@@ -471,14 +471,14 @@ internal class GitSettingsLogTest {
|
||||
fun `use empty email if JBA doesn't provide one`() {
|
||||
val jbaName = "JBA Name 2"
|
||||
|
||||
userData = SettingsSyncUserData(jbaName, null)
|
||||
userData = SettingsSyncUserData(jbaName, "dummy", jbaName, null)
|
||||
checkUsernameEmail(jbaName, "")
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestFor(issues = ["EA-844607"])
|
||||
fun `use empty name if JBA doesn't provide one`() {
|
||||
userData = SettingsSyncUserData(null, null)
|
||||
userData = SettingsSyncUserData("empty", "dummy", null, null)
|
||||
checkUsernameEmail("", "")
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.settingsSync.auth.SettingsSyncAuthService
|
||||
import com.intellij.settingsSync.communicator.SettingsSyncCommunicatorProvider
|
||||
import com.intellij.settingsSync.communicator.SettingsSyncUserData
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import org.junit.Assert
|
||||
import java.awt.Component
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
@@ -14,9 +17,10 @@ import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.swing.Icon
|
||||
import kotlin.isInitialized
|
||||
|
||||
internal class MockRemoteCommunicator : AbstractServerCommunicator() {
|
||||
internal class MockRemoteCommunicator(override val userId: String) : AbstractServerCommunicator() {
|
||||
private val filesAndVersions = mutableMapOf<String, Version>()
|
||||
private val versionIdStorage = mutableMapOf<String, String>()
|
||||
private val LOG = logger<MockRemoteCommunicator>()
|
||||
@@ -43,11 +47,11 @@ internal class MockRemoteCommunicator : AbstractServerCommunicator() {
|
||||
return e.message ?: "unknown error"
|
||||
}
|
||||
|
||||
override fun readFileInternal(snapshotFilePath: String): Pair<InputStream?, String?> {
|
||||
override fun readFileInternal(filePath: String): Pair<InputStream?, String?> {
|
||||
checkConnected()
|
||||
val version = filesAndVersions[snapshotFilePath] ?: throw IOException("file $snapshotFilePath is not found")
|
||||
versionIdStorage.put(snapshotFilePath, version.versionId)
|
||||
LOG.warn("Put version '${version.versionId}' for file $snapshotFilePath (after read)")
|
||||
val version = filesAndVersions[filePath] ?: throw IOException("file $filePath is not found")
|
||||
versionIdStorage.put(filePath, version.versionId)
|
||||
LOG.warn("Put version '${version.versionId}' for file $filePath (after read)")
|
||||
return Pair(ByteArrayInputStream(version.content), version.versionId)
|
||||
}
|
||||
|
||||
@@ -150,7 +154,7 @@ internal class MockCommunicatorProvider (
|
||||
override val providerCode: String
|
||||
get() = "MOCK"
|
||||
|
||||
override fun createCommunicator(): SettingsSyncRemoteCommunicator? = remoteCommunicator
|
||||
override fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator? = remoteCommunicator
|
||||
}
|
||||
|
||||
internal class MockAuthService (
|
||||
@@ -158,21 +162,21 @@ internal class MockAuthService (
|
||||
): SettingsSyncAuthService {
|
||||
override val providerCode: String
|
||||
get() = "MOCK"
|
||||
override val providerName: String
|
||||
get() = TODO("Not yet implemented")
|
||||
override val icon: Icon?
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override fun login() {
|
||||
// do nothing
|
||||
override suspend fun login(parentComponent: Component?) : SettingsSyncUserData? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun isLoggedIn(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getUserData(): SettingsSyncUserData {
|
||||
override fun getUserData(userId: String): SettingsSyncUserData {
|
||||
return userData
|
||||
}
|
||||
|
||||
override fun isLoginAvailable(): Boolean {
|
||||
return true
|
||||
override fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -73,7 +73,7 @@ internal class SettingsSyncFlowTest : SettingsSyncTestBase() {
|
||||
// emulate first session with initialization
|
||||
val fileName = "options/laf.xml"
|
||||
val file = configDir.resolve(fileName).write("LaF Initial")
|
||||
val log = GitSettingsLog(settingsSyncStorage, configDir, disposable, { SettingsSyncUserData.EMPTY },
|
||||
val log = GitSettingsLog(settingsSyncStorage, configDir, disposable, { SettingsSyncUserData("empty", "dummy") },
|
||||
initialSnapshotProvider = { MockSettingsSyncIdeMediator.getAllFilesFromSettingsAsSnapshot(configDir) })
|
||||
log.initialize()
|
||||
log.logExistingSettings()
|
||||
|
||||
+1
-1
@@ -167,7 +167,7 @@ internal abstract class SettingsSyncRealIdeTestBase : SettingsSyncTestBase() {
|
||||
tempDir.resolve("storage").toPath(),
|
||||
tempDir.resolve("config").toPath(),
|
||||
parentDisposable,
|
||||
{ SettingsSyncUserData.EMPTY },
|
||||
{ SettingsSyncUserData("empty", "dummy") },
|
||||
initialSnapshotProvider = {
|
||||
SettingsSnapshot(
|
||||
SettingsSnapshot.MetaInfo(Instant.now(), null, true),
|
||||
|
||||
@@ -55,7 +55,7 @@ internal abstract class SettingsSyncTestBase {
|
||||
TODO("Implement with real server via TestRemoteCommunicator()")
|
||||
}
|
||||
else {
|
||||
MockRemoteCommunicator().apply {this.isConnected = true }
|
||||
MockRemoteCommunicator("mockUser").apply {this.isConnected = true }
|
||||
}
|
||||
val providerEP = SettingsSyncCommunicatorProvider.PROVIDER_EP.point
|
||||
if (providerEP.extensions.size > 0) {
|
||||
|
||||
Reference in New Issue
Block a user