first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
@@ -1,21 +1,14 @@
#![allow(dead_code, unused_imports, unused_variables)]
use crate::editor::Event as EditorEvent;
use crate::modal::{Modal, ModalViewState};
use crate::server::server_api::auth::AuthClient;
use crate::util::truncation::truncate_from_end;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::{
appearance::Appearance,
editor::{EditorView, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions},
view_components::{Dropdown as DropdownView, DropdownItem},
};
use chrono::Utc;
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
use pathfinder_geometry::vector::vec2f;
use galaxy_core::features::FeatureFlag;
use galaxy_server_client::auth::AgentIdentity;
use galaxyui::elements::{
Border, ChildView, ConstrainedBox, Container, CornerRadius, Empty, Fill, Flex,
MouseStateHandle, ParentElement, Radius, Text,
Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
Empty, Expanded, Fill, Flex, FormattedTextElement, HighlightedHyperlink, MainAxisAlignment,
MainAxisSize, MouseStateHandle, OffsetPositioning, Padding, ParentElement,
PositionedElementAnchor, PositionedElementOffsetBounds, Radius, SavePosition, Stack, Text,
};
use galaxyui::elements::{CrossAxisAlignment, Expanded, MainAxisAlignment, MainAxisSize, Padding};
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::ui_components::segmented_control::{
@@ -24,15 +17,31 @@ use galaxyui::ui_components::segmented_control::{
use galaxyui::{
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
};
use pathfinder_geometry::vector::vec2f;
use crate::appearance::Appearance;
use crate::editor::{
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions,
TextOptions,
};
use crate::modal::{Modal, ModalViewState};
use crate::util::truncation::truncate_from_end;
use crate::view_components::dropdown::{DROPDOWN_PADDING, TOP_MENU_BAR_HEIGHT};
use crate::view_components::{Dropdown as DropdownView, DropdownItem, FilterableDropdown};
use crate::workspaces::user_workspaces::UserWorkspaces;
const OZ_AGENTS_URL: &str = "https://oz.warp.dev/agents?new=true";
const API_KEY_DOCS_URL: &str =
"https://docs.warp.dev/reference/cli/api-keys/#personal-vs-agent-keys";
const LABEL_FONT_SIZE: f32 = 14.;
const INPUT_WIDTH: f32 = 428.; // 460px - (2 * 16px) padding
const AGENT_DROPDOWN_POSITION_ID: &str = "create_api_key_modal_agent_dropdown";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ApiKeyType {
Personal,
Team,
Agent,
}
impl ApiKeyType {
@@ -44,6 +53,9 @@ impl ApiKeyType {
ApiKeyType::Team => {
"This API key is tied to your team and can make requests on behalf of your team."
}
ApiKeyType::Agent => {
"This API key is tied to an agent and can make requests on behalf of the agent."
}
}
}
}
@@ -51,14 +63,20 @@ impl ApiKeyType {
pub struct CreateApiKeyModal {
name_editor: ViewHandle<EditorView>,
expiration_dropdown: ViewHandle<DropdownView<CreateApiKeyModalAction>>,
agent_dropdown: ViewHandle<FilterableDropdown<CreateApiKeyModalAction>>,
api_key_type_control: ViewHandle<SegmentedControl<ApiKeyType>>,
expiration: ExpirationOption,
cancel_button_mouse_state: MouseStateHandle,
create_button_mouse_state: MouseStateHandle,
create_agent_button_mouse_state: MouseStateHandle,
request_state: RequestState,
raw_key_copied: bool,
raw_key: Option<String>,
has_team: bool,
has_named_agents: bool,
agents: Vec<AgentIdentity>,
selected_agent_uid: Option<String>,
is_loading_agents: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -104,6 +122,8 @@ pub enum CreateApiKeyModalAction {
Create,
CopyRawKey,
SetExpiration(ExpirationOption),
SelectAgent(String),
CreateNewAgent,
}
pub enum CreateApiKeyModalEvent {
@@ -129,6 +149,7 @@ impl CreateApiKeyModal {
let has_team = FeatureFlag::TeamApiKeys.is_enabled()
&& UserWorkspaces::as_ref(ctx).current_team_uid().is_some();
let has_named_agents = FeatureFlag::NamedAgents.is_enabled();
let name_editor = ctx.add_typed_action_view(|ctx| {
let options = SingleLineEditorOptions {
@@ -145,13 +166,22 @@ impl CreateApiKeyModal {
editor
});
// Expiration dropdown
let expiration_dropdown =
ctx.add_typed_action_view(DropdownView::<CreateApiKeyModalAction>::new);
// API key type segmented control
let agent_dropdown =
ctx.add_typed_action_view(FilterableDropdown::<CreateApiKeyModalAction>::new);
agent_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_top_bar_max_width(INPUT_WIDTH);
// Match the open menu width to the rendered top-bar (input) width so
// the dropdown doesn't overhang the search field.
dropdown.set_match_menu_width_to_top_bar(true, ctx);
});
let api_key_type_control = ctx.add_typed_action_view(move |ctx| {
let options = if has_team {
let options = if has_named_agents {
vec![ApiKeyType::Personal, ApiKeyType::Agent]
} else if has_team {
vec![ApiKeyType::Personal, ApiKeyType::Team]
} else {
vec![ApiKeyType::Personal]
@@ -169,6 +199,7 @@ impl CreateApiKeyModal {
label: match key_type {
ApiKeyType::Personal => "Personal".into(),
ApiKeyType::Team => "Team".into(),
ApiKeyType::Agent => "Agent".into(),
},
width_override: Some(55.0),
color: if is_selected {
@@ -191,21 +222,22 @@ impl CreateApiKeyModal {
});
ctx.subscribe_to_view(&api_key_type_control, |me, _, _, ctx| {
let selected = me.api_key_type_control.as_ref(ctx).selected_option();
if selected == ApiKeyType::Agent && me.agents.is_empty() && !me.is_loading_agents {
me.fetch_agents(ctx);
}
ctx.notify();
me.name_editor.update(ctx, |_, ctx| ctx.notify());
});
// Subscribe to UserWorkspaces to update has_team when team membership changes
ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _, _, ctx| {
me.update_has_team(ctx);
});
// Subscribe to editor events for navigation and validation
ctx.subscribe_to_view(&name_editor, |me, _, event, ctx| {
me.handle_name_editor_event(event, ctx);
});
// Populate expiration dropdown items and default selection (90 days)
let default_expiration = ExpirationOption::NinetyDays;
let items: Vec<DropdownItem<CreateApiKeyModalAction>> = ExpirationOption::all()
.into_iter()
@@ -218,7 +250,6 @@ impl CreateApiKeyModal {
.collect();
expiration_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_items(items, ctx);
// Match the input width (460 - 2*16 padding = 428)
dropdown.set_top_bar_max_width(INPUT_WIDTH);
dropdown.set_menu_width(INPUT_WIDTH, ctx);
dropdown.set_selected_by_action(
@@ -230,24 +261,84 @@ impl CreateApiKeyModal {
Self {
name_editor,
expiration_dropdown,
agent_dropdown,
api_key_type_control,
expiration: default_expiration,
cancel_button_mouse_state: Default::default(),
create_button_mouse_state: Default::default(),
create_agent_button_mouse_state: Default::default(),
request_state: RequestState::Idle,
raw_key_copied: false,
raw_key: None,
has_team,
has_named_agents,
agents: Vec::new(),
selected_agent_uid: None,
is_loading_agents: false,
}
}
fn fetch_agents(&mut self, ctx: &mut ViewContext<Self>) {
self.is_loading_agents = true;
ctx.notify();
let auth_client =
crate::server::server_api::ServerApiProvider::as_ref(ctx).get_auth_client();
ctx.spawn(
async move { auth_client.list_agent_identities().await },
|me, res, ctx| {
me.is_loading_agents = false;
match res {
Ok(agents) => {
me.agents = agents;
me.populate_agent_dropdown(ctx);
}
Err(err) => {
log::error!("Failed to load agent identities: {err}");
ctx.emit(CreateApiKeyModalEvent::Error {
message: "Failed to load agents. Please close and try again."
.to_string(),
});
}
}
ctx.notify();
},
);
}
fn populate_agent_dropdown(&mut self, ctx: &mut ViewContext<Self>) {
let items: Vec<DropdownItem<CreateApiKeyModalAction>> = self
.agents
.iter()
.filter(|a| a.available)
.map(|agent| {
DropdownItem::new(
&agent.name,
CreateApiKeyModalAction::SelectAgent(agent.uid.clone()),
)
})
.collect();
self.agent_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_items(items, ctx);
});
}
#[cfg(test)]
pub(crate) fn set_agents_for_test(
&mut self,
agents: Vec<AgentIdentity>,
ctx: &mut ViewContext<Self>,
) {
self.agents = agents;
self.populate_agent_dropdown(ctx);
}
fn create(&mut self, ctx: &mut ViewContext<Self>) {
if self.request_state == RequestState::Pending {
return;
}
let name = self.name_editor.as_ref(ctx).buffer_text(ctx);
// Always allow creation, even with empty name (we'll use a default)
let final_name = if name.trim().is_empty() {
"Warp API Key".to_string()
} else {
@@ -257,7 +348,6 @@ impl CreateApiKeyModal {
self.request_state = RequestState::Pending;
ctx.notify();
// Compute expiration timestamp based on selected option
let expires_at = match self.expiration.days() {
Some(days) => {
let t = Utc::now() + chrono::Duration::days(days);
@@ -266,15 +356,29 @@ impl CreateApiKeyModal {
None => None,
};
// Get team_id if creating for team
let for_team = self.api_key_type_control.as_ref(ctx).selected_option() == ApiKeyType::Team;
let team_id = if for_team {
let selected_type = self.api_key_type_control.as_ref(ctx).selected_option();
let agent_uid = if selected_type == ApiKeyType::Agent {
match &self.selected_agent_uid {
Some(uid) => Some(cynic::Id::new(uid.clone())),
None => {
self.request_state = RequestState::Idle;
ctx.emit(CreateApiKeyModalEvent::Error {
message: "Please select an agent.".to_string(),
});
ctx.notify();
return;
}
}
} else {
None
};
let team_id = if selected_type == ApiKeyType::Team {
let workspaces = UserWorkspaces::as_ref(ctx);
match workspaces.current_team_uid() {
Some(uid) => Some(cynic::Id::new(uid.uid())),
None => {
// Fail fast if the user requested a team key but there is no current team.
// This can happen if the team state changed between render and click.
self.request_state = RequestState::Idle;
ctx.emit(CreateApiKeyModalEvent::Error {
message:
@@ -289,16 +393,14 @@ impl CreateApiKeyModal {
None
};
// Fire mutation via ServerApi AuthClient
let server_api = crate::server::server_api::ServerApiProvider::as_ref(ctx).get();
let auth_client =
crate::server::server_api::ServerApiProvider::as_ref(ctx).get_auth_client();
ctx.spawn(
async move { server_api.create_api_key(final_name, team_id, expires_at).await },
async move { auth_client.create_api_key(final_name, team_id, agent_uid, expires_at).await },
|me, res, ctx| {
match res {
Ok(galaxy_graphql::mutations::generate_api_key::GenerateApiKeyResult::GenerateApiKeyOutput(output)) => {
// Notify parent to append
ctx.emit(CreateApiKeyModalEvent::Created { api_key: output.api_key });
// Switch to success view and show raw key
me.request_state = RequestState::Succeeded;
me.raw_key_copied = false;
me.raw_key = Some(output.raw_api_key);
@@ -328,6 +430,7 @@ impl CreateApiKeyModal {
self.request_state = RequestState::Idle;
self.raw_key_copied = false;
self.raw_key = None;
self.selected_agent_uid = None;
self.name_editor.update(ctx, |editor, ctx| {
editor.clear_buffer_and_reset_undo_stack(ctx);
});
@@ -335,16 +438,22 @@ impl CreateApiKeyModal {
pub fn on_open(&mut self, ctx: &mut ViewContext<Self>) {
ctx.focus(&self.name_editor);
if self.has_named_agents {
self.fetch_agents(ctx);
}
}
fn update_has_team(&mut self, ctx: &mut ViewContext<Self>) {
let new_has_team = FeatureFlag::TeamApiKeys.is_enabled()
&& UserWorkspaces::as_ref(ctx).current_team_uid().is_some();
let new_has_named_agents = FeatureFlag::NamedAgents.is_enabled();
if new_has_team != self.has_team {
if new_has_team != self.has_team || new_has_named_agents != self.has_named_agents {
self.has_team = new_has_team;
// Update the segmented control options
let options = if new_has_team {
self.has_named_agents = new_has_named_agents;
let options = if new_has_named_agents {
vec![ApiKeyType::Personal, ApiKeyType::Agent]
} else if new_has_team {
vec![ApiKeyType::Personal, ApiKeyType::Team]
} else {
vec![ApiKeyType::Personal]
@@ -508,16 +617,34 @@ impl View for CreateApiKeyModal {
match self.request_state {
RequestState::Succeeded => self.render_success_content(app),
_ => {
// Entry form (Idle, Pending, Failed)
let selected_key_type = self.api_key_type_control.as_ref(app).selected_option();
let description_text = Text::new(
selected_key_type.description(),
appearance.ui_font_family(),
LABEL_FONT_SIZE,
)
.with_color(theme.nonactive_ui_text_color().into())
.finish();
let description_text = if selected_key_type == ApiKeyType::Agent {
FormattedTextElement::new(
FormattedText::new([FormattedTextLine::Line(vec![
FormattedTextFragment::plain_text(selected_key_type.description()),
FormattedTextFragment::plain_text(" "),
FormattedTextFragment::hyperlink("Learn more", API_KEY_DOCS_URL),
])]),
LABEL_FONT_SIZE,
appearance.ui_font_family(),
appearance.ui_font_family(),
theme.nonactive_ui_text_color().into(),
HighlightedHyperlink::default(),
)
.with_hyperlink_font_color(theme.accent().into_solid())
.register_default_click_handlers(|url, _, ctx| {
ctx.open_url(&url.url);
})
.finish()
} else {
Text::new(
selected_key_type.description(),
appearance.ui_font_family(),
LABEL_FONT_SIZE,
)
.with_color(theme.nonactive_ui_text_color().into())
.finish()
};
let name_label = Text::new("Name", appearance.ui_font_family(), LABEL_FONT_SIZE)
.with_color(theme.active_ui_text_color().into())
@@ -525,6 +652,10 @@ impl View for CreateApiKeyModal {
let is_pending = self.request_state == RequestState::Pending;
let is_create_disabled = is_pending
|| (selected_key_type == ApiKeyType::Agent
&& (self.selected_agent_uid.is_none() || self.is_loading_agents));
let mut cancel_button_hover = appearance
.ui_builder()
.button(
@@ -558,7 +689,7 @@ impl View for CreateApiKeyModal {
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(CreateApiKeyModalAction::Create);
});
if is_pending {
if is_create_disabled {
create_button_hover = create_button_hover.disable();
}
let create_button = create_button_hover.finish();
@@ -576,9 +707,9 @@ impl View for CreateApiKeyModal {
.finish();
let mut col = Flex::column();
let mut render_agent_dropdown = false;
// Show segmented control only if user has a team
if self.has_team {
if self.has_team || self.has_named_agents {
let type_label =
Text::new("Type", appearance.ui_font_family(), LABEL_FONT_SIZE)
.with_color(theme.active_ui_text_color().into())
@@ -596,6 +727,83 @@ impl View for CreateApiKeyModal {
.with_margin_bottom(24.)
.finish(),
);
if selected_key_type == ApiKeyType::Agent {
let agent_label =
Text::new("Agent", appearance.ui_font_family(), LABEL_FONT_SIZE)
.with_color(theme.active_ui_text_color().into())
.finish();
col.add_child(Container::new(agent_label).with_margin_bottom(4.).finish());
let available_agents: Vec<&AgentIdentity> =
self.agents.iter().filter(|a| a.available).collect();
if !self.is_loading_agents && available_agents.is_empty() {
let empty_text = Text::new(
"No agents available. Create one first.",
appearance.ui_font_family(),
LABEL_FONT_SIZE,
)
.with_color(theme.nonactive_ui_text_color().into())
.finish();
let create_agent_button = appearance
.ui_builder()
.button(
ButtonVariant::Secondary,
self.create_agent_button_mouse_state.clone(),
)
.with_text_label("Create agent".to_string())
.with_style(button_style)
.build()
.on_click(|ctx, _, _| {
ctx.dispatch_typed_action(CreateApiKeyModalAction::CreateNewAgent);
})
.finish();
col.add_child(
Container::new(
Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Container::new(empty_text).with_margin_bottom(8.).finish(),
)
.with_child(create_agent_button)
.finish(),
)
.with_border(Border::all(1.).with_border_fill(theme.outline()))
.with_padding(Padding::uniform(16.))
.with_background(theme.surface_2())
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_margin_bottom(16.)
.finish(),
);
} else {
// The agent list can grow long, so use a FilterableDropdown
// (search input + substring filtering). Its open menu must
// paint above the fields below it (Name/Expiration), so the
// dropdown is hoisted into the modal's outermost Stack as a
// positioned overlay child anchored to this placeholder,
// rather than rendered inline in the column (which would let
// later siblings paint over the open menu).
render_agent_dropdown = true;
col.add_child(
Container::new(
SavePosition::new(
ConstrainedBox::new(Empty::new().finish())
.with_width(INPUT_WIDTH)
.with_height(TOP_MENU_BAR_HEIGHT + (2. * DROPDOWN_PADDING))
.finish(),
AGENT_DROPDOWN_POSITION_ID,
)
.finish(),
)
.with_margin_bottom(16.)
.finish(),
);
}
}
col.add_child(Container::new(name_label).with_margin_bottom(4.).finish());
col.add_child(
ConstrainedBox::new(
@@ -632,7 +840,24 @@ impl View for CreateApiKeyModal {
);
col.add_child(buttons_row);
col.finish()
let mut stack = Stack::new()
.with_constrain_absolute_children()
.with_child(col.finish());
if render_agent_dropdown {
stack.add_positioned_overlay_child(
ConstrainedBox::new(ChildView::new(&self.agent_dropdown).finish())
.with_width(INPUT_WIDTH)
.finish(),
OffsetPositioning::offset_from_save_position_element(
AGENT_DROPDOWN_POSITION_ID,
vec2f(0., 0.),
PositionedElementOffsetBounds::WindowByPosition,
PositionedElementAnchor::TopLeft,
ChildAnchor::TopLeft,
),
);
}
stack.finish()
}
}
}
@@ -666,6 +891,13 @@ impl TypedActionView for CreateApiKeyModal {
self.expiration = *exp;
ctx.notify();
}
CreateApiKeyModalAction::SelectAgent(uid) => {
self.selected_agent_uid = Some(uid.clone());
ctx.notify();
}
CreateApiKeyModalAction::CreateNewAgent => {
ctx.open_url(OZ_AGENTS_URL);
}
}
}
}
@@ -730,3 +962,7 @@ fn api_key_type_control_styles(app: &AppContext) -> UiComponentStyles {
..Default::default()
}
}
#[cfg(test)]
#[path = "create_api_key_modal_tests.rs"]
mod tests;
@@ -0,0 +1,107 @@
use galaxy_core::ui::appearance::Appearance;
use warp_server_client::auth::AgentIdentity;
use warpui::platform::WindowStyle;
use warpui::App;
use super::CreateApiKeyModal;
use crate::auth::AuthStateProvider;
use crate::server::telemetry::context_provider::AppTelemetryContextProvider;
use crate::settings_view::keybindings::KeybindingChangedNotifier;
use crate::test_util::settings::initialize_settings_for_tests;
use crate::vim_registers::VimRegisters;
use crate::workspace::sync_inputs::SyncedInputState;
use crate::workspaces::user_workspaces::UserWorkspaces;
fn agent(uid: &str, name: &str, available: bool) -> AgentIdentity {
AgentIdentity {
uid: uid.to_string(),
name: name.to_string(),
available,
}
}
/// Regression test for the searchable Agent picker in the New API key modal:
/// the agent dropdown is a `FilterableDropdown`, lists only available agents,
/// and filtering by a query narrows the visible list case-insensitively.
#[test]
fn test_agent_dropdown_is_searchable() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.add_singleton_model(AppTelemetryContextProvider::new_context_provider);
app.add_singleton_model(|_| Appearance::mock());
app.add_singleton_model(|_| SyncedInputState::mock());
app.add_singleton_model(|_| VimRegisters::new());
app.add_singleton_model(|_| KeybindingChangedNotifier::mock());
app.add_singleton_model(UserWorkspaces::default_mock);
let (_, view) = app.add_window(WindowStyle::NotStealFocus, CreateApiKeyModal::new);
// Populate the picker with several agents; the unavailable one should be
// excluded from the list entirely.
view.update(&mut app, |modal, ctx| {
modal.set_agents_for_test(
vec![
agent("1", "Default Service Account", true),
agent("2", "Ben's Agent", true),
agent("3", "Server Migration Agent", true),
agent("4", "Unavailable Agent", false),
],
ctx,
);
});
// Only the 3 available agents are listed, and all are visible with no filter.
let total = view.read(&app, |modal, ctx| modal.agent_dropdown.as_ref(ctx).len());
assert_eq!(total, 3, "only available agents should be listed");
let all_visible = view.read(&app, |modal, ctx| {
modal
.agent_dropdown
.as_ref(ctx)
.visible_items_len_for_test(ctx)
});
assert_eq!(all_visible, 3);
// Typing a query filters the list case-insensitively.
view.update(&mut app, |modal, ctx| {
modal.agent_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_filter_query_for_test("BEN", ctx)
});
});
let filtered = view.read(&app, |modal, ctx| {
modal
.agent_dropdown
.as_ref(ctx)
.visible_items_len_for_test(ctx)
});
assert_eq!(filtered, 1, "query should match only \"Ben's Agent\"");
// A non-matching query yields no matches.
view.update(&mut app, |modal, ctx| {
modal.agent_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_filter_query_for_test("zzz", ctx)
});
});
let none = view.read(&app, |modal, ctx| {
modal
.agent_dropdown
.as_ref(ctx)
.visible_items_len_for_test(ctx)
});
assert_eq!(none, 0);
// Clearing the query restores the full list.
view.update(&mut app, |modal, ctx| {
modal.agent_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_filter_query_for_test("", ctx)
});
});
let restored = view.read(&app, |modal, ctx| {
modal
.agent_dropdown
.as_ref(ctx)
.visible_items_len_for_test(ctx)
});
assert_eq!(restored, 3);
})
}
@@ -1,11 +1,11 @@
use crate::server::{ids::ApiKeyUid, server_api::auth::AuthClient};
use galaxy_core::ui::appearance::Appearance;
use galaxyui::{
elements::MouseStateHandle, ui_components::components::UiComponent, AppContext, Element,
Entity, SingletonEntity, TypedActionView, View, ViewContext,
};
use galaxyui::elements::MouseStateHandle;
use galaxyui::ui_components::components::UiComponent;
use galaxyui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext};
use crate::ui_components::{buttons::icon_button, icons::Icon};
use crate::server::ids::ApiKeyUid;
use crate::ui_components::buttons::icon_button;
use crate::ui_components::icons::Icon;
#[derive(PartialEq, Eq)]
enum RequestState {
@@ -45,10 +45,11 @@ impl ExpireApiKeyButton {
self.request_state = RequestState::Pending;
ctx.notify();
let server_api = crate::server::server_api::ServerApiProvider::as_ref(ctx).get();
let auth_client =
crate::server::server_api::ServerApiProvider::as_ref(ctx).get_auth_client();
let uid_for_req = self.key_uid.clone();
ctx.spawn(
async move { server_api.expire_api_key(&uid_for_req).await },
async move { auth_client.expire_api_key(&uid_for_req).await },
move |me, res, ctx| match res {
Ok(
galaxy_graphql::mutations::expire_api_key::ExpireApiKeyResult::ExpireApiKeyOutput(