first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,45 +1,95 @@
|
||||
#![allow(dead_code, unused_imports, unused_variables)]
|
||||
use super::{
|
||||
platform::{
|
||||
CreateApiKeyModal, CreateApiKeyModalEvent, CreateApiKeyModalViewState, ExpireApiKeyButton,
|
||||
ExpireApiKeyButtonEvent,
|
||||
},
|
||||
settings_page::{
|
||||
MatchData, PageType, SettingsPageMeta, SettingsPageViewHandle, SettingsWidget,
|
||||
CONTENT_FONT_SIZE, SUBHEADER_FONT_SIZE,
|
||||
},
|
||||
SettingsSection,
|
||||
};
|
||||
use crate::auth::AuthStateProvider;
|
||||
use crate::server::{ids::ApiKeyUid, server_api::auth::AuthClient};
|
||||
use crate::util::truncation::truncate_from_end;
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
modal::{Modal, ModalEvent, ModalViewState},
|
||||
ui_components::icons::Icon,
|
||||
util::time_format::format_approx_duration_from_now_utc,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Border, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Element, Empty,
|
||||
Expanded, Flex, FormattedTextElement, HighlightedHyperlink, MainAxisSize, MouseStateHandle,
|
||||
Padding, ParentElement, Shrinkable, Text,
|
||||
},
|
||||
fonts::{Properties, Weight},
|
||||
ui_components::{
|
||||
button::ButtonVariant,
|
||||
components::{Coords, UiComponent, UiComponentStyles},
|
||||
},
|
||||
AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_graphql::object_permissions::OwnerType;
|
||||
use galaxy_graphql::queries::api_keys::ApiKeyProperties as GqlApiKeyProperties;
|
||||
use galaxyui::elements::{
|
||||
resizable_state_handle, Align, Border, ChildView, ConstrainedBox, Container,
|
||||
CrossAxisAlignment, DragBarSide, Element, Empty, Expanded, Flex, FormattedTextElement,
|
||||
HighlightedHyperlink, MainAxisSize, MouseStateHandle, Padding, ParentElement, Resizable,
|
||||
ResizableStateHandle, Shrinkable, Text,
|
||||
};
|
||||
use galaxyui::fonts::{Properties, Weight};
|
||||
use galaxyui::text_layout::ClipConfig;
|
||||
use galaxyui::ui_components::button::ButtonVariant;
|
||||
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use galaxyui::{AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle};
|
||||
|
||||
use super::platform::{
|
||||
CreateApiKeyModal, CreateApiKeyModalEvent, CreateApiKeyModalViewState, ExpireApiKeyButton,
|
||||
ExpireApiKeyButtonEvent,
|
||||
};
|
||||
use super::settings_page::{
|
||||
MatchData, PageType, SettingsPageMeta, SettingsPageViewHandle, SettingsWidget,
|
||||
CONTENT_FONT_SIZE, SUBHEADER_FONT_SIZE,
|
||||
};
|
||||
use super::SettingsSection;
|
||||
use crate::appearance::Appearance;
|
||||
use crate::auth::AuthStateProvider;
|
||||
use crate::editor::{
|
||||
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions,
|
||||
TextOptions,
|
||||
};
|
||||
use crate::modal::{Modal, ModalEvent, ModalViewState};
|
||||
use crate::search_bar::SearchBar;
|
||||
use crate::server::ids::ApiKeyUid;
|
||||
use crate::ui_components::icons::Icon;
|
||||
use crate::util::time_format::format_approx_duration_from_now_utc;
|
||||
|
||||
const MODAL_WIDTH: f32 = 460.;
|
||||
const MODAL_HEIGHT: f32 = 320.;
|
||||
const API_KEY_DOCS_URL: &str = "https://docs.warp.dev/reference/cli/api-keys";
|
||||
const API_KEY_NAME_COLUMN_DEFAULT_WIDTH: f32 = 220.;
|
||||
const API_KEY_NAME_COLUMN_MIN_WIDTH: f32 = 120.;
|
||||
const API_KEY_KEY_COLUMN_WIDTH: f32 = 120.;
|
||||
const API_KEY_METADATA_COLUMN_MIN_WIDTH: f32 = 80.;
|
||||
const API_KEY_ACTION_COLUMN_MIN_WIDTH: f32 = 48.;
|
||||
const API_KEY_TABLE_MIN_NON_RESIZABLE_COLUMNS_WIDTH: f32 = API_KEY_KEY_COLUMN_WIDTH
|
||||
+ (API_KEY_METADATA_COLUMN_MIN_WIDTH * 3.)
|
||||
+ API_KEY_ACTION_COLUMN_MIN_WIDTH;
|
||||
const API_KEY_TABLE_MIN_SCOPE_COLUMN_WIDTH: f32 = API_KEY_METADATA_COLUMN_MIN_WIDTH;
|
||||
const API_KEY_TABLE_LAYOUT_SAFETY_PADDING: f32 = 16.;
|
||||
const SETTINGS_SIDEBAR_WIDTH_DEFAULT: f32 = 200.;
|
||||
const SETTINGS_SIDEBAR_WIDTH_WITH_FOOTER: f32 = 248.;
|
||||
const SETTINGS_SECTION_BORDER_WIDTH: f32 = 1.;
|
||||
const SETTINGS_PAGE_HORIZONTAL_PADDING: f32 = 56.;
|
||||
const SETTINGS_PAGE_MAX_CONTENT_WIDTH: f32 = 800.;
|
||||
const API_KEY_SEARCH_BAR_MAX_WIDTH: f32 = 640.;
|
||||
fn settings_sidebar_width_for_platform_page() -> f32 {
|
||||
if FeatureFlag::SettingsFile.is_enabled() {
|
||||
SETTINGS_SIDEBAR_WIDTH_WITH_FOOTER
|
||||
} else {
|
||||
SETTINGS_SIDEBAR_WIDTH_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
fn api_key_table_width_chrome() -> f32 {
|
||||
settings_sidebar_width_for_platform_page()
|
||||
+ SETTINGS_SECTION_BORDER_WIDTH
|
||||
+ SETTINGS_PAGE_HORIZONTAL_PADDING
|
||||
+ API_KEY_TABLE_LAYOUT_SAFETY_PADDING
|
||||
}
|
||||
|
||||
fn api_key_table_min_non_resizable_columns_width(show_scope_column: bool) -> f32 {
|
||||
if show_scope_column {
|
||||
API_KEY_TABLE_MIN_NON_RESIZABLE_COLUMNS_WIDTH + API_KEY_TABLE_MIN_SCOPE_COLUMN_WIDTH
|
||||
} else {
|
||||
API_KEY_TABLE_MIN_NON_RESIZABLE_COLUMNS_WIDTH
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_api_key_name_column_max_width(
|
||||
window_width: f32,
|
||||
min_width: f32,
|
||||
min_non_resizable_columns_width: f32,
|
||||
table_width_chrome: f32,
|
||||
) -> f32 {
|
||||
let available_table_width =
|
||||
(window_width - table_width_chrome).clamp(0., SETTINGS_PAGE_MAX_CONTENT_WIDTH);
|
||||
(available_table_width - min_non_resizable_columns_width).max(min_width)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum PlatformPageViewEvent {
|
||||
@@ -57,6 +107,10 @@ pub struct PlatformPageView {
|
||||
page: PageType<Self>,
|
||||
create_api_key_modal_state: CreateApiKeyModalViewState,
|
||||
api_keys: Vec<APIKeyProperties>,
|
||||
api_key_search_query: String,
|
||||
api_key_search_editor: ViewHandle<EditorView>,
|
||||
api_key_search_bar: ViewHandle<SearchBar>,
|
||||
api_key_table_column_widths: ApiKeyTableColumnWidths,
|
||||
expire_buttons: HashMap<ApiKeyUid, ViewHandle<ExpireApiKeyButton>>,
|
||||
is_loading: bool,
|
||||
documentation_link_highlight: HighlightedHyperlink,
|
||||
@@ -71,10 +125,11 @@ impl PlatformPageView {
|
||||
}
|
||||
|
||||
// Build and send the GraphQL query
|
||||
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.list_api_keys().await },
|
||||
async move { auth_client.list_api_keys().await },
|
||||
|me, res, ctx| {
|
||||
me.is_loading = false;
|
||||
match res {
|
||||
@@ -82,26 +137,9 @@ impl PlatformPageView {
|
||||
me.api_keys = keys
|
||||
.into_iter()
|
||||
.map(|gql_key| {
|
||||
// Ensure the per-key expire button exists
|
||||
let uid = gql_key.uid.into_inner();
|
||||
me.ensure_expire_button_for_key(ctx, uid.clone());
|
||||
let scope = match gql_key.owner_type {
|
||||
galaxy_graphql::object_permissions::OwnerType::User => {
|
||||
ApiKeyScope::Personal
|
||||
}
|
||||
galaxy_graphql::object_permissions::OwnerType::Team => {
|
||||
ApiKeyScope::Team
|
||||
}
|
||||
};
|
||||
APIKeyProperties::new(
|
||||
uid,
|
||||
gql_key.name,
|
||||
gql_key.key_suffix,
|
||||
scope,
|
||||
gql_key.created_at.utc(),
|
||||
gql_key.last_used_at.map(|t| t.utc()),
|
||||
gql_key.expires_at.map(|t| t.utc()),
|
||||
)
|
||||
let ui_key = APIKeyProperties::from(&gql_key);
|
||||
me.ensure_expire_button_for_key(ctx, ui_key.uid.clone());
|
||||
ui_key
|
||||
})
|
||||
.collect();
|
||||
ctx.notify();
|
||||
@@ -120,13 +158,33 @@ impl PlatformPageView {
|
||||
);
|
||||
}
|
||||
pub fn new(ctx: &mut ViewContext<PlatformPageView>) -> Self {
|
||||
// Create the modal body
|
||||
let api_key_search_editor = ctx.add_typed_action_view(|ctx| {
|
||||
let appearance = Appearance::as_ref(ctx);
|
||||
let options = SingleLineEditorOptions {
|
||||
text: TextOptions {
|
||||
font_size_override: Some(appearance.ui_font_size()),
|
||||
font_family_override: Some(appearance.ui_font_family()),
|
||||
..Default::default()
|
||||
},
|
||||
propagate_and_no_op_vertical_navigation_keys:
|
||||
PropagateAndNoOpNavigationKeys::Always,
|
||||
..Default::default()
|
||||
};
|
||||
let mut editor = EditorView::single_line(options, ctx);
|
||||
editor.set_placeholder_text("Search API keys", ctx);
|
||||
editor
|
||||
});
|
||||
ctx.subscribe_to_view(&api_key_search_editor, |me, _, event, ctx| {
|
||||
me.handle_search_editor_event(event, ctx);
|
||||
});
|
||||
|
||||
let api_key_search_bar =
|
||||
ctx.add_typed_action_view(|_| SearchBar::new(api_key_search_editor.clone()));
|
||||
let create_api_key_body = ctx.add_typed_action_view(CreateApiKeyModal::new);
|
||||
ctx.subscribe_to_view(&create_api_key_body, |me, _, event, ctx| {
|
||||
me.handle_create_api_key_modal_event(event, ctx);
|
||||
});
|
||||
|
||||
// Create the modal wrapper
|
||||
let create_api_key_modal_view = ctx.add_typed_action_view(|ctx| {
|
||||
Modal::new(Some("New API key".to_string()), create_api_key_body, ctx)
|
||||
.with_modal_style(UiComponentStyles {
|
||||
@@ -167,6 +225,10 @@ impl PlatformPageView {
|
||||
create_api_key_modal_view,
|
||||
)),
|
||||
api_keys: vec![],
|
||||
api_key_search_query: String::new(),
|
||||
api_key_search_editor,
|
||||
api_key_search_bar,
|
||||
api_key_table_column_widths: ApiKeyTableColumnWidths::default(),
|
||||
expire_buttons: HashMap::new(),
|
||||
is_loading: true,
|
||||
documentation_link_highlight: HighlightedHyperlink::default(),
|
||||
@@ -174,7 +236,6 @@ impl PlatformPageView {
|
||||
}
|
||||
|
||||
fn show_create_api_key_modal(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
// Ensure header reads "New API key" when opening the form
|
||||
self.create_api_key_modal_state
|
||||
.set_title(Some("New API key".to_string()), ctx);
|
||||
self.create_api_key_modal_state.open(ctx);
|
||||
@@ -204,32 +265,14 @@ impl PlatformPageView {
|
||||
self.hide_create_api_key_modal(ctx);
|
||||
}
|
||||
CreateApiKeyModalEvent::Created { api_key } => {
|
||||
// Switch modal header off for success screen
|
||||
self.create_api_key_modal_state
|
||||
.set_title(Some("Save your key".to_string()), ctx);
|
||||
// Append to list locally
|
||||
// Ensure the per-key expire button exists
|
||||
let uid = api_key.uid.clone().into_inner();
|
||||
self.ensure_expire_button_for_key(ctx, uid.clone());
|
||||
|
||||
let scope = match api_key.owner_type {
|
||||
galaxy_graphql::object_permissions::OwnerType::User => ApiKeyScope::Personal,
|
||||
galaxy_graphql::object_permissions::OwnerType::Team => ApiKeyScope::Team,
|
||||
};
|
||||
let ui_key = APIKeyProperties::new(
|
||||
uid,
|
||||
api_key.name.clone(),
|
||||
api_key.key_suffix.clone(),
|
||||
scope,
|
||||
api_key.created_at.utc(),
|
||||
api_key.last_used_at.map(|t| t.utc()),
|
||||
api_key.expires_at.map(|t| t.utc()),
|
||||
);
|
||||
let ui_key = APIKeyProperties::from(api_key);
|
||||
self.ensure_expire_button_for_key(ctx, ui_key.uid.clone());
|
||||
self.api_keys.push(ui_key);
|
||||
ctx.notify();
|
||||
}
|
||||
CreateApiKeyModalEvent::Error { message } => {
|
||||
// Show an error toast with the provided message
|
||||
let window_id = ctx.window_id();
|
||||
crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||
let toast = crate::view_components::DismissibleToast::error(message.clone());
|
||||
@@ -240,6 +283,23 @@ impl PlatformPageView {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_search_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
EditorEvent::Edited(_) => {
|
||||
self.api_key_search_query = self.api_key_search_editor.as_ref(ctx).buffer_text(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
EditorEvent::Escape => {
|
||||
self.api_key_search_query.clear();
|
||||
self.api_key_search_editor.update(ctx, |editor, ctx| {
|
||||
editor.clear_buffer_and_reset_undo_stack(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_modal_content(&self) -> Option<Box<dyn Element>> {
|
||||
if self.create_api_key_modal_state.is_open() {
|
||||
Some(self.create_api_key_modal_state.render())
|
||||
@@ -314,6 +374,7 @@ struct APIKeyProperties {
|
||||
name: String,
|
||||
key_suffix: String,
|
||||
scope: ApiKeyScope,
|
||||
agent_name: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
last_used_at: Option<DateTime<Utc>>,
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
@@ -323,30 +384,79 @@ struct APIKeyProperties {
|
||||
enum ApiKeyScope {
|
||||
Personal,
|
||||
Team,
|
||||
/// Not yet constructed — the server doesn't distinguish agent-scoped keys
|
||||
/// from team keys yet, but the create modal already supports the Agent
|
||||
/// type and the render path needs this variant for display.
|
||||
#[allow(dead_code)]
|
||||
Agent,
|
||||
}
|
||||
|
||||
impl APIKeyProperties {
|
||||
fn new(
|
||||
uid: ApiKeyUid,
|
||||
name: impl Into<String>,
|
||||
key_suffix: impl Into<String>,
|
||||
scope: ApiKeyScope,
|
||||
created_at: DateTime<Utc>,
|
||||
last_used_at: Option<DateTime<Utc>>,
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
) -> Self {
|
||||
fn matches_search_query(&self, query: &str, include_agent_names: bool) -> bool {
|
||||
let query = query.trim();
|
||||
if query.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let needle = query.to_lowercase();
|
||||
self.name.to_lowercase().contains(&needle)
|
||||
|| (include_agent_names
|
||||
&& self
|
||||
.agent_name
|
||||
.as_ref()
|
||||
.is_some_and(|agent_name| agent_name.to_lowercase().contains(&needle)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&GqlApiKeyProperties> for APIKeyProperties {
|
||||
fn from(gql_key: &GqlApiKeyProperties) -> Self {
|
||||
let agent_name = gql_key.agent_info.as_ref().map(|agent| agent.name.clone());
|
||||
let scope = if agent_name.is_some() {
|
||||
ApiKeyScope::Agent
|
||||
} else {
|
||||
match gql_key.owner_type {
|
||||
OwnerType::User => ApiKeyScope::Personal,
|
||||
OwnerType::Team => ApiKeyScope::Team,
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
uid,
|
||||
name: name.into(),
|
||||
key_suffix: key_suffix.into(),
|
||||
uid: gql_key.uid.clone().into_inner(),
|
||||
name: gql_key.name.clone(),
|
||||
key_suffix: gql_key.key_suffix.clone(),
|
||||
scope,
|
||||
created_at,
|
||||
last_used_at,
|
||||
expires_at,
|
||||
agent_name,
|
||||
created_at: gql_key.created_at.utc(),
|
||||
last_used_at: gql_key.last_used_at.map(|t| t.utc()),
|
||||
expires_at: gql_key.expires_at.map(|t| t.utc()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ApiKeyTableColumnWidths {
|
||||
name: ResizableStateHandle,
|
||||
}
|
||||
|
||||
impl Default for ApiKeyTableColumnWidths {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: resizable_state_handle(API_KEY_NAME_COLUMN_DEFAULT_WIDTH),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiKeyTableColumnWidths {
|
||||
fn width(state_handle: &ResizableStateHandle) -> f32 {
|
||||
state_handle
|
||||
.lock()
|
||||
.expect("API key table column width handle should lock")
|
||||
.size()
|
||||
}
|
||||
|
||||
fn name_width(&self) -> f32 {
|
||||
Self::width(&self.name)
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct PlatformPageWidget {
|
||||
create_api_key_button_mouse_state: MouseStateHandle,
|
||||
@@ -417,6 +527,7 @@ impl PlatformPageWidget {
|
||||
Text::new_inline("Oz Cloud API Keys", appearance.ui_font_family(), 16.)
|
||||
.with_style(Properties::default().weight(Weight::Bold))
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.with_clip(ClipConfig::end())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(Shrinkable::new(1.0, Empty::new().finish()).finish())
|
||||
@@ -449,22 +560,61 @@ impl PlatformPageWidget {
|
||||
col.add_child(self.render_zero_state(appearance));
|
||||
}
|
||||
} else {
|
||||
col.add_child(self.render_api_keys_header(appearance));
|
||||
col.add_child(self.render_api_keys_rows(appearance, view, api_keys));
|
||||
col.add_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(ChildView::new(&view.api_key_search_bar).finish())
|
||||
.with_max_width(API_KEY_SEARCH_BAR_MAX_WIDTH)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(16.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
let include_agent_names = FeatureFlag::NamedAgents.is_enabled();
|
||||
let filtered_api_keys: Vec<&APIKeyProperties> = api_keys
|
||||
.iter()
|
||||
.filter(|key| {
|
||||
key.matches_search_query(&view.api_key_search_query, include_agent_names)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if filtered_api_keys.is_empty() {
|
||||
col.add_child(self.render_no_search_results(appearance));
|
||||
} else {
|
||||
col.add_child(self.render_api_keys_header(appearance, view));
|
||||
col.add_child(self.render_api_keys_rows(appearance, view, &filtered_api_keys));
|
||||
}
|
||||
}
|
||||
|
||||
col.finish()
|
||||
}
|
||||
|
||||
fn render_api_keys_header(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
fn render_api_keys_header(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
view: &PlatformPageView,
|
||||
) -> Box<dyn Element> {
|
||||
let table_width_chrome = api_key_table_width_chrome();
|
||||
let show_scope_column =
|
||||
FeatureFlag::TeamApiKeys.is_enabled() || FeatureFlag::NamedAgents.is_enabled();
|
||||
let min_non_resizable_columns_width =
|
||||
api_key_table_min_non_resizable_columns_width(show_scope_column);
|
||||
let mut header_row = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_main_axis_size(MainAxisSize::Max);
|
||||
header_row
|
||||
.add_child(Expanded::new(1., self.render_header_cell(appearance, "Name")).finish());
|
||||
header_row
|
||||
.add_child(Expanded::new(1., self.render_header_cell(appearance, "Key")).finish());
|
||||
if FeatureFlag::TeamApiKeys.is_enabled() {
|
||||
header_row.add_child(self.render_resizable_header_cell(
|
||||
appearance,
|
||||
"Name",
|
||||
view.api_key_table_column_widths.name.clone(),
|
||||
API_KEY_NAME_COLUMN_MIN_WIDTH,
|
||||
min_non_resizable_columns_width,
|
||||
table_width_chrome,
|
||||
));
|
||||
header_row.add_child(
|
||||
ConstrainedBox::new(self.render_header_cell(appearance, "Key"))
|
||||
.with_width(API_KEY_KEY_COLUMN_WIDTH)
|
||||
.finish(),
|
||||
);
|
||||
if show_scope_column {
|
||||
header_row.add_child(
|
||||
Expanded::new(1., self.render_header_cell(appearance, "Scope")).finish(),
|
||||
);
|
||||
@@ -486,11 +636,59 @@ impl PlatformPageWidget {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_resizable_header_cell(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
label: &str,
|
||||
width_handle: ResizableStateHandle,
|
||||
min_width: f32,
|
||||
min_non_resizable_columns_width: f32,
|
||||
table_width_chrome: f32,
|
||||
) -> Box<dyn Element> {
|
||||
let width = width_handle
|
||||
.lock()
|
||||
.expect("API key header width handle should lock")
|
||||
.size();
|
||||
let header_cell = ConstrainedBox::new(
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_child(Expanded::new(1., self.render_header_cell(appearance, label)).finish())
|
||||
.with_child(
|
||||
Container::new(
|
||||
Text::new_inline("⋮", appearance.ui_font_family(), CONTENT_FONT_SIZE)
|
||||
.with_color(appearance.theme().nonactive_ui_detail().into())
|
||||
.finish(),
|
||||
)
|
||||
.with_padding_right(3.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(width)
|
||||
.finish();
|
||||
Resizable::new(width_handle, header_cell)
|
||||
.with_dragbar_side(DragBarSide::Right)
|
||||
.with_bounds_callback(Box::new(move |window_size| {
|
||||
let max_width = compute_api_key_name_column_max_width(
|
||||
window_size.x(),
|
||||
min_width,
|
||||
min_non_resizable_columns_width,
|
||||
table_width_chrome,
|
||||
);
|
||||
(min_width, max_width)
|
||||
}))
|
||||
.on_resize(|ctx, _| {
|
||||
ctx.notify();
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_api_keys_rows(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
view: &PlatformPageView,
|
||||
api_keys: &[APIKeyProperties],
|
||||
api_keys: &[&APIKeyProperties],
|
||||
) -> Box<dyn Element> {
|
||||
let mut col = Flex::column();
|
||||
for key in api_keys.iter() {
|
||||
@@ -508,6 +706,7 @@ impl PlatformPageWidget {
|
||||
)
|
||||
.with_style(Properties::default().weight(Weight::Semibold))
|
||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||
.with_clip(ClipConfig::end())
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(8.))
|
||||
@@ -528,29 +727,28 @@ impl PlatformPageWidget {
|
||||
.expires_at
|
||||
.map(|dt| format!("{}", dt.format("%b %-d, %Y")))
|
||||
.unwrap_or_else(|| "Never".to_owned());
|
||||
|
||||
// Truncate long names to keep columns aligned
|
||||
let name_display = truncate_from_end(&key.name, 21);
|
||||
let name_column_width = view.api_key_table_column_widths.name_width();
|
||||
let key_column_width = API_KEY_KEY_COLUMN_WIDTH;
|
||||
let mut row = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_main_axis_size(MainAxisSize::Max);
|
||||
// TODO: use appearance.ui_font_size() instead of hardcoded 12
|
||||
row.add_child(
|
||||
Expanded::new(
|
||||
1.,
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Text::new_inline(name_display, appearance.ui_font_family(), 13.)
|
||||
Text::new_inline(key.name.clone(), appearance.ui_font_family(), 13.)
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.with_clip(ClipConfig::end())
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(8.))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(name_column_width)
|
||||
.finish(),
|
||||
);
|
||||
row.add_child(
|
||||
Expanded::new(
|
||||
1.,
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
format!("wk-**{}", key.key_suffix),
|
||||
@@ -558,17 +756,20 @@ impl PlatformPageWidget {
|
||||
12.,
|
||||
)
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.with_clip(ClipConfig::end())
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(8.))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(key_column_width)
|
||||
.finish(),
|
||||
);
|
||||
if FeatureFlag::TeamApiKeys.is_enabled() {
|
||||
if FeatureFlag::TeamApiKeys.is_enabled() || FeatureFlag::NamedAgents.is_enabled() {
|
||||
let scope_display = match key.scope {
|
||||
ApiKeyScope::Personal => "Personal",
|
||||
ApiKeyScope::Team => "Team",
|
||||
ApiKeyScope::Agent => "Agent",
|
||||
};
|
||||
row.add_child(
|
||||
Expanded::new(
|
||||
@@ -687,6 +888,20 @@ impl PlatformPageWidget {
|
||||
.with_margin_top(80.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_no_search_results(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
Text::new(
|
||||
"No API keys match your search",
|
||||
appearance.ui_font_family(),
|
||||
CONTENT_FONT_SIZE,
|
||||
)
|
||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(24.)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsPageMeta for PlatformPageView {
|
||||
@@ -725,3 +940,7 @@ impl From<ViewHandle<PlatformPageView>> for SettingsPageViewHandle {
|
||||
SettingsPageViewHandle::OzCloudAPIKeys(view_handle)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "platform_page_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user