GithubLoginDialog: extract all login related components and logic into separate class

To be able to reuse it in new Github clone ui

GitOrigin-RevId: 70e50625119431fb93a54feed3a6273f10f8dbe6
This commit is contained in:
Aleksandr Krasilnikov
2019-07-02 06:52:16 +03:00
committed by intellij-monorepo-bot
parent 33b9c9641d
commit 8be1346696
6 changed files with 390 additions and 315 deletions
@@ -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<String, String>
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<String, String> {
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<String, String> {
var scopes: String? = null
val login = executor.execute(indicator,
object : GithubApiRequest.Get.Json<GithubAuthenticatedUser>(
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
}
}
}
@@ -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<ProgressManager>().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<ValidationInfo> {
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 = "<html>Password is not saved and used only to <br>acquire GitHub token. <a href=''>Enter token</a></html>"
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<String, String> {
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<String, String> {
var scopes: String? = null
val login = executor.execute(indicator,
object : GithubApiRequest.Get.Json<GithubAuthenticatedUser>(
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<String, String>
abstract fun handleAcquireError(error: Throwable): ValidationInfo
abstract fun setBusy(busy: Boolean)
}
private class LoginNotUniqueException(val login: String? = null) : RuntimeException()
override fun doValidateAll() = githubLoginPanel.doValidateAll()
}
@@ -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<ValidationInfo> {
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<Pair<String, String>> {
setBusy(true)
tokenAcquisitionError = null
val server = getServer()
val executor = currentUi.createExecutor()
return service<ProgressManager>()
.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)
}
}
@@ -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()
@@ -123,7 +123,6 @@ public class GithubRepositoryEditor extends BaseRepositoryEditor<GithubRepositor
private void generateToken() {
GithubLoginDialog dialog = new GithubLoginDialog(GithubApiRequestExecutor.Factory.getInstance(), myProject);
dialog.withServer(getHost(), false);
dialog.setClientName("Tasks Plugin");
if (dialog.showAndGet()) {
myToken.setText(dialog.getToken());
}
@@ -60,7 +60,7 @@ object GithubAsyncUtil {
}
}
fun <T> ProgressManager.submitBackgroundTask(project: Project,
fun <T> ProgressManager.submitBackgroundTask(project: Project?,
title: String,
canBeCancelled: Boolean,
progressIndicator: ProgressIndicator,