[gitlab] Clone dialog: handle connection exception

#IDEA-324778 Fixed

GitOrigin-RevId: 19df27aec55e9d0c9228394deb625b69257f742b
This commit is contained in:
Pavel Gromov
2023-07-21 18:19:16 +02:00
committed by intellij-monorepo-bot
parent 8122b63c84
commit 5d185c30d9
9 changed files with 99 additions and 42 deletions

View File

@@ -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}

View File

@@ -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:

View File

@@ -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) {

View File

@@ -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
)
}

View File

@@ -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
}

View File

@@ -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<GitLabCloneListItem>() {
internal class GitLabCloneListRenderer : ColoredListCellRenderer<GitLabCloneListItem>() {
override fun customizeCellRenderer(list: JList<out GitLabCloneListItem>,
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)
}

View File

@@ -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<GitLabAccount>,
repositoriesModel: ListModel<GitLabCloneListItem>
): JBList<GitLabCloneListItem> {
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<GitLabAccount>,
repositoriesModel: ListModel<GitLabCloneListItem>,
switchToLoginAction: (GitLabAccount) -> Unit
repositoriesModel: ListModel<GitLabCloneListItem>
): ListCellRenderer<GitLabCloneListItem> {
return GroupedRenderer(
baseRenderer = GitLabCloneListRenderer(switchToLoginAction),
baseRenderer = GitLabCloneListRenderer(),
hasSeparatorAbove = { value, index ->
when (index) {
0 -> accountsModel.size > 1

View File

@@ -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<VcsNotifier>()
@@ -69,6 +71,8 @@ internal class GitLabCloneRepositoriesViewModelImpl(
private val taskLauncher: SingleCoroutineLauncher = SingleCoroutineLauncher(cs)
override val isLoading: Flow<Boolean> = taskLauncher.busy
private val reloadRepositoriesRequest: MutableSharedFlow<Unit> = MutableSharedFlow(replay = 1)
override val accountsUpdatedRequest: SharedFlow<Set<GitLabAccount>> = accountManager.accountsState.transformLatest { accounts ->
emit(accounts)
coroutineScope {
@@ -84,14 +88,15 @@ internal class GitLabCloneRepositoriesViewModelImpl(
private val _selectedItem: MutableStateFlow<GitLabCloneListItem?> = MutableStateFlow(null)
override val items: SharedFlow<List<GitLabCloneListItem>> = accountsUpdatedRequest.transformLatest { accounts ->
taskLauncher.launch {
val repositories = accounts.flatMap { account ->
collectRepositoriesByAccount(account)
override val items: SharedFlow<List<GitLabCloneListItem>> = 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<String> = MutableStateFlow("")
override val searchValue: SharedFlow<SearchModel> = _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<GitLabCloneListItem> {
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) {

View File

@@ -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<Set<GitLabAccount>> = accountManager.accountsState