diff --git a/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubCredentialsUI.kt b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubCredentialsUI.kt new file mode 100644 index 000000000000..3bcd150819db --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubCredentialsUI.kt @@ -0,0 +1,208 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.github.authentication.ui + +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.components.JBTextField +import com.intellij.ui.components.fields.ExtendableTextField +import com.intellij.ui.components.labels.LinkLabel +import com.intellij.ui.layout.* +import com.intellij.util.ui.JBUI +import org.jetbrains.plugins.github.api.* +import org.jetbrains.plugins.github.api.data.GithubAuthenticatedUser +import org.jetbrains.plugins.github.authentication.util.GithubTokenCreator +import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException +import org.jetbrains.plugins.github.exceptions.GithubParseException +import org.jetbrains.plugins.github.ui.util.DialogValidationUtils +import org.jetbrains.plugins.github.ui.util.Validator +import java.net.UnknownHostException +import java.util.function.Supplier +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.JPasswordField + +sealed class GithubCredentialsUI { + abstract fun getPanel(): JPanel + abstract fun getPreferredFocus(): JComponent + abstract fun getValidator(): Validator + abstract fun createExecutor(): GithubApiRequestExecutor + abstract fun acquireLoginAndToken(server: GithubServerPath, + executor: GithubApiRequestExecutor, + indicator: ProgressIndicator): Pair + + abstract fun handleAcquireError(error: Throwable): ValidationInfo + abstract fun setBusy(busy: Boolean) + + internal class PasswordUI(private val serverTextField: ExtendableTextField, + private val clientName: String, + switchUi: () -> Unit, + private val executorFactory: GithubApiRequestExecutor.Factory, + private val isAccountUnique: (login: String, server: GithubServerPath) -> Boolean, + private val dialogMode: Boolean) : GithubCredentialsUI() { + private val loginTextField = JBTextField() + private val passwordField = JPasswordField() + private val switchUiLink = LinkLabel.create("Use Token", switchUi) + + fun setLogin(login: String, editable: Boolean = true) { + loginTextField.text = login + loginTextField.isEditable = editable + } + + fun setPassword(password: String) { + passwordField.text = password + } + + override fun getPanel(): JPanel = JBUI.Panels.simplePanel() + .addToTop(panel { + row { + if (!dialogMode) label("Log in to GitHub", bold = true) + right { switchUiLink() } + } + }) + .addToCenter(panel { + row("Server:") { serverTextField() } + row("Login:") { loginTextField() } + row("Password:") { passwordField(comment = "Password is not saved and used only to acquire GitHub token") } + }) + + override fun getPreferredFocus() = if (loginTextField.isEditable && loginTextField.text.isEmpty()) loginTextField else passwordField + + override fun getValidator() = DialogValidationUtils.chain( + { DialogValidationUtils.notBlank(loginTextField, "Login cannot be empty") }, + { DialogValidationUtils.notBlank(passwordField, "Password cannot be empty") }) + + + override fun createExecutor(): GithubApiRequestExecutor.WithBasicAuth { + val modalityState = ModalityState.stateForComponent(passwordField) + return executorFactory.create(loginTextField.text, passwordField.password, Supplier { + invokeAndWaitIfNeeded(modalityState) { + Messages.showInputDialog(passwordField, + "Authentication Code", + "GitHub Two-Factor Authentication", + null) + } + }) + } + + override fun acquireLoginAndToken(server: GithubServerPath, + executor: GithubApiRequestExecutor, + indicator: ProgressIndicator): Pair { + val login = loginTextField.text.trim() + if (!isAccountUnique(login, server)) throw LoginNotUniqueException(login) + val token = GithubTokenCreator(server, executor, indicator).createMaster(clientName).token + return login to token + } + + override fun handleAcquireError(error: Throwable): ValidationInfo { + return when (error) { + is LoginNotUniqueException -> ValidationInfo( + "Account already added", loginTextField).withOKEnabled() + is UnknownHostException -> ValidationInfo("Server is unreachable").withOKEnabled() + is GithubAuthenticationException -> ValidationInfo( + "Incorrect credentials. ${error.message.orEmpty()}").withOKEnabled() + is GithubParseException -> ValidationInfo( + error.message ?: "Invalid server path", serverTextField) + else -> ValidationInfo("Invalid authentication data.\n ${error.message}").withOKEnabled() + } + } + + override fun setBusy(busy: Boolean) { + loginTextField.isEnabled = !busy + passwordField.isEnabled = !busy + switchUiLink.isEnabled = !busy + } + } + + internal class TokenUI(val factory: GithubApiRequestExecutor.Factory, + val isAccountUnique: (name: String, server: GithubServerPath) -> Boolean, + private val serverTextField: ExtendableTextField, + switchUi: () -> Unit, + private val dialogMode: Boolean) : GithubCredentialsUI() { + private val GIST_SCOPE_PATTERN = Regex("(?:^|, )repo(?:,|$)") + private val REPO_SCOPE_PATTERN = Regex("(?:^|, )gist(?:,|$)") + + private val tokenTextField = JBTextField() + private val switchUiLink = LinkLabel.create("Use Credentials", switchUi) + private var fixedLogin: String? = null + + fun setToken(token: String) { + tokenTextField.text = token + } + + override fun getPanel() = JBUI.Panels.simplePanel() + .addToTop(panel { + row { + if (!dialogMode) label("Log in to GitHub", bold = true) + right { switchUiLink() } + } + }) + .addToCenter(panel { + row("Server:") { serverTextField() } + row("Token:") { tokenTextField() } + }) + + + override fun getPreferredFocus() = tokenTextField + + override fun getValidator(): () -> ValidationInfo? = { + DialogValidationUtils.notBlank(tokenTextField, "Token cannot be empty") + } + + override fun createExecutor(): GithubApiRequestExecutor { + return factory.create(tokenTextField.text) + } + + override fun acquireLoginAndToken(server: GithubServerPath, + executor: GithubApiRequestExecutor, + indicator: ProgressIndicator): Pair { + var scopes: String? = null + val login = executor.execute(indicator, + object : GithubApiRequest.Get.Json( + GithubApiRequests.getUrl(server, + GithubApiRequests.CurrentUser.urlSuffix), + GithubAuthenticatedUser::class.java) { + override fun extractResult(response: GithubApiResponse): GithubAuthenticatedUser { + scopes = response.findHeader("X-OAuth-Scopes") + return super.extractResult(response) + } + }.withOperationName("get profile information")).login + if (scopes.isNullOrEmpty() + || !GIST_SCOPE_PATTERN.containsMatchIn(scopes!!) + || !REPO_SCOPE_PATTERN.containsMatchIn(scopes!!)) { + throw GithubAuthenticationException("Access token should have `repo` and `gist` scopes.") + } + + fixedLogin?.let { + if (it != login) throw GithubAuthenticationException("Token should match username \"$it\"") + } + + if (!isAccountUnique(login, server)) throw LoginNotUniqueException(login) + return login to tokenTextField.text + } + + override fun handleAcquireError(error: Throwable): ValidationInfo { + return when (error) { + is LoginNotUniqueException -> ValidationInfo( + "Account ${error.login} already added").withOKEnabled() + is UnknownHostException -> ValidationInfo("Server is unreachable").withOKEnabled() + is GithubAuthenticationException -> ValidationInfo( + "Incorrect credentials. ${error.message.orEmpty()}").withOKEnabled() + is GithubParseException -> ValidationInfo( + error.message ?: "Invalid server path", serverTextField) + else -> ValidationInfo("Invalid authentication data.\n ${error.message}").withOKEnabled() + } + } + + override fun setBusy(busy: Boolean) { + tokenTextField.isEnabled = !busy + switchUiLink.isEnabled = !busy + } + + fun setFixedLogin(fixedLogin: String?) { + this.fixedLogin = fixedLogin + } + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginDialog.kt b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginDialog.kt index b357f6eb4dd5..47cd3f63a478 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginDialog.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginDialog.kt @@ -5,158 +5,82 @@ import com.intellij.icons.AllIcons import com.intellij.ide.BrowserUtil import com.intellij.openapi.Disposable import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.application.invokeAndWaitIfNeeded -import com.intellij.openapi.components.service import com.intellij.openapi.progress.EmptyProgressIndicator -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.util.Disposer -import com.intellij.ui.AnimatedIcon -import com.intellij.ui.JBColor import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBTextField -import com.intellij.ui.components.fields.ExtendableTextComponent -import com.intellij.ui.components.fields.ExtendableTextField import com.intellij.ui.components.labels.LinkLabel import com.intellij.ui.components.panels.Wrapper import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UI.PanelFactory.grid -import com.intellij.util.ui.UI.PanelFactory.panel import com.intellij.util.ui.UIUtil -import org.jetbrains.plugins.github.api.* -import org.jetbrains.plugins.github.api.data.GithubAuthenticatedUser -import org.jetbrains.plugins.github.authentication.util.GithubTokenCreator -import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException -import org.jetbrains.plugins.github.exceptions.GithubParseException -import org.jetbrains.plugins.github.ui.util.DialogValidationUtils.chain -import org.jetbrains.plugins.github.ui.util.DialogValidationUtils.notBlank -import org.jetbrains.plugins.github.ui.util.Validator +import org.jetbrains.plugins.github.api.GithubApiRequestExecutor +import org.jetbrains.plugins.github.api.GithubServerPath +import org.jetbrains.plugins.github.util.GithubAsyncUtil +import org.jetbrains.plugins.github.util.handleOnEdt import java.awt.Component -import java.awt.Cursor -import java.net.UnknownHostException -import java.util.function.Supplier -import javax.swing.* -import javax.swing.event.HyperlinkEvent -import javax.swing.text.html.HTMLEditorKit +import javax.swing.JComponent +import javax.swing.JTextArea -class GithubLoginDialog @JvmOverloads constructor(private val executorFactory: GithubApiRequestExecutor.Factory, - private val project: Project? = null, +class GithubLoginDialog @JvmOverloads constructor(executorFactory: GithubApiRequestExecutor.Factory, + project: Project?, parent: Component? = null, - private val isAccountUnique: (name: String, server: GithubServerPath) -> Boolean = { _, _ -> true }, + isAccountUnique: (name: String, server: GithubServerPath) -> Boolean = { _, _ -> true }, title: String = "Log In to GitHub", private val message: String? = null) : DialogWrapper(project, parent, false, IdeModalityType.PROJECT) { + private var githubLoginPanel = GithubLoginPanel(executorFactory, isAccountUnique, project) - private val serverTextField = ExtendableTextField(GithubServerPath.DEFAULT_HOST) - private var centerPanel = Wrapper() - - private var southAdditionalPanel = Wrapper().apply { JBUI.Borders.emptyRight(UIUtil.DEFAULT_HGAP) } - - private var passwordUi = LoginPasswordCredentialsUI() - private var tokenUi = TokenCredentialsUI() - - private lateinit var currentUi: CredentialsUI - - private var progressIndicator: ProgressIndicator? = null - private val progressIcon = AnimatedIcon.Default() - private val progressExtension = ExtendableTextComponent.Extension { progressIcon } - - private lateinit var login: String - private lateinit var token: String - - var clientName: String = GithubTokenCreator.DEFAULT_CLIENT_NAME - private var tokenAcquisitionError: ValidationInfo? = null - private var fixedLogin: String? = null + internal lateinit var login: String + internal lateinit var token: String init { this.title = title setOKButtonText("Log In") - applyUi(passwordUi) init() - Disposer.register(disposable, Disposable { progressIndicator?.cancel() }) } @JvmOverloads fun withServer(path: String, editable: Boolean = true): GithubLoginDialog { - serverTextField.apply { - text = path - isEditable = editable - } + githubLoginPanel.setServer(path, editable) return this } @JvmOverloads fun withCredentials(login: String? = null, password: String? = null, editableLogin: Boolean = true): GithubLoginDialog { - if (login != null) { - passwordUi.setLogin(login, editableLogin) - fixedLogin = if (editableLogin) null else login - } - if (password != null) passwordUi.setPassword(password) - applyUi(passwordUi) + githubLoginPanel.setCredentials(login, password, editableLogin) return this } @JvmOverloads fun withToken(token: String? = null): GithubLoginDialog { - if (token != null) tokenUi.setToken(token) - applyUi(tokenUi) + githubLoginPanel.setToken(token) return this } fun withError(exception: Throwable): GithubLoginDialog { - tokenAcquisitionError = currentUi.handleAcquireError(exception) + githubLoginPanel.setError(exception) startTrackingValidation() return this } - fun getServer(): GithubServerPath = GithubServerPath.from(serverTextField.text.trim()) - fun getLogin(): String = login.trim() - fun getToken(): String = token.trim() + fun getServer(): GithubServerPath = githubLoginPanel.getServer() + + fun getLogin(): String = login + + fun getToken(): String = token override fun doOKAction() { - setBusy(true) - tokenAcquisitionError = null - - val server = getServer() - val executor = currentUi.createExecutor() - service().runProcessWithProgressAsynchronously(object : Task.Backgroundable(project, "Not Visible") { - override fun run(indicator: ProgressIndicator) { - val (newLogin, newToken) = currentUi.acquireLoginAndToken(server, executor, indicator) - login = newLogin - token = newToken - } - - override fun onSuccess() { + val emptyProgressIndicator = EmptyProgressIndicator(ModalityState.stateForComponent(githubLoginPanel)) + Disposer.register(disposable, Disposable { emptyProgressIndicator.cancel() }) + githubLoginPanel.acquireLoginAndToken(emptyProgressIndicator).handleOnEdt { loginToken, throwable -> + if (throwable != null && !GithubAsyncUtil.isCancellation(throwable)) startTrackingValidation() + if (loginToken != null) { + login = loginToken.first + token = loginToken.second close(OK_EXIT_CODE, true) - setBusy(false) } - - override fun onThrowable(error: Throwable) { - startTrackingValidation() - tokenAcquisitionError = currentUi.handleAcquireError(error) - setBusy(false) - } - }, progressIndicator!!) - } - - private fun setBusy(busy: Boolean) { - if (busy) { - if (!serverTextField.extensions.contains(progressExtension)) serverTextField.addExtension(progressExtension) - progressIndicator?.cancel() - progressIndicator = EmptyProgressIndicator(ModalityState.stateForComponent(window)) - Disposer.register(disposable, Disposable { progressIndicator?.cancel() }) } - else { - serverTextField.removeExtension(progressExtension) - progressIndicator = null - } - serverTextField.isEnabled = !busy - currentUi.setBusy(busy) } override fun createNorthPanel(): JComponent? { @@ -173,216 +97,13 @@ class GithubLoginDialog @JvmOverloads constructor(private val executorFactory: G } } - override fun createSouthAdditionalPanel(): Wrapper = southAdditionalPanel - override fun createCenterPanel(): Wrapper = centerPanel - override fun getPreferredFocusedComponent(): JComponent = currentUi.getPreferredFocus() + override fun createSouthAdditionalPanel() = JBUI.Panels.simplePanel() + .addToCenter(LinkLabel.create("Sign up for GitHub", Runnable { BrowserUtil.browse("https://github.com") })) + .addToRight(JBLabel(AllIcons.Ide.External_link_arrow)) - private fun applyUi(ui: CredentialsUI) { - currentUi = ui - centerPanel.setContent(currentUi.getPanel()) - southAdditionalPanel.setContent(currentUi.getSouthPanel()) - currentUi.getPreferredFocus().requestFocus() - tokenAcquisitionError = null - } + override fun createCenterPanel(): Wrapper = githubLoginPanel - override fun doValidateAll(): List { - return listOf(chain(chain({ notBlank(serverTextField, "Server cannot be empty") }, - serverPathValidator(serverTextField)), - currentUi.getValidator()), - { tokenAcquisitionError }) - .mapNotNull { it() } - } + override fun getPreferredFocusedComponent(): JComponent = githubLoginPanel.getPreferredFocus() - private fun serverPathValidator(textField: JTextField): Validator { - return { - val text = textField.text - try { - GithubServerPath.from(text) - null - } - catch (e: Exception) { - ValidationInfo("$text is not a valid server path:\n${e.message}", textField) - } - } - } - - private inner class LoginPasswordCredentialsUI : CredentialsUI() { - private val loginTextField = JBTextField() - - private val passwordField = JPasswordField() - private val contextHelp = JEditorPane() - - init { - contextHelp.apply { - editorKit = UIUtil.getHTMLEditorKit() - val linkColor = JBUI.CurrentTheme.Link.linkColor() - //language=CSS - (editorKit as HTMLEditorKit).styleSheet.addRule("a {color: rgb(${linkColor.red}, ${linkColor.green}, ${linkColor.blue})}") - //language=HTML - text = "Password is not saved and used only to
acquire GitHub token. Enter token" - addHyperlinkListener { e -> - if (e.eventType == HyperlinkEvent.EventType.ACTIVATED) { - applyUi(tokenUi) - } - else { - cursor = if (e.eventType == HyperlinkEvent.EventType.ENTERED) Cursor(Cursor.HAND_CURSOR) - else Cursor(Cursor.DEFAULT_CURSOR) - } - } - isEditable = false - isFocusable = false - isOpaque = false - border = null - margin = JBUI.emptyInsets() - foreground = JBColor.GRAY - } - } - - fun setLogin(login: String, editable: Boolean = true) { - loginTextField.text = login - loginTextField.isEditable = editable - } - - fun setPassword(password: String) { - passwordField.text = password - } - - override fun getPanel() = grid() - .add(panel(serverTextField).withLabel("Server:")) - .add(panel(loginTextField).withLabel("Login:")) - .add(panel(passwordField).withLabel("Password:")) - .add(panel(contextHelp)).createPanel() - - override fun getPreferredFocus() = if (loginTextField.isEditable && loginTextField.text.isEmpty()) loginTextField else passwordField - - override fun getValidator() = chain({ notBlank(loginTextField, "Login cannot be empty") }, - { notBlank(passwordField, "Password cannot be empty") }) - - override fun getSouthPanel() = JBUI.Panels.simplePanel() - .addToCenter(LinkLabel.create("Sign up for GitHub", Runnable { BrowserUtil.browse("https://github.com") })) - .addToRight(JBLabel(AllIcons.Ide.External_link_arrow)) - - override fun createExecutor(): GithubApiRequestExecutor.WithBasicAuth { - val modalityState = ModalityState.stateForComponent(passwordField) - return executorFactory.create(loginTextField.text, passwordField.password, Supplier { - invokeAndWaitIfNeeded(modalityState) { - Messages.showInputDialog(passwordField, - "Authentication Code", - "GitHub Two-Factor Authentication", - null) - } - }) - } - - override fun acquireLoginAndToken(server: GithubServerPath, - executor: GithubApiRequestExecutor, - indicator: ProgressIndicator): Pair { - if (!isAccountUnique(loginTextField.text, server)) throw LoginNotUniqueException() - - val token = GithubTokenCreator(server, executor, indicator).createMaster(clientName).token - - return loginTextField.text to token - } - - override fun handleAcquireError(error: Throwable): ValidationInfo { - return when (error) { - is LoginNotUniqueException -> ValidationInfo("Account already added", loginTextField) - is UnknownHostException -> ValidationInfo("Server is unreachable").withOKEnabled() - is GithubAuthenticationException -> ValidationInfo("Incorrect credentials. ${error.message.orEmpty()}").withOKEnabled() - is GithubParseException -> ValidationInfo(error.message ?: "Invalid server path", serverTextField) - else -> ValidationInfo("Invalid authentication data.\n ${error.message}").withOKEnabled() - } - } - - override fun setBusy(busy: Boolean) { - loginTextField.isEnabled = !busy - passwordField.isEnabled = !busy - contextHelp.isEnabled = !busy - } - } - - private inner class TokenCredentialsUI : CredentialsUI() { - private val GIST_SCOPE_PATTERN = Regex("(?:^|, )repo(?:,|$)") - private val REPO_SCOPE_PATTERN = Regex("(?:^|, )gist(?:,|$)") - - private val tokenTextField = JBTextField() - - private val switchUiLink = LinkLabel.create("Log In with Username") { applyUi(passwordUi) } - fun setToken(token: String) { - tokenTextField.text = token - } - - override fun getPanel() = grid() - .add(panel(serverTextField).withLabel("Server:")) - .add(panel(tokenTextField).withLabel("Token:")).createPanel() - - override fun getPreferredFocus() = tokenTextField - - override fun getValidator() = { notBlank(tokenTextField, "Token cannot be empty") } - - override fun getSouthPanel() = switchUiLink - - override fun createExecutor(): GithubApiRequestExecutor { - return executorFactory.create(tokenTextField.text) - } - - override fun acquireLoginAndToken(server: GithubServerPath, - executor: GithubApiRequestExecutor, - indicator: ProgressIndicator): Pair { - var scopes: String? = null - val login = executor.execute(indicator, - object : GithubApiRequest.Get.Json( - GithubApiRequests.getUrl(server, GithubApiRequests.CurrentUser.urlSuffix), - GithubAuthenticatedUser::class.java) { - override fun extractResult(response: GithubApiResponse): GithubAuthenticatedUser { - scopes = response.findHeader("X-OAuth-Scopes") - return super.extractResult(response) - } - }.withOperationName("get profile information")).login - if (scopes.isNullOrEmpty() - || !GIST_SCOPE_PATTERN.containsMatchIn(scopes!!) - || !REPO_SCOPE_PATTERN.containsMatchIn(scopes!!)) { - throw GithubAuthenticationException("Access token should have `repo` and `gist` scopes.") - } - - fixedLogin?.let { - if (it != login) throw GithubAuthenticationException("Token should match username \"$it\"") - } - - if (!isAccountUnique(login, server)) throw LoginNotUniqueException(login) - - return login to tokenTextField.text - } - - override fun handleAcquireError(error: Throwable): ValidationInfo { - return when (error) { - is LoginNotUniqueException -> ValidationInfo("Account ${error.login} already added").withOKEnabled() - is UnknownHostException -> ValidationInfo("Server is unreachable").withOKEnabled() - is GithubAuthenticationException -> ValidationInfo("Incorrect credentials. ${error.message.orEmpty()}").withOKEnabled() - is GithubParseException -> ValidationInfo(error.message ?: "Invalid server path", serverTextField) - else -> ValidationInfo("Invalid authentication data.\n ${error.message}").withOKEnabled() - } - } - - override fun setBusy(busy: Boolean) { - tokenTextField.isEnabled = !busy - switchUiLink.isEnabled = !busy - } - } - - private abstract inner class CredentialsUI { - abstract fun getPanel(): JPanel - abstract fun getPreferredFocus(): JComponent - abstract fun getSouthPanel(): JComponent - abstract fun getValidator(): Validator - abstract fun createExecutor(): GithubApiRequestExecutor - abstract fun acquireLoginAndToken(server: GithubServerPath, - executor: GithubApiRequestExecutor, - indicator: ProgressIndicator): Pair - - abstract fun handleAcquireError(error: Throwable): ValidationInfo - abstract fun setBusy(busy: Boolean) - } - - private class LoginNotUniqueException(val login: String? = null) : RuntimeException() + override fun doValidateAll() = githubLoginPanel.doValidateAll() } diff --git a/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginPanel.kt b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginPanel.kt new file mode 100644 index 000000000000..5b9b06a1e1d9 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/GithubLoginPanel.kt @@ -0,0 +1,143 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.github.authentication.ui + +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.components.service +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.AnimatedIcon +import com.intellij.ui.components.fields.ExtendableTextComponent +import com.intellij.ui.components.fields.ExtendableTextField +import com.intellij.ui.components.panels.Wrapper +import org.jetbrains.plugins.github.api.GithubApiRequestExecutor +import org.jetbrains.plugins.github.api.GithubServerPath +import org.jetbrains.plugins.github.authentication.util.GithubTokenCreator +import org.jetbrains.plugins.github.ui.util.DialogValidationUtils +import org.jetbrains.plugins.github.ui.util.Validator +import org.jetbrains.plugins.github.util.GithubAsyncUtil +import org.jetbrains.plugins.github.util.submitBackgroundTask +import java.util.concurrent.CompletableFuture +import javax.swing.JTextField + +class GithubLoginPanel(executorFactory: GithubApiRequestExecutor.Factory, + isAccountUnique: (name: String, server: GithubServerPath) -> Boolean, + val project: Project?, + isDialogMode: Boolean = true) : Wrapper() { + private var clientName: String = GithubTokenCreator.DEFAULT_CLIENT_NAME + private val serverTextField = ExtendableTextField(GithubServerPath.DEFAULT_HOST, 0) + private var tokenAcquisitionError: ValidationInfo? = null + + private lateinit var currentUi: GithubCredentialsUI + private var passwordUi = GithubCredentialsUI.PasswordUI(serverTextField, clientName, ::switchToTokenUI, executorFactory, isAccountUnique, + isDialogMode) + private var tokenUi = GithubCredentialsUI.TokenUI(executorFactory, isAccountUnique, serverTextField, ::switchToPasswordUI, isDialogMode) + + private val progressIcon = AnimatedIcon.Default() + private val progressExtension = ExtendableTextComponent.Extension { progressIcon } + + init { + applyUi(passwordUi) + } + + private fun switchToPasswordUI() { + applyUi(passwordUi) + } + + private fun switchToTokenUI() { + applyUi(tokenUi) + } + + private fun applyUi(ui: GithubCredentialsUI) { + currentUi = ui + setContent(currentUi.getPanel()) + currentUi.getPreferredFocus().requestFocus() + tokenAcquisitionError = null + } + + fun getPreferredFocus() = currentUi.getPreferredFocus() + + fun doValidateAll(): List { + return listOf(DialogValidationUtils.chain( + DialogValidationUtils.chain( + { DialogValidationUtils.notBlank(serverTextField, "Server cannot be empty") }, + serverPathValidator(serverTextField)), + currentUi.getValidator()), + { tokenAcquisitionError }) + .mapNotNull { it() } + } + + private fun serverPathValidator(textField: JTextField): Validator { + return { + val text = textField.text + try { + GithubServerPath.from(text) + null + } + catch (e: Exception) { + ValidationInfo("$text is not a valid server path:\n${e.message}", textField) + } + } + } + + private fun setBusy(busy: Boolean) { + if (busy) { + if (!serverTextField.extensions.contains(progressExtension)) + serverTextField.addExtension(progressExtension) + } + else { + serverTextField.removeExtension(progressExtension) + } + serverTextField.isEnabled = !busy + currentUi.setBusy(busy) + } + + fun acquireLoginAndToken(progressIndicator: ProgressIndicator): CompletableFuture> { + setBusy(true) + tokenAcquisitionError = null + + val server = getServer() + val executor = currentUi.createExecutor() + + return service() + .submitBackgroundTask(project, "Not Visible", true, progressIndicator) { + currentUi.acquireLoginAndToken(server, executor, it) + }.whenComplete { _, throwable -> + runInEdt { + setBusy(false) + if (throwable != null && !GithubAsyncUtil.isCancellation(throwable)) { + tokenAcquisitionError = currentUi.handleAcquireError(throwable) + } + } + } + } + + fun getServer(): GithubServerPath = GithubServerPath.from( + serverTextField.text.trim()) + + fun setServer(path: String, editable: Boolean = true) { + serverTextField.apply { + text = path + isEditable = editable + } + } + + fun setCredentials(login: String? = null, password: String? = null, editableLogin: Boolean = true) { + if (login != null) { + passwordUi.setLogin(login, editableLogin) + tokenUi.setFixedLogin(if (editableLogin) null else login) + } + if (password != null) passwordUi.setPassword(password) + applyUi(passwordUi) + } + + fun setToken(token: String? = null) { + if (token != null) tokenUi.setToken(token) + applyUi(tokenUi) + } + + fun setError(exception: Throwable) { + tokenAcquisitionError = currentUi.handleAcquireError(exception) + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/LoginNotUniqueException.kt b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/LoginNotUniqueException.kt new file mode 100644 index 000000000000..44dfdaa0e708 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/authentication/ui/LoginNotUniqueException.kt @@ -0,0 +1,4 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.github.authentication.ui + +internal class LoginNotUniqueException(val login: String) : RuntimeException() \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryEditor.java b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryEditor.java index 8d410881292f..b5fab041dcd4 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryEditor.java +++ b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryEditor.java @@ -123,7 +123,6 @@ public class GithubRepositoryEditor extends BaseRepositoryEditor ProgressManager.submitBackgroundTask(project: Project, +fun ProgressManager.submitBackgroundTask(project: Project?, title: String, canBeCancelled: Boolean, progressIndicator: ProgressIndicator,