Account · Profile and access: profiles, switchers, a user menu, sessions, keys

ProfileCard shows someone at a glance; ProfileEditor edits a profile
seeded by its owner and saves once it changed, has a name and an email
that reads, with a way to put it back. AccountSwitcher and
WorkspaceSwitcher show the one in use in a row that opens a menu of the
others and a way to add one; UserMenu opens from an avatar with who is
signed in, the owner's rows and Sign out, through the new
menus::menu_under. SessionList puts this device first, signs others
out one by one or all after a second press. ApiKeyManager lists keys by
their ends, makes one by name and shows its secret once.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-27 13:29:35 -04:00
1 parent 0177d9f3c7
commit 31f5f91507
12 files changed
+1654 -8

No files matched your search

+232
View File
@@ -0,0 +1,232 @@
use std::rc::Rc;
use gpui::{
App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement, RenderOnce,
SharedString, Styled, Window, div, prelude::*,
};
use super::login::{Run, field};
use crate::{
buttons::{Button, ButtonVariant, ConfirmButton, ConfirmMode, CopyButton},
feedback::Callout,
forms::{Enter, Input},
primitives::Severity,
theme::{ActiveTheme, ControlSize, TextSize},
typography::Ellipsis,
};
type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
/// An API key as a list shows it: its key, its name, its ends only, and when it was made and last used.
#[derive(Clone, Debug, PartialEq)]
pub struct ApiKey {
pub key: SharedString,
pub name: SharedString,
pub hint: SharedString,
pub created: SharedString,
pub used: SharedString,
}
/// API keys by name, each with its ends, when it was made and last used, and a Revoke that asks twice. A name makes a new key; its secret, handed back by the owner, shows once with a way to copy it.
#[derive(IntoElement)]
pub struct ApiKeyManager {
id: ElementId,
keys: Vec<ApiKey>,
secret: Option<SharedString>,
on_create: Option<OnKey>,
on_revoke: Option<OnKey>,
on_dismiss: Option<Run>,
}
impl ApiKeyManager {
pub fn new(id: impl Into<ElementId>, keys: impl IntoIterator<Item = ApiKey>) -> Self {
Self {
id: id.into(),
keys: keys.into_iter().collect(),
secret: None,
on_create: None,
on_revoke: None,
on_dismiss: None,
}
}
/// A new key's secret, shown until the owner clears it.
pub fn secret(mut self, secret: impl Into<SharedString>) -> Self {
self.secret = Some(secret.into());
self
}
/// Runs with the new key's name.
pub fn on_create(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_create = Some(Rc::new(handler));
self
}
pub fn on_revoke(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_revoke = Some(Rc::new(handler));
self
}
/// Runs on Done under a secret; the owner clears it.
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_dismiss = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ApiKeyManager {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let on_create = self
.on_create
.unwrap_or_else(|| panic!("api key manager {id:?} has no on_create"));
let on_revoke = self
.on_revoke
.unwrap_or_else(|| panic!("api key manager {id:?} has no on_revoke"));
let name = field(&id, "name", "A name, such as Deploys", false, window, cx);
let named: SharedString = name.read(cx).text().trim().to_string().into();
let ready = !named.is_empty();
let clear = name.clone();
let create: Run = Rc::new(move |window, cx| {
log::info!("api key manager: create {named}");
on_create(&named, window, cx);
clear.update(cx, |field, cx| field.set_text("", cx));
});
let enter = create.clone();
let theme = cx.theme();
let mono = theme.mono_family.clone();
let secret = self.secret.map(|secret| {
let dismiss = self.on_dismiss.clone().unwrap_or_else(|| {
panic!("api key manager {id:?} shows a secret with no on_dismiss")
});
Callout::new(Severity::Warning)
.title("Copy this key now. It will not show again.")
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div().flex().child(
div()
.flex_1()
.min_w_0()
.font_family(mono.clone())
.child(secret.clone()),
),
)
.child(
div()
.flex()
.flex_wrap()
.gap_2()
.child(CopyButton::new((id.clone(), "copy"), secret))
.child(
Button::new((id.clone(), "done"), "Done")
.size(ControlSize::Sm)
.on_click(move |_, window, cx| dismiss(window, cx)),
),
),
)
});
let rows = self.keys.iter().enumerate().map(|(ix, each)| {
let (key, revoke) = (each.key.clone(), on_revoke.clone());
div()
.flex()
.flex_wrap()
.items_center()
.gap_3()
.py_2p5()
.when(ix > 0, |row| {
row.border_t_1().border_color(theme.colors.border)
})
.child(
div()
.flex_1()
.min_w(theme.label_width())
.child(
div()
.flex()
.gap_2()
.child(
div()
.flex_none()
.font_weight(FontWeight::MEDIUM)
.child(each.name.clone()),
)
.child(
div()
.flex_1()
.min_w_0()
.font_family(mono.clone())
.text_color(theme.colors.fg_muted)
.child(Ellipsis::new(each.hint.clone())),
),
)
.child(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(theme.colors.fg_muted)
.child(Ellipsis::new(format!("{} · {}", each.created, each.used))),
),
)
.child(
div().flex_none().child(
ConfirmButton::new(
(id.clone(), format!("revoke-{}", each.key)),
"Revoke",
ConfirmMode::Twice,
)
.size(ControlSize::Sm)
.on_confirm(move |window, cx| {
log::info!("api key manager: revoke {key}");
revoke(&key, window, cx);
}),
),
)
});
let empty = self.keys.is_empty().then(|| {
div()
.py_2()
.text_color(theme.colors.fg_muted)
.child("No keys yet. Name one to make it.")
});
div()
.flex()
.flex_col()
.gap_3()
.text_size(theme.text_size(TextSize::Sm))
.children(secret)
.child(
div()
.flex()
.flex_wrap()
.gap_2()
.capture_action(move |_: &Enter, window, cx| {
if ready {
cx.stop_propagation();
enter(window, cx);
}
})
.child(
div()
.flex_1()
.min_w(theme.label_width())
.child(Input::new(&name)),
)
.child(
Button::new((id.clone(), "create"), "Create key")
.variant(ButtonVariant::Primary)
.disabled(!ready)
.on_click(move |_, window, cx| create(window, cx)),
),
)
.child(div().flex().flex_col().children(rows).children(empty))
}
}