mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
refactor(jetbrains): polish profile settings
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Polish the JetBrains user profile settings layout with a compact account stack, copyable email, simplified organization names, and a refreshable balance card.
|
||||
+109
-35
@@ -3,19 +3,24 @@ package ai.kilocode.client.settings.profile
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.rpc.dto.ProfileDto
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.RoundedLineBorder
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.BottomGap
|
||||
import com.intellij.ui.dsl.builder.RightGap
|
||||
import com.intellij.ui.dsl.builder.TopGap
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.JBValue
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Font
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import java.text.DecimalFormat
|
||||
import javax.swing.Box
|
||||
import javax.swing.DefaultComboBoxModel
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JComboBox
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
/**
|
||||
* Retained logged-in UI. Labels, combo box, and buttons are built once and
|
||||
@@ -25,48 +30,76 @@ internal class LoggedInProfileUi(
|
||||
private val dashboard: () -> Unit,
|
||||
private val logout: () -> Unit,
|
||||
private val organization: (String?) -> Unit,
|
||||
private val refresh: () -> Unit,
|
||||
) : JPanel(BorderLayout()) {
|
||||
|
||||
private val nameLabel = JBLabel().apply { font = font.deriveFont(Font.BOLD) }
|
||||
private val emailLabel = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
private val emailLabel = JBLabel().apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
setCopyable(true)
|
||||
}
|
||||
|
||||
private val balanceLabel = JBLabel().apply { font = font.deriveFont(Font.BOLD) }
|
||||
private val balanceContainer = panel {
|
||||
row {
|
||||
label(KiloBundle.message("profile.balance.title")).gap(RightGap.SMALL)
|
||||
cell(balanceLabel)
|
||||
private val titleLabel = JBLabel(KiloBundle.message("profile.balance.title")).apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
}
|
||||
private val valueLabel = JBLabel().apply {
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
font = JBFont.h1().asBold()
|
||||
}
|
||||
private val refreshBtn = JButton(KiloBundle.message("profile.action.refresh"), AllIcons.Actions.Refresh)
|
||||
.also {
|
||||
it.addActionListener {
|
||||
if (refreshing) return@addActionListener
|
||||
setRefreshing(true)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private val balanceCard = JPanel(BorderLayout()).apply {
|
||||
border = JBUI.Borders.compound(
|
||||
RoundedLineBorder(JBColor.border(), JBValue.UIInteger("Component.arc", 8).get()),
|
||||
JBUI.Borders.empty(JBUI.scale(12), JBUI.scale(16)),
|
||||
)
|
||||
add(titleLabel, BorderLayout.NORTH)
|
||||
add(JPanel(GridBagLayout()).apply {
|
||||
add(valueLabel, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridy = 0
|
||||
anchor = GridBagConstraints.CENTER
|
||||
})
|
||||
add(refreshBtn, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridy = 1
|
||||
anchor = GridBagConstraints.CENTER
|
||||
})
|
||||
}, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private val comboModel = DefaultComboBoxModel<String>()
|
||||
val combo = JComboBox(comboModel)
|
||||
private val orgContainer = panel {
|
||||
group(KiloBundle.message("profile.group.organization")) {
|
||||
row(KiloBundle.message("profile.label.account")) {
|
||||
cell(combo).align(AlignX.FILL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dashboardBtn = JButton(KiloBundle.message("profile.action.dashboard"))
|
||||
.also { it.addActionListener { dashboard() } }
|
||||
val logoutBtn = JButton(KiloBundle.message("profile.action.logout"))
|
||||
.also { it.addActionListener { logout() } }
|
||||
|
||||
private val content = panel {
|
||||
group(KiloBundle.message("profile.group.account")) {
|
||||
row { cell(nameLabel) }
|
||||
row { cell(emailLabel) }
|
||||
}
|
||||
row { cell(balanceContainer) }.topGap(TopGap.SMALL)
|
||||
row { cell(orgContainer) }
|
||||
row {
|
||||
cell(dashboardBtn).gap(RightGap.SMALL)
|
||||
cell(logoutBtn)
|
||||
}.bottomGap(BottomGap.SMALL)
|
||||
private val buttons = JPanel().apply {
|
||||
layout = javax.swing.BoxLayout(this, javax.swing.BoxLayout.X_AXIS)
|
||||
add(dashboardBtn)
|
||||
add(Box.createHorizontalStrut(JBUI.scale(6)))
|
||||
add(logoutBtn)
|
||||
}
|
||||
|
||||
private val content = JPanel(GridBagLayout()).apply {
|
||||
addRow(nameLabel, 0)
|
||||
addRow(emailLabel, 1, UiStyle.Gap.lg())
|
||||
addRow(combo, 2, UiStyle.Gap.lg())
|
||||
addRow(balanceCard, 3, UiStyle.Gap.lg())
|
||||
addRow(buttons, 4, UiStyle.Gap.lg())
|
||||
}
|
||||
|
||||
private var applying = false
|
||||
private var refreshing = false
|
||||
private var currentProf: ProfileDto? = null
|
||||
|
||||
init {
|
||||
@@ -83,6 +116,17 @@ internal class LoggedInProfileUi(
|
||||
add(content, BorderLayout.NORTH)
|
||||
}
|
||||
|
||||
private fun JPanel.addRow(comp: java.awt.Component, y: Int, top: Int = 0) {
|
||||
add(comp, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridy = y
|
||||
weightx = 1.0
|
||||
fill = GridBagConstraints.HORIZONTAL
|
||||
anchor = GridBagConstraints.WEST
|
||||
insets = JBUI.insets(top, 0, 0, 0)
|
||||
})
|
||||
}
|
||||
|
||||
fun update(profile: ProfileDto) {
|
||||
currentProf = profile
|
||||
|
||||
@@ -94,22 +138,52 @@ internal class LoggedInProfileUi(
|
||||
if (showEmail && emailLabel.text != profile.email) emailLabel.text = profile.email
|
||||
|
||||
val bal = profile.balance
|
||||
var changed = false
|
||||
if (bal != null) {
|
||||
val fmt = DecimalFormat("$#,##0.00")
|
||||
val balText = fmt.format(bal.balance)
|
||||
if (balanceLabel.text != balText) balanceLabel.text = balText
|
||||
balanceContainer.isVisible = true
|
||||
if (valueLabel.text != balText) {
|
||||
valueLabel.text = balText
|
||||
changed = true
|
||||
}
|
||||
if (!balanceCard.isVisible) {
|
||||
balanceCard.isVisible = true
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
balanceContainer.isVisible = false
|
||||
if (balanceCard.isVisible) {
|
||||
balanceCard.isVisible = false
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
applyOrganizations(profile)
|
||||
if (changed) syncLayout()
|
||||
}
|
||||
|
||||
fun setRefreshing(refreshing: Boolean) {
|
||||
this.refreshing = refreshing
|
||||
val text = if (refreshing) {
|
||||
KiloBundle.message("profile.action.refreshing")
|
||||
} else {
|
||||
KiloBundle.message("profile.action.refresh")
|
||||
}
|
||||
if (refreshBtn.text != text) refreshBtn.text = text
|
||||
refreshBtn.maximumSize = refreshBtn.preferredSize
|
||||
syncLayout()
|
||||
}
|
||||
|
||||
private fun syncLayout() {
|
||||
balanceCard.revalidate()
|
||||
content.revalidate()
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun applyOrganizations(profile: ProfileDto) {
|
||||
val orgs = profile.organizations
|
||||
val options = listOf(KiloBundle.message("profile.personalAccount")) +
|
||||
orgs.map { "${it.name} (${it.role.lowercase()})" }
|
||||
orgs.map { it.name }
|
||||
|
||||
val target = profile.currentOrgId
|
||||
?.let { id -> orgs.indexOfFirst { it.id == id }.takeIf { it >= 0 }?.plus(1) }
|
||||
@@ -128,8 +202,8 @@ internal class LoggedInProfileUi(
|
||||
}
|
||||
|
||||
val show = orgs.isNotEmpty()
|
||||
if (orgContainer.isVisible != show) {
|
||||
orgContainer.isVisible = show
|
||||
if (combo.isVisible != show) {
|
||||
combo.isVisible = show
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
+21
@@ -52,6 +52,7 @@ internal class ProfileUi(
|
||||
dashboard = { browse(DASHBOARD_URL) },
|
||||
logout = ::logout,
|
||||
organization = ::organization,
|
||||
refresh = ::refreshProfile,
|
||||
)
|
||||
|
||||
private var prof = profile
|
||||
@@ -186,6 +187,26 @@ internal class ProfileUi(
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshProfile() {
|
||||
cs.launch {
|
||||
try {
|
||||
val profile = app.refreshProfile()
|
||||
val state = app.state.value
|
||||
withContext(edt) {
|
||||
update(profile ?: state.profile, state.status)
|
||||
account.setRefreshing(false)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
withContext(edt) {
|
||||
applyState()
|
||||
account.setRefreshing(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancel() {
|
||||
auth = null
|
||||
sync()
|
||||
|
||||
@@ -132,13 +132,15 @@ profile.label.account=Active account:
|
||||
profile.notLoggedIn=Not logged in
|
||||
profile.status.connecting=Connecting to Kilo...
|
||||
profile.status.error=Connection error
|
||||
profile.balance.title=Balance:
|
||||
profile.balance.title=BALANCE
|
||||
profile.personalAccount=Personal Account
|
||||
profile.switchingAccount=Switching account...
|
||||
profile.action.login=Login with Kilo Code
|
||||
profile.action.logout=Log Out
|
||||
profile.action.dashboard=Dashboard
|
||||
profile.action.retry=Retry
|
||||
profile.action.refresh=Refresh
|
||||
profile.action.refreshing=Refreshing....
|
||||
profile.login.signingIn=Signing in to Kilo Code
|
||||
profile.login.urlLabel=Open this URL:
|
||||
profile.login.codeLabel=Enter this code:
|
||||
|
||||
+84
-1
@@ -10,6 +10,7 @@ import ai.kilocode.rpc.dto.ProfileDto
|
||||
import ai.kilocode.rpc.dto.ProfileOrganizationDto
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -20,7 +21,9 @@ import java.awt.Component
|
||||
import java.awt.Container
|
||||
import javax.swing.AbstractButton
|
||||
import javax.swing.JComboBox
|
||||
import javax.swing.JEditorPane
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
class UserProfileConfigurableTest : BasePlatformTestCase() {
|
||||
@@ -110,7 +113,10 @@ class UserProfileConfigurableTest : BasePlatformTestCase() {
|
||||
edt {
|
||||
val t = text(panel)
|
||||
assertTrue(t, t.contains("\$10.00"))
|
||||
combos(panel).single().selectedIndex = 1
|
||||
val combo = combos(panel).single()
|
||||
assertEquals("Acme", combo.getItemAt(1))
|
||||
assertFalse(combo.getItemAt(1).toString().contains("admin", ignoreCase = true))
|
||||
combo.selectedIndex = 1
|
||||
}
|
||||
flush()
|
||||
|
||||
@@ -121,6 +127,67 @@ class UserProfileConfigurableTest : BasePlatformTestCase() {
|
||||
assertEquals(listOf("org_1"), rpc.orgSelections)
|
||||
}
|
||||
|
||||
fun `test logged in profile uses compact stack and copyable email`() {
|
||||
val profile = ProfileDto(
|
||||
email = "alice@test.com",
|
||||
name = "Alice",
|
||||
organizations = listOf(ProfileOrganizationDto(id = "org_1", name = "Acme", role = "MEMBER")),
|
||||
balance = ProfileBalanceDto(10.0),
|
||||
)
|
||||
app._state.value = KiloAppStateDto(KiloAppStatusDto.READY, profile = profile)
|
||||
edt { panel.update(profile, KiloAppStatusDto.READY) }
|
||||
|
||||
edt {
|
||||
val t = text(panel)
|
||||
assertTrue(t, t.contains("Alice"))
|
||||
assertTrue(t, t.contains("alice@test.com"))
|
||||
assertTrue(t, t.contains("BALANCE"))
|
||||
assertTrue(t, t.contains("Refresh"))
|
||||
assertFalse(t, t.contains("Active account"))
|
||||
assertFalse(t, t.contains("Organization"))
|
||||
|
||||
val mail = labels(panel).filterIsInstance<JBLabel>().first { it.text == "alice@test.com" }
|
||||
assertTrue(editorPanes(mail).isNotEmpty())
|
||||
|
||||
panel.setSize(800, 600)
|
||||
layout(panel)
|
||||
val refresh = buttons(panel).first { it.text == "Refresh" }
|
||||
val card = refresh.parent
|
||||
val dash = buttons(panel).first { it.text == "Dashboard" }
|
||||
val cardLoc = SwingUtilities.convertPoint(card.parent, card.location, panel)
|
||||
val dashLoc = SwingUtilities.convertPoint(dash.parent, dash.location, panel)
|
||||
assertTrue(dashLoc.y >= cardLoc.y + card.height)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test refresh updates balance UI`() {
|
||||
val profile = ProfileDto(
|
||||
email = "alice@test.com",
|
||||
name = "Alice",
|
||||
balance = ProfileBalanceDto(10.0),
|
||||
)
|
||||
val updated = profile.copy(balance = ProfileBalanceDto(25.0))
|
||||
rpc.fakeProfile = profile
|
||||
app._state.value = KiloAppStateDto(KiloAppStatusDto.READY, profile = profile)
|
||||
edt { panel.update(profile, KiloAppStatusDto.READY) }
|
||||
|
||||
edt {
|
||||
assertTrue(text(panel).contains("\$10.00"))
|
||||
rpc.fakeProfile = updated
|
||||
buttons(panel).first { it.text == "Refresh" }.doClick()
|
||||
assertTrue(text(panel).contains("Refreshing...."))
|
||||
}
|
||||
flush()
|
||||
|
||||
edt {
|
||||
val t = text(panel)
|
||||
assertTrue(t, t.contains("\$25.00"))
|
||||
assertTrue(t, t.contains("Refresh"))
|
||||
assertFalse(t, t.contains("Refreshing...."))
|
||||
assertTrue(buttons(panel).first { it.text == "Refresh" }.isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test logged out update retains login button`() {
|
||||
edt {
|
||||
val btn = buttons(panel).first { it.text == "Login with Kilo Code" }
|
||||
@@ -252,6 +319,21 @@ class UserProfileConfigurableTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun layout(root: Container) {
|
||||
root.doLayout()
|
||||
for (comp in root.components) {
|
||||
if (comp is Container) layout(comp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun editorPanes(root: Container): List<JEditorPane> = buildList {
|
||||
for (comp in root.components) {
|
||||
if (!comp.isVisible) continue
|
||||
if (comp is JEditorPane) add(comp)
|
||||
if (comp is Container) addAll(editorPanes(comp))
|
||||
}
|
||||
}
|
||||
|
||||
private fun text(root: Container): String {
|
||||
val acc = mutableListOf<String>()
|
||||
collectText(root, acc)
|
||||
@@ -263,6 +345,7 @@ class UserProfileConfigurableTest : BasePlatformTestCase() {
|
||||
if (!comp.isVisible) continue
|
||||
when (comp) {
|
||||
is AbstractButton -> comp.text?.let { acc.add(it) }
|
||||
is JEditorPane -> comp.text?.let { acc.add(it) }
|
||||
is JLabel -> comp.text?.let { acc.add(it) }
|
||||
}
|
||||
if (comp is Container) collectText(comp, acc)
|
||||
|
||||
Reference in New Issue
Block a user