diff --git a/.github/workflows/build_and_publish.yml b/.github/workflows/build_and_publish.yml index ce37132..e3bc421 100644 --- a/.github/workflows/build_and_publish.yml +++ b/.github/workflows/build_and_publish.yml @@ -58,7 +58,9 @@ jobs: if: startsWith(github.ref, 'refs/tags/') with: prerelease: false + draft: true generate_release_notes: true + append_body: true files: | **/*.AppImage **/*.zip diff --git a/Cargo.lock b/Cargo.lock index 85f19da..32102f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,6 +277,25 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -366,6 +385,12 @@ dependencies = [ "shared_child", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "equivalent" version = "1.0.1" @@ -422,6 +447,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.30" @@ -853,7 +884,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -900,6 +931,7 @@ dependencies = [ "copypasta", "csv", "directories", + "fs_extra", "futures", "gtk4", "humansize", @@ -915,6 +947,7 @@ dependencies = [ "serde_json", "sourceview5", "strum", + "sysinfo 0.31.4", "thiserror", "tokio", "tokio-util", @@ -982,7 +1015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -1497,6 +1530,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rdkafka" version = "0.36.2" @@ -1965,7 +2018,21 @@ dependencies = [ "libc", "ntapi", "once_cell", - "windows", + "windows 0.52.0", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", ] [[package]] @@ -2291,7 +2358,7 @@ dependencies = [ "anyhow", "cfg-if", "rustversion", - "sysinfo", + "sysinfo 0.30.11", "time", ] @@ -2491,7 +2558,17 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-core", + "windows-core 0.52.0", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", "windows-targets 0.52.5", ] @@ -2504,6 +2581,49 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 25c7670..eb10a4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "krust" +rust-version = "1.77" version = "0.0.1" edition = "2021" resolver = "2" @@ -15,7 +16,7 @@ anyhow = "1.0.51" gtk = { version = "0.8.1", package = "gtk4", features = ["v4_14"] } adw = { version = "0.6.0", package = "libadwaita", features = ["v1_5"] } relm4 = { version = "0.8.1", features = ["libadwaita", "gnome_46"] } -relm4-components = { version = "0.8.1", features = ["libadwaita"]} +relm4-components = { version = "0.8.1", features = ["libadwaita"] } tokio = { version = "1.37.0", features = ["full"] } tokio-util = "0.7.10" rusqlite = { version = "0.31.0", features = ["bundled", "hooks"] } @@ -23,28 +24,32 @@ sourceview5 = { version = "0.8.0", features = ["v5_4"] } directories = "4.0.1" futures = { version = "0.3.25", default-features = false } serde = { version = "1.0.136", features = ["derive"] } -serde_json = { version= "1.0.79", features = ["preserve_order"] } +serde_json = { version = "1.0.79", features = ["preserve_order"] } ron = "0.8" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-tree = "0.3.0" thiserror = "1.0.58" chrono = { version = "0.4.37", features = ["serde"] } -chrono-tz = { version = "0.9.0", features = [ "filter-by-regex" ] } +chrono-tz = { version = "0.9.0", features = ["filter-by-regex"] } strum = { version = "0.26.2", features = ["derive"] } -rdkafka = { version = "0.36.2", features = [ "cmake-build", "gssapi", "ssl" ] } +rdkafka = { version = "0.36.2", features = ["cmake-build", "gssapi", "ssl"] } csv = "1.3.0" -uuid = { version = "1.8.0", features = ["v4", "fast-rng", "macro-diagnostics"]} -humansize = "2.0.0" +uuid = { version = "1.8.0", features = ["v4", "fast-rng", "macro-diagnostics"] } +humansize = "2.1.3" copypasta = { version = "0.10.1", default-features = true } +sysinfo = "0.31.4" +fs_extra = "1.3.0" [target.'cfg(target_os = "windows")'.dependencies] -sasl2-sys = { version = "0.1.20",features = ["openssl-vendored"]} +sasl2-sys = { version = "0.1.20", features = ["openssl-vendored"] } [build-dependencies] anyhow = "1.0.51" -openssl-src = { version = "300", default-features = false, features = ["force-engine"] } -vergen = { version = "8.3.1", features = ["build","git","gitcl","si"] } +openssl-src = { version = "300", default-features = false, features = [ + "force-engine", +] } +vergen = { version = "8.3.1", features = ["build", "git", "gitcl", "si"] } #glib-build-tools = "0.19.0" [package.metadata.appimage] diff --git a/src/backend/kafka.rs b/src/backend/kafka.rs index 2328c5b..0f2216f 100644 --- a/src/backend/kafka.rs +++ b/src/backend/kafka.rs @@ -5,7 +5,7 @@ use rdkafka::config::{ClientConfig, FromClientConfigAndContext, RDKafkaLogLevel} use rdkafka::consumer::BaseConsumer; use rdkafka::consumer::{Consumer, ConsumerContext}; use rdkafka::error::{KafkaError, KafkaResult}; -use rdkafka::message::Headers; +use rdkafka::message::{Header, Headers, OwnedHeaders}; use rdkafka::producer::{FutureProducer, FutureRecord}; use rdkafka::topic_partition_list::TopicPartitionList; @@ -252,22 +252,26 @@ impl KafkaBackend { .map(|message| async move { // The send operation on the topic returns a future, which will be // completed once the result or failure from Kafka is received. + let mut kheaders = OwnedHeaders::new(); + for h in message.headers.clone().iter() { + let value = h.value.as_ref(); + let key = h.key.as_str(); + let header = Header { key, value }; + kheaders = kheaders.insert(header); + } let delivery_status = producer .send( FutureRecord::to(topic) .partition(message.partition) .payload(&message.value) - .key(&message.key.clone().unwrap_or_default()), - // .headers(OwnedHeaders::new().insert(Header { - // key: "header_key", - // value: Some("header_value"), - // })), + .key(&message.key.clone().unwrap_or_default()) + .headers(kheaders), Duration::from_secs(0), ) .await; // This will be executed when the result is received. - info!("Delivery status for message {:?} received", message); + trace!("Delivery status for message {:?} received", message); delivery_status }) .collect::>(); diff --git a/src/component/connection_page.rs b/src/component/connection_page.rs index 240ff4f..fa316ce 100644 --- a/src/component/connection_page.rs +++ b/src/component/connection_page.rs @@ -89,7 +89,7 @@ impl Component for ConnectionPageModel { #[root] adw::PreferencesDialog { set_title: "Connection", - set_height_request: 500, + set_height_request: 570, add = &adw::PreferencesPage { add = &adw::PreferencesGroup { #[name = "name_entry" ] diff --git a/src/component/messages/messages_tab.rs b/src/component/messages/messages_tab.rs index 1673c74..b21651e 100644 --- a/src/component/messages/messages_tab.rs +++ b/src/component/messages/messages_tab.rs @@ -16,14 +16,16 @@ use relm4::{ }; use relm4_components::simple_combo_box::SimpleComboBox; use tokio_util::sync::CancellationToken; -use tracing::{info, trace, warn}; +use tracing::*; use uuid::Uuid; +use crate::backend::kafka::KafkaBackend; use crate::backend::repository::MessagesSearchOrder; use crate::backend::settings::Settings; use crate::backend::worker::MessagesTotalCounterRequest; use crate::component::settings_dialog::MessagesSortOrder; use crate::component::task_manager::{Task, TaskManagerMsg, TaskVariant, TASK_MANAGER_BROKER}; +use crate::modals::utils::show_error_alert; use crate::{ backend::{ kafka::KafkaFetch, @@ -58,6 +60,8 @@ relm4::new_stateless_action!(pub(super) CopyMessagesAsCsv, MessagesListActionGro relm4::new_stateless_action!(pub(super) CopyMessagesKeyValue, MessagesListActionGroup, "copy-messages-key-value"); relm4::new_stateless_action!(pub(super) CopyMessagesValue, MessagesListActionGroup, "copy-messages-value"); relm4::new_stateless_action!(pub(super) CopyMessagesKey, MessagesListActionGroup, "copy-messages-key"); +relm4::new_stateless_action!(pub(super) ResendMessagesKeyValue, MessagesListActionGroup, "resend-messages-key-value"); +relm4::new_stateless_action!(pub(super) ResendMessagesValue, MessagesListActionGroup, "resend-messages-value"); pub struct MessagesTabModel { token: CancellationToken, @@ -109,6 +113,7 @@ pub enum MessagesTabMsg { ToggleMode(bool), DigitsOnly(f64), CopyMessages(Copy), + ResendMessages(Copy), AddMessages, SetCacheOrder(Option, String), } @@ -118,6 +123,7 @@ pub enum CommandMsg { Data(MessagesResponse), CopyToClipboard(String, String), RefreshTotalCounterResult(String, usize), + MessagesResendResult(String, Option<()>), } const AVAILABLE_PAGE_SIZES: [u16; 7] = [1000, 2000, 5000, 7000, 10000, 20000, 50000]; @@ -137,6 +143,8 @@ impl FactoryComponent for MessagesTabModel { "_Copy key,value" => CopyMessagesKeyValue, "_Copy value" => CopyMessagesValue, "_Copy key" => CopyMessagesKey, + "_Resend message(s) with key/value" => ResendMessagesKeyValue, + "_Resend message(s) with value only" => ResendMessagesValue, } } } @@ -478,10 +486,26 @@ impl FactoryComponent for MessagesTabModel { .send(MessagesTabMsg::CopyMessages(Copy::Key)) .unwrap(); }); + let messages_menu_sender = sender.input_sender().clone(); + let menu_resend_key_value_action = + RelmAction::::new_stateless(move |_| { + messages_menu_sender + .send(MessagesTabMsg::ResendMessages(Copy::KeyValue)) + .unwrap(); + }); + let messages_menu_sender = sender.input_sender().clone(); + let menu_resend_value_action = + RelmAction::::new_stateless(move |_| { + messages_menu_sender + .send(MessagesTabMsg::ResendMessages(Copy::Value)) + .unwrap(); + }); messages_actions.add_action(menu_copy_all_csv_action); messages_actions.add_action(menu_copy_key_value_action); messages_actions.add_action(menu_copy_value_action); messages_actions.add_action(menu_copy_key_action); + messages_actions.add_action(menu_resend_key_value_action); + messages_actions.add_action(menu_resend_value_action); messages_actions.register_for_widget(&messages_popover_menu); let add_messages = MessagesSendDialogModel::builder() @@ -645,9 +669,6 @@ impl FactoryComponent for MessagesTabModel { widgets.cached_controls.set_visible(false); widgets.cached_centered_controls.set_visible(false); widgets.live_centered_controls.set_visible(true); - // widgets.cache_timestamp.set_visible(false); - // widgets.cache_timestamp.set_text(""); - MessagesMode::Live }; } @@ -703,6 +724,42 @@ impl FactoryComponent for MessagesTabModel { } }); } + MessagesTabMsg::ResendMessages(copy) => { + info!("resend selected messages"); + let topic = self.topic.clone().unwrap().name; + let mut selected_items = vec![]; + for i in 0..self.messages_wrapper.selection_model.n_items() { + if self.messages_wrapper.selection_model.is_selected(i) { + let item = self.messages_wrapper.get_visible(i).unwrap(); + selected_items.push(KrustMessage { + headers: item.borrow().headers.clone(), + topic: topic.clone(), + partition: item.borrow().partition, + offset: item.borrow().offset, + key: match copy { + Copy::KeyValue => Some(item.borrow().key.clone()), + _ => None, + }, + value: item.borrow().value.clone(), + timestamp: item.borrow().timestamp, + }); + } + } + selected_items.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap()); + let connection = self.connection.clone().unwrap(); + sender.oneshot_command(async move { + let id = Uuid::new_v4(); + TOASTER_BROKER.send(AppMsg::ShowToast( + id.to_string(), + "Resending...".to_string(), + )); + debug!("sending messages::{:?}", &selected_items); + // Run async background task + let kafka = KafkaBackend::new(&connection); + kafka.send_messages(&topic, &selected_items).await; + CommandMsg::MessagesResendResult(id.to_string(), Some(())) + }); + } MessagesTabMsg::Open(connection, topic) => { let timestamp_format = Settings::read().unwrap_or_default().timestamp_formatter(); let conn_id = &connection.id.unwrap(); @@ -1001,6 +1058,15 @@ impl FactoryComponent for MessagesTabModel { sender: FactorySender, ) { match message { + CommandMsg::MessagesResendResult(task_id, result) => { + if result.is_some() { + info!("messages resent!"); + } else { + let main_window = main_application().active_window().unwrap(); + show_error_alert(&main_window, "Unable to send messages".to_string()); + } + TOASTER_BROKER.send(AppMsg::HideToast(task_id)); + } CommandMsg::Data(messages) => { sender.input(MessagesTabMsg::UpdateMessages(Box::new(messages))) } diff --git a/src/component/task_manager.rs b/src/component/task_manager.rs index 374ea54..361387a 100644 --- a/src/component/task_manager.rs +++ b/src/component/task_manager.rs @@ -322,6 +322,7 @@ impl Component for TaskManagerModel { #[wrap(Some)] set_child = model.sidebar_list_wrapper.view.borrow() -> >k::ListView { set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, }, } } @@ -438,6 +439,7 @@ impl Component for TaskManagerModel { ) { match message { TaskManagerCommand::RemoveTask(task) => { + debug!("TaskManagerCommand::RemoveTask[{:?}]", task); let maybe_index = self .sidebar_list_wrapper .find(|t| t.variant == task.variant); @@ -463,6 +465,7 @@ impl Component for TaskManagerModel { } } TaskManagerCommand::RemoveSidebarTask(idx) => { + debug!("TaskManagerCommand::RemoveSidebarTask[{}]", idx); self.sidebar_list_wrapper.remove(idx); widgets.tasks_popover.popdown(); widgets.tasks_button.set_sensitive(false); diff --git a/src/modals/about.rs b/src/modals/about.rs index ad696dd..59115ba 100644 --- a/src/modals/about.rs +++ b/src/modals/about.rs @@ -1,7 +1,11 @@ +use fs_extra::dir::get_size; use gtk::prelude::GtkWindowExt; +use humansize::{format_size, DECIMAL}; use relm4::{adw, gtk, ComponentParts, ComponentSender, SimpleComponent}; +use sysinfo::Disks; +use tracing::*; -use crate::{APP_ID, APP_NAME, VERSION}; +use crate::{Settings, APP_ID, APP_NAME, VERSION}; #[derive(Debug)] pub struct AboutDialog {} @@ -31,7 +35,11 @@ impl SimpleComponent for AboutDialog { .designers(vec!["Miguel A. Baldi Hörlle"]) .hide_on_close(true) .build(); - let ack = &["Adelar Escobar Vieira", "Francivaldo Napoleão Herculano", "Jessica dos Santos Rodrigues"]; + let ack = &[ + "Adelar Escobar Vieira", + "Francivaldo Napoleão Herculano", + "Jessica dos Santos Rodrigues", + ]; about.add_acknowledgement_section(Some("Special thanks to"), ack); about } @@ -49,6 +57,19 @@ impl SimpleComponent for AboutDialog { } fn update_view(&self, dialog: &mut Self::Widgets, _sender: ComponentSender) { + let disks = Disks::new_with_refreshed_list(); + let settings = Settings::read().unwrap_or_default(); + let cache_dir_size = format_size(get_size(settings.cache_dir).unwrap_or(0), DECIMAL); + info!("[DISK] Cache directory size: {}", cache_dir_size); + for disk in disks.list() { + info!( + "[DISK]{:?}: {:?}:{:?} / {}", + disk.name(), + disk.kind(), + disk.mount_point(), + format_size(disk.total_space(), DECIMAL), + ); + } dialog.present(); } }