title_bar: Show organization plans (#49769)

This PR updates the organization selector to also show the plan that is
available through each organization.

Also makes the `plan` method on the `UserStore` return the plan for the
current organization, if it exists.

We aren't yet populating the `plans_by_organization` collection.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2026-02-20 20:20:05 -05:00 committed by GitHub
parent 1dc574f83d
commit 57725ca982
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 44 additions and 5 deletions

View file

@ -2,7 +2,9 @@ use super::{Client, Status, TypedEnvelope, proto};
use anyhow::{Context as _, Result};
use chrono::{DateTime, Utc};
use cloud_api_client::websocket_protocol::MessageToClient;
use cloud_api_client::{GetAuthenticatedUserResponse, Organization, Plan, PlanInfo};
use cloud_api_client::{
GetAuthenticatedUserResponse, Organization, OrganizationId, Plan, PlanInfo,
};
use cloud_llm_client::{
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
};
@ -111,6 +113,7 @@ pub struct UserStore {
current_user: watch::Receiver<Option<Arc<User>>>,
current_organization: Option<Arc<Organization>>,
organizations: Vec<Arc<Organization>>,
plans_by_organization: HashMap<OrganizationId, Plan>,
contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>,
@ -185,6 +188,7 @@ impl UserStore {
current_user: current_user_rx,
current_organization: None,
organizations: Vec::new(),
plans_by_organization: HashMap::default(),
plan_info: None,
edit_prediction_usage: None,
contacts: Default::default(),
@ -698,6 +702,10 @@ impl UserStore {
&self.organizations
}
pub fn plan_for_organization(&self, organization_id: &OrganizationId) -> Option<Plan> {
self.plans_by_organization.get(organization_id).copied()
}
pub fn plan(&self) -> Option<Plan> {
#[cfg(debug_assertions)]
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
@ -713,6 +721,12 @@ impl UserStore {
};
}
if let Some(organization) = &self.current_organization
&& let Some(plan) = self.plan_for_organization(&organization.id)
{
return Some(plan);
}
self.plan_info.as_ref().map(|info| info.plan())
}

View file

@ -35,8 +35,8 @@ pub struct AuthenticatedUser {
pub accepted_tos_at: Option<Timestamp>,
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct OrganizationId(Arc<str>);
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub struct OrganizationId(pub Arc<str>);
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Organization {

View file

@ -969,8 +969,18 @@ impl TitleBar {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu.header("Organizations").separator();
let current_organization = user_store.read(cx).current_organization();
for organization in user_store.read(cx).organizations() {
let organization = organization.clone();
let plan = user_store.read(cx).plan_for_organization(&organization.id);
let is_current =
current_organization
.as_ref()
.is_some_and(|current_organization| {
current_organization.id == organization.id
});
menu = menu.custom_entry(
{
@ -978,8 +988,23 @@ impl TitleBar {
move |_window, _cx| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(&organization.name))
.gap_1()
.child(
div()
.flex_none()
.when(!is_current, |parent| parent.invisible())
.child(Icon::new(IconName::Check)),
)
.child(
h_flex()
.w_full()
.gap_3()
.justify_between()
.child(Label::new(&organization.name))
.child(PlanChip::new(
plan.unwrap_or(Plan::ZedFree),
)),
)
.into_any_element()
}
},