mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
[gitlab] Clone dialog: handle connection exception
#IDEA-324778 Fixed GitOrigin-RevId: 19df27aec55e9d0c9228394deb625b69257f742b
This commit is contained in:
committed by
intellij-monorepo-bot
parent
8122b63c84
commit
5d185c30d9
@@ -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}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user