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:
1 parent
0177d9f3c7
commit
31f5f91507
12 files changed
+1654
-8
No files matched your search
@@ -0,0 +1,268 @@
|
||||
use ely_gpui_component::{
|
||||
account::{
|
||||
Account, AccountSwitcher, ApiKey, ApiKeyManager, Profile, ProfileCard, ProfileEditor,
|
||||
Session, SessionList, UserMenu, Workspace, WorkspaceSwitcher,
|
||||
},
|
||||
buttons::Button,
|
||||
data_display::Presence,
|
||||
menus::MenuItem,
|
||||
primitives::IconName,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section},
|
||||
};
|
||||
|
||||
/// The switchers' demo: the account and workspace in use.
|
||||
struct Places {
|
||||
account: SharedString,
|
||||
workspace: SharedString,
|
||||
}
|
||||
|
||||
pub fn switchers(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep(
|
||||
"account-places",
|
||||
|| Places {
|
||||
account: "work".into(),
|
||||
workspace: "atlas".into(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let now = state.read(cx);
|
||||
let (account, workspace) = (now.account.clone(), now.workspace.clone());
|
||||
let [to_account, to_workspace] = [(); 2].map(|_| state.clone());
|
||||
let accounts = [
|
||||
Account {
|
||||
key: "work".into(),
|
||||
name: "Ada Lovelace".into(),
|
||||
email: "ada@atlas.dev".into(),
|
||||
image: None,
|
||||
},
|
||||
Account {
|
||||
key: "home".into(),
|
||||
name: "Ada Lovelace".into(),
|
||||
email: "ada@hey.com".into(),
|
||||
image: None,
|
||||
},
|
||||
];
|
||||
let workspaces = [
|
||||
Workspace {
|
||||
key: "atlas".into(),
|
||||
name: "Atlas".into(),
|
||||
detail: "Pro · 12 members".into(),
|
||||
},
|
||||
Workspace {
|
||||
key: "field".into(),
|
||||
name: "Field Notes".into(),
|
||||
detail: "Free · 3 members".into(),
|
||||
},
|
||||
];
|
||||
section(
|
||||
"AccountSwitcher · WorkspaceSwitcher / OrgSwitcher · UserMenu",
|
||||
"The account and the workspace in use, each a row that opens a menu of the others; the avatar opens who is signed in, the owner's rows, and Sign out.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_start()
|
||||
.gap_6()
|
||||
.child(
|
||||
div().w(px(260.)).flex().flex_col().gap_2().child(
|
||||
WorkspaceSwitcher::new("account-workspaces", workspaces, workspace)
|
||||
.on_select(move |key, _, cx| change(&to_workspace, cx, |places| places.workspace = key.clone()))
|
||||
.on_create(|_, _| log::info!("gallery: create workspace")),
|
||||
)
|
||||
.child(
|
||||
AccountSwitcher::new("account-accounts", accounts, account)
|
||||
.on_select(move |key, _, cx| change(&to_account, cx, |places| places.account = key.clone()))
|
||||
.on_add(|_, _| log::info!("gallery: add account")),
|
||||
),
|
||||
)
|
||||
.child(probe(
|
||||
"account-user",
|
||||
UserMenu::new("account-user", "Ada Lovelace", "ada@atlas.dev")
|
||||
.presence(Presence::Online)
|
||||
.item(MenuItem::new("Profile").icon(IconName::User).on_click(|_, _| log::info!("gallery: profile")))
|
||||
.item(MenuItem::new("Settings").icon(IconName::Settings).on_click(|_, _| log::info!("gallery: settings")))
|
||||
.on_sign_out(|_, _| log::info!("gallery: sign out")),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn profile(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep(
|
||||
"account-profile",
|
||||
|| Profile {
|
||||
name: "Ada Lovelace".into(),
|
||||
username: "ada".into(),
|
||||
email: "ada@atlas.dev".into(),
|
||||
bio: "Writes the notes that make machines do more than count.".into(),
|
||||
location: "London".into(),
|
||||
picture: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let now = state.read(cx).clone();
|
||||
let saved = state.clone();
|
||||
section(
|
||||
"ProfileCard · ProfileEditor",
|
||||
"Someone at a glance, and their profile to edit: Save waits for a change, a name and an email that reads; Discard puts the saved profile back.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_start()
|
||||
.gap_6()
|
||||
.child(
|
||||
div().w(px(320.)).child(
|
||||
ProfileCard::new("account-card", now.name.clone())
|
||||
.title("Analyst, Atlas")
|
||||
.presence(Presence::Online)
|
||||
.bio(now.bio.clone())
|
||||
.detail(IconName::Mail, now.email.clone())
|
||||
.detail(IconName::MapPin, now.location.clone())
|
||||
.action(Button::new("account-card-message", "Message").primary())
|
||||
.action(Button::new("account-card-profile", "View profile")),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(400.)).child(ProfileEditor::new("account-editor", now).on_save(move |profile, _, cx| {
|
||||
let profile = profile.clone();
|
||||
change(&saved, cx, move |now| *now = profile)
|
||||
}))),
|
||||
)
|
||||
}
|
||||
|
||||
/// The security demo: sessions still signed in, keys, and a secret just made.
|
||||
struct Security {
|
||||
sessions: Vec<Session>,
|
||||
keys: Vec<ApiKey>,
|
||||
secret: Option<SharedString>,
|
||||
made: usize,
|
||||
}
|
||||
|
||||
fn security() -> Security {
|
||||
let session =
|
||||
|key: &str, icon, device: &str, client: &str, place: &str, seen: &str, current| Session {
|
||||
key: key.to_string().into(),
|
||||
icon,
|
||||
device: device.to_string().into(),
|
||||
client: client.to_string().into(),
|
||||
place: place.to_string().into(),
|
||||
seen: seen.to_string().into(),
|
||||
current,
|
||||
};
|
||||
Security {
|
||||
sessions: vec![
|
||||
session(
|
||||
"phone",
|
||||
IconName::Smartphone,
|
||||
"iPhone 17",
|
||||
"Ely for iOS",
|
||||
"Lisbon, Portugal",
|
||||
"2 hours ago",
|
||||
false,
|
||||
),
|
||||
session(
|
||||
"mac",
|
||||
IconName::Monitor,
|
||||
"MacBook Pro",
|
||||
"Ely 4.2",
|
||||
"Lisbon, Portugal",
|
||||
"Active now",
|
||||
true,
|
||||
),
|
||||
session(
|
||||
"office",
|
||||
IconName::Monitor,
|
||||
"Office desktop",
|
||||
"Firefox 150",
|
||||
"Porto, Portugal",
|
||||
"3 days ago",
|
||||
false,
|
||||
),
|
||||
],
|
||||
keys: vec![
|
||||
ApiKey {
|
||||
key: "ci".into(),
|
||||
name: "CI".into(),
|
||||
hint: "ely_live_…4f2a".into(),
|
||||
created: "Made 3 Sep 2026".into(),
|
||||
used: "Last used today".into(),
|
||||
},
|
||||
ApiKey {
|
||||
key: "backup".into(),
|
||||
name: "Backups".into(),
|
||||
hint: "ely_live_…91c0".into(),
|
||||
created: "Made 12 Aug 2026".into(),
|
||||
used: "Never used".into(),
|
||||
},
|
||||
],
|
||||
secret: None,
|
||||
made: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("account-security", security, window, cx);
|
||||
let now = state.read(cx);
|
||||
let (sessions, keys, secret) = (now.sessions.clone(), now.keys.clone(), now.secret.clone());
|
||||
let [out, others, made, revoked, done] = [(); 5].map(|_| state.clone());
|
||||
let mut manager = ApiKeyManager::new("account-keys", keys)
|
||||
.on_create(move |name, _, cx| {
|
||||
let name = name.clone();
|
||||
change(&made, cx, move |security| {
|
||||
security.made += 1;
|
||||
let key: SharedString = format!("new-{}", security.made).into();
|
||||
security.keys.insert(
|
||||
0,
|
||||
ApiKey {
|
||||
key,
|
||||
name,
|
||||
hint: "ely_live_…7d3e".into(),
|
||||
created: "Made today".into(),
|
||||
used: "Never used".into(),
|
||||
},
|
||||
);
|
||||
security.secret = Some("ely_live_5f1c0a9e2b7d4c8a3e6f7d3e".into());
|
||||
})
|
||||
})
|
||||
.on_revoke(move |key, _, cx| {
|
||||
let key = key.clone();
|
||||
change(&revoked, cx, move |security| {
|
||||
security.keys.retain(|each| each.key != key)
|
||||
})
|
||||
})
|
||||
.on_dismiss(move |_, cx| change(&done, cx, |security| security.secret = None));
|
||||
if let Some(secret) = secret {
|
||||
manager = manager.secret(secret);
|
||||
}
|
||||
section(
|
||||
"SessionList · ApiKeyManager",
|
||||
"Where the account is signed in, this device first, each other one signing out on its own or all at once after a second press. API keys show their ends only; a new key's secret shows once.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_start()
|
||||
.gap_8()
|
||||
.child(div().w(px(380.)).child(
|
||||
SessionList::new("account-sessions", sessions)
|
||||
.on_sign_out(move |key, _, cx| {
|
||||
let key = key.clone();
|
||||
change(&out, cx, move |security| security.sessions.retain(|each| each.key != key))
|
||||
})
|
||||
.on_sign_out_others(move |_, cx| change(&others, cx, |security| security.sessions.retain(|each| each.current))),
|
||||
))
|
||||
.child(div().w(px(420.)).child(manager)),
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use gpui::{AnyElement, App, Window, div, prelude::*};
|
||||
|
||||
mod access;
|
||||
mod sign_in;
|
||||
|
||||
use super::Page;
|
||||
@@ -34,6 +35,11 @@ const SCRIPT: &[Step] = &[
|
||||
Step::Type("123456"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("code-failed"),
|
||||
Step::DownAt("account-user", 12.0, 12.0),
|
||||
Step::UpAt("account-user", 12.0, 12.0),
|
||||
Step::Wait(200),
|
||||
Step::Shot("user-menu"),
|
||||
Step::Key("escape"),
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
@@ -42,5 +48,8 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
.child(sign_in::sign_up(window, cx))
|
||||
.child(sign_in::by_email(window, cx))
|
||||
.child(sign_in::second_factor(window, cx))
|
||||
.child(access::switchers(window, cx))
|
||||
.child(access::profile(window, cx))
|
||||
.child(access::security_page(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
Reference in new issue
Block a user