diff --git a/platform/collaboration-tools/resources/messages/CollaborationToolsBundle.properties b/platform/collaboration-tools/resources/messages/CollaborationToolsBundle.properties index 9592b47ef263..bee0e88b4542 100644 --- a/platform/collaboration-tools/resources/messages/CollaborationToolsBundle.properties +++ b/platform/collaboration-tools/resources/messages/CollaborationToolsBundle.properties @@ -140,6 +140,8 @@ clone.dialog.button.login.mnemonic=&Log In clone.dialog.clone.failed=Clone failed clone.dialog.directory.to.clone.label.text=Directory: clone.dialog.error.destination.not.exist=Clone Failed. Destination doesn't exist +clone.dialog.error.load.repositories=Unable to load repositories +clone.dialog.error.retry=Retry clone.dialog.error.unable.to.create.destination.directory=Unable to create destination directory clone.dialog.error.unable.to.find.destination.directory=Unable to find destination directory clone.dialog.insufficient.scopes=The following scopes must be granted to the access token: {0} diff --git a/plugins/github/resources/messages/GithubBundle.properties b/plugins/github/resources/messages/GithubBundle.properties index 31fb68dbf542..f8b4c8ecdd38 100644 --- a/plugins/github/resources/messages/GithubBundle.properties +++ b/plugins/github/resources/messages/GithubBundle.properties @@ -44,8 +44,6 @@ account.choose.button=Choose account.choose.not.selected=Account is not selected account.choose.as.default=Set as default account for current project account.scopes.insufficient=Insufficient security scopes -#clone -clone.error.load.repositories=Unable to load repositories #tasks task.repo.host.field=Host: task.repo.repository.field=Repository: diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponentBase.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponentBase.kt index d2cdba109068..142582421da5 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponentBase.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponentBase.kt @@ -185,7 +185,7 @@ internal abstract class GHCloneDialogExtensionComponentBase( override fun getPresentableText(error: Throwable): @Nls String = when (error) { is GithubMissingTokenException -> CollaborationToolsBundle.message("account.token.missing") is GithubAuthenticationException -> GithubBundle.message("credentials.invalid.auth.data", "") - else -> GithubBundle.message("clone.error.load.repositories") + else -> CollaborationToolsBundle.message("clone.dialog.error.load.repositories") } override fun getAction(account: GithubAccount, error: Throwable) = when (error) { diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneException.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneException.kt new file mode 100644 index 000000000000..6b02e9cc210d --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneException.kt @@ -0,0 +1,35 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.gitlab.ui.clone + +import com.intellij.collaboration.messages.CollaborationToolsBundle +import org.jetbrains.annotations.Nls + +internal sealed class GitLabCloneException( + val message: @Nls String, + val errorActionConfig: ErrorActionConfig +) { + class MissingAccessToken(loginAction: () -> Unit) : GitLabCloneException( + CollaborationToolsBundle.message("account.token.missing"), + ErrorActionConfig(loginAction, CollaborationToolsBundle.message("login.again.action.text")) + ) + + class RevokedToken(loginAction: () -> Unit) : GitLabCloneException( + CollaborationToolsBundle.message("http.status.error.refresh.token"), + ErrorActionConfig(loginAction, CollaborationToolsBundle.message("login.again.action.text")) + ) + + class ConnectionError(reloadAction: () -> Unit) : GitLabCloneException( + CollaborationToolsBundle.message("error.connection.error"), + ErrorActionConfig(reloadAction, CollaborationToolsBundle.message("clone.dialog.error.retry")) + ) + + class Unknown(message: @Nls String, reloadAction: () -> Unit) : GitLabCloneException( + message, + ErrorActionConfig(reloadAction, CollaborationToolsBundle.message("clone.dialog.error.retry")) + ) + + class ErrorActionConfig( + val action: () -> Unit, + val name: @Nls String + ) +} \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneListItem.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneListItem.kt index e8611e96152f..49953eed1411 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneListItem.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneListItem.kt @@ -2,7 +2,6 @@ package org.jetbrains.plugins.gitlab.ui.clone import com.intellij.openapi.util.NlsSafe -import org.jetbrains.annotations.Nls import org.jetbrains.plugins.gitlab.api.dto.ProjectMemberDTO import org.jetbrains.plugins.gitlab.authentication.accounts.GitLabAccount @@ -16,7 +15,7 @@ internal sealed interface GitLabCloneListItem { data class Error( override val account: GitLabAccount, - val message: @Nls String + val error: GitLabCloneException ) : GitLabCloneListItem } diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneListRenderer.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneListRenderer.kt index e3a33f31667d..4c4a99dd5e92 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneListRenderer.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneListRenderer.kt @@ -1,17 +1,13 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.plugins.gitlab.ui.clone -import com.intellij.collaboration.messages.CollaborationToolsBundle import com.intellij.collaboration.ui.util.name import com.intellij.collaboration.ui.util.swingAction import com.intellij.ui.ColoredListCellRenderer import com.intellij.ui.SimpleTextAttributes -import org.jetbrains.plugins.gitlab.authentication.accounts.GitLabAccount import javax.swing.JList -internal class GitLabCloneListRenderer( - private val switchToLoginAction: (GitLabAccount) -> Unit -) : ColoredListCellRenderer() { +internal class GitLabCloneListRenderer : ColoredListCellRenderer() { override fun customizeCellRenderer(list: JList, value: GitLabCloneListItem, index: Int, @@ -20,8 +16,11 @@ internal class GitLabCloneListRenderer( clear() when (value) { is GitLabCloneListItem.Error -> { - val action = swingAction(CollaborationToolsBundle.message("login.again.action.text")) { switchToLoginAction(value.account) } - append(value.message, SimpleTextAttributes.ERROR_ATTRIBUTES) + val cloneError = value.error + val errorActionConfig = cloneError.errorActionConfig + val action = swingAction(errorActionConfig.name) { errorActionConfig.action() } + + append(cloneError.message, SimpleTextAttributes.ERROR_ATTRIBUTES) append(" ") append(action.name.orEmpty(), SimpleTextAttributes.LINK_ATTRIBUTES, action) } diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneRepositoriesComponentFactory.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneRepositoriesComponentFactory.kt index c5706a75c14c..c2ab57924ce9 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneRepositoriesComponentFactory.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/GitLabCloneRepositoriesComponentFactory.kt @@ -62,7 +62,7 @@ internal object GitLabCloneRepositoriesComponentFactory { VcsCloneDialogUiSpec.Components.avatarSize, AccountsPopupConfig(cloneVm) ) - val repositoryList = createRepositoryList(cs, repositoriesVm, cloneVm, accountsModel, repositoriesModel) + val repositoryList = createRepositoryList(cs, repositoriesVm, accountsModel, repositoriesModel) CollaborationToolsUIUtil.attachSearch(repositoryList, searchField) { cloneItem -> when (cloneItem) { is GitLabCloneListItem.Error -> "" @@ -100,12 +100,11 @@ internal object GitLabCloneRepositoriesComponentFactory { private fun createRepositoryList( cs: CoroutineScope, repositoriesVm: GitLabCloneRepositoriesViewModel, - cloneVm: GitLabCloneViewModel, accountsModel: ListModel, repositoriesModel: ListModel ): JBList { return JBList(repositoriesModel).apply { - cellRenderer = createRepositoryRenderer(accountsModel, repositoriesModel, cloneVm::switchToLoginPanel) + cellRenderer = createRepositoryRenderer(accountsModel, repositoriesModel) isFocusable = false selectionModel.addListSelectionListener { repositoriesVm.selectItem(selectedValue) @@ -154,11 +153,10 @@ internal object GitLabCloneRepositoriesComponentFactory { private fun createRepositoryRenderer( accountsModel: ListModel, - repositoriesModel: ListModel, - switchToLoginAction: (GitLabAccount) -> Unit + repositoriesModel: ListModel ): ListCellRenderer { return GroupedRenderer( - baseRenderer = GitLabCloneListRenderer(switchToLoginAction), + baseRenderer = GitLabCloneListRenderer(), hasSeparatorAbove = { value, index -> when (index) { 0 -> accountsModel.size > 1 diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/model/GitLabCloneRepositoriesViewModel.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/model/GitLabCloneRepositoriesViewModel.kt index f51b165675f6..ab926b665437 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/model/GitLabCloneRepositoriesViewModel.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/model/GitLabCloneRepositoriesViewModel.kt @@ -15,19 +15,18 @@ import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.util.childScope import git4idea.checkout.GitCheckoutProvider import git4idea.commands.Git -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import org.jetbrains.plugins.gitlab.api.GitLabApiImpl import org.jetbrains.plugins.gitlab.api.request.getCurrentUser import org.jetbrains.plugins.gitlab.authentication.accounts.GitLabAccount import org.jetbrains.plugins.gitlab.authentication.accounts.GitLabAccountManager import org.jetbrains.plugins.gitlab.authentication.ui.GitLabAccountsDetailsProvider +import org.jetbrains.plugins.gitlab.ui.clone.GitLabCloneException import org.jetbrains.plugins.gitlab.ui.clone.GitLabCloneListItem import org.jetbrains.plugins.gitlab.ui.clone.model.GitLabCloneRepositoriesViewModel.SearchModel import org.jetbrains.plugins.gitlab.ui.clone.presentation +import java.net.ConnectException import java.net.MalformedURLException import java.net.URL import java.nio.file.Paths @@ -48,6 +47,8 @@ internal interface GitLabCloneRepositoriesViewModel : GitLabClonePanelViewModel fun setDirectoryPath(path: String) + fun reload() + fun doClone(checkoutListener: CheckoutProvider.Listener) sealed interface SearchModel { @@ -60,7 +61,8 @@ internal interface GitLabCloneRepositoriesViewModel : GitLabClonePanelViewModel internal class GitLabCloneRepositoriesViewModelImpl( private val project: Project, parentCs: CoroutineScope, - private val accountManager: GitLabAccountManager + private val accountManager: GitLabAccountManager, + private val switchToLoginAction: (GitLabAccount) -> Unit ) : GitLabCloneRepositoriesViewModel { private val vcsNotifier: VcsNotifier = project.service() @@ -69,6 +71,8 @@ internal class GitLabCloneRepositoriesViewModelImpl( private val taskLauncher: SingleCoroutineLauncher = SingleCoroutineLauncher(cs) override val isLoading: Flow = taskLauncher.busy + private val reloadRepositoriesRequest: MutableSharedFlow = MutableSharedFlow(replay = 1) + override val accountsUpdatedRequest: SharedFlow> = accountManager.accountsState.transformLatest { accounts -> emit(accounts) coroutineScope { @@ -84,14 +88,15 @@ internal class GitLabCloneRepositoriesViewModelImpl( private val _selectedItem: MutableStateFlow = MutableStateFlow(null) - override val items: SharedFlow> = accountsUpdatedRequest.transformLatest { accounts -> - taskLauncher.launch { - val repositories = accounts.flatMap { account -> - collectRepositoriesByAccount(account) + override val items: SharedFlow> = combine(reloadRepositoriesRequest, accountsUpdatedRequest, ::Pair) + .transformLatest { (_, accounts) -> + taskLauncher.launch { + val repositories = accounts.flatMap { account -> + collectRepositoriesByAccount(account) + } + emit(repositories) } - emit(repositories) - } - }.modelFlow(cs, thisLogger()) + }.modelFlow(cs, thisLogger()) private val _searchValue: MutableStateFlow = MutableStateFlow("") override val searchValue: SharedFlow = _searchValue.mapState(cs) { text -> @@ -133,6 +138,12 @@ internal class GitLabCloneRepositoriesViewModelImpl( directoryPath.value = path } + override fun reload() { + cs.launch { + reloadRepositoriesRequest.emit(Unit) + } + } + override fun doClone(checkoutListener: CheckoutProvider.Listener) { val selectedUrl = _selectedUrl.value ?: error("Clone button is enabled when repository is not selected") val directoryPath = directoryPath.value @@ -160,18 +171,31 @@ internal class GitLabCloneRepositoriesViewModelImpl( } private suspend fun collectRepositoriesByAccount(account: GitLabAccount): List { - val token = accountManager.findCredentials(account) ?: return listOf( - GitLabCloneListItem.Error(account, CollaborationToolsBundle.message("account.token.missing")) - ) - val apiClient = GitLabApiImpl { token } - val currentUser = apiClient.graphQL.getCurrentUser(account.server) ?: return listOf( - GitLabCloneListItem.Error(account, CollaborationToolsBundle.message("http.status.error.refresh.token")) - ) - val accountRepositories = currentUser.projectMemberships.map { projectMember -> - GitLabCloneListItem.Repository(account, projectMember) + return withContext(Dispatchers.IO) { + try { + val token = accountManager.findCredentials(account) ?: return@withContext listOf( + GitLabCloneListItem.Error(account, GitLabCloneException.MissingAccessToken { switchToLoginAction(account) }) + ) + val apiClient = GitLabApiImpl { token } + val currentUser = apiClient.graphQL.getCurrentUser(account.server) ?: return@withContext listOf( + GitLabCloneListItem.Error(account, GitLabCloneException.RevokedToken { switchToLoginAction(account) }) + ) + val accountRepositories = currentUser.projectMemberships.map { projectMember -> + GitLabCloneListItem.Repository(account, projectMember) + } + accountRepositories.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.presentation() }) + } + catch (e: CancellationException) { + throw e + } + catch (_: ConnectException) { + listOf(GitLabCloneListItem.Error(account, GitLabCloneException.ConnectionError(::reload))) + } + catch (e: Throwable) { + val errorMessage = e.localizedMessage ?: CollaborationToolsBundle.message("clone.dialog.error.load.repositories") + listOf(GitLabCloneListItem.Error(account, GitLabCloneException.Unknown(errorMessage, ::reload))) + } } - - return accountRepositories.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.presentation() }) } private fun notifyCreateDirectoryFailed(message: String) { diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/model/GitLabCloneViewModel.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/model/GitLabCloneViewModel.kt index 1a8a91d9ce1f..b3fe5f47d271 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/model/GitLabCloneViewModel.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/ui/clone/model/GitLabCloneViewModel.kt @@ -32,7 +32,9 @@ internal class GitLabCloneViewModelImpl( private val cs: CoroutineScope = parentCs.childScope() private val loginVm = GitLabCloneLoginViewModelImpl(cs, accountManager) - private val repositoriesVm = GitLabCloneRepositoriesViewModelImpl(project, cs, accountManager) + private val repositoriesVm = GitLabCloneRepositoriesViewModelImpl(project, cs, accountManager, ::switchToLoginPanel).apply { + reload() + } private val accounts: SharedFlow> = accountManager.accountsState