diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index abebcb6..4e074bd 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -16,6 +16,7 @@ mod notes_markdown; mod plugin_catalog; mod plugin_details; mod plugins; +mod privacy_data_inventory; mod privacy_security; mod profiles; mod reading_list; diff --git a/crates/ely_app/src/shell/internal_pages/privacy_data_inventory.rs b/crates/ely_app/src/shell/internal_pages/privacy_data_inventory.rs new file mode 100644 index 0000000..90a95cb --- /dev/null +++ b/crates/ely_app/src/shell/internal_pages/privacy_data_inventory.rs @@ -0,0 +1,127 @@ +use ely_browser_core::{BrowserSnapshot, LocalDataInventory}; +use ely_design_system::colors; +use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, rgb}; +use gpui_component::{IconName, StyledExt}; + +pub(super) fn render_local_data_inventory(snapshot: &BrowserSnapshot) -> AnyElement { + let inventory = snapshot.local_data_inventory; + + div() + .rounded_md() + .border_1() + .border_color(rgb(colors::HAIRLINE)) + .bg(rgb(colors::CANVAS_SOFT)) + .px_4() + .py_3() + .flex() + .flex_col() + .gap_3() + .child(render_inventory_header(snapshot, inventory)) + .child(render_inventory_rows(inventory)) + .into_any_element() +} + +fn render_inventory_header( + snapshot: &BrowserSnapshot, + inventory: LocalDataInventory, +) -> AnyElement { + div() + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .items_center() + .gap_3() + .child(div().text_color(rgb(colors::PRIMARY)).child(IconName::Inspector)) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .text_color(rgb(colors::INK)) + .child("Local Data"), + ) + .child(div().text_xs().truncate().text_color(rgb(colors::MUTED)).child( + format!( + "{} Profile inventory for review, export, and deletion.", + snapshot.active_profile_name + ), + )), + ), + ) + .child( + div() + .flex_none() + .rounded_md() + .border_1() + .border_color(rgb(colors::HAIRLINE)) + .bg(rgb(colors::CANVAS)) + .px_3() + .py_2() + .text_xs() + .font_semibold() + .text_color(rgb(colors::INK)) + .child(format!("{} items", inventory.total_items())), + ) + .into_any_element() +} + +fn render_inventory_rows(inventory: LocalDataInventory) -> AnyElement { + div() + .flex() + .flex_col() + .child(inventory_row(IconName::Globe, "Open Tabs", inventory.open_tabs())) + .child(inventory_row(IconName::Inbox, "Archived Tabs", inventory.archived_tabs())) + .child(inventory_row(IconName::Undo2, "History", inventory.history_entries())) + .child(inventory_row(IconName::BookOpen, "Bookmarks", inventory.bookmarks())) + .child(inventory_row(IconName::File, "Notes", inventory.notes())) + .child(inventory_row(IconName::CircleCheck, "Reading List", inventory.reading_list())) + .child(inventory_row(IconName::Folder, "Downloads", inventory.downloads())) + .child(inventory_row(IconName::Eye, "Site Permissions", inventory.site_permissions())) + .child(inventory_row( + IconName::Inspector, + "Audit Events", + inventory.site_permission_audit_events(), + )) + .into_any_element() +} + +fn inventory_row(icon: IconName, label: &'static str, count: usize) -> AnyElement { + div() + .py_2() + .border_t_1() + .border_color(rgb(colors::HAIRLINE)) + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .items_center() + .gap_3() + .child(div().text_color(rgb(colors::MUTED_SOFT)).child(icon)) + .child( + div().min_w_0().truncate().text_sm().text_color(rgb(colors::INK)).child(label), + ), + ) + .child( + div() + .flex_none() + .text_sm() + .font_semibold() + .text_color(rgb(colors::MUTED)) + .child(count.to_string()), + ) + .into_any_element() +} diff --git a/crates/ely_app/src/shell/internal_pages/privacy_security.rs b/crates/ely_app/src/shell/internal_pages/privacy_security.rs index 82dadf5..87bdb83 100644 --- a/crates/ely_app/src/shell/internal_pages/privacy_security.rs +++ b/crates/ely_app/src/shell/internal_pages/privacy_security.rs @@ -9,7 +9,7 @@ use gpui_component::{ scroll::ScrollableElement, }; -use super::{ElyShell, render_canvas_surface}; +use super::{ElyShell, privacy_data_inventory::render_local_data_inventory, render_canvas_surface}; impl ElyShell { pub(super) fn render_privacy_security_page( @@ -32,6 +32,7 @@ impl ElyShell { .when(snapshot.active_profile_history_entry_count > 0, |this| { this.child(render_history_clear_controls(confirming_clear, cx)) }) + .child(render_local_data_inventory(snapshot)) .child(render_privacy_settings_rows(snapshot, cx)), ) } diff --git a/crates/ely_browser_core/src/lib.rs b/crates/ely_browser_core/src/lib.rs index eff64c8..a0b4a94 100644 --- a/crates/ely_browser_core/src/lib.rs +++ b/crates/ely_browser_core/src/lib.rs @@ -6,6 +6,7 @@ pub use error::CoreError; pub use state::{ BookmarkImportSummary, BrowserCore, BrowserSnapshot, ELYBOOKMARKS_FILE_EXTENSION, ELYBOOKMARKS_SCHEMA_VERSION, ELYSPACE_FILE_EXTENSION, ELYSPACE_SCHEMA_VERSION, - ElyBookmarksPackage, ElySpacePackage, InitialBrowserConfig, InstalledPlugin, PluginAuditAction, - PluginAuditEvent, SiteDataClearance, SpaceImportProfileMapping, TrashedSpace, + ElyBookmarksPackage, ElySpacePackage, InitialBrowserConfig, InstalledPlugin, + LocalDataInventory, PluginAuditAction, PluginAuditEvent, SiteDataClearance, + SpaceImportProfileMapping, TrashedSpace, }; diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 1d7f0f1..90bef8e 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -40,6 +40,7 @@ pub use bookmarks::{ ElyBookmarksPackage, }; pub use plugins::{InstalledPlugin, PluginAuditAction, PluginAuditEvent}; +pub use privacy::LocalDataInventory; pub use site_data::SiteDataClearance; pub use space_exports::{ ELYSPACE_FILE_EXTENSION, ELYSPACE_SCHEMA_VERSION, ElySpacePackage, SpaceImportProfileMapping, @@ -97,6 +98,7 @@ pub struct BrowserSnapshot { pub download_entries: Vec, pub history_entries: Vec, pub active_profile_history_entry_count: usize, + pub local_data_inventory: LocalDataInventory, pub tab_groups: Vec, pub split_layouts: Vec, pub installed_plugins: Vec, @@ -385,6 +387,7 @@ impl BrowserCore { download_entries: self.visible_downloads(), history_entries: self.visible_history(), active_profile_history_entry_count: self.active_profile_history_count(), + local_data_inventory: self.active_profile_local_data_inventory(), tab_groups: self.visible_tab_groups(), split_layouts: self.visible_split_layouts(), installed_plugins: self.installed_plugins.clone(), diff --git a/crates/ely_browser_core/src/state/privacy.rs b/crates/ely_browser_core/src/state/privacy.rs index 251f58c..58e3b30 100644 --- a/crates/ely_browser_core/src/state/privacy.rs +++ b/crates/ely_browser_core/src/state/privacy.rs @@ -2,6 +2,79 @@ use ely_domain::{DiagnosticsReportingPolicy, HistoryRecordingPolicy}; use super::BrowserCore; +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct LocalDataInventory { + open_tabs: usize, + archived_tabs: usize, + bookmarks: usize, + notes: usize, + reading_list: usize, + history_entries: usize, + site_permissions: usize, + site_permission_audit_events: usize, + downloads: usize, +} + +impl LocalDataInventory { + #[must_use] + pub fn open_tabs(self) -> usize { + self.open_tabs + } + + #[must_use] + pub fn archived_tabs(self) -> usize { + self.archived_tabs + } + + #[must_use] + pub fn bookmarks(self) -> usize { + self.bookmarks + } + + #[must_use] + pub fn notes(self) -> usize { + self.notes + } + + #[must_use] + pub fn reading_list(self) -> usize { + self.reading_list + } + + #[must_use] + pub fn history_entries(self) -> usize { + self.history_entries + } + + #[must_use] + pub fn site_permissions(self) -> usize { + self.site_permissions + } + + #[must_use] + pub fn site_permission_audit_events(self) -> usize { + self.site_permission_audit_events + } + + #[must_use] + pub fn downloads(self) -> usize { + self.downloads + } + + #[must_use] + pub fn total_items(self) -> usize { + self.open_tabs + + self.archived_tabs + + self.bookmarks + + self.notes + + self.reading_list + + self.history_entries + + self.site_permissions + + self.site_permission_audit_events + + self.downloads + } +} + impl BrowserCore { pub fn set_history_recording_policy(&mut self, policy: HistoryRecordingPolicy) { self.history_recording_policy = policy; @@ -25,4 +98,49 @@ impl BrowserCore { pub fn diagnostics_reporting_policy(&self) -> DiagnosticsReportingPolicy { self.diagnostics_reporting_policy } + + #[must_use] + pub fn active_profile_local_data_inventory(&self) -> LocalDataInventory { + let profile_id = &self.active_profile_id; + + LocalDataInventory { + open_tabs: self.tabs.iter().filter(|tab| tab.profile_id() == profile_id).count(), + archived_tabs: self + .archived_tabs + .iter() + .filter(|archived| archived.tab().profile_id() == profile_id) + .count(), + bookmarks: self + .bookmarks + .iter() + .filter(|entry| entry.profile_id() == profile_id) + .count(), + notes: self.notes.iter().filter(|entry| entry.profile_id() == profile_id).count(), + reading_list: self + .reading_list + .iter() + .filter(|entry| entry.profile_id() == profile_id) + .count(), + history_entries: self + .history_entries + .iter() + .filter(|entry| entry.profile_id() == profile_id) + .count(), + site_permissions: self + .site_permissions + .iter() + .filter(|entry| entry.profile_id() == profile_id) + .count(), + site_permission_audit_events: self + .site_permission_audit_events + .iter() + .filter(|event| event.profile_id() == profile_id) + .count(), + downloads: self + .download_entries + .iter() + .filter(|entry| entry.profile_id() == profile_id) + .count(), + } + } } diff --git a/crates/ely_browser_core/tests/privacy.rs b/crates/ely_browser_core/tests/privacy.rs new file mode 100644 index 0000000..57f1c1e --- /dev/null +++ b/crates/ely_browser_core/tests/privacy.rs @@ -0,0 +1,68 @@ +use std::error::Error; + +use ely_browser_core::{BrowserCore, InitialBrowserConfig}; +use ely_domain::{ProfileKind, SiteOrigin, SitePermissionDecision, SitePermissionFeature, UrlText}; + +#[test] +fn local_data_inventory_counts_active_profile_data() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let default_profile_id = core.snapshot()?.active_profile_id; + + core.open_tab(UrlText::parse("https://example.com/research")?); + core.bookmark_active_tab()?; + core.save_active_url_note("profile note")?; + core.save_active_tab_to_reading_list()?; + core.record_download_started( + UrlText::parse("https://example.com/report.pdf")?, + "report.pdf", + Some(2048), + )?; + core.set_site_permission( + SiteOrigin::parse("https://example.com")?, + SitePermissionFeature::Camera, + SitePermissionDecision::AllowAlways, + )?; + let archived_tab_id = core.open_tab(UrlText::parse("https://servo.org/")?); + core.close_tab(&archived_tab_id)?; + + let personal_profile_id = core.create_profile("Personal", 0xf54e00, ProfileKind::Standard)?; + core.open_tab(UrlText::parse("https://personal.example/research")?); + core.bookmark_active_tab()?; + core.save_active_url_note("personal note")?; + core.record_download_started( + UrlText::parse("https://personal.example/report.pdf")?, + "personal-report.pdf", + Some(4096), + )?; + core.set_site_permission( + SiteOrigin::parse("https://personal.example")?, + SitePermissionFeature::Notifications, + SitePermissionDecision::DenyAlways, + )?; + + core.select_profile(&default_profile_id)?; + let inventory = core.snapshot()?.local_data_inventory; + + assert_eq!(inventory.open_tabs(), 2); + assert_eq!(inventory.archived_tabs(), 1); + assert_eq!(inventory.bookmarks(), 1); + assert_eq!(inventory.notes(), 1); + assert_eq!(inventory.reading_list(), 1); + assert_eq!(inventory.history_entries(), 2); + assert_eq!(inventory.site_permissions(), 1); + assert_eq!(inventory.site_permission_audit_events(), 1); + assert_eq!(inventory.downloads(), 1); + assert_eq!(inventory.total_items(), 11); + + core.select_profile(&personal_profile_id)?; + let personal_inventory = core.snapshot()?.local_data_inventory; + + assert_eq!(personal_inventory.open_tabs(), 2); + assert_eq!(personal_inventory.bookmarks(), 1); + assert_eq!(personal_inventory.notes(), 1); + assert_eq!(personal_inventory.history_entries(), 1); + assert_eq!(personal_inventory.site_permissions(), 1); + assert_eq!(personal_inventory.site_permission_audit_events(), 1); + assert_eq!(personal_inventory.downloads(), 1); + Ok(()) +}