diff --git a/app/src/settings_view/billing_and_usage/billing_cycle_usage_section.rs b/app/src/settings_view/billing_and_usage/billing_cycle_usage_section.rs index 94fc2b3644b..e6e37b1f832 100644 --- a/app/src/settings_view/billing_and_usage/billing_cycle_usage_section.rs +++ b/app/src/settings_view/billing_and_usage/billing_cycle_usage_section.rs @@ -299,7 +299,9 @@ impl BillingCycleUsageSectionView { .finish(), ); - if is_admin && let Some(banner) = self.render_visibility_cta_banner(workspace, appearance) { + if is_admin + && let Some(banner) = self.render_visibility_cta_banner(workspace, appearance, app) + { column.add_child(Container::new(banner).with_margin_top(16.).finish()); } @@ -644,28 +646,44 @@ impl BillingCycleUsageSectionView { .finish() } + /// Whether the viewer is an admin of this workspace on a tier that has + /// native workspaces enabled -- the case where the admin panel owns the + /// full workspace settings surface, not just per-user spend limits. + fn viewer_is_native_workspaces_admin(&self, workspace: &Workspace, app: &AppContext) -> bool { + workspace.is_native_workspaces_enabled() + && Self::resolved_viewer_email(app) + .as_deref() + .is_some_and(|email| workspace.is_workspace_admin(email)) + } + /// Renders the CTA banner that sits between the team-totals block and - /// the per-member rows. The copy and action vary by visibility tier: - /// non-FullBreakdown admins see an upgrade nudge; FullBreakdown admins - /// see a pointer to the admin panel where per-user spend limits actually - /// get configured. + /// the per-member rows. Workspace admins on a native-workspaces tier are + /// pointed at the admin panel for the whole workspace settings surface. + /// Otherwise the copy and action vary by visibility tier: non-FullBreakdown + /// admins see an upgrade nudge; FullBreakdown admins see a pointer to the + /// admin panel where per-user spend limits actually get configured. fn render_visibility_cta_banner( &self, workspace: &Workspace, appearance: &Appearance, + app: &AppContext, ) -> Option> { - let admin_granularity = workspace - .billing_metadata - .tier - .usage_visibility_policy? - .admin_granularity; - if admin_granularity == UsageVisibilityGranularity::FullBreakdown - && !workspace.billing_metadata.is_enterprise_plan() - { - return None; - } let (link_text, trailing_copy, action, leading_icon) = - visibility_cta_for(admin_granularity)?; + if self.viewer_is_native_workspaces_admin(workspace, app) { + NATIVE_WORKSPACES_ADMIN_CTA + } else { + let admin_granularity = workspace + .billing_metadata + .tier + .usage_visibility_policy? + .admin_granularity; + if admin_granularity == UsageVisibilityGranularity::FullBreakdown + && !workspace.billing_metadata.is_enterprise_plan() + { + return None; + } + visibility_cta_for(admin_granularity)? + }; // Only show when there are teammates -- a single-member workspace // doesn't benefit from any of the team-level visibility CTAs. @@ -718,6 +736,16 @@ impl BillingCycleUsageSectionView { } } +/// The CTA shown to workspace admins whose tier has native workspaces +/// enabled: the admin panel owns workspace settings as a whole there, so the +/// copy points at that rather than only at per-user spend limits. +const NATIVE_WORKSPACES_ADMIN_CTA: (&str, &str, BillingCycleUsageAction, Icon) = ( + "Open the admin panel", + "to view and edit workspace settings and spend limits.", + BillingCycleUsageAction::OpenAdminPanel, + Icon::Users, +); + /// Returns the (link text, trailing copy, action, icon) tuple for the /// visibility CTA banner, or `None` to suppress the banner entirely. fn visibility_cta_for( diff --git a/app/src/workspaces/gql_convert.rs b/app/src/workspaces/gql_convert.rs index 192307f4101..dcd52a94bc8 100644 --- a/app/src/workspaces/gql_convert.rs +++ b/app/src/workspaces/gql_convert.rs @@ -12,6 +12,7 @@ use warp_graphql::billing::{ EnterpriseCreditsAutoReloadPolicy as GqlEnterpriseCreditsAutoReloadPolicy, EnterprisePayAsYouGoPolicy as GqlEnterprisePayAsYouGoPolicy, InstanceShape as GqlInstanceShape, ManagedByokByoePolicy as GqlManagedByokByoePolicy, MultiAdminPolicy as GqlMultiAdminPolicy, + NativeWorkspacesPolicy as GqlNativeWorkspacesPolicy, PurchaseAddOnCreditsPolicy as GqlPurchaseAddOnCreditsPolicy, ServiceAgreementType, SessionSharingPolicy as GqlSessionSharingPolicy, SharedNotebooksPolicy as GqlSharedNotebooksPolicy, @@ -76,7 +77,8 @@ use crate::settings::AgentModeCommandExecutionPredicate; use crate::workspaces::workspace::{ AiOverages, BonusGrantsPurchased, ByoApiKeyPolicy, ByoEndpointPolicy, CodebaseContextPolicy, EnterpriseCreditsAutoReloadPolicy, EnterprisePayAsYouGoPolicy, ManagedByokByoePolicy, - MultiAdminPolicy, PurchaseAddOnCreditsPolicy, UsageBasedPricingSettings, + MultiAdminPolicy, NativeWorkspacesPolicy, PurchaseAddOnCreditsPolicy, + UsageBasedPricingSettings, }; pub const PLACEHOLDER_WORKSPACE_UID: &str = "NOT_A_REAL_WORKSPACE_UID"; @@ -504,6 +506,14 @@ impl From for MultiAdminPolicy { } } +impl From for NativeWorkspacesPolicy { + fn from(gql_policy: GqlNativeWorkspacesPolicy) -> NativeWorkspacesPolicy { + Self { + enabled: gql_policy.enabled, + } + } +} + impl From for InstanceShape { fn from(gql_instance_shape: GqlInstanceShape) -> InstanceShape { Self { @@ -629,6 +639,7 @@ impl From for Tier { .enterprise_credits_auto_reload_policy .map(From::from), multi_admin_policy: gql_tier.multi_admin_policy.map(From::from), + native_workspaces_policy: gql_tier.native_workspaces_policy.map(From::from), ambient_agents_policy: gql_tier.ambient_agents_policy.map(From::from), usage_visibility_policy: gql_tier.usage_visibility_policy.map(From::from), } diff --git a/app/src/workspaces/workspace.rs b/app/src/workspaces/workspace.rs index ddbfdcf8806..a2de4cf912f 100644 --- a/app/src/workspaces/workspace.rs +++ b/app/src/workspaces/workspace.rs @@ -95,6 +95,15 @@ impl Workspace { .is_some_and(|member| member.role.is_admin_or_owner()) } + /// Whether the workspace's tier enables native workspaces, which move + /// workspace-level settings (including spend limits) into the admin panel. + pub fn is_native_workspaces_enabled(&self) -> bool { + self.billing_metadata + .tier + .native_workspaces_policy + .is_some_and(|policy| policy.enabled) + } + pub fn resolve_usage_visibility(&self, is_admin: bool) -> UsageVisibility { let Some(policy) = self.billing_metadata.tier.usage_visibility_policy else { return UsageVisibility::default(); @@ -405,6 +414,11 @@ pub struct MultiAdminPolicy { pub enabled: bool, } +#[derive(Clone, Debug, Copy, Serialize, Deserialize)] +pub struct NativeWorkspacesPolicy { + pub enabled: bool, +} + #[derive(Clone, Debug, Copy, Serialize, Deserialize)] pub struct AmbientAgentsPolicy { pub max_concurrent_agents: i32, @@ -489,6 +503,7 @@ pub struct Tier { pub enterprise_pay_as_you_go_policy: Option, pub enterprise_credits_auto_reload_policy: Option, pub multi_admin_policy: Option, + pub native_workspaces_policy: Option, pub ambient_agents_policy: Option, pub usage_visibility_policy: Option, } diff --git a/crates/graphql/src/api/billing.rs b/crates/graphql/src/api/billing.rs index c07346a3220..0506a7f400d 100644 --- a/crates/graphql/src/api/billing.rs +++ b/crates/graphql/src/api/billing.rs @@ -112,6 +112,7 @@ pub struct Tier { pub enterprise_pay_as_you_go_policy: Option, pub enterprise_credits_auto_reload_policy: Option, pub multi_admin_policy: Option, + pub native_workspaces_policy: Option, pub ambient_agents_policy: Option, pub usage_visibility_policy: Option, } @@ -220,6 +221,11 @@ pub struct MultiAdminPolicy { pub enabled: bool, } +#[derive(cynic::QueryFragment, Debug, Clone)] +pub struct NativeWorkspacesPolicy { + pub enabled: bool, +} + #[derive(cynic::QueryFragment, Debug, Clone)] pub struct AmbientAgentsPolicy { pub enabled: bool, diff --git a/crates/warp_graphql_schema/api/schema.graphql b/crates/warp_graphql_schema/api/schema.graphql index 1dff585bc2d..1c5f22504b5 100644 --- a/crates/warp_graphql_schema/api/schema.graphql +++ b/crates/warp_graphql_schema/api/schema.graphql @@ -2403,6 +2403,10 @@ type MultiAdminPolicy { enabled: Boolean! } +type NativeWorkspacesPolicy { + enabled: Boolean! +} + scalar NodeHash type Notebook { @@ -3799,6 +3803,7 @@ type Tier { managedByokByoePolicy: ManagedByokByoePolicy multiAdminPolicy: MultiAdminPolicy name: String! + nativeWorkspacesPolicy: NativeWorkspacesPolicy pricing: TierPricing purchaseAddOnCreditsPolicy: PurchaseAddOnCreditsPolicy sessionSharingPolicy: SessionSharingPolicy