From 4cfb285a5b8f37542c8b62fbfb016eeaa74b8120 Mon Sep 17 00:00:00 2001 From: Enrico Risa Date: Thu, 9 May 2024 21:33:45 +0200 Subject: [PATCH] feat: boostrap tui --- Cargo.lock | 1043 ++++++++++++++++- Cargo.toml | 3 +- edc-connector-client/src/api/catalog.rs | 4 +- .../src/api/contract_agreement.rs | 4 +- .../src/api/contract_definitions.rs | 10 +- .../src/api/contract_negotiations.rs | 10 +- edc-connector-client/src/api/dataplanes.rs | 14 +- edc-connector-client/src/api/edrs.rs | 6 +- edc-connector-client/src/api/policies.rs | 8 +- .../src/api/transfer_process.rs | 14 +- edc-connector-client/src/types/asset.rs | 14 +- .../src/types/contract_definition.rs | 8 +- .../src/types/data_address.rs | 2 +- edc-connector-client/src/types/dataplane.rs | 97 +- edc-connector-client/src/types/policy.rs | 2 +- edc-connector-client/src/types/properties.rs | 6 +- edc-connector-client/src/types/query.rs | 2 +- .../src/types/transfer_process.rs | 10 - edc-connector-client/tests/common/mod.rs | 15 - edc-connector-client/tests/dataplane-tests.rs | 12 + edc-connector-client/tests/edr-tests.rs | 33 +- .../tests/transfer-process-tests.rs | 83 +- edc-connector-tui/Cargo.toml | 23 + edc-connector-tui/src/app.rs | 286 +++++ edc-connector-tui/src/app/action.rs | 18 + edc-connector-tui/src/app/fetch.rs | 50 + edc-connector-tui/src/app/model.rs | 9 + edc-connector-tui/src/app/msg.rs | 22 + edc-connector-tui/src/components.rs | 211 ++++ edc-connector-tui/src/components/assets.rs | 76 ++ .../src/components/connectors.rs | 102 ++ .../src/components/connectors/msg.rs | 7 + .../src/components/contract_definitions.rs | 59 + edc-connector-tui/src/components/footer.rs | 23 + .../src/components/footer/msg.rs | 1 + edc-connector-tui/src/components/header.rs | 97 ++ .../src/components/header/help.rs | 78 ++ .../src/components/header/msg.rs | 4 + .../src/components/launch_bar.rs | 92 ++ .../src/components/launch_bar/msg.rs | 11 + edc-connector-tui/src/components/policies.rs | 54 + edc-connector-tui/src/components/resources.rs | 196 ++++ .../src/components/resources/msg.rs | 12 + .../src/components/resources/resource.rs | 204 ++++ .../src/components/resources/resource/msg.rs | 6 + edc-connector-tui/src/components/table.rs | 169 +++ edc-connector-tui/src/components/table/msg.rs | 17 + edc-connector-tui/src/config.rs | 60 + edc-connector-tui/src/main.rs | 55 + edc-connector-tui/src/runner.rs | 68 ++ edc-connector-tui/src/types.rs | 3 + edc-connector-tui/src/types/connector.rs | 33 + edc-connector-tui/src/types/info.rs | 43 + edc-connector-tui/src/types/nav.rs | 68 ++ .../vault.properties | 1 + testing/connector/build.gradle.kts | 6 +- testing/connector/gradle/libs.versions.toml | 8 +- 57 files changed, 3336 insertions(+), 236 deletions(-) create mode 100644 edc-connector-client/tests/dataplane-tests.rs create mode 100644 edc-connector-tui/Cargo.toml create mode 100644 edc-connector-tui/src/app.rs create mode 100644 edc-connector-tui/src/app/action.rs create mode 100644 edc-connector-tui/src/app/fetch.rs create mode 100644 edc-connector-tui/src/app/model.rs create mode 100644 edc-connector-tui/src/app/msg.rs create mode 100644 edc-connector-tui/src/components.rs create mode 100644 edc-connector-tui/src/components/assets.rs create mode 100644 edc-connector-tui/src/components/connectors.rs create mode 100644 edc-connector-tui/src/components/connectors/msg.rs create mode 100644 edc-connector-tui/src/components/contract_definitions.rs create mode 100644 edc-connector-tui/src/components/footer.rs create mode 100644 edc-connector-tui/src/components/footer/msg.rs create mode 100644 edc-connector-tui/src/components/header.rs create mode 100644 edc-connector-tui/src/components/header/help.rs create mode 100644 edc-connector-tui/src/components/header/msg.rs create mode 100644 edc-connector-tui/src/components/launch_bar.rs create mode 100644 edc-connector-tui/src/components/launch_bar/msg.rs create mode 100644 edc-connector-tui/src/components/policies.rs create mode 100644 edc-connector-tui/src/components/resources.rs create mode 100644 edc-connector-tui/src/components/resources/msg.rs create mode 100644 edc-connector-tui/src/components/resources/resource.rs create mode 100644 edc-connector-tui/src/components/resources/resource/msg.rs create mode 100644 edc-connector-tui/src/components/table.rs create mode 100644 edc-connector-tui/src/components/table/msg.rs create mode 100644 edc-connector-tui/src/config.rs create mode 100644 edc-connector-tui/src/main.rs create mode 100644 edc-connector-tui/src/runner.rs create mode 100644 edc-connector-tui/src/types.rs create mode 100644 edc-connector-tui/src/types/connector.rs create mode 100644 edc-connector-tui/src/types/info.rs create mode 100644 edc-connector-tui/src/types/nav.rs diff --git a/Cargo.lock b/Cargo.lock index 68302a5..2051bc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,24 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -32,6 +50,42 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" + +[[package]] +name = "arboard" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" +dependencies = [ + "clipboard-win", + "core-graphics", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "windows-sys 0.48.0", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.2.0" @@ -77,18 +131,54 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.94" @@ -101,6 +191,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.38" @@ -114,6 +210,28 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "clipboard-win" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +dependencies = [ + "error-code", +] + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -130,6 +248,64 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "darling" version = "0.20.8" @@ -175,6 +351,53 @@ dependencies = [ "serde", ] +[[package]] +name = "derive-new" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "edc-connector-client" version = "0.1.1" @@ -188,6 +411,33 @@ dependencies = [ "uuid", ] +[[package]] +name = "edc-connector-tui" +version = "0.1.0" +dependencies = [ + "anyhow", + "arboard", + "async-trait", + "crossterm", + "dirs-next", + "edc-connector-client", + "enum-ordinalize", + "futures", + "ratatui", + "serde", + "serde_json", + "strum", + "tokio", + "toml", + "tui-textarea", +] + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -197,6 +447,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -213,12 +483,43 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + [[package]] name = "fastrand" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -231,7 +532,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -240,6 +562,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -249,6 +577,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -256,6 +599,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -264,6 +608,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -282,10 +654,26 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", ] [[package]] @@ -335,6 +723,16 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" @@ -348,6 +746,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "1.1.0" @@ -483,6 +890,19 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "num-traits", + "png", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -505,18 +925,39 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.69" @@ -538,6 +979,26 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -560,6 +1021,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown 0.14.3", +] + [[package]] name = "memchr" version = "2.7.2" @@ -572,6 +1042,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -579,6 +1055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -588,6 +1065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -610,29 +1088,150 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.5.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.5.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.5.0", + "block2", + "libc", + "objc2", +] [[package]] -name = "num-traits" -version = "0.2.18" +name = "objc2-metal" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "autocfg", + "bitflags 2.5.0", + "block2", + "objc2", + "objc2-foundation", ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "objc2-quartz-core" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "hermit-abi", - "libc", + "bitflags 2.5.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", ] [[package]] @@ -658,7 +1257,7 @@ checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.5.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -694,6 +1293,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_pipe" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -717,12 +1326,28 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.2.6", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -761,6 +1386,19 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -776,6 +1414,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.36" @@ -785,6 +1432,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" +dependencies = [ + "bitflags 2.5.0", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "itertools", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -794,6 +1461,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "reqwest" version = "0.12.3" @@ -871,6 +1549,12 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +[[package]] +name = "rustversion" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" + [[package]] name = "ryu" version = "1.0.17" @@ -886,6 +1570,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -946,6 +1636,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -988,6 +1687,27 @@ dependencies = [ "syn", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -997,6 +1717,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -1022,12 +1748,50 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.60" @@ -1098,6 +1862,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.36" @@ -1198,6 +1973,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -1246,12 +2055,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tree_magic_mini" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2" +dependencies = [ + "fnv", + "home", + "memchr", + "nom", + "once_cell", + "petgraph", +] + [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-textarea" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e38ced1f941a9cfc923fbf2fe6858443c42cc5220bfd35bdd3648371e7bd8e" +dependencies = [ + "crossterm", + "ratatui", + "unicode-width", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -1273,6 +2107,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + [[package]] name = "url" version = "2.5.0" @@ -1299,6 +2145,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.1" @@ -1380,6 +2232,79 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wayland-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" +dependencies = [ + "bitflags 2.5.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.69" @@ -1390,6 +2315,34 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" @@ -1538,6 +2491,15 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "winnow" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" @@ -1547,3 +2509,60 @@ dependencies = [ "cfg-if", "windows-sys 0.48.0", ] + +[[package]] +name = "wl-clipboard-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" +dependencies = [ + "derive-new", + "libc", + "log", + "nix", + "os_pipe", + "tempfile", + "thiserror", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index c957f85..a66c5d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] resolver="2" members = [ - "edc-connector-client" + "edc-connector-client", + "edc-connector-tui" ] [workspace.dependencies] diff --git a/edc-connector-client/src/api/catalog.rs b/edc-connector-client/src/api/catalog.rs index 54c2431..440e3c0 100644 --- a/edc-connector-client/src/api/catalog.rs +++ b/edc-connector-client/src/api/catalog.rs @@ -15,7 +15,7 @@ impl<'a> CatalogApi<'a> { } pub async fn request(&self, request: &CatalogRequest) -> EdcResult { - let url = format!("{}/v2/catalog/request", self.0.management_url); + let url = format!("{}/v3/catalog/request", self.0.management_url); self.0 .post::<_, WithContext>(url, &WithContextRef::default_context(request)) .await @@ -23,7 +23,7 @@ impl<'a> CatalogApi<'a> { } pub async fn dataset(&self, request: &DatasetRequest) -> EdcResult { - let url = format!("{}/v2/catalog/dataset/request", self.0.management_url); + let url = format!("{}/v3/catalog/dataset/request", self.0.management_url); self.0 .post::<_, WithContext>(url, &WithContextRef::default_context(request)) .await diff --git a/edc-connector-client/src/api/contract_agreement.rs b/edc-connector-client/src/api/contract_agreement.rs index 89718e4..77fa207 100644 --- a/edc-connector-client/src/api/contract_agreement.rs +++ b/edc-connector-client/src/api/contract_agreement.rs @@ -16,7 +16,7 @@ impl<'a> ContractAgreementApi<'a> { } pub async fn get(&self, id: &str) -> EdcResult { - let url = format!("{}/v2/contractagreements/{}", self.0.management_url, id); + let url = format!("{}/v3/contractagreements/{}", self.0.management_url, id); self.0 .get::>(url) .await @@ -24,7 +24,7 @@ impl<'a> ContractAgreementApi<'a> { } pub async fn query(&self, query: Query) -> EdcResult> { - let url = format!("{}/v2/contractagreements/request", self.0.management_url); + let url = format!("{}/v3/contractagreements/request", self.0.management_url); self.0 .post::<_, Vec>>( url, diff --git a/edc-connector-client/src/api/contract_definitions.rs b/edc-connector-client/src/api/contract_definitions.rs index c492c55..34f4f2f 100644 --- a/edc-connector-client/src/api/contract_definitions.rs +++ b/edc-connector-client/src/api/contract_definitions.rs @@ -20,7 +20,7 @@ impl<'a> ContractDefinitionApi<'a> { &self, contract_definition: &NewContractDefinition, ) -> EdcResult> { - let url = format!("{}/v2/contractdefinitions", self.0.management_url); + let url = format!("{}/v3/contractdefinitions", self.0.management_url); self.0 .post::<_, WithContext>>( url, @@ -31,7 +31,7 @@ impl<'a> ContractDefinitionApi<'a> { } pub async fn get(&self, id: &str) -> EdcResult { - let url = format!("{}/v2/contractdefinitions/{}", self.0.management_url, id); + let url = format!("{}/v3/contractdefinitions/{}", self.0.management_url, id); self.0 .get::>(url) .await @@ -39,14 +39,14 @@ impl<'a> ContractDefinitionApi<'a> { } pub async fn update(&self, contract_definition: &ContractDefinition) -> EdcResult<()> { - let url = format!("{}/v2/contractdefinitions", self.0.management_url); + let url = format!("{}/v3/contractdefinitions", self.0.management_url); self.0 .put(url, &WithContextRef::default_context(contract_definition)) .await } pub async fn query(&self, query: Query) -> EdcResult> { - let url = format!("{}/v2/contractdefinitions/request", self.0.management_url); + let url = format!("{}/v3/contractdefinitions/request", self.0.management_url); self.0 .post::<_, Vec>>( url, @@ -57,7 +57,7 @@ impl<'a> ContractDefinitionApi<'a> { } pub async fn delete(&self, id: &str) -> EdcResult<()> { - let url = format!("{}/v2/contractdefinitions/{}", self.0.management_url, id); + let url = format!("{}/v3/contractdefinitions/{}", self.0.management_url, id); self.0.del(url).await } } diff --git a/edc-connector-client/src/api/contract_negotiations.rs b/edc-connector-client/src/api/contract_negotiations.rs index 159464f..ba2da9e 100644 --- a/edc-connector-client/src/api/contract_negotiations.rs +++ b/edc-connector-client/src/api/contract_negotiations.rs @@ -23,7 +23,7 @@ impl<'a> ContractNegotiationApi<'a> { &self, contract_request: &ContractRequest, ) -> EdcResult> { - let url = format!("{}/v2/contractnegotiations", self.0.management_url); + let url = format!("{}/v3/contractnegotiations", self.0.management_url); self.0 .post::<_, WithContext>>( url, @@ -34,7 +34,7 @@ impl<'a> ContractNegotiationApi<'a> { } pub async fn get(&self, id: &str) -> EdcResult { - let url = format!("{}/v2/contractnegotiations/{}", self.0.management_url, id); + let url = format!("{}/v3/contractnegotiations/{}", self.0.management_url, id); self.0 .get::>(url) .await @@ -42,7 +42,7 @@ impl<'a> ContractNegotiationApi<'a> { } pub async fn get_state(&self, id: &str) -> EdcResult { - let url = format!("{}/v2/contractnegotiations/{}", self.0.management_url, id); + let url = format!("{}/v3/contractnegotiations/{}", self.0.management_url, id); self.0 .get::>(url) .await @@ -51,7 +51,7 @@ impl<'a> ContractNegotiationApi<'a> { pub async fn terminate(&self, id: &str, reason: &str) -> EdcResult<()> { let url = format!( - "{}/v2/contractnegotiations/{}/terminate", + "{}/v3/contractnegotiations/{}/terminate", self.0.management_url, id ); @@ -66,7 +66,7 @@ impl<'a> ContractNegotiationApi<'a> { } pub async fn query(&self, query: Query) -> EdcResult> { - let url = format!("{}/v2/contractnegotiations/request", self.0.management_url); + let url = format!("{}/v3/contractnegotiations/request", self.0.management_url); self.0 .post::<_, Vec>>( url, diff --git a/edc-connector-client/src/api/dataplanes.rs b/edc-connector-client/src/api/dataplanes.rs index 20a1d33..906975a 100644 --- a/edc-connector-client/src/api/dataplanes.rs +++ b/edc-connector-client/src/api/dataplanes.rs @@ -1,9 +1,8 @@ use crate::{ client::EdcConnectorClientInternal, types::{ - context::{WithContext, WithContextRef}, + context::WithContext, dataplane::DataPlaneInstance, - response::IdResponse, }, EdcResult, }; @@ -15,14 +14,11 @@ impl<'a> DataPlaneApi<'a> { DataPlaneApi(client) } - pub async fn register(&self, data_plane: &DataPlaneInstance) -> EdcResult> { - let url = format!("{}/v2/dataplanes", self.0.management_url); + pub async fn list(&self) -> EdcResult> { + let url = format!("{}/v3/dataplanes", self.0.management_url); self.0 - .post::<_, WithContext>>( - url, - &WithContextRef::default_context(data_plane), - ) + .get::>>(url) .await - .map(|ctx| ctx.inner) + .map(|results| results.into_iter().map(|ctx| ctx.inner).collect()) } } diff --git a/edc-connector-client/src/api/edrs.rs b/edc-connector-client/src/api/edrs.rs index e31bbbe..9f432e2 100644 --- a/edc-connector-client/src/api/edrs.rs +++ b/edc-connector-client/src/api/edrs.rs @@ -37,7 +37,7 @@ impl<'a> EdrApi<'a> { } pub async fn get_data_address(&self, id: &str) -> EdcResult { - let url = format!("{}/v2/edrs/{}/dataaddress", self.0.management_url, id); + let url = format!("{}/v3/edrs/{}/dataaddress", self.0.management_url, id); self.0 .get::>(url) .await @@ -45,7 +45,7 @@ impl<'a> EdrApi<'a> { } pub async fn query(&self, query: Query) -> EdcResult> { - let url = format!("{}/v1/edrs/request", self.0.management_url); + let url = format!("{}/v3/edrs/request", self.0.management_url); self.0 .post::<_, Vec>>( url, @@ -56,7 +56,7 @@ impl<'a> EdrApi<'a> { } pub async fn delete(&self, id: &str) -> EdcResult<()> { - let url = format!("{}/v1/edrs/{}", self.0.management_url, id); + let url = format!("{}/v3/edrs/{}", self.0.management_url, id); self.0.del(url).await } } diff --git a/edc-connector-client/src/api/policies.rs b/edc-connector-client/src/api/policies.rs index 7b42512..ab05ec3 100644 --- a/edc-connector-client/src/api/policies.rs +++ b/edc-connector-client/src/api/policies.rs @@ -20,7 +20,7 @@ impl<'a> PolicyApi<'a> { &self, policy_definition: &NewPolicyDefinition, ) -> EdcResult> { - let url = format!("{}/v2/policydefinitions", self.0.management_url); + let url = format!("{}/v3/policydefinitions", self.0.management_url); self.0 .post::<_, WithContext>>( url, @@ -31,7 +31,7 @@ impl<'a> PolicyApi<'a> { } pub async fn get(&self, id: &str) -> EdcResult { - let url = format!("{}/v2/policydefinitions/{}", self.0.management_url, id); + let url = format!("{}/v3/policydefinitions/{}", self.0.management_url, id); self.0 .get::>(url) .await @@ -50,7 +50,7 @@ impl<'a> PolicyApi<'a> { } pub async fn query(&self, query: Query) -> EdcResult> { - let url = format!("{}/v2/policydefinitions/request", self.0.management_url); + let url = format!("{}/v3/policydefinitions/request", self.0.management_url); self.0 .post::<_, Vec>>( url, @@ -61,7 +61,7 @@ impl<'a> PolicyApi<'a> { } pub async fn delete(&self, id: &str) -> EdcResult<()> { - let url = format!("{}/v2/policydefinitions/{}", self.0.management_url, id); + let url = format!("{}/v3/policydefinitions/{}", self.0.management_url, id); self.0.del(url).await } } diff --git a/edc-connector-client/src/api/transfer_process.rs b/edc-connector-client/src/api/transfer_process.rs index f859812..e6143ba 100644 --- a/edc-connector-client/src/api/transfer_process.rs +++ b/edc-connector-client/src/api/transfer_process.rs @@ -23,7 +23,7 @@ impl<'a> TransferProcessApi<'a> { &self, transfer_request: &TransferRequest, ) -> EdcResult> { - let url = format!("{}/v2/transferprocesses", self.0.management_url); + let url = format!("{}/v3/transferprocesses", self.0.management_url); self.0 .post::<_, WithContext>>( url, @@ -34,7 +34,7 @@ impl<'a> TransferProcessApi<'a> { } pub async fn get(&self, id: &str) -> EdcResult { - let url = format!("{}/v2/transferprocesses/{}", self.0.management_url, id); + let url = format!("{}/v3/transferprocesses/{}", self.0.management_url, id); self.0 .get::>(url) .await @@ -42,7 +42,7 @@ impl<'a> TransferProcessApi<'a> { } pub async fn get_state(&self, id: &str) -> EdcResult { - let url = format!("{}/v2/transferprocesses/{}", self.0.management_url, id); + let url = format!("{}/v3/transferprocesses/{}", self.0.management_url, id); self.0 .get::>(url) .await @@ -50,7 +50,7 @@ impl<'a> TransferProcessApi<'a> { } pub async fn query(&self, query: Query) -> EdcResult> { - let url = format!("{}/v2/transferprocesses/request", self.0.management_url); + let url = format!("{}/v3/transferprocesses/request", self.0.management_url); self.0 .post::<_, Vec>>( url, @@ -62,7 +62,7 @@ impl<'a> TransferProcessApi<'a> { pub async fn terminate(&self, id: &str, reason: &str) -> EdcResult<()> { let url = format!( - "{}/v2/transferprocesses/{}/terminate", + "{}/v3/transferprocesses/{}/terminate", self.0.management_url, id ); @@ -78,7 +78,7 @@ impl<'a> TransferProcessApi<'a> { pub async fn suspend(&self, id: &str, reason: &str) -> EdcResult<()> { let url = format!( - "{}/v2/transferprocesses/{}/suspend", + "{}/v3/transferprocesses/{}/suspend", self.0.management_url, id ); @@ -94,7 +94,7 @@ impl<'a> TransferProcessApi<'a> { pub async fn resume(&self, id: &str) -> EdcResult<()> { let url = format!( - "{}/v2/transferprocesses/{}/resume", + "{}/v3/transferprocesses/{}/resume", self.0.management_url, id ); diff --git a/edc-connector-client/src/types/asset.rs b/edc-connector-client/src/types/asset.rs index 009e08c..80857f1 100644 --- a/edc-connector-client/src/types/asset.rs +++ b/edc-connector-client/src/types/asset.rs @@ -7,7 +7,7 @@ use super::{ properties::{FromValue, Properties, PropertyValue, ToValue}, }; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Asset { #[serde(rename = "@id")] @@ -55,6 +55,18 @@ where { pub fn id(&self) -> &str { &self.id } + + pub fn properties(&self) -> &Properties { + &self.properties + } + + pub fn private_properties(&self) -> &Properties { + &self.private_properties + } + + pub fn data_address(&self) -> &DataAddress { + &self.data_address + } } #[derive(Default)] diff --git a/edc-connector-client/src/types/contract_definition.rs b/edc-connector-client/src/types/contract_definition.rs index aa900a3..97afb1a 100644 --- a/edc-connector-client/src/types/contract_definition.rs +++ b/edc-connector-client/src/types/contract_definition.rs @@ -1,19 +1,21 @@ -use serde::{Deserialize, Serialize}; - use crate::{BuilderError, ConversionError}; +use serde::{Deserialize, Serialize}; +use serde_with::{formats::PreferMany, serde_as, OneOrMany}; use super::{ properties::{FromValue, Properties, ToValue}, query::Criterion, }; -#[derive(Debug, Serialize, Deserialize)] +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ContractDefinition { #[serde(rename = "@id")] id: String, access_policy_id: String, contract_policy_id: String, + #[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")] #[serde(default)] assets_selector: Vec, #[serde(default)] diff --git a/edc-connector-client/src/types/data_address.rs b/edc-connector-client/src/types/data_address.rs index a9baa8a..d190523 100644 --- a/edc-connector-client/src/types/data_address.rs +++ b/edc-connector-client/src/types/data_address.rs @@ -4,7 +4,7 @@ use crate::{error::BuilderError, ConversionError}; use super::properties::{FromValue, Properties, ToValue}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct DataAddress(Properties); impl DataAddress { diff --git a/edc-connector-client/src/types/dataplane.rs b/edc-connector-client/src/types/dataplane.rs index 924d564..b8f58fc 100644 --- a/edc-connector-client/src/types/dataplane.rs +++ b/edc-connector-client/src/types/dataplane.rs @@ -1,86 +1,63 @@ -use std::collections::HashSet; +use serde::Deserialize; +use serde_with::{formats::PreferMany, serde_as, OneOrMany}; -use serde::{Deserialize, Serialize}; +use super::properties::Properties; -use crate::BuilderError; - -use super::properties::{Properties, ToValue}; - -#[derive(Debug, Deserialize, Serialize)] +#[serde_as] +#[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DataPlaneInstance { #[serde(rename = "@id")] id: String, url: String, - allowed_source_types: HashSet, - allowed_dest_types: HashSet, - allowed_transfer_types: HashSet, + #[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")] + allowed_source_types: Vec, + #[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")] + allowed_dest_types: Vec, + #[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")] + allowed_transfer_types: Vec, + state: DataPlaneInstanceState, + #[serde(default)] properties: Properties, } impl DataPlaneInstance { - pub fn builder() -> DataPlaneInstanceBuilder { - DataPlaneInstanceBuilder::default() + pub fn id(&self) -> &str { + &self.id } -} - -#[derive(Debug, Default)] -pub struct DataPlaneInstanceBuilder { - id: Option, - url: Option, - properties: Properties, - allowed_source_types: HashSet, - allowed_dest_types: HashSet, - allowed_transfer_types: HashSet, -} -impl DataPlaneInstanceBuilder { - pub fn id(mut self, id: &str) -> Self { - self.id = Some(id.to_string()); - self + pub fn url(&self) -> &str { + &self.url } - pub fn url(mut self, url: &str) -> Self { - self.url = Some(url.to_string()); - self + pub fn allowed_source_types(&self) -> &Vec { + &self.allowed_source_types } - pub fn property(mut self, property: &str, value: T) -> Self - where - T: ToValue, - { - self.properties.set(property, value); - self + pub fn allowed_dest_types(&self) -> &Vec { + &self.allowed_dest_types } - pub fn allowed_destination_type(mut self, dest_type: &str) -> Self { - self.allowed_dest_types.insert(dest_type.to_string()); - self + pub fn allowed_transfer_types(&self) -> &Vec { + &self.allowed_transfer_types } - pub fn allowed_source_type(mut self, source_type: &str) -> Self { - self.allowed_source_types.insert(source_type.to_string()); - self + pub fn state(&self) -> &DataPlaneInstanceState { + &self.state } - pub fn allowed_transfer_type(mut self, transfer_type: &str) -> Self { - self.allowed_transfer_types - .insert(transfer_type.to_string()); - self + pub fn properties(&self) -> &Properties { + &self.properties } +} - pub fn build(self) -> Result { - Ok(DataPlaneInstance { - id: self - .id - .ok_or_else(|| BuilderError::missing_property("id"))?, - url: self - .url - .ok_or_else(|| BuilderError::missing_property("url"))?, - allowed_source_types: self.allowed_source_types, - allowed_dest_types: self.allowed_dest_types, - allowed_transfer_types: self.allowed_transfer_types, - properties: self.properties, - }) - } +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DataPlaneInstanceState { + Available, + Registered, + Unavailable, + Unregistered, + #[serde(untagged)] + Other(String), } diff --git a/edc-connector-client/src/types/policy.rs b/edc-connector-client/src/types/policy.rs index 665eae9..0bff2ef 100644 --- a/edc-connector-client/src/types/policy.rs +++ b/edc-connector-client/src/types/policy.rs @@ -7,7 +7,7 @@ use crate::{BuilderError, ConversionError}; use super::properties::{FromValue, Properties, PropertyValue, ToValue}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct PolicyDefinition { #[serde(rename = "@id")] diff --git a/edc-connector-client/src/types/properties.rs b/edc-connector-client/src/types/properties.rs index 4c70ff8..7afb823 100644 --- a/edc-connector-client/src/types/properties.rs +++ b/edc-connector-client/src/types/properties.rs @@ -9,7 +9,7 @@ use crate::error::ConversionError; pub use self::conversion::{FromValue, ToValue}; -#[derive(Debug, Serialize, Deserialize, Default)] +#[derive(Debug, Serialize, Deserialize, Default, Clone)] pub struct Properties(HashMap); #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] @@ -30,6 +30,10 @@ impl Properties { self.0.get(property) } + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + pub(crate) fn set(&mut self, property: &str, value: T) where T: ToValue, diff --git a/edc-connector-client/src/types/query.rs b/edc-connector-client/src/types/query.rs index d397979..b491519 100644 --- a/edc-connector-client/src/types/query.rs +++ b/edc-connector-client/src/types/query.rs @@ -74,7 +74,7 @@ pub enum SortOrder { Desc, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Criterion { operand_left: String, diff --git a/edc-connector-client/src/types/transfer_process.rs b/edc-connector-client/src/types/transfer_process.rs index 80490a8..729ea9b 100644 --- a/edc-connector-client/src/types/transfer_process.rs +++ b/edc-connector-client/src/types/transfer_process.rs @@ -14,7 +14,6 @@ pub struct TransferRequest { protocol: String, counter_party_address: String, contract_id: String, - asset_id: String, transfer_type: String, data_destination: Option, callback_addresses: Vec, @@ -32,7 +31,6 @@ pub struct TransferRequestBuilder { counter_party_address: Option, contract_id: Option, transfer_type: Option, - asset_id: Option, data_destination: Option, callback_addresses: Vec, } @@ -58,11 +56,6 @@ impl TransferRequestBuilder { self } - pub fn asset_id(mut self, asset_id: &str) -> Self { - self.asset_id = Some(asset_id.to_string()); - self - } - pub fn destination(mut self, destination: DataAddress) -> Self { self.data_destination = Some(destination); self @@ -84,9 +77,6 @@ impl TransferRequestBuilder { contract_id: self .contract_id .ok_or_else(|| BuilderError::missing_property("contract_id"))?, - asset_id: self - .asset_id - .ok_or_else(|| BuilderError::missing_property("asset_id"))?, transfer_type: self .transfer_type .ok_or_else(|| BuilderError::missing_property("transfer_type"))?, diff --git a/edc-connector-client/tests/common/mod.rs b/edc-connector-client/tests/common/mod.rs index 85137c8..5005560 100644 --- a/edc-connector-client/tests/common/mod.rs +++ b/edc-connector-client/tests/common/mod.rs @@ -9,7 +9,6 @@ use edc_connector_client::{ contract_definition::NewContractDefinition, contract_negotiation::{ContractNegotiationState, ContractRequest}, data_address::DataAddress, - dataplane::DataPlaneInstance, policy::{NewPolicyDefinition, Policy, PolicyKind}, query::Criterion, transfer_process::{TransferProcessState, TransferRequest}, @@ -192,7 +191,6 @@ pub async fn seed_transfer_process( let request = TransferRequest::builder() .counter_party_address(PROVIDER_PROTOCOL) .contract_id(&agreement_id) - .asset_id(&asset_id) .transfer_type("HttpData-PULL") .destination(DataAddress::builder().kind("HttpProxy").build().unwrap()) .build() @@ -212,19 +210,6 @@ pub async fn seed_transfer_process( ) } -pub async fn seed_data_plane(client: &EdcConnectorClient, id: &str, url: &str) { - let dataplane = DataPlaneInstance::builder() - .id(id) - .url(url) - .allowed_transfer_type("HttpData-PULL") - .allowed_source_type("HttpData") - .allowed_destination_type("HttpProxy") - .property("publicApiUrl", "http://provider-connector:9291/public/") - .build() - .unwrap(); - client.data_planes().register(&dataplane).await.unwrap(); -} - pub async fn wait_for_negotiation_state( client: &EdcConnectorClient, id: &str, diff --git a/edc-connector-client/tests/dataplane-tests.rs b/edc-connector-client/tests/dataplane-tests.rs new file mode 100644 index 0000000..4e38b6a --- /dev/null +++ b/edc-connector-client/tests/dataplane-tests.rs @@ -0,0 +1,12 @@ +use common::setup_provider_client; + +mod common; + +#[tokio::test] +async fn should_fetch_dataplanes() { + let client = setup_provider_client(); + + let response = client.data_planes().list().await.unwrap(); + assert!(response.len() > 0); + +} diff --git a/edc-connector-client/tests/edr-tests.rs b/edc-connector-client/tests/edr-tests.rs index 0a457d7..77ce5d9 100644 --- a/edc-connector-client/tests/edr-tests.rs +++ b/edc-connector-client/tests/edr-tests.rs @@ -4,23 +4,14 @@ mod get { use edc_connector_client::types::transfer_process::TransferProcessState; - use crate::common::{ - seed_data_plane, setup_consumer_client, setup_provider_client, wait_for_transfer_state, - }; use crate::common::{seed_transfer_process, wait_for}; + use crate::common::{setup_consumer_client, setup_provider_client, wait_for_transfer_state}; #[tokio::test] async fn should_receive_an_edr_in_cache() { let provider = setup_provider_client(); let consumer = setup_consumer_client(); - seed_data_plane( - &provider, - "dataplane", - "http://provider-connector:9192/control/transfer", - ) - .await; - let (transfer_process_id, agreement_id, _, asset_id) = seed_transfer_process(&consumer, &provider).await; @@ -45,23 +36,14 @@ mod query { use edc_connector_client::types::query::Query; use edc_connector_client::types::transfer_process::TransferProcessState; - use crate::common::{ - seed_data_plane, setup_consumer_client, setup_provider_client, wait_for_transfer_state, - }; use crate::common::{seed_transfer_process, wait_for}; + use crate::common::{setup_consumer_client, setup_provider_client, wait_for_transfer_state}; #[tokio::test] async fn should_query_the_edr_cache() { let provider = setup_provider_client(); let consumer = setup_consumer_client(); - seed_data_plane( - &provider, - "dataplane", - "http://provider-connector:9192/control/transfer", - ) - .await; - let (transfer_process_id, _, _, asset_id) = seed_transfer_process(&consumer, &provider).await; @@ -91,23 +73,14 @@ mod delete { use edc_connector_client::{Error, ManagementApiError, ManagementApiErrorDetailKind}; use reqwest::StatusCode; - use crate::common::{ - seed_data_plane, setup_consumer_client, setup_provider_client, wait_for_transfer_state, - }; use crate::common::{seed_transfer_process, wait_for}; + use crate::common::{setup_consumer_client, setup_provider_client, wait_for_transfer_state}; #[tokio::test] async fn should_delete_a_cached_edr() { let provider = setup_provider_client(); let consumer = setup_consumer_client(); - seed_data_plane( - &provider, - "dataplane", - "http://provider-connector:9192/control/transfer", - ) - .await; - let (transfer_process_id, _, _, _) = seed_transfer_process(&consumer, &provider).await; wait_for_transfer_state( diff --git a/edc-connector-client/tests/transfer-process-tests.rs b/edc-connector-client/tests/transfer-process-tests.rs index 127d055..c7dced2 100644 --- a/edc-connector-client/tests/transfer-process-tests.rs +++ b/edc-connector-client/tests/transfer-process-tests.rs @@ -1,16 +1,19 @@ mod common; mod initiate { - use edc_connector_client::types::{ - data_address::DataAddress, - transfer_process::{TransferProcessState, TransferRequest}, + use edc_connector_client::{ + types::{ + data_address::DataAddress, + transfer_process::{TransferProcessState, TransferRequest}, + }, + Error, ManagementApiError, ManagementApiErrorDetailKind, }; + use reqwest::StatusCode; use uuid::Uuid; use crate::common::seed_contract_agreement; use crate::common::{ - seed_data_plane, setup_consumer_client, setup_provider_client, wait_for_transfer_state, - PROVIDER_PROTOCOL, + setup_consumer_client, setup_provider_client, wait_for_transfer_state, PROVIDER_PROTOCOL, }; #[tokio::test] @@ -18,20 +21,12 @@ mod initiate { let provider = setup_provider_client(); let consumer = setup_consumer_client(); - seed_data_plane( - &provider, - "dataplane", - "http://provider-connector:9192/control/transfer", - ) - .await; - - let (agreement_id, _, asset_id) = seed_contract_agreement(&consumer, &provider).await; + let (agreement_id, _, _) = seed_contract_agreement(&consumer, &provider).await; let request = TransferRequest::builder() .counter_party_address(PROVIDER_PROTOCOL) .contract_id(&agreement_id) .transfer_type("HttpData-PULL") - .asset_id(&asset_id) .destination(DataAddress::builder().kind("HttpProxy").build().unwrap()) .build() .unwrap(); @@ -54,19 +49,20 @@ mod initiate { let request = TransferRequest::builder() .counter_party_address(PROVIDER_PROTOCOL) .contract_id(&Uuid::new_v4().to_string()) - .asset_id(&Uuid::new_v4().to_string()) .transfer_type("HttpData-PULL") .destination(DataAddress::builder().kind("HttpProxy").build().unwrap()) .build() .unwrap(); - let response = consumer - .transfer_processes() - .initiate(&request) - .await - .unwrap(); + let response = consumer.transfer_processes().initiate(&request).await; - wait_for_transfer_state(&consumer, response.id(), TransferProcessState::Terminated).await; + assert!(matches!( + response, + Err(Error::ManagementApi(ManagementApiError { + status_code: StatusCode::BAD_REQUEST, + error_detail: ManagementApiErrorDetailKind::Parsed(..) + })) + )) } } @@ -80,8 +76,7 @@ mod get { use crate::common::seed_contract_agreement; use crate::common::{ - seed_data_plane, setup_consumer_client, setup_provider_client, wait_for_transfer_state, - PROVIDER_PROTOCOL, + setup_consumer_client, setup_provider_client, wait_for_transfer_state, PROVIDER_PROTOCOL, }; #[tokio::test] @@ -89,13 +84,6 @@ mod get { let provider = setup_provider_client(); let consumer = setup_consumer_client(); - seed_data_plane( - &provider, - "dataplane", - "http://provider-connector:9192/control/transfer", - ) - .await; - let (agreement_id, _, asset_id) = seed_contract_agreement(&consumer, &provider).await; let cb = CallbackAddress::builder() @@ -107,7 +95,6 @@ mod get { let request = TransferRequest::builder() .counter_party_address(PROVIDER_PROTOCOL) .contract_id(&agreement_id) - .asset_id(&asset_id) .transfer_type("HttpData-PULL") .callback_address(cb.clone()) .destination(DataAddress::builder().kind("HttpProxy").build().unwrap()) @@ -156,7 +143,7 @@ mod query { }; use crate::common::{ - seed_contract_agreement, seed_data_plane, setup_consumer_client, setup_provider_client, + seed_contract_agreement, setup_consumer_client, setup_provider_client, wait_for_transfer_state, PROVIDER_PROTOCOL, }; @@ -165,19 +152,11 @@ mod query { let provider = setup_provider_client(); let consumer = setup_consumer_client(); - seed_data_plane( - &provider, - "dataplane", - "http://provider-connector:9192/control/transfer", - ) - .await; - let (agreement_id, _, asset_id) = seed_contract_agreement(&consumer, &provider).await; let request = TransferRequest::builder() .counter_party_address(PROVIDER_PROTOCOL) .contract_id(&agreement_id) - .asset_id(&asset_id) .transfer_type("HttpData-PULL") .destination(DataAddress::builder().kind("HttpProxy").build().unwrap()) .build() @@ -211,7 +190,7 @@ mod terminate { }; use crate::common::{ - seed_contract_agreement, seed_data_plane, setup_consumer_client, setup_provider_client, + seed_contract_agreement, setup_consumer_client, setup_provider_client, wait_for_transfer_state, PROVIDER_PROTOCOL, }; @@ -220,19 +199,11 @@ mod terminate { let provider = setup_provider_client(); let consumer = setup_consumer_client(); - seed_data_plane( - &provider, - "dataplane", - "http://provider-connector:9192/control/transfer", - ) - .await; - - let (agreement_id, _, asset_id) = seed_contract_agreement(&consumer, &provider).await; + let (agreement_id, _, _) = seed_contract_agreement(&consumer, &provider).await; let request = TransferRequest::builder() .counter_party_address(PROVIDER_PROTOCOL) .contract_id(&agreement_id) - .asset_id(&asset_id) .transfer_type("HttpData-PULL") .destination(DataAddress::builder().kind("HttpProxy").build().unwrap()) .build() @@ -266,7 +237,7 @@ mod suspend { }; use crate::common::{ - seed_contract_agreement, seed_data_plane, setup_consumer_client, setup_provider_client, + seed_contract_agreement, setup_consumer_client, setup_provider_client, wait_for_transfer_state, PROVIDER_PROTOCOL, }; @@ -275,19 +246,11 @@ mod suspend { let provider = setup_provider_client(); let consumer = setup_consumer_client(); - seed_data_plane( - &provider, - "dataplane", - "http://provider-connector:9192/control/transfer", - ) - .await; - - let (agreement_id, _, asset_id) = seed_contract_agreement(&consumer, &provider).await; + let (agreement_id, _, _) = seed_contract_agreement(&consumer, &provider).await; let request = TransferRequest::builder() .counter_party_address(PROVIDER_PROTOCOL) .contract_id(&agreement_id) - .asset_id(&asset_id) .transfer_type("HttpData-PULL") .destination(DataAddress::builder().kind("HttpProxy").build().unwrap()) .build() diff --git a/edc-connector-tui/Cargo.toml b/edc-connector-tui/Cargo.toml new file mode 100644 index 0000000..96d1a69 --- /dev/null +++ b/edc-connector-tui/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "edc-connector-tui" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +crossterm = "0.27.0" +ratatui = "0.26.0" +tui-textarea = "0.4" +anyhow = "1.0.83" +dirs-next = "2.0.0" +toml = "0.8.12" +futures = "0.3.30" +tokio = {workspace=true} +serde = {workspace=true} +serde_json = {workspace=true} +async-trait = {workspace=true} +edc-connector-client = { path = "../edc-connector-client", version = "0.1.0"} +enum-ordinalize = "4.3.0" +strum = "0.26.2" +arboard = { version = "3.4.0", features = ["wayland-data-control"] } diff --git a/edc-connector-tui/src/app.rs b/edc-connector-tui/src/app.rs new file mode 100644 index 0000000..925556d --- /dev/null +++ b/edc-connector-tui/src/app.rs @@ -0,0 +1,286 @@ +use std::rc::Rc; +mod action; +mod fetch; +pub mod model; +mod msg; + +use crossterm::event::{self, Event, KeyCode}; +use edc_connector_client::EdcConnectorClient; +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + Frame, +}; + +use crate::{ + components::{ + assets::AssetsComponent, connectors::ConnectorsComponent, + contract_definitions::ContractDefinitionsComponent, footer::Footer, + header::HeaderComponent, launch_bar::LaunchBar, policies::PolicyDefinitionsComponent, + Component, ComponentEvent, ComponentMsg, ComponentReturn, + }, + config::Config, + types::{ + connector::Connector, + info::InfoSheet, + nav::{Menu, Nav}, + }, +}; + +use self::{model::AppFocus, msg::AppMsg}; + +pub struct App { + connectors: ConnectorsComponent, + policies: PolicyDefinitionsComponent, + assets: AssetsComponent, + contract_definitions: ContractDefinitionsComponent, + launch_bar: LaunchBar, + launch_bar_visible: bool, + focus: AppFocus, + header: HeaderComponent, + footer: Footer, +} + +impl App { + pub fn init(cfg: Config) -> App { + let connectors = cfg + .connectors + .into_iter() + .map(|cfg| { + let client = EdcConnectorClient::builder() + .management_url(cfg.address()) + .build() + .unwrap(); + Connector::new(cfg, client) + }) + .collect(); + let connectors = ConnectorsComponent::new(connectors); + + let sheet = connectors.info_sheet().merge(Self::info_sheet()); + + App { + connectors, + policies: PolicyDefinitionsComponent::default().on_fetch(Self::fetch_policies), + assets: AssetsComponent::default().on_fetch(Self::fetch_assets), + contract_definitions: ContractDefinitionsComponent::default() + .on_fetch(Self::fetch_contract_definitions), + launch_bar: LaunchBar::default(), + launch_bar_visible: false, + focus: AppFocus::ConnectorList, + footer: Footer::default(), + header: HeaderComponent::with_sheet(sheet), + } + } + + pub fn info_sheet() -> InfoSheet { + InfoSheet::default() + .key_binding("", "Switch menu") + .key_binding("", "Back/Clear") + .key_binding("<:>", "Launch bar") + .key_binding("<:q>", "Quit") + } + + pub fn change_sheet(&mut self) -> anyhow::Result> { + let component_sheet = match self.header.selected_menu() { + Menu::Connectors => InfoSheet::default(), + Menu::Assets => self.assets.info_sheet(), + Menu::Policies => self.policies.info_sheet(), + Menu::ContractDefinitions => self.contract_definitions.info_sheet(), + }; + + self.header.update_sheet( + self.connectors + .info_sheet() + .merge(Self::info_sheet()) + .merge(component_sheet), + ); + Ok(ComponentReturn::empty()) + } + + pub async fn handle_routing(&mut self, nav: Nav) -> anyhow::Result> { + self.launch_bar_visible = false; + self.launch_bar.clear(); + self.header.set_selected_menu(nav); + self.change_sheet()?; + + match self.header.selected_menu() { + Menu::Connectors => { + self.focus = AppFocus::ConnectorList; + Ok(ComponentReturn::empty()) + } + Menu::Assets => { + self.focus = AppFocus::Assets; + if let Some(connector) = self.connectors.selected() { + return Self::forward_init( + &mut self.assets, + connector.clone(), + AppMsg::AssetsMsg, + ) + .await; + } + Ok(ComponentReturn::empty()) + } + Menu::Policies => { + self.focus = AppFocus::Policies; + if let Some(connector) = self.connectors.selected() { + return Self::forward_init( + &mut self.policies, + connector.clone(), + AppMsg::PoliciesMsg, + ) + .await; + } + Ok(ComponentReturn::empty()) + } + Menu::ContractDefinitions => { + self.focus = AppFocus::ContractDefinitions; + if let Some(connector) = self.connectors.selected() { + return Self::forward_init( + &mut self.contract_definitions, + connector.clone(), + AppMsg::ContractDefinitions, + ) + .await; + } + Ok(ComponentReturn::empty()) + } + } + } +} + +#[async_trait::async_trait] +impl Component for App { + type Msg = AppMsg; + type Props = (); + + fn view(&mut self, f: &mut Frame, rect: Rect) { + let main = self.main_layout(rect); + + self.header.view(f, main[0]); + self.launch_bar.view(f, main[1]); + + match self.header.selected_menu() { + Menu::Connectors => self.connectors.view(f, main[2]), + Menu::Assets => self.assets.view(f, main[2]), + Menu::Policies => self.policies.view(f, main[2]), + Menu::ContractDefinitions => self.contract_definitions.view(f, main[2]), + } + + self.footer.view(f, main[3]); + } + + async fn update( + &mut self, + msg: ComponentMsg, + ) -> anyhow::Result> { + match msg.to_owned() { + AppMsg::ConnectorsMsg(m) => { + Self::forward_update::<_, ConnectorsComponent>( + &mut self.connectors, + m.into(), + AppMsg::ConnectorsMsg, + ) + .await + } + AppMsg::ShowLaunchBar => { + self.launch_bar_visible = true; + self.focus = AppFocus::LaunchBar; + Ok(ComponentReturn::empty()) + } + AppMsg::HideLaunchBar => { + self.launch_bar.clear(); + self.launch_bar_visible = false; + self.focus = AppFocus::ConnectorList; + Ok(ComponentReturn::empty()) + } + AppMsg::LaunchBarMsg(m) => { + Self::forward_update(&mut self.launch_bar, m.into(), AppMsg::LaunchBarMsg).await + } + AppMsg::AssetsMsg(m) => { + Self::forward_update(&mut self.assets, m.into(), AppMsg::AssetsMsg).await + } + AppMsg::PoliciesMsg(m) => { + Self::forward_update(&mut self.policies, m.into(), AppMsg::PoliciesMsg).await + } + AppMsg::ContractDefinitions(m) => { + Self::forward_update( + &mut self.contract_definitions, + m.into(), + AppMsg::ContractDefinitions, + ) + .await + } + AppMsg::HeaderMsg(m) => { + Self::forward_update(&mut self.header, m.into(), AppMsg::HeaderMsg).await + } + AppMsg::RoutingMsg(nav) => self.handle_routing(nav).await, + AppMsg::ChangeSheet => self.change_sheet(), + } + } + + fn handle_event( + &mut self, + evt: ComponentEvent, + ) -> anyhow::Result>> { + let msg = match self.focus { + AppFocus::ConnectorList => { + Self::forward_event(&mut self.connectors, evt.clone(), AppMsg::ConnectorsMsg)? + } + AppFocus::LaunchBar => { + Self::forward_event(&mut self.launch_bar, evt.clone(), AppMsg::LaunchBarMsg)? + } + AppFocus::Assets => { + Self::forward_event(&mut self.assets, evt.clone(), AppMsg::AssetsMsg)? + } + AppFocus::Policies => { + Self::forward_event(&mut self.policies, evt.clone(), AppMsg::PoliciesMsg)? + } + AppFocus::ContractDefinitions => Self::forward_event( + &mut self.contract_definitions, + evt.clone(), + AppMsg::ContractDefinitions, + )?, + }; + + let msg = if msg.is_empty() { + Self::forward_event(&mut self.header, evt.clone(), AppMsg::HeaderMsg)? + } else { + msg + }; + + if msg.is_empty() { + if let ComponentEvent::Event(Event::Key(key)) = evt { + if key.kind == event::KeyEventKind::Press { + return Ok(Self::handle_key(key)); + } + } + } else { + return Ok(msg); + } + + Ok(vec![]) + } +} + +impl App { + fn main_layout(&self, rect: Rect) -> Rc<[Rect]> { + Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(10), + Constraint::Percentage(if self.launch_bar_visible { 5 } else { 0 }), + Constraint::Min(1), + Constraint::Length(3), + ] + .as_ref(), + ) + .split(rect) + } + + fn handle_key(key: event::KeyEvent) -> Vec> { + match key.code { + KeyCode::Char(':') => vec![(AppMsg::ShowLaunchBar.into())], + _ => vec![], + } + } +} diff --git a/edc-connector-tui/src/app/action.rs b/edc-connector-tui/src/app/action.rs new file mode 100644 index 0000000..339fdaa --- /dev/null +++ b/edc-connector-tui/src/app/action.rs @@ -0,0 +1,18 @@ +use crate::components::{Action, ActionHandler, ComponentMsg}; + +use super::{model::AppFocus, msg::AppMsg, App}; + +impl ActionHandler for App { + type Msg = AppMsg; + fn handle_action( + &mut self, + action: crate::components::Action, + ) -> anyhow::Result>> { + match (&self.focus, action) { + (AppFocus::LaunchBar, Action::Esc) => Ok(vec![AppMsg::HideLaunchBar.into()]), + (_, Action::NavTo(nav)) => Ok(vec![AppMsg::RoutingMsg(nav).into()]), + (_, Action::ChangeSheet) => Ok(vec![AppMsg::ChangeSheet.into()]), + _ => Ok(vec![]), + } + } +} diff --git a/edc-connector-tui/src/app/fetch.rs b/edc-connector-tui/src/app/fetch.rs new file mode 100644 index 0000000..41e6ed9 --- /dev/null +++ b/edc-connector-tui/src/app/fetch.rs @@ -0,0 +1,50 @@ +use edc_connector_client::types::query::Query; + +use crate::{ + components::{ + assets::AssetEntry, contract_definitions::ContractDefinitionEntry, + policies::PolicyDefinitionEntry, + }, + types::connector::Connector, +}; + +use super::App; + +impl App { + pub async fn fetch_assets(connector: Connector) -> anyhow::Result> { + Ok(connector + .client() + .assets() + .query(Query::default()) + .await? + .into_iter() + .map(AssetEntry::new) + .collect()) + } + + pub async fn fetch_contract_definitions( + connector: Connector, + ) -> anyhow::Result> { + Ok(connector + .client() + .contract_definitions() + .query(Query::default()) + .await? + .into_iter() + .map(ContractDefinitionEntry::new) + .collect()) + } + + pub async fn fetch_policies( + connector: Connector, + ) -> anyhow::Result> { + Ok(connector + .client() + .policies() + .query(Query::default()) + .await? + .into_iter() + .map(PolicyDefinitionEntry::new) + .collect()) + } +} diff --git a/edc-connector-tui/src/app/model.rs b/edc-connector-tui/src/app/model.rs new file mode 100644 index 0000000..6443916 --- /dev/null +++ b/edc-connector-tui/src/app/model.rs @@ -0,0 +1,9 @@ +#[derive(Debug, Default)] +pub enum AppFocus { + #[default] + ConnectorList, + LaunchBar, + Assets, + Policies, + ContractDefinitions, +} diff --git a/edc-connector-tui/src/app/msg.rs b/edc-connector-tui/src/app/msg.rs new file mode 100644 index 0000000..0a666b7 --- /dev/null +++ b/edc-connector-tui/src/app/msg.rs @@ -0,0 +1,22 @@ +use crate::{ + components::{ + assets::AssetsMsg, connectors::msg::ConnectorsMsg, + contract_definitions::ContractDefinitionsMsg, header::msg::HeaderMsg, + launch_bar::msg::LaunchBarMsg, policies::PoliciesMsg, + }, + types::nav::Nav, +}; + +#[derive(Debug)] +pub enum AppMsg { + ConnectorsMsg(ConnectorsMsg), + ShowLaunchBar, + HideLaunchBar, + LaunchBarMsg(LaunchBarMsg), + AssetsMsg(AssetsMsg), + PoliciesMsg(PoliciesMsg), + ContractDefinitions(ContractDefinitionsMsg), + HeaderMsg(HeaderMsg), + RoutingMsg(Nav), + ChangeSheet, +} diff --git a/edc-connector-tui/src/components.rs b/edc-connector-tui/src/components.rs new file mode 100644 index 0000000..4b23177 --- /dev/null +++ b/edc-connector-tui/src/components.rs @@ -0,0 +1,211 @@ +use std::{fmt::Debug, sync::Arc}; + +use crossterm::event::Event; +use futures::{future::BoxFuture, FutureExt}; +use ratatui::{layout::Rect, Frame}; + +use crate::types::nav::Nav; + +pub mod assets; +pub mod connectors; +pub mod contract_definitions; +pub mod footer; +pub mod header; +pub mod launch_bar; +pub mod policies; +pub mod resources; +pub mod table; + +pub trait StatelessComponent { + type Props: Send; + + fn view(&mut self, props: &Self::Props, f: &mut Frame, rect: Rect); +} + +#[async_trait::async_trait] +pub trait Component { + type Msg: Send; + type Props: Send; + + async fn init(&mut self, _props: Self::Props) -> anyhow::Result> { + Ok(ComponentReturn::empty()) + } + + fn view(&mut self, f: &mut Frame, rect: Rect); + + async fn update( + &mut self, + _message: ComponentMsg, + ) -> anyhow::Result> { + Ok(ComponentReturn::empty()) + } + + fn handle_event( + &mut self, + _evt: ComponentEvent, + ) -> anyhow::Result>> { + Ok(vec![]) + } + + async fn forward_update<'a, F, C>( + other: &'a mut C, + msg: ComponentMsg, + mapper: F, + ) -> anyhow::Result> + where + F: Fn(C::Msg) -> Self::Msg + Send + Sync + 'a, + C: Component + Sync + Send + 'a, + { + Ok(other.update(msg).await?.map(mapper)) + } + + async fn forward_init<'a, F, C>( + other: &'a mut C, + props: C::Props, + mapper: F, + ) -> anyhow::Result> + where + F: Fn(C::Msg) -> Self::Msg + Send + Sync + 'a, + C: Component + Sync + Send + 'a, + { + Ok(other.init(props).await?.map(mapper)) + } + + fn forward_event( + other: &mut C, + evt: ComponentEvent, + mapper: F, + ) -> anyhow::Result>> + where + F: Fn(C::Msg) -> Self::Msg, + C: Component, + { + Ok(other + .handle_event(evt)? + .into_iter() + .map(|c| c.map(&mapper)) + .collect()) + } +} + +#[derive(Debug)] +pub struct ComponentMsg(T); + +#[derive(Default)] +pub struct ComponentReturn<'a, T> { + pub(crate) msgs: Vec>, + pub(crate) cmds: Vec>>>>, + pub(crate) actions: Vec, +} + +impl<'a, T: Debug> Debug for ComponentReturn<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ComponentReturn") + .field("msgs", &self.msgs) + .field("cmds", &self.cmds.len()) + .finish() + } +} + +#[derive(Debug, Clone)] +pub enum Action { + Quit, + Esc, + NavTo(Nav), + ChangeSheet, +} + +impl ComponentMsg { + pub fn to_owned(self) -> T { + self.0 + } + + pub fn map(self, mapper: F) -> ComponentMsg + where + F: FnOnce(T) -> M, + { + ComponentMsg(mapper(self.0)) + } +} + +impl<'a, T: 'a> ComponentReturn<'a, T> { + pub fn cmd(cmd: BoxFuture<'a, anyhow::Result>>>) -> ComponentReturn<'a, T> { + ComponentReturn { + msgs: vec![], + cmds: vec![cmd], + actions: vec![], + } + } + pub fn empty() -> ComponentReturn<'a, T> { + ComponentReturn { + msgs: vec![], + cmds: vec![], + actions: vec![], + } + } + + pub fn action(action: Action) -> ComponentReturn<'a, T> { + ComponentReturn { + msgs: vec![], + cmds: vec![], + actions: vec![action], + } + } + + pub fn map(self, mapper: F) -> ComponentReturn<'a, M> + where + F: Fn(T) -> M + Sync + Send + 'a, + { + let msgs = self.msgs.into_iter().map(|msg| msg.map(&mapper)).collect(); + + let shared = Arc::new(mapper); + let cmds = self + .cmds + .into_iter() + .map(|fut| { + let inner_mapper = shared.clone(); + async move { + let msgs = fut.await?; + + Ok(msgs + .into_iter() + .map(|msg| msg.map(inner_mapper.as_ref())) + .collect()) + } + .boxed() + }) + .collect::>(); + + ComponentReturn { + msgs, + cmds, + actions: self.actions, + } + } +} + +impl From for ComponentMsg { + fn from(value: T) -> Self { + ComponentMsg(value) + } +} + +impl From> for ComponentReturn<'_, T> { + fn from(value: ComponentMsg) -> Self { + ComponentReturn { + msgs: vec![value], + cmds: vec![], + actions: vec![], + } + } +} + +pub trait ActionHandler { + type Msg; + fn handle_action(&mut self, action: Action) -> anyhow::Result>>; +} + +#[derive(Clone)] +pub enum ComponentEvent { + Event(Event), +} diff --git a/edc-connector-tui/src/components/assets.rs b/edc-connector-tui/src/components/assets.rs new file mode 100644 index 0000000..175944b --- /dev/null +++ b/edc-connector-tui/src/components/assets.rs @@ -0,0 +1,76 @@ +use crate::components::resources::Field; + +use super::{ + resources::{msg::ResourcesMsg, DrawableResource, FieldValue, ResourcesComponent}, + table::TableEntry, +}; +use edc_connector_client::types::asset::Asset; +use ratatui::widgets::Row; + +pub type AssetsMsg = ResourcesMsg; +pub type AssetsComponent = ResourcesComponent; + +#[derive(Debug, Clone)] +pub struct AssetEntry(Asset); + +impl AssetEntry { + pub fn new(asset: Asset) -> AssetEntry { + AssetEntry(asset) + } +} + +impl TableEntry for AssetEntry { + fn row(&self) -> Row { + let properties = serde_json::to_string(self.0.properties()).unwrap(); + let private_properties = serde_json::to_string(self.0.private_properties()).unwrap(); + let data_address = serde_json::to_string(self.0.data_address()).unwrap(); + Row::new(vec![ + self.0.id().to_string(), + properties, + private_properties, + data_address, + ]) + } + + fn headers() -> Row<'static> { + Row::new(vec![ + "ID", + "PROPERTIES", + "PRIVATE PROPERTIES", + "DATA ADDRESS", + ]) + } +} + +impl DrawableResource for AssetEntry { + fn id(&self) -> &str { + self.0.id() + } + + fn title() -> &'static str { + "Assets" + } + + fn fields(&self) -> Vec { + let mut fields = vec![Field::new( + "id".to_string(), + FieldValue::Str(self.0.id().to_string()), + )]; + + fields.push(Field::new( + "properties".to_string(), + FieldValue::Json(serde_json::to_string_pretty(&self.0.properties()).unwrap()), + )); + + fields.push(Field::new( + "private_properties".to_string(), + FieldValue::Json(serde_json::to_string_pretty(&self.0.private_properties()).unwrap()), + )); + fields.push(Field::new( + "data_address".to_string(), + FieldValue::Json(serde_json::to_string_pretty(self.0.data_address()).unwrap()), + )); + + fields + } +} diff --git a/edc-connector-tui/src/components/connectors.rs b/edc-connector-tui/src/components/connectors.rs new file mode 100644 index 0000000..60bf522 --- /dev/null +++ b/edc-connector-tui/src/components/connectors.rs @@ -0,0 +1,102 @@ +use ratatui::{layout::Rect, widgets::Row, Frame}; + +use crate::types::{connector::Connector, info::InfoSheet, nav::Nav}; + +use self::msg::ConnectorsMsg; + +use super::{ + table::{msg::TableMsg, TableEntry, UiTable}, + Action, Component, ComponentEvent, ComponentMsg, ComponentReturn, +}; + +pub mod msg; + +pub type ConnectorsTable = UiTable>; + +#[derive(Debug, Default)] +pub struct ConnectorsComponent { + table: ConnectorsTable, + selected: Option, +} + +#[derive(Debug)] +pub struct ConnectorEntry(Connector); + +impl TableEntry for ConnectorEntry { + fn row(&self) -> Row { + Row::new(vec![self.0.config().name(), self.0.config().address()]) + } + + fn headers() -> Row<'static> { + Row::new(vec!["NAME", "ADDRESS"]) + } +} + +#[async_trait::async_trait] +impl Component for ConnectorsComponent { + type Msg = ConnectorsMsg; + type Props = (); + + fn view(&mut self, f: &mut Frame, rect: Rect) { + self.table.view(f, rect); + } + + async fn update( + &mut self, + msg: ComponentMsg, + ) -> anyhow::Result> { + match msg.to_owned() { + ConnectorsMsg::ConnectorSelected(connector) => { + self.selected = Some(connector.clone()); + Ok(ComponentReturn::action(Action::NavTo(Nav::AssetsList))) + } + ConnectorsMsg::TableEvent(table) => { + Self::forward_update::<_, ConnectorsTable>( + &mut self.table, + table.into(), + ConnectorsMsg::TableEvent, + ) + .await + } + } + } + + fn handle_event( + &mut self, + evt: ComponentEvent, + ) -> anyhow::Result>> { + Self::forward_event(&mut self.table, evt, |msg| match msg { + TableMsg::Local(table) => ConnectorsMsg::TableEvent(TableMsg::Local(table)), + TableMsg::Outer(outer) => *outer, + }) + } +} + +impl ConnectorsComponent { + pub fn new(connectors: Vec) -> Self { + Self { + table: ConnectorsTable::with_elements( + "Connectors".to_string(), + connectors.into_iter().map(ConnectorEntry).collect(), + ) + .on_select(|connector| Box::new(ConnectorsMsg::ConnectorSelected(connector.0.clone()))), + selected: None, + } + } + + pub fn selected(&self) -> Option<&Connector> { + self.selected.as_ref() + } + + pub fn info_sheet(&self) -> InfoSheet { + if let Some(c) = self.selected.as_ref() { + InfoSheet::default() + .info("Connector Name", c.config().name()) + .info("Connector Address", c.config().address()) + } else { + InfoSheet::default() + .info("Connector Name", "n/a") + .info("Connector Address", "n/a") + } + } +} diff --git a/edc-connector-tui/src/components/connectors/msg.rs b/edc-connector-tui/src/components/connectors/msg.rs new file mode 100644 index 0000000..d809c38 --- /dev/null +++ b/edc-connector-tui/src/components/connectors/msg.rs @@ -0,0 +1,7 @@ +use crate::{components::table::msg::TableMsg, types::connector::Connector}; + +#[derive(Debug)] +pub enum ConnectorsMsg { + TableEvent(TableMsg>), + ConnectorSelected(Connector), +} diff --git a/edc-connector-tui/src/components/contract_definitions.rs b/edc-connector-tui/src/components/contract_definitions.rs new file mode 100644 index 0000000..fceb83a --- /dev/null +++ b/edc-connector-tui/src/components/contract_definitions.rs @@ -0,0 +1,59 @@ +use edc_connector_client::types::contract_definition::ContractDefinition; +use ratatui::widgets::Row; + +use super::{ + resources::{msg::ResourcesMsg, DrawableResource, Field, ResourcesComponent}, + table::TableEntry, +}; + +#[derive(Debug, Clone)] +pub struct ContractDefinitionEntry(ContractDefinition); + +impl ContractDefinitionEntry { + pub fn new(contract_definition: ContractDefinition) -> Self { + Self(contract_definition) + } +} + +pub type ContractDefinitionsMsg = ResourcesMsg; +pub type ContractDefinitionsComponent = ResourcesComponent; + +impl TableEntry for ContractDefinitionEntry { + fn row(&self) -> Row { + let asset_selector = serde_json::to_string(self.0.assets_selector()).unwrap(); + Row::new(vec![ + self.0.id().to_string(), + self.0.access_policy_id().to_string(), + self.0.contract_policy_id().to_string(), + asset_selector, + ]) + } + + fn headers() -> Row<'static> { + Row::new(vec![ + "ID", + "ACCESS_POLICY_ID", + "CONTRACT_POLICY_ID", + "ASSETS_SELECTOR", + ]) + } +} + +impl DrawableResource for ContractDefinitionEntry { + fn id(&self) -> &str { + self.0.id() + } + + fn title() -> &'static str { + "Contract Definitions" + } + + fn fields(&self) -> Vec { + vec![ + Field::string("id", self.0.id()), + Field::string("access_policy_id", self.0.access_policy_id()), + Field::string("contract_policy_id", self.0.contract_policy_id()), + Field::json("assets_selector", self.0.assets_selector()), + ] + } +} diff --git a/edc-connector-tui/src/components/footer.rs b/edc-connector-tui/src/components/footer.rs new file mode 100644 index 0000000..71f0992 --- /dev/null +++ b/edc-connector-tui/src/components/footer.rs @@ -0,0 +1,23 @@ +use ratatui::{ + layout::Rect, + widgets::{Block, Borders}, + Frame, +}; + +use super::Component; + +pub mod msg; + +#[derive(Default)] +pub struct Footer {} + +#[async_trait::async_trait] +impl Component for Footer { + type Msg = (); + type Props = (); + + fn view(&mut self, f: &mut Frame, rect: Rect) { + let block = Block::default().borders(Borders::all()); + f.render_widget(block, rect) + } +} diff --git a/edc-connector-tui/src/components/footer/msg.rs b/edc-connector-tui/src/components/footer/msg.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/edc-connector-tui/src/components/footer/msg.rs @@ -0,0 +1 @@ + diff --git a/edc-connector-tui/src/components/header.rs b/edc-connector-tui/src/components/header.rs new file mode 100644 index 0000000..8e95963 --- /dev/null +++ b/edc-connector-tui/src/components/header.rs @@ -0,0 +1,97 @@ +use crossterm::event::{Event, KeyCode, KeyEventKind}; +use enum_ordinalize::Ordinalize; +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Style, Stylize}, + widgets::{Block, Tabs}, + Frame, +}; + +use crate::types::{info::InfoSheet, nav::Menu}; + +use self::{help::InfoComponent, msg::HeaderMsg}; + +use super::{Component, ComponentEvent, ComponentMsg, ComponentReturn, StatelessComponent}; + +pub mod help; +pub mod msg; + +#[derive(Default)] +pub struct HeaderComponent { + menu: Menu, + info: InfoComponent, + sheet: InfoSheet, +} + +impl HeaderComponent { + pub fn with_sheet(sheet: InfoSheet) -> HeaderComponent { + HeaderComponent { + menu: Menu::default(), + info: InfoComponent::default(), + sheet, + } + } + pub fn set_selected_menu(&mut self, menu: impl Into) { + self.menu = menu.into(); + } + + pub fn update_sheet(&mut self, sheet: InfoSheet) { + self.sheet = sheet; + } + + pub fn selected_menu(&self) -> &Menu { + &self.menu + } +} + +#[async_trait::async_trait] +impl Component for HeaderComponent { + type Msg = HeaderMsg; + type Props = (); + + fn view(&mut self, f: &mut Frame, rect: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref()) + .split(rect); + + let tabs = Tabs::new(Menu::names()) + .block(Block::bordered().title("Menu")) + .style(Style::default().white()) + .highlight_style(Style::default().yellow()) + .select(self.menu.ordinal()) + .divider("|") + .padding(" ", " "); + + f.render_widget(tabs, layout[0]); + self.info.view(&self.sheet, f, layout[1]); + } + + async fn update( + &mut self, + msg: ComponentMsg, + ) -> anyhow::Result> { + match msg.to_owned() { + HeaderMsg::NextTab => { + let current = self.menu.clone(); + let idx = (self.menu.ordinal() + 1) % Menu::VALUES.len(); + self.menu = Menu::from_ordinal(idx).unwrap_or_else(|| current); + Ok(ComponentReturn::action(super::Action::NavTo( + self.menu.clone().into(), + ))) + } + } + } + + fn handle_event( + &mut self, + evt: ComponentEvent, + ) -> anyhow::Result>> { + if let ComponentEvent::Event(Event::Key(key)) = evt { + if key.kind == KeyEventKind::Press && key.code == KeyCode::Tab { + return Ok(vec![HeaderMsg::NextTab.into()]); + } + } + Ok(vec![]) + } +} diff --git a/edc-connector-tui/src/components/header/help.rs b/edc-connector-tui/src/components/header/help.rs new file mode 100644 index 0000000..6f9540f --- /dev/null +++ b/edc-connector-tui/src/components/header/help.rs @@ -0,0 +1,78 @@ +use ratatui::{ + layout::{Constraint, Layout, Rect}, + style::{Color, Style, Styled}, + text::{Line, Span}, + widgets::{Block, List}, + Frame, +}; + +use crate::{components::StatelessComponent, types::info::InfoSheet}; + +#[derive(Default)] +pub struct InfoComponent {} + +#[async_trait::async_trait] +impl StatelessComponent for InfoComponent { + type Props = InfoSheet; + + fn view(&mut self, props: &Self::Props, f: &mut Frame, rect: Rect) { + let layout = + Layout::horizontal(vec![Constraint::Percentage(25), Constraint::Percentage(75)]) + .split(rect); + + self.view_info(props, f, layout[0]); + self.view_key_bindings(props, f, layout[1]) + } +} + +impl InfoComponent { + fn view_info(&self, props: &InfoSheet, f: &mut Frame, rect: Rect) { + let max = props + .iter_info() + .map(|(name, _)| name.len()) + .max() + .unwrap_or(0); + + let list = props + .iter_info() + .map(|(name, value)| { + let padding = max + 2 - name.len(); + Line::from(vec![ + name.to_string() + .set_style(Style::default().fg(Color::Yellow)), + Span::raw(format!("{:() + .block(Block::default()); + + f.render_widget(list, rect) + } + + fn view_key_bindings(&self, props: &InfoSheet, f: &mut Frame, rect: Rect) { + let max = props + .iter_key_bindings() + .map(|(name, _)| name.len()) + .max() + .unwrap_or(0); + + let list = props + .iter_key_bindings() + .map(|(name, value)| { + let padding = max + 2 - name.len(); + Line::from(vec![ + name.to_string() + .set_style(Style::default().fg(Color::Magenta)), + Span::raw(format!("{:() + .block(Block::default()); + + f.render_widget(list, rect) + } +} diff --git a/edc-connector-tui/src/components/header/msg.rs b/edc-connector-tui/src/components/header/msg.rs new file mode 100644 index 0000000..d7c7f86 --- /dev/null +++ b/edc-connector-tui/src/components/header/msg.rs @@ -0,0 +1,4 @@ +#[derive(Debug)] +pub enum HeaderMsg { + NextTab, +} diff --git a/edc-connector-tui/src/components/launch_bar.rs b/edc-connector-tui/src/components/launch_bar.rs new file mode 100644 index 0000000..e658919 --- /dev/null +++ b/edc-connector-tui/src/components/launch_bar.rs @@ -0,0 +1,92 @@ +use self::msg::LaunchBarMsg; +use super::{Action, Component, ComponentEvent, ComponentMsg, ComponentReturn}; +use ratatui::{ + layout::Rect, + style::Style, + widgets::{Block, Borders}, + Frame, +}; +use tui_textarea::{Input, Key, TextArea}; +pub mod msg; + +pub static PROMPT: &str = "$ "; + +#[derive(Debug)] +pub struct LaunchBar { + pub(crate) area: TextArea<'static>, +} +impl Default for LaunchBar { + fn default() -> Self { + let mut area = TextArea::default(); + area.insert_str(PROMPT); + Self { area } + } +} + +#[async_trait::async_trait] +impl Component for LaunchBar { + type Msg = LaunchBarMsg; + type Props = (); + + fn view(&mut self, f: &mut Frame, rect: Rect) { + let text_area = &mut self.area; + text_area.set_block(Block::default().borders(Borders::all())); + text_area.set_cursor_line_style(Style::default()); + text_area.set_placeholder_text("Enter command"); + f.render_widget(self.area.widget(), rect) + } + + async fn update( + &mut self, + msg: ComponentMsg, + ) -> anyhow::Result> { + match msg.to_owned() { + LaunchBarMsg::AppendCommand(input) => { + self.area.input(input); + Ok(ComponentReturn::empty()) + } + LaunchBarMsg::Quit => Ok(ComponentReturn::action(Action::Quit)), + LaunchBarMsg::Esc => Ok(ComponentReturn::action(Action::Esc)), + LaunchBarMsg::NavTo(nav) => Ok(ComponentReturn::action(Action::NavTo(nav))), + } + } + + fn handle_event( + &mut self, + evt: ComponentEvent, + ) -> anyhow::Result>> { + match evt { + ComponentEvent::Event(evt) => { + let input: Input = evt.into(); + + let current = &self.area.lines()[0].replacen(PROMPT, "", 1); + + match input.key { + Key::Backspace if current.is_empty() => Ok(vec![]), + Key::Char('q') if current.is_empty() => Ok(vec![ + LaunchBarMsg::AppendCommand(input).into(), + LaunchBarMsg::AppendCommand(Input { + key: Key::Char('!'), + ..Default::default() + }) + .into(), + ]), + Key::Enter if current == "q!" => Ok(vec![LaunchBarMsg::Quit.into()]), + Key::Enter if !current.is_empty() => { + Ok(vec![LaunchBarMsg::NavTo(current.parse()?).into()]) + } + Key::Esc => Ok(vec![LaunchBarMsg::Esc.into()]), + _ => Ok(vec![LaunchBarMsg::AppendCommand(input).into()]), + } + } + } + } +} + +impl LaunchBar { + pub fn clear(&mut self) { + self.area.move_cursor(tui_textarea::CursorMove::Head); + self.area.delete_line_by_end(); + self.area.insert_str(PROMPT); + } +} diff --git a/edc-connector-tui/src/components/launch_bar/msg.rs b/edc-connector-tui/src/components/launch_bar/msg.rs new file mode 100644 index 0000000..3129adf --- /dev/null +++ b/edc-connector-tui/src/components/launch_bar/msg.rs @@ -0,0 +1,11 @@ +use tui_textarea::Input; + +use crate::types::nav::Nav; + +#[derive(Debug)] +pub enum LaunchBarMsg { + AppendCommand(Input), + Quit, + NavTo(Nav), + Esc, +} diff --git a/edc-connector-tui/src/components/policies.rs b/edc-connector-tui/src/components/policies.rs new file mode 100644 index 0000000..d636246 --- /dev/null +++ b/edc-connector-tui/src/components/policies.rs @@ -0,0 +1,54 @@ +use edc_connector_client::types::policy::PolicyDefinition; +use ratatui::widgets::Row; + +use super::{ + resources::{msg::ResourcesMsg, DrawableResource, Field, FieldValue, ResourcesComponent}, + table::TableEntry, +}; + +pub type PoliciesMsg = ResourcesMsg; +pub type PolicyDefinitionsComponent = ResourcesComponent; + +#[derive(Debug, Clone)] +pub struct PolicyDefinitionEntry(PolicyDefinition); + +impl PolicyDefinitionEntry { + pub fn new(definition: PolicyDefinition) -> Self { + PolicyDefinitionEntry(definition) + } +} + +impl TableEntry for PolicyDefinitionEntry { + fn row(&self) -> ratatui::widgets::Row { + let policy = serde_json::to_string(self.0.policy()).unwrap(); + Row::new(vec![self.0.id().to_string(), policy]) + } + + fn headers() -> ratatui::widgets::Row<'static> { + Row::new(vec!["ID", "POLICY"]) + } +} + +impl DrawableResource for PolicyDefinitionEntry { + fn id(&self) -> &str { + self.0.id() + } + + fn title() -> &'static str { + "Policies" + } + + fn fields(&self) -> Vec { + let mut fields = vec![Field::new( + "id".to_string(), + FieldValue::Str(self.0.id().to_string()), + )]; + + fields.push(Field::new( + "policy".to_string(), + FieldValue::Json(serde_json::to_string_pretty(&self.0.policy()).unwrap()), + )); + + fields + } +} diff --git a/edc-connector-tui/src/components/resources.rs b/edc-connector-tui/src/components/resources.rs new file mode 100644 index 0000000..f5de2bd --- /dev/null +++ b/edc-connector-tui/src/components/resources.rs @@ -0,0 +1,196 @@ +use std::{fmt::Debug, sync::Arc}; + +use self::{msg::ResourcesMsg, resource::ResourceComponent}; +use super::{ + table::{msg::TableMsg, TableEntry, UiTable}, + Action, Component, ComponentEvent, ComponentMsg, ComponentReturn, +}; +use crate::types::{connector::Connector, info::InfoSheet}; +use crossterm::event::{Event, KeyCode}; +use futures::future::BoxFuture; +use futures::FutureExt; +use ratatui::{layout::Rect, Frame}; +use serde::Serialize; +use std::future::Future; +pub mod msg; +pub mod resource; + +pub type ResourceTable = UiTable>>; + +pub type OnFetch = + Arc BoxFuture<'static, anyhow::Result>> + Send + Sync>; + +#[derive(Debug)] +pub enum Focus { + ResourceList, + Resource, +} + +pub struct ResourcesComponent { + table: ResourceTable, + resource: ResourceComponent, + focus: Focus, + connector: Option, + on_fetch: Option>, +} + +impl ResourcesComponent { + pub fn on_fetch(mut self, on_fetch: F) -> Self + where + F: Fn(Connector) -> Fut + Send + Sync + 'static, + Fut: Future>> + Send, + { + let handler = Arc::new(on_fetch); + self.on_fetch = Some(Arc::new(move |conn| { + let c = conn.clone(); + let inner_handler = handler.clone(); + async move { inner_handler(c).await }.boxed() + })); + + self + } + + pub fn info_sheet(&self) -> InfoSheet { + match self.focus { + Focus::ResourceList => self.table.info_sheet(), + Focus::Resource => self.resource.info_sheet(), + } + } +} + +impl Default for ResourcesComponent { + fn default() -> Self { + Self { + table: ResourceTable::new(T::title().to_string()) + .on_select(|res: &T| Box::new(ResourcesMsg::ResourceSelected(res.clone()))), + resource: ResourceComponent::new(T::title().to_string()), + focus: Focus::ResourceList, + connector: None, + on_fetch: None, + } + } +} + +#[async_trait::async_trait] +impl Component for ResourcesComponent { + type Msg = ResourcesMsg; + type Props = Connector; + + async fn init(&mut self, props: Self::Props) -> anyhow::Result> { + self.connector = Some(props.clone()); + + let connector = props; + + if let Some(on_fetch) = self.on_fetch.as_ref() { + Ok(ComponentReturn::cmd( + async move { + let elements = on_fetch(&connector).await?; + Ok(vec![ResourcesMsg::ResourcesFetched(elements).into()]) + } + .boxed(), + )) + } else { + Ok(ComponentReturn::empty()) + } + } + + fn view(&mut self, f: &mut Frame, rect: Rect) { + match self.focus { + Focus::ResourceList => self.table.view(f, rect), + Focus::Resource => self.resource.view(f, rect), + } + } + + async fn update( + &mut self, + msg: ComponentMsg, + ) -> anyhow::Result> { + match msg.to_owned() { + ResourcesMsg::ResourceSelected(selected) => { + self.resource.update_resource(Some(selected)); + self.focus = Focus::Resource; + Ok(ComponentReturn::action(Action::ChangeSheet)) + } + ResourcesMsg::TableEvent(table) => { + Self::forward_update(&mut self.table, table.into(), ResourcesMsg::TableEvent).await + } + ResourcesMsg::ResourcesFetched(resources) => { + self.table.elements = resources; + Ok(ComponentReturn::empty()) + } + ResourcesMsg::Back => { + self.focus = Focus::ResourceList; + Ok(ComponentReturn::action(Action::ChangeSheet)) + } + ResourcesMsg::ResourceMsg(msg) => { + Self::forward_update(&mut self.resource, msg.into(), ResourcesMsg::ResourceMsg) + .await + } + } + } + + fn handle_event( + &mut self, + evt: ComponentEvent, + ) -> anyhow::Result>> { + match self.focus { + Focus::ResourceList => Self::forward_event(&mut self.table, evt, |msg| match msg { + TableMsg::Local(table) => ResourcesMsg::TableEvent(TableMsg::Local(table)), + TableMsg::Outer(outer) => *outer, + }), + Focus::Resource => match evt { + ComponentEvent::Event(Event::Key(k)) if k.code == KeyCode::Esc => { + Ok(vec![ResourcesMsg::Back.into()]) + } + _ => Self::forward_event(&mut self.resource, evt, ResourcesMsg::ResourceMsg), + }, + } + } +} + +pub trait DrawableResource { + fn id(&self) -> &str; + + fn title() -> &'static str; + + fn fields(&self) -> Vec; +} + +pub struct Field { + name: String, + value: FieldValue, +} + +impl Field { + pub fn new(name: String, value: FieldValue) -> Self { + Self { name, value } + } + + pub fn string(name: impl Into, value: impl Into) -> Self { + Self { + name: name.into(), + value: FieldValue::Str(value.into()), + } + } + + pub fn json(name: impl Into, value: &T) -> Self { + Self { + name: name.into(), + value: FieldValue::Json(serde_json::to_string_pretty(value).unwrap()), + } + } +} + +pub enum FieldValue { + Str(String), + Json(String), +} + +impl AsRef for FieldValue { + fn as_ref(&self) -> &str { + match self { + FieldValue::Str(s) => s, + FieldValue::Json(s) => s, + } + } +} diff --git a/edc-connector-tui/src/components/resources/msg.rs b/edc-connector-tui/src/components/resources/msg.rs new file mode 100644 index 0000000..39d04c6 --- /dev/null +++ b/edc-connector-tui/src/components/resources/msg.rs @@ -0,0 +1,12 @@ +use crate::components::table::msg::TableMsg; + +use super::resource::msg::ResourceMsg; + +#[derive(Debug)] +pub enum ResourcesMsg { + ResourceSelected(T), + Back, + TableEvent(TableMsg>>), + ResourceMsg(ResourceMsg), + ResourcesFetched(Vec), +} diff --git a/edc-connector-tui/src/components/resources/resource.rs b/edc-connector-tui/src/components/resources/resource.rs new file mode 100644 index 0000000..fa0ddba --- /dev/null +++ b/edc-connector-tui/src/components/resources/resource.rs @@ -0,0 +1,204 @@ +use std::fmt::Debug; + +use arboard::Clipboard; +use crossterm::event::{Event, KeyCode, KeyEvent}; +use msg::ResourceMsg; +use ratatui::{ + layout::{Alignment, Constraint, Layout, Rect}, + style::{Color, Style}, + text::Span, + widgets::{block::Title, Block, BorderType, Borders, Paragraph}, + Frame, +}; + +pub mod msg; +use super::{Component, DrawableResource, Field, FieldValue}; +use crate::{ + components::{ComponentEvent, ComponentMsg, ComponentReturn, StatelessComponent}, + types::info::InfoSheet, +}; + +pub struct ResourceComponent { + resource: Option, + name: String, + selected_field: usize, + clip: Clipboard, +} + +impl Debug for ResourceComponent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResourceComponent") + .field("name", &self.name) + .field("selected_field", &self.selected_field) + .finish() + } +} + +impl Default for ResourceComponent { + fn default() -> Self { + Self { + resource: Default::default(), + name: String::default(), + selected_field: 0, + clip: Clipboard::new().unwrap(), + } + } +} + +impl ResourceComponent { + pub fn info_sheet(&self) -> InfoSheet { + InfoSheet::default().key_binding("", "Copy value") + } +} + +impl ResourceComponent { + pub fn new(name: String) -> Self { + Self { + name, + resource: None, + selected_field: 0, + clip: Clipboard::new().unwrap(), + } + } + + pub fn update_resource(&mut self, resource: Option) { + self.resource = resource; + } + + pub fn field_constraints(&self) -> Vec { + if let Some(res) = self.resource.as_ref() { + res.fields() + .into_iter() + .map(|f| match f.value { + FieldValue::Str(_) => Constraint::Length(3), + FieldValue::Json(_) => Constraint::Min(5), + }) + .collect() + } else { + vec![] + } + } + + fn handle_key(&self, key: KeyEvent) -> Vec> { + match key.code { + KeyCode::Char('j') => vec![(ComponentMsg(ResourceMsg::MoveDown.into()))], + KeyCode::Char('k') => vec![(ComponentMsg(ResourceMsg::MoveUp.into()))], + KeyCode::Char('y') => vec![(ComponentMsg(ResourceMsg::Yank.into()))], + _ => vec![], + } + } + + fn yank(&mut self) { + if let Some(res) = self.resource.as_ref() { + if let Some(field) = res.fields().get(self.selected_field) { + self.clip + .set_text(field.value.as_ref().to_string()) + .unwrap(); + } + } + } + + fn move_up(&mut self) { + if let Some(res) = self.resource.as_ref() { + let pos = if self.selected_field == 0 { + res.fields().len() - 1 + } else { + self.selected_field - 1 + }; + self.selected_field = pos; + } + } + + fn move_down(&mut self) { + if let Some(res) = self.resource.as_ref() { + let pos = if self.selected_field == res.fields().len() - 1 { + 0 + } else { + self.selected_field + 1 + }; + self.selected_field = pos; + } + } +} + +#[async_trait::async_trait] +impl Component for ResourceComponent { + type Msg = ResourceMsg; + type Props = (); + + fn view(&mut self, f: &mut Frame, rect: Rect) { + let styled_text = Span::styled( + format!( + " {}({}) ", + self.name, + self.resource.as_ref().map(|a| a.id()).unwrap_or("N/A") + ), + Style::default().fg(Color::Red), + ); + let block = Block::default() + .title(Title::from(styled_text).alignment(Alignment::Center)) + .borders(Borders::ALL); + + let constraints = self.field_constraints(); + + if !constraints.is_empty() { + let area = block.inner(rect); + let layout = Layout::vertical(constraints).split(area); + if let Some(res) = self.resource.as_ref() { + for (idx, elem) in res.fields().into_iter().enumerate() { + let mut field = FieldComponent; + field.view(&(elem, idx == self.selected_field), f, layout[idx]); + } + } + } + + f.render_widget(block, rect); + } + + async fn update( + &mut self, + message: ComponentMsg, + ) -> anyhow::Result> { + match message.to_owned() { + ResourceMsg::MoveUp => self.move_up(), + ResourceMsg::MoveDown => self.move_down(), + ResourceMsg::Yank => self.yank(), + }; + + Ok(ComponentReturn::empty()) + } + + fn handle_event( + &mut self, + evt: ComponentEvent, + ) -> anyhow::Result>> { + match evt { + ComponentEvent::Event(Event::Key(key)) => Ok(self.handle_key(key)), + _ => Ok(vec![]), + } + } +} + +pub struct FieldComponent; + +impl StatelessComponent for FieldComponent { + type Props = (Field, bool); + + fn view(&mut self, (field, selected): &Self::Props, f: &mut Frame, rect: Rect) { + let style = if *selected { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + let styled_text = Span::styled(format!(" {} ", field.name), style); + + let value = Paragraph::new(field.value.as_ref()).block( + Block::bordered() + .title(styled_text) + .border_style(style) + .border_type(BorderType::Double), + ); + + f.render_widget(value, rect) + } +} diff --git a/edc-connector-tui/src/components/resources/resource/msg.rs b/edc-connector-tui/src/components/resources/resource/msg.rs new file mode 100644 index 0000000..41b87b7 --- /dev/null +++ b/edc-connector-tui/src/components/resources/resource/msg.rs @@ -0,0 +1,6 @@ +#[derive(Debug)] +pub enum ResourceMsg { + MoveUp, + MoveDown, + Yank, +} diff --git a/edc-connector-tui/src/components/table.rs b/edc-connector-tui/src/components/table.rs new file mode 100644 index 0000000..e807a42 --- /dev/null +++ b/edc-connector-tui/src/components/table.rs @@ -0,0 +1,169 @@ +use std::fmt::Debug; + +use crossterm::event::{Event, KeyCode, KeyEvent}; +use ratatui::{ + layout::{Alignment, Rect}, + style::{Color, Modifier, Style}, + text::Span, + widgets::{block::Title, Block, Borders, Row, Table, TableState}, + Frame, +}; +pub mod msg; + +use crate::types::info::InfoSheet; + +use self::msg::{TableLocalMsg, TableMsg}; + +use super::{Component, ComponentEvent, ComponentMsg, ComponentReturn}; + +pub struct UiTable { + name: String, + pub elements: Vec, + table_state: TableState, + on_select: Option M + Send + Sync>>, +} + +impl Debug for UiTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UiTable") + .field("name", &self.name) + .field("elements", &self.elements) + .field("table_state", &self.table_state) + .finish() + } +} + +impl Default for UiTable { + fn default() -> Self { + Self { + name: String::new(), + elements: vec![], + table_state: TableState::default().with_selected(0), + on_select: None, + } + } +} + +pub trait TableEntry { + fn row(&self) -> Row; + fn headers() -> Row<'static>; +} + +#[async_trait::async_trait] +impl Component for UiTable { + type Msg = TableMsg; + type Props = (); + + fn view(&mut self, f: &mut Frame, area: Rect) { + let styled_text = Span::styled(format!(" {} ", self.name), Style::default().fg(Color::Red)); + let block = Block::default() + .title(Title::from(styled_text).alignment(Alignment::Center)) + .borders(Borders::ALL); + + let rows = self + .elements + .iter() + .map(TableEntry::row) + .collect::>(); + + let table = Table::default() + .rows(rows) + .header(T::headers()) + .block(block) + .highlight_style(Style::new().add_modifier(Modifier::REVERSED)); + + f.render_stateful_widget(table, area, &mut self.table_state); + } + + async fn update( + &mut self, + msg: ComponentMsg, + ) -> anyhow::Result> { + match msg.to_owned() { + TableMsg::Local(TableLocalMsg::MoveDown) => self.move_down(), + TableMsg::Local(TableLocalMsg::MoveUp) => self.move_up(), + TableMsg::Outer(_) => {} + }; + + Ok(ComponentReturn::empty()) + } + + fn handle_event( + &mut self, + evt: ComponentEvent, + ) -> anyhow::Result>> { + match evt { + ComponentEvent::Event(Event::Key(key)) => Ok(self.handle_key(key)), + _ => Ok(vec![]), + } + } +} + +impl UiTable { + pub fn new(name: String) -> Self { + Self { + name, + elements: vec![], + table_state: TableState::default().with_selected(0), + on_select: None, + } + } + + pub fn info_sheet(&self) -> InfoSheet { + InfoSheet::default() + } + + pub fn with_elements(name: String, elements: Vec) -> Self { + Self { + name, + elements, + table_state: TableState::default().with_selected(0), + on_select: None, + } + } + pub fn on_select(mut self, cb: impl Fn(&T) -> M + Send + Sync + 'static) -> Self { + self.on_select = Some(Box::new(cb)); + self + } + + fn handle_key(&self, key: KeyEvent) -> Vec>> { + match key.code { + KeyCode::Enter => { + if let Some(cb) = self.on_select.as_ref() { + if let Some(idx) = self.table_state.selected() { + if let Some(element) = self.elements.get(idx) { + vec![ComponentMsg(TableMsg::Outer(cb(&element)))] + } else { + vec![] + } + } else { + vec![] + } + } else { + vec![] + } + } + KeyCode::Char('j') => vec![(ComponentMsg(TableLocalMsg::MoveDown.into()))], + KeyCode::Char('k') => vec![(ComponentMsg(TableLocalMsg::MoveUp.into()))], + _ => vec![], + } + } + + fn move_up(&mut self) { + let new_pos = match self.table_state.selected() { + Some(i) if i == 0 => self.elements.len() - 1, + Some(i) => i - 1, + None => 0, + }; + self.table_state.select(Some(new_pos)) + } + + fn move_down(&mut self) { + let new_pos = match self.table_state.selected() { + Some(i) if i == self.elements.len() - 1 => 0, + Some(i) => i + 1, + None => 0, + }; + self.table_state.select(Some(new_pos)) + } +} diff --git a/edc-connector-tui/src/components/table/msg.rs b/edc-connector-tui/src/components/table/msg.rs new file mode 100644 index 0000000..4b8bd09 --- /dev/null +++ b/edc-connector-tui/src/components/table/msg.rs @@ -0,0 +1,17 @@ +#[derive(Debug)] +pub enum TableMsg { + Local(TableLocalMsg), + Outer(T), +} + +#[derive(Debug)] +pub enum TableLocalMsg { + MoveUp, + MoveDown, +} + +impl From for TableMsg { + fn from(value: TableLocalMsg) -> Self { + TableMsg::Local(value) + } +} diff --git a/edc-connector-tui/src/config.rs b/edc-connector-tui/src/config.rs new file mode 100644 index 0000000..946777b --- /dev/null +++ b/edc-connector-tui/src/config.rs @@ -0,0 +1,60 @@ +use std::{ + fs::File, + io::{BufReader, Read}, + path::PathBuf, +}; + +use serde::Deserialize; + +pub fn get_app_config_path() -> anyhow::Result { + let mut path = if cfg!(target_os = "macos") { + dirs_next::home_dir().map(|h| h.join(".config")) + } else { + dirs_next::config_dir() + } + .ok_or_else(|| anyhow::anyhow!("failed to find os config dir."))?; + + path.push("edc-tui"); + std::fs::create_dir_all(&path)?; + Ok(path) +} + +#[derive(Deserialize, Clone)] +pub struct Config { + pub connectors: Vec, +} + +impl Config { + pub fn parse(path: &PathBuf) -> anyhow::Result { + let file = File::open(path)?; + let mut buf_reader = BufReader::new(file); + let mut contents = String::new(); + buf_reader.read_to_string(&mut contents)?; + + let config: Result = toml::from_str(&contents); + match config { + Ok(config) => return Ok(config), + Err(e) => panic!("fail to parse config file: {}", e), + } + } +} + +pub fn default_file() -> anyhow::Result { + Ok(get_app_config_path()?.join("config.toml")) +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ConnectorConfig { + name: String, + address: String, +} + +impl ConnectorConfig { + pub fn name(&self) -> &str { + &self.name + } + + pub fn address(&self) -> &str { + &self.address + } +} diff --git a/edc-connector-tui/src/main.rs b/edc-connector-tui/src/main.rs new file mode 100644 index 0000000..09e04ef --- /dev/null +++ b/edc-connector-tui/src/main.rs @@ -0,0 +1,55 @@ +use app::App; +use config::{default_file, Config}; +use runner::Runner; +use std::time::Duration; + +mod app; +mod components; +mod config; +mod runner; +mod types; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let config = Config::parse(&default_file()?)?; + tui::install_panic_hook(); + let terminal = tui::init_terminal()?; + let mut runner = Runner::new(Duration::from_millis(250), App::init(config)); + runner.run(terminal).await?; + tui::restore_terminal()?; + Ok(()) +} + +mod tui { + use crossterm::{ + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, + }; + use ratatui::prelude::*; + use std::{ + io::{self, stdout}, + panic, + }; + + pub fn init_terminal() -> io::Result> { + enable_raw_mode()?; + stdout().execute(EnterAlternateScreen)?; + let terminal = Terminal::new(CrosstermBackend::new(stdout()))?; + Ok(terminal) + } + + pub fn restore_terminal() -> io::Result<()> { + stdout().execute(LeaveAlternateScreen)?; + disable_raw_mode()?; + Ok(()) + } + + pub fn install_panic_hook() { + let original_hook = panic::take_hook(); + panic::set_hook(Box::new(move |panic_info| { + stdout().execute(LeaveAlternateScreen).unwrap(); + disable_raw_mode().unwrap(); + original_hook(panic_info); + })); + } +} diff --git a/edc-connector-tui/src/runner.rs b/edc-connector-tui/src/runner.rs new file mode 100644 index 0000000..c1a02b7 --- /dev/null +++ b/edc-connector-tui/src/runner.rs @@ -0,0 +1,68 @@ +use std::{collections::VecDeque, time::Duration}; + +use crossterm::event; +use ratatui::{backend::Backend, Terminal}; + +use crate::components::{Action, ActionHandler, Component, ComponentEvent}; + +pub struct Runner { + tick_rate: Duration, + component: C, +} + +impl::Msg> + Send> Runner { + pub fn new(tick_rate: Duration, component: C) -> Self { + Self { + tick_rate, + component, + } + } + + pub async fn run(&mut self, mut terminal: Terminal) -> anyhow::Result<()> { + terminal.clear()?; + + let mut should_quit = false; + loop { + if should_quit { + break; + } + terminal.draw(|frame| self.component.view(frame, frame.size()))?; + + if event::poll(self.tick_rate)? { + let evt = event::read()?; + let mut msgs = self + .component + .handle_event(ComponentEvent::Event(evt))? + .into_iter() + .collect::>(); + + while let Some(msg) = msgs.pop_front() { + let actions = { + let ret = self.component.update(msg).await?; + + for m in ret.msgs { + msgs.push_back(m); + } + + for c in ret.cmds { + for m in c.await.unwrap() { + msgs.push_back(m); + } + } + + ret.actions + }; + + for a in actions { + should_quit = should_quit || matches!(a, Action::Quit); + for m in self.component.handle_action(a)? { + msgs.push_back(m) + } + } + } + }; + } + + Ok(()) + } +} diff --git a/edc-connector-tui/src/types.rs b/edc-connector-tui/src/types.rs new file mode 100644 index 0000000..69adb34 --- /dev/null +++ b/edc-connector-tui/src/types.rs @@ -0,0 +1,3 @@ +pub mod connector; +pub mod info; +pub mod nav; diff --git a/edc-connector-tui/src/types/connector.rs b/edc-connector-tui/src/types/connector.rs new file mode 100644 index 0000000..e0feb0f --- /dev/null +++ b/edc-connector-tui/src/types/connector.rs @@ -0,0 +1,33 @@ +use std::fmt::Debug; + +use edc_connector_client::EdcConnectorClient; + +use crate::config::ConnectorConfig; + +#[derive(Clone)] +pub struct Connector { + config: ConnectorConfig, + client: EdcConnectorClient, +} + +impl Connector { + pub fn new(config: ConnectorConfig, client: EdcConnectorClient) -> Self { + Self { config, client } + } + + pub fn config(&self) -> &ConnectorConfig { + &self.config + } + + pub fn client(&self) -> &EdcConnectorClient { + &self.client + } +} + +impl Debug for Connector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Connctor") + .field("config", &self.config) + .finish() + } +} diff --git a/edc-connector-tui/src/types/info.rs b/edc-connector-tui/src/types/info.rs new file mode 100644 index 0000000..0768476 --- /dev/null +++ b/edc-connector-tui/src/types/info.rs @@ -0,0 +1,43 @@ +use std::collections::BTreeMap; + +#[derive(Default, Debug)] +pub struct InfoSheet { + info: BTreeMap, + key_bindings: BTreeMap, +} + +impl InfoSheet { + pub fn info(mut self, key: impl Into, value: impl Into) -> Self { + self.info.insert(key.into(), value.into()); + self + } + + pub fn key_binding(mut self, key: impl Into, value: impl Into) -> Self { + self.key_bindings.insert(key.into(), value.into()); + self + } + + pub fn iter_info(&self) -> impl Iterator { + self.info.iter() + } + + pub fn iter_key_bindings(&self) -> impl Iterator { + self.key_bindings.iter() + } + + pub fn merge(&self, other: InfoSheet) -> InfoSheet { + let info = self + .iter_info() + .chain(other.iter_info()) + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>(); + + let key_bindings = self + .iter_key_bindings() + .chain(other.iter_key_bindings()) + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>(); + + InfoSheet { info, key_bindings } + } +} diff --git a/edc-connector-tui/src/types/nav.rs b/edc-connector-tui/src/types/nav.rs new file mode 100644 index 0000000..3f0cc79 --- /dev/null +++ b/edc-connector-tui/src/types/nav.rs @@ -0,0 +1,68 @@ +use std::str::FromStr; + +use anyhow::bail; +use enum_ordinalize::Ordinalize; +use strum::{EnumString, VariantNames}; + +#[derive(Debug, Clone, Default)] +pub enum Nav { + #[default] + ConnectorsList, + AssetsList, + PoliciesList, + ContractDefinitionsList, +} + +impl FromStr for Nav { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "connectors" => Ok(Nav::ConnectorsList), + "assets" => Ok(Nav::AssetsList), + "policies" => Ok(Nav::PoliciesList), + _ => bail!("Command {} not recognized", s), + } + } +} + +#[derive(Debug, Default, Ordinalize, Clone, EnumString, VariantNames)] +#[repr(usize)] +pub enum Menu { + #[default] + Connectors, + Assets, + Policies, + ContractDefinitions, +} + +impl Menu { + pub fn names() -> Vec { + ::VARIANTS + .iter() + .map(|s| s.to_string()) + .collect() + } +} + +impl Into for Nav { + fn into(self) -> Menu { + match self { + Nav::ConnectorsList => Menu::Connectors, + Nav::AssetsList => Menu::Assets, + Nav::PoliciesList => Menu::Policies, + Nav::ContractDefinitionsList => Menu::ContractDefinitions, + } + } +} + +impl Into