├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile-CI-Linux ├── LICENSE ├── Makefile ├── README.md ├── dev-tools ├── create_topic.sh ├── fake_topic_names ├── list_consumers.sh ├── read_topic.sh └── write_topic.sh ├── docker-compose.yml ├── rustfmt.toml ├── src ├── error_codes.rs ├── event_bus │ ├── event_bus_test.rs │ └── mod.rs ├── main.rs ├── state.rs ├── user_interface │ ├── mod.rs │ ├── offset_progress_bar.rs │ ├── selectable_list.rs │ ├── ui.rs │ └── user_input.rs └── util │ ├── mod.rs │ ├── paged_vec.rs │ └── utils.rs └── tag-release /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target/ 3 | *.iml 4 | scratchpad 5 | /.DS_Store 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: stable 3 | services: docker 4 | 5 | osx_image: xcode9.4 #OSX 10.13 6 | dist: trusty 7 | 8 | matrix: 9 | include: 10 | - os: osx 11 | env: RUST_TARGET=x86_64-apple-darwin OS_TARGET=osx-10.13 MACOSX_DEPLOYMENT_TARGET=10.13 12 | before_deploy: 13 | - make package 14 | - os: linux 15 | env: RUST_TARGET=x86_64-unknown-linux-gnu OS_TARGET=ubuntu_trusty 16 | before_deploy: 17 | - make linux-package 18 | - os: linux 19 | env: RUST_TARGET=x86_64-unknown-linux-gnu OS_TARGET=centos_7 20 | before_deploy: 21 | - make linux-package 22 | 23 | script: 24 | - make test 25 | 26 | deploy: 27 | provider: releases 28 | skip_cleanup: true 29 | api_key: 30 | secure: f7OAL6Pyo4AW6RJaNkR0oXDmc6H/2ux8ig6znvAnxbbWYNZXf15pea1MnmdjYzEdI+KMsIc5y5t9Ts3Sr/XclsKH/XZ3lobV6leQX5MK03Qtbe3VsdKjk6P5vpMC725rTV4b8i1SLqJP+Sa7ZUzoKbYwH+NPUpLJewtnf3KsfmLybJThfJ65p/2eUPnQHQUturZUuTd+NpWVVZPGWmNpmLT39EpizKHt+SdsgpA7faLN8QjeSPnAD8Opj+ZvJMJ4PoqXUOD759oX0p0GzYNyHVc3N59JvUhaIKmINN9zLwRKAXe6m+vtjpMEqo+W98VAzEK7IRYi8KnMuAtCzf7EYOYTyjGrWrLNH+sGQvXOiwTJ20gDmhJRHDIHWzBG/aruUj9ba4+LN5Tqromtv84ZycmjYM9OrxG1Dc7l8EQtCn92S4ZanUAPCfH8l5p5j4yvCdt2Oqoxsw9CZtWyl7c1qSQqtsM2W0i2IXSYfW9M0Wl52EEGWdoVssG6E6fJt1fKZhCIFhT7UpSTI34p4RweknBGJem9l0PWbVNwTjugdmfKa5nAuceevEy/m9B+E5CtNa+982CA9SGPLawpKd6Pe+yQJ1fhtIimDDRiBtrEXuk3KbWGUNxQBrDl+ykk+3GoivH6lUAn2v0cwmsoziVtFe2cCGLPun9DuMZOAUspGEQ= 31 | file: topiks_${RUST_TARGET}_${OS_TARGET}.tar.gz 32 | on: 33 | repo: kdrakon/topiks 34 | draft: true 35 | tags: true 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.0-alpha+003 4 | ### Changed 5 | - Switched to Rust 2018 edition 6 | - Reduced terminal screen writing (i.e. grouping certain screen `write!`'s into single batched `write!`'s) 7 | - Reduced or removed heap alloc when: 8 | - printing partition metadata to the screen 9 | - printing topic names to the screen 10 | - creating paged vectors (`PagedVec`) 11 | 12 | ### Fixed 13 | - Was unnecessarily creating a new _screen_ when using Termion. Now, a single mutable screen is created and used for the lifetime of the program. This now means the screen buffer is correctly used and the application has to explicitly clear the screen (or part of it) when updating. This has the effect of greatly reducing notable screen tearing. 14 | 15 | 16 | ## 0.1.0-alpha+002 17 | ### Added 18 | - You can now create topics by entering `c` on the topics view. The `-M` flag must be set when running Topiks to allow topic creation. The expected input is `[topic name]:[partitions]:[replication factor]`. Topic config will be set to the default cluster settings, which can be changed via Topiks after successful creation. Topic names are limited to 249 alphanumeric characters, `_`, or `.`. 19 | 20 | ### Changed 21 | - Separated the TCP Kafka API client and protocol code into https://github.com/kdrakon/topiks-kafka-client 22 | Updated the consumer offset progress bar to include partial blocks using block element unicode characters. 23 | 24 | ### Fixed 25 | - Corrected alphabetical sorting of topics 26 | 27 | ## 0.1.0-alpha+001 28 | First build 29 | - compatible with Apache Kafka >=2.0 30 | - list topics, configurations, and offsets 31 | - selectively delete topics 32 | - modify a topics configuration 33 | - get offset and lag for a consumer group 34 | - TLS/SSL capable via rust-native-tls crate (OpenSSL on Linux, security-framework on OSX) 35 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "MacTypes-sys" 3 | version = "2.1.0" 4 | source = "registry+https://github.com/rust-lang/crates.io-index" 5 | dependencies = [ 6 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 7 | ] 8 | 9 | [[package]] 10 | name = "aho-corasick" 11 | version = "0.6.10" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | dependencies = [ 14 | "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 15 | ] 16 | 17 | [[package]] 18 | name = "ansi_term" 19 | version = "0.11.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | dependencies = [ 22 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 23 | ] 24 | 25 | [[package]] 26 | name = "atty" 27 | version = "0.2.11" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | dependencies = [ 30 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 31 | "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 32 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 33 | ] 34 | 35 | [[package]] 36 | name = "autocfg" 37 | version = "0.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | 40 | [[package]] 41 | name = "bit-set" 42 | version = "0.5.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | dependencies = [ 45 | "bit-vec 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 46 | ] 47 | 48 | [[package]] 49 | name = "bit-vec" 50 | version = "0.5.0" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | 53 | [[package]] 54 | name = "bitflags" 55 | version = "1.0.4" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | 58 | [[package]] 59 | name = "byteorder" 60 | version = "1.3.1" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | 63 | [[package]] 64 | name = "cc" 65 | version = "1.0.31" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | 68 | [[package]] 69 | name = "cfg-if" 70 | version = "0.1.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | 73 | [[package]] 74 | name = "clap" 75 | version = "2.32.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | dependencies = [ 78 | "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 79 | "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 80 | "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 81 | "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 82 | "textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", 83 | "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 84 | "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 85 | ] 86 | 87 | [[package]] 88 | name = "cloudabi" 89 | version = "0.0.3" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | dependencies = [ 92 | "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 93 | ] 94 | 95 | [[package]] 96 | name = "core-foundation" 97 | version = "0.5.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | dependencies = [ 100 | "core-foundation-sys 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 101 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 102 | ] 103 | 104 | [[package]] 105 | name = "core-foundation-sys" 106 | version = "0.5.1" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | dependencies = [ 109 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 110 | ] 111 | 112 | [[package]] 113 | name = "fnv" 114 | version = "1.0.6" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | 117 | [[package]] 118 | name = "foreign-types" 119 | version = "0.3.2" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | dependencies = [ 122 | "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 123 | ] 124 | 125 | [[package]] 126 | name = "foreign-types-shared" 127 | version = "0.1.1" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | 130 | [[package]] 131 | name = "fuchsia-cprng" 132 | version = "0.1.1" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | 135 | [[package]] 136 | name = "lazy_static" 137 | version = "1.3.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | 140 | [[package]] 141 | name = "libc" 142 | version = "0.2.50" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | 145 | [[package]] 146 | name = "log" 147 | version = "0.4.6" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | dependencies = [ 150 | "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 151 | ] 152 | 153 | [[package]] 154 | name = "memchr" 155 | version = "2.2.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | 158 | [[package]] 159 | name = "native-tls" 160 | version = "0.2.2" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | dependencies = [ 163 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 164 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 165 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 166 | "openssl 0.10.19 (registry+https://github.com/rust-lang/crates.io-index)", 167 | "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 168 | "openssl-sys 0.9.42 (registry+https://github.com/rust-lang/crates.io-index)", 169 | "schannel 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", 170 | "security-framework 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 171 | "security-framework-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 172 | "tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)", 173 | ] 174 | 175 | [[package]] 176 | name = "num-traits" 177 | version = "0.2.6" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | 180 | [[package]] 181 | name = "openssl" 182 | version = "0.10.19" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | dependencies = [ 185 | "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 186 | "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 187 | "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 188 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 189 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 190 | "openssl-sys 0.9.42 (registry+https://github.com/rust-lang/crates.io-index)", 191 | ] 192 | 193 | [[package]] 194 | name = "openssl-probe" 195 | version = "0.1.2" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | 198 | [[package]] 199 | name = "openssl-sys" 200 | version = "0.9.42" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | dependencies = [ 203 | "cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", 204 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 205 | "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 206 | "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 207 | "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 208 | ] 209 | 210 | [[package]] 211 | name = "pkg-config" 212 | version = "0.3.14" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | 215 | [[package]] 216 | name = "proptest" 217 | version = "0.8.7" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | dependencies = [ 220 | "bit-set 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 221 | "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 222 | "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 223 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 224 | "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 225 | "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 226 | "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", 227 | "regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", 228 | "rusty-fork 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 229 | "tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)", 230 | ] 231 | 232 | [[package]] 233 | name = "quick-error" 234 | version = "1.2.2" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | 237 | [[package]] 238 | name = "rand" 239 | version = "0.5.6" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | dependencies = [ 242 | "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 243 | "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 244 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 245 | "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 246 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 247 | ] 248 | 249 | [[package]] 250 | name = "rand" 251 | version = "0.6.5" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | dependencies = [ 254 | "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 255 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 256 | "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 257 | "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 258 | "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 259 | "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 260 | "rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 261 | "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 262 | "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 263 | "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 264 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 265 | ] 266 | 267 | [[package]] 268 | name = "rand_chacha" 269 | version = "0.1.1" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | dependencies = [ 272 | "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 273 | "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 274 | ] 275 | 276 | [[package]] 277 | name = "rand_core" 278 | version = "0.3.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | dependencies = [ 281 | "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 282 | ] 283 | 284 | [[package]] 285 | name = "rand_core" 286 | version = "0.4.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | 289 | [[package]] 290 | name = "rand_hc" 291 | version = "0.1.0" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | dependencies = [ 294 | "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 295 | ] 296 | 297 | [[package]] 298 | name = "rand_isaac" 299 | version = "0.1.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | dependencies = [ 302 | "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 303 | ] 304 | 305 | [[package]] 306 | name = "rand_jitter" 307 | version = "0.1.3" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | dependencies = [ 310 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 311 | "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 312 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 313 | ] 314 | 315 | [[package]] 316 | name = "rand_os" 317 | version = "0.1.3" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | dependencies = [ 320 | "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 321 | "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 322 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 323 | "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 324 | "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 325 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 326 | ] 327 | 328 | [[package]] 329 | name = "rand_pcg" 330 | version = "0.1.2" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | dependencies = [ 333 | "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 334 | "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 335 | ] 336 | 337 | [[package]] 338 | name = "rand_xorshift" 339 | version = "0.1.1" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | dependencies = [ 342 | "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 343 | ] 344 | 345 | [[package]] 346 | name = "rdrand" 347 | version = "0.4.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | dependencies = [ 350 | "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 351 | ] 352 | 353 | [[package]] 354 | name = "redox_syscall" 355 | version = "0.1.51" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | 358 | [[package]] 359 | name = "redox_termios" 360 | version = "0.1.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | dependencies = [ 363 | "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", 364 | ] 365 | 366 | [[package]] 367 | name = "regex" 368 | version = "1.1.2" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | dependencies = [ 371 | "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", 372 | "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 373 | "regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", 374 | "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 375 | "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 376 | ] 377 | 378 | [[package]] 379 | name = "regex-syntax" 380 | version = "0.6.5" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | dependencies = [ 383 | "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 384 | ] 385 | 386 | [[package]] 387 | name = "remove_dir_all" 388 | version = "0.5.1" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | dependencies = [ 391 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 392 | ] 393 | 394 | [[package]] 395 | name = "rustc_version" 396 | version = "0.2.3" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | dependencies = [ 399 | "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", 400 | ] 401 | 402 | [[package]] 403 | name = "rusty-fork" 404 | version = "0.2.1" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | dependencies = [ 407 | "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", 408 | "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 409 | "tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)", 410 | "wait-timeout 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 411 | ] 412 | 413 | [[package]] 414 | name = "schannel" 415 | version = "0.1.15" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | dependencies = [ 418 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 419 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 420 | ] 421 | 422 | [[package]] 423 | name = "security-framework" 424 | version = "0.2.2" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | dependencies = [ 427 | "core-foundation 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 428 | "core-foundation-sys 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 429 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 430 | "security-framework-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 431 | ] 432 | 433 | [[package]] 434 | name = "security-framework-sys" 435 | version = "0.2.3" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | dependencies = [ 438 | "MacTypes-sys 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 439 | "core-foundation-sys 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 440 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 441 | ] 442 | 443 | [[package]] 444 | name = "semver" 445 | version = "0.9.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | dependencies = [ 448 | "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 449 | ] 450 | 451 | [[package]] 452 | name = "semver-parser" 453 | version = "0.7.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | 456 | [[package]] 457 | name = "strsim" 458 | version = "0.7.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | 461 | [[package]] 462 | name = "tempfile" 463 | version = "3.0.7" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | dependencies = [ 466 | "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 467 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 468 | "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", 469 | "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", 470 | "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 471 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 472 | ] 473 | 474 | [[package]] 475 | name = "termion" 476 | version = "1.5.1" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | dependencies = [ 479 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 480 | "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", 481 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 482 | ] 483 | 484 | [[package]] 485 | name = "textwrap" 486 | version = "0.10.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | dependencies = [ 489 | "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 490 | ] 491 | 492 | [[package]] 493 | name = "thread_local" 494 | version = "0.3.6" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | dependencies = [ 497 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 498 | ] 499 | 500 | [[package]] 501 | name = "topiks" 502 | version = "0.1.0-alpha+003" 503 | dependencies = [ 504 | "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", 505 | "proptest 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)", 506 | "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 507 | "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 508 | "topiks-kafka-client 0.1.0-alpha+003 (git+https://github.com/kdrakon/topiks-kafka-client?tag=0.1.0-alpha+003)", 509 | ] 510 | 511 | [[package]] 512 | name = "topiks-kafka-client" 513 | version = "0.1.0-alpha+003" 514 | source = "git+https://github.com/kdrakon/topiks-kafka-client?tag=0.1.0-alpha+003#ead3e6df10d897079c248536149ec2a686c3a7b5" 515 | dependencies = [ 516 | "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 517 | "native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 518 | ] 519 | 520 | [[package]] 521 | name = "ucd-util" 522 | version = "0.1.3" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | 525 | [[package]] 526 | name = "unicode-width" 527 | version = "0.1.5" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | 530 | [[package]] 531 | name = "utf8-ranges" 532 | version = "1.0.2" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | 535 | [[package]] 536 | name = "vcpkg" 537 | version = "0.2.6" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | 540 | [[package]] 541 | name = "vec_map" 542 | version = "0.8.1" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | 545 | [[package]] 546 | name = "wait-timeout" 547 | version = "0.1.5" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | dependencies = [ 550 | "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", 551 | ] 552 | 553 | [[package]] 554 | name = "winapi" 555 | version = "0.3.6" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | dependencies = [ 558 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 559 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 560 | ] 561 | 562 | [[package]] 563 | name = "winapi-i686-pc-windows-gnu" 564 | version = "0.4.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | 567 | [[package]] 568 | name = "winapi-x86_64-pc-windows-gnu" 569 | version = "0.4.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | 572 | [metadata] 573 | "checksum MacTypes-sys 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eaf9f0d0b1cc33a4d2aee14fb4b2eac03462ef4db29c8ac4057327d8a71ad86f" 574 | "checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" 575 | "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 576 | "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" 577 | "checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799" 578 | "checksum bit-set 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6f1efcc46c18245a69c38fcc5cc650f16d3a59d034f3106e9ed63748f695730a" 579 | "checksum bit-vec 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4440d5cb623bb7390ae27fec0bb6c61111969860f8e3ae198bfa0663645e67cf" 580 | "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" 581 | "checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb" 582 | "checksum cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)" = "c9ce8bb087aacff865633f0bd5aeaed910fe2fe55b55f4739527f2e023a2e53d" 583 | "checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" 584 | "checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" 585 | "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 586 | "checksum core-foundation 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "286e0b41c3a20da26536c6000a280585d519fd07b3956b43aed8a79e9edce980" 587 | "checksum core-foundation-sys 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "716c271e8613ace48344f723b60b900a93150271e5be206212d052bbc0883efa" 588 | "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" 589 | "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 590 | "checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 591 | "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 592 | "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" 593 | "checksum libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "aab692d7759f5cd8c859e169db98ae5b52c924add2af5fbbca11d12fefb567c1" 594 | "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" 595 | "checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39" 596 | "checksum native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff8e08de0070bbf4c31f452ea2a70db092f36f6f2e4d897adf5674477d488fb2" 597 | "checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" 598 | "checksum openssl 0.10.19 (registry+https://github.com/rust-lang/crates.io-index)" = "84321fb9004c3bce5611188a644d6171f895fa2889d155927d528782edb21c5d" 599 | "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 600 | "checksum openssl-sys 0.9.42 (registry+https://github.com/rust-lang/crates.io-index)" = "cb534d752bf98cf363b473950659ac2546517f9c6be9723771614ab3f03bbc9e" 601 | "checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" 602 | "checksum proptest 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)" = "926d0604475349f463fe44130aae73f2294b5309ab2ca0310b998bd334ef191f" 603 | "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" 604 | "checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" 605 | "checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" 606 | "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" 607 | "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 608 | "checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0" 609 | "checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" 610 | "checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" 611 | "checksum rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b9ea758282efe12823e0d952ddb269d2e1897227e464919a554f2a03ef1b832" 612 | "checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 613 | "checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" 614 | "checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" 615 | "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 616 | "checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85" 617 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 618 | "checksum regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53ee8cfdddb2e0291adfb9f13d31d3bbe0a03c9a402c01b1e24188d86c35b24f" 619 | "checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861" 620 | "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" 621 | "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" 622 | "checksum rusty-fork 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9591f190d2852720b679c21f66ad929f9f1d7bb09d1193c26167586029d8489c" 623 | "checksum schannel 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "f2f6abf258d99c3c1c5c2131d99d064e94b7b3dd5f416483057f308fea253339" 624 | "checksum security-framework 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfab8dda0e7a327c696d893df9ffa19cadc4bd195797997f5223cf5831beaf05" 625 | "checksum security-framework-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3d6696852716b589dff9e886ff83778bb635150168e83afa8ac6b8a78cb82abc" 626 | "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 627 | "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 628 | "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" 629 | "checksum tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b86c784c88d98c801132806dadd3819ed29d8600836c4088e855cdf3e178ed8a" 630 | "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 631 | "checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6" 632 | "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" 633 | "checksum topiks-kafka-client 0.1.0-alpha+003 (git+https://github.com/kdrakon/topiks-kafka-client?tag=0.1.0-alpha+003)" = "" 634 | "checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" 635 | "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" 636 | "checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" 637 | "checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d" 638 | "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 639 | "checksum wait-timeout 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b9f3bf741a801531993db6478b95682117471f76916f5e690dd8d45395b09349" 640 | "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" 641 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 642 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 643 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "topiks" 3 | version = "0.1.0-alpha+003" 4 | authors = ["Sean Policarpio "] 5 | license = "MIT" 6 | readme = "README.md" 7 | repository = "https://github.com/kdrakon/topiks" 8 | keywords = ["cli", "kafka"] 9 | edition = "2018" 10 | 11 | [profile.release] 12 | lto = true 13 | panic = 'abort' 14 | 15 | [dependencies.topiks-kafka-client] 16 | git = "https://github.com/kdrakon/topiks-kafka-client" 17 | tag = "0.1.0-alpha+003" 18 | 19 | [dependencies] 20 | termion = "1.5.1" 21 | clap = "2.31.2" 22 | regex = "1.1.2" 23 | 24 | [dev-dependencies] 25 | proptest = "0.8.7" 26 | 27 | -------------------------------------------------------------------------------- /Dockerfile-CI-Linux: -------------------------------------------------------------------------------- 1 | FROM ubuntu:trusty as ubuntu_trusty 2 | RUN apt-get -y update 3 | RUN apt-get -y install curl build-essential pkg-config libssl-dev 4 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 5 | WORKDIR topiks 6 | CMD bash -c 'source ~/.cargo/env && make package' 7 | 8 | FROM centos:7 as centos_7 9 | RUN yum -y groupinstall "Development Tools" 10 | RUN yum -y install openssl-devel 11 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 12 | ENV SHA_COMMAND sha512sum 13 | WORKDIR topiks 14 | CMD bash -c 'source ~/.cargo/env && make package' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sean Policarpio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHA_COMMAND ?= shasum -a 512 2 | RUST_TARGET ?= x86_64-apple-darwin 3 | OS_TARGET ?= osx-10.13 4 | 5 | clean: 6 | cargo clean 7 | 8 | test: clean 9 | cargo test 10 | 11 | build: clean 12 | rustup target add ${RUST_TARGET} 13 | cargo build -v --release --target ${RUST_TARGET} 14 | strip target/${RUST_TARGET}/release/topiks 15 | file target/${RUST_TARGET}/release/topiks 16 | 17 | package: build 18 | cd target/${RUST_TARGET}/release/ && ${SHA_COMMAND} topiks > checksum-sha512 19 | tar vczf topiks_${RUST_TARGET}_${OS_TARGET}.tar.gz -C target/${RUST_TARGET}/release topiks checksum-sha512 20 | 21 | linux-package: 22 | docker build --target ${OS_TARGET} -t ${OS_TARGET} -f Dockerfile-CI-Linux . 23 | docker run --rm -v ${PWD}:/topiks -e RUST_TARGET=${RUST_TARGET} -e OS_TARGET=${OS_TARGET} ${OS_TARGET} 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # topiks 2 | 3 | [![Build Status](https://travis-ci.org/kdrakon/topiks.svg?branch=master)](https://travis-ci.org/kdrakon/topiks) 4 | [![Release](https://img.shields.io/github/tag-date/kdrakon/topiks.svg?style=popout)](https://github.com/kdrakon/topiks/releases) 5 | 6 | An interactive CLI tool for managing Kafka topics. 7 | 8 | ## Features 9 | - compatible with Apache Kafka >=2.0 10 | - list topics, configurations, and offsets 11 | - interactively create topics 12 | - selectively delete topics 13 | - modify a topics configuration 14 | - _modify a topics replication factor (**WIP**)_ 15 | - _increase the partitions for a topic (**WIP**)_ 16 | - get offset and lag for a consumer group 17 | - TLS/SSL protocol support 18 | 19 | ## Usage 20 | ``` 21 | USAGE: 22 | topiks [FLAGS] [OPTIONS] 23 | 24 | FLAGS: 25 | -D Enable topic/config deletion 26 | -h, --help Prints help information 27 | -M Enable creation of topics and modification of topic configurations 28 | --no-delete-confirmation Disable delete confirmation 29 | --tls Enable TLS 30 | -V, --version Prints version information 31 | 32 | OPTIONS: 33 | -c, --consumer-group Consumer group for fetching offsets 34 | 35 | ARGS: 36 | A single Kafka broker [DOMAIN|IP]:PORT 37 | ``` 38 | 39 | ### Commands 40 | ``` 41 | h → Toggle this screen 42 | q → Quit 43 | t → Toggle topics view 44 | i → Toggle topic config view 45 | p → Toggle partitions view 46 | / → Enter search query for topic name 47 | n → Find next search result 48 | r → Refresh. Retrieves metadata from Kafka cluster 49 | c → Create a new topic with [topic]:[partitions]:[replication factor] 50 | : → Modify a resource (e.g. topic config) via text input 51 | d → Delete a resource. Will delete a topic or reset a topic config 52 | Up⬆ → Move up one topic 53 | Down⬇ → Move down one topic 54 | PgUp⇞ → Move up ten topics 55 | PgDown⇟ → Move down ten topics 56 | Home⤒ → Go to first topic 57 | End⤓ → Go to last topic 58 | ``` 59 | 60 | ## Build your own 61 | 1. Install rust and Cargo: https://rustup.rs/ 62 | 1. `RUST_TARGET=??? make build` where `RUST_TARGET` is the rust toolchain. For example, `x86_64-apple-darwin` or `x86_64-unknown-linux-gnu` 63 | 1. The binary will be in `target/${RUST_TARGET}/release/` 64 | 65 | ## FAQ 66 | 67 | > What was the main reason for building this? 68 | 69 | I found a substantial amount of my time developing Kafka applications—namely via Kafka Streams—involved keeping an eye on the consumer groups I was using. This primarily meant tracking the group offsets of the topics my applications were reading and writing. With a mix of `grep`, `awk`, and `sed` commands, we would do this by periodically reading data from `kafka-consumer-groups`. Although this worked, it wasn't ideal when we started working on other applications or had different topics and consumer groups we wanted to track. 70 | 71 | Furthermore, when developing new Kafka applications, we found ourselves periodically creating and deleting topics in our local and test environments. 72 | 73 | For these reasons and more, this led me to imagining a Kafka topics tool that would be in the same vein as [`htop`](https://github.com/hishamhm/htop). 74 | 75 | > Why can't you read or write to topics? 76 | 77 | Although that feature wouldn't be completely difficult to implement, there are two reasons I prefer not to do so: 78 | 1. In almost all of my experience with Kafka, the data we read/write is in some schema format (e.g. Avro). This in-turn implies in most cases cumbersome work for the user to both correctly produce and consume via the command-line. 79 | 1. Topiks would have to work with its own consumer group or one specified by the user. I'd prefer to minimize the impact that Topiks leaves on your Kafka cluster, such as creating throwaway consumer groups. 80 | 81 | > How do you support TLS? 82 | 83 | TLS/SSL is capable via the [rust-native-tls](https://github.com/sfackler/rust-native-tls) crate. This library provides bindings to native TLS implementations based on your operating system. For this reason, you may find the binaries I release not compatible with your OS. The simple solution is to then build Topiks on your machine. 84 | 85 | 86 | ## A Quick Look 87 | 88 | - Listing and searching for topics 89 | 90 | ![Listing and searching for topics](https://media.giphy.com/media/9Pgz8yJOB8RDTOyDPL/source.gif) 91 | 92 | - Looking at partitions and offsets. In this case, we can see the consumer group has read three messages across the partitions. 93 | 94 | ![Looking at partitions and offsets](https://media.giphy.com/media/fjyp0OZqs0TXIZbeil/source.gif) 95 | 96 | - Topic configuration. In this case, Topiks applies a config override. To reset the override, simply _delete_ (`d`) the config and it will revert to the global default. 97 | 98 | ![Topic configuration](https://media.giphy.com/media/fQovPSOuIwB6BIle7q/source.gif) 99 | 100 | - Topic creation and deletion. 101 | 102 | ![Topic creation and deletion](https://media.giphy.com/media/9oIFgyK3paNmmo1RUW/source.gif) 103 | 104 | - You can also override the delete confirmation if you're absolutely sure you know what you're doing. 105 | 106 | ![Topic deletion with no confirmation](https://media.giphy.com/media/9D6PuYyr7rdXWUDozH/source.gif) 107 | 108 | -------------------------------------------------------------------------------- /dev-tools/create_topic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Creating topic \"$1\"" 4 | 5 | docker exec topiks_broker_1 \ 6 | kafka-topics --zookeeper zookeeper:2181 --create --if-not-exists --topic $1 --partitions 16 --replication-factor 1 7 | -------------------------------------------------------------------------------- /dev-tools/fake_topic_names: -------------------------------------------------------------------------------- 1 | Lorem 2 | ipsum 3 | dolor 4 | sit 5 | amet 6 | consectetur 7 | adipiscing 8 | elit 9 | Sed 10 | placerat 11 | nunc 12 | vitae 13 | nibh 14 | pharetra 15 | elementum 16 | Donec 17 | ultricies 18 | augue 19 | accumsan 20 | sapien 21 | pharetra 22 | ac 23 | aliquet 24 | purus 25 | cursus 26 | Lorem 27 | ipsum 28 | dolor 29 | sit 30 | amet 31 | consectetur 32 | adipiscing 33 | elit 34 | Quisque 35 | quis 36 | tincidunt 37 | justo 38 | eget 39 | dapibus 40 | purus 41 | Pellentesque 42 | ac 43 | dolor 44 | facilisis 45 | placerat 46 | turpis 47 | quis 48 | sollicitudin 49 | neque 50 | Nulla 51 | laoreet 52 | augue 53 | id 54 | interdum 55 | ullamcorper 56 | augue 57 | enim 58 | condimentum 59 | ligula 60 | at 61 | varius 62 | mauris 63 | eros 64 | id 65 | lacus 66 | Quisque 67 | eu 68 | nibh 69 | ac 70 | leo 71 | porttitor 72 | malesuada 73 | quis 74 | ut 75 | orci 76 | Nullam 77 | sollicitudin 78 | ex 79 | semper 80 | quam 81 | finibus 82 | in 83 | semper 84 | dolor 85 | eleifend 86 | Lorem 87 | ipsum 88 | dolor 89 | sit 90 | amet 91 | consectetur 92 | adipiscing 93 | elit 94 | Quisque 95 | commodo 96 | gravida 97 | lorem 98 | vulputate 99 | accumsan 100 | enim 101 | -------------------------------------------------------------------------------- /dev-tools/list_consumers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker exec topiks_broker_1 \ 4 | kafka-consumer-groups --bootstrap-server localhost:9092 --list -------------------------------------------------------------------------------- /dev-tools/read_topic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Reading topic \"$1\"" 4 | 5 | docker exec topiks_broker_1 \ 6 | kafka-console-consumer --bootstrap-server localhost:9092 --from-beginning --topic $1 --max-messages $2 --group $3 7 | -------------------------------------------------------------------------------- /dev-tools/write_topic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Writing to topic \"$1\"" 4 | 5 | docker exec topiks_broker_1 \ 6 | sh -c "echo $2 | kafka-console-producer --broker-list localhost:9092 --topic $1" 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | zookeeper: 5 | image: 'confluentinc/cp-zookeeper:5.0.0' 6 | restart: always 7 | hostname: zookeeper 8 | ports: 9 | - "2181:2181" 10 | environment: 11 | ZOO_MY_ID: 1 12 | ZOO_PORT: 2181 13 | ZOO_SERVERS: server.1=zookeeper:2888:3888 14 | COMPONENT: zookeeper 15 | ZOOKEEPER_CLIENT_PORT: 2181 16 | ZOOKEEPER_TICK_TIME: 2000 17 | TZ: Australia/Sydney 18 | extra_hosts: 19 | - "moby:127.0.0.1" 20 | 21 | broker: 22 | image: 'confluentinc/cp-kafka:5.0.0' 23 | hostname: broker_0 24 | stop_grace_period: 120s 25 | depends_on: 26 | - zookeeper 27 | ports: 28 | - "9092:9092" 29 | environment: 30 | TZ: Australia/Sydney 31 | COMPONENT: kafka 32 | KAFKA_BROKER_ID: 1000 33 | KAFKA_RESERVED_BROKER_MAX_ID: 10000 34 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 35 | KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://broker_0:9090,EXTERNAL://localhost:9092' 36 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT' 37 | KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL' 38 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 39 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 40 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'false' 41 | extra_hosts: 42 | - "moby:127.0.0.1" 43 | 44 | broker_1: 45 | image: 'confluentinc/cp-kafka:5.0.0' 46 | hostname: broker_1 47 | stop_grace_period: 120s 48 | depends_on: 49 | - broker 50 | ports: 51 | - "9093:9093" 52 | environment: 53 | TZ: Australia/Sydney 54 | COMPONENT: kafka 55 | KAFKA_BROKER_ID: 1001 56 | KAFKA_RESERVED_BROKER_MAX_ID: 10000 57 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 58 | KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://broker_1:9090,EXTERNAL://localhost:9093' 59 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT' 60 | KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL' 61 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 62 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 63 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'false' 64 | extra_hosts: 65 | - "moby:127.0.0.1" -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | reorder_imports = true 3 | reorder_modules = true 4 | use_small_heuristics = "Max" 5 | merge_derives = true 6 | max_width = 150 7 | use_field_init_shorthand = true -------------------------------------------------------------------------------- /src/error_codes.rs: -------------------------------------------------------------------------------- 1 | pub const COULD_NOT_PARSE_BOOTSTRAP_SERVER: u8 = 100; 2 | 3 | pub const KAFKA_API_VERIFICATION_FAIL: u8 = 200; 4 | -------------------------------------------------------------------------------- /src/event_bus/event_bus_test.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | 4 | use crate::api_client::ApiClientProvider; 5 | use crate::api_client::ApiClientTrait; 6 | use crate::api_client::ApiRequestError; 7 | use crate::kafka_protocol::protocol_request::Request; 8 | use crate::kafka_protocol::protocol_response::Response; 9 | use crate::kafka_protocol::protocol_serializable::ProtocolDeserializable; 10 | use crate::kafka_protocol::protocol_serializable::ProtocolSerializable; 11 | use crate::KafkaServerAddr; 12 | use crate::IO; 13 | 14 | use crate::event_bus; 15 | use crate::event_bus::*; 16 | use crate::state::State; 17 | use crate::state::StateFNError; 18 | use crate::state::{CurrentView, DialogMessage}; 19 | 20 | struct FakeApiClient(HashMap>); // ApiKey => Byte Response 21 | 22 | impl ApiClientTrait for FakeApiClient { 23 | fn request(&self, _server_addr: &KafkaServerAddr, request: Request) -> Result, ApiRequestError> 24 | where 25 | T: ProtocolSerializable, 26 | Vec: ProtocolDeserializable>, 27 | { 28 | let response = self.0.get(&request.header.api_key).expect(format!("ApiKey response not defined for {}", &request.header.api_key).as_str()); 29 | response.clone().into_protocol_type().map_err(|e| ApiRequestError::of(e.error)) 30 | } 31 | } 32 | 33 | fn swap_state(state: &RefCell, event: Event) { 34 | match event_bus::update_state(event, state.borrow_mut()) { 35 | Ok(state_result) => state.swap(&RefCell::new(state_result)), 36 | Err(StateFNError::Error(err)) => panic!(err), 37 | Err(StateFNError::Caused(err, _)) => panic!(err), 38 | } 39 | } 40 | 41 | fn test_bootstrap_server() -> KafkaServerAddr { 42 | KafkaServerAddr::of(String::from("fake"), 9092, false) 43 | } 44 | 45 | fn empty_api_client_provider() -> ApiClientProvider { 46 | Box::new(|| IO::new(Box::new(|| Ok(FakeApiClient(HashMap::new()))))) 47 | } 48 | 49 | fn test_api_client_provider(_responses: HashMap>) -> ApiClientProvider { 50 | Box::new(move || { 51 | let _responses = _responses.clone(); 52 | IO::new(Box::new(move || Ok(FakeApiClient(_responses.clone())))) 53 | }) 54 | } 55 | 56 | #[test] 57 | fn get_metadata_and_select_topics() { 58 | let state = RefCell::new(State::new()); 59 | 60 | let mut responses: HashMap> = HashMap::new(); 61 | // metadata 62 | responses.insert( 63 | 3, 64 | vec![ 65 | 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x09, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 66 | 0x68, 0x6F, 0x73, 0x74, 0x00, 0x00, 0x23, 0x85, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x09, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x68, 0x6F, 67 | 0x73, 0x74, 0x00, 0x00, 0x23, 0x84, 0xFF, 0xFF, 0x00, 0x16, 0x55, 0x37, 0x7A, 0x53, 0x31, 0x4A, 0x51, 0x6D, 0x51, 0x70, 0x6D, 0x66, 0x65, 68 | 0x6C, 0x6F, 0x5F, 0x63, 0x4E, 0x6D, 0x4E, 0x77, 0x51, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1B, 0x5F, 0x5F, 69 | 0x63, 0x6F, 0x6E, 0x66, 0x6C, 0x75, 0x65, 0x6E, 0x74, 0x2E, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x2E, 0x6D, 0x65, 0x74, 0x72, 0x69, 70 | 0x63, 0x73, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 71 | 0x03, 0xE9, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 72 | 0x00, 0x00, 0x03, 0x66, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 73 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 74 | 0x0B, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 75 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 76 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 77 | 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 78 | 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 79 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 80 | 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 81 | 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 82 | 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 83 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 84 | 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 85 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x03, 0xE9, 0x00, 86 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 87 | 0x00, 0x03, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 88 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 89 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 90 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 91 | 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 92 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 93 | 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x62, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 94 | 0x00, 0x00, 0x08, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 95 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 96 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 97 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 98 | 0x05, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 99 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 100 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 101 | 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 102 | 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 103 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 104 | 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 105 | 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x03, 106 | 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 107 | 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 108 | 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 109 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0xE9, 0x00, 110 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | 0x00, 0x0F, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 113 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 114 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 115 | ], 116 | ); 117 | // topic config for 'bar' 118 | responses.insert( 119 | 32, 120 | vec![ 121 | 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x02, 0x00, 0x03, 0x62, 0x61, 0x72, 0x00, 122 | 0x00, 0x00, 0x19, 0x00, 0x10, 0x63, 0x6F, 0x6D, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6F, 0x6E, 0x2E, 0x74, 0x79, 0x70, 0x65, 0x00, 0x08, 123 | 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x72, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0x6C, 0x65, 0x61, 0x64, 0x65, 0x72, 124 | 0x2E, 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x74, 0x68, 0x72, 0x6F, 0x74, 0x74, 0x6C, 0x65, 0x64, 0x2E, 125 | 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1D, 0x6D, 0x65, 0x73, 0x73, 126 | 0x61, 0x67, 0x65, 0x2E, 0x64, 0x6F, 0x77, 0x6E, 0x63, 0x6F, 0x6E, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x2E, 0x65, 0x6E, 0x61, 0x62, 127 | 0x6C, 0x65, 0x00, 0x04, 0x74, 0x72, 0x75, 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x6D, 0x69, 0x6E, 0x2E, 0x69, 0x6E, 128 | 0x73, 0x79, 0x6E, 0x63, 0x2E, 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x73, 0x00, 0x01, 0x31, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 129 | 0x00, 0x11, 0x73, 0x65, 0x67, 0x6D, 0x65, 0x6E, 0x74, 0x2E, 0x6A, 0x69, 0x74, 0x74, 0x65, 0x72, 0x2E, 0x6D, 0x73, 0x00, 0x01, 0x30, 0x00, 130 | 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x63, 0x6C, 0x65, 0x61, 0x6E, 0x75, 0x70, 0x2E, 0x70, 0x6F, 0x6C, 0x69, 0x63, 0x79, 0x00, 131 | 0x06, 0x64, 0x65, 0x6C, 0x65, 0x74, 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x66, 0x6C, 0x75, 0x73, 0x68, 0x2E, 0x6D, 132 | 0x73, 0x00, 0x13, 0x39, 0x32, 0x32, 0x33, 0x33, 0x37, 0x32, 0x30, 0x33, 0x36, 0x38, 0x35, 0x34, 0x37, 0x37, 0x35, 0x38, 0x30, 0x37, 0x00, 133 | 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x66, 0x6F, 0x6C, 0x6C, 0x6F, 0x77, 0x65, 0x72, 0x2E, 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 134 | 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x74, 0x68, 0x72, 0x6F, 0x74, 0x74, 0x6C, 0x65, 0x64, 0x2E, 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 0x61, 135 | 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x73, 0x65, 0x67, 0x6D, 0x65, 0x6E, 0x74, 0x2E, 0x62, 0x79, 0x74, 136 | 0x65, 0x73, 0x00, 0x0A, 0x31, 0x30, 0x37, 0x33, 0x37, 0x34, 0x31, 0x38, 0x32, 0x34, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 137 | 0x72, 0x65, 0x74, 0x65, 0x6E, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x6D, 0x73, 0x00, 0x09, 0x36, 0x30, 0x34, 0x38, 0x30, 0x30, 0x30, 0x30, 0x30, 138 | 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x66, 0x6C, 0x75, 0x73, 0x68, 0x2E, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 139 | 0x00, 0x13, 0x39, 0x32, 0x32, 0x33, 0x33, 0x37, 0x32, 0x30, 0x33, 0x36, 0x38, 0x35, 0x34, 0x37, 0x37, 0x35, 0x38, 0x30, 0x37, 0x00, 0x05, 140 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x2E, 0x76, 141 | 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x00, 0x07, 0x32, 0x2E, 0x30, 0x2D, 0x49, 0x56, 0x31, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 142 | 0x14, 0x66, 0x69, 0x6C, 0x65, 0x2E, 0x64, 0x65, 0x6C, 0x65, 0x74, 0x65, 0x2E, 0x64, 0x65, 0x6C, 0x61, 0x79, 0x2E, 0x6D, 0x73, 0x00, 0x05, 143 | 0x36, 0x30, 0x30, 0x30, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x6D, 0x61, 0x78, 0x2E, 0x6D, 0x65, 0x73, 0x73, 0x61, 144 | 0x67, 0x65, 0x2E, 0x62, 0x79, 0x74, 0x65, 0x73, 0x00, 0x07, 0x31, 0x30, 0x30, 0x30, 0x30, 0x31, 0x32, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 145 | 0x00, 0x00, 0x15, 0x6D, 0x69, 0x6E, 0x2E, 0x63, 0x6F, 0x6D, 0x70, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x6C, 0x61, 0x67, 0x2E, 0x6D, 146 | 0x73, 0x00, 0x01, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2E, 0x74, 0x69, 147 | 0x6D, 0x65, 0x73, 0x74, 0x61, 0x6D, 0x70, 0x2E, 0x74, 0x79, 0x70, 0x65, 0x00, 0x0A, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6D, 148 | 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x70, 0x72, 0x65, 0x61, 0x6C, 0x6C, 0x6F, 0x63, 0x61, 0x74, 0x65, 0x00, 0x05, 149 | 0x66, 0x61, 0x6C, 0x73, 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x6D, 0x69, 0x6E, 0x2E, 0x63, 0x6C, 0x65, 0x61, 0x6E, 150 | 0x61, 0x62, 0x6C, 0x65, 0x2E, 0x64, 0x69, 0x72, 0x74, 0x79, 0x2E, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x00, 0x03, 0x30, 0x2E, 0x35, 0x00, 0x05, 151 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x2E, 0x69, 0x6E, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6C, 0x2E, 0x62, 152 | 0x79, 0x74, 0x65, 0x73, 0x00, 0x04, 0x34, 0x30, 0x39, 0x36, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x75, 0x6E, 0x63, 0x6C, 153 | 0x65, 0x61, 0x6E, 0x2E, 0x6C, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2E, 0x65, 0x6C, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x65, 0x6E, 0x61, 154 | 0x62, 0x6C, 0x65, 0x00, 0x05, 0x66, 0x61, 0x6C, 0x73, 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x72, 0x65, 0x74, 0x65, 155 | 0x6E, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x62, 0x79, 0x74, 0x65, 0x73, 0x00, 0x02, 0x2D, 0x31, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 156 | 0x13, 0x64, 0x65, 0x6C, 0x65, 0x74, 0x65, 0x2E, 0x72, 0x65, 0x74, 0x65, 0x6E, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x6D, 0x73, 0x00, 0x08, 0x38, 157 | 0x36, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x73, 0x65, 0x67, 0x6D, 0x65, 0x6E, 0x74, 158 | 0x2E, 0x6D, 0x73, 0x00, 0x09, 0x36, 0x30, 0x34, 0x38, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 159 | 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2E, 0x74, 0x69, 0x6D, 0x65, 0x73, 0x74, 0x61, 0x6D, 0x70, 0x2E, 0x64, 0x69, 0x66, 0x66, 0x65, 160 | 0x72, 0x65, 0x6E, 0x63, 0x65, 0x2E, 0x6D, 0x61, 0x78, 0x2E, 0x6D, 0x73, 0x00, 0x13, 0x39, 0x32, 0x32, 0x33, 0x33, 0x37, 0x32, 0x30, 0x33, 161 | 0x36, 0x38, 0x35, 0x34, 0x37, 0x37, 0x35, 0x38, 0x30, 0x37, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x73, 0x65, 0x67, 0x6D, 162 | 0x65, 0x6E, 0x74, 0x2E, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x2E, 0x62, 0x79, 0x74, 0x65, 0x73, 0x00, 0x08, 0x31, 0x30, 0x34, 0x38, 0x35, 0x37, 163 | 0x36, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 164 | ], 165 | ); 166 | // topic partitions for 'foo' 167 | responses.insert( 168 | 2, 169 | vec![ 170 | 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x66, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 171 | 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 172 | 0x03, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 173 | 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 174 | 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 175 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0xFF, 176 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0xFF, 0xFF, 177 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 178 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 179 | ], 180 | ); 181 | 182 | /* Get metadata */ 183 | let metadata_retrieved_event = 184 | match event_bus::to_event(Message::GetMetadata(test_bootstrap_server(), None), test_api_client_provider(responses.clone())) { 185 | Event::MetadataRetrieved(statefn) => match statefn(&state.borrow_mut()) { 186 | Ok(MetadataPayload::Metadata(metadata_response)) => { 187 | assert_eq!(metadata_response.topic_metadata.len(), 3); 188 | assert_eq!( 189 | metadata_response.topic_metadata.iter().map(|t| t.topic.as_str()).collect::>(), 190 | vec!["__confluent.support.metrics", "bar", "foo"] 191 | ); 192 | Event::MetadataRetrieved(statefn) 193 | } 194 | _ => panic!("Expected MetadataPayload::Metadata"), 195 | }, 196 | _ => panic!("Expected Event::MetadataRetrieved"), 197 | }; 198 | swap_state(&state, metadata_retrieved_event); 199 | let updated_state = state.borrow().clone(); 200 | 201 | /* Select the second topic */ 202 | let selection_updated = match event_bus::to_event(Message::Select(MoveSelection::Down), empty_api_client_provider()) { 203 | Event::SelectionUpdated(statefn) => match statefn(&updated_state) { 204 | Ok((current_view, topic_index)) => { 205 | assert_eq!(current_view, updated_state.current_view); 206 | assert_eq!(topic_index, updated_state.selected_index + 1); 207 | Event::SelectionUpdated(statefn) 208 | } 209 | _ => panic!("Expected (CurrentView, usize)"), 210 | }, 211 | _ => panic!("Expected Event::SelectionUpdated"), 212 | }; 213 | swap_state(&state, selection_updated); 214 | 215 | /* Show topic config */ 216 | let view_toggled = event_bus::to_event(Message::ToggleView(CurrentView::TopicInfo), empty_api_client_provider()); 217 | swap_state(&state, view_toggled); 218 | 219 | /* Get topic metadata (topic config) */ 220 | let metadata_retrieved_event = 221 | event_bus::to_event(Message::GetMetadata(test_bootstrap_server(), None), test_api_client_provider(responses.clone())); 222 | swap_state(&state, metadata_retrieved_event); 223 | let updated_state = state.borrow().clone(); 224 | let topic_info_state = updated_state.topic_info_state.expect("Should have selected topic info for topic"); 225 | assert_eq!(updated_state.selected_index, 1); 226 | assert_eq!(topic_info_state.selected_index, 0); 227 | assert_eq!(topic_info_state.topic_metadata.topic, "bar"); 228 | assert_eq!(topic_info_state.topic_metadata.partition_metadata.len(), 16); 229 | assert_eq!(topic_info_state.config_resource.config_entries.len(), 25); 230 | 231 | /* Select the third topic */ 232 | let view_toggled = event_bus::to_event(Message::ToggleView(CurrentView::Topics), empty_api_client_provider()); 233 | swap_state(&state, view_toggled); 234 | let selection_updated = event_bus::to_event(Message::Select(MoveSelection::Down), empty_api_client_provider()); 235 | swap_state(&state, selection_updated); 236 | 237 | /* Show topic partitions */ 238 | let view_toggled = event_bus::to_event(Message::ToggleView(CurrentView::Partitions), empty_api_client_provider()); 239 | swap_state(&state, view_toggled); 240 | 241 | /* Get topic metadata (topic partitions) */ 242 | let metadata_retrieved_event = 243 | event_bus::to_event(Message::GetMetadata(test_bootstrap_server(), None), test_api_client_provider(responses.clone())); 244 | swap_state(&state, metadata_retrieved_event); 245 | let updated_state = state.borrow().clone(); 246 | let partition_info_state = updated_state.partition_info_state.expect("Should have selected partitions for topic"); 247 | assert_eq!(updated_state.selected_index, 2); 248 | assert_eq!(partition_info_state.partition_metadata.len(), 16); 249 | } 250 | 251 | #[test] 252 | fn topic_deletion_marking() { 253 | let state = RefCell::new(State::new()); 254 | 255 | let mut responses: HashMap> = HashMap::new(); 256 | // metadata 257 | responses.insert( 258 | 3, 259 | vec![ 260 | 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x09, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 261 | 0x68, 0x6F, 0x73, 0x74, 0x00, 0x00, 0x23, 0x85, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x09, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x68, 0x6F, 262 | 0x73, 0x74, 0x00, 0x00, 0x23, 0x84, 0xFF, 0xFF, 0x00, 0x16, 0x55, 0x37, 0x7A, 0x53, 0x31, 0x4A, 0x51, 0x6D, 0x51, 0x70, 0x6D, 0x66, 0x65, 263 | 0x6C, 0x6F, 0x5F, 0x63, 0x4E, 0x6D, 0x4E, 0x77, 0x51, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1B, 0x5F, 0x5F, 264 | 0x63, 0x6F, 0x6E, 0x66, 0x6C, 0x75, 0x65, 0x6E, 0x74, 0x2E, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x2E, 0x6D, 0x65, 0x74, 0x72, 0x69, 265 | 0x63, 0x73, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 266 | 0x03, 0xE9, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 267 | 0x00, 0x00, 0x03, 0x66, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 268 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 269 | 0x0B, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 270 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 271 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 272 | 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 273 | 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 274 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 275 | 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 276 | 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 277 | 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 278 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 279 | 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 280 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x03, 0xE9, 0x00, 281 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 282 | 0x00, 0x03, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 283 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 284 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 285 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 286 | 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 287 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 288 | 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x62, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 289 | 0x00, 0x00, 0x08, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 290 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 291 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 292 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 293 | 0x05, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 294 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 295 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 296 | 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 297 | 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 298 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 299 | 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 300 | 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x03, 301 | 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 302 | 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 303 | 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 304 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0xE9, 0x00, 305 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 306 | 0x00, 0x0F, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 307 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 308 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 309 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 310 | ], 311 | ); 312 | // delete 'bar' 313 | responses.insert(20, vec![0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x62, 0x61, 0x72, 0x00, 0x00]); 314 | 315 | /* Get metadata */ 316 | let metadata_retrieved_event = 317 | event_bus::to_event(Message::GetMetadata(test_bootstrap_server(), None), test_api_client_provider(responses.clone())); 318 | swap_state(&state, metadata_retrieved_event); 319 | 320 | /* Select the second topic */ 321 | let selection_updated = event_bus::to_event(Message::Select(MoveSelection::Down), test_api_client_provider(responses.clone())); 322 | swap_state(&state, selection_updated); 323 | 324 | /* Delete the second topic */ 325 | let delete_event = match event_bus::to_event(Message::Delete(test_bootstrap_server(), 30_000), test_api_client_provider(responses.clone())) { 326 | Event::ResourceDeleted(deletion) => match deletion(&state.borrow()) { 327 | Ok(Deletion::Topic(topic_deleted)) => { 328 | assert_eq!(topic_deleted, "bar"); 329 | Event::ResourceDeleted(deletion) 330 | } 331 | _ => panic!("Expected deletion of 'bar' topic"), 332 | }, 333 | _ => panic!("Expected Event::ResourceDeleted"), 334 | }; 335 | swap_state(&state, delete_event); 336 | let updated_state = state.borrow().clone(); 337 | assert_eq!(updated_state.marked_deleted, vec!["bar"]); 338 | } 339 | 340 | #[test] 341 | fn modify_topic_config() { 342 | let state = RefCell::new(State::new()); 343 | 344 | let mut responses = HashMap::new(); 345 | // metadata 346 | responses.insert( 347 | 3, 348 | vec![ 349 | 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x09, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 350 | 0x68, 0x6F, 0x73, 0x74, 0x00, 0x00, 0x23, 0x85, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x09, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x68, 0x6F, 351 | 0x73, 0x74, 0x00, 0x00, 0x23, 0x84, 0xFF, 0xFF, 0x00, 0x16, 0x55, 0x37, 0x7A, 0x53, 0x31, 0x4A, 0x51, 0x6D, 0x51, 0x70, 0x6D, 0x66, 0x65, 352 | 0x6C, 0x6F, 0x5F, 0x63, 0x4E, 0x6D, 0x4E, 0x77, 0x51, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1B, 0x5F, 0x5F, 353 | 0x63, 0x6F, 0x6E, 0x66, 0x6C, 0x75, 0x65, 0x6E, 0x74, 0x2E, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x2E, 0x6D, 0x65, 0x74, 0x72, 0x69, 354 | 0x63, 0x73, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 355 | 0x03, 0xE9, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 356 | 0x00, 0x00, 0x03, 0x66, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 357 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 358 | 0x0B, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 359 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 360 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 361 | 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 362 | 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 363 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 364 | 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 365 | 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 366 | 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 367 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 368 | 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 369 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x03, 0xE9, 0x00, 370 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 371 | 0x00, 0x03, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 372 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 373 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 374 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 375 | 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 376 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 377 | 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x62, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 378 | 0x00, 0x00, 0x08, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 379 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 380 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 381 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 382 | 0x05, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 383 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 384 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 385 | 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 386 | 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 387 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 388 | 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 389 | 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x03, 390 | 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 391 | 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 392 | 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 393 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0xE9, 0x00, 394 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 395 | 0x00, 0x0F, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE9, 0x00, 396 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 397 | 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 398 | 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 399 | ], 400 | ); 401 | // topic config for 'foo' 402 | responses.insert( 403 | 32, 404 | vec![ 405 | 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x02, 0x00, 0x03, 0x66, 0x6F, 0x6F, 0x00, 406 | 0x00, 0x00, 0x19, 0x00, 0x10, 0x63, 0x6F, 0x6D, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6F, 0x6E, 0x2E, 0x74, 0x79, 0x70, 0x65, 0x00, 0x08, 407 | 0x70, 0x72, 0x6F, 0x64, 0x75, 0x63, 0x65, 0x72, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0x6C, 0x65, 0x61, 0x64, 0x65, 0x72, 408 | 0x2E, 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x74, 0x68, 0x72, 0x6F, 0x74, 0x74, 0x6C, 0x65, 0x64, 0x2E, 409 | 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1D, 0x6D, 0x65, 0x73, 0x73, 410 | 0x61, 0x67, 0x65, 0x2E, 0x64, 0x6F, 0x77, 0x6E, 0x63, 0x6F, 0x6E, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x2E, 0x65, 0x6E, 0x61, 0x62, 411 | 0x6C, 0x65, 0x00, 0x04, 0x74, 0x72, 0x75, 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x6D, 0x69, 0x6E, 0x2E, 0x69, 0x6E, 412 | 0x73, 0x79, 0x6E, 0x63, 0x2E, 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x73, 0x00, 0x01, 0x31, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 413 | 0x00, 0x11, 0x73, 0x65, 0x67, 0x6D, 0x65, 0x6E, 0x74, 0x2E, 0x6A, 0x69, 0x74, 0x74, 0x65, 0x72, 0x2E, 0x6D, 0x73, 0x00, 0x01, 0x30, 0x00, 414 | 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x63, 0x6C, 0x65, 0x61, 0x6E, 0x75, 0x70, 0x2E, 0x70, 0x6F, 0x6C, 0x69, 0x63, 0x79, 0x00, 415 | 0x06, 0x64, 0x65, 0x6C, 0x65, 0x74, 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x66, 0x6C, 0x75, 0x73, 0x68, 0x2E, 0x6D, 416 | 0x73, 0x00, 0x13, 0x39, 0x32, 0x32, 0x33, 0x33, 0x37, 0x32, 0x30, 0x33, 0x36, 0x38, 0x35, 0x34, 0x37, 0x37, 0x35, 0x38, 0x30, 0x37, 0x00, 417 | 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x66, 0x6F, 0x6C, 0x6C, 0x6F, 0x77, 0x65, 0x72, 0x2E, 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 418 | 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x74, 0x68, 0x72, 0x6F, 0x74, 0x74, 0x6C, 0x65, 0x64, 0x2E, 0x72, 0x65, 0x70, 0x6C, 0x69, 0x63, 0x61, 419 | 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x73, 0x65, 0x67, 0x6D, 0x65, 0x6E, 0x74, 0x2E, 0x62, 0x79, 0x74, 420 | 0x65, 0x73, 0x00, 0x0A, 0x31, 0x30, 0x37, 0x33, 0x37, 0x34, 0x31, 0x38, 0x32, 0x34, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 421 | 0x72, 0x65, 0x74, 0x65, 0x6E, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x6D, 0x73, 0x00, 0x09, 0x36, 0x30, 0x34, 0x38, 0x30, 0x30, 0x30, 0x30, 0x30, 422 | 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x66, 0x6C, 0x75, 0x73, 0x68, 0x2E, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 423 | 0x00, 0x13, 0x39, 0x32, 0x32, 0x33, 0x33, 0x37, 0x32, 0x30, 0x33, 0x36, 0x38, 0x35, 0x34, 0x37, 0x37, 0x35, 0x38, 0x30, 0x37, 0x00, 0x05, 424 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2E, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x2E, 0x76, 425 | 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x00, 0x07, 0x32, 0x2E, 0x30, 0x2D, 0x49, 0x56, 0x31, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 426 | 0x14, 0x66, 0x69, 0x6C, 0x65, 0x2E, 0x64, 0x65, 0x6C, 0x65, 0x74, 0x65, 0x2E, 0x64, 0x65, 0x6C, 0x61, 0x79, 0x2E, 0x6D, 0x73, 0x00, 0x05, 427 | 0x36, 0x30, 0x30, 0x30, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x6D, 0x61, 0x78, 0x2E, 0x6D, 0x65, 0x73, 0x73, 0x61, 428 | 0x67, 0x65, 0x2E, 0x62, 0x79, 0x74, 0x65, 0x73, 0x00, 0x07, 0x31, 0x30, 0x30, 0x30, 0x30, 0x31, 0x32, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 429 | 0x00, 0x00, 0x15, 0x6D, 0x69, 0x6E, 0x2E, 0x63, 0x6F, 0x6D, 0x70, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x6C, 0x61, 0x67, 0x2E, 0x6D, 430 | 0x73, 0x00, 0x01, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2E, 0x74, 0x69, 431 | 0x6D, 0x65, 0x73, 0x74, 0x61, 0x6D, 0x70, 0x2E, 0x74, 0x79, 0x70, 0x65, 0x00, 0x0A, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6D, 432 | 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x70, 0x72, 0x65, 0x61, 0x6C, 0x6C, 0x6F, 0x63, 0x61, 0x74, 0x65, 0x00, 0x05, 433 | 0x66, 0x61, 0x6C, 0x73, 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x6D, 0x69, 0x6E, 0x2E, 0x63, 0x6C, 0x65, 0x61, 0x6E, 434 | 0x61, 0x62, 0x6C, 0x65, 0x2E, 0x64, 0x69, 0x72, 0x74, 0x79, 0x2E, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x00, 0x03, 0x30, 0x2E, 0x35, 0x00, 0x05, 435 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x2E, 0x69, 0x6E, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6C, 0x2E, 0x62, 436 | 0x79, 0x74, 0x65, 0x73, 0x00, 0x04, 0x34, 0x30, 0x39, 0x36, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x75, 0x6E, 0x63, 0x6C, 437 | 0x65, 0x61, 0x6E, 0x2E, 0x6C, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2E, 0x65, 0x6C, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x65, 0x6E, 0x61, 438 | 0x62, 0x6C, 0x65, 0x00, 0x05, 0x66, 0x61, 0x6C, 0x73, 0x65, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x72, 0x65, 0x74, 0x65, 439 | 0x6E, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x62, 0x79, 0x74, 0x65, 0x73, 0x00, 0x02, 0x2D, 0x31, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 440 | 0x13, 0x64, 0x65, 0x6C, 0x65, 0x74, 0x65, 0x2E, 0x72, 0x65, 0x74, 0x65, 0x6E, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x6D, 0x73, 0x00, 0x08, 0x38, 441 | 0x36, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x73, 0x65, 0x67, 0x6D, 0x65, 0x6E, 0x74, 442 | 0x2E, 0x6D, 0x73, 0x00, 0x09, 0x36, 0x30, 0x34, 0x38, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 443 | 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2E, 0x74, 0x69, 0x6D, 0x65, 0x73, 0x74, 0x61, 0x6D, 0x70, 0x2E, 0x64, 0x69, 0x66, 0x66, 0x65, 444 | 0x72, 0x65, 0x6E, 0x63, 0x65, 0x2E, 0x6D, 0x61, 0x78, 0x2E, 0x6D, 0x73, 0x00, 0x13, 0x39, 0x32, 0x32, 0x33, 0x33, 0x37, 0x32, 0x30, 0x33, 445 | 0x36, 0x38, 0x35, 0x34, 0x37, 0x37, 0x35, 0x38, 0x30, 0x37, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x73, 0x65, 0x67, 0x6D, 446 | 0x65, 0x6E, 0x74, 0x2E, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x2E, 0x62, 0x79, 0x74, 0x65, 0x73, 0x00, 0x08, 0x31, 0x30, 0x34, 0x38, 0x35, 0x37, 447 | 0x36, 0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 448 | ], 449 | ); 450 | // alter config 451 | responses.insert( 452 | 33, 453 | vec![0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x02, 0x00, 0x03, 0x66, 0x6F, 0x6F], 454 | ); 455 | 456 | /* Get metadata */ 457 | let metadata_retrieved_event = 458 | event_bus::to_event(Message::GetMetadata(test_bootstrap_server(), None), test_api_client_provider(responses.clone())); 459 | swap_state(&state, metadata_retrieved_event); 460 | 461 | /* Select the third topic and switch view to topic config */ 462 | let selection_updated = event_bus::to_event(Message::Select(MoveSelection::Down), empty_api_client_provider()); 463 | swap_state(&state, selection_updated); 464 | let selection_updated = event_bus::to_event(Message::Select(MoveSelection::Down), empty_api_client_provider()); 465 | swap_state(&state, selection_updated); 466 | let view_toggled = event_bus::to_event(Message::ToggleView(CurrentView::TopicInfo), empty_api_client_provider()); 467 | swap_state(&state, view_toggled); 468 | 469 | /* Get topic metadata (topic config) */ 470 | let metadata_retrieved_event = 471 | event_bus::to_event(Message::GetMetadata(test_bootstrap_server(), None), test_api_client_provider(responses.clone())); 472 | swap_state(&state, metadata_retrieved_event); 473 | let updated_state = state.borrow().clone(); 474 | let topic_info_state = updated_state.topic_info_state.unwrap(); 475 | assert_eq!(topic_info_state.config_resource.config_entries.as_slice()[0].clone().config_value.unwrap(), "producer"); 476 | 477 | /* Modify the third topic's first config ('compression.type') */ 478 | let modify_event = match event_bus::to_event( 479 | Message::ModifyValue(test_bootstrap_server(), Some(String::from("snappy"))), 480 | test_api_client_provider(responses.clone()), 481 | ) { 482 | Event::ValueModified(modification) => match modification(&state.borrow()) { 483 | Ok(Modification::Config(config_name)) => { 484 | assert_eq!(config_name, "compression.type"); 485 | Event::ValueModified(modification) 486 | } 487 | _ => panic!("Expected modification config"), 488 | }, 489 | _ => panic!("Expected Event::ValueModified"), 490 | }; 491 | swap_state(&state, modify_event); 492 | let topic_info_state = state.borrow().clone().topic_info_state.unwrap(); 493 | assert_eq!(topic_info_state.configs_marked_modified, vec!["compression.type"]); 494 | 495 | /* Delete (reset) the config */ 496 | let delete_event = match event_bus::to_event(Message::Delete(test_bootstrap_server(), 30_000), test_api_client_provider(responses.clone())) { 497 | Event::ResourceDeleted(deletion) => match deletion(&state.borrow()) { 498 | Ok(Deletion::Config(config_reset)) => { 499 | assert_eq!(config_reset, "compression.type"); 500 | Event::ResourceDeleted(deletion) 501 | } 502 | _ => panic!("Expected deletion of 'compression.type' config"), 503 | }, 504 | _ => panic!("Expected Event::ResourceDeleted"), 505 | }; 506 | swap_state(&state, delete_event); 507 | let topic_info_state = state.borrow().clone().topic_info_state.unwrap(); 508 | assert_eq!(topic_info_state.configs_marked_deleted, vec!["compression.type"]); 509 | } 510 | 511 | #[test] 512 | fn create_topic() { 513 | let state = RefCell::new(State::new()); 514 | 515 | let mut responses = HashMap::new(); 516 | // metadata 517 | responses.insert( 518 | 3, 519 | vec![ 520 | 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x09, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 521 | 0x68, 0x6F, 0x73, 0x74, 0x00, 0x00, 0x23, 0x85, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x09, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x68, 0x6F, 522 | 0x73, 0x74, 0x00, 0x00, 0x23, 0x84, 0xFF, 0xFF, 0x00, 0x16, 0x38, 0x46, 0x31, 0x47, 0x53, 0x56, 0x78, 0x37, 0x51, 0x5F, 0x65, 0x44, 0x6D, 523 | 0x6B, 0x46, 0x77, 0x53, 0x39, 0x64, 0x59, 0x32, 0x41, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1B, 0x5F, 0x5F, 524 | 0x63, 0x6F, 0x6E, 0x66, 0x6C, 0x75, 0x65, 0x6E, 0x74, 0x2E, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x2E, 0x6D, 0x65, 0x74, 0x72, 0x69, 525 | 0x63, 0x73, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 526 | 0x03, 0xE8, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x00, 527 | ], 528 | ); 529 | // create response 530 | responses.insert(19, vec![0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x46, 0x6F, 0x6F, 0x00, 0x00, 0xFF, 0xFF]); 531 | 532 | /* Get metadata */ 533 | let metadata_retrieved_event = 534 | event_bus::to_event(Message::GetMetadata(test_bootstrap_server(), None), test_api_client_provider(responses.clone())); 535 | swap_state(&state, metadata_retrieved_event); 536 | assert_eq!(state.borrow().metadata.clone().unwrap().topic_metadata.len(), 1); 537 | 538 | /* Create new topic */ 539 | let creation = Creation::Topic { name: "Foo".to_string(), partitions: 10, replication_factor: 2 }; 540 | let create_topic_event = 541 | event_bus::to_event(Message::Create(test_bootstrap_server(), creation, 30_000), test_api_client_provider(responses.clone())); 542 | swap_state(&state, create_topic_event); 543 | let dialog_message = state.borrow().dialog_message.clone(); 544 | match dialog_message { 545 | Some(DialogMessage::Info(info)) => assert_eq!(info, "Topic 'Foo' created. Press 'r' to refresh view.".to_string()), 546 | _ => panic!(), 547 | } 548 | 549 | // duplicate topic create response 550 | responses.insert( 551 | 19, 552 | vec![ 553 | 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x46, 0x6F, 0x6F, 0x00, 0x24, 0x00, 0x1B, 0x54, 0x6F, 0x70, 0x69, 0x63, 0x20, 554 | 0x27, 0x46, 0x6F, 0x6F, 0x27, 0x20, 0x61, 0x6C, 0x72, 0x65, 0x61, 0x64, 0x79, 0x20, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x2E, 555 | ], 556 | ); 557 | 558 | /* Create same topic */ 559 | let creation = Creation::Topic { name: "Foo".to_string(), partitions: 10, replication_factor: 2 }; 560 | let create_topic_event = 561 | event_bus::to_event(Message::Create(test_bootstrap_server(), creation, 30_000), test_api_client_provider(responses.clone())); 562 | let failed_state = update_state(create_topic_event, state.borrow_mut()); 563 | match failed_state { 564 | Err(StateFNError::Error(err)) => assert_eq!(err, "(36) Topic 'Foo' already exists.".to_string()), 565 | _ => panic!(), 566 | } 567 | } 568 | -------------------------------------------------------------------------------- /src/event_bus/mod.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::cell::RefMut; 3 | use std::collections::HashMap; 4 | use std::io::{stdout, Write}; 5 | use std::sync::mpsc; 6 | use std::sync::mpsc::Receiver; 7 | use std::sync::mpsc::Sender; 8 | use std::thread; 9 | 10 | use termion::raw::IntoRawMode; 11 | use termion::screen::AlternateScreen; 12 | 13 | use crate::api_client::ApiClient; 14 | use crate::api_client::ApiClientProvider; 15 | use crate::api_client::ApiClientTrait; 16 | use crate::api_client::ApiRequestError; 17 | use crate::event_bus::Event::*; 18 | use crate::event_bus::Message::Delete; 19 | use crate::event_bus::Message::*; 20 | use crate::event_bus::MoveSelection::*; 21 | use crate::event_bus::TopicQuery::*; 22 | use crate::kafka_protocol::protocol_request::Request; 23 | use crate::kafka_protocol::protocol_requests; 24 | use crate::kafka_protocol::protocol_requests::*; 25 | use crate::kafka_protocol::protocol_response::Response; 26 | use crate::kafka_protocol::protocol_responses::findcoordinator_response::Coordinator; 27 | use crate::kafka_protocol::protocol_responses::*; 28 | use crate::state::CurrentView; 29 | use crate::state::*; 30 | use crate::user_interface::ui; 31 | use crate::util::utils::{controller_broker, Flatten}; 32 | use crate::KafkaServerAddr; 33 | use crate::IO; 34 | 35 | #[derive(Clone)] 36 | pub struct ConsumerGroup(pub String, pub findcoordinator_response::Coordinator); 37 | 38 | const PAGE_MOVEMENT: i32 = 10; 39 | 40 | pub enum MoveSelection { 41 | Up, 42 | PageUp, 43 | Down, 44 | PageDown, 45 | Top, 46 | Bottom, 47 | SearchNext, 48 | } 49 | 50 | pub enum TopicQuery { 51 | NoQuery, 52 | Query(String), 53 | } 54 | 55 | #[derive(Clone, Debug)] 56 | pub enum Creation { 57 | Topic { name: String, partitions: i32, replication_factor: i16 }, 58 | } 59 | 60 | pub enum Deletion { 61 | Topic(String), 62 | Config(String), 63 | } 64 | 65 | pub enum Modification { 66 | Config(String), 67 | } 68 | 69 | pub enum MetadataPayload { 70 | Metadata(metadata_response::MetadataResponse), 71 | PartitionsMetadata( 72 | metadata_response::MetadataResponse, 73 | Vec, 74 | HashMap, 75 | Option>, 76 | ), 77 | TopicInfoMetadata(metadata_response::MetadataResponse, describeconfigs_response::Resource), 78 | } 79 | 80 | pub enum Message { 81 | Quit, 82 | Noop, 83 | GetMetadata(KafkaServerAddr, Option), 84 | ToggleView(CurrentView), 85 | DisplayUIMessage(DialogMessage), 86 | UserInput(String), 87 | Select(MoveSelection), 88 | SetTopicQuery(TopicQuery), 89 | Create(KafkaServerAddr, Creation, i32), 90 | Delete(KafkaServerAddr, i32), 91 | ModifyValue(KafkaServerAddr, Option), 92 | } 93 | 94 | enum Event { 95 | Exiting, 96 | StateIdentity, 97 | MetadataRetrieved(StateFn), 98 | ViewToggled(CurrentView), 99 | ShowUIMessage(DialogMessage), 100 | UserInputUpdated(String), 101 | SelectionUpdated(StateFn<(CurrentView, usize)>), 102 | TopicQuerySet(Option), 103 | ResourceCreated(StateFn), 104 | ResourceDeleted(StateFn), 105 | ValueModified(StateFn), 106 | } 107 | 108 | pub fn start() -> Sender { 109 | let (sender, receiver): (Sender, Receiver) = mpsc::channel(); 110 | let thread_sender = sender.clone(); 111 | 112 | thread::spawn(move || { 113 | let state = RefCell::new(State::new()); // RefCell for interior mutability 114 | let screen = &mut AlternateScreen::from(stdout().into_raw_mode().unwrap()); 115 | 116 | for message in receiver { 117 | match to_event(message, Box::new(|| IO::new(Box::new(|| Ok(ApiClient::new()))))) { 118 | Exiting => break, 119 | non_exit_event => { 120 | match update_state(non_exit_event, state.borrow_mut()) { 121 | Ok(updated_state) => state.swap(&RefCell::new(updated_state)), 122 | Err(StateFNError::Error(error)) => { 123 | thread_sender.send(Message::DisplayUIMessage(DialogMessage::Error(error))).unwrap(); 124 | } 125 | Err(StateFNError::Caused(error, cause)) => { 126 | thread_sender.send(Message::DisplayUIMessage(DialogMessage::Error(format!("{}: {}", error, cause)))).unwrap(); 127 | } 128 | } 129 | ui::update_with_state(&state.borrow(), screen); 130 | } 131 | } 132 | } 133 | 134 | screen.flush().unwrap(); // final flush before handing screen back to shell 135 | }); 136 | 137 | sender 138 | } 139 | 140 | fn to_event(message: Message, api_client_provider: ApiClientProvider) -> Event { 141 | match message { 142 | Quit => Exiting, 143 | Noop => StateIdentity, 144 | DisplayUIMessage(message) => ShowUIMessage(message), 145 | UserInput(input) => UserInputUpdated(input), 146 | ToggleView(view) => ViewToggled(view), 147 | 148 | GetMetadata(bootstrap_server, opt_consumer_group) => MetadataRetrieved(Box::from(move |state: &State| { 149 | let metadata_response = retrieve_metadata(api_client_provider(), &bootstrap_server) 150 | .into_result() 151 | .map_err(|err| StateFNError::caused("Error encountered trying to retrieve topics", err)); 152 | 153 | match state.current_view { 154 | CurrentView::Topics | CurrentView::HelpScreen => { 155 | metadata_response.map(|metadata_response| MetadataPayload::Metadata(metadata_response)) 156 | } 157 | CurrentView::Partitions => metadata_response.and_then(|metadata_response| { 158 | state 159 | .selected_topic_metadata() 160 | .map(|topic_metadata| { 161 | retrieve_partition_metadata_and_offsets(api_client_provider(), &bootstrap_server, &metadata_response, &topic_metadata) 162 | .into_result() 163 | .and_then(|(partition_metadata, partition_offsets)| match opt_consumer_group { 164 | None => Ok(MetadataPayload::PartitionsMetadata(metadata_response, partition_metadata, partition_offsets, None)), 165 | Some(ConsumerGroup(ref group_id, ref coordinator)) => retrieve_consumer_offsets( 166 | api_client_provider(), 167 | group_id, 168 | coordinator, 169 | &topic_metadata, 170 | bootstrap_server.use_tls, 171 | ) 172 | .into_result() 173 | .map(|consumer_offsets| { 174 | MetadataPayload::PartitionsMetadata( 175 | metadata_response, 176 | partition_metadata, 177 | partition_offsets, 178 | Some(consumer_offsets), 179 | ) 180 | }), 181 | }) 182 | .map_err(|err| StateFNError::caused("Error retrieving partition metadata", err)) 183 | }) 184 | .unwrap_or(Err(StateFNError::error("Could not find selected topic metadata"))) 185 | }), 186 | CurrentView::TopicInfo => metadata_response.and_then(|metadata_response| { 187 | state 188 | .selected_topic_name() 189 | .map(|topic_name| { 190 | retrieve_topic_metadata(api_client_provider(), &bootstrap_server, &topic_name) 191 | .into_result() 192 | .map_err(|err| StateFNError::caused("Error retrieving topic config", err)) 193 | .map(|resource| MetadataPayload::TopicInfoMetadata(metadata_response, resource)) 194 | }) 195 | .unwrap_or(Err(StateFNError::error("No topic selected"))) 196 | }), 197 | } 198 | })), 199 | 200 | Select(direction) => { 201 | SelectionUpdated(Box::from(move |state: &State| { 202 | match state.current_view { 203 | CurrentView::HelpScreen => Ok((CurrentView::HelpScreen, 0)), 204 | CurrentView::Topics => { 205 | let selected_index = match direction { 206 | Up => { 207 | if state.selected_index > 0 { 208 | state.selected_index - 1 209 | } else { 210 | state.selected_index 211 | } 212 | } 213 | PageUp => { 214 | if (state.selected_index as i32 - PAGE_MOVEMENT) <= 0 { 215 | state.selected_index 216 | } else { 217 | state.selected_index - (PAGE_MOVEMENT as usize) 218 | } 219 | } 220 | Down => match state.metadata { 221 | Some(ref metadata) => { 222 | if state.selected_index < (metadata.topic_metadata.len() - 1) { 223 | state.selected_index + 1 224 | } else { 225 | state.selected_index 226 | } 227 | } 228 | None => 0, 229 | }, 230 | PageDown => match state.metadata { 231 | Some(ref metadata) => { 232 | if ((state.selected_index as i32) + PAGE_MOVEMENT) < (metadata.topic_metadata.len() as i32 - 1) { 233 | state.selected_index + (PAGE_MOVEMENT as usize) 234 | } else { 235 | state.selected_index 236 | } 237 | } 238 | None => 0, 239 | }, 240 | Top => 0, 241 | Bottom => state.metadata.as_ref().map(|metadata| metadata.topic_metadata.len() - 1).unwrap_or(0), 242 | SearchNext => state.find_next_index(false).unwrap_or(state.selected_index), 243 | }; 244 | Ok((CurrentView::Topics, selected_index)) 245 | } 246 | CurrentView::Partitions => { 247 | let selected_index = state 248 | .partition_info_state 249 | .as_ref() 250 | .map(|partition_info_state| { 251 | let selected_index = partition_info_state.selected_index; 252 | let entries_len = partition_info_state.partition_metadata.len() - 1; 253 | match direction { 254 | Up => { 255 | if selected_index > 0 { 256 | selected_index - 1 257 | } else { 258 | selected_index 259 | } 260 | } 261 | Down => { 262 | if selected_index < entries_len { 263 | selected_index + 1 264 | } else { 265 | selected_index 266 | } 267 | } 268 | PageUp => selected_index, // not implemented 269 | PageDown => selected_index, // not implemented 270 | Top => 0, 271 | Bottom => partition_info_state.partition_metadata.len() - 1, 272 | SearchNext => selected_index, // not implemented 273 | } 274 | }) 275 | .unwrap_or(0); 276 | Ok((CurrentView::Partitions, selected_index)) 277 | } 278 | CurrentView::TopicInfo => { 279 | let selected_index = state 280 | .topic_info_state 281 | .as_ref() 282 | .map(|topic_info_state| { 283 | let selected_index = topic_info_state.selected_index; 284 | let entries_len = topic_info_state.config_resource.config_entries.len() - 1; 285 | match direction { 286 | Up => { 287 | if selected_index > 0 { 288 | selected_index - 1 289 | } else { 290 | selected_index 291 | } 292 | } 293 | Down => { 294 | if selected_index < entries_len { 295 | selected_index + 1 296 | } else { 297 | selected_index 298 | } 299 | } 300 | PageUp => selected_index, // not implemented 301 | PageDown => selected_index, // not implemented 302 | Top => 0, 303 | Bottom => entries_len, 304 | SearchNext => selected_index, // not implemented 305 | } 306 | }) 307 | .unwrap_or(0); 308 | Ok((CurrentView::TopicInfo, selected_index)) 309 | } 310 | } 311 | })) 312 | } 313 | 314 | SetTopicQuery(query) => match query { 315 | Query(q) => TopicQuerySet(Some(q)), 316 | NoQuery => TopicQuerySet(None), 317 | }, 318 | 319 | Create(bootstrap_sever, creation, request_timeout_ms) => ResourceCreated(Box::from(move |state: &State| match &creation { 320 | Creation::Topic { name, partitions, replication_factor } => { 321 | if state.current_view != CurrentView::Topics { 322 | Err(StateFNError::error("Cannot create topic here")) 323 | } else { 324 | state 325 | .metadata 326 | .as_ref() 327 | .map(|metadata| { 328 | controller_broker(metadata) 329 | .map(|controller| { 330 | create_topic( 331 | api_client_provider(), 332 | name.clone(), 333 | *partitions, 334 | *replication_factor, 335 | controller, 336 | bootstrap_sever.use_tls, 337 | request_timeout_ms, 338 | ) 339 | .into_result() 340 | .map_err(|err| StateFNError::caused("Error creating topic", err)) 341 | .and_then(|response| { 342 | let error_message = response 343 | .response_message 344 | .topic_errors 345 | .into_iter() 346 | .filter(|err| err.error_code != 0) 347 | .collect::>() 348 | .first() 349 | .map(|error| { 350 | format!( 351 | "({}) {}", 352 | error.error_code, 353 | error.error_message.clone().unwrap_or("Unknown error".to_string()) 354 | ) 355 | }); 356 | match error_message { 357 | Some(ref err) => Err(StateFNError::error(err)), 358 | None => Ok(name.clone()), 359 | } 360 | }) 361 | }) 362 | .unwrap_or(Err(StateFNError::error("Could not find Kafka controller host from Metadata"))) 363 | }) 364 | .unwrap_or(Err(StateFNError::error("Topic metadata not available"))) 365 | } 366 | } 367 | })), 368 | 369 | Delete(bootstrap_server, request_timeout_ms) => ResourceDeleted(Box::from(move |state: &State| match state.current_view { 370 | CurrentView::Partitions => Err(StateFNError::error("Partition deletion not supported")), 371 | CurrentView::HelpScreen => Err(StateFNError::error("You can't delete this...")), 372 | CurrentView::Topics => state 373 | .metadata 374 | .as_ref() 375 | .map(|metadata| { 376 | metadata 377 | .topic_metadata 378 | .get(state.selected_index) 379 | .map(|delete_topic_metadata: &metadata_response::TopicMetadata| { 380 | if delete_topic_metadata.is_internal { 381 | Err(StateFNError::error("Can not delete internal topics")) 382 | } else { 383 | controller_broker(&metadata) 384 | .map(|controller_broker| { 385 | let delete_topic_name = delete_topic_metadata.topic.clone(); 386 | let result: Result, ApiRequestError> = delete_topic( 387 | api_client_provider(), 388 | &delete_topic_name, 389 | controller_broker, 390 | bootstrap_server.use_tls, 391 | request_timeout_ms, 392 | ) 393 | .into_result(); 394 | match result { 395 | Ok(response) => { 396 | let map = response 397 | .response_message 398 | .topic_error_codes 399 | .iter() 400 | .map(|err| (&err.topic, err.error_code)) 401 | .collect::>(); 402 | if map.values().all(|err_code| *err_code == 0) { 403 | Ok(Deletion::Topic(delete_topic_name)) 404 | } else { 405 | Err(StateFNError::caused( 406 | "Failed to delete topic", 407 | ApiRequestError::of(format!("Non-zero topic error code encountered: {:?}", map)), 408 | )) 409 | } 410 | } 411 | Err(err) => Err(StateFNError::caused("Failed to delete topic", err)), 412 | } 413 | }) 414 | .unwrap_or(Err(StateFNError::error("Could not find Kafka controller host from Metadata"))) 415 | } 416 | }) 417 | .unwrap_or(Err(StateFNError::error("Could not select or find topic to delete"))) 418 | }) 419 | .unwrap_or(Err(StateFNError::error("Topic metadata not available"))), 420 | CurrentView::TopicInfo => state 421 | .topic_info_state 422 | .as_ref() 423 | .map(|topic_info_state| match topic_info_state.config_resource.config_entries.get(topic_info_state.selected_index) { 424 | None => Err(StateFNError::error("Error trying to modify selected config")), 425 | Some(config_entry) => { 426 | let existing_configs = topic_info_state 427 | .config_resource 428 | .config_entries 429 | .iter() 430 | .filter(|c| c.config_source == describeconfigs_response::ConfigSource::TopicConfig as i8) 431 | .filter(|c| c.config_name != config_entry.config_name) 432 | .map(|c| alterconfigs_request::ConfigEntry { config_name: c.config_name.clone(), config_value: c.config_value.clone() }) 433 | .collect::>(); 434 | 435 | let resource = alterconfigs_request::Resource { 436 | resource_type: protocol_requests::ResourceTypes::Topic as i8, 437 | resource_name: topic_info_state.topic_metadata.topic.clone(), 438 | config_entries: existing_configs, 439 | }; 440 | 441 | alter_config(api_client_provider(), &bootstrap_server, &resource).map(|_| Deletion::Config(config_entry.config_name.clone())) 442 | } 443 | }) 444 | .unwrap_or(Err(StateFNError::error("Topic metadata not available"))), 445 | })), 446 | 447 | ModifyValue(bootstrap_server, new_value) => ValueModified(Box::from(move |state: &State| match state.current_view { 448 | CurrentView::Topics => Err(StateFNError::error("Modifications not supported for topics")), 449 | CurrentView::Partitions => Err(StateFNError::error("Modifications not supported for partitions")), 450 | CurrentView::HelpScreen => Err(StateFNError::error("You can't modify this...")), 451 | CurrentView::TopicInfo => state 452 | .topic_info_state 453 | .as_ref() 454 | .map(|topic_info_state| match topic_info_state.config_resource.config_entries.get(topic_info_state.selected_index) { 455 | None => Err(StateFNError::error("Error trying to modify selected config")), 456 | Some(config_entry) => { 457 | let mut existing_configs = topic_info_state 458 | .config_resource 459 | .config_entries 460 | .iter() 461 | .filter(|c| c.config_source == describeconfigs_response::ConfigSource::TopicConfig as i8) 462 | .filter(|c| c.config_name != config_entry.config_name) 463 | .map(|c| alterconfigs_request::ConfigEntry { config_name: c.config_name.clone(), config_value: c.config_value.clone() }) 464 | .collect::>(); 465 | 466 | existing_configs.push(alterconfigs_request::ConfigEntry { 467 | config_name: config_entry.config_name.clone(), 468 | config_value: new_value.clone(), 469 | }); 470 | 471 | let resource = alterconfigs_request::Resource { 472 | resource_type: protocol_requests::ResourceTypes::Topic as i8, 473 | resource_name: topic_info_state.topic_metadata.topic.clone(), 474 | config_entries: existing_configs, 475 | }; 476 | 477 | alter_config(api_client_provider(), &bootstrap_server, &resource) 478 | .map(|_| Modification::Config(config_entry.config_name.clone())) 479 | } 480 | }) 481 | .unwrap_or(Err(StateFNError::error("Topic info not available"))), 482 | })), 483 | } 484 | } 485 | 486 | fn update_state(event: Event, mut current_state: RefMut) -> Result { 487 | match event { 488 | Exiting => Err(StateFNError::error("Invalid State, can not update state from Exiting event")), 489 | StateIdentity => Ok(current_state.clone()), 490 | UserInputUpdated(input) => { 491 | current_state.user_input = if !input.is_empty() { Some(input) } else { None }; 492 | Ok(current_state.clone()) 493 | } 494 | ShowUIMessage(message) => { 495 | match message { 496 | DialogMessage::None => current_state.dialog_message = None, 497 | _ => current_state.dialog_message = Some(message), 498 | } 499 | Ok(current_state.clone()) 500 | } 501 | MetadataRetrieved(payload_fn) => payload_fn(¤t_state).and_then(|payload: MetadataPayload| match payload { 502 | MetadataPayload::Metadata(metadata_response) => { 503 | let mut state = State::new(); 504 | state.current_view = CurrentView::Topics; 505 | state.selected_index = current_state.selected_index; 506 | state.metadata = Some(metadata_response); 507 | Ok(state) 508 | } 509 | MetadataPayload::PartitionsMetadata(metadata_response, partition_metadata, partition_offsets, consumer_offsets) => { 510 | current_state.metadata = Some(metadata_response); 511 | current_state.partition_info_state = 512 | Some(PartitionInfoState::new(partition_metadata, partition_offsets, consumer_offsets.unwrap_or(HashMap::new()))); 513 | Ok(current_state.clone()) 514 | } 515 | MetadataPayload::TopicInfoMetadata(metadata_response, config_resources) => { 516 | current_state.metadata = Some(metadata_response); 517 | current_state.topic_info_state = 518 | current_state.selected_topic_metadata().map(|topic_metadata| TopicInfoState::new(topic_metadata, config_resources)); 519 | Ok(current_state.clone()) 520 | } 521 | }), 522 | ViewToggled(view) => { 523 | current_state.current_view = view; 524 | Ok(current_state.clone()) 525 | } 526 | SelectionUpdated(select_fn) => match select_fn(¤t_state) { 527 | Err(e) => Err(e), 528 | Ok((CurrentView::HelpScreen, _)) => Ok(current_state.clone()), 529 | Ok((CurrentView::Topics, selected_index)) => { 530 | current_state.selected_index = selected_index; 531 | Ok(current_state.clone()) 532 | } 533 | Ok((CurrentView::Partitions, selected_index)) => { 534 | current_state.partition_info_state = current_state.partition_info_state.as_mut().map(|state| { 535 | state.selected_index = selected_index; 536 | state.clone() 537 | }); 538 | Ok(current_state.clone()) 539 | } 540 | Ok((CurrentView::TopicInfo, selected_index)) => { 541 | current_state.topic_info_state = current_state.topic_info_state.as_mut().map(|state| { 542 | state.selected_index = selected_index; 543 | state.clone() 544 | }); 545 | Ok(current_state.clone()) 546 | } 547 | }, 548 | TopicQuerySet(query) => { 549 | current_state.topic_name_query = query; 550 | Ok(current_state.clone()) 551 | } 552 | ResourceCreated(create_fn) => create_fn(¤t_state).map(|new_topic_name| { 553 | current_state.dialog_message = Some(DialogMessage::Info(format!("Topic '{}' created. Press 'r' to refresh view.", new_topic_name))); 554 | current_state.clone() 555 | }), 556 | ResourceDeleted(delete_fn) => delete_fn(¤t_state).map(|deleted: Deletion| match deleted { 557 | Deletion::Topic(topic) => { 558 | current_state.marked_deleted.push(topic); 559 | current_state.clone() 560 | } 561 | Deletion::Config(config) => { 562 | let current_topic_info_state = current_state.topic_info_state.clone(); 563 | current_state.topic_info_state = current_topic_info_state.map(|mut topic_info_state| { 564 | topic_info_state.configs_marked_deleted.push(config); 565 | topic_info_state 566 | }); 567 | current_state.clone() 568 | } 569 | }), 570 | ValueModified(modify_fn) => modify_fn(¤t_state).map(|modification: Modification| { 571 | let current_topic_info_state = current_state.topic_info_state.clone(); 572 | current_state.topic_info_state = current_topic_info_state.map(|mut topic_info_state| { 573 | match modification { 574 | Modification::Config(config_name) => topic_info_state.configs_marked_modified.push(config_name), 575 | } 576 | topic_info_state 577 | }); 578 | current_state.clone() 579 | }), 580 | } 581 | } 582 | 583 | fn retrieve_metadata( 584 | client: IO, 585 | bootstrap_server: &KafkaServerAddr, 586 | ) -> IO { 587 | let bootstrap_server = bootstrap_server.clone(); 588 | client.and_then_result(Box::new(move |client: T| { 589 | let result: Result, ApiRequestError> = 590 | client.request(&bootstrap_server, Request::of(metadata_request::MetadataRequest { topics: None, allow_auto_topic_creation: false })); 591 | 592 | result.map(|response| { 593 | let mut metadata_response = response.response_message; 594 | // sort by topic names before returning 595 | metadata_response.topic_metadata.sort_by(|a, b| a.topic.to_lowercase().cmp(&b.topic.to_lowercase())); 596 | metadata_response 597 | }) 598 | })) 599 | } 600 | 601 | fn retrieve_topic_metadata( 602 | client: IO, 603 | bootstrap_server: &KafkaServerAddr, 604 | topic_name: &String, 605 | ) -> IO { 606 | let bootstrap_server = bootstrap_server.clone(); 607 | let topic_name = topic_name.clone(); 608 | 609 | client.and_then_result(Box::new(move |client: T| { 610 | let resource = describeconfigs_request::Resource { 611 | resource_type: protocol_requests::ResourceTypes::Topic as i8, 612 | resource_name: topic_name.clone(), 613 | config_names: None, 614 | }; 615 | 616 | let result: Result, ApiRequestError> = client.request( 617 | &bootstrap_server, 618 | Request::of(describeconfigs_request::DescribeConfigsRequest { resources: vec![resource], include_synonyms: false }), 619 | ); 620 | 621 | result.and_then(|response| { 622 | let resource = response 623 | .response_message 624 | .resources 625 | .into_iter() 626 | .filter(|resource| resource.resource_name.eq(&topic_name)) 627 | .collect::>(); 628 | 629 | match resource.first() { 630 | None => Err(ApiRequestError::from("API response missing topic resource info")), 631 | Some(resource) => { 632 | if resource.error_code == 0 { 633 | Ok(resource.clone()) 634 | } else { 635 | let error_msg = resource.error_message.clone().unwrap_or(format!("")); 636 | Err(ApiRequestError::of(format!("Error describing config. {}", error_msg))) 637 | } 638 | } 639 | } 640 | }) 641 | })) 642 | } 643 | 644 | fn retrieve_partition_metadata_and_offsets( 645 | client: IO, 646 | bootstrap_server: &KafkaServerAddr, 647 | metadata_response: &metadata_response::MetadataResponse, 648 | topic_metadata: &metadata_response::TopicMetadata, 649 | ) -> IO<(Vec, HashMap), ApiRequestError> { 650 | let metadata_response = metadata_response.clone(); 651 | let topic_metadata = topic_metadata.clone(); 652 | let bootstrap_server = bootstrap_server.clone(); 653 | 654 | client.and_then_result(Box::new(move |client: T| { 655 | let partition_metadata = &topic_metadata.partition_metadata; 656 | let broker_id_to_host_map = metadata_response 657 | .brokers 658 | .iter() 659 | .map(|b| (b.node_id, KafkaServerAddr::of(b.host.clone(), b.port, bootstrap_server.use_tls))) 660 | .collect::>(); 661 | 662 | let mut sorted_partition_metadata = partition_metadata.clone(); 663 | sorted_partition_metadata.sort_by(|a, b| a.partition.cmp(&b.partition)); 664 | 665 | let partitions_grouped_by_broker: HashMap> = partition_metadata.iter().fold(HashMap::new(), |mut map, partition| { 666 | let mut partitions = map.get_mut(&partition.leader).map(|vec| vec.clone()).unwrap_or(vec![]); 667 | partitions.push(partition.partition); 668 | map.insert(partition.leader, partitions.clone()); 669 | map 670 | }); 671 | 672 | let partition_offset_requests = partitions_grouped_by_broker 673 | .iter() 674 | .map(|(broker_id, partitions)| { 675 | ( 676 | broker_id_to_host_map.get(broker_id).unwrap_or(&bootstrap_server), 677 | listoffsets_request::Topic { 678 | topic: topic_metadata.topic.clone(), 679 | partitions: partitions.iter().map(|p| listoffsets_request::Partition { partition: *p, timestamp: -1 }).collect(), 680 | }, 681 | ) 682 | }) 683 | .collect::>(); 684 | 685 | let partition_offset_responses = partition_offset_requests 686 | .into_iter() 687 | .map(|(broker_address, topic)| { 688 | let listoffsets_response: Result, ApiRequestError> = client.request( 689 | broker_address, 690 | Request::of(listoffsets_request::ListOffsetsRequest { replica_id: -1, isolation_level: 0, topics: vec![topic] }), 691 | ); 692 | listoffsets_response.and_then(|response| match response.response_message.responses.first() { 693 | Some(topic_offsets) => Ok(topic_offsets.partition_responses.clone()), 694 | None => Err(ApiRequestError::from("ListOffsets API did not return any topic offsets")), 695 | }) 696 | }) 697 | .collect::>, ApiRequestError>>() 698 | .map(|vecs| vecs.flatten()); 699 | 700 | let partition_offsets = partition_offset_responses.map(|partition_offset_responses| { 701 | partition_offset_responses 702 | .into_iter() 703 | .map(|partition_response| (partition_response.partition, partition_response)) 704 | .collect::>() 705 | }); 706 | 707 | partition_offsets.map(|offsets| (sorted_partition_metadata, offsets)) 708 | })) 709 | } 710 | 711 | fn retrieve_consumer_offsets( 712 | client: IO, 713 | group_id: &String, 714 | coordinator: &Coordinator, 715 | topic_metadata: &metadata_response::TopicMetadata, 716 | use_tls: bool, 717 | ) -> IO, ApiRequestError> { 718 | let group_id = group_id.clone(); 719 | let coordinator = coordinator.clone(); 720 | let topic_metadata = topic_metadata.clone(); 721 | let coordinator_server = KafkaServerAddr::of(coordinator.host.clone(), coordinator.port, use_tls); 722 | 723 | client.and_then_result(Box::from(move |client: T| { 724 | let topic = offsetfetch_request::Topic { 725 | topic: topic_metadata.topic.clone(), 726 | partitions: topic_metadata.partition_metadata.iter().map(|p| p.partition).collect(), 727 | }; 728 | 729 | let offsetfetch_result: Result, ApiRequestError> = client 730 | .request( 731 | &coordinator_server, 732 | Request::of(offsetfetch_request::OffsetFetchRequest { group_id: group_id.clone(), topics: vec![topic.clone()] }), 733 | ) 734 | .and_then(|result: Response| { 735 | if result.response_message.error_code != 0 { 736 | Err(ApiRequestError::of(format!("Error code {} with OffsetFetchRequest", result.response_message.error_code))) 737 | } else { 738 | Ok(result) 739 | } 740 | }); 741 | 742 | let partition_responses = offsetfetch_result.and_then(|result| { 743 | let responses = result 744 | .response_message 745 | .responses 746 | .into_iter() 747 | .find(|response| response.topic.eq(&topic.topic)) 748 | .map(|response| response.partition_responses); 749 | match responses { 750 | None => Err(ApiRequestError::from("Topic not returned from API request")), 751 | Some(partition_responses) => Ok(partition_responses), 752 | } 753 | }); 754 | 755 | partition_responses.map(|partition_responses| { 756 | partition_responses.into_iter().map(|p| (p.partition, p)).collect::>() 757 | }) 758 | })) 759 | } 760 | 761 | fn create_topic( 762 | client: IO, 763 | topic: String, 764 | num_partitions: i32, 765 | replication_factor: i16, 766 | controller_broker: &metadata_response::BrokerMetadata, 767 | use_tls: bool, 768 | request_timeout_ms: i32, 769 | ) -> IO, ApiRequestError> { 770 | let controller_server = KafkaServerAddr::of(controller_broker.host.clone(), controller_broker.port, use_tls); 771 | 772 | client.and_then_result(Box::new(move |client: T| { 773 | client.request( 774 | &controller_server, 775 | Request::of(createtopics_request::CreateTopicsRequest { 776 | create_topic_requests: vec![createtopics_request::Request { 777 | topic: topic.clone(), 778 | num_partitions, 779 | replication_factor, 780 | replica_assignments: vec![], 781 | config_entries: vec![], 782 | }], 783 | timeout: request_timeout_ms, 784 | validate_only: false, 785 | }), 786 | ) 787 | })) 788 | } 789 | 790 | fn delete_topic( 791 | client: IO, 792 | delete_topic_name: &String, 793 | controller_broker: &metadata_response::BrokerMetadata, 794 | use_tls: bool, 795 | request_timeout_ms: i32, 796 | ) -> IO, ApiRequestError> { 797 | let delete_topic_name = delete_topic_name.clone(); 798 | let controller_broker = controller_broker.clone(); 799 | let controller_server = KafkaServerAddr::of(controller_broker.host.clone(), controller_broker.port, use_tls); 800 | 801 | client.and_then_result(Box::new(move |client: T| { 802 | client.request( 803 | &controller_server, 804 | Request::of(deletetopics_request::DeleteTopicsRequest { topics: vec![delete_topic_name.clone()], timeout: request_timeout_ms }), 805 | ) 806 | })) 807 | } 808 | 809 | fn alter_config( 810 | client: IO, 811 | bootstrap_server: &KafkaServerAddr, 812 | resource: &alterconfigs_request::Resource, 813 | ) -> Result<(), StateFNError> { 814 | let resource = resource.clone(); 815 | let bootstrap_server = bootstrap_server.clone(); 816 | 817 | let alterconfigs_response: Result, ApiRequestError> = client 818 | .and_then_result(Box::new(move |client: T| { 819 | client.request( 820 | &bootstrap_server, 821 | Request::of(alterconfigs_request::AlterConfigsRequest { resources: vec![resource.clone()], validate_only: false }), 822 | ) 823 | })) 824 | .into_result(); 825 | 826 | alterconfigs_response.map_err(|tcp_error| StateFNError::caused("AlterConfigs request failed", tcp_error)).and_then(|alterconfigs_response| { 827 | match alterconfigs_response.response_message.resources.first() { 828 | None => Err(StateFNError::error("Missing resources from AlterConfigs request")), 829 | Some(resource) => { 830 | if resource.error_code == 0 { 831 | Ok(()) 832 | } else { 833 | Err(StateFNError::Error(format!( 834 | "AlterConfigs request failed with error code {}, {}", 835 | resource.error_code, 836 | resource.error_message.clone().unwrap_or(format!("")) 837 | ))) 838 | } 839 | } 840 | } 841 | }) 842 | } 843 | 844 | #[cfg(test)] 845 | #[path = "./event_bus_test.rs"] 846 | mod event_bus_test; 847 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io::stdin; 3 | 4 | use clap::{App, Arg}; 5 | use regex::Regex; 6 | use termion::event::Key; 7 | use termion::input::TermRead; 8 | use termion::raw::IntoRawMode; 9 | use termion::screen::AlternateScreen; 10 | use termion::terminal_size; 11 | use topiks_kafka_client::*; 12 | 13 | use crate::api_client::ApiClient; 14 | use crate::api_client::ApiClientTrait; 15 | use crate::api_client::ApiRequestError; 16 | use crate::event_bus::ConsumerGroup; 17 | use crate::event_bus::Creation; 18 | use crate::event_bus::Message; 19 | use crate::event_bus::MoveSelection::*; 20 | use crate::event_bus::TopicQuery::*; 21 | use crate::kafka_protocol::protocol_request::*; 22 | use crate::kafka_protocol::protocol_requests::findcoordinator_request::CoordinatorType; 23 | use crate::kafka_protocol::protocol_requests::findcoordinator_request::FindCoordinatorRequest; 24 | use crate::kafka_protocol::protocol_response::*; 25 | use crate::kafka_protocol::protocol_responses::findcoordinator_response::FindCoordinatorResponse; 26 | use crate::state::CurrentView; 27 | use crate::state::DialogMessage; 28 | use crate::user_interface::user_input; 29 | 30 | pub mod error_codes; 31 | pub mod event_bus; 32 | pub mod state; 33 | pub mod user_interface; 34 | pub mod util; 35 | 36 | const VERSION: &'static str = env!("CARGO_PKG_VERSION"); 37 | 38 | struct AppConfig<'a> { 39 | bootstrap_server: KafkaServerAddr, 40 | consumer_group: Option<&'a str>, 41 | request_timeout_ms: i32, 42 | deletion_allowed: bool, 43 | deletion_confirmation: bool, 44 | modification_enabled: bool, 45 | } 46 | 47 | fn main() -> Result<(), u8> { 48 | let _args: Vec = env::args().collect(); 49 | 50 | let matches = App::new("topiks") 51 | .version(VERSION) 52 | .arg(Arg::with_name("bootstrap-server").required(true).takes_value(true).help("A single Kafka broker [DOMAIN|IP]:PORT")) 53 | .arg(Arg::with_name("tls").long("tls").required(false).help("Enable TLS")) 54 | .arg(Arg::with_name("consumer-group").long("consumer-group").short("c").takes_value(true).help("Consumer group for fetching offsets")) 55 | .arg(Arg::with_name("delete").short("D").help("Enable topic/config deletion")) 56 | .arg(Arg::with_name("no-delete-confirmation").long("no-delete-confirmation").help("Disable delete confirmation ")) 57 | .arg(Arg::with_name("modify").short("M").help("Enable creation of topics and modification of topic configurations")) 58 | .get_matches(); 59 | 60 | let enable_tls = matches.is_present("tls"); 61 | let bootstrap_server = 62 | KafkaServerAddr::from_arg(matches.value_of("bootstrap-server").unwrap(), enable_tls).ok_or(error_codes::COULD_NOT_PARSE_BOOTSTRAP_SERVER)?; 63 | 64 | let app_config = AppConfig { 65 | bootstrap_server, 66 | consumer_group: matches.value_of("consumer-group"), 67 | request_timeout_ms: 300_000, // 5 minutes 68 | deletion_allowed: matches.is_present("delete"), 69 | deletion_confirmation: !matches.is_present("no-delete-confirmation"), 70 | modification_enabled: matches.is_present("modify"), 71 | }; 72 | 73 | if let Err(err) = 74 | kafka_protocol::api_verification::apply(ApiClient::new(), &app_config.bootstrap_server, &kafka_protocol::api_verification::apis_in_use()) 75 | { 76 | eprintln!("Kafka Protocol API Error(s): {:?}", err); 77 | Err(error_codes::KAFKA_API_VERIFICATION_FAIL) 78 | } else { 79 | let sender = event_bus::start(); 80 | let _stdout = &mut AlternateScreen::from(std::io::stdout().into_raw_mode().unwrap()); // raw mode to avoid screen output 81 | let stdin = stdin(); 82 | 83 | let bootstrap_server = || app_config.bootstrap_server.clone(); 84 | 85 | let consumer_group = app_config.consumer_group.clone().and_then(|cg| { 86 | let find_coordinator_response: Result, ApiRequestError> = ApiClient::new().request( 87 | &app_config.bootstrap_server, 88 | Request::of(FindCoordinatorRequest { coordinator_key: String::from(cg), coordinator_type: CoordinatorType::Group as i8 }), 89 | ); 90 | find_coordinator_response.ok().and_then(|find_coordinator_response| { 91 | if find_coordinator_response.response_message.error_code == 0 { 92 | Some(ConsumerGroup(String::from(cg), find_coordinator_response.response_message.coordinator)) 93 | } else { 94 | sender 95 | .send(Message::DisplayUIMessage(DialogMessage::Error(format!("Could not determine coordinator for consumer group {}", cg)))) 96 | .unwrap(); 97 | None 98 | } 99 | }) 100 | }); 101 | 102 | sender.send(Message::GetMetadata(bootstrap_server(), consumer_group.clone())).unwrap(); 103 | 104 | let create_topic_regex = Regex::new(r"^[A-Za-z0-9\._]{1, 249}:[0-9]{1,3}:[0-9]{1,3}$").expect("Could not compile regex for creating topics"); 105 | 106 | for key in stdin.keys() { 107 | match key.unwrap() { 108 | Key::Char('h') => { 109 | sender.send(Message::ToggleView(CurrentView::HelpScreen)).unwrap(); 110 | } 111 | Key::Char('q') => match sender.send(Message::Quit) { 112 | Ok(_) => break, 113 | Err(_) => { 114 | eprintln!("Failed to signal event bus/user interface thread. Exiting now"); 115 | break; 116 | } 117 | }, 118 | Key::Char('r') => { 119 | sender.send(Message::DisplayUIMessage(DialogMessage::None)).unwrap(); 120 | sender.send(Message::GetMetadata(bootstrap_server(), consumer_group.clone())).unwrap(); 121 | } 122 | Key::Char('c') => { 123 | if app_config.modification_enabled { 124 | let (_, height) = terminal_size().unwrap(); 125 | match user_input::read("▶︎ ", (1, height), sender.clone()) { 126 | Ok(Some(ref input)) if create_topic_regex.is_match(input.as_str()) => { 127 | match input.split(":").collect::>().as_slice() { 128 | &[topic, partitions, replication_factor] => { 129 | let create_topic = partitions.to_string().parse::().and_then(|partitions| { 130 | replication_factor.to_string().parse::().map(|replication_factor| Creation::Topic { 131 | name: topic.to_string(), 132 | partitions, 133 | replication_factor, 134 | }) 135 | }); 136 | match create_topic { 137 | Ok(create_topic) => { 138 | sender.send(Message::Create(bootstrap_server(), create_topic, app_config.request_timeout_ms)).unwrap() 139 | } 140 | Err(_) => sender 141 | .send(Message::DisplayUIMessage(DialogMessage::Error("Invalid input for creating topic".to_string()))) 142 | .unwrap(), 143 | } 144 | } 145 | _ => sender 146 | .send(Message::DisplayUIMessage(DialogMessage::Error("Invalid input for creating topic".to_string()))) 147 | .unwrap(), 148 | } 149 | } 150 | _ => sender 151 | .send(Message::DisplayUIMessage(DialogMessage::Error( 152 | "Input should be [topic]:[partitions]:[replication factor]".to_string(), 153 | ))) 154 | .unwrap(), 155 | } 156 | } 157 | } 158 | Key::Char('d') => { 159 | if app_config.deletion_allowed { 160 | sender.send(Message::DisplayUIMessage(DialogMessage::Warn(format!("Deleting...")))).unwrap(); 161 | if app_config.deletion_confirmation { 162 | let (_, height) = terminal_size().unwrap(); 163 | match user_input::read("[Yes]?: ", (1, height), sender.clone()) { 164 | Ok(Some(confirm)) => { 165 | if confirm.eq("Yes") { 166 | sender.send(Message::Delete(bootstrap_server(), app_config.request_timeout_ms)).unwrap(); 167 | } else { 168 | sender.send(Message::Noop).unwrap(); 169 | } 170 | } 171 | _ => (), 172 | } 173 | } else { 174 | sender.send(Message::Delete(bootstrap_server(), app_config.request_timeout_ms)).unwrap(); 175 | } 176 | sender.send(Message::DisplayUIMessage(DialogMessage::None)).unwrap(); 177 | } 178 | } 179 | Key::Up => { 180 | sender.send(Message::Select(Up)).unwrap(); 181 | } 182 | Key::PageUp => { 183 | sender.send(Message::Select(PageUp)).unwrap(); 184 | } 185 | Key::Down => { 186 | sender.send(Message::Select(Down)).unwrap(); 187 | } 188 | Key::PageDown => { 189 | sender.send(Message::Select(PageDown)).unwrap(); 190 | } 191 | Key::Home => { 192 | sender.send(Message::Select(Top)).unwrap(); 193 | } 194 | Key::End => { 195 | sender.send(Message::Select(Bottom)).unwrap(); 196 | } 197 | Key::Char('t') => { 198 | sender.send(Message::DisplayUIMessage(DialogMessage::None)).unwrap(); 199 | sender.send(Message::ToggleView(CurrentView::Topics)).unwrap(); 200 | sender.send(Message::GetMetadata(bootstrap_server(), consumer_group.clone())).unwrap(); 201 | } 202 | Key::Char('i') => { 203 | sender.send(Message::DisplayUIMessage(DialogMessage::None)).unwrap(); 204 | sender.send(Message::ToggleView(CurrentView::TopicInfo)).unwrap(); 205 | sender.send(Message::GetMetadata(bootstrap_server(), consumer_group.clone())).unwrap(); 206 | } 207 | Key::Char('p') => { 208 | sender.send(Message::DisplayUIMessage(DialogMessage::None)).unwrap(); 209 | sender.send(Message::ToggleView(CurrentView::Partitions)).unwrap(); 210 | sender.send(Message::GetMetadata(bootstrap_server(), consumer_group.clone())).unwrap(); 211 | } 212 | Key::Char('/') => { 213 | sender.send(Message::DisplayUIMessage(DialogMessage::Info(format!("Search")))).unwrap(); 214 | let (_width, height) = terminal_size().unwrap(); 215 | let query = match user_input::read("/", (1, height), sender.clone()) { 216 | Ok(Some(query)) => Message::SetTopicQuery(Query(query)), 217 | Ok(None) => Message::SetTopicQuery(NoQuery), 218 | Err(_) => Message::SetTopicQuery(NoQuery), 219 | }; 220 | sender.send(query).unwrap(); 221 | sender.send(Message::Select(SearchNext)).unwrap(); 222 | sender.send(Message::DisplayUIMessage(DialogMessage::None)).unwrap(); 223 | } 224 | Key::Char('n') => { 225 | // TODO support Shift+n for reverse 226 | sender.send(Message::Select(SearchNext)).unwrap(); 227 | } 228 | Key::Char(':') => { 229 | if app_config.modification_enabled { 230 | let (_width, height) = terminal_size().unwrap(); 231 | match user_input::read(":", (1, height), sender.clone()) { 232 | Ok(modify_value) => { 233 | sender.send(Message::ModifyValue(bootstrap_server(), modify_value)).unwrap(); 234 | } 235 | _ => (), 236 | } 237 | } 238 | } 239 | _ => {} 240 | } 241 | } 242 | 243 | Ok(()) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Display; 3 | 4 | use crate::kafka_protocol::protocol_responses::describeconfigs_response::Resource; 5 | use crate::kafka_protocol::protocol_responses::listoffsets_response; 6 | use crate::kafka_protocol::protocol_responses::metadata_response::MetadataResponse; 7 | use crate::kafka_protocol::protocol_responses::metadata_response::PartitionMetadata; 8 | use crate::kafka_protocol::protocol_responses::metadata_response::TopicMetadata; 9 | use crate::kafka_protocol::protocol_responses::offsetfetch_response; 10 | use crate::state::CurrentView::*; 11 | 12 | #[derive(Clone)] 13 | pub struct State { 14 | pub dialog_message: Option, 15 | pub user_input: Option, 16 | pub current_view: CurrentView, 17 | pub metadata: Option, 18 | pub selected_index: usize, 19 | pub marked_deleted: Vec, 20 | pub topic_name_query: Option, 21 | pub topic_info_state: Option, 22 | pub partition_info_state: Option, 23 | } 24 | 25 | #[derive(Clone)] 26 | pub enum DialogMessage { 27 | None, 28 | Warn(String), 29 | Info(String), 30 | Error(String), 31 | } 32 | 33 | #[derive(Clone, Debug, PartialEq)] 34 | pub enum CurrentView { 35 | Topics, 36 | Partitions, 37 | TopicInfo, 38 | HelpScreen, 39 | } 40 | 41 | pub type StateFn = Box Result>; 42 | 43 | pub enum StateFNError { 44 | Error(String), 45 | Caused(String, Box), 46 | } 47 | 48 | impl StateFNError { 49 | pub fn caused(error: &str, cause: T) -> StateFNError { 50 | StateFNError::Caused(String::from(error), Box::from(cause)) 51 | } 52 | pub fn error(error: &str) -> StateFNError { 53 | StateFNError::Error(String::from(error)) 54 | } 55 | } 56 | 57 | impl State { 58 | pub fn new() -> State { 59 | State { 60 | dialog_message: None, 61 | user_input: None, 62 | current_view: Topics, 63 | metadata: None, 64 | selected_index: 0, 65 | marked_deleted: vec![], 66 | topic_name_query: None, 67 | topic_info_state: None, 68 | partition_info_state: None, 69 | } 70 | } 71 | 72 | pub fn selected_topic_name(&self) -> Option { 73 | self.metadata.as_ref().and_then(|metadata| metadata.topic_metadata.get(self.selected_index).map(|m: &TopicMetadata| m.topic.clone())) 74 | } 75 | 76 | pub fn selected_topic_metadata(&self) -> Option { 77 | self.metadata.as_ref().and_then(|metadata| metadata.topic_metadata.get(self.selected_index).map(|topic_metadata| topic_metadata.clone())) 78 | } 79 | 80 | pub fn find_next_index(&self, in_reverse: bool) -> Option { 81 | self.topic_name_query.as_ref().and_then(|query| { 82 | self.metadata.as_ref().and_then(|metadata| { 83 | let indexed = metadata.topic_metadata.iter().zip(0..metadata.topic_metadata.len()).collect::>(); 84 | let slice = if self.selected_index < metadata.topic_metadata.len() { 85 | if in_reverse { 86 | let mut vec = indexed.as_slice()[0..self.selected_index].to_vec(); 87 | vec.reverse(); 88 | vec 89 | } else { 90 | indexed.as_slice()[(self.selected_index + 1)..].to_vec() // +1 since we don't want to find the selected index 91 | } 92 | } else { 93 | vec![] 94 | }; 95 | 96 | slice 97 | .iter() 98 | .find(|m| { 99 | let (topic_metadata, _index) = **m; 100 | topic_metadata.topic.contains(query) 101 | }) 102 | .map(|result| result.1) 103 | }) 104 | }) 105 | } 106 | } 107 | 108 | #[derive(Clone)] 109 | pub struct TopicInfoState { 110 | pub topic_metadata: TopicMetadata, 111 | pub config_resource: Resource, 112 | pub selected_index: usize, 113 | pub configs_marked_deleted: Vec, 114 | pub configs_marked_modified: Vec, 115 | } 116 | 117 | impl TopicInfoState { 118 | pub fn new(topic_metadata: TopicMetadata, config_resource: Resource) -> TopicInfoState { 119 | TopicInfoState { topic_metadata, config_resource, selected_index: 0, configs_marked_deleted: vec![], configs_marked_modified: vec![] } 120 | } 121 | } 122 | 123 | #[derive(Clone)] 124 | pub struct PartitionInfoState { 125 | pub selected_index: usize, 126 | pub partition_metadata: Vec, 127 | pub partition_offsets: HashMap, 128 | pub consumer_offsets: HashMap, 129 | } 130 | 131 | impl PartitionInfoState { 132 | pub fn new( 133 | partition_metadata: Vec, 134 | partition_offsets: HashMap, 135 | consumer_offsets: HashMap, 136 | ) -> PartitionInfoState { 137 | PartitionInfoState { selected_index: 0, partition_metadata, partition_offsets, consumer_offsets } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/user_interface/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod offset_progress_bar; 2 | pub mod selectable_list; 3 | pub mod ui; 4 | pub mod user_input; 5 | -------------------------------------------------------------------------------- /src/user_interface/offset_progress_bar.rs: -------------------------------------------------------------------------------- 1 | pub fn new(offset: i64, max_offset: i64, width: i64) -> String { 2 | let offset = offset.max(0); 3 | let max_offset = max_offset.max(1); 4 | 5 | let whole_blocks = (offset * width) / max_offset; 6 | let last_block = match whole_blocks { 7 | 0 => None, 8 | n if n == width => None, 9 | _ => Some(((offset * width) % max_offset) as f64 / max_offset as f64), 10 | }; 11 | let empty_blocks = width - whole_blocks - if last_block.is_some() { 1 } else { 0 }; 12 | 13 | format!( 14 | "[{}{}{}]", 15 | vec![WHOLE; whole_blocks as usize].join(""), 16 | last_block.map(last_block_str).unwrap_or(""), 17 | vec![EMPTY; empty_blocks as usize].join("") 18 | ) 19 | } 20 | 21 | const EMPTY: &str = "░"; 22 | const WHOLE: &str = "█"; 23 | const ONE_EIGHTH: &str = "▏"; 24 | const ONE_QUARTER: &str = "▎"; 25 | const THREE_EIGHTHS: &str = "▍"; 26 | const ONE_HALF: &str = "▌"; 27 | const FIVE_EIGHTHS: &str = "▋"; 28 | const THREE_QUARTERS: &str = "▊"; 29 | const SEVEN_EIGHTHS: &str = "▉"; 30 | 31 | fn last_block_str(percent: f64) -> &'static str { 32 | if percent == 0_f64 { 33 | EMPTY 34 | } else if percent <= 0.125_f64 { 35 | ONE_EIGHTH 36 | } else if percent <= 0.25_f64 { 37 | ONE_QUARTER 38 | } else if percent <= 0.375_f64 { 39 | THREE_EIGHTHS 40 | } else if percent <= 0.50_f64 { 41 | ONE_HALF 42 | } else if percent <= 0.625_f64 { 43 | FIVE_EIGHTHS 44 | } else if percent <= 0.75_f64 { 45 | THREE_QUARTERS 46 | } else if percent <= 0.875_f64 { 47 | SEVEN_EIGHTHS 48 | } else { 49 | WHOLE 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/user_interface/selectable_list.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use termion::{clear, color, cursor, style}; 4 | 5 | use crate::kafka_protocol::protocol_responses::metadata_response::PartitionMetadata; 6 | use crate::user_interface::offset_progress_bar; 7 | use crate::util::utils::VecToCSV; 8 | 9 | pub struct SelectableList 10 | where 11 | A: SelectableListItem, 12 | { 13 | pub list: Vec, 14 | } 15 | 16 | impl SelectableList 17 | where 18 | A: SelectableListItem, 19 | { 20 | pub fn display(&self, screen: &mut impl Write, (start_x, start_y): (u16, u16), height: u16) { 21 | let blank_items = vec![format!("{}{}", clear::CurrentLine, cursor::Down(1)); height as usize - self.list.len()].join(""); 22 | 23 | let list_items = self 24 | .list 25 | .iter() 26 | .map(|list_item| { 27 | let display = list_item.display(); 28 | format!("{}{}{}{}{}", display, style::Reset, clear::UntilNewline, cursor::Left(display.len() as u16), cursor::Down(1)) 29 | }) 30 | .collect::>() 31 | .join(""); 32 | 33 | write!( 34 | screen, 35 | "{cursor_to_start}{style_reset}{list_items}{blank_items}{style_reset}", 36 | cursor_to_start = cursor::Goto(start_x, start_y), 37 | style_reset = style::Reset, 38 | list_items = list_items, 39 | blank_items = blank_items 40 | ) 41 | .unwrap(); 42 | } 43 | } 44 | 45 | pub trait SelectableListItem { 46 | fn display(&self) -> String; 47 | } 48 | 49 | pub enum TopicListItem<'a> { 50 | Normal(&'a str, usize), 51 | Internal(&'a str, usize), 52 | Deleted(&'a str, usize), 53 | Selected(Box>), 54 | } 55 | 56 | impl<'a> SelectableListItem for TopicListItem<'a> { 57 | fn display(&self) -> String { 58 | match &self { 59 | TopicListItem::Normal(label, partitions) => { 60 | format!("{}{} [{}{}{}]", color::Fg(color::Cyan), &label, color::Fg(color::LightYellow), partitions, color::Fg(color::Cyan)) 61 | } 62 | TopicListItem::Internal(label, partitions) => format!( 63 | "{}{}{} [{}{}{}]", 64 | color::Fg(color::LightMagenta), 65 | &label, 66 | color::Fg(color::Cyan), 67 | color::Fg(color::LightYellow), 68 | partitions, 69 | color::Fg(color::Cyan) 70 | ), 71 | TopicListItem::Deleted(label, partitions) => { 72 | format!("{}{}{} [{}]", color::Fg(color::Black), color::Bg(color::LightRed), &label, partitions) 73 | } 74 | TopicListItem::Selected(topic_list_item) => format!("{}{}", color::Bg(color::LightBlack), topic_list_item.display()), 75 | } 76 | } 77 | } 78 | 79 | pub enum PartitionListItem<'a> { 80 | Normal { partition: i32, partition_metadata: &'a PartitionMetadata, consumer_offset: i64, partition_offset: i64 }, 81 | Selected(Box>), 82 | } 83 | 84 | impl<'a> SelectableListItem for PartitionListItem<'a> { 85 | fn display(&self) -> String { 86 | use self::PartitionListItem::*; 87 | match &self { 88 | Normal { partition, partition_metadata, consumer_offset, partition_offset } => format!( 89 | "{}▶ {}{:<4} {}{}{} C:{:10} OF:{:10} L:{} R:{} ISR:{} O:{}{}", 90 | color::Fg(color::LightYellow), 91 | color::Fg(color::White), 92 | partition, 93 | color::Fg(color::Green), 94 | offset_progress_bar::new(*consumer_offset, *partition_offset, 50), 95 | color::Fg(color::White), 96 | if *consumer_offset > 0 { format!("{}", consumer_offset) } else { String::from("--") }, 97 | format!("{}", partition_offset), 98 | partition_metadata.leader, 99 | partition_metadata.replicas.as_csv(), 100 | partition_metadata.isr.as_csv(), 101 | color::Fg(color::LightRed), 102 | if !partition_metadata.offline_replicas.is_empty() { partition_metadata.offline_replicas.as_csv() } else { String::from("--") } 103 | ), 104 | Selected(item) => format!("{}{}", color::Bg(color::LightBlack), item.display()), 105 | } 106 | } 107 | } 108 | 109 | pub enum TopicConfigurationItem { 110 | Config { name: String, value: Option }, 111 | Selected(Box), 112 | Override(Box), 113 | Deleted(Box), 114 | Modified(Box), 115 | } 116 | 117 | impl SelectableListItem for TopicConfigurationItem { 118 | fn display(&self) -> String { 119 | use self::TopicConfigurationItem::*; 120 | match &self { 121 | Config { name, value } => format!("{}: {}", name, value.as_ref().unwrap_or(&format!(""))), 122 | Selected(config) => format!("{}{}", color::Bg(color::LightBlack), config.display()), 123 | Override(config) => format!("{}{}", color::Fg(color::LightMagenta), config.display()), 124 | Deleted(config) => format!("{}{} {}", color::Bg(color::LightBlue), config.display(), "[refresh]"), 125 | Modified(config) => format!("{}{} {}", color::Bg(color::LightBlue), config.display(), "[refresh]"), 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/user_interface/ui.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use termion::clear; 4 | use termion::color; 5 | use termion::cursor; 6 | use termion::style; 7 | use termion::terminal_size; 8 | 9 | use crate::kafka_protocol::protocol_responses::describeconfigs_response::ConfigEntry; 10 | use crate::kafka_protocol::protocol_responses::describeconfigs_response::ConfigSource; 11 | use crate::kafka_protocol::protocol_responses::metadata_response::MetadataResponse; 12 | use crate::kafka_protocol::protocol_responses::metadata_response::PartitionMetadata; 13 | use crate::kafka_protocol::protocol_responses::metadata_response::TopicMetadata; 14 | use crate::state::CurrentView; 15 | use crate::state::DialogMessage; 16 | use crate::state::PartitionInfoState; 17 | use crate::state::State; 18 | use crate::state::TopicInfoState; 19 | use crate::user_interface::selectable_list::PartitionListItem; 20 | use crate::user_interface::selectable_list::SelectableList; 21 | use crate::user_interface::selectable_list::TopicConfigurationItem; 22 | use crate::user_interface::selectable_list::TopicListItem; 23 | use crate::util::paged_vec::PagedVec; 24 | use crate::util::utils; 25 | use crate::util::utils::pad_right; 26 | 27 | pub fn update_with_state(state: &State, screen: &mut impl Write) { 28 | let (width, height): (u16, u16) = terminal_size().unwrap(); 29 | 30 | if let Some(ref metadata) = state.metadata { 31 | show_dialog_header(screen, width, metadata, &state.dialog_message); 32 | 33 | match state.current_view { 34 | CurrentView::Topics => { 35 | show_topics(screen, height - 2, (1, 2), metadata, state.selected_index, &state.marked_deleted); 36 | } 37 | CurrentView::Partitions => { 38 | if let Some(partition_info_state) = state.partition_info_state.as_ref() { 39 | show_topic_partitions(screen, height - 2, (1, 2), partition_info_state); 40 | } 41 | } 42 | CurrentView::TopicInfo => { 43 | if let Some(ref topic_info) = state.topic_info_state { 44 | show_topic_info(screen, (width, height - 4), (1, 2), topic_info); 45 | } 46 | } 47 | CurrentView::HelpScreen => show_help(screen), 48 | } 49 | } 50 | 51 | show_user_input(screen, (width, height), state.user_input.as_ref()); 52 | 53 | screen.flush().unwrap(); // flush complete buffer to screen once 54 | } 55 | 56 | const HELP: [(&str, &str); 17] = [ 57 | ("h", "Toggle this screen"), 58 | ("q", "Quit"), 59 | ("t", "Toggle topics view"), 60 | ("i", "Toggle topic config view"), 61 | ("p", "Toggle partitions view"), 62 | ("/", "Enter search query for topic name"), 63 | ("n", "Find next search result"), 64 | ("r", "Refresh. Retrieves metadata from Kafka cluster"), 65 | ("c", "Create a new topic with [topic]:[partitions]:[replication factor]"), 66 | (":", "Modify a resource (e.g. topic config) via text input"), 67 | ("d", "Delete a resource. Will delete a topic or reset a topic config"), 68 | ("Up⬆", "Move up one topic"), 69 | ("Down⬇", "Move down one topic"), 70 | ("PgUp⇞", "Move up ten topics"), 71 | ("PgDown⇟", "Move down ten topics"), 72 | ("Home⤒", "Go to first topic"), 73 | ("End⤓", "Go to last topic"), 74 | ]; 75 | 76 | fn show_help(screen: &mut impl Write) { 77 | for (index, (key, help)) in HELP.iter().enumerate() { 78 | write!(screen, "\n{}{}{}{} → {}", cursor::Goto(2, (index + 2) as u16), style::Bold, key, style::Reset, help).unwrap(); 79 | } 80 | } 81 | 82 | fn show_dialog_header(screen: &mut impl Write, width: u16, metadata: &MetadataResponse, message: &Option) { 83 | let dialog = match message.as_ref() { 84 | None => { 85 | let cluster_name = metadata.cluster_id.as_ref().map(|s| s.as_str()).unwrap_or("unknown"); 86 | let header = format!("cluster:{} brokers:{} topics:{}", cluster_name, metadata.brokers.len(), metadata.topic_metadata.len()); 87 | Some(format!("{}{}{}{}", color::Fg(color::White), cursor::Right(width - (header.len() as u16)), style::Bold, header)) 88 | } 89 | Some(DialogMessage::None) => None, 90 | Some(DialogMessage::Error(error)) => Some(format!("{}{}", color::Bg(color::LightRed), pad_right(&error, width))), 91 | Some(DialogMessage::Warn(warn)) => Some(format!("{}{}", color::Bg(color::LightYellow), pad_right(&warn, width))), 92 | Some(DialogMessage::Info(info)) => Some(format!("{}{}", color::Bg(color::LightBlue), pad_right(&info, width))), 93 | }; 94 | 95 | if let Some(dialog) = dialog { 96 | write!(screen, "{}{}{}{}", cursor::Goto(1, 1), clear::CurrentLine, dialog, style::Reset).unwrap(); 97 | } 98 | } 99 | 100 | fn show_topics( 101 | screen: &mut impl Write, 102 | height: u16, 103 | (start_x, start_y): (u16, u16), 104 | metadata: &MetadataResponse, 105 | selected_index: usize, 106 | marked_deleted: &Vec, 107 | ) { 108 | use crate::user_interface::selectable_list::TopicListItem::*; 109 | 110 | let paged = PagedVec::from(&metadata.topic_metadata, height as usize); 111 | 112 | if let Some((page_index, page)) = paged.page(selected_index) { 113 | let indexed = page.iter().zip(0..page.len()).collect::>(); 114 | let list_items = indexed 115 | .iter() 116 | .map(|&(topic_metadata, index)| { 117 | let topic_name = topic_metadata.topic.as_str(); 118 | let partitions = topic_metadata.partition_metadata.len(); 119 | 120 | let item = if marked_deleted.contains(&topic_metadata.topic) { 121 | Deleted(topic_name, partitions) 122 | } else if topic_metadata.is_internal { 123 | Internal(topic_name, partitions) 124 | } else { 125 | Normal(topic_name, partitions) 126 | }; 127 | if page_index == index { 128 | Selected(Box::from(item)) 129 | } else { 130 | item 131 | } 132 | }) 133 | .collect::>(); 134 | 135 | (SelectableList { list: list_items }).display(screen, (start_x, start_y), height); 136 | } 137 | } 138 | 139 | fn show_topic_partitions(screen: &mut impl Write, height: u16, (start_x, start_y): (u16, u16), partition_info_state: &PartitionInfoState) { 140 | use crate::user_interface::selectable_list::PartitionListItem::*; 141 | 142 | let paged = PagedVec::from(&partition_info_state.partition_metadata, height as usize); 143 | 144 | if let Some((page_index, page)) = paged.page(partition_info_state.selected_index) { 145 | let indexed = page.iter().zip(0..page.len()).collect::>(); 146 | let list_items = indexed 147 | .iter() 148 | .map(|&(partition_metadata, index)| { 149 | let consumer_offset = partition_info_state.consumer_offsets.get(&partition_metadata.partition).map(|p| p.offset).unwrap_or(-1); 150 | let partition_offset = partition_info_state.partition_offsets.get(&partition_metadata.partition).map(|p| p.offset).unwrap_or(-1); 151 | 152 | let item = 153 | Normal { partition: partition_metadata.partition, partition_metadata: &partition_metadata, consumer_offset, partition_offset }; 154 | if page_index == index { 155 | Selected(Box::from(item)) 156 | } else { 157 | item 158 | } 159 | }) 160 | .collect::>(); 161 | 162 | (SelectableList { list: list_items }).display(screen, (start_x, start_y), height); 163 | } 164 | } 165 | 166 | fn show_topic_info(screen: &mut impl Write, (width, height): (u16, u16), (start_x, start_y): (u16, u16), topic_info: &TopicInfoState) { 167 | use crate::user_interface::selectable_list::TopicConfigurationItem::*; 168 | 169 | let ref topic_metadata = topic_info.topic_metadata; 170 | let ref config_resource = topic_info.config_resource; 171 | 172 | // header 173 | write!( 174 | screen, 175 | "{}{}{}{}", 176 | cursor::Goto(start_x, start_y), 177 | style::Bold, 178 | pad_right(&format!("Topic: {}", &topic_metadata.topic), width), 179 | style::Reset 180 | ) 181 | .unwrap(); 182 | write!(screen, "{}", pad_right(&format!("Internal: {}", &(utils::bool_yes_no(topic_metadata.is_internal))), width)).unwrap(); 183 | 184 | // configs 185 | write!(screen, "{}{}{}", style::Bold, pad_right(&String::from("Configs:"), width), style::Reset).unwrap(); 186 | let longest_config_name_len = config_resource.config_entries.iter().map(|config_entry| config_entry.config_name.len()).max().unwrap_or(0) as u16; 187 | let paged = PagedVec::from(&config_resource.config_entries, (height - 1) as usize); 188 | 189 | if let Some((page_index, page)) = paged.page(topic_info.selected_index) { 190 | let indexed = page.iter().zip(0..page.len()).collect::>(); 191 | let list_items = indexed 192 | .iter() 193 | .map(|&(config_entry, index)| { 194 | let item = Config { name: pad_right(&config_entry.config_name, longest_config_name_len), value: config_entry.config_value.clone() }; 195 | let item = if page_index == index { Selected(Box::from(item)) } else { item }; 196 | let item = if config_entry.config_source == ConfigSource::TopicConfig as i8 { Override(Box::from(item)) } else { item }; 197 | let item = if topic_info.configs_marked_deleted.contains(&config_entry.config_name) { Deleted(Box::from(item)) } else { item }; 198 | let item = if topic_info.configs_marked_modified.contains(&config_entry.config_name) { Modified(Box::from(item)) } else { item }; 199 | item 200 | }) 201 | .collect::>(); 202 | 203 | (SelectableList { list: list_items }).display(screen, (start_x, start_y + 3), height); 204 | } 205 | } 206 | 207 | fn show_user_input(screen: &mut impl Write, (_width, height): (u16, u16), user_input: Option<&String>) { 208 | write!(screen, "{}", cursor::Goto(1, height)).unwrap(); 209 | match user_input { 210 | None => write!(screen, "{}", clear::CurrentLine).unwrap(), 211 | Some(input) => write!(screen, "{}{}", clear::CurrentLine, input).unwrap(), 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/user_interface/user_input.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::sync::mpsc::Sender; 3 | 4 | use termion::event::Key; 5 | use termion::input::TermRead; 6 | 7 | use crate::event_bus::Message; 8 | use crate::event_bus::Message::UserInput; 9 | 10 | pub fn read(label: &str, (_cursor_x, _cursor_y): (u16, u16), sender: Sender) -> Result, ()> { 11 | let stdin = std::io::stdin(); 12 | sender.send(UserInput(String::from(label))).unwrap(); 13 | 14 | let mut input: Vec = vec![]; 15 | let mut cancelled = false; 16 | 17 | for key in stdin.keys() { 18 | match key.unwrap() { 19 | Key::Backspace => { 20 | input.pop(); 21 | } 22 | Key::Char('\n') => { 23 | break; 24 | } 25 | Key::Char(c) => { 26 | input.push(c); 27 | } 28 | Key::Esc => { 29 | cancelled = true; 30 | break; 31 | } 32 | _ => {} // ignore everything else 33 | } 34 | 35 | sender.send(UserInput(format!("{}{}", label, input.iter().collect::()))).unwrap(); 36 | } 37 | 38 | sender.send(UserInput(String::from(""))).unwrap(); 39 | 40 | if !cancelled { 41 | let read = input.iter().collect::(); 42 | match read.len() { 43 | 0 => Ok(None), 44 | _ => Ok(Some(read)), 45 | } 46 | } else { 47 | Err(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod paged_vec; 2 | pub mod utils; 3 | -------------------------------------------------------------------------------- /src/util/paged_vec.rs: -------------------------------------------------------------------------------- 1 | pub struct PagedVec<'a, A: 'a> { 2 | page_length: usize, 3 | vec: &'a Vec, 4 | } 5 | 6 | impl<'a, A> PagedVec<'a, A> { 7 | pub fn from(vec: &'a Vec, page_length: usize) -> PagedVec<'a, A> { 8 | PagedVec { page_length, vec } 9 | } 10 | 11 | pub fn page(&'a self, index: usize) -> Option<(usize, Vec<&'a A>)> { 12 | let mut paged = self.vec.chunks(self.page_length); 13 | let opt_page = paged.nth((index as f32 / self.page_length as f32).floor() as usize); 14 | 15 | opt_page.map(|page| (index % self.page_length, page.iter().collect::>())) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/util/utils.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::fmt::Display; 3 | use topiks_kafka_client::kafka_protocol::protocol_responses::metadata_response; 4 | 5 | pub fn pad_right(input: &String, width: u16) -> String { 6 | let str_len = cmp::min(input.len() as u16, width); 7 | let pad_length = cmp::max(width - str_len, 0); 8 | format!("{input}{padding}", input = input, padding = vec![" "; pad_length as usize].join("")) 9 | } 10 | 11 | pub fn bool_yes_no(b: bool) -> String { 12 | match b { 13 | true => String::from("Yes"), 14 | false => String::from("No"), 15 | } 16 | } 17 | 18 | pub fn to_hex_array(bytes: &Vec) -> Vec { 19 | bytes.iter().cloned().map(|b| format!("0x{:02X}", b)).collect::>() 20 | } 21 | 22 | pub fn current_ms() -> u64 { 23 | use std::time::{SystemTime, UNIX_EPOCH}; 24 | let now = SystemTime::now(); 25 | let since_the_epoch = now.duration_since(UNIX_EPOCH).expect("Failed to get current time"); 26 | (since_the_epoch.as_secs() * 1000) + (since_the_epoch.subsec_nanos() as u64 / 1_000_000) 27 | } 28 | 29 | pub fn controller_broker(metadata: &metadata_response::MetadataResponse) -> Option<&metadata_response::BrokerMetadata> { 30 | metadata 31 | .brokers 32 | .iter() 33 | .filter(|b| b.node_id == metadata.controller_id) 34 | .collect::>() 35 | .first() 36 | .map(|broker_metadata| *broker_metadata) 37 | } 38 | 39 | pub trait VecToCSV { 40 | fn as_csv(&self) -> String; 41 | } 42 | 43 | impl VecToCSV for Vec { 44 | fn as_csv(&self) -> String { 45 | let fold = |a: String, b: &T| { 46 | if a.is_empty() { 47 | format!("{}", b) 48 | } else { 49 | format!("{},{}", a, b) 50 | } 51 | }; 52 | format!("{}", self.iter().fold(String::from(""), fold)) 53 | } 54 | } 55 | 56 | pub trait Flatten { 57 | fn flatten(self) -> Vec; 58 | } 59 | 60 | impl Flatten for Vec> { 61 | fn flatten(self) -> Vec { 62 | let init = vec![]; 63 | self.into_iter().fold(init, |mut acc, mut a| { 64 | acc.append(&mut a); 65 | acc 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tag-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION=$(cargo read-manifest | jq -r .version) 4 | 5 | git tag -s -a ${VERSION} -m "Topiks version ${VERSION}" 6 | --------------------------------------------------------------------------------