From dd4b653b8e56cbde3b875aa6e35e113559ba0c86 Mon Sep 17 00:00:00 2001 From: Ilya Kuramshin Date: Mon, 3 Apr 2023 17:29:07 +0400 Subject: [PATCH 01/17] Support for old ksql versions 1. Fixing ksql response column retrieval (existence check) (#3594) 2. checking "format" for streams commands if "valueFormat" not set --- .../provectus/kafka/ui/service/ksql/KsqlApiClient.java | 5 ++++- .../provectus/kafka/ui/service/ksql/KsqlServiceV2.java | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java index fd68add726..e8f4954bf0 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java @@ -52,7 +52,10 @@ public class KsqlApiClient { boolean error; public Optional getColumnValue(List row, String column) { - return Optional.ofNullable(row.get(columnNames.indexOf(column))); + int colIdx = columnNames.indexOf(column); + return colIdx >= 0 + ? Optional.ofNullable(row.get(colIdx)) + : Optional.empty(); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2.java index efd7e9ca2e..e8c2a4c65a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2.java @@ -89,7 +89,14 @@ public class KsqlServiceV2 { .name(resp.getColumnValue(row, "name").map(JsonNode::asText).orElse(null)) .topic(resp.getColumnValue(row, "topic").map(JsonNode::asText).orElse(null)) .keyFormat(resp.getColumnValue(row, "keyFormat").map(JsonNode::asText).orElse(null)) - .valueFormat(resp.getColumnValue(row, "valueFormat").map(JsonNode::asText).orElse(null))) + .valueFormat( + // for old versions (<0.13) "format" column is filled, + // for new version "keyFormat" & "valueFormat" columns should be filled + resp.getColumnValue(row, "valueFormat") + .or(() -> resp.getColumnValue(row, "format")) + .map(JsonNode::asText) + .orElse(null)) + ) .collect(Collectors.toList())); }); } From 0ff7e6338632b9f901edf530662d369351d8f8d9 Mon Sep 17 00:00:00 2001 From: Nail Badiullin Date: Mon, 3 Apr 2023 18:52:14 +0400 Subject: [PATCH 02/17] FE: AirGap: Remove internet dependency / google fonts (#3602) * improvement/offline-fonts remove google fonts, add local fonts with font-face * improvement/offline-fonts add font-display rule * improvement/offline-fonts fix fonts path resolving --- kafka-ui-react-app/index.html | 36 ++++++++++++++---- .../public/fonts/Inter-Medium.ttf | Bin 0 -> 308392 bytes .../public/fonts/Inter-Regular.ttf | Bin 0 -> 303504 bytes .../public/fonts/RobotoMono-Medium.ttf | Bin 0 -> 78948 bytes .../public/fonts/RobotoMono-Regular.ttf | Bin 0 -> 78996 bytes 5 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 kafka-ui-react-app/public/fonts/Inter-Medium.ttf create mode 100644 kafka-ui-react-app/public/fonts/Inter-Regular.ttf create mode 100644 kafka-ui-react-app/public/fonts/RobotoMono-Medium.ttf create mode 100644 kafka-ui-react-app/public/fonts/RobotoMono-Regular.ttf diff --git a/kafka-ui-react-app/index.html b/kafka-ui-react-app/index.html index 33e18ad268..be10fc78a3 100644 --- a/kafka-ui-react-app/index.html +++ b/kafka-ui-react-app/index.html @@ -3,13 +3,6 @@ - - - - @@ -25,6 +18,35 @@ return window.basePath+ "/" + importer; }; + diff --git a/kafka-ui-react-app/public/fonts/Inter-Medium.ttf b/kafka-ui-react-app/public/fonts/Inter-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9a3396fc4364afffd1984813abb564d4509b6606 GIT binary patch literal 308392 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%0DMg>>O3W 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¨QWDHOpnSPm=TUbqkLhX-IKJP51cA$S;K@CZB# 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

TwxS z7Pqc-x;@mLmmt0Iz$d9rl|se`Job&nGKH!$^~op0yMuwKfRGOv8z3|Sd*@LTZ4sf{^snJj9pndbALtD-A3PG(K@T^<5!!aumMC-25u9W`swLkh?`@iOj2FJ|`+u z98m%uGfZwSR>)8u!+tij!)>q$$ft~v{OLv}i%n*89PR|JjbE^-=#-)*I4x5Hp)3>5 zD|R_w~T8*=7by1Z*D3Z#Mkh`Kivku*2rB{~f74j+vU|NI{trlJJLYtZa z!flO7RLwrLimqAW33jNIsuwLrZM(;mQ0cwGvU1+_xynMS=p|_F+G0V`B36f+Tq*Vt zOnPSiKQC6{-DH#N<-O(#bG6D{D-}yhOGQbCI%N)bqltgmN2BWhDZX$_+Ribf}Ls^k$WziO{jYi{TB1zNep zr;(V1YI_+v`I*e4%?&Gkz6SY4xrKugC6Shv$p>moU8u4(X}uz$LuDQchx*N~I;l|j z7{3@$FZ{1qrwclHY-fNY+1{XBrd90-%nGY?=a9AfGvN zQdEKn4HS2dS<1{=BW%hay-F@Kt{X`1?w@`^Rr81*K3j!;eq+rgv!-8M-85oUYsNn) zs}Kfl#=dB5z~qh#OG{AyTP_OQOhd86fYBbTME=m0!)<%&7vC6lxP#vC#j`S(bRX`z zuxZwXVSgm(4egoMawRD#O)L-BoML7RVQr^2`iFz1cSzz(gEb2*$gwb3m(&Fog#jkQ zLl);3ENTK`ZB90BmG@KSExAH}s3UK`bH_)PH?g=$E@)|LdTPt&r2MnFEKUJhFFH-;Vfd)Fyxi3_J%=EHP%ZcJl`*tGSy5ZN0*z3z zW}nNstgdbuI%7aHk%CM8gMWm@`D0y^F~W@^7E8Hu5d@xq8eyJfz9q;0MwXUqBqBE) zVZO`~x4T~Rsg|5g2T_A`syT`jrC$@b$GvSZi_<|k4!sJq2fMaR+!XVi%pDT249-Sg z{}nL^ltPh3T4A77>bM~~!{%;KU#wJq{4bu7t1ag7o)rl!7TdaBrfKJ4^nTbV&f zcs7$e#!=IhkX506{nPOvc#-l31AO4@SY#lnJXKK_yAiGKY(*p+e{3sTJ2$wU?&r~9 z$Y(aA@ioBvDm*3wS`Ae`CaO_X59xK5(y#6wkX2OQeXWwu7CIZsBjqqqR`ll)CKX_CA!>kidw`=FjSv%aUGIosF71!wSW4Ii)xw{ zz{~eFDX$Rut)^*-)R4*It0*s%sVZ!0(?BdW!|VuElnLl_+33i3$JxjTj~s5yl`gbvfjf3S2a&RFX#z`JmGVvgO^B-Bu@<|=9)}4hsnAi6BiKXVJRvm(%m;SGYCEM?v#zQ0Qx!;!gYN{imLN;Qj}gB{^80Cpo?X4wDC` zF`GwQTNayv#bdIJHbc6aO*|c|rH}V=edeG0QQopo(SCL$Qd1L&oZViKR*do1fdjrF zsDZrP%|^8edLcr*u|GN2{7D9m9i0@R90SaUZm~_NM2-I?l&WwjwC@z^ zq1q*$NRQfNCSpm2&@8N!8w|SsghQ%zR){O%VlY)owa2O}13F!uVd+9=JNAn8wp_sf zuxKXG0a*7-Zm+Q96Fia2>`MTC?MF8XeJ9y3{_nQP@$cJlvgRtkWinMxkK`W3^BDXR znWD0?0$tKw!g4W9@qWXjXmL4eo)qSCu~;q_2Mk?jha*){zi(3}&~HYCV1-^{o*vF@ z^!uV!;mFz9mw!f}YI=40TC1VSW$g?2y}mB1F{O2NdumQG>hy08Xu?vN>Jl5>cSt7J z%xYvV3AZWX1rn88l<0Bx{r6IHrOH|^tPqKnol&y3ai&HgI~0LWaD+K$_BnpP->63o z1E|Sl!JBhtlha2o#XldAs_k%2t3{Qj%5w2J5+oB=NKho9>yI;^R$&XY`7MAZ&Q=Rt zCnyH5Yh!l+`L5`54(arRJ(zHRO|mu4A;zAoAMt<@5fL3CW^(_>KyEHKNRVh<#?tqv zgh%nkD0h*;!z32x-C?02vqWD}29Ls=1m_MD6t6wwjJ3>NaD#QoMF=)m+VPJv))NN* zml}v4Xd@=VbTji#K5a%i5Kd|=9GuJYEv!P_K zN-A+V-CTpwcpOy?53FakfVQS|-94+A{gkz-y85bFL^|`z>Z&G} z-L<~+w8v4YGH+<;qjQ*JTi+TOST16&fj;#ZvyamzQMENWhQicaMd29OlQw-G)F94p3HIuA#*Z4Ss}Ig^-yTh$VlbLw90~8EEOpWFnVHENmVW;*>u@(4tV=Qvy11jnHxM+%9V1hzh$77Ji-6NYrUIx@z`(Nhi}}R{{}j#Dg?aS$P+bu z`D@#GyEYY-Ft;#AiyQd&W5ryES@$DjZVO`KH{wzR<3L1HcJIeNll{o|ct7&lk6rs? zpZ(aiKlTiA@p^A68CZwxioQq~P)i1?e}gZP46Fy!><{A;o$;yletfVFAM~*w=nXGU z{Xl2T9?pGo@-EqDH%z^+@zDA}JpZ6@q*tdm-oC+)57rxWhQ8bf^xs_dbskTvMx#f? zaVvAgRiAcGzRmt|>f5)cyk(xsU8_}d*V4zQ-0DAHi~W0%87-!fD@P;iL&*9549M+5 z6oqetOA$Xd9va3b9QaZ22`Wy|uavKH@KQX1j0j{Y9`*9xwuRf@JP&_S50W{|!zY$8 zx07Dpog>82!W_;HjWF-ElE(8Fz0`CW@12nsGndyfH|a#t z=*ulvkLyNXZoQf}dlB)qGEZikN10!@lU7LapW|en3lAbMpRuQBXv}{6bYpGJElXD1 zQCr(c;P4tFS+>w*K}}+=#o{2WRdzixGx}hk@N(IDd^{ah<4%|pIngfQ`p*<7Za=6m zEI7w>n%T03oOc!u_KnL^3tWb%vh@8iMMUSCmqb=TGBu{v8%NsQjXIrAB=+gGjgIDp zCZmBSx6ESZQ>zr)U+e4~P^l0NUHhGrqEs%STsgg~^VRbet*3_*^=f*s*!YNduvLRXD1`}%>O-6+?{`NbBKX?}*B`#g$@r}g!H zxbFcjg`L`Q)|vj(n$swy^*%Yfx96=b7rfWi)dN&Q7UB5M^cwgXfAFllo~FtvQ@LHR zC6INq{;$t9uXVfqMx%bHYQ~a_LqYFgN9T*1pW{-|$jf${ePt`^S*>yD-P)B+P3PBc zei#|FpJFOp@9e*0UTjfVE{|y)MF2JRRnahwld<7xQ z_tZ*I&Biak%wE9bYRWFbdLz9gcP}0XgpFm#QbFxO(4EkGnRnj|T-i4^)^}y>_awp~ zZH5_m`Q==4)T56gS&9WLVe?|>XE7!r-e3qUa1NVJf;&-Hl1jt~ZXqe=i@&vJ-y!K^ z)T-=dT>AV?)PmPf=qZ$55cV9?&tW9&9q{17J3RbLQo}q(Jj}13pP&5|sr?(dotaBj zQ-n=xePjGmayza^Gz)!^C;PA7ufB6fE|Alm9-{iGWt1$mf+5Pm#uV*jA zRSR*|EqH1rp2|&p@~Y{7|2wJq8?k1;pmvbw5G#0q>F2F^ooT{V-OMvwi~#<4!aWBI zmOs!?0Q0qflQf3oGwEn^QJ-fQeg69A6}Bq5%2*;OCUmhttB}TQRO@ru zqp!qlY&l=|(@`tAUpms*e0kG~L+FK0kz3gqzye@&?txaucA_rgvwiP5QcwMeF9`4@ zHqL$lD>`Xe=4}?7$O)Y-r$H-~$TKcbDe|T%cLREls}+7vu|kRXv42NT2f_dWwMvn8 zmP$$kX`R183InUGgsuG+CFZ$;K+4CiSkIV|*wNawF_yW4xvjji%p_OY(36#|{1yWd zPb5-4Usmdv9<1NrP1@H!-P+REo5?)1jCrEx{B&xum!go^VO-SU8%>fZQef@1Qla>D zyT!h-wQgS*^LG30x`hWrMxc)v&D3qAyK9}zWJN;i+79ODiBX>=tu?eb0&wqBMZK^w z9s!T;Mc1K%Rm-~%PO|Eg8sx;<;VYb~bk!HkB*O8<*J}E=02616k+yHsk&_i+NtE_M*YAb8Fge zM}OsFfm)1mEU~^^tSUhOLnMYlVhka;B)b`SS`F6tmc}#F>msQ+27^hjF?L6St833r zuaCOg%@mJRhoigu2Wmzfb}#jc%Wj#Qh^|lf4PMqzzr^nJ7*QHPMR%KPH2M&r0Dx(Ut0{<>`rU_;%a1Yle4I_vAD_@O zH&G!RBNMH7{dM+~c#u8EqPXB%8GCvQuKkvJ_D|A%|6q_fB{+7B$tOS_yqBf?U9NGE+@bqM2suqZ+-+uo& zy$`jm)ogwjk8AL%I_WiYZ&DvPDdGqk~GGgFtMx1+Kx8aZcLdTls8V%C}{BGIAIIcX=t3Zc)aj2Ru( z3WKEtL6Vvod!w<8aQ@jJ3=YjtnqR3WqAD+eQ9rKY|R^^sbdpfR{C)%4WWLNoVJ_aI{%evf}AX zOAqe~yU{!p)Wn8uR^V9zi}EmspytFeffUcH72KdsdYlSNTwS~(av}2p^Z1#^kN>V~ zquU=Wttj`HjDtzC`*1zLSyHJR7xCiM&g?;IAtdi+zb|~%(%`Vi-=Wj!pPJLx^X8Uq zM>{))rFi$`ood4kc&{!cDK`B?o=r}VxhIpI&=4Q26008{E5zt{4cX- zVP@xrb3YB%q$ym0Z$mp@BZG+ zPOHsf1EgEkDQ1i9IAHGXA71-ZYfBG+gKiz9$T?(u6a6K#?k@yb=$6clW(_-< zy1J^HdkNV_#y9Y`FstvdI&3RCqccoqo84@h9$I@4#aZlVmauuR4JG86>s|o5YYf!I zRxUfq4y1~>sKM`f$iXb8W?sZ1&ki#k^y#2ICwT4^oDvI*zK1XLSB0;i^+j%^?=-NcGsw$bd_OfUIc{S{xmO!)v7--uTd@($H zd`{D8$P_1p{%iJQ)MEb}BTUz~z7T+38;{Pj+kGt0UH}<&9Z*R7i#AL~iP>|Kb8K;E zCN<&YV#m#LZtm0k=rMtb^OM3vIEj^K^2Nmtw$Oo_WnK)hhb*VZO$f6EWhIhKz}y?~ zcNv8m_@_mVB@O9SeoH`9UP<6LFH=^0Ua3Uaasu{JEG{oo3CpD=B^7YRld&>cg)(Uj z4_W+mqOvmbJP@`8;!>eWE;Ussbj1K_;}_ ze)hjL0Xb@}fihSwZx3i{jC<{EreLQ=swgkzSKzGFVP&tx&a-ht#lJ7)kFMebSIV)d zyxBbU)K8?ykau}*yK_@_-$hB}QBR z7)w{9oc1@riS=qwMz7J%tXp!ey+)}}lWi+HSKHi9s%hApjhGN<^Ns$2h=L4loE+* zpym?lh^JmIk@B2&_vzDXS9$^LD3T(-qO?p~`8iAy5K)CyDc6-keyFvomWZ`VVYX1; z_H6Tb{70aXrOdhjaQvSnQW>%nJL-lFRx=e0>N*0xm1$R>%QNKeyQMF^$nNl#mQ=XZ zbpd3bcq&9zg^`_P(Y?&M^yS#;R-B;xO!ERKHpKuFl;UyR7Zbg~{al#H*#k@{zjL0e?m=4;%cO+Lqn`266;GpIM2-px(l& zA4R_ahd&j&$L5Ou=efW^7mw$#Mp>>^U?|(!Z83AWcFcP`VoMHa)oP_mJ6QD@xegj$t!r923LQdN_c`(#%w12117d)&+#hk?)hbqj@OM=sQFZKwcfaJ=rK#)Je*#r{-#!0r8 zn9z&zRLLGdR)fgv5j?7eRFt9Rh4qxG%;o^nGT0MXDkzqe{#LF8WDu_mTC*Cgf?q=M zsO-fkjw~z1`9!Uz(%QGhk9t3;3apCIUyMgwEjm}NLa#-AjDJ&ydu}ATG9F$STKw8# zTch69tVNnyX{kV7UZEAymt_Yc!|vKeZc9=g^d61Ht;5x+J^hhY)#MAz*oPV4fSv5d zj07Wi|5b>EBm_r^sH3DU=jfx{V4tP&?nQa>9d9Qe1L=Wl5z+ zZ(mSbx7u%si(y4j9^L0cC7`CU@q9^nxj|ee0+a%LR#K*sR2syX=WNUq zmF)T4_q^)Z9a`)BnwA|wty_p0Kme3#D!wq72|+4k zg#h-Cpn?{ZVkhBQJc~MuRezb^_cA2Jm0Pxa?g$e)%(KJ2wA~fqK_owFAyfqS0C0goQhoTrE)^z^i1>PQ$B`!zp zPI+00tW0E+^D3BT@(jFwPhxOIr$PI?7}|AR&Yok5zEE}HB!7WR3tawWidCUwi@I#d zqd;1wD5v}jeR!wHO;R}U#h68 zs=9mQ><4Gzr)Gf}g|wNP>T70^lB(sw>Nz@@!e%+-4I4)yrH>uz0=F8 zr(AcR^{_^h@Ezm^dNN{JXpBxNMQ5#WEPl@HqMn_x)#Je-)ITlNl@tqRcXb_I0ROInTuC9QAGi58cMe@vWdMSJ z6iR^ZF9r(*Y7;0b0qZ0)3x#l13CNQ1x!^Bn1O}|pKCQzFQE!`*jBd_A)PqX>{Wg1A zWp|PC@^Xh#9@g^Ce(^<&uNC9l4oY=Tj&FiVa5DMi;PRBf4QmUX1SsY7fs2#LCW@4o zmT4+KW5uvSrl@h7P@XO!%f`3zwq}nlJ`fD~DeUpj*SdOL&R%=h_1!QHtagGBTZ@|# zA34JAE`BAv9s7!Q_OC9Yc)o^CSPLANubH8Y_=90u+H4%(o>Xfx^ceW3+cCgRn}c;ecnz})j> zkbZ3rY-8<|o0vu#wgKV*ON+`F1}N@J%P?#5GVgEH7`g{k;7tOGgimU)*q_t@seVyEI>!~hRW6G`D-3%`dlhB(neg2+q}MTAt0)*u76jEge> z&j}Tqh;$?X!YbkY?0%-2M_#$>^6UfSa^7lcR`$G+#~(kN%yBbkVkA}6e%@RBCpknA zmi=(DFRvWnZBEI{ncn2hE1n0an$)i|N2LPut( zpiKCpP}17c{QHeOe8%7LyG&cFSa=i}r+&}H(`dwA(-_?~pu!mE^XGD~3zJ9+DQ-$Y zeF}e(ogDR~4^yRMC?-!BYO6@C-KQ{r;>am{S?2#AsQef!$PK zS92@Hn-UDksnfjqnYlW>YB37uQvr`>eRE>iVT3o^5g19vM?y7qwU4Zm$pG0dHI$W= zYoPKge(SJWN9!A-ySuxt%y@ft_`M;mMiw_k&mCe`m6kn<^wOob*VZ*rpONmW0ks${ zGsF?2KejmJ?Ze)0g%!Vre;Dz18_?aFV985tGR|iPr%WMBDxAzu@2CvioBHf=ge4z; zoD`8VXt&jNXDwSiCzsbD;)Lpn>d51`PNyIW^~r`F;CSyrRBL^CQbLXVuh>+8v%*?d?yW zI{GB@=ia@E?8ns?%wG-!lNjj;ZF;G*W614rEUT}-sCV<*-CZ-&ykDf_%%6y&8cWD8 zn*04X^e_4Mfa4{hmoF_^2__&xx2}|gUoe>5`sUmSQ`VaMPR{NqOj0X6h(Gl?!I2^l zikZhB;XTN5{;Je`s}-4J6=o^wGgMly!@i`ZX0hGw(P_|FNNOxssxAelo?D~$i^_47 zgg!0clPiQ$yf!OTHh2NxHn*wRz*;%L?uvFWMbu~1(UoWWv*RB{y0scDe!3&(q*ACV z1>&+Q?TGc_Gko3~b2h3gm8B($N@W%E74izpD@DR`qg2Mi_9LKYBr2JytQ5~E*GnaO zxEv7VGca%Eju*O+VjbTBf9@&TS#(*^Jw=CDPtFvRJ13r}6y+DJUg6e*b+MMsl&kY* ze1VVZ|Nd@#+G|Z9t+Jw0s6wZ{&a`{Y9KrCYHF z9<{*(V^Sm%`SiStkmfOeVjs&bQl(v_(s$H7QXRQLEz!J4k znJ`CsvBf&m-76TUt|IWJjQ<*UgCL0rxjgLgzSb~ zBC8UXMc(ZK8i2lr_c`M#;-Sq4|C z$!I#I&ebB6kvP*vx8qb!*-e~EZa1rI78mTMQpN2iP9?jW@xOs)!FsH6EUHjS zus)7NoPvjnRkKtRoK59)=HSNQ{wvNqb_$I|s%PDyy1Ir;W5W}h`*&A0kLsk-j<$|p z?%M=+YwM=ImaV~HgRt)Enz}hAlM_zYa);J1y}D%tz+(opX%Wru$uvK+LL}A}5Rw5zGeYXvg@@W8~J)i?9CIGX8ttIq@wY^EWPb$=L~nv%rwOjowR}vS0H= zCmyAGvk%d`#-F5`jnoB4F3(nFvBvazqp)Dm9tSMBLEYLz!O|2RltF; zHTe`gY`|oI!1eFZZnWNFWfFdJVhKDt4d6msJ4ZWePo}{>}{BD{rP)kJ)mC7sCIH4}Fp@#~) zMjFtVgCYQL^6BwkBbcC)ikxbFL?JWccKA+NrA>owL<*Y_745}*s^1x$>2x=%v<9(I z5;Mq=jnHE>I5bM_K-kvnb^3>F!7eS@EE!F`9#5~y=uoS5Lost#pHt(PYuJiUxmZ@E zRoKcDWoEfLE|(}2vP!E==1V)#=v?CymRAA>&JQyP->}G4DV0< z6dn0svGXG*b?1}j)5%o#$rvy7x<%{d{8^r)nAK`|n-kx=*|R=3;(n(6_?^4YwyQYwZ}1d8wl!0@7q znO)@U7sy%68H*GjzAu;k@%M0`xw0PWUFvr)rx`!vsZEn#lm9|3;GCi|`e$@8?xBZ{ zS_5ipgKjf~E?5hYySxk)ert<1Lf1J92(0IU4ljn*15X;)vX6}~u~Y&(mFwb%N#Y;S zQi?tQEHy&y+IpML{JRZBvO$nnlfpA^Sq*WQm zFS%<~J#W`t>DA+B)~~vYw`+C!F6P0uHiJ&L=jyB9zxryX^QxGzU}<;-`cyE+_-n|+mo-bzoP7~>HA9- zfhUAu5vN!zG{Ah>CJrjjYHrB|IlPHNeKN;xLBI)UsRqIyzlez7%byP8J zyfK_=TVgRAU3SN^Mo**OkTQu4BC%`JU3ZP6mr>bXnua$-OMlAyXYt=5ef^*ubO=U+ zV417HoX;^Z1)4!la-E#o#QP`nXXLy-^WcL+b6MAxP#0Be;s&-vUpWWPVztl^P%9HQ za}qWj{VBuTbQ2jfN~~y33s4M&Nuz19s3jIaMk(>usL`Oo2t`WhPwA(NpTl^pljBMK zhbE7_Pnj_7^r79KXfPAb)Qs*D7U9ZDwEtB^m(^-cWw0oALpsV zhs#wycsc=+;#Ea}2V}9p&xV_lPw17@H^pw)=->rhr59{l^2wXTYT8vbiyk|7d&ic< zfcHLj7OnJu@qST!0@+@y5590Lu0kWp5t|a$n$l;J_lvs#v{~(TS7V6eF17)4XV+%0 zC&27lQ?5%OC2AMBT zJM%97p&wbZ@xzC)YGGjA9S(cg0uZUvzAaGQm^O z#}l&mhxS8pv5J$-Ii5=W_2<|BBRht+V=Yc6(AW$HwT6AA)fz@g)Zqw+9gb-6)i1oj zEOa(1)fx?eU!2Y+wMvKXgH29XH0p9hqnQ2Q>2r9`!5hfu&_bb~fGvYpaC!ji@&g;8 z9vph=e&*vd|5tI}9T-*7#XB>1OF}}Co`4~QBqV`^-DEcf5J=gC9$ILjZIXoqLN?j# zrc;z6#e$$%kdD$UsMxTf*icbGMN||-R6YwuP*BP4`%TFb@cZ8T^JTf*JLjG`=giER zbI#12dj}7^jqQVnw~NI4V7iU?dRB*b8mhXcgoK*fQ~0sYSdPZ%Ox?%uj9-bjwr_0n z8}v_u8IYCC9fmBKEVTC1Zt?l8*tE8{+_F0!A4M9ozf+632#dO${4M2i=M{?pw9&=*;OFZSH{Wz)R5zKJPkq zjyx|~0Z(|?x$^*oHvWier@8Rx3FKp53sKEpRP%dptCnycvsI?B4t?9T>fS$M{GIV} zy+8K;(JP}~p~OEr;Cch7O<+ z%dyGa#+Q9wP;q!W>*nd-1{_QJ!Lyhg^u7uBD!>QpMgpFo;2Q+o)HkUiuXgnP2QYl_y#&NDL`Nkx&eD8xkJ=IBBXrIFdtNypi<+7 zX@~UB1HRU<0N-ISWOC{?jSYBb%jbI;@Vl{#ZZruTF5wFW{$&FKWkAJlREAxEJ|J*wU*8Yt zE=c=`t}mp`71BDg#rI%dI}>;^{-hV_TQf8 ziD4zow02i)M33ny>3BxbM}L7%95V8Q5t;c-XY-ANjQBF z@c&nz7VX3N@%91!&3$~aXul-TeO%J@{Xka$@4xlk=6(O>zPkc+u!l7GEEIfh?#se= z=-+|1!*_uG&3zkYQ+Gh*zeC)=xoUdP2M_?e{eLaTlm-~ZhZZDrG@8;ew z%CA2QJwf?VUi;ZD$h8Tz&`19XY5^Vz(0n)V=c{08xUV#DUy1ZlV2#utLpoBV4166nq(F-c+M8LUC z^yt3z5{-^kY%#3goFYvHIdt8 zu8BrE*_Tg(03QQ7!dp@JQKZ$5d+0F_iga7t|HC*H`A&;`)w+q)2|U!RuGG*6@b48o zm81hc*4u&a!~^(a9M1B5KlthaulIK2-^vI4CBO|V@B9)#`Cf(C8|8eNKWq>XO zG!>emA~%+Ig+p)lvubB(NP4G`^k!QZ015XA+q6wcI2|p@Z4;sf_-o!LU*tUL^b5RC z-kduTSkD=4kb%kEzA z;E#do96@!opeoX$Zz7G-*jw|yDQgKHl)qdEK6c*~;X!3BagQx)$$pl5Y_ZRh(F~|) zU0`3JMSzQ)5cEF+E?Sq`FNFXqbkx9mm9*m1V8tzfOI>^%aA_5%o%yo~+t|sEZTEd6 ze2m2Al1Oa(2`-7S^u__Gi>6ZlNh<&swpG-)j4A9Fd5x>Dut8T&iMmkVC`0-?krwxj z-N6I*MJ@bI$047<(_aM~c)ZSz^afPyAR}sFkHY?dIsg~nCg;5A`x5(G&Rh6M!v#Qh z2<&O_k%Iu2Hfj?}AZ^sN))$(vc|@bI?WD744~dQbtFY|>20u2mRACpA-OaEsD{Lbz zB##h|zZII7BdG6ElD{JG(f4&jM11y^l)MYCV~nqKvw7r7pHkQ#39YUIyg4PIK{ zVeAKZZ@{so1mLeI_$2{P^lofsU9rwg_X&~qbz3w0%Hj7A?h|AmeGhWTK3b(aA}prt z(TJ=?k2caO-9Ex&Yv5x`SMJLMJ{#_n@j1soZ-Dkn3&4h-Kr8#spiZ7Me&NR+=KG#z z0saxNGYk*-v4?%cW#qCvZ-ibc+CzO;X*uxH&ui4_ zmVU|F`msmA?ydD>ANFI9ILmvJXn}y+{Mfm^voynxea(-Zdjz9Yg&pC?9_Rar4)$Yn z4<{@z8}~fdY+33id)<>x=l1$vrEap@(XjfHo0WQt_i-`CY_hlx-EqQc zn>ut~3Eh(x`c24S;5rGNIZ87*KH6`R7}10)_$xy9B>n_H{#0@wJa7|!7r;5bfljRr z0~FZ$0ORQDqJ6nd=Cn5&Uh-oPJ^izh1=L-#S>lZD6dt`Z|&ISS?d*d z0XovdROt@VX&j%Akoiblj9R%o2ZX)g_+DdU6Fyq)XTL}KOV|tHk=XDbxV`uq*yTpv zcLjAe%=Z|fA(WLbZ?5@UE*DXx79cM zr2$-M3g6Rnh|su>f~!Nf-^XVmwt`NY@NgytuMu<>p>Bp4_k+H~uhJbC_HHZcCe2U* zxWuo5#$5uPWxikao5lFzS3xHqEy2eZ7%Az;=--Dthz~_wa{DN;xqXz_lYP~c$Ac0Z zbtbT7UF!V!ynjjjD%}U7e+hgpjlxG=Hj`$GAAc%YMyEH?#{=gkX{OHS-gOy1c(R#C zfQovP_N?+gsvo0!&`;f=!w3ub@!l83tXb3SmqYg+;qhoc%%JiSri^o-h58SD4ICeK zYrFAXz~yXNmF^wk4Mbcd>XJuMTob+)(o1|Uy~LkN77OVmzQIp_DtVX$f_|y@sO~t7 zTr}x+k??mmX%I_?)teb{FYa#iZqWV1Tb|E{AU4TkEeWsF)!udbAQ7AFQ*bH5|6l(7ciH|jQoPILk7X*D7 zYvnQK$G=b19_9|XoD>ls@V5g0H;zwtDY%TSxSZI!3-GrjoKZhM_RGAEPY?mBa$!%V z>-c=dAK)862m6r%j`JzO8I$Z$oEb#fL zgV!@`4y@u8j0o{P1Jr*D35WLI3H+|!NdDeG;6Z?6{5WqppzEM3St7FR0Vv~h^y1xN zx$}sB|BoB?9_fykt(w0o*quG&{q+3pxBuUED$;uFa5{Hf`{w#yMR+~F*=|M5_bE8@ zorV=>@v)zO?Kpuqp_+5$HXq>?|VWvw&&Cm~(hp)SD z%5(egY}2)hS6@9(kL>Us@DAMJ?L&_s$9Zu109??f{wGOT7r_@$NM=ztJW;xI>B7E! z&7LaJ^zG}`ZU1LAD+jF;p!Ep)JZRxN^(pefl<^inx$PJ5A_9%UXX799p7Op{c>45( z4?m<+>bNN;|NX zF2Cyiyd7=l{p2sY{GlV~=?-npwLeFW)LLAdy92u-Yyu56=XzZlDU56_?Z8relj2A3 z58j`>KhfevtGo;G^{oi-=NHDXAlwGiRa5mHT%>2>8|)r;uo|Fwa}sTS^5Dz z&2C{PHi*q&HS8BHTzg3ST$iPrpj)Batb0fIqt2)Aq+g`}#gJ-fFuZU0$LKOXZu}x3 zBp@|lM8IPKUj#-4)(0L5yxPLiVq=R#Exr#j2E_zT4RQw^4muqi6?}j2-e6zLf|l-9 zTC16@YFfS0I-Dysz6`gi=HgqoPT-*8H&g(jF@BCutk30VrVhCv$5*`vC(m!NeNLk3T zkj)`ag}f4SHsnIc)zH474WajjZVo-&#nNSPm;5f3T^4rP*kym0b6r{2sIFCA@9Mg> z>;A6ac0J$KduvPVQu=DxwyEV>`~OC zyhlTiWj*fiv8l%sJugH|hjL4CZlOxwgZi##crAd2}zE8TEbk(A_q*$^nlPt?EKUssV1FTinCD!|`>#ZMJPg^f0hbPA*rzVe0 z-j)1%^6}(fQj96~lyxbWQfH=qoq9E`D(zg~A${ld{j?wHH>}^0{=GHC9ggX#M8l=M;QOVbaepUaq;F+bzkj2|+*nX#GanbR`w z&pe!^%bJk&OxCZqLffmhk8Hl|*z8T&FJ&LgG2}$#^vxNOGb87Xoa2LygS!pxKiD<+ zslne5u?$%`HJ)ZWp?(Mh8VMt?JA{Fviot>c2m9m-41dpPgw@!iLd7(ZkD!{c|2-#6ir3Fjup zPrPg5#);2PJUc0DQr4v6Ny{hgob>jjlaqT*F2eNuru^6Qzn;=@%J3;wQ_f8tHg(0+ zcc)&N7BlUkX(y*gPq$9DO|O~${tVBIi}u;}!v*~do-d3oyrb}oB5To|MPE2V9n1gc z?*Yftj#nLD&1^Mu>db#;<rJTc)sGzijOKjt2j|{rsCI%%N5sLI#;l(y{oG$($(8#arJi% zc8zmQcg=QrTnk+*U29zHTw7hcUC+5*cO7(n<~r&6+4W~7tE{WMqw=20wUrwxcU8Vn z`FiEMl^<6gt2|x#bLH=qf4iBxg}bdg)E(iDb0@m{x^3A)_B%=wtJrQyy5x4^SS4g=d9;~ z=W3O%s&!STsvcDdReh^$RU@kMs_a#zRW(%$t5#M$Shc0d@+_YIAk}>cQ1xtEX4buCA)SqxzoehpQi}-c$W*_50O_t4~y)t-f4CYJzJz z*F@Izsp(gfT{EU;YE4OvyJm6CnwpI@yJ}vjd9&tF&GDKuHRo%5wXJGHYoluuYX{U0 zt<9^o*Ou1S)Gn&MxAu|R$7`RdeY5s(?TOm6wU_HiU2t9Jy2v_HUH`hAy0LZB>Sot@ z>Ta*QtM1Xd-E}Y4y;b*d-B)!#*8N`Rtq-j4P~W{izCNwqRzJFaYJG9NyMAH)UG)#u zZ>xX0{+0Uo>OZYNS^rD@l?JV$bwgM~Ttjk0M#IR4$qh3bTn+OZRy3?_*wXM+!@h=h z8;&-7+wgP49}SK3`p)yr+cWQjdEd_uoS!g%!2A*OC(U=vub4k?{_^<`%-=Wv!~*?- zgaye9GNn)C>yKiho}4B=GZB#|Q4drf>Y*kb_m9SnMh0OmU$* z#P4FBa1GYyuE4C>cI@S|2v2fm1F{QG>lOjN9cy|YA`XnS7vV@iG!c)Lyo<3SX(6e{ zH{X|&28e=B^Sfa;CEjbsa;Q^c>6sOgtc+s%Kn=69jwyHL}M)USzVKA9n?_eEXhqgD9N zp?)UOLQOe%+co_X&}#e_uYQGSKi-1>{>7tBUdZ(bm_0M{#`! z^>YwUrVfJd5om$@IZ=c1SK_~NLN|Uyi?33@C8Pp+()3&9zuF4Tkv}*t@mmHhT1MB? z735LOMzo*_^n2Qtt|i~mHjSO>TXZqSlMwO_c@C|3kSri0@Z9AP-FxlH#<|yO8~gp! z$kJ#FmVnhQF~knqi=cx}k}1}wyP;`TQjDl_2ISfcU5LZjd^&Ws2<=sdlI4@p_?p66 zyhYj$dFMg{O0mLV5wKJ66mTr+GY)sS&hz?B!qcBMpk0V4wHDgE33t1o$E*a!-r~Fx z{bDS1ZxNXZKB?Gy_yI(%xp>O{C2IOs=x6}2$Kz`};rQZJ8OFe)fj1qyy^SI1_+rgY z@-BMObnL{#Q&z}V4*AAI+EJ*d7fHGI8ShTty}qaL74dhV>+4a%tCn?(c(#6VPi-Lumk^z)zw- zQX92W6Y^iCGpUc}LUv4MK$e9lV*}O}I>6n88s|r%?|I+LzGt!4Zl3QDT8~?`EvS8N z&(`>DnF-vI0f{GWa9jnvmFQ_sly@vzgQuK0`XlECQ0F~!Hn^@qyK;IP0O#*5aJ#qy zHJT63nV`2A?ZtDhhh+Ugrw*;X3O4Z|tm0}wMnVe@Bi~q1S?rrBsPG(P(4uYeJaR1b zftM;Ct@tupy$k*arZ~7o+yaVYg)QXwIQC-X=0`rbavk7DCbXv=xHTPHA?GY;RWc;1 zhxYaM)8tZ|@qN@h{a>#+-)09^A{=U-hLi_!92HV`g~0>V7paSE8N^L^))>MQDjlyq<)vt|RSzpW=4`Wjc>J30^Ne&oWhNUaPzg z?u2&`mm(g#D{t-Rq0ckgR){c;wOk(kQ2K=4E z^3FCSJRg_gJe`x9n?nYXmcB;GBcOow3MX`9F2|6 zThIan}u}RRqK}OKGuDq`wlyIQOszx3!`BJ!_{*>uWw=LMqixlm(e`N{Xh%>6f-g~;@IBd4n1w9I>d9rgPjtt1 zr*(honf{hA5;h=gVc4dy-@`7W3?#f=c&G62@FW?iM=6d*9Dn1$+}W9z&fs~+8O)5I$@_lp=_sC;|DBq8YS5|l z)77VPzT5xZ3n%xUe3p>!62FW3uE$B&scGK@pE`Bwwv$z-%!GXFJ{5B+>Qun2NVtW=te{XbF8oBwpYPXG0%8~QI$6M#Z{=D=f(!P{aF z!5X{aHP*tHOowfof#}nYp1l};YYE<;ei+scQ6-{-nRGHd%{}nE>)Awji?Q%fli|be zC67=$Y)U4bO_s4qWDT7LuQwk$nnCliLU$c0qBGI&vS=yEhKI@_(_rzqwb>3|^%&M0 z?S_|l8nNPY(1JI}o9MCJ0)9b0C7&ZIJcg*_Yw`>E2@%N8AF(d%HEMY9pTjwK^$9@$D6?4XxyqZ-)> z@Aw3stW0M4hP;#CQCzt3ba)pi~SIOUW0=Y&fk$>n! zT27ARnd7f`w)QyHlO5DZP9UbALW}5hI)fI_S#&Bb#A@zZ=AdCZ2D{9AGWIgVo^^s*71YYhd%(e71lsWVf?B=sj#PTSD(; zi|8tPA1u}ydOv-Du4PND?**)xDwu;?HH_(lA6I%m&d_UdH9-v#; zT5KP%l|4ka(Z}fH>|wT=ZfB3Mb?i~Lo^4OP{ANux<23`VxJa zJx2G@SLmzsHTF1too#13=o@S&-OqNh*Vu0QA$x*;#GYhNv8UM{dWe2ZKVi?X*Rd_g zVS0of)g0^%wx6ED=7Fc__v}sf7QI9-YqPaET9H;r|G?Ksuh6U7Ol=nZi~g-m)26cn z>}_q7Hk$rJuW7|v2{z{NYGbsq@G*GlhT&6jOvm0~?`q?;Jb0V;HJ4VYjn^hHJu_$% zwMop#0$89{z*?{%ZH8utE&qUh$b#{WvvRG1wPLNcQf;m_S9Ak%7h-Muui4NMf}FgQ;V@~32a+2EL}LFG80CfR$Pq(#DVh|U=`qS;|Pa+ zabje^uSOxC6ZX-8n3v~Fhn;jIj}!1pq+u z2UpGMG?ghHrMpgLJg#|3_&Bl*<+)Dh|DBiDB`>)wcQrgjHKecuVn?}oy(WOF3sUxh zlqG;Uz@rlX^O6t8s?tpOf|2kdHpHfhq6E#kUT=ry5>G(>rj{H6{&q!sq~JAIq~Tz> znj*fKjru~UDH6^8(-0XzV^za9K+A4U%J$M51Gx-g6oRQ`5OGfzMCGg9S!-DHzr(YvF#P<>K zovkreycKVd?Is6A1l~sY&K8KSU%(jkZ86e*2j0<$-P?mPwjBhIJA=Fq-`WbJ!ZqXq zc?qq3FZ^#eeD~;nc7_ZyOoA7mJ1^zw_p1&XM58wF<{N^ikAiU;}G@X>dTh4*j z%%+(%3v*C5GM8o(FOFijJmZh?|Z@L{v>Y zbR5m223lV88knsX7rxnyiE2*1$XcatkHKGmpVe%8LLw~s&R`pAaf4@fbc@H9^ zlZXlG$+w6Q8ldw#5z+0!6S{enk8KuGd|-fkW4eSc#hlUU%!&xMenBf(0jQr zCL6DNWA2OT!}Jl%={-u;VMa)JVT_Qr(5=$%((QBy-AQ-R-Si3iBz+3wjy?1lJPTAl zS9;tge@kDdZ_xepP0S%3pl@SV@m=~J<{b{=$>E3eBk60Um!+T5&+x5-WAqDp95ahw z(XZ(V`VIY-o^0~G^bGw0a~D6+v-CgoXUs(YN`Is0=y{BLE@GY%KDo&kb3e>|FZa7Q z_+0o`>07yH<^GiWQut4JOyMazu#T(~#!w+F6r;he>{b@Wy0LK9okg%7ERscGJx5O# z!(v$+>&1Grc$UEW;Q6PSC9))DVOIFb6qbtZi2AaAtUtSr4PXP=AePQDSSHJ2HkQqD z*kCq<4MoiT2;%C;5HmkYUS`ACa5jRCWVvh<8_mYx&Hr&&V=|siU=uMWMO4bBkSEww zHjPckvsODR;E^A5$heG^u-R-5D`j(8nT*ePbjD*dR>i7$RL1IVj>p&{9*?o5>`u0f zEoUp(N_H2!yD9o&s}XtK&mQ107kdb?)(vr%jI!7^_85DdM^kJkBB|Z%iKZ9|(bKc+ zIkuNQ&t70JvX|J)81uY>NbWU6Rfwf{48>z49s}|CN5wZEu|w=*_6a-8jNVLz}R*-!YU*nim1>=*Vc`;DDr=SAFun1@~Fu@1Y! zuCl+_-|QcDjWsea^I=R(HHLAqPSa}!&8P)nl;1)N(t@>?S}TnA+i16FZMAk)%ha+oo0hHRXoIyO+E8tnHe4H_jnr~^ z+&)k`%jv7N&p&i&bxtw;l z!6s-JYy#NAR^)Ws3kw}(Zlg_7)of+6j5bL*DCatv;G9C|+_`p%tQ%ZlcLfc;e%CNW ziZ(=vHbfFI3~`r~7CC~3UdId`){Kr}xDeMcTu2iQsz!$-r#aBx z4%ObXuICS))vSti#k{?XuirJ4NSR8MOeLzlOJsY`zK$6@rx_hXsgTo9DqEqn*)3fu zZ|^dBzf9gQYj)q@l+EB2&EUMD8JtpzAZN4oc1cPuNy*imQl+e{%IiYv+`PTr0@vV? zj68yo=LSX|Ra}p1Z;xnik8JO1Nw)er*}!UldsoZ$uDyQKP%GQJPGoCQ=Wm$+TopJ< zoz+3jgl0pAvXa^QQfJw$N~5Q&#FUtnBG1`Lfr1c_3jzf#aV2EpSGr=SbB;hwGG*)C z&N63Z%OW9- z&5Dj$)}zIos_<04#3Y$cRCtLBFHzwoDt?KIUt&|fOqDN7oo(u@1Cxl$Fs zRK+h<@k>?wQdRj<72j0FH&xM1Rdmx7-84lvP0>kHbkdYuX-ckig`ckQ(-nTY!cSLx z(^b8uEB@(p9UoG6vf zoM;XzvzI$7-7aT&u|woZRE{9gEEOx!EEOx!EEUU~EIYb6IjKcDcZ9-;2HT_u6NvyD zO3EE=;1FJBeifWMw}c1nGLhE|xkWDbbpn11ca=BzX1(B`%YYv>Dh|5INNC`o~PAz#g>dLnH08uaA^@DkMIaYU zT(wu8x+$vw>gRAFj84g3)M8RbCM)x3h2Y%0P_1vu9<1^RGQsMmNVE_kfVdO{EwX=_ zEXqDulwGhWyI@gv!J_PfMKznnr0kaJ{}$QrO%}7V*RpdVt?Zv_c8gi%Gb?*%R(9H~ z?6guLOQN!CiHcsLvWK#7n=DCE)htPhev-1QNs69D-M1(@7G+m03g4>WRt2{XcQJOf@v$jB zHpR!L%5778Y>KZ<@v|v@HpS1T_}LUc)n1ltRS(&Uf41VEt@vjv{@IFOw&Ev^yvdR+ z>04C~vMSBDO3gRhWCVe;Y|QE+ZG< zDgGIXztlIgP3jxYioc9-kXGf-RQxj)e;M)MzAAsF;-9JbXDa@gs{EO%{F#cs)Mv9z z>NC!&{8FEhR^^xajI`n}^%-eZei>;Yt@um*Mq2TgdX2Q=FZCK}Rel+vnQbyc!&#MI z>N(P?{8G=6R^^v^j!)N`Cw`K4YXt;(CN_+_i| zNM_o$zeqhqTJ{Hv)H9Ps>KV?056*&*$s*eiXVp)o z9OzH#ewxBhQ)#JJ$gldZ)IXCYRq;zzbW{LgNfY%9d6j%=qF#_z`BPPSQdN1RenB1u zm->XX$|v;`>85fhJ1P4Ws-|oU9m#pmJ z<47ptt5On%xym^ttkmu>9{o{gcQK7=OaoUi=?PST({^POijlFU8X+tTn?s X<{Ut1YpgjFPpI+m9q)T_5F!5unYy$I literal 0 HcmV?d00001 diff --git a/kafka-ui-react-app/public/fonts/RobotoMono-Regular.ttf b/kafka-ui-react-app/public/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2ab8f34a5bb5fdd2e87ba931bbafc8792fc92bb5 GIT binary patch literal 78996 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 literal 0 HcmV?d00001 From e3ee4c7fa7dae75d92b715ff7efa96e22455913c Mon Sep 17 00:00:00 2001 From: David Bejanyan <58771979+David-DB88@users.noreply.github.com> Date: Tue, 4 Apr 2023 07:49:05 +0400 Subject: [PATCH 03/17] FE: Redirect the user to the wizard page if no clusters (#3481) * Redirect the user to the wizard page if no clusters * added test case * changed test case for redirect to new cluster config page * replaced mockedUsedNavigate with mockedNavigate * added ability to show dashboard if clicking on it --------- Co-authored-by: Oleg Shur --- .../src/components/Dashboard/Dashboard.tsx | 11 ++++- .../Dashboard/__test__/Dashboard.spec.tsx | 45 +++++++++++++++++++ .../contexts/GlobalSettingsContext.tsx | 2 +- kafka-ui-react-app/src/lib/testHelpers.tsx | 9 +++- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx diff --git a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx index 46c35d5079..7eab4c1d2f 100644 --- a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx +++ b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PageHeading from 'components/common/PageHeading/PageHeading'; import * as Metrics from 'components/common/Metrics'; import { Tag } from 'components/common/Tag/Tag.styled'; @@ -11,6 +11,7 @@ import useBoolean from 'lib/hooks/useBoolean'; import { Button } from 'components/common/Button/Button'; import { clusterNewConfigPath } from 'lib/paths'; import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext'; +import { useNavigate } from 'react-router-dom'; import * as S from './Dashboard.styled'; import ClusterName from './ClusterName'; @@ -20,7 +21,7 @@ const Dashboard: React.FC = () => { const clusters = useClusters(); const { value: showOfflineOnly, toggle } = useBoolean(false); const appInfo = React.useContext(GlobalSettingsContext); - + const navigate = useNavigate(); const config = React.useMemo(() => { const clusterList = clusters.data || []; const offlineClusters = clusterList.filter( @@ -55,6 +56,12 @@ const Dashboard: React.FC = () => { return initialColumns; }, []); + useEffect(() => { + if (appInfo.hasDynamicConfig && !clusters.data) { + navigate(clusterNewConfigPath); + } + }, [clusters, appInfo.hasDynamicConfig]); + return ( <> diff --git a/kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx b/kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx new file mode 100644 index 0000000000..e9141e980e --- /dev/null +++ b/kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useClusters } from 'lib/hooks/api/clusters'; +import Dashboard from 'components/Dashboard/Dashboard'; +import { Cluster, ServerStatus } from 'generated-sources'; +import { render } from 'lib/testHelpers'; + +interface DataType { + data: Cluster[] | undefined; +} +jest.mock('lib/hooks/api/clusters'); +const mockedNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockedNavigate, +})); +describe('Dashboard component', () => { + const renderComponent = (hasDynamicConfig: boolean, data: DataType) => { + const useClustersMock = useClusters as jest.Mock; + useClustersMock.mockReturnValue(data); + render(, { + globalSettings: { hasDynamicConfig }, + }); + }; + it('redirects to new cluster configuration page if there are no clusters and dynamic config is enabled', async () => { + await renderComponent(true, { data: undefined }); + + expect(mockedNavigate).toHaveBeenCalled(); + }); + + it('should not navigate to new cluster config page when there are clusters', async () => { + await renderComponent(true, { + data: [{ name: 'Cluster 1', status: ServerStatus.ONLINE }], + }); + + expect(mockedNavigate).not.toHaveBeenCalled(); + }); + + it('should not navigate to new cluster config page when there are no clusters and hasDynamicConfig is false', async () => { + await renderComponent(false, { + data: [], + }); + + expect(mockedNavigate).not.toHaveBeenCalled(); + }); +}); diff --git a/kafka-ui-react-app/src/components/contexts/GlobalSettingsContext.tsx b/kafka-ui-react-app/src/components/contexts/GlobalSettingsContext.tsx index 4de05307b1..563fb175f3 100644 --- a/kafka-ui-react-app/src/components/contexts/GlobalSettingsContext.tsx +++ b/kafka-ui-react-app/src/components/contexts/GlobalSettingsContext.tsx @@ -2,7 +2,7 @@ import { useAppInfo } from 'lib/hooks/api/appConfig'; import React from 'react'; import { ApplicationInfoEnabledFeaturesEnum } from 'generated-sources'; -interface GlobalSettingsContextProps { +export interface GlobalSettingsContextProps { hasDynamicConfig: boolean; } diff --git a/kafka-ui-react-app/src/lib/testHelpers.tsx b/kafka-ui-react-app/src/lib/testHelpers.tsx index 508904d146..42539a0aac 100644 --- a/kafka-ui-react-app/src/lib/testHelpers.tsx +++ b/kafka-ui-react-app/src/lib/testHelpers.tsx @@ -26,7 +26,10 @@ import { } from '@tanstack/react-query'; import { ConfirmContextProvider } from 'components/contexts/ConfirmContext'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; -import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext'; +import { + GlobalSettingsContext, + GlobalSettingsContextProps, +} from 'components/contexts/GlobalSettingsContext'; import { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext'; import { RolesType, modifyRolesData } from './permissions'; @@ -35,6 +38,7 @@ interface CustomRenderOptions extends Omit { preloadedState?: Partial; store?: Store, AnyAction>; initialEntries?: MemoryRouterProps['initialEntries']; + globalSettings?: GlobalSettingsContextProps; userInfo?: { roles?: RolesType; rbacFlag: boolean; @@ -110,6 +114,7 @@ const customRender = ( preloadedState, }), initialEntries, + globalSettings = { hasDynamicConfig: false }, userInfo, ...renderOptions }: CustomRenderOptions = {} @@ -119,7 +124,7 @@ const customRender = ( children, }) => ( - + From 1bcdec4accc7ef9e319a3f671183f01eb4e5bdba Mon Sep 17 00:00:00 2001 From: Shubham Jain Date: Tue, 4 Apr 2023 04:51:38 +0100 Subject: [PATCH 04/17] FE: Add Search By consumer name in Topic View (#3089) --- .../TopicConsumerGroups.styled.ts | 6 ++++ .../ConsumerGroups/TopicConsumerGroups.tsx | 36 +++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.styled.ts diff --git a/kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.styled.ts b/kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.styled.ts new file mode 100644 index 0000000000..77768dc998 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.styled.ts @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +export const SearchWrapper = styled.div` + margin: 10px; + width: 21%; +`; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.tsx b/kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.tsx index 69948fd66d..173c28c76e 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.tsx @@ -5,14 +5,27 @@ import useAppParams from 'lib/hooks/useAppParams'; import { useTopicConsumerGroups } from 'lib/hooks/api/topics'; import { ColumnDef } from '@tanstack/react-table'; import Table, { LinkCell, TagCell } from 'components/common/NewTable'; +import Search from 'components/common/Search/Search'; + +import * as S from './TopicConsumerGroups.styled'; const TopicConsumerGroups: React.FC = () => { + const [keyword, setKeyword] = React.useState(''); const { clusterName, topicName } = useAppParams(); - const { data: consumerGroups = [] } = useTopicConsumerGroups({ + const { data = [] } = useTopicConsumerGroups({ clusterName, topicName, }); + + const consumerGroups = React.useMemo( + () => + data.filter( + (item) => item.groupId.toLocaleLowerCase().indexOf(keyword) > -1 + ), + [data, keyword] + ); + const columns = React.useMemo[]>( () => [ { @@ -61,12 +74,21 @@ const TopicConsumerGroups: React.FC = () => { [] ); return ( - + <> + + + +
+ ); }; From 87a8f08ae1f72719c02e570a74de6cdb8f4a6058 Mon Sep 17 00:00:00 2001 From: Vlad Senyuta <66071557+VladSenyuta@users.noreply.github.com> Date: Tue, 4 Apr 2023 06:56:02 +0300 Subject: [PATCH 05/17] [e2e] Setup selenoid for multithreading (#3609) * add browserSetup * add browserSetup * upd browserSetup * upd browserSetup * upd browserSetup * upd browserSetup * upd browserSetup * upd read me --- .github/workflows/e2e-automation.yml | 5 +- .github/workflows/e2e-checks.yaml | 15 +-- .github/workflows/e2e-weekly.yml | 5 +- documentation/compose/e2e-tests.yaml | 54 +++++------ kafka-ui-e2e-checks/README.md | 11 ++- .../{selenoid.yaml => selenoid-git.yaml} | 12 ++- .../docker/selenoid-local.yaml | 33 +++++++ kafka-ui-e2e-checks/pom.xml | 27 +++--- .../selenoid/config/browsersGit.json | 15 +++ .../{browsers.json => browsersLocal.json} | 6 +- .../provectus/kafka/ui/pages/BasePage.java | 12 ++- .../kafka/ui/pages/panels/NaviSideBar.java | 2 +- .../ui/pages/topics/TopicCreateEditForm.java | 8 +- .../kafka/ui/pages/topics/TopicDetails.java | 17 ++-- .../kafka/ui/pages/topics/TopicsList.java | 23 ++++- .../kafka/ui/services/ApiService.java | 10 +- .../kafka/ui/settings/BaseSource.java | 11 ++- .../ui/settings/drivers/LocalWebDriver.java | 68 ------------- .../kafka/ui/settings/drivers/WebDriver.java | 96 +++++++++++++++++++ .../kafka/ui/utilities/WebUtils.java | 27 ++++-- .../com/provectus/kafka/ui/variables/Url.java | 4 +- .../java/com/provectus/kafka/ui/BaseTest.java | 83 ++-------------- .../kafka/ui/smokeSuite/SmokeTest.java | 14 +-- .../ui/smokeSuite/ksqlDb/KsqlDbTest.java | 12 +++ .../ui/smokeSuite/topics/TopicsTest.java | 9 +- .../src/test/resources/manual.xml | 2 +- .../src/test/resources/qase.xml | 2 +- .../src/test/resources/regression.xml | 2 +- .../src/test/resources/sanity.xml | 2 +- .../src/test/resources/smoke.xml | 2 +- 30 files changed, 334 insertions(+), 255 deletions(-) rename kafka-ui-e2e-checks/docker/{selenoid.yaml => selenoid-git.yaml} (61%) create mode 100644 kafka-ui-e2e-checks/docker/selenoid-local.yaml create mode 100644 kafka-ui-e2e-checks/selenoid/config/browsersGit.json rename kafka-ui-e2e-checks/selenoid/config/{browsers.json => browsersLocal.json} (52%) delete mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/LocalWebDriver.java create mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java diff --git a/.github/workflows/e2e-automation.yml b/.github/workflows/e2e-automation.yml index eb10985747..5ce1437614 100644 --- a/.github/workflows/e2e-automation.yml +++ b/.github/workflows/e2e-automation.yml @@ -36,7 +36,7 @@ jobs: - name: Pull with Docker id: pull_chrome run: | - docker pull selenium/standalone-chrome:103.0 + docker pull selenoid/vnc_chrome:103.0 - name: Set up JDK uses: actions/setup-java@v3 with: @@ -52,6 +52,7 @@ jobs: id: compose_app # use the following command until #819 will be fixed run: | + docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d docker-compose -f ./documentation/compose/e2e-tests.yaml up -d - name: Run test suite run: | @@ -78,7 +79,7 @@ jobs: uses: Sibz/github-status-action@v1.1.6 with: authToken: ${{secrets.GITHUB_TOKEN}} - context: "Test report" + context: "Click Details button to open Allure report" state: "success" sha: ${{ github.sha }} target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} diff --git a/.github/workflows/e2e-checks.yaml b/.github/workflows/e2e-checks.yaml index 83be371bea..771eb698fe 100644 --- a/.github/workflows/e2e-checks.yaml +++ b/.github/workflows/e2e-checks.yaml @@ -15,20 +15,20 @@ jobs: - uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.sha }} - - name: Configure AWS credentials for Kafka-UI account + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v2 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: eu-central-1 - - name: Set the values + - name: Set up environment id: set_env_values run: | cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env" - - name: pull docker + - name: Pull with Docker id: pull_chrome run: | - docker pull selenium/standalone-chrome:103.0 + docker pull selenoid/vnc_chrome:103.0 - name: Set up JDK uses: actions/setup-java@v3 with: @@ -40,12 +40,13 @@ jobs: run: | ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} - - name: compose app + - name: Compose with Docker id: compose_app # use the following command until #819 will be fixed run: | + docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d docker-compose -f ./documentation/compose/e2e-tests.yaml up -d - - name: e2e run + - name: Run test suite run: | ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} ./mvnw -B -V -ntp -Dsurefire.suiteXmlFiles='src/test/resources/smoke.xml' -f 'kafka-ui-e2e-checks' test -Pprod @@ -65,7 +66,7 @@ jobs: AWS_S3_BUCKET: 'kafkaui-allure-reports' AWS_REGION: 'eu-central-1' SOURCE_DIR: 'allure-history/allure-results' - - name: Post the link to allure report + - name: Deploy report to Amazon S3 if: always() uses: Sibz/github-status-action@v1.1.6 with: diff --git a/.github/workflows/e2e-weekly.yml b/.github/workflows/e2e-weekly.yml index 4683c7e111..ea5ca7e522 100644 --- a/.github/workflows/e2e-weekly.yml +++ b/.github/workflows/e2e-weekly.yml @@ -23,7 +23,7 @@ jobs: - name: Pull with Docker id: pull_chrome run: | - docker pull selenium/standalone-chrome:103.0 + docker pull selenoid/vnc_chrome:103.0 - name: Set up JDK uses: actions/setup-java@v3 with: @@ -39,6 +39,7 @@ jobs: id: compose_app # use the following command until #819 will be fixed run: | + docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d docker-compose -f ./documentation/compose/e2e-tests.yaml up -d - name: Run test suite run: | @@ -65,7 +66,7 @@ jobs: uses: Sibz/github-status-action@v1.1.6 with: authToken: ${{secrets.GITHUB_TOKEN}} - context: "Test report" + context: "Click Details button to open Allure report" state: "success" sha: ${{ github.sha }} target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} diff --git a/documentation/compose/e2e-tests.yaml b/documentation/compose/e2e-tests.yaml index 2a422c54d4..ae56f5d576 100644 --- a/documentation/compose/e2e-tests.yaml +++ b/documentation/compose/e2e-tests.yaml @@ -11,14 +11,14 @@ services: test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health interval: 30s timeout: 10s - retries: 10 + retries: 10 depends_on: - kafka0: - condition: service_healthy - schemaregistry0: - condition: service_healthy - kafka-connect0: - condition: service_healthy + kafka0: + condition: service_healthy + schemaregistry0: + condition: service_healthy + kafka-connect0: + condition: service_healthy environment: KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 @@ -33,10 +33,10 @@ services: hostname: kafka0 container_name: kafka0 healthcheck: - test: unset JMX_PORT && KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9999" && kafka-broker-api-versions --bootstrap-server=localhost:9092 - interval: 30s - timeout: 10s - retries: 10 + test: unset JMX_PORT && KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9999" && kafka-broker-api-versions --bootstrap-server=localhost:9092 + interval: 30s + timeout: 10s + retries: 10 ports: - "9092:9092" - "9997:9997" @@ -68,12 +68,12 @@ services: - 8085:8085 depends_on: kafka0: - condition: service_healthy + condition: service_healthy healthcheck: - test: ["CMD", "timeout", "1", "curl", "--silent", "--fail", "http://schemaregistry0:8085/subjects"] - interval: 30s - timeout: 10s - retries: 10 + test: [ "CMD", "timeout", "1", "curl", "--silent", "--fail", "http://schemaregistry0:8085/subjects" ] + interval: 30s + timeout: 10s + retries: 10 environment: SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT @@ -93,11 +93,11 @@ services: - 8083:8083 depends_on: kafka0: - condition: service_healthy + condition: service_healthy schemaregistry0: - condition: service_healthy + condition: service_healthy healthcheck: - test: ["CMD", "nc", "127.0.0.1", "8083"] + test: [ "CMD", "nc", "127.0.0.1", "8083" ] interval: 30s timeout: 10s retries: 10 @@ -118,8 +118,8 @@ services: CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0 CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" -# AWS_ACCESS_KEY_ID: "" -# AWS_SECRET_ACCESS_KEY: "" + # AWS_ACCESS_KEY_ID: "" + # AWS_SECRET_ACCESS_KEY: "" kafka-init-topics: image: confluentinc/cp-kafka:7.2.1 @@ -127,7 +127,7 @@ services: - ./message.json:/data/message.json depends_on: kafka0: - condition: service_healthy + condition: service_healthy command: "bash -c 'echo Waiting for Kafka to be ready... && \ cub kafka-ready -b kafka0:29092 1 30 && \ kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ @@ -142,10 +142,10 @@ services: ports: - 5432:5432 healthcheck: - test: ["CMD-SHELL", "pg_isready -U dev_user"] + test: [ "CMD-SHELL", "pg_isready -U dev_user" ] interval: 10s timeout: 5s - retries: 5 + retries: 5 environment: POSTGRES_USER: 'dev_user' POSTGRES_PASSWORD: '12345' @@ -154,7 +154,7 @@ services: image: ellerbrock/alpine-bash-curl-ssl depends_on: postgres-db: - condition: service_healthy + condition: service_healthy kafka-connect0: condition: service_healthy volumes: @@ -164,7 +164,7 @@ services: ksqldb: image: confluentinc/ksqldb-server:0.18.0 healthcheck: - test: ["CMD", "timeout", "1", "curl", "--silent", "--fail", "http://localhost:8088/info"] + test: [ "CMD", "timeout", "1", "curl", "--silent", "--fail", "http://localhost:8088/info" ] interval: 30s timeout: 10s retries: 10 @@ -174,7 +174,7 @@ services: kafka-connect0: condition: service_healthy schemaregistry0: - condition: service_healthy + condition: service_healthy ports: - 8088:8088 environment: diff --git a/kafka-ui-e2e-checks/README.md b/kafka-ui-e2e-checks/README.md index d7f3c77c1f..a33b92739e 100644 --- a/kafka-ui-e2e-checks/README.md +++ b/kafka-ui-e2e-checks/README.md @@ -27,7 +27,7 @@ This repository is for E2E UI automation. ``` git clone https://github.com/provectus/kafka-ui.git cd kafka-ui-e2e-checks -docker pull selenoid/vnc:chrome_86.0 +docker pull selenoid/vnc_chrome:103.0 ``` ### How to run checks @@ -36,6 +36,7 @@ docker pull selenoid/vnc:chrome_86.0 ``` cd kafka-ui +docker-compose -f kafka-ui-e2e-checks/docker/selenoid-local.yaml up -d docker-compose -f documentation/compose/e2e-tests.yaml up -d ``` @@ -51,6 +52,14 @@ docker-compose -f documentation/compose/e2e-tests.yaml up -d -Dbrowser=local ``` +Expected Location of Chrome +``` +Linux: /usr/bin/google-chrome1 +Mac: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome +Windows XP: %HOMEPATH%\Local Settings\Application Data\Google\Chrome\Application\chrome.exe +Windows Vista and newer: C:\Users%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe +``` + ### Qase integration Found instruction for Qase.io integration (for internal use only) at `kafka-ui-e2e-checks/QASE.md` diff --git a/kafka-ui-e2e-checks/docker/selenoid.yaml b/kafka-ui-e2e-checks/docker/selenoid-git.yaml similarity index 61% rename from kafka-ui-e2e-checks/docker/selenoid.yaml rename to kafka-ui-e2e-checks/docker/selenoid-git.yaml index df2f095506..f4c5430f16 100644 --- a/kafka-ui-e2e-checks/docker/selenoid.yaml +++ b/kafka-ui-e2e-checks/docker/selenoid-git.yaml @@ -1,17 +1,19 @@ +--- version: '3' services: + selenoid: network_mode: bridge image: aerokube/selenoid:1.10.7 volumes: - "../selenoid/config:/etc/selenoid" - "/var/run/docker.sock:/var/run/docker.sock" - - "../selenoid/video:/video" + - "../selenoid/video:/opt/selenoid/video" - "../selenoid/logs:/opt/selenoid/logs" environment: - - OVERRIDE_VIDEO_OUTPUT_DIR=video - command: [ "-conf", "/etc/selenoid/browsers.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs" ] + - OVERRIDE_VIDEO_OUTPUT_DIR=../selenoid/video + command: [ "-conf", "/etc/selenoid/browsersGit.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs" ] ports: - "4444:4444" @@ -22,10 +24,10 @@ services: - selenoid ports: - "8081:8080" - command: [ "--selenoid-uri", "http://localhost:4444" ] + command: [ "--selenoid-uri", "http://selenoid:4444" ] selenoid-chrome: network_mode: bridge - image: selenoid/vnc:chrome_96.0 + image: selenoid/vnc_chrome:103.0 extra_hosts: - "host.docker.internal:host-gateway" diff --git a/kafka-ui-e2e-checks/docker/selenoid-local.yaml b/kafka-ui-e2e-checks/docker/selenoid-local.yaml new file mode 100644 index 0000000000..9d7fb8e0be --- /dev/null +++ b/kafka-ui-e2e-checks/docker/selenoid-local.yaml @@ -0,0 +1,33 @@ +--- +version: '3' + +services: + + selenoid: + network_mode: bridge + image: aerokube/selenoid:1.10.7 + volumes: + - "../selenoid/config:/etc/selenoid" + - "/var/run/docker.sock:/var/run/docker.sock" + - "../selenoid/video:/opt/selenoid/video" + - "../selenoid/logs:/opt/selenoid/logs" + environment: + - OVERRIDE_VIDEO_OUTPUT_DIR=../selenoid/video + command: [ "-conf", "/etc/selenoid/browsersLocal.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs" ] + ports: + - "4444:4444" + + selenoid-ui: + network_mode: bridge + image: aerokube/selenoid-ui:latest-release + links: + - selenoid + ports: + - "8081:8080" + command: [ "--selenoid-uri", "http://selenoid:4444" ] + + selenoid-chrome: + network_mode: bridge + image: selenoid/vnc_chrome:103.0 + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/kafka-ui-e2e-checks/pom.xml b/kafka-ui-e2e-checks/pom.xml index 0b458cf173..e4d912dbe6 100644 --- a/kafka-ui-e2e-checks/pom.xml +++ b/kafka-ui-e2e-checks/pom.xml @@ -17,15 +17,14 @@ 1.17.65.2.14.8.1 - 6.11.2 + 6.12.37.7.02.21.0 - 3.0.3 + 3.0.41.9.9.13.24.22.2 - 1.7.36 - 2.3.1 + 2.0.53.3.1 @@ -122,6 +121,11 @@ selenium${testcontainers.version} + + org.projectlombok + lombok + ${org.projectlombok.version} + org.apache.httpcomponents.core5 httpcore5 @@ -132,6 +136,11 @@ httpclient5 ${httpcomponents.version} + + org.seleniumhq.selenium + selenium-http-jdk-client + ${selenium.version} + org.seleniumhq.selenium selenium-http @@ -187,16 +196,6 @@ slf4j-simple ${slf4j.version} - - org.projectlombok - lombok - ${org.projectlombok.version} - - - io.github.cdimascio - dotenv-java - ${dotenv.version} - com.provectus kafka-ui-contract diff --git a/kafka-ui-e2e-checks/selenoid/config/browsersGit.json b/kafka-ui-e2e-checks/selenoid/config/browsersGit.json new file mode 100644 index 0000000000..9e01861615 --- /dev/null +++ b/kafka-ui-e2e-checks/selenoid/config/browsersGit.json @@ -0,0 +1,15 @@ +{ + "chrome": { + "default": "103.0", + "versions": { + "103.0": { + "image": "selenoid/vnc_chrome:103.0", + "hosts": [ + "host.docker.internal:172.17.0.1" + ], + "port": "4444", + "path": "/" + } + } + } +} diff --git a/kafka-ui-e2e-checks/selenoid/config/browsers.json b/kafka-ui-e2e-checks/selenoid/config/browsersLocal.json similarity index 52% rename from kafka-ui-e2e-checks/selenoid/config/browsers.json rename to kafka-ui-e2e-checks/selenoid/config/browsersLocal.json index 6387ffd139..35a494f33d 100644 --- a/kafka-ui-e2e-checks/selenoid/config/browsers.json +++ b/kafka-ui-e2e-checks/selenoid/config/browsersLocal.json @@ -1,9 +1,9 @@ { "chrome": { - "default": "96.0", + "default": "103.0", "versions": { - "96.0": { - "image": "selenoid/vnc_chrome:96.0", + "103.0": { + "image": "selenoid/vnc_chrome:103.0", "port": "4444", "path": "/" } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java index 201079fe3b..2bb4ecce51 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java @@ -21,6 +21,7 @@ public abstract class BasePage extends WebUtils { protected SelenideElement loadingSpinner = $x("//div[@role='progressbar']"); protected SelenideElement submitBtn = $x("//button[@type='submit']"); protected SelenideElement tableGrid = $x("//table"); + protected SelenideElement searchFld = $x("//input[@type='text'][contains(@id, ':r')]"); protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']"); protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']"); protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']"); @@ -37,13 +38,20 @@ public abstract class BasePage extends WebUtils { protected String pageTitleFromHeader = "//h1[text()='%s']"; protected String pagePathFromHeader = "//a[text()='%s']/../h1"; - protected void waitUntilSpinnerDisappear() { + protected void waitUntilSpinnerDisappear(int... timeoutInSeconds) { log.debug("\nwaitUntilSpinnerDisappear"); - if (isVisible(loadingSpinner)) { + if (isVisible(loadingSpinner, timeoutInSeconds)) { loadingSpinner.shouldBe(Condition.disappear, Duration.ofSeconds(60)); } } + protected void searchItem(String tag) { + log.debug("\nsearchItem: {}", tag); + sendKeysAfterClear(searchFld, tag); + searchFld.pressEnter().shouldHave(Condition.value(tag)); + waitUntilSpinnerDisappear(1); + } + protected SelenideElement getPageTitleFromHeader(MenuItem menuItem) { return $x(String.format(pageTitleFromHeader, menuItem.getPageTitle())); } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java index df10360844..35490a27d8 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java @@ -2,8 +2,8 @@ package com.provectus.kafka.ui.pages.panels; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.pages.panels.enums.MenuItem; import com.provectus.kafka.ui.pages.BasePage; +import com.provectus.kafka.ui.pages.panels.enums.MenuItem; import io.qameta.allure.Step; import java.time.Duration; diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java index f60bd6d431..9c255d5711 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java @@ -9,12 +9,13 @@ import com.provectus.kafka.ui.pages.topics.enums.TimeToRetain; import io.qameta.allure.Step; import static com.codeborne.selenide.Selenide.*; +import static org.openqa.selenium.By.id; public class TopicCreateEditForm extends BasePage { protected SelenideElement timeToRetainField = $x("//input[@id='timeToRetain']"); protected SelenideElement partitionsField = $x("//input[@name='partitions']"); - protected SelenideElement nameField = $x("//input[@name='name']"); + protected SelenideElement nameField = $(id("topicFormName")); protected SelenideElement maxMessageBytesField = $x("//input[@name='maxMessageBytes']"); protected SelenideElement minInSyncReplicasField = $x("//input[@name='minInSyncReplicas']"); protected SelenideElement cleanUpPolicyDdl = $x("//ul[@id='topicFormCleanupPolicy']"); @@ -49,10 +50,7 @@ public class TopicCreateEditForm extends BasePage { @Step public TopicCreateEditForm setTopicName(String topicName) { - nameField.shouldBe(Condition.enabled).clear(); - if (topicName != null) { - nameField.sendKeys(topicName); - } + sendKeysAfterClear(nameField, topicName); return this; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java index a99afd903b..d9c0105314 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java @@ -6,7 +6,6 @@ import com.codeborne.selenide.ElementsCollection; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import org.openqa.selenium.By; import java.time.LocalDate; import java.time.LocalDateTime; @@ -17,6 +16,7 @@ import java.time.format.DateTimeFormatterBuilder; import java.util.*; import static com.codeborne.selenide.Selenide.*; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW; import static org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils.nextInt; public class TopicDetails extends BasePage { @@ -24,8 +24,6 @@ public class TopicDetails extends BasePage { protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]")); protected SelenideElement recreateTopicBtn = $x("//div[text()='Recreate Topic']"); protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]"); - protected SelenideElement overviewTab = $x("//a[contains(text(),'Overview')]"); - protected SelenideElement messagesTab = $x("//a[contains(text(),'Messages')]"); protected SelenideElement seekTypeDdl = $x("//ul[@id='selectSeekType']/li"); protected SelenideElement seekTypeField = $x("//label[text()='Seek Type']//..//div/input"); protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']"); @@ -50,6 +48,7 @@ public class TopicDetails extends BasePage { protected SelenideElement previousMonthButton = $x("//button[@aria-label='Previous Month']"); protected SelenideElement nextMonthButton = $x("//button[@aria-label='Next Month']"); protected SelenideElement calendarTimeFld = $x("//input[@placeholder='Time']"); + protected String detailsTabLtr = "//nav//a[contains(text(),'%s')]"; protected String dayCellLtr = "//div[@role='option'][contains(text(),'%d')]"; protected String seekFilterDdlLocator = "//ul[@id='selectSeekType']/ul/li[text()='%s']"; protected String savedFilterNameLocator = "//div[@role='savedFilter']/div[contains(text(),'%s')]"; @@ -61,13 +60,13 @@ public class TopicDetails extends BasePage { @Step public TopicDetails waitUntilScreenReady() { waitUntilSpinnerDisappear(); - overviewTab.shouldBe(Condition.visible); + $x(String.format(detailsTabLtr, OVERVIEW)).shouldBe(Condition.visible); return this; } @Step public TopicDetails openDetailsTab(TopicMenu menu) { - $(By.linkText(menu.toString())).shouldBe(Condition.visible).click(); + $x(String.format(detailsTabLtr, menu.toString())).shouldBe(Condition.enabled).click(); waitUntilSpinnerDisappear(); return this; } @@ -172,9 +171,9 @@ public class TopicDetails extends BasePage { @Step public TopicDetails clickNextButton() { - nextBtn.shouldBe(Condition.enabled).click(); - waitUntilSpinnerDisappear(); - return this; + nextBtn.shouldBe(Condition.enabled).click(); + waitUntilSpinnerDisappear(); + return this; } @Step @@ -254,7 +253,7 @@ public class TopicDetails extends BasePage { @Step public boolean isNextButtonEnabled() { - return isEnabled(nextBtn); + return isEnabled(nextBtn); } @Step diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java index 0f25489128..538f714e83 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java @@ -54,7 +54,17 @@ public class TopicsList extends BasePage { @Step public TopicsList setShowInternalRadioButton(boolean select) { - selectElement(showInternalRadioBtn, select); + if (select) { + if (!showInternalRadioBtn.isSelected()) { + clickByJavaScript(showInternalRadioBtn); + waitUntilSpinnerDisappear(1); + } + } else { + if (showInternalRadioBtn.isSelected()) { + clickByJavaScript(showInternalRadioBtn); + waitUntilSpinnerDisappear(1); + } + } return this; } @@ -169,9 +179,16 @@ public class TopicsList extends BasePage { @Step public TopicGridItem getTopicItem(String name) { - return initGridItems().stream() + TopicGridItem topicGridItem = initGridItems().stream() .filter(e -> e.getName().equals(name)) - .findFirst().orElseThrow(); + .findFirst().orElse(null); + if (topicGridItem == null) { + searchItem(name); + topicGridItem = initGridItems().stream() + .filter(e -> e.getName().equals(name)) + .findFirst().orElseThrow(); + } + return topicGridItem; } @Step diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java index 315cf56d18..808f0fdbc8 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java @@ -29,27 +29,27 @@ public class ApiService extends BaseSource { @SneakyThrows private TopicsApi topicApi() { - return new TopicsApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + return new TopicsApi(new ApiClient().setBasePath(BASE_API_URL)); } @SneakyThrows private SchemasApi schemaApi() { - return new SchemasApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + return new SchemasApi(new ApiClient().setBasePath(BASE_API_URL)); } @SneakyThrows private KafkaConnectApi connectorApi() { - return new KafkaConnectApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + return new KafkaConnectApi(new ApiClient().setBasePath(BASE_API_URL)); } @SneakyThrows private MessagesApi messageApi() { - return new MessagesApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + return new MessagesApi(new ApiClient().setBasePath(BASE_API_URL)); } @SneakyThrows private KsqlApi ksqlApi() { - return new KsqlApi(new ApiClient().setBasePath(BASE_LOCAL_URL)); + return new KsqlApi(new ApiClient().setBasePath(BASE_API_URL)); } @SneakyThrows diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java index d294c5a116..cbb1bc2345 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java @@ -3,15 +3,22 @@ package com.provectus.kafka.ui.settings; import com.provectus.kafka.ui.settings.configs.Config; import org.aeonbits.owner.ConfigFactory; +import static com.provectus.kafka.ui.variables.Browser.LOCAL; + public abstract class BaseSource { - public static final String BASE_CONTAINER_URL = "http://host.testcontainers.internal:8080"; - public static final String BASE_LOCAL_URL = "http://localhost:8080"; public static final String CLUSTER_NAME = "local"; public static final String CONNECT_NAME = "first"; + private static final String LOCAL_HOST = "localhost"; private static Config config; public static final String BROWSER = config().browser(); public static final String SUITE_NAME = config().suite(); + public static final String BASE_HOST = BROWSER.equals(LOCAL) + ? LOCAL_HOST + : "host.docker.internal"; + public static final String REMOTE_URL = String.format("http://%s:4444/wd/hub", LOCAL_HOST); + public static final String BASE_API_URL = String.format("http://%s:8080", LOCAL_HOST); + public static final String BASE_UI_URL = String.format("http://%s:8080", BASE_HOST); private static Config config() { if (config == null) { diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/LocalWebDriver.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/LocalWebDriver.java deleted file mode 100644 index e96213dd06..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/LocalWebDriver.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.provectus.kafka.ui.settings.drivers; - -import static com.codeborne.selenide.Selenide.clearBrowserCookies; -import static com.codeborne.selenide.Selenide.clearBrowserLocalStorage; -import static com.codeborne.selenide.Selenide.open; -import static com.codeborne.selenide.Selenide.refresh; - -import com.codeborne.selenide.Configuration; -import com.codeborne.selenide.WebDriverRunner; -import com.codeborne.selenide.logevents.SelenideLogger; -import io.qameta.allure.Step; -import io.qameta.allure.selenide.AllureSelenide; -import org.openqa.selenium.chrome.ChromeOptions; - -public abstract class LocalWebDriver { - - private static org.openqa.selenium.WebDriver getWebDriver() { - try { - return WebDriverRunner.getWebDriver(); - } catch (IllegalStateException ex) { - Configuration.headless = false; - Configuration.browser = "chrome"; - Configuration.browserSize = "1920x1080"; - /**screenshots and savePageSource config is needed for local debug - * optionally can be set as 'false' to not duplicate Allure report - */ - Configuration.screenshots = true; - Configuration.savePageSource = true; - Configuration.pageLoadTimeout = 120000; - Configuration.browserCapabilities = new ChromeOptions() - .addArguments("--remote-allow-origins=*") - .addArguments("--lang=en_US"); - open(); - return WebDriverRunner.getWebDriver(); - } - } - - @Step - public static void openUrl(String url) { - if (!getWebDriver().getCurrentUrl().equals(url)) { - getWebDriver().get(url); - } - } - - @Step - public static void browserInit() { - getWebDriver(); - } - - @Step - public static void browserClear() { - clearBrowserLocalStorage(); - clearBrowserCookies(); - refresh(); - } - - @Step - public static void browserQuit() { - getWebDriver().quit(); - } - - @Step - public static void loggerSetup() { - SelenideLogger.addListener("AllureSelenide", new AllureSelenide() - .screenshots(true) - .savePageSource(false)); - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java new file mode 100644 index 0000000000..2b210e91e1 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java @@ -0,0 +1,96 @@ +package com.provectus.kafka.ui.settings.drivers; + +import com.codeborne.selenide.Configuration; +import com.codeborne.selenide.Selenide; +import com.codeborne.selenide.WebDriverRunner; +import com.codeborne.selenide.logevents.SelenideLogger; +import io.qameta.allure.Step; +import io.qameta.allure.selenide.AllureSelenide; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.remote.DesiredCapabilities; + +import static com.codeborne.selenide.Selenide.*; +import static com.provectus.kafka.ui.settings.BaseSource.BROWSER; +import static com.provectus.kafka.ui.settings.BaseSource.REMOTE_URL; +import static com.provectus.kafka.ui.variables.Browser.CONTAINER; +import static com.provectus.kafka.ui.variables.Browser.LOCAL; + +public abstract class WebDriver { + + @Step + public static void browserSetup() { + Configuration.headless = false; + Configuration.browser = "chrome"; + Configuration.browserSize = "1920x1080"; + /**screenshots and savePageSource config is needed for local debug + * optionally can be set as 'false' to not duplicate Allure report + */ + Configuration.screenshots = true; + Configuration.savePageSource = false; + Configuration.pageLoadTimeout = 120000; + ChromeOptions options = new ChromeOptions() + .addArguments("--no-sandbox") + .addArguments("--verbose") + .addArguments("--remote-allow-origins=*") + .addArguments("--disable-dev-shm-usage") + .addArguments("--disable-gpu") + .addArguments("--lang=en_US"); + switch (BROWSER) { + case (LOCAL) -> Configuration.browserCapabilities = options; + case (CONTAINER) -> { + Configuration.remote = REMOTE_URL; + Configuration.remoteConnectionTimeout = 180000; + DesiredCapabilities capabilities = new DesiredCapabilities(); + capabilities.setCapability("enableVNC", true); + capabilities.setCapability("enableVideo", false); + Configuration.browserCapabilities = capabilities.merge(options); + } + default -> throw new IllegalStateException("Unexpected value: " + BROWSER); + } + } + + private static org.openqa.selenium.WebDriver getWebDriver() { + try { + return WebDriverRunner.getWebDriver(); + } catch (IllegalStateException ex) { + browserSetup(); + Selenide.open(); + return WebDriverRunner.getWebDriver(); + } + } + + @Step + public static void openUrl(String url) { + org.openqa.selenium.WebDriver driver = getWebDriver(); + if (!driver.getCurrentUrl().equals(url)) driver.get(url); + } + + @Step + public static void browserInit() { + getWebDriver(); + } + + @Step + public static void browserClear() { + clearBrowserLocalStorage(); + clearBrowserCookies(); + refresh(); + } + + @Step + public static void browserQuit() { + org.openqa.selenium.WebDriver driver = null; + try { + driver = WebDriverRunner.getWebDriver(); + } catch (Throwable ignored) { + } + if (driver != null) driver.quit(); + } + + @Step + public static void loggerSetup() { + SelenideLogger.addListener("AllureSelenide", new AllureSelenide() + .screenshots(true) + .savePageSource(false)); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java index 8358880503..427662d9d7 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java @@ -7,11 +7,23 @@ import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.Keys; import org.openqa.selenium.interactions.Actions; +import java.time.Duration; + import static com.codeborne.selenide.Selenide.executeJavaScript; @Slf4j public class WebUtils { + public static int getTimeout(int... timeoutInSeconds) { + return (timeoutInSeconds != null && timeoutInSeconds.length > 0) ? timeoutInSeconds[0] : 4; + } + + public static void sendKeysAfterClear(SelenideElement element, String keys) { + log.debug("\nsendKeysAfterClear: {} \nsend keys '{}'", element.getSearchCriteria(), keys); + element.shouldBe(Condition.enabled).clear(); + if (keys != null) element.sendKeys(keys); + } + public static void clickByActions(SelenideElement element) { log.debug("\nclickByActions: {}", element.getSearchCriteria()); element.shouldBe(Condition.enabled); @@ -43,11 +55,12 @@ public class WebUtils { field.sendKeys(Keys.chord(Keys.CONTROL + "a"), Keys.DELETE); } - public static boolean isVisible(SelenideElement element) { + public static boolean isVisible(SelenideElement element, int... timeoutInSeconds) { log.debug("\nisVisible: {}", element.getSearchCriteria()); boolean isVisible = false; try { - element.shouldBe(Condition.visible); + element.shouldBe(Condition.visible, + Duration.ofSeconds(getTimeout(timeoutInSeconds))); isVisible = true; } catch (Throwable e) { log.debug("{} is not visible", element.getSearchCriteria()); @@ -55,11 +68,12 @@ public class WebUtils { return isVisible; } - public static boolean isEnabled(SelenideElement element) { + public static boolean isEnabled(SelenideElement element, int... timeoutInSeconds) { log.debug("\nisEnabled: {}", element.getSearchCriteria()); boolean isEnabled = false; try { - element.shouldBe(Condition.enabled); + element.shouldBe(Condition.enabled, + Duration.ofSeconds(getTimeout(timeoutInSeconds))); isEnabled = true; } catch (Throwable e) { log.debug("{} is not enabled", element.getSearchCriteria()); @@ -67,11 +81,12 @@ public class WebUtils { return isEnabled; } - public static boolean isSelected(SelenideElement element) { + public static boolean isSelected(SelenideElement element, int... timeoutInSeconds) { log.debug("\nisSelected: {}", element.getSearchCriteria()); boolean isSelected = false; try { - element.shouldBe(Condition.selected); + element.shouldBe(Condition.selected, + Duration.ofSeconds(getTimeout(timeoutInSeconds))); isSelected = true; } catch (Throwable e) { log.debug("{} is not selected", element.getSearchCriteria()); diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java index f612d743a5..8800463bdc 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java @@ -1,9 +1,9 @@ package com.provectus.kafka.ui.variables; public interface Url { - + String BROKERS_LIST_URL = "http://%s:8080/ui/clusters/local/brokers"; - String TOPICS_LIST_URL = "http://%s:8080/ui/clusters/local/all-topics?perPage=25"; + String TOPICS_LIST_URL = "http://%s:8080/ui/clusters/local/all-topics"; String CONSUMERS_LIST_URL = "http://%s:8080/ui/clusters/local/consumer-groups"; String SCHEMA_REGISTRY_LIST_URL = "http://%s:8080/ui/clusters/local/schemas"; String KAFKA_CONNECT_LIST_URL = "http://%s:8080/ui/clusters/local/connectors"; diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java index dfb4230001..424c699f04 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java @@ -3,104 +3,40 @@ package com.provectus.kafka.ui; import com.codeborne.selenide.Condition; import com.codeborne.selenide.Selenide; import com.codeborne.selenide.SelenideElement; -import com.codeborne.selenide.WebDriverRunner; import com.provectus.kafka.ui.settings.listeners.AllureListener; import com.provectus.kafka.ui.settings.listeners.LoggerListener; import com.provectus.kafka.ui.settings.listeners.QaseResultListener; import io.qameta.allure.Step; import lombok.extern.slf4j.Slf4j; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.chrome.ChromeOptions; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.testcontainers.Testcontainers; -import org.testcontainers.containers.BrowserWebDriverContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.utility.DockerImageName; import org.testng.annotations.*; import org.testng.asserts.SoftAssert; -import java.time.Duration; import java.util.List; import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.*; -import static com.provectus.kafka.ui.settings.BaseSource.*; -import static com.provectus.kafka.ui.settings.drivers.LocalWebDriver.*; +import static com.provectus.kafka.ui.settings.BaseSource.BASE_UI_URL; +import static com.provectus.kafka.ui.settings.drivers.WebDriver.*; import static com.provectus.kafka.ui.utilities.qaseUtils.QaseSetup.qaseIntegrationSetup; -import static com.provectus.kafka.ui.variables.Browser.CONTAINER; -import static com.provectus.kafka.ui.variables.Browser.LOCAL; @Slf4j @Listeners({AllureListener.class, LoggerListener.class, QaseResultListener.class}) public abstract class BaseTest extends Facade { - private static final String SELENIUM_IMAGE_NAME = "selenium/standalone-chrome:103.0"; - private static final String SELENIARM_STANDALONE_CHROMIUM = "seleniarm/standalone-chromium:103.0"; - protected static BrowserWebDriverContainer webDriverContainer = null; - - private static boolean isARM64() { - return System.getProperty("os.arch").equals("aarch64"); - } - @BeforeSuite(alwaysRun = true) public void beforeSuite() { qaseIntegrationSetup(); - switch (BROWSER) { - case (CONTAINER) -> { - DockerImageName image = isARM64() - ? DockerImageName.parse(SELENIARM_STANDALONE_CHROMIUM).asCompatibleSubstituteFor(SELENIUM_IMAGE_NAME) - : DockerImageName.parse(SELENIUM_IMAGE_NAME); - log.info("Using [{}] as image name for chrome", image.getUnversionedPart()); - webDriverContainer = new BrowserWebDriverContainer<>(image) - .withEnv("JAVA_OPTS", "-Dwebdriver.chrome.whitelistedIps=") - .withStartupTimeout(Duration.ofSeconds(180)) - .withCapabilities(new ChromeOptions() - .addArguments("--disable-dev-shm-usage") - .addArguments("--disable-gpu") - .addArguments("--no-sandbox") - .addArguments("--verbose") - .addArguments("--lang=es") - ) - .withLogConsumer(new Slf4jLogConsumer(log).withPrefix("[CHROME]: ")); - try { - Testcontainers.exposeHostPorts(8080); - log.info("Starting browser container"); - webDriverContainer.start(); - } catch (Throwable e) { - log.error("Couldn't start a container", e); - } - } - case (LOCAL) -> loggerSetup(); - default -> throw new IllegalStateException("Unexpected value: " + BROWSER); - } + loggerSetup(); + browserSetup(); } @AfterSuite(alwaysRun = true) public void afterSuite() { - switch (BROWSER) { - case (CONTAINER) -> { - if (webDriverContainer.isRunning()) { - webDriverContainer.close(); - webDriverContainer.stop(); - } - } - case (LOCAL) -> browserQuit(); - default -> throw new IllegalStateException("Unexpected value: " + BROWSER); - } + browserQuit(); } @BeforeMethod(alwaysRun = true) public void beforeMethod() { - switch (BROWSER) { - case (CONTAINER) -> { - RemoteWebDriver remoteWebDriver = webDriverContainer.getWebDriver(); - WebDriverRunner.setWebDriver(remoteWebDriver); - remoteWebDriver.manage() - .window().setSize(new Dimension(1440, 1024)); - Selenide.open(BASE_CONTAINER_URL); - } - case (LOCAL) -> openUrl(BASE_LOCAL_URL); - default -> throw new IllegalStateException("Unexpected value: " + BROWSER); - } + Selenide.open(BASE_UI_URL); naviSideBar.waitUntilScreenReady(); } @@ -133,15 +69,14 @@ public abstract class BaseTest extends Facade { naviSideBar .openSideMenu(TOPICS); topicsList - .waitUntilScreenReady(); + .waitUntilScreenReady() + .setShowInternalRadioButton(false); } @Step protected void navigateToTopicsAndOpenDetails(String topicName) { - naviSideBar - .openSideMenu(TOPICS); + navigateToTopics(); topicsList - .waitUntilScreenReady() .openTopic(topicName); topicDetails .waitUntilScreenReady(); diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java index aa48cde2fc..c8439e70dd 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java @@ -3,10 +3,10 @@ package com.provectus.kafka.ui.smokeSuite; import com.codeborne.selenide.Condition; import com.codeborne.selenide.WebDriverRunner; import com.provectus.kafka.ui.BaseTest; -import com.provectus.kafka.ui.pages.panels.enums.MenuItem; import com.provectus.kafka.ui.models.Connector; import com.provectus.kafka.ui.models.Schema; import com.provectus.kafka.ui.models.Topic; +import com.provectus.kafka.ui.pages.panels.enums.MenuItem; import io.qameta.allure.Step; import io.qase.api.annotation.QaseId; import org.testng.Assert; @@ -18,9 +18,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.*; -import static com.provectus.kafka.ui.settings.BaseSource.BROWSER; +import static com.provectus.kafka.ui.settings.BaseSource.BASE_HOST; import static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString; -import static com.provectus.kafka.ui.variables.Browser.LOCAL; import static com.provectus.kafka.ui.variables.Url.*; import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; @@ -73,7 +72,7 @@ public class SmokeTest extends BaseTest { @QaseId(46) @Test - public void checkComponentsPathWhileNavigating() { + public void checkPathWhileNavigating() { navigateToBrokersAndOpenDetails(BROKER_ID); verifyComponentsPath(BROKERS, String.format("Broker %d", BROKER_ID)); navigateToTopicsAndOpenDetails(TEST_TOPIC.getName()); @@ -86,9 +85,10 @@ public class SmokeTest extends BaseTest { @Step private void verifyCurrentUrl(String expectedUrl) { - String host = BROWSER.equals(LOCAL) ? "localhost" : "host.testcontainers.internal"; - Assert.assertEquals(WebDriverRunner.getWebDriver().getCurrentUrl(), - String.format(expectedUrl, host), "getCurrentUrl()"); + String urlWithoutParameters = WebDriverRunner.getWebDriver().getCurrentUrl(); + if (urlWithoutParameters.contains("?")) + urlWithoutParameters = urlWithoutParameters.substring(0, urlWithoutParameters.indexOf("?")); + Assert.assertEquals(urlWithoutParameters, String.format(expectedUrl, BASE_HOST), "getCurrentUrl()"); } @Step diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java index ab1705922a..210e5549f4 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java @@ -4,10 +4,14 @@ import com.provectus.kafka.ui.BaseTest; import com.provectus.kafka.ui.pages.ksqlDb.models.Stream; import com.provectus.kafka.ui.pages.ksqlDb.models.Table; import io.qase.api.annotation.QaseId; +import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.testng.asserts.SoftAssert; +import java.util.ArrayList; +import java.util.List; + import static com.provectus.kafka.ui.pages.ksqlDb.enums.KsqlQueryConfig.SHOW_TABLES; import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; @@ -22,12 +26,15 @@ public class KsqlDbTest extends BaseTest { private static final Table SECOND_TABLE = new Table() .setName("SECOND_TABLE" + randomAlphabetic(4).toUpperCase()) .setStreamName(STREAM_FOR_CHECK_TABLES.getName()); + private static final List TOPIC_NAMES_LIST = new ArrayList<>(); @BeforeClass(alwaysRun = true) public void beforeClass() { apiService .createStream(STREAM_FOR_CHECK_TABLES) .createTables(FIRST_TABLE, SECOND_TABLE); + TOPIC_NAMES_LIST.addAll(List.of(STREAM_FOR_CHECK_TABLES.getTopicName(), + FIRST_TABLE.getName(), SECOND_TABLE.getName())); } @QaseId(41) @@ -65,4 +72,9 @@ public class KsqlDbTest extends BaseTest { softly.assertFalse(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); softly.assertAll(); } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName)); + } } diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java index 8cca35beb6..0ce4d6c718 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java @@ -3,7 +3,6 @@ package com.provectus.kafka.ui.smokeSuite.topics; import com.codeborne.selenide.Condition; import com.provectus.kafka.ui.BaseTest; import com.provectus.kafka.ui.models.Topic; -import com.provectus.kafka.ui.pages.topics.TopicDetails; import io.qameta.allure.Issue; import io.qase.api.annotation.QaseId; import org.testng.Assert; @@ -17,8 +16,7 @@ import java.util.ArrayList; import java.util.List; import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; -import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES; -import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.SETTINGS; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.*; import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.COMPACT; import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.DELETE; import static com.provectus.kafka.ui.pages.topics.enums.CustomParameterType.COMPRESSION_TYPE; @@ -206,7 +204,7 @@ public class TopicsTest extends BaseTest { String consumerGroupId = "connect-sink_postgres_activities"; navigateToTopicsAndOpenDetails(topicName); topicDetails - .openDetailsTab(TopicDetails.TopicMenu.CONSUMERS) + .openDetailsTab(CONSUMERS) .openConsumerGroup(consumerGroupId); consumersDetails .waitUntilScreenReady(); @@ -361,8 +359,9 @@ public class TopicsTest extends BaseTest { @Test(priority = 15) public void checkShowInternalTopicsButton() { navigateToTopics(); + topicsList + .setShowInternalRadioButton(true); SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicsList.isShowInternalRadioBtnSelected(), "isInternalRadioBtnSelected()"); softly.assertTrue(topicsList.getInternalTopics().size() > 0, "getInternalTopics()"); softly.assertTrue(topicsList.getNonInternalTopics().size() > 0, "getNonInternalTopics()"); softly.assertAll(); diff --git a/kafka-ui-e2e-checks/src/test/resources/manual.xml b/kafka-ui-e2e-checks/src/test/resources/manual.xml index dff467651e..89dbd7e172 100644 --- a/kafka-ui-e2e-checks/src/test/resources/manual.xml +++ b/kafka-ui-e2e-checks/src/test/resources/manual.xml @@ -1,6 +1,6 @@ - + diff --git a/kafka-ui-e2e-checks/src/test/resources/qase.xml b/kafka-ui-e2e-checks/src/test/resources/qase.xml index 2b5d023b1a..8dd1dd1527 100644 --- a/kafka-ui-e2e-checks/src/test/resources/qase.xml +++ b/kafka-ui-e2e-checks/src/test/resources/qase.xml @@ -1,6 +1,6 @@ - + diff --git a/kafka-ui-e2e-checks/src/test/resources/regression.xml b/kafka-ui-e2e-checks/src/test/resources/regression.xml index 01db95d03b..6cc4c5bd49 100644 --- a/kafka-ui-e2e-checks/src/test/resources/regression.xml +++ b/kafka-ui-e2e-checks/src/test/resources/regression.xml @@ -1,6 +1,6 @@ - + diff --git a/kafka-ui-e2e-checks/src/test/resources/sanity.xml b/kafka-ui-e2e-checks/src/test/resources/sanity.xml index e1c8e8a31f..01aa279c0f 100644 --- a/kafka-ui-e2e-checks/src/test/resources/sanity.xml +++ b/kafka-ui-e2e-checks/src/test/resources/sanity.xml @@ -1,6 +1,6 @@ - + diff --git a/kafka-ui-e2e-checks/src/test/resources/smoke.xml b/kafka-ui-e2e-checks/src/test/resources/smoke.xml index e0a8f082e8..b370468e4f 100644 --- a/kafka-ui-e2e-checks/src/test/resources/smoke.xml +++ b/kafka-ui-e2e-checks/src/test/resources/smoke.xml @@ -1,6 +1,6 @@ - + From e31580a16ab8db762d2f7d1152101ae36404dec1 Mon Sep 17 00:00:00 2001 From: Ilya Kuramshin Date: Thu, 6 Apr 2023 18:11:39 +0400 Subject: [PATCH 06/17] "Done" event sending fix (#3631) * Done message sending fix: Making filtering and limiting a part of emitter to make it possible to gracefully finish emitting --- .../kafka/ui/emitter/AbstractEmitter.java | 44 ++++------ .../ui/emitter/BackwardRecordEmitter.java | 10 +-- .../kafka/ui/emitter/ConsumingStats.java | 8 +- .../ui/emitter/ForwardRecordEmitter.java | 12 +-- .../kafka/ui/emitter/MessageFilterStats.java | 16 ---- .../kafka/ui/emitter/MessagesProcessing.java | 82 +++++++++++++++++++ .../kafka/ui/emitter/TailingEmitter.java | 5 +- .../kafka/ui/service/MessagesService.java | 57 ++++--------- .../kafka/ui/service/RecordEmitterTest.java | 25 +++--- 9 files changed, 143 insertions(+), 116 deletions(-) delete mode 100644 kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilterStats.java create mode 100644 kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessagesProcessing.java diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/AbstractEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/AbstractEmitter.java index 646cf81ca6..9ea0526bac 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/AbstractEmitter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/AbstractEmitter.java @@ -1,9 +1,6 @@ package com.provectus.kafka.ui.emitter; -import com.provectus.kafka.ui.model.TopicMessageDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.model.TopicMessagePhaseDTO; -import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; import java.time.Duration; import java.time.Instant; import org.apache.kafka.clients.consumer.Consumer; @@ -14,13 +11,12 @@ import reactor.core.publisher.FluxSink; public abstract class AbstractEmitter { - private final ConsumerRecordDeserializer recordDeserializer; - private final ConsumingStats consumingStats = new ConsumingStats(); + private final MessagesProcessing messagesProcessing; private final PollingThrottler throttler; protected final PollingSettings pollingSettings; - protected AbstractEmitter(ConsumerRecordDeserializer recordDeserializer, PollingSettings pollingSettings) { - this.recordDeserializer = recordDeserializer; + protected AbstractEmitter(MessagesProcessing messagesProcessing, PollingSettings pollingSettings) { + this.messagesProcessing = messagesProcessing; this.pollingSettings = pollingSettings; this.throttler = pollingSettings.getPollingThrottler(); } @@ -40,39 +36,27 @@ public abstract class AbstractEmitter { return records; } + protected boolean sendLimitReached() { + return messagesProcessing.limitReached(); + } + protected void sendMessage(FluxSink sink, - ConsumerRecord msg) { - final TopicMessageDTO topicMessage = recordDeserializer.deserialize(msg); - sink.next( - new TopicMessageEventDTO() - .type(TopicMessageEventDTO.TypeEnum.MESSAGE) - .message(topicMessage) - ); + ConsumerRecord msg) { + messagesProcessing.sendMsg(sink, msg); } protected void sendPhase(FluxSink sink, String name) { - sink.next( - new TopicMessageEventDTO() - .type(TopicMessageEventDTO.TypeEnum.PHASE) - .phase(new TopicMessagePhaseDTO().name(name)) - ); + messagesProcessing.sendPhase(sink, name); } protected int sendConsuming(FluxSink sink, - ConsumerRecords records, - long elapsed) { - return consumingStats.sendConsumingEvt(sink, records, elapsed, getFilterApplyErrors(sink)); + ConsumerRecords records, + long elapsed) { + return messagesProcessing.sentConsumingInfo(sink, records, elapsed); } protected void sendFinishStatsAndCompleteSink(FluxSink sink) { - consumingStats.sendFinishEvent(sink, getFilterApplyErrors(sink)); + messagesProcessing.sendFinishEvent(sink); sink.complete(); } - - protected Number getFilterApplyErrors(FluxSink sink) { - return sink.contextView() - .getOrEmpty(MessageFilterStats.class) - .map(MessageFilterStats::getFilterApplyErrors) - .orElse(0); - } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardRecordEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardRecordEmitter.java index 42f94a1e01..ccd24e8568 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardRecordEmitter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardRecordEmitter.java @@ -2,7 +2,6 @@ package com.provectus.kafka.ui.emitter; import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -31,9 +30,9 @@ public class BackwardRecordEmitter Supplier> consumerSupplier, ConsumerPosition consumerPosition, int messagesPerPage, - ConsumerRecordDeserializer recordDeserializer, + MessagesProcessing messagesProcessing, PollingSettings pollingSettings) { - super(recordDeserializer, pollingSettings); + super(messagesProcessing, pollingSettings); this.consumerPosition = consumerPosition; this.messagesPerPage = messagesPerPage; this.consumerSupplier = consumerSupplier; @@ -52,7 +51,7 @@ public class BackwardRecordEmitter int msgsToPollPerPartition = (int) Math.ceil((double) messagesPerPage / readUntilOffsets.size()); log.debug("'Until' offsets for polling: {}", readUntilOffsets); - while (!sink.isCancelled() && !readUntilOffsets.isEmpty()) { + while (!sink.isCancelled() && !readUntilOffsets.isEmpty() && !sendLimitReached()) { new TreeMap<>(readUntilOffsets).forEach((tp, readToOffset) -> { if (sink.isCancelled()) { return; //fast return in case of sink cancellation @@ -61,8 +60,6 @@ public class BackwardRecordEmitter long readFromOffset = Math.max(beginOffset, readToOffset - msgsToPollPerPartition); partitionPollIteration(tp, readFromOffset, readToOffset, consumer, sink) - .stream() - .filter(r -> !sink.isCancelled()) .forEach(r -> sendMessage(sink, r)); if (beginOffset == readFromOffset) { @@ -106,6 +103,7 @@ public class BackwardRecordEmitter EmptyPollsCounter emptyPolls = pollingSettings.createEmptyPollsCounter(); while (!sink.isCancelled() + && !sendLimitReached() && recordsToSend.size() < desiredMsgsToPoll && !emptyPolls.noDataEmptyPollsReached()) { var polledRecords = poll(sink, consumer, pollingSettings.getPartitionPollTimeout()); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ConsumingStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ConsumingStats.java index 68c1cff098..0e002f36a4 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ConsumingStats.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ConsumingStats.java @@ -19,7 +19,7 @@ class ConsumingStats { int sendConsumingEvt(FluxSink sink, ConsumerRecords polledRecords, long elapsed, - Number filterApplyErrors) { + int filterApplyErrors) { int polledBytes = ConsumerRecordsUtil.calculatePolledSize(polledRecords); bytes += polledBytes; this.records += polledRecords.count(); @@ -32,7 +32,7 @@ class ConsumingStats { return polledBytes; } - void sendFinishEvent(FluxSink sink, Number filterApplyErrors) { + void sendFinishEvent(FluxSink sink, int filterApplyErrors) { sink.next( new TopicMessageEventDTO() .type(TopicMessageEventDTO.TypeEnum.DONE) @@ -41,12 +41,12 @@ class ConsumingStats { } private TopicMessageConsumingDTO createConsumingStats(FluxSink sink, - Number filterApplyErrors) { + int filterApplyErrors) { return new TopicMessageConsumingDTO() .bytesConsumed(this.bytes) .elapsedMs(this.elapsed) .isCancelled(sink.isCancelled()) - .filterApplyErrors(filterApplyErrors.intValue()) + .filterApplyErrors(filterApplyErrors) .messagesConsumed(this.records); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardRecordEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardRecordEmitter.java index 971e2f7c9c..8f85e0a8ba 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardRecordEmitter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardRecordEmitter.java @@ -2,7 +2,6 @@ package com.provectus.kafka.ui.emitter; import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; import java.util.function.Supplier; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -23,9 +22,9 @@ public class ForwardRecordEmitter public ForwardRecordEmitter( Supplier> consumerSupplier, ConsumerPosition position, - ConsumerRecordDeserializer recordDeserializer, + MessagesProcessing messagesProcessing, PollingSettings pollingSettings) { - super(recordDeserializer, pollingSettings); + super(messagesProcessing, pollingSettings); this.position = position; this.consumerSupplier = consumerSupplier; } @@ -40,6 +39,7 @@ public class ForwardRecordEmitter EmptyPollsCounter emptyPolls = pollingSettings.createEmptyPollsCounter(); while (!sink.isCancelled() + && !sendLimitReached() && !seekOperations.assignedPartitionsFullyPolled() && !emptyPolls.noDataEmptyPollsReached()) { @@ -50,11 +50,7 @@ public class ForwardRecordEmitter log.debug("{} records polled", records.count()); for (ConsumerRecord msg : records) { - if (!sink.isCancelled()) { - sendMessage(sink, msg); - } else { - break; - } + sendMessage(sink, msg); } } sendFinishStatsAndCompleteSink(sink); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilterStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilterStats.java deleted file mode 100644 index 3b6df3cdea..0000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilterStats.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.provectus.kafka.ui.emitter; - -import java.util.concurrent.atomic.AtomicLong; -import lombok.AccessLevel; -import lombok.Getter; - -public class MessageFilterStats { - - @Getter(AccessLevel.PACKAGE) - private final AtomicLong filterApplyErrors = new AtomicLong(); - - public final void incrementApplyErrors() { - filterApplyErrors.incrementAndGet(); - } - -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessagesProcessing.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessagesProcessing.java new file mode 100644 index 0000000000..b6d23bc90d --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessagesProcessing.java @@ -0,0 +1,82 @@ +package com.provectus.kafka.ui.emitter; + +import com.provectus.kafka.ui.model.TopicMessageDTO; +import com.provectus.kafka.ui.model.TopicMessageEventDTO; +import com.provectus.kafka.ui.model.TopicMessagePhaseDTO; +import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; +import java.util.function.Predicate; +import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.common.utils.Bytes; +import reactor.core.publisher.FluxSink; + +@Slf4j +public class MessagesProcessing { + + private final ConsumingStats consumingStats = new ConsumingStats(); + private long sentMessages = 0; + private int filterApplyErrors = 0; + + private final ConsumerRecordDeserializer deserializer; + private final Predicate filter; + private final @Nullable Integer limit; + + public MessagesProcessing(ConsumerRecordDeserializer deserializer, + Predicate filter, + @Nullable Integer limit) { + this.deserializer = deserializer; + this.filter = filter; + this.limit = limit; + } + + boolean limitReached() { + return limit != null && sentMessages >= limit; + } + + void sendMsg(FluxSink sink, ConsumerRecord rec) { + if (!sink.isCancelled() && !limitReached()) { + TopicMessageDTO topicMessage = deserializer.deserialize(rec); + try { + if (filter.test(topicMessage)) { + sink.next( + new TopicMessageEventDTO() + .type(TopicMessageEventDTO.TypeEnum.MESSAGE) + .message(topicMessage) + ); + sentMessages++; + } + } catch (Exception e) { + filterApplyErrors++; + log.trace("Error applying filter for message {}", topicMessage); + } + } + } + + int sentConsumingInfo(FluxSink sink, + ConsumerRecords polledRecords, + long elapsed) { + if (!sink.isCancelled()) { + return consumingStats.sendConsumingEvt(sink, polledRecords, elapsed, filterApplyErrors); + } + return 0; + } + + void sendFinishEvent(FluxSink sink) { + if (!sink.isCancelled()) { + consumingStats.sendFinishEvent(sink, filterApplyErrors); + } + } + + void sendPhase(FluxSink sink, String name) { + if (!sink.isCancelled()) { + sink.next( + new TopicMessageEventDTO() + .type(TopicMessageEventDTO.TypeEnum.PHASE) + .phase(new TopicMessagePhaseDTO().name(name)) + ); + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/TailingEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/TailingEmitter.java index 4554069c1c..b17d69a09b 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/TailingEmitter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/TailingEmitter.java @@ -2,7 +2,6 @@ package com.provectus.kafka.ui.emitter; import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; import java.util.HashMap; import java.util.function.Supplier; import lombok.extern.slf4j.Slf4j; @@ -20,9 +19,9 @@ public class TailingEmitter extends AbstractEmitter public TailingEmitter(Supplier> consumerSupplier, ConsumerPosition consumerPosition, - ConsumerRecordDeserializer recordDeserializer, + MessagesProcessing messagesProcessing, PollingSettings pollingSettings) { - super(recordDeserializer, pollingSettings); + super(messagesProcessing, pollingSettings); this.consumerSupplier = consumerSupplier; this.consumerPosition = consumerPosition; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java index 27f751ac80..4f9f0f59f4 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java @@ -3,9 +3,8 @@ package com.provectus.kafka.ui.service; import com.google.common.util.concurrent.RateLimiter; import com.provectus.kafka.ui.emitter.BackwardRecordEmitter; import com.provectus.kafka.ui.emitter.ForwardRecordEmitter; -import com.provectus.kafka.ui.emitter.MessageFilterStats; import com.provectus.kafka.ui.emitter.MessageFilters; -import com.provectus.kafka.ui.emitter.ResultSizeLimiter; +import com.provectus.kafka.ui.emitter.MessagesProcessing; import com.provectus.kafka.ui.emitter.TailingEmitter; import com.provectus.kafka.ui.exception.TopicNotFoundException; import com.provectus.kafka.ui.exception.ValidationException; @@ -14,9 +13,9 @@ import com.provectus.kafka.ui.model.CreateTopicMessageDTO; import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.MessageFilterTypeDTO; import com.provectus.kafka.ui.model.SeekDirectionDTO; +import com.provectus.kafka.ui.model.TopicMessageDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; import com.provectus.kafka.ui.serde.api.Serde; -import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; import com.provectus.kafka.ui.serdes.ProducerRecordCreator; import com.provectus.kafka.ui.util.SslPropertiesUtil; import java.util.List; @@ -162,13 +161,18 @@ public class MessagesService { @Nullable String valueSerde) { java.util.function.Consumer> emitter; - ConsumerRecordDeserializer recordDeserializer = - deserializationService.deserializerFor(cluster, topic, keySerde, valueSerde); + + var processing = new MessagesProcessing( + deserializationService.deserializerFor(cluster, topic, keySerde, valueSerde), + getMsgFilter(query, filterQueryType), + seekDirection == SeekDirectionDTO.TAILING ? null : limit + ); + if (seekDirection.equals(SeekDirectionDTO.FORWARD)) { emitter = new ForwardRecordEmitter( () -> consumerGroupService.createConsumer(cluster), consumerPosition, - recordDeserializer, + processing, cluster.getPollingSettings() ); } else if (seekDirection.equals(SeekDirectionDTO.BACKWARD)) { @@ -176,33 +180,22 @@ public class MessagesService { () -> consumerGroupService.createConsumer(cluster), consumerPosition, limit, - recordDeserializer, + processing, cluster.getPollingSettings() ); } else { emitter = new TailingEmitter( () -> consumerGroupService.createConsumer(cluster), consumerPosition, - recordDeserializer, + processing, cluster.getPollingSettings() ); } - MessageFilterStats filterStats = new MessageFilterStats(); return Flux.create(emitter) - .contextWrite(ctx -> ctx.put(MessageFilterStats.class, filterStats)) - .filter(getMsgFilter(query, filterQueryType, filterStats)) .map(getDataMasker(cluster, topic)) - .takeWhile(createTakeWhilePredicate(seekDirection, limit)) .map(throttleUiPublish(seekDirection)); } - private Predicate createTakeWhilePredicate( - SeekDirectionDTO seekDirection, int limit) { - return seekDirection == SeekDirectionDTO.TAILING - ? evt -> true // no limit for tailing - : new ResultSizeLimiter(limit); - } - private UnaryOperator getDataMasker(KafkaCluster cluster, String topicName) { var keyMasker = cluster.getMasking().getMaskingFunction(topicName, Serde.Target.KEY); var valMasker = cluster.getMasking().getMaskingFunction(topicName, Serde.Target.VALUE); @@ -211,32 +204,18 @@ public class MessagesService { return evt; } return evt.message( - evt.getMessage() - .key(keyMasker.apply(evt.getMessage().getKey())) - .content(valMasker.apply(evt.getMessage().getContent()))); + evt.getMessage() + .key(keyMasker.apply(evt.getMessage().getKey())) + .content(valMasker.apply(evt.getMessage().getContent()))); }; } - private Predicate getMsgFilter(String query, - MessageFilterTypeDTO filterQueryType, - MessageFilterStats filterStats) { + private Predicate getMsgFilter(String query, + MessageFilterTypeDTO filterQueryType) { if (StringUtils.isEmpty(query)) { return evt -> true; } - var messageFilter = MessageFilters.createMsgFilter(query, filterQueryType); - return evt -> { - // we only apply filter for message events - if (evt.getType() == TopicMessageEventDTO.TypeEnum.MESSAGE) { - try { - return messageFilter.test(evt.getMessage()); - } catch (Exception e) { - filterStats.incrementApplyErrors(); - log.trace("Error applying filter '{}' for message {}", query, evt.getMessage()); - return false; - } - } - return true; - }; + return MessageFilters.createMsgFilter(query, filterQueryType); } private UnaryOperator throttleUiPublish(SeekDirectionDTO seekDirection) { diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/RecordEmitterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/RecordEmitterTest.java index e7b9edf834..239f3cf994 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/RecordEmitterTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/RecordEmitterTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.provectus.kafka.ui.AbstractIntegrationTest; import com.provectus.kafka.ui.emitter.BackwardRecordEmitter; import com.provectus.kafka.ui.emitter.ForwardRecordEmitter; +import com.provectus.kafka.ui.emitter.MessagesProcessing; import com.provectus.kafka.ui.emitter.PollingSettings; import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.TopicMessageEventDTO; @@ -106,12 +107,16 @@ class RecordEmitterTest extends AbstractIntegrationTest { ); } + private MessagesProcessing createMessagesProcessing() { + return new MessagesProcessing(RECORD_DESERIALIZER, msg -> true, null); + } + @Test void pollNothingOnEmptyTopic() { var forwardEmitter = new ForwardRecordEmitter( this::createConsumer, new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null), - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); @@ -119,7 +124,7 @@ class RecordEmitterTest extends AbstractIntegrationTest { this::createConsumer, new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null), 100, - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); @@ -141,7 +146,7 @@ class RecordEmitterTest extends AbstractIntegrationTest { var forwardEmitter = new ForwardRecordEmitter( this::createConsumer, new ConsumerPosition(BEGINNING, TOPIC, null), - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); @@ -149,7 +154,7 @@ class RecordEmitterTest extends AbstractIntegrationTest { this::createConsumer, new ConsumerPosition(LATEST, TOPIC, null), PARTITIONS * MSGS_PER_PARTITION, - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); @@ -170,7 +175,7 @@ class RecordEmitterTest extends AbstractIntegrationTest { var forwardEmitter = new ForwardRecordEmitter( this::createConsumer, new ConsumerPosition(OFFSET, TOPIC, targetOffsets), - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); @@ -178,7 +183,7 @@ class RecordEmitterTest extends AbstractIntegrationTest { this::createConsumer, new ConsumerPosition(OFFSET, TOPIC, targetOffsets), PARTITIONS * MSGS_PER_PARTITION, - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); @@ -215,7 +220,7 @@ class RecordEmitterTest extends AbstractIntegrationTest { var forwardEmitter = new ForwardRecordEmitter( this::createConsumer, new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps), - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); @@ -223,7 +228,7 @@ class RecordEmitterTest extends AbstractIntegrationTest { this::createConsumer, new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps), PARTITIONS * MSGS_PER_PARTITION, - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); @@ -254,7 +259,7 @@ class RecordEmitterTest extends AbstractIntegrationTest { this::createConsumer, new ConsumerPosition(OFFSET, TOPIC, targetOffsets), numMessages, - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); @@ -280,7 +285,7 @@ class RecordEmitterTest extends AbstractIntegrationTest { this::createConsumer, new ConsumerPosition(OFFSET, TOPIC, offsets), 100, - RECORD_DESERIALIZER, + createMessagesProcessing(), PollingSettings.createDefault() ); From ca225440d84744add5ce291ba792eccda2e2e627 Mon Sep 17 00:00:00 2001 From: David Bejanyan <58771979+David-DB88@users.noreply.github.com> Date: Thu, 6 Apr 2023 18:21:24 +0400 Subject: [PATCH 07/17] [FE] Get rid of "Topic analysis successfully started" message (#3628) --- kafka-ui-react-app/src/lib/hooks/api/topics.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/kafka-ui-react-app/src/lib/hooks/api/topics.ts b/kafka-ui-react-app/src/lib/hooks/api/topics.ts index 23e6d65ce3..f71299f19b 100644 --- a/kafka-ui-react-app/src/lib/hooks/api/topics.ts +++ b/kafka-ui-react-app/src/lib/hooks/api/topics.ts @@ -314,9 +314,6 @@ export function useAnalyzeTopic(props: GetTopicDetailsRequest) { const client = useQueryClient(); return useMutation(() => api.analyzeTopic(props), { onSuccess: () => { - showSuccessAlert({ - message: `Topic analysis successfully started`, - }); client.invalidateQueries(topicKeys.statistics(props)); }, }); From a3daa45ccb473ba77b7b5888bf0e38c32f7f65d4 Mon Sep 17 00:00:00 2001 From: David Bejanyan <58771979+David-DB88@users.noreply.github.com> Date: Thu, 6 Apr 2023 19:37:49 +0400 Subject: [PATCH 08/17] Display smart filters filtered stats #2680 (#3408) * added filterApplyErrors * resoled show errors and message event type --------- Co-authored-by: Oleg Shur Co-authored-by: Roman Zabaluev Co-authored-by: Ilya Kuramshin --- .../Topics/Topic/Messages/Filters/Filters.tsx | 18 +++++++++++++++++- .../Messages/Filters/FiltersContainer.ts | 4 ++++ .../Filters/__tests__/Filters.spec.tsx | 19 ++++++++++++++++++- .../src/redux/interfaces/topic.ts | 1 + .../redux/reducers/topicMessages/selectors.ts | 5 +++++ .../topicMessages/topicMessagesSlice.ts | 6 ++++++ 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx index f9fa3401fc..2941536f8b 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx @@ -53,11 +53,13 @@ export interface FiltersProps { phaseMessage?: string; meta: TopicMessageConsuming; isFetching: boolean; + messageEventType?: string; addMessage(content: { message: TopicMessage; prepend: boolean }): void; resetMessages(): void; updatePhase(phase: string): void; updateMeta(meta: TopicMessageConsuming): void; setIsFetching(status: boolean): void; + setMessageType(messageType: string): void; } export interface MessageFilters { @@ -80,13 +82,15 @@ export const SeekTypeOptions = [ const Filters: React.FC = ({ phaseMessage, - meta: { elapsedMs, bytesConsumed, messagesConsumed }, + meta: { elapsedMs, bytesConsumed, messagesConsumed, filterApplyErrors }, isFetching, addMessage, resetMessages, updatePhase, updateMeta, setIsFetching, + setMessageType, + messageEventType, }) => { const { clusterName, topicName } = useAppParams(); const location = useLocation(); @@ -355,6 +359,12 @@ const Filters: React.FC = ({ case TopicMessageEventTypeEnum.CONSUMING: if (consuming) updateMeta(consuming); break; + case TopicMessageEventTypeEnum.DONE: + if (consuming && type) { + setMessageType(type); + updateMeta(consuming); + } + break; default: } }; @@ -551,6 +561,7 @@ const Filters: React.FC = ({ {seekDirection !== SeekDirection.TAILING && isFetching && phaseMessage} + {!isFetching && messageEventType} @@ -582,6 +593,11 @@ const Filters: React.FC = ({ {messagesConsumed} messages consumed + {!!filterApplyErrors && ( + + {filterApplyErrors} errors + + )} ); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/FiltersContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/FiltersContainer.ts index 144672ee60..19dca0184b 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/FiltersContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/FiltersContainer.ts @@ -6,11 +6,13 @@ import { updateTopicMessagesMeta, updateTopicMessagesPhase, setTopicMessagesFetchingStatus, + setMessageEventType, } from 'redux/reducers/topicMessages/topicMessagesSlice'; import { getTopicMessgesMeta, getTopicMessgesPhase, getIsTopicMessagesFetching, + getIsTopicMessagesType, } from 'redux/reducers/topicMessages/selectors'; import Filters from './Filters'; @@ -19,6 +21,7 @@ const mapStateToProps = (state: RootState) => ({ phaseMessage: getTopicMessgesPhase(state), meta: getTopicMessgesMeta(state), isFetching: getIsTopicMessagesFetching(state), + messageEventType: getIsTopicMessagesType(state), }); const mapDispatchToProps = { @@ -27,6 +30,7 @@ const mapDispatchToProps = { updatePhase: updateTopicMessagesPhase, updateMeta: updateTopicMessagesMeta, setIsFetching: setTopicMessagesFetchingStatus, + setMessageType: setMessageEventType, }; export default connect(mapStateToProps, mapDispatchToProps)(Filters); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx index e248fdeaeb..3e75f11787 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx @@ -44,13 +44,17 @@ const renderComponent = ( @@ -228,4 +232,17 @@ describe('Filters component', () => { expect(anotherSmartFilterElement).not.toBeInTheDocument(); }); }); + + describe('show errors when get an filterApplyErrors and message event type', () => { + it('show errors', () => { + renderComponent(); + const errors = screen.getByText('10 errors'); + expect(errors).toBeInTheDocument(); + }); + it('message event type when fetching is false ', () => { + renderComponent(); + const messageType = screen.getByText('Done'); + expect(messageType).toBeInTheDocument(); + }); + }); }); diff --git a/kafka-ui-react-app/src/redux/interfaces/topic.ts b/kafka-ui-react-app/src/redux/interfaces/topic.ts index 9f667e135e..153002240a 100644 --- a/kafka-ui-react-app/src/redux/interfaces/topic.ts +++ b/kafka-ui-react-app/src/redux/interfaces/topic.ts @@ -56,5 +56,6 @@ export interface TopicMessagesState { messages: TopicMessage[]; phase?: string; meta: TopicMessageConsuming; + messageEventType?: string; isFetching: boolean; } diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/selectors.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/selectors.ts index 03adca8e42..b2636cdf2a 100644 --- a/kafka-ui-react-app/src/redux/reducers/topicMessages/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/selectors.ts @@ -23,3 +23,8 @@ export const getIsTopicMessagesFetching = createSelector( topicMessagesState, ({ isFetching }) => isFetching ); + +export const getIsTopicMessagesType = createSelector( + topicMessagesState, + ({ messageEventType }) => messageEventType +); diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts index 846cbd0c97..530a378114 100644 --- a/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts +++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts @@ -10,6 +10,7 @@ export const initialState: TopicMessagesState = { messagesConsumed: 0, isCancelled: false, }, + messageEventType: '', isFetching: false, }; @@ -37,6 +38,10 @@ const topicMessagesSlice = createSlice({ setTopicMessagesFetchingStatus: (state, action) => { state.isFetching = action.payload; }, + + setMessageEventType: (state, action) => { + state.messageEventType = action.payload; + }, }, }); @@ -46,6 +51,7 @@ export const { updateTopicMessagesPhase, updateTopicMessagesMeta, setTopicMessagesFetchingStatus, + setMessageEventType, } = topicMessagesSlice.actions; export default topicMessagesSlice.reducer; From 7a47e6e8ba918d33f08180749980bb939226cb1d Mon Sep 17 00:00:00 2001 From: Nisan Ohana <78907315+nisanohana3@users.noreply.github.com> Date: Fri, 7 Apr 2023 14:03:56 +0300 Subject: [PATCH 09/17] [Infra] Add devcontainer configuration file (#3603) * Add devcontainer configuration file * Allow development using github codespace with pre-configuration of the needed dependencies Signed-off-by: Nisan Ohana <78907315+nisanohana3@users.noreply.github.com> * Add pull request extention --------- Signed-off-by: Nisan Ohana <78907315+nisanohana3@users.noreply.github.com> Co-authored-by: Roman Zabaluev --- .devcontainer/devcontainer.json | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..74f5825021 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,36 @@ +{ + "name": "Java", + + "image": "mcr.microsoft.com/devcontainers/java:0-17", + + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "none", + "installMaven": "true", + "installGradle": "false" + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "java -version", + + "customizations": { + "vscode": { + "extensions" : [ + "vscjava.vscode-java-pack", + "vscjava.vscode-maven", + "vscjava.vscode-java-debug", + "EditorConfig.EditorConfig", + "ms-azuretools.vscode-docker", + "antfu.vite", + "ms-kubernetes-tools.vscode-kubernetes-tools", + "github.vscode-pull-request-github" + ] + } + } + +} From ee1cd72dd5b6b74f8d856e4ed9135850ebc12e10 Mon Sep 17 00:00:00 2001 From: Ilya Kuramshin Date: Fri, 7 Apr 2023 17:31:04 +0400 Subject: [PATCH 10/17] ISSUE-3144: CVE fixes, Springboot upd (#3624) * ISSUE-3144: Spring boot version bump to 3.0.5, snakeyaml upd * explicit spring security dependency removed * openapi plugin updated to 6.5 * Some javax.annotation imports migrated to jakarta.annotation * base container sha specified * Update CognitoAuthorityExtractor --- kafka-ui-api/Dockerfile | 3 +- .../ui/client/RetryingKafkaConnectClient.java | 212 +++++++++++++++--- .../kafka/ui/config/ClustersProperties.java | 2 +- .../kafka/ui/config/auth/OAuthProperties.java | 2 +- .../ui/controller/KafkaConnectController.java | 10 +- .../GlobalErrorWebExceptionHandler.java | 2 +- .../ui/service/KafkaConfigSanitizer.java | 65 ++++-- .../kafka/ui/service/KafkaConnectService.java | 8 +- .../service/analyze/TopicAnalysisStats.java | 6 +- .../ui/service/rbac/AccessControlService.java | 2 +- .../extractor/CognitoAuthorityExtractor.java | 7 +- .../kafka/ui/AbstractIntegrationTest.java | 4 +- .../ui/service/KafkaConfigSanitizerTest.java | 9 +- .../ui/service/ksql/KsqlServiceV2Test.java | 3 - kafka-ui-contract/pom.xml | 36 ++- .../main/resources/swagger/kafka-ui-api.yaml | 4 + pom.xml | 30 +-- 17 files changed, 276 insertions(+), 129 deletions(-) diff --git a/kafka-ui-api/Dockerfile b/kafka-ui-api/Dockerfile index fcd29c0f06..d969ec7631 100644 --- a/kafka-ui-api/Dockerfile +++ b/kafka-ui-api/Dockerfile @@ -1,4 +1,5 @@ -FROM azul/zulu-openjdk-alpine:17-jre +#FROM azul/zulu-openjdk-alpine:17-jre-headless +FROM azul/zulu-openjdk-alpine@sha256:a36679ac0d28cb835e2a8c00e1e0d95509c6c51c5081c7782b85edb1f37a771a RUN apk add --no-cache gcompat # need to make snappy codec work RUN addgroup -S kafkaui && adduser -S kafkaui -G kafkaui diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java index 5ec5a779d3..74b9485008 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java @@ -6,7 +6,13 @@ import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.connect.ApiClient; import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; import com.provectus.kafka.ui.connect.model.Connector; +import com.provectus.kafka.ui.connect.model.ConnectorPlugin; +import com.provectus.kafka.ui.connect.model.ConnectorPluginConfigValidationResponse; +import com.provectus.kafka.ui.connect.model.ConnectorStatus; +import com.provectus.kafka.ui.connect.model.ConnectorTask; +import com.provectus.kafka.ui.connect.model.ConnectorTopics; import com.provectus.kafka.ui.connect.model.NewConnector; +import com.provectus.kafka.ui.connect.model.TaskStatus; import com.provectus.kafka.ui.exception.KafkaConnectConflictReponseException; import com.provectus.kafka.ui.exception.ValidationException; import com.provectus.kafka.ui.util.WebClientConfigurator; @@ -15,11 +21,7 @@ import java.util.List; import java.util.Map; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.util.MultiValueMap; +import org.springframework.http.ResponseEntity; import org.springframework.util.unit.DataSize; import org.springframework.web.client.RestClientException; import org.springframework.web.reactive.function.client.WebClient; @@ -79,6 +81,176 @@ public class RetryingKafkaConnectClient extends KafkaConnectClientApi { ); } + @Override + public Mono> createConnectorWithHttpInfo(NewConnector newConnector) + throws WebClientResponseException { + return withRetryOnConflict(super.createConnectorWithHttpInfo(newConnector)); + } + + @Override + public Mono deleteConnector(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.deleteConnector(connectorName)); + } + + @Override + public Mono> deleteConnectorWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.deleteConnectorWithHttpInfo(connectorName)); + } + + + @Override + public Mono getConnector(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnector(connectorName)); + } + + @Override + public Mono> getConnectorWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorWithHttpInfo(connectorName)); + } + + @Override + public Mono> getConnectorConfig(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorConfig(connectorName)); + } + + @Override + public Mono>> getConnectorConfigWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorConfigWithHttpInfo(connectorName)); + } + + @Override + public Flux getConnectorPlugins() throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorPlugins()); + } + + @Override + public Mono>> getConnectorPluginsWithHttpInfo() + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorPluginsWithHttpInfo()); + } + + @Override + public Mono getConnectorStatus(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorStatus(connectorName)); + } + + @Override + public Mono> getConnectorStatusWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorStatusWithHttpInfo(connectorName)); + } + + @Override + public Mono getConnectorTaskStatus(String connectorName, Integer taskId) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTaskStatus(connectorName, taskId)); + } + + @Override + public Mono> getConnectorTaskStatusWithHttpInfo(String connectorName, Integer taskId) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTaskStatusWithHttpInfo(connectorName, taskId)); + } + + @Override + public Flux getConnectorTasks(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTasks(connectorName)); + } + + @Override + public Mono>> getConnectorTasksWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTasksWithHttpInfo(connectorName)); + } + + @Override + public Mono> getConnectorTopics(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTopics(connectorName)); + } + + @Override + public Mono>> getConnectorTopicsWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTopicsWithHttpInfo(connectorName)); + } + + @Override + public Flux getConnectors(String search) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectors(search)); + } + + @Override + public Mono>> getConnectorsWithHttpInfo(String search) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorsWithHttpInfo(search)); + } + + @Override + public Mono pauseConnector(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.pauseConnector(connectorName)); + } + + @Override + public Mono> pauseConnectorWithHttpInfo(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.pauseConnectorWithHttpInfo(connectorName)); + } + + @Override + public Mono restartConnector(String connectorName, Boolean includeTasks, Boolean onlyFailed) + throws WebClientResponseException { + return withRetryOnConflict(super.restartConnector(connectorName, includeTasks, onlyFailed)); + } + + @Override + public Mono> restartConnectorWithHttpInfo(String connectorName, Boolean includeTasks, + Boolean onlyFailed) throws WebClientResponseException { + return withRetryOnConflict(super.restartConnectorWithHttpInfo(connectorName, includeTasks, onlyFailed)); + } + + @Override + public Mono restartConnectorTask(String connectorName, Integer taskId) throws WebClientResponseException { + return withRetryOnConflict(super.restartConnectorTask(connectorName, taskId)); + } + + @Override + public Mono> restartConnectorTaskWithHttpInfo(String connectorName, Integer taskId) + throws WebClientResponseException { + return withRetryOnConflict(super.restartConnectorTaskWithHttpInfo(connectorName, taskId)); + } + + @Override + public Mono resumeConnector(String connectorName) throws WebClientResponseException { + return super.resumeConnector(connectorName); + } + + @Override + public Mono> resumeConnectorWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.resumeConnectorWithHttpInfo(connectorName)); + } + + @Override + public Mono> setConnectorConfigWithHttpInfo(String connectorName, + Map requestBody) + throws WebClientResponseException { + return withRetryOnConflict(super.setConnectorConfigWithHttpInfo(connectorName, requestBody)); + } + + @Override + public Mono validateConnectorPluginConfig(String pluginName, + Map requestBody) + throws WebClientResponseException { + return withRetryOnConflict(super.validateConnectorPluginConfig(pluginName, requestBody)); + } + + @Override + public Mono> validateConnectorPluginConfigWithHttpInfo( + String pluginName, Map requestBody) throws WebClientResponseException { + return withRetryOnConflict(super.validateConnectorPluginConfigWithHttpInfo(pluginName, requestBody)); + } + private static class RetryingApiClient extends ApiClient { public RetryingApiClient(ConnectCluster config, @@ -108,35 +280,5 @@ public class RetryingKafkaConnectClient extends KafkaConnectClientApi { .configureBufferSize(maxBuffSize) .build(); } - - @Override - public Mono invokeAPI(String path, HttpMethod method, Map pathParams, - MultiValueMap queryParams, Object body, - HttpHeaders headerParams, - MultiValueMap cookieParams, - MultiValueMap formParams, List accept, - MediaType contentType, String[] authNames, - ParameterizedTypeReference returnType) - throws RestClientException { - return withRetryOnConflict( - super.invokeAPI(path, method, pathParams, queryParams, body, headerParams, cookieParams, - formParams, accept, contentType, authNames, returnType) - ); - } - - @Override - public Flux invokeFluxAPI(String path, HttpMethod method, Map pathParams, - MultiValueMap queryParams, Object body, - HttpHeaders headerParams, - MultiValueMap cookieParams, - MultiValueMap formParams, - List accept, MediaType contentType, - String[] authNames, ParameterizedTypeReference returnType) - throws RestClientException { - return withRetryOnConflict( - super.invokeFluxAPI(path, method, pathParams, queryParams, body, headerParams, - cookieParams, formParams, accept, contentType, authNames, returnType) - ); - } } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java index 919e0633e4..15436c1cd8 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java @@ -1,6 +1,7 @@ package com.provectus.kafka.ui.config; import com.provectus.kafka.ui.model.MetricsConfig; +import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -8,7 +9,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; -import javax.annotation.PostConstruct; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java index f79d217fa7..a76403bf70 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java @@ -1,9 +1,9 @@ package com.provectus.kafka.ui.config.auth; +import jakarta.annotation.PostConstruct; import java.util.HashMap; import java.util.Map; import java.util.Set; -import javax.annotation.PostConstruct; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.Assert; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java index 9ffd901c07..080c6020f9 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java @@ -149,10 +149,9 @@ public class KafkaConnectController extends AbstractController implements KafkaC } @Override - public Mono> setConnectorConfig(String clusterName, - String connectName, + public Mono> setConnectorConfig(String clusterName, String connectName, String connectorName, - @Valid Mono requestBody, + Mono> requestBody, ServerWebExchange exchange) { Mono validateAccess = accessControlService.validateAccess(AccessContext.builder() @@ -164,8 +163,7 @@ public class KafkaConnectController extends AbstractController implements KafkaC return validateAccess.then( kafkaConnectService .setConnectorConfig(getCluster(clusterName), connectName, connectorName, requestBody) - .map(ResponseEntity::ok) - ); + .map(ResponseEntity::ok)); } @Override @@ -242,7 +240,7 @@ public class KafkaConnectController extends AbstractController implements KafkaC @Override public Mono> validateConnectorPluginConfig( - String clusterName, String connectName, String pluginName, @Valid Mono requestBody, + String clusterName, String connectName, String pluginName, @Valid Mono> requestBody, ServerWebExchange exchange) { return kafkaConnectService .validateConnectorPluginConfig( diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java index 394e2aa730..8ad83fe47d 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java @@ -134,7 +134,7 @@ public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHan .timestamp(currentTimestamp()) .stackTrace(Throwables.getStackTraceAsString(exception)); return ServerResponse - .status(exception.getStatus()) + .status(exception.getStatusCode()) .contentType(MediaType.APPLICATION_JSON) .bodyValue(response); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java index aa26709822..375afb0fef 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java @@ -1,38 +1,58 @@ package com.provectus.kafka.ui.service; +import static java.util.regex.Pattern.CASE_INSENSITIVE; + +import com.google.common.collect.ImmutableList; import java.util.Arrays; -import java.util.HashSet; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.kafka.common.config.ConfigDef; import org.apache.kafka.common.config.SaslConfigs; import org.apache.kafka.common.config.SslConfigs; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.stereotype.Component; @Component -class KafkaConfigSanitizer extends Sanitizer { - private static final List DEFAULT_PATTERNS_TO_SANITIZE = Arrays.asList( - "basic.auth.user.info", /* For Schema Registry credentials */ - "password", "secret", "token", "key", ".*credentials.*", /* General credential patterns */ - "aws.access.*", "aws.secret.*", "aws.session.*" /* AWS-related credential patterns */ - ); +class KafkaConfigSanitizer { + + private static final String SANITIZED_VALUE = "******"; + + private static final String[] REGEX_PARTS = {"*", "$", "^", "+"}; + + private static final List DEFAULT_PATTERNS_TO_SANITIZE = ImmutableList.builder() + .addAll(kafkaConfigKeysToSanitize()) + .add( + "basic.auth.user.info", /* For Schema Registry credentials */ + "password", "secret", "token", "key", ".*credentials.*", /* General credential patterns */ + "aws.access.*", "aws.secret.*", "aws.session.*" /* AWS-related credential patterns */ + ) + .build(); + + private final List sanitizeKeysPatterns; KafkaConfigSanitizer( @Value("${kafka.config.sanitizer.enabled:true}") boolean enabled, @Value("${kafka.config.sanitizer.patterns:}") List patternsToSanitize ) { - if (!enabled) { - setKeysToSanitize(); - } else { - var keysToSanitize = new HashSet<>( - patternsToSanitize.isEmpty() ? DEFAULT_PATTERNS_TO_SANITIZE : patternsToSanitize); - keysToSanitize.addAll(kafkaConfigKeysToSanitize()); - setKeysToSanitize(keysToSanitize.toArray(new String[] {})); - } + this.sanitizeKeysPatterns = enabled + ? compile(patternsToSanitize.isEmpty() ? DEFAULT_PATTERNS_TO_SANITIZE : patternsToSanitize) + : List.of(); + } + + private static List compile(Collection patternStrings) { + return patternStrings.stream() + .map(p -> isRegex(p) + ? Pattern.compile(p, CASE_INSENSITIVE) + : Pattern.compile(".*" + p + "$", CASE_INSENSITIVE)) + .toList(); + } + + private static boolean isRegex(String str) { + return Arrays.stream(REGEX_PARTS).anyMatch(str::contains); } private static Set kafkaConfigKeysToSanitize() { @@ -45,4 +65,17 @@ class KafkaConfigSanitizer extends Sanitizer { .collect(Collectors.toSet()); } + public Object sanitize(String key, Object value) { + if (value == null) { + return null; + } + for (Pattern pattern : sanitizeKeysPatterns) { + if (pattern.matcher(key).matches()) { + return SANITIZED_VALUE; + } + } + return value; + } + + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java index 163732fae9..d07ef7ed2d 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java @@ -225,11 +225,11 @@ public class KafkaConnectService { } public Mono setConnectorConfig(KafkaCluster cluster, String connectName, - String connectorName, Mono requestBody) { + String connectorName, Mono> requestBody) { return api(cluster, connectName) .mono(c -> requestBody - .flatMap(body -> c.setConnectorConfig(connectorName, (Map) body)) + .flatMap(body -> c.setConnectorConfig(connectorName, body)) .map(kafkaConnectMapper::fromClient)); } @@ -298,12 +298,12 @@ public class KafkaConnectService { } public Mono validateConnectorPluginConfig( - KafkaCluster cluster, String connectName, String pluginName, Mono requestBody) { + KafkaCluster cluster, String connectName, String pluginName, Mono> requestBody) { return api(cluster, connectName) .mono(client -> requestBody .flatMap(body -> - client.validateConnectorPluginConfig(pluginName, (Map) body)) + client.validateConnectorPluginConfig(pluginName, body)) .map(kafkaConnectMapper::fromClient) ); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java index 2d8e0dc38f..d5b4400807 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java @@ -2,7 +2,7 @@ package com.provectus.kafka.ui.service.analyze; import com.provectus.kafka.ui.model.TopicAnalysisSizeStatsDTO; import com.provectus.kafka.ui.model.TopicAnalysisStatsDTO; -import com.provectus.kafka.ui.model.TopicAnalysisStatsHourlyMsgCountsDTO; +import com.provectus.kafka.ui.model.TopicAnalysisStatsHourlyMsgCountsInnerDTO; import java.time.Duration; import java.time.Instant; import java.util.Comparator; @@ -78,10 +78,10 @@ class TopicAnalysisStats { } } - List toDto() { + List toDto() { return hourlyStats.entrySet().stream() .sorted(Comparator.comparingLong(Map.Entry::getKey)) - .map(e -> new TopicAnalysisStatsHourlyMsgCountsDTO() + .map(e -> new TopicAnalysisStatsHourlyMsgCountsInnerDTO() .hourStart(e.getKey()) .count(e.getValue())) .collect(Collectors.toList()); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java index ee17d21111..3178feae34 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java @@ -21,6 +21,7 @@ import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.LdapAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor; +import jakarta.annotation.PostConstruct; import java.util.Collections; import java.util.List; import java.util.Set; @@ -28,7 +29,6 @@ import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.Nullable; -import javax.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java index a10e0829cc..f7da0a19db 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java @@ -1,9 +1,9 @@ package com.provectus.kafka.ui.service.rbac.extractor; -import com.nimbusds.jose.shaded.json.JSONArray; import com.provectus.kafka.ui.model.rbac.Role; import com.provectus.kafka.ui.model.rbac.provider.Provider; import com.provectus.kafka.ui.service.rbac.AccessControlService; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -44,7 +44,7 @@ public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor { .map(Role::getName) .collect(Collectors.toSet()); - JSONArray groups = principal.getAttribute(COGNITO_GROUPS_ATTRIBUTE_NAME); + List groups = principal.getAttribute(COGNITO_GROUPS_ATTRIBUTE_NAME); if (groups == null) { log.debug("Cognito groups param is not present"); return Mono.just(groupsByUsername); @@ -56,9 +56,8 @@ public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor { .stream() .filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO)) .filter(s -> s.getType().equals("group")) - .anyMatch(subject -> Stream.of(groups.toArray()) + .anyMatch(subject -> Stream.of(groups) .map(Object::toString) - .distinct() .anyMatch(cognitoGroup -> cognitoGroup.equals(subject.getValue())) )) .map(Role::getName) diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/AbstractIntegrationTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/AbstractIntegrationTest.java index a30e45fb3e..dbdfb67fd5 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/AbstractIntegrationTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/AbstractIntegrationTest.java @@ -16,7 +16,7 @@ import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; -import org.springframework.util.SocketUtils; +import org.springframework.test.util.TestSocketUtils; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.Network; import org.testcontainers.utility.DockerImageName; @@ -61,7 +61,7 @@ public abstract class AbstractIntegrationTest { System.setProperty("kafka.clusters.0.bootstrapServers", kafka.getBootstrapServers()); // List unavailable hosts to verify failover System.setProperty("kafka.clusters.0.schemaRegistry", String.format("http://localhost:%1$s,http://localhost:%1$s,%2$s", - SocketUtils.findAvailableTcpPort(), schemaRegistry.getUrl())); + TestSocketUtils.findAvailableTcpPort(), schemaRegistry.getUrl())); System.setProperty("kafka.clusters.0.kafkaConnect.0.name", "kafka-connect"); System.setProperty("kafka.clusters.0.kafkaConnect.0.userName", "kafka-connect"); System.setProperty("kafka.clusters.0.kafkaConnect.0.password", "kafka-connect"); diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java index 232e1d3703..6454cd9f2a 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java @@ -5,13 +5,12 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.endpoint.Sanitizer; class KafkaConfigSanitizerTest { @Test void doNothingIfEnabledPropertySetToFalse() { - final Sanitizer sanitizer = new KafkaConfigSanitizer(false, Collections.emptyList()); + final var sanitizer = new KafkaConfigSanitizer(false, Collections.emptyList()); assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("sasl.jaas.config", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("database.password", "secret")).isEqualTo("secret"); @@ -19,7 +18,7 @@ class KafkaConfigSanitizerTest { @Test void obfuscateCredentials() { - final Sanitizer sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList()); + final var sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList()); assertThat(sanitizer.sanitize("sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("consumer.sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("producer.sasl.jaas.config", "secret")).isEqualTo("******"); @@ -37,7 +36,7 @@ class KafkaConfigSanitizerTest { @Test void notObfuscateNormalConfigs() { - final Sanitizer sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList()); + final var sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList()); assertThat(sanitizer.sanitize("security.protocol", "SASL_SSL")).isEqualTo("SASL_SSL"); final String[] bootstrapServer = new String[] {"test1:9092", "test2:9092"}; assertThat(sanitizer.sanitize("bootstrap.servers", bootstrapServer)).isEqualTo(bootstrapServer); @@ -45,7 +44,7 @@ class KafkaConfigSanitizerTest { @Test void obfuscateCredentialsWithDefinedPatterns() { - final Sanitizer sanitizer = new KafkaConfigSanitizer(true, Arrays.asList("kafka.ui", ".*test.*")); + final var sanitizer = new KafkaConfigSanitizer(true, Arrays.asList("kafka.ui", ".*test.*")); assertThat(sanitizer.sanitize("consumer.kafka.ui", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("this.is.test.credentials", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("this.is.not.credential", "not.credential")) diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java index b4a48d3879..afa3700c0f 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java @@ -15,7 +15,6 @@ import java.util.concurrent.CopyOnWriteArraySet; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.springframework.util.unit.DataSize; import org.testcontainers.utility.DockerImageName; class KsqlServiceV2Test extends AbstractIntegrationTest { @@ -27,8 +26,6 @@ class KsqlServiceV2Test extends AbstractIntegrationTest { private static final Set STREAMS_TO_DELETE = new CopyOnWriteArraySet<>(); private static final Set TABLES_TO_DELETE = new CopyOnWriteArraySet<>(); - private static final DataSize maxBuffSize = DataSize.ofMegabytes(20); - @BeforeAll static void init() { KSQL_DB.start(); diff --git a/kafka-ui-contract/pom.xml b/kafka-ui-contract/pom.xml index 5f6cb58b7d..f99f20d3d8 100644 --- a/kafka-ui-contract/pom.xml +++ b/kafka-ui-contract/pom.xml @@ -27,20 +27,24 @@ spring-boot-starter-validation - io.swagger - swagger-annotations - ${swagger-annotations.version} + io.swagger.core.v3 + swagger-integration-jakarta + 2.2.8 org.openapitools jackson-databind-nullable - ${jackson-databind-nullable.version} + 0.2.4 - com.google.code.findbugs - jsr305 - 3.0.2 - provided + jakarta.annotation + jakarta.annotation-api + 2.1.1 + + + javax.annotation + javax.annotation-api + 1.3.2 @@ -71,6 +75,7 @@ webclient true java8 + true @@ -80,8 +85,7 @@ generate - ${project.basedir}/src/main/resources/swagger/kafka-ui-api.yaml - + ${project.basedir}/src/main/resources/swagger/kafka-ui-api.yaml ${project.build.directory}/generated-sources/api spring DTO @@ -89,14 +93,12 @@ com.provectus.kafka.ui.model com.provectus.kafka.ui.api kafka-ui-contract - true - true true true true - + true java8 @@ -116,15 +118,13 @@ java false false - com.provectus.kafka.ui.connect.model com.provectus.kafka.ui.connect.api kafka-connect-client - true webclient - + true true java8 @@ -142,15 +142,13 @@ java false false - com.provectus.kafka.ui.sr.model com.provectus.kafka.ui.sr.api kafka-sr-client - true webclient - + true true java8 diff --git a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml index fe9723b2dd..d3b9f33136 100644 --- a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml +++ b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml @@ -2387,6 +2387,10 @@ components: - UNKNOWN ConsumerGroup: + discriminator: + propertyName: inherit + mapping: + details: "#/components/schemas/ConsumerGroupDetails" type: object properties: groupId: diff --git a/pom.xml b/pom.xml index 3281ff183e..a82dc72391 100644 --- a/pom.xml +++ b/pom.xml @@ -30,17 +30,13 @@ 3.1.0 3.0.13 2.14.0 - 0.2.4 3.3.1 - 4.1.85.Final 1.4.2.Final 1.18.24 3.21.9 - 1.1.0 2.13.9 - 1.33 - 2.7.5 - 5.7.5 + 2.0 + 3.0.5 1.0.0 0.1.15 0.1.23 @@ -62,9 +58,8 @@ 3.10.1 3.2.0 2.22.2 - 4.3.0 + 6.5.0 1.2.32 - 1.6.0 @@ -111,20 +106,6 @@ pom import - - org.springframework.security - spring-security-bom - ${spring-security.version} - pom - import - - - io.netty - netty-bom - ${netty.version} - pom - import - com.fasterxml.jackson jackson-bom @@ -147,11 +128,6 @@ protobuf-java ${protobuf-java.version} - - io.projectreactor.netty - reactor-netty-http - ${reactor-netty.version} - org.junit junit-bom From 5c357f94fdde5356a2126193605476194f3cfca6 Mon Sep 17 00:00:00 2001 From: Ilya Kuramshin Date: Fri, 7 Apr 2023 17:49:07 +0400 Subject: [PATCH 11/17] BE: Opt out of version check (#3570) * Build & commit info added to /api/info endpoint --- .../ApplicationConfigController.java | 13 +--- .../ui/service/ApplicationInfoService.java | 76 +++++++++++++++++++ .../kafka/ui/util/GithubReleaseInfo.java | 53 +++++++++++++ .../kafka/ui/util/GithubReleaseInfoTest.java | 54 +++++++++++++ .../main/resources/swagger/kafka-ui-api.yaml | 20 +++++ 5 files changed, 206 insertions(+), 10 deletions(-) create mode 100644 kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java create mode 100644 kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java create mode 100644 kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java index b21ef10c61..571250ba94 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java @@ -13,12 +13,12 @@ import com.provectus.kafka.ui.model.ClusterConfigValidationDTO; import com.provectus.kafka.ui.model.RestartRequestDTO; import com.provectus.kafka.ui.model.UploadedFileInfoDTO; import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.service.ApplicationInfoService; import com.provectus.kafka.ui.service.KafkaClusterFactory; import com.provectus.kafka.ui.service.rbac.AccessControlService; import com.provectus.kafka.ui.util.ApplicationRestarter; import com.provectus.kafka.ui.util.DynamicConfigOperations; import com.provectus.kafka.ui.util.DynamicConfigOperations.PropertiesStructure; -import java.util.List; import java.util.Map; import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; @@ -53,18 +53,11 @@ public class ApplicationConfigController implements ApplicationConfigApi { private final DynamicConfigOperations dynamicConfigOperations; private final ApplicationRestarter restarter; private final KafkaClusterFactory kafkaClusterFactory; - + private final ApplicationInfoService applicationInfoService; @Override public Mono> getApplicationInfo(ServerWebExchange exchange) { - return Mono.just( - new ApplicationInfoDTO() - .enabledFeatures( - dynamicConfigOperations.dynamicConfigEnabled() - ? List.of(ApplicationInfoDTO.EnabledFeaturesEnum.DYNAMIC_CONFIG) - : List.of() - ) - ).map(ResponseEntity::ok); + return Mono.just(applicationInfoService.getApplicationInfo()).map(ResponseEntity::ok); } @Override diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java new file mode 100644 index 0000000000..750a7179fb --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java @@ -0,0 +1,76 @@ +package com.provectus.kafka.ui.service; + +import static com.provectus.kafka.ui.model.ApplicationInfoDTO.EnabledFeaturesEnum; + +import com.provectus.kafka.ui.model.ApplicationInfoBuildDTO; +import com.provectus.kafka.ui.model.ApplicationInfoDTO; +import com.provectus.kafka.ui.model.ApplicationInfoLatestReleaseDTO; +import com.provectus.kafka.ui.util.DynamicConfigOperations; +import com.provectus.kafka.ui.util.GithubReleaseInfo; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.info.GitProperties; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +public class ApplicationInfoService { + + private final GithubReleaseInfo githubReleaseInfo = new GithubReleaseInfo(); + + private final DynamicConfigOperations dynamicConfigOperations; + private final BuildProperties buildProperties; + private final GitProperties gitProperties; + + public ApplicationInfoService(DynamicConfigOperations dynamicConfigOperations, + @Autowired(required = false) BuildProperties buildProperties, + @Autowired(required = false) GitProperties gitProperties) { + this.dynamicConfigOperations = dynamicConfigOperations; + this.buildProperties = Optional.ofNullable(buildProperties).orElse(new BuildProperties(new Properties())); + this.gitProperties = Optional.ofNullable(gitProperties).orElse(new GitProperties(new Properties())); + } + + public ApplicationInfoDTO getApplicationInfo() { + var releaseInfo = githubReleaseInfo.get(); + return new ApplicationInfoDTO() + .build(getBuildInfo(releaseInfo)) + .enabledFeatures(getEnabledFeatures()) + .latestRelease(convert(releaseInfo)); + } + + private ApplicationInfoLatestReleaseDTO convert(GithubReleaseInfo.GithubReleaseDto releaseInfo) { + return new ApplicationInfoLatestReleaseDTO() + .htmlUrl(releaseInfo.html_url()) + .publishedAt(releaseInfo.published_at()) + .versionTag(releaseInfo.tag_name()); + } + + private ApplicationInfoBuildDTO getBuildInfo(GithubReleaseInfo.GithubReleaseDto release) { + return new ApplicationInfoBuildDTO() + .isLatestRelease(release.tag_name() != null && release.tag_name().equals(buildProperties.getVersion())) + .commitId(gitProperties.getShortCommitId()) + .version(buildProperties.getVersion()) + .buildTime(buildProperties.getTime() != null + ? DateTimeFormatter.ISO_INSTANT.format(buildProperties.getTime()) : null); + } + + private List getEnabledFeatures() { + var enabledFeatures = new ArrayList(); + if (dynamicConfigOperations.dynamicConfigEnabled()) { + enabledFeatures.add(EnabledFeaturesEnum.DYNAMIC_CONFIG); + } + return enabledFeatures; + } + + // updating on startup and every hour + @Scheduled(fixedRateString = "${github-release-info-update-rate:3600000}") + public void updateGithubReleaseInfo() { + githubReleaseInfo.refresh().block(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java new file mode 100644 index 0000000000..2ad0c9c399 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java @@ -0,0 +1,53 @@ +package com.provectus.kafka.ui.util; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Duration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Slf4j +public class GithubReleaseInfo { + + private static final String GITHUB_LATEST_RELEASE_RETRIEVAL_URL = + "https://api.github.com/repos/provectus/kafka-ui/releases/latest"; + + private static final Duration GITHUB_API_MAX_WAIT_TIME = Duration.ofSeconds(2); + + public record GithubReleaseDto(String html_url, String tag_name, String published_at) { + + static GithubReleaseDto empty() { + return new GithubReleaseDto(null, null, null); + } + } + + private volatile GithubReleaseDto release = GithubReleaseDto.empty(); + + private final Mono refreshMono; + + public GithubReleaseInfo() { + this(GITHUB_LATEST_RELEASE_RETRIEVAL_URL); + } + + @VisibleForTesting + GithubReleaseInfo(String url) { + this.refreshMono = WebClient.create() + .get() + .uri(url) + .exchangeToMono(resp -> resp.bodyToMono(GithubReleaseDto.class)) + .timeout(GITHUB_API_MAX_WAIT_TIME) + .doOnError(th -> log.trace("Error getting latest github release info", th)) + .onErrorResume(th -> true, th -> Mono.just(GithubReleaseDto.empty())) + .doOnNext(release -> this.release = release) + .then(); + } + + public GithubReleaseDto get() { + return release; + } + + public Mono refresh() { + return refreshMono; + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java new file mode 100644 index 0000000000..6ec4bb7863 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java @@ -0,0 +1,54 @@ +package com.provectus.kafka.ui.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.time.Duration; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +class GithubReleaseInfoTest { + + private final MockWebServer mockWebServer = new MockWebServer(); + + @BeforeEach + void startMockServer() throws IOException { + mockWebServer.start(); + } + + @AfterEach + void stopMockServer() throws IOException { + mockWebServer.close(); + } + + @Test + void test() { + mockWebServer.enqueue(new MockResponse() + .addHeader("content-type: application/json") + .setBody(""" + { + "published_at": "2023-03-09T16:11:31Z", + "tag_name": "v0.6.0", + "html_url": "https://github.com/provectus/kafka-ui/releases/tag/v0.6.0", + "some_unused_prop": "ololo" + } + """)); + var url = mockWebServer.url("repos/provectus/kafka-ui/releases/latest").toString(); + + var infoHolder = new GithubReleaseInfo(url); + infoHolder.refresh().block(); + + var i = infoHolder.get(); + assertThat(i.html_url()) + .isEqualTo("https://github.com/provectus/kafka-ui/releases/tag/v0.6.0"); + assertThat(i.published_at()) + .isEqualTo("2023-03-09T16:11:31Z"); + assertThat(i.tag_name()) + .isEqualTo("v0.6.0"); + } + +} diff --git a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml index d3b9f33136..7b6fd3c113 100644 --- a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml +++ b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml @@ -1917,6 +1917,26 @@ components: type: string enum: - DYNAMIC_CONFIG + build: + type: object + properties: + commitId: + type: string + version: + type: string + buildTime: + type: string + isLatestRelease: + type: boolean + latestRelease: + type: object + properties: + versionTag: + type: string + publishedAt: + type: string + htmlUrl: + type: string Cluster: type: object From a2741291bfd5bff2283e9361adebf30b2db56cca Mon Sep 17 00:00:00 2001 From: Vlad Senyuta <66071557+VladSenyuta@users.noreply.github.com> Date: Mon, 10 Apr 2023 11:22:57 +0300 Subject: [PATCH 12/17] [e2e] Clearing messages availability with Topic's cleanup policy update (#3632) * add verifyClearMessagesMenuStateAfterTopicUpdate * fix indents * upd imports * Add checkstyle plugin execution for e2e * upd violations * upd violations * Impl a separate checkstyle config for e2e --------- Co-authored-by: Roman Zabaluev --- etc/checkstyle/checkstyle-e2e.xml | 333 +++++++ etc/checkstyle/checkstyle.xml | 4 +- kafka-ui-e2e-checks/pom.xml | 31 + .../provectus/kafka/ui/models/Connector.java | 2 +- .../com/provectus/kafka/ui/models/Schema.java | 39 +- .../com/provectus/kafka/ui/models/Topic.java | 12 +- .../provectus/kafka/ui/pages/BasePage.java | 245 +++-- .../ui/pages/brokers/BrokersConfigTab.java | 47 +- .../ui/pages/brokers/BrokersDetails.java | 145 ++- .../kafka/ui/pages/brokers/BrokersList.java | 179 ++-- .../pages/connectors/ConnectorCreateForm.java | 68 +- .../ui/pages/connectors/ConnectorDetails.java | 128 +-- .../ui/pages/connectors/KafkaConnectList.java | 56 +- .../ui/pages/consumers/ConsumersDetails.java | 38 +- .../ui/pages/consumers/ConsumersList.java | 16 +- .../kafka/ui/pages/ksqlDb/KsqlDbList.java | 139 --- .../kafka/ui/pages/ksqlDb/KsqlQueryForm.java | 154 ---- .../ui/pages/ksqlDb/enums/KsqlMenuTabs.java | 17 - .../pages/ksqlDb/enums/KsqlQueryConfig.java | 19 - .../kafka/ui/pages/ksqldb/KsqlDbList.java | 138 +++ .../kafka/ui/pages/ksqldb/KsqlQueryForm.java | 153 ++++ .../ui/pages/ksqldb/enums/KsqlMenuTabs.java | 17 + .../pages/ksqldb/enums/KsqlQueryConfig.java | 18 + .../{ksqlDb => ksqldb}/models/Stream.java | 4 +- .../{ksqlDb => ksqldb}/models/Table.java | 4 +- .../kafka/ui/pages/panels/NaviSideBar.java | 83 +- .../kafka/ui/pages/panels/TopPanel.java | 33 +- .../kafka/ui/pages/panels/enums/MenuItem.java | 48 +- .../ui/pages/schemas/SchemaCreateForm.java | 216 ++--- .../kafka/ui/pages/schemas/SchemaDetails.java | 102 +-- .../ui/pages/schemas/SchemaRegistryList.java | 52 +- .../ui/pages/topics/ProduceMessagePanel.java | 81 +- .../ui/pages/topics/TopicCreateEditForm.java | 508 +++++------ .../kafka/ui/pages/topics/TopicDetails.java | 847 +++++++++--------- .../ui/pages/topics/TopicSettingsTab.java | 87 +- .../kafka/ui/pages/topics/TopicsList.java | 469 +++++----- .../topics/enums/CleanupPolicyValue.java | 30 +- .../topics/enums/CustomParameterType.java | 58 +- .../ui/pages/topics/enums/MaxSizeOnDisk.java | 34 +- .../ui/pages/topics/enums/TimeToRetain.java | 34 +- .../kafka/ui/services/ApiService.java | 454 +++++----- .../kafka/ui/settings/BaseSource.java | 38 +- .../kafka/ui/settings/configs/Profiles.java | 16 +- .../kafka/ui/settings/drivers/WebDriver.java | 149 +-- .../ui/settings/listeners/AllureListener.java | 46 +- .../ui/settings/listeners/LoggerListener.java | 48 +- .../listeners/QaseCreateListener.java | 199 ++-- .../listeners/QaseResultListener.java | 161 ++-- .../kafka/ui/utilities/FileUtils.java | 33 +- .../kafka/ui/utilities/TimeUtils.java | 15 +- .../kafka/ui/utilities/WebUtils.java | 161 ++-- .../kafka/ui/utilities/qase/QaseSetup.java | 33 + .../annotations/Automation.java | 7 +- .../annotations/Status.java | 4 +- .../annotations/Suite.java | 4 +- .../kafka/ui/utilities/qase/enums/State.java | 18 + .../kafka/ui/utilities/qase/enums/Status.java | 18 + .../ui/utilities/qaseUtils/QaseSetup.java | 34 - .../ui/utilities/qaseUtils/enums/State.java | 18 - .../ui/utilities/qaseUtils/enums/Status.java | 18 - .../provectus/kafka/ui/variables/Browser.java | 4 +- .../provectus/kafka/ui/variables/Suite.java | 10 +- .../com/provectus/kafka/ui/variables/Url.java | 12 +- .../config_for_create_connector.json | 2 +- .../config_for_create_connector_via_api.json | 2 +- .../connectors/delete_connector_config.json | 2 +- .../schemas/schema_avro_for_update.json | 2 +- .../testData/schemas/schema_avro_value.json | 2 +- .../testData/schemas/schema_json_Value.json | 2 +- .../topics/message_content_create_topic.json | 2 +- .../java/com/provectus/kafka/ui/BaseTest.java | 235 ++--- .../java/com/provectus/kafka/ui/Facade.java | 56 +- .../kafka/ui/manualSuite/BaseManualTest.java | 30 - .../ui/manualSuite/backlog/SanityBacklog.java | 19 - .../ui/manualSuite/backlog/SmokeBacklog.java | 75 -- .../ui/manualSuite/suite/DataMaskingTest.java | 29 - .../kafka/ui/manualSuite/suite/RbacTest.java | 53 -- .../ui/manualSuite/suite/TopicsTest.java | 95 -- .../ui/manualSuite/suite/WizardTest.java | 17 - .../kafka/ui/manualsuite/BaseManualTest.java | 30 + .../ui/manualsuite/backlog/SanityBacklog.java | 7 + .../ui/manualsuite/backlog/SmokeBacklog.java | 78 ++ .../ui/manualsuite/suite/DataMaskingTest.java | 29 + .../kafka/ui/manualsuite/suite/RbacTest.java | 53 ++ .../ui/manualsuite/suite/TopicsTest.java | 95 ++ .../ui/manualsuite/suite/WizardTest.java | 17 + .../kafka/ui/qaseSuite/BaseQaseTest.java | 23 - .../kafka/ui/qaseSuite/Template.java | 58 -- .../kafka/ui/qasesuite/BaseQaseTest.java | 25 + .../kafka/ui/qasesuite/Template.java | 58 ++ .../kafka/ui/sanitySuite/TestClass.java | 4 - .../kafka/ui/sanitysuite/TopicsTest.java | 66 ++ .../kafka/ui/smokeSuite/SmokeTest.java | 107 --- .../ui/smokeSuite/brokers/BrokersTest.java | 41 - .../smokeSuite/connectors/ConnectorsTest.java | 106 --- .../ui/smokeSuite/ksqlDb/KsqlDbTest.java | 80 -- .../ui/smokeSuite/schemas/SchemasTest.java | 190 ---- .../ui/smokeSuite/topics/MessagesTest.java | 283 ------ .../ui/smokeSuite/topics/TopicsTest.java | 496 ---------- .../kafka/ui/smokesuite/SmokeTest.java | 115 +++ .../ui/smokesuite/brokers/BrokersTest.java | 41 + .../smokesuite/connectors/ConnectorsTest.java | 107 +++ .../ui/smokesuite/ksqldb/KsqlDbTest.java | 79 ++ .../ui/smokesuite/schemas/SchemasTest.java | 190 ++++ .../ui/smokesuite/topics/MessagesTest.java | 279 ++++++ .../ui/smokesuite/topics/TopicsTest.java | 500 +++++++++++ .../src/test/resources/manual.xml | 2 +- .../src/test/resources/qase.xml | 2 +- .../src/test/resources/regression.xml | 6 +- .../src/test/resources/sanity.xml | 2 +- .../src/test/resources/smoke.xml | 2 +- 111 files changed, 5238 insertions(+), 4744 deletions(-) create mode 100644 etc/checkstyle/checkstyle-e2e.xml delete mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlDbList.java delete mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlQueryForm.java delete mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlMenuTabs.java delete mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlQueryConfig.java create mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlDbList.java create mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java create mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlMenuTabs.java create mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlQueryConfig.java rename kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/{ksqlDb => ksqldb}/models/Stream.java (50%) rename kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/{ksqlDb => ksqldb}/models/Table.java (56%) create mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/QaseSetup.java rename kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/{qaseUtils => qase}/annotations/Automation.java (63%) rename kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/{qaseUtils => qase}/annotations/Status.java (65%) rename kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/{qaseUtils => qase}/annotations/Suite.java (76%) create mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/State.java create mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/Status.java delete mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/QaseSetup.java delete mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/State.java delete mode 100644 kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/Status.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/BaseManualTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SanityBacklog.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SmokeBacklog.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/DataMaskingTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/RbacTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/TopicsTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/WizardTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/BaseManualTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SanityBacklog.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/DataMaskingTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/RbacTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/BaseQaseTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/Template.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitySuite/TestClass.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitysuite/TopicsTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/brokers/BrokersTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/connectors/ConnectorsTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/schemas/SchemasTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/MessagesTest.java delete mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/SmokeTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/schemas/SchemasTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java create mode 100644 kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java diff --git a/etc/checkstyle/checkstyle-e2e.xml b/etc/checkstyle/checkstyle-e2e.xml new file mode 100644 index 0000000000..c2af9c987b --- /dev/null +++ b/etc/checkstyle/checkstyle-e2e.xml @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/etc/checkstyle/checkstyle.xml b/etc/checkstyle/checkstyle.xml index 0348f809b4..745f1bc368 100644 --- a/etc/checkstyle/checkstyle.xml +++ b/etc/checkstyle/checkstyle.xml @@ -318,7 +318,7 @@ - + @@ -330,4 +330,4 @@ - \ No newline at end of file + diff --git a/kafka-ui-e2e-checks/pom.xml b/kafka-ui-e2e-checks/pom.xml index e4d912dbe6..c93f6bcabb 100644 --- a/kafka-ui-e2e-checks/pom.xml +++ b/kafka-ui-e2e-checks/pom.xml @@ -264,6 +264,37 @@ allure-maven 2.10.0 + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + + com.puppycrawl.tools + checkstyle + 10.3.1 + + + + + checkstyle + validate + + check + + + warning + true + true + true + file:${basedir}/../etc/checkstyle/checkstyle-e2e.xml + file:${basedir}/../etc/checkstyle/apache-header.txt + + + + + + diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java index 48088cdf91..493010a3f6 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java @@ -7,5 +7,5 @@ import lombok.experimental.Accessors; @Accessors(chain = true) public class Connector { - private String name, config; + private String name, config; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java index 19dc44a028..55a6e63112 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java @@ -1,33 +1,34 @@ package com.provectus.kafka.ui.models; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + import com.provectus.kafka.ui.api.model.SchemaType; import lombok.Data; import lombok.experimental.Accessors; -import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; - @Data @Accessors(chain = true) public class Schema { - private String name, valuePath; - private SchemaType type; + private String name, valuePath; + private SchemaType type; - public static Schema createSchemaAvro() { - return new Schema().setName("schema_avro-" + randomAlphabetic(5)) - .setType(SchemaType.AVRO) - .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_avro_value.json"); - } + public static Schema createSchemaAvro() { + return new Schema().setName("schema_avro-" + randomAlphabetic(5)) + .setType(SchemaType.AVRO) + .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_avro_value.json"); + } - public static Schema createSchemaJson() { - return new Schema().setName("schema_json-" + randomAlphabetic(5)) - .setType(SchemaType.JSON) - .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_json_Value.json"); - } + public static Schema createSchemaJson() { + return new Schema().setName("schema_json-" + randomAlphabetic(5)) + .setType(SchemaType.JSON) + .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_json_Value.json"); + } - public static Schema createSchemaProtobuf() { - return new Schema().setName("schema_protobuf-" + randomAlphabetic(5)) - .setType(SchemaType.PROTOBUF) - .setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_protobuf_value.txt"); - } + public static Schema createSchemaProtobuf() { + return new Schema().setName("schema_protobuf-" + randomAlphabetic(5)) + .setType(SchemaType.PROTOBUF) + .setValuePath( + System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_protobuf_value.txt"); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java index 21486a93f1..ece16b4cc1 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java @@ -11,10 +11,10 @@ import lombok.experimental.Accessors; @Accessors(chain = true) public class Topic { - private String name, timeToRetainData, maxMessageBytes, messageKey, messageContent, customParameterValue; - private int numberOfPartitions; - private CustomParameterType customParameterType; - private CleanupPolicyValue cleanupPolicyValue; - private MaxSizeOnDisk maxSizeOnDisk; - private TimeToRetain timeToRetain; + private String name, timeToRetainData, maxMessageBytes, messageKey, messageContent, customParameterValue; + private int numberOfPartitions; + private CustomParameterType customParameterType; + private CleanupPolicyValue cleanupPolicyValue; + private MaxSizeOnDisk maxSizeOnDisk; + private TimeToRetain timeToRetain; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java index 2bb4ecce51..fb2e0877e2 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java @@ -1,143 +1,142 @@ package com.provectus.kafka.ui.pages; +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.ElementsCollection; import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.WebDriverRunner; import com.provectus.kafka.ui.pages.panels.enums.MenuItem; import com.provectus.kafka.ui.utilities.WebUtils; +import java.time.Duration; import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.Keys; import org.openqa.selenium.interactions.Actions; -import java.time.Duration; - -import static com.codeborne.selenide.Selenide.$$x; -import static com.codeborne.selenide.Selenide.$x; - @Slf4j public abstract class BasePage extends WebUtils { - protected SelenideElement loadingSpinner = $x("//div[@role='progressbar']"); - protected SelenideElement submitBtn = $x("//button[@type='submit']"); - protected SelenideElement tableGrid = $x("//table"); - protected SelenideElement searchFld = $x("//input[@type='text'][contains(@id, ':r')]"); - protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']"); - protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']"); - protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']"); - protected SelenideElement confirmationMdl = $x("//div[text()= 'Confirm the action']/.."); - protected SelenideElement confirmBtn = $x("//button[contains(text(),'Confirm')]"); - protected SelenideElement cancelBtn = $x("//button[contains(text(),'Cancel')]"); - protected SelenideElement backBtn = $x("//button[contains(text(),'Back')]"); - protected SelenideElement nextBtn = $x("//button[contains(text(),'Next')]"); - protected ElementsCollection ddlOptions = $$x("//li[@value]"); - protected ElementsCollection gridItems = $$x("//tr[@class]"); - protected String summaryCellLocator = "//div[contains(text(),'%s')]"; - protected String tableElementNameLocator = "//tbody//a[contains(text(),'%s')]"; - protected String columnHeaderLocator = "//table//tr/th//div[text()='%s']"; - protected String pageTitleFromHeader = "//h1[text()='%s']"; - protected String pagePathFromHeader = "//a[text()='%s']/../h1"; + protected SelenideElement loadingSpinner = $x("//div[@role='progressbar']"); + protected SelenideElement submitBtn = $x("//button[@type='submit']"); + protected SelenideElement tableGrid = $x("//table"); + protected SelenideElement searchFld = $x("//input[@type='text'][contains(@id, ':r')]"); + protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']"); + protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']"); + protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']"); + protected SelenideElement confirmationMdl = $x("//div[text()= 'Confirm the action']/.."); + protected SelenideElement confirmBtn = $x("//button[contains(text(),'Confirm')]"); + protected SelenideElement cancelBtn = $x("//button[contains(text(),'Cancel')]"); + protected SelenideElement backBtn = $x("//button[contains(text(),'Back')]"); + protected SelenideElement nextBtn = $x("//button[contains(text(),'Next')]"); + protected ElementsCollection ddlOptions = $$x("//li[@value]"); + protected ElementsCollection gridItems = $$x("//tr[@class]"); + protected String summaryCellLocator = "//div[contains(text(),'%s')]"; + protected String tableElementNameLocator = "//tbody//a[contains(text(),'%s')]"; + protected String columnHeaderLocator = "//table//tr/th//div[text()='%s']"; + protected String pageTitleFromHeader = "//h1[text()='%s']"; + protected String pagePathFromHeader = "//a[text()='%s']/../h1"; - protected void waitUntilSpinnerDisappear(int... timeoutInSeconds) { - log.debug("\nwaitUntilSpinnerDisappear"); - if (isVisible(loadingSpinner, timeoutInSeconds)) { - loadingSpinner.shouldBe(Condition.disappear, Duration.ofSeconds(60)); - } + protected void waitUntilSpinnerDisappear(int... timeoutInSeconds) { + log.debug("\nwaitUntilSpinnerDisappear"); + if (isVisible(loadingSpinner, timeoutInSeconds)) { + loadingSpinner.shouldBe(Condition.disappear, Duration.ofSeconds(60)); + } + } + + protected void searchItem(String tag) { + log.debug("\nsearchItem: {}", tag); + sendKeysAfterClear(searchFld, tag); + searchFld.pressEnter().shouldHave(Condition.value(tag)); + waitUntilSpinnerDisappear(1); + } + + protected SelenideElement getPageTitleFromHeader(MenuItem menuItem) { + return $x(String.format(pageTitleFromHeader, menuItem.getPageTitle())); + } + + protected SelenideElement getPagePathFromHeader(MenuItem menuItem) { + return $x(String.format(pagePathFromHeader, menuItem.getPageTitle())); + } + + protected void clickSubmitBtn() { + clickByJavaScript(submitBtn); + } + + protected void setJsonInputValue(SelenideElement jsonInput, String jsonConfig) { + sendKeysByActions(jsonInput, jsonConfig.replace(" ", "")); + new Actions(WebDriverRunner.getWebDriver()) + .keyDown(Keys.SHIFT) + .sendKeys(Keys.PAGE_DOWN) + .keyUp(Keys.SHIFT) + .sendKeys(Keys.DELETE) + .perform(); + } + + protected SelenideElement getTableElement(String elementName) { + log.debug("\ngetTableElement: {}", elementName); + return $x(String.format(tableElementNameLocator, elementName)); + } + + protected ElementsCollection getDdlOptions() { + return ddlOptions; + } + + protected String getAlertHeader() { + log.debug("\ngetAlertHeader"); + String result = alertHeader.shouldBe(Condition.visible).getText(); + log.debug("-> {}", result); + return result; + } + + protected String getAlertMessage() { + log.debug("\ngetAlertMessage"); + String result = alertMessage.shouldBe(Condition.visible).getText(); + log.debug("-> {}", result); + return result; + } + + protected boolean isAlertVisible(AlertHeader header) { + log.debug("\nisAlertVisible: {}", header.toString()); + boolean result = getAlertHeader().equals(header.toString()); + log.debug("-> {}", result); + return result; + } + + protected boolean isAlertVisible(AlertHeader header, String message) { + log.debug("\nisAlertVisible: {} {}", header, message); + boolean result = isAlertVisible(header) && getAlertMessage().equals(message); + log.debug("-> {}", result); + return result; + } + + protected void clickConfirmButton() { + confirmBtn.shouldBe(Condition.enabled).click(); + confirmBtn.shouldBe(Condition.disappear); + } + + protected void clickCancelButton() { + cancelBtn.shouldBe(Condition.enabled).click(); + cancelBtn.shouldBe(Condition.disappear); + } + + protected boolean isConfirmationModalVisible() { + return isVisible(confirmationMdl); + } + + public enum AlertHeader { + SUCCESS("Success"), + VALIDATION_ERROR("Validation Error"), + BAD_REQUEST("400 Bad Request"); + + private final String value; + + AlertHeader(String value) { + this.value = value; } - protected void searchItem(String tag) { - log.debug("\nsearchItem: {}", tag); - sendKeysAfterClear(searchFld, tag); - searchFld.pressEnter().shouldHave(Condition.value(tag)); - waitUntilSpinnerDisappear(1); - } - - protected SelenideElement getPageTitleFromHeader(MenuItem menuItem) { - return $x(String.format(pageTitleFromHeader, menuItem.getPageTitle())); - } - - protected SelenideElement getPagePathFromHeader(MenuItem menuItem) { - return $x(String.format(pagePathFromHeader, menuItem.getPageTitle())); - } - - protected void clickSubmitBtn() { - clickByJavaScript(submitBtn); - } - - protected void setJsonInputValue(SelenideElement jsonInput, String jsonConfig) { - sendKeysByActions(jsonInput, jsonConfig.replace(" ", "")); - new Actions(WebDriverRunner.getWebDriver()) - .keyDown(Keys.SHIFT) - .sendKeys(Keys.PAGE_DOWN) - .keyUp(Keys.SHIFT) - .sendKeys(Keys.DELETE) - .perform(); - } - - protected SelenideElement getTableElement(String elementName) { - log.debug("\ngetTableElement: {}", elementName); - return $x(String.format(tableElementNameLocator, elementName)); - } - - protected ElementsCollection getDdlOptions() { - return ddlOptions; - } - - protected String getAlertHeader() { - log.debug("\ngetAlertHeader"); - String result = alertHeader.shouldBe(Condition.visible).getText(); - log.debug("-> {}", result); - return result; - } - - protected String getAlertMessage() { - log.debug("\ngetAlertMessage"); - String result = alertMessage.shouldBe(Condition.visible).getText(); - log.debug("-> {}", result); - return result; - } - - protected boolean isAlertVisible(AlertHeader header) { - log.debug("\nisAlertVisible: {}", header.toString()); - boolean result = getAlertHeader().equals(header.toString()); - log.debug("-> {}", result); - return result; - } - - protected boolean isAlertVisible(AlertHeader header, String message) { - log.debug("\nisAlertVisible: {} {}", header, message); - boolean result = isAlertVisible(header) && getAlertMessage().equals(message); - log.debug("-> {}", result); - return result; - } - - protected void clickConfirmButton() { - confirmBtn.shouldBe(Condition.enabled).click(); - confirmBtn.shouldBe(Condition.disappear); - } - - protected void clickCancelButton() { - cancelBtn.shouldBe(Condition.enabled).click(); - cancelBtn.shouldBe(Condition.disappear); - } - - protected boolean isConfirmationModalVisible() { - return isVisible(confirmationMdl); - } - - public enum AlertHeader { - SUCCESS("Success"), - VALIDATION_ERROR("Validation Error"), - BAD_REQUEST("400 Bad Request"); - - private final String value; - - AlertHeader(String value) { - this.value = value; - } - - public String toString() { - return value; - } + public String toString() { + return value; } + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java index 7b37d6709c..f358614dc8 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java @@ -1,41 +1,40 @@ package com.provectus.kafka.ui.pages.brokers; +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; - import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import static com.codeborne.selenide.Selenide.$$x; -import static com.codeborne.selenide.Selenide.$x; - public class BrokersConfigTab extends BasePage { - protected List editBtn = $$x("//button[@aria-label='editAction']"); - protected SelenideElement searchByKeyField = $x("//input[@placeholder='Search by Key']"); + protected List editBtn = $$x("//button[@aria-label='editAction']"); + protected SelenideElement searchByKeyField = $x("//input[@placeholder='Search by Key']"); - @Step - public BrokersConfigTab waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - searchByKeyField.shouldBe(Condition.visible); - return this; - } + @Step + public BrokersConfigTab waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + searchByKeyField.shouldBe(Condition.visible); + return this; + } - @Step - public boolean isSearchByKeyVisible() { - return isVisible(searchByKeyField); - } + @Step + public boolean isSearchByKeyVisible() { + return isVisible(searchByKeyField); + } - public List getColumnHeaders() { - return Stream.of("Key", "Value", "Source") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } + public List getColumnHeaders() { + return Stream.of("Key", "Value", "Source") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } - public List getEditButtons() { - return editBtn; - } + public List getEditButtons() { + return editBtn; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java index 8cc3dd98ba..4eca65f1f4 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java @@ -1,92 +1,91 @@ package com.provectus.kafka.ui.pages.brokers; +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$x; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import org.openqa.selenium.By; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; - -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$x; +import org.openqa.selenium.By; public class BrokersDetails extends BasePage { - protected SelenideElement logDirectoriesTab = $x("//a[text()='Log directories']"); - protected SelenideElement metricsTab = $x("//a[text()='Metrics']"); - protected String brokersTabLocator = "//a[text()='%s']"; + protected SelenideElement logDirectoriesTab = $x("//a[text()='Log directories']"); + protected SelenideElement metricsTab = $x("//a[text()='Metrics']"); + protected String brokersTabLocator = "//a[text()='%s']"; - @Step - public BrokersDetails waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - Arrays.asList(logDirectoriesTab, metricsTab).forEach(element -> element.shouldBe(Condition.visible)); - return this; + @Step + public BrokersDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + Arrays.asList(logDirectoriesTab, metricsTab).forEach(element -> element.shouldBe(Condition.visible)); + return this; + } + + @Step + public BrokersDetails openDetailsTab(DetailsTab menu) { + $(By.linkText(menu.toString())).shouldBe(Condition.enabled).click(); + waitUntilSpinnerDisappear(); + return this; + } + + private List getVisibleColumnHeaders() { + return Stream.of("Name", "Topics", "Error", "Partitions") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + private List getEnabledColumnHeaders() { + return Stream.of("Name", "Error") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + private List getVisibleSummaryCells() { + return Stream.of("Segment Size", "Segment Count", "Port", "Host") + .map(name -> $x(String.format(summaryCellLocator, name))) + .collect(Collectors.toList()); + } + + private List getDetailsTabs() { + return Stream.of(DetailsTab.values()) + .map(name -> $x(String.format(brokersTabLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllEnabledElements() { + List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); + enabledElements.addAll(getDetailsTabs()); + return enabledElements; + } + + @Step + public List getAllVisibleElements() { + List visibleElements = new ArrayList<>(getVisibleSummaryCells()); + visibleElements.addAll(getVisibleColumnHeaders()); + visibleElements.addAll(getDetailsTabs()); + return visibleElements; + } + + public enum DetailsTab { + LOG_DIRECTORIES("Log directories"), + CONFIGS("Configs"), + METRICS("Metrics"); + + private final String value; + + DetailsTab(String value) { + this.value = value; } - @Step - public BrokersDetails openDetailsTab(DetailsTab menu) { - $(By.linkText(menu.toString())).shouldBe(Condition.enabled).click(); - waitUntilSpinnerDisappear(); - return this; - } - - private List getVisibleColumnHeaders() { - return Stream.of("Name", "Topics", "Error", "Partitions") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } - - private List getEnabledColumnHeaders() { - return Stream.of("Name", "Error") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } - - private List getVisibleSummaryCells() { - return Stream.of("Segment Size", "Segment Count", "Port", "Host") - .map(name -> $x(String.format(summaryCellLocator, name))) - .collect(Collectors.toList()); - } - - private List getDetailsTabs() { - return Stream.of(DetailsTab.values()) - .map(name -> $x(String.format(brokersTabLocator, name))) - .collect(Collectors.toList()); - } - - @Step - public List getAllEnabledElements() { - List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); - enabledElements.addAll(getDetailsTabs()); - return enabledElements; - } - - @Step - public List getAllVisibleElements() { - List visibleElements = new ArrayList<>(getVisibleSummaryCells()); - visibleElements.addAll(getVisibleColumnHeaders()); - visibleElements.addAll(getDetailsTabs()); - return visibleElements; - } - - public enum DetailsTab { - LOG_DIRECTORIES("Log directories"), - CONFIGS("Configs"), - METRICS("Metrics"); - - private final String value; - - DetailsTab(String value) { - this.value = value; - } - - public String toString() { - return value; - } + public String toString() { + return value; } + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java index f1f08c3cf5..50ecdff359 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java @@ -1,123 +1,122 @@ package com.provectus.kafka.ui.pages.brokers; +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS; + import com.codeborne.selenide.CollectionCondition; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; - import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import static com.codeborne.selenide.Selenide.$x; -import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS; - public class BrokersList extends BasePage { - @Step - public BrokersList waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - getPageTitleFromHeader(BROKERS).shouldBe(Condition.visible); - return this; + @Step + public BrokersList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(BROKERS).shouldBe(Condition.visible); + return this; + } + + @Step + public BrokersList openBroker(int brokerId) { + getBrokerItem(brokerId).openItem(); + return this; + } + + private List getUptimeSummaryCells() { + return Stream.of("Broker Count", "Active Controller", "Version") + .map(name -> $x(String.format(summaryCellLocator, name))) + .collect(Collectors.toList()); + } + + private List getPartitionsSummaryCells() { + return Stream.of("Online", "URP", "In Sync Replicas", "Out Of Sync Replicas") + .map(name -> $x(String.format(summaryCellLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllVisibleElements() { + List visibleElements = new ArrayList<>(getUptimeSummaryCells()); + visibleElements.addAll(getPartitionsSummaryCells()); + return visibleElements; + } + + private List getEnabledColumnHeaders() { + return Stream.of("Broker ID", "Segment Size", "Segment Count", "Port", "Host") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllEnabledElements() { + return getEnabledColumnHeaders(); + } + + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new BrokersList.BrokerGridItem(item))); + return gridItemList; + } + + @Step + public BrokerGridItem getBrokerItem(int id) { + return initGridItems().stream() + .filter(e -> e.getId() == id) + .findFirst().orElseThrow(); + } + + @Step + public List getAllBrokers() { + return initGridItems(); + } + + public static class BrokerGridItem extends BasePage { + + private final SelenideElement element; + + public BrokerGridItem(SelenideElement element) { + this.element = element; + } + + private SelenideElement getIdElm() { + return element.$x("./td[1]/div/a"); } @Step - public BrokersList openBroker(int brokerId) { - getBrokerItem(brokerId).openItem(); - return this; - } - - private List getUptimeSummaryCells() { - return Stream.of("Broker Count", "Active Controller", "Version") - .map(name -> $x(String.format(summaryCellLocator, name))) - .collect(Collectors.toList()); - } - - private List getPartitionsSummaryCells() { - return Stream.of("Online", "URP", "In Sync Replicas", "Out Of Sync Replicas") - .map(name -> $x(String.format(summaryCellLocator, name))) - .collect(Collectors.toList()); + public int getId() { + return Integer.parseInt(getIdElm().getText().trim()); } @Step - public List getAllVisibleElements() { - List visibleElements = new ArrayList<>(getUptimeSummaryCells()); - visibleElements.addAll(getPartitionsSummaryCells()); - return visibleElements; - } - - private List getEnabledColumnHeaders() { - return Stream.of("Broker ID", "Segment Size", "Segment Count", "Port", "Host") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); + public void openItem() { + getIdElm().click(); } @Step - public List getAllEnabledElements() { - return getEnabledColumnHeaders(); - } - - private List initGridItems() { - List gridItemList = new ArrayList<>(); - gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new BrokersList.BrokerGridItem(item))); - return gridItemList; + public int getSegmentSize() { + return Integer.parseInt(element.$x("./td[2]").getText().trim()); } @Step - public BrokerGridItem getBrokerItem(int id) { - return initGridItems().stream() - .filter(e -> e.getId() == id) - .findFirst().orElseThrow(); + public int getSegmentCount() { + return Integer.parseInt(element.$x("./td[3]").getText().trim()); } @Step - public List getAllBrokers() { - return initGridItems(); + public int getPort() { + return Integer.parseInt(element.$x("./td[4]").getText().trim()); } - public static class BrokerGridItem extends BasePage { - - private final SelenideElement element; - - public BrokerGridItem(SelenideElement element) { - this.element = element; - } - - private SelenideElement getIdElm() { - return element.$x("./td[1]/div/a"); - } - - @Step - public int getId() { - return Integer.parseInt(getIdElm().getText().trim()); - } - - @Step - public void openItem() { - getIdElm().click(); - } - - @Step - public int getSegmentSize() { - return Integer.parseInt(element.$x("./td[2]").getText().trim()); - } - - @Step - public int getSegmentCount() { - return Integer.parseInt(element.$x("./td[3]").getText().trim()); - } - - @Step - public int getPort() { - return Integer.parseInt(element.$x("./td[4]").getText().trim()); - } - - @Step - public String getHost() { - return element.$x("./td[5]").getText().trim(); - } + @Step + public String getHost() { + return element.$x("./td[5]").getText().trim(); } + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java index fbbea0f1c0..0b6b7b5608 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java @@ -1,49 +1,49 @@ package com.provectus.kafka.ui.pages.connectors; +import static com.codeborne.selenide.Selenide.$x; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import static com.codeborne.selenide.Selenide.$x; - public class ConnectorCreateForm extends BasePage { - protected SelenideElement nameField = $x("//input[@name='name']"); - protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); - protected SelenideElement configField = $x("//div[@id='config']"); + protected SelenideElement nameField = $x("//input[@name='name']"); + protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); + protected SelenideElement configField = $x("//div[@id='config']"); - @Step - public ConnectorCreateForm waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - nameField.shouldBe(Condition.visible); - return this; - } + @Step + public ConnectorCreateForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + nameField.shouldBe(Condition.visible); + return this; + } - @Step - public ConnectorCreateForm setName(String connectName) { - nameField.shouldBe(Condition.enabled).setValue(connectName); - return this; - } + @Step + public ConnectorCreateForm setName(String connectName) { + nameField.shouldBe(Condition.enabled).setValue(connectName); + return this; + } - @Step - public ConnectorCreateForm setConfig(String configJson) { - configField.shouldBe(Condition.enabled).click(); - setJsonInputValue(contentTextArea, configJson); - return this; - } + @Step + public ConnectorCreateForm setConfig(String configJson) { + configField.shouldBe(Condition.enabled).click(); + setJsonInputValue(contentTextArea, configJson); + return this; + } - @Step - public ConnectorCreateForm setConnectorDetails(String connectName, String configJson) { - setName(connectName); - setConfig(configJson); - return this; - } + @Step + public ConnectorCreateForm setConnectorDetails(String connectName, String configJson) { + setName(connectName); + setConfig(configJson); + return this; + } - @Step - public ConnectorCreateForm clickSubmitButton() { - clickSubmitBtn(); - waitUntilSpinnerDisappear(); - return this; - } + @Step + public ConnectorCreateForm clickSubmitButton() { + clickSubmitBtn(); + waitUntilSpinnerDisappear(); + return this; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java index fbe1984ce3..de74f67e1c 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java @@ -1,84 +1,84 @@ package com.provectus.kafka.ui.pages.connectors; +import static com.codeborne.selenide.Selenide.$x; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import static com.codeborne.selenide.Selenide.$x; - public class ConnectorDetails extends BasePage { - protected SelenideElement deleteBtn = $x("//li/div[contains(text(),'Delete')]"); - protected SelenideElement confirmBtnMdl = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); - protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); - protected SelenideElement taskTab = $x("//a[contains(text(),'Tasks')]"); - protected SelenideElement configTab = $x("//a[contains(text(),'Config')]"); - protected SelenideElement configField = $x("//div[@id='config']"); - protected String connectorHeaderLocator = "//h1[contains(text(),'%s')]"; + protected SelenideElement deleteBtn = $x("//li/div[contains(text(),'Delete')]"); + protected SelenideElement confirmBtnMdl = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); + protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); + protected SelenideElement taskTab = $x("//a[contains(text(),'Tasks')]"); + protected SelenideElement configTab = $x("//a[contains(text(),'Config')]"); + protected SelenideElement configField = $x("//div[@id='config']"); + protected String connectorHeaderLocator = "//h1[contains(text(),'%s')]"; - @Step - public ConnectorDetails waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - dotMenuBtn.shouldBe(Condition.visible); - return this; - } + @Step + public ConnectorDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + dotMenuBtn.shouldBe(Condition.visible); + return this; + } - @Step - public ConnectorDetails openConfigTab() { - clickByJavaScript(configTab); - return this; - } + @Step + public ConnectorDetails openConfigTab() { + clickByJavaScript(configTab); + return this; + } - @Step - public ConnectorDetails setConfig(String configJson) { - configField.shouldBe(Condition.enabled).click(); - clearByKeyboard(contentTextArea); - contentTextArea.setValue(configJson); - configField.shouldBe(Condition.enabled).click(); - return this; - } + @Step + public ConnectorDetails setConfig(String configJson) { + configField.shouldBe(Condition.enabled).click(); + clearByKeyboard(contentTextArea); + contentTextArea.setValue(configJson); + configField.shouldBe(Condition.enabled).click(); + return this; + } - @Step - public ConnectorDetails clickSubmitButton() { - clickSubmitBtn(); - return this; - } + @Step + public ConnectorDetails clickSubmitButton() { + clickSubmitBtn(); + return this; + } - @Step - public ConnectorDetails openDotMenu() { - clickByJavaScript(dotMenuBtn); - return this; - } + @Step + public ConnectorDetails openDotMenu() { + clickByJavaScript(dotMenuBtn); + return this; + } - @Step - public ConnectorDetails clickDeleteBtn() { - clickByJavaScript(deleteBtn); - return this; - } + @Step + public ConnectorDetails clickDeleteBtn() { + clickByJavaScript(deleteBtn); + return this; + } - @Step - public ConnectorDetails clickConfirmBtn() { - confirmBtnMdl.shouldBe(Condition.enabled).click(); - confirmBtnMdl.shouldBe(Condition.disappear); - return this; - } + @Step + public ConnectorDetails clickConfirmBtn() { + confirmBtnMdl.shouldBe(Condition.enabled).click(); + confirmBtnMdl.shouldBe(Condition.disappear); + return this; + } - @Step - public ConnectorDetails deleteConnector() { - openDotMenu(); - clickDeleteBtn(); - clickConfirmBtn(); - return this; - } + @Step + public ConnectorDetails deleteConnector() { + openDotMenu(); + clickDeleteBtn(); + clickConfirmBtn(); + return this; + } - @Step - public boolean isConnectorHeaderVisible(String connectorName) { - return isVisible($x(String.format(connectorHeaderLocator, connectorName))); - } + @Step + public boolean isConnectorHeaderVisible(String connectorName) { + return isVisible($x(String.format(connectorHeaderLocator, connectorName))); + } - @Step - public boolean isAlertWithMessageVisible(AlertHeader header, String message) { - return isAlertVisible(header, message); - } + @Step + public boolean isAlertWithMessageVisible(AlertHeader header, String message) { + return isAlertVisible(header, message); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java index 6c672855a6..e4b0d94e64 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java @@ -1,44 +1,44 @@ package com.provectus.kafka.ui.pages.connectors; +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import static com.codeborne.selenide.Selenide.$x; -import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT; - public class KafkaConnectList extends BasePage { - protected SelenideElement createConnectorBtn = $x("//button[contains(text(),'Create Connector')]"); + protected SelenideElement createConnectorBtn = $x("//button[contains(text(),'Create Connector')]"); - public KafkaConnectList() { - tableElementNameLocator = "//tbody//td[contains(text(),'%s')]"; - } + public KafkaConnectList() { + tableElementNameLocator = "//tbody//td[contains(text(),'%s')]"; + } - @Step - public KafkaConnectList waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - getPageTitleFromHeader(KAFKA_CONNECT).shouldBe(Condition.visible); - return this; - } + @Step + public KafkaConnectList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(KAFKA_CONNECT).shouldBe(Condition.visible); + return this; + } - @Step - public KafkaConnectList clickCreateConnectorBtn() { - clickByJavaScript(createConnectorBtn); - return this; - } + @Step + public KafkaConnectList clickCreateConnectorBtn() { + clickByJavaScript(createConnectorBtn); + return this; + } - @Step - public KafkaConnectList openConnector(String connectorName) { - getTableElement(connectorName).shouldBe(Condition.enabled).click(); - return this; - } + @Step + public KafkaConnectList openConnector(String connectorName) { + getTableElement(connectorName).shouldBe(Condition.enabled).click(); + return this; + } - @Step - public boolean isConnectorVisible(String connectorName) { - tableGrid.shouldBe(Condition.visible); - return isVisible(getTableElement(connectorName)); - } + @Step + public boolean isConnectorVisible(String connectorName) { + tableGrid.shouldBe(Condition.visible); + return isVisible(getTableElement(connectorName)); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java index 240dc613c4..46025927e5 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java @@ -1,31 +1,31 @@ package com.provectus.kafka.ui.pages.consumers; +import static com.codeborne.selenide.Selenide.$x; + import com.codeborne.selenide.Condition; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import static com.codeborne.selenide.Selenide.$x; - public class ConsumersDetails extends BasePage { - protected String consumerIdHeaderLocator = "//h1[contains(text(),'%s')]"; - protected String topicElementLocator = "//tbody//td//a[text()='%s']"; + protected String consumerIdHeaderLocator = "//h1[contains(text(),'%s')]"; + protected String topicElementLocator = "//tbody//td//a[text()='%s']"; - @Step - public ConsumersDetails waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - tableGrid.shouldBe(Condition.visible); - return this; - } + @Step + public ConsumersDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + tableGrid.shouldBe(Condition.visible); + return this; + } - @Step - public boolean isRedirectedConsumerTitleVisible(String consumerGroupId) { - return isVisible($x(String.format(consumerIdHeaderLocator, consumerGroupId))); - } + @Step + public boolean isRedirectedConsumerTitleVisible(String consumerGroupId) { + return isVisible($x(String.format(consumerIdHeaderLocator, consumerGroupId))); + } - @Step - public boolean isTopicInConsumersDetailsVisible(String topicName) { - tableGrid.shouldBe(Condition.visible); - return isVisible($x(String.format(topicElementLocator, topicName))); - } + @Step + public boolean isTopicInConsumersDetailsVisible(String topicName) { + tableGrid.shouldBe(Condition.visible); + return isVisible($x(String.format(topicElementLocator, topicName))); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java index 35ef404344..bc10b8f238 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java @@ -1,17 +1,17 @@ package com.provectus.kafka.ui.pages.consumers; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.CONSUMERS; + import com.codeborne.selenide.Condition; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.CONSUMERS; - public class ConsumersList extends BasePage { - @Step - public ConsumersList waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - getPageTitleFromHeader(CONSUMERS).shouldBe(Condition.visible); - return this; - } + @Step + public ConsumersList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(CONSUMERS).shouldBe(Condition.visible); + return this; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlDbList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlDbList.java deleted file mode 100644 index e80229d931..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlDbList.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.provectus.kafka.ui.pages.ksqlDb; - -import com.codeborne.selenide.CollectionCondition; -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.pages.BasePage; -import com.provectus.kafka.ui.pages.ksqlDb.enums.KsqlMenuTabs; -import io.qameta.allure.Step; -import org.openqa.selenium.By; - -import java.util.ArrayList; -import java.util.List; - -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$x; -import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB; - -public class KsqlDbList extends BasePage { - - protected SelenideElement executeKsqlBtn = $x("//button[text()='Execute KSQL Request']"); - protected SelenideElement tablesTab = $x("//nav[@role='navigation']/a[text()='Tables']"); - protected SelenideElement streamsTab = $x("//nav[@role='navigation']/a[text()='Streams']"); - - @Step - public KsqlDbList waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - getPageTitleFromHeader(KSQL_DB).shouldBe(Condition.visible); - return this; - } - - @Step - public KsqlDbList clickExecuteKsqlRequestBtn() { - clickByJavaScript(executeKsqlBtn); - return this; - } - - @Step - public KsqlDbList openDetailsTab(KsqlMenuTabs menu) { - $(By.linkText(menu.toString())).shouldBe(Condition.visible).click(); - waitUntilSpinnerDisappear(); - return this; - } - - private List initTablesItems() { - List gridItemList = new ArrayList<>(); - gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new KsqlDbList.KsqlTablesGridItem(item))); - return gridItemList; - } - - @Step - public KsqlDbList.KsqlTablesGridItem getTableByName(String tableName) { - return initTablesItems().stream() - .filter(e -> e.getTableName().equals(tableName)) - .findFirst().orElseThrow(); - } - - private List initStreamsItems() { - List gridItemList = new ArrayList<>(); - gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new KsqlDbList.KsqlStreamsGridItem(item))); - return gridItemList; - } - - @Step - public KsqlDbList.KsqlStreamsGridItem getStreamByName(String streamName) { - return initStreamsItems().stream() - .filter(e -> e.getStreamName().equals(streamName)) - .findFirst().orElseThrow(); - } - - public static class KsqlTablesGridItem extends BasePage { - - private final SelenideElement element; - - public KsqlTablesGridItem(SelenideElement element) { - this.element = element; - } - - @Step - public String getTableName() { - return element.$x("./td[1]").getText().trim(); - } - - @Step - public String getTopicName() { - return element.$x("./td[2]").getText().trim(); - } - - @Step - public String getKeyFormat() { - return element.$x("./td[3]").getText().trim(); - } - - @Step - public String getValueFormat() { - return element.$x("./td[4]").getText().trim(); - } - - @Step - public String getIsWindowed() { - return element.$x("./td[5]").getText().trim(); - } - } - - public static class KsqlStreamsGridItem extends BasePage { - - private final SelenideElement element; - - public KsqlStreamsGridItem(SelenideElement element) { - this.element = element; - } - - @Step - public String getStreamName() { - return element.$x("./td[1]").getText().trim(); - } - - @Step - public String getTopicName() { - return element.$x("./td[2]").getText().trim(); - } - - @Step - public String getKeyFormat() { - return element.$x("./td[3]").getText().trim(); - } - - @Step - public String getValueFormat() { - return element.$x("./td[4]").getText().trim(); - } - - @Step - public String getIsWindowed() { - return element.$x("./td[5]").getText().trim(); - } - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlQueryForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlQueryForm.java deleted file mode 100644 index df915c0098..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/KsqlQueryForm.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.provectus.kafka.ui.pages.ksqlDb; - -import com.codeborne.selenide.CollectionCondition; -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.ElementsCollection; -import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.pages.BasePage; -import io.qameta.allure.Step; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; - -import static com.codeborne.selenide.Condition.visible; -import static com.codeborne.selenide.Selenide.$$x; -import static com.codeborne.selenide.Selenide.$x; - -public class KsqlQueryForm extends BasePage { - protected SelenideElement clearBtn = $x("//div/button[text()='Clear']"); - protected SelenideElement executeBtn = $x("//div/button[text()='Execute']"); - protected SelenideElement stopQueryBtn = $x("//div/button[text()='Stop query']"); - protected SelenideElement clearResultsBtn = $x("//div/button[text()='Clear results']"); - protected SelenideElement addStreamPropertyBtn = $x("//button[text()='Add Stream Property']"); - protected SelenideElement queryAreaValue = $x("//div[@class='ace_content']"); - protected SelenideElement queryArea = $x("//div[@id='ksql']/textarea[@class='ace_text-input']"); - protected ElementsCollection ksqlGridItems = $$x("//tbody//tr"); - protected ElementsCollection keyField = $$x("//input[@aria-label='key']"); - protected ElementsCollection valueField = $$x("//input[@aria-label='value']"); - - @Step - public KsqlQueryForm waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - executeBtn.shouldBe(Condition.visible); - return this; - } - - @Step - public KsqlQueryForm clickClearBtn() { - clickByJavaScript(clearBtn); - return this; - } - - @Step - public KsqlQueryForm clickExecuteBtn() { - clickByActions(executeBtn); - if (queryAreaValue.getText().contains("EMIT CHANGES;")) { - loadingSpinner.shouldBe(Condition.visible); - } else { - waitUntilSpinnerDisappear(); - } - return this; - } - - @Step - public KsqlQueryForm clickStopQueryBtn() { - clickByActions(stopQueryBtn); - waitUntilSpinnerDisappear(); - return this; - } - - @Step - public KsqlQueryForm clickClearResultsBtn() { - clickByActions(clearResultsBtn); - waitUntilSpinnerDisappear(); - return this; - } - - @Step - public KsqlQueryForm clickAddStreamProperty() { - clickByJavaScript(addStreamPropertyBtn); - return this; - } - - @Step - public KsqlQueryForm setQuery(String query) { - queryAreaValue.shouldBe(Condition.visible).click(); - queryArea.setValue(query); - return this; - } - - @Step - public KsqlQueryForm.KsqlResponseGridItem getTableByName(String name) { - return initItems().stream() - .filter(e -> e.getName().equalsIgnoreCase(name)) - .findFirst().orElseThrow(); - } - - @Step - public boolean areResultsVisible() { - boolean visible = false; - try { - visible = initItems().size() > 0; - } catch (Throwable ignored) { - } - return visible; - } - - private List initItems() { - List gridItemList = new ArrayList<>(); - ksqlGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new KsqlQueryForm.KsqlResponseGridItem(item))); - return gridItemList; - } - - public static class KsqlResponseGridItem extends BasePage { - - private final SelenideElement element; - - private KsqlResponseGridItem(SelenideElement element) { - this.element = element; - } - - @Step - public String getType() { - return element.$x("./td[1]").getText().trim(); - } - - @Step - public String getName() { - return element.$x("./td[2]").scrollTo().getText().trim(); - } - - @Step - public boolean isVisible() { - boolean isVisible = false; - try { - element.$x("./td[2]").shouldBe(visible, Duration.ofMillis(500)); - isVisible = true; - } catch (Throwable ignored) { - } - return isVisible; - } - - @Step - public String getTopic() { - return element.$x("./td[3]").getText().trim(); - } - - @Step - public String getKeyFormat() { - return element.$x("./td[4]").getText().trim(); - } - - @Step - public String getValueFormat() { - return element.$x("./td[5]").getText().trim(); - } - - @Step - public String getIsWindowed() { - return element.$x("./td[6]").getText().trim(); - } - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlMenuTabs.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlMenuTabs.java deleted file mode 100644 index bb719dc0f6..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlMenuTabs.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.provectus.kafka.ui.pages.ksqlDb.enums; - -public enum KsqlMenuTabs { - - TABLES("Table"), - STREAMS("Streams"); - - private final String value; - - KsqlMenuTabs(String value) { - this.value = value; - } - - public String toString() { - return value; - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlQueryConfig.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlQueryConfig.java deleted file mode 100644 index 9f85837474..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/enums/KsqlQueryConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.provectus.kafka.ui.pages.ksqlDb.enums; - -public enum KsqlQueryConfig { - - SHOW_TABLES("show tables;"), - SHOW_STREAMS("show streams;"), - SELECT_ALL_FROM("SELECT * FROM %s\n" + - "EMIT CHANGES;"); - - private final String query; - - KsqlQueryConfig(String query) { - this.query = query; - } - - public String getQuery() { - return query; - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlDbList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlDbList.java new file mode 100644 index 0000000000..7eb35d52f3 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlDbList.java @@ -0,0 +1,138 @@ +package com.provectus.kafka.ui.pages.ksqldb; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs; +import io.qameta.allure.Step; +import java.util.ArrayList; +import java.util.List; +import org.openqa.selenium.By; + +public class KsqlDbList extends BasePage { + + protected SelenideElement executeKsqlBtn = $x("//button[text()='Execute KSQL Request']"); + protected SelenideElement tablesTab = $x("//nav[@role='navigation']/a[text()='Tables']"); + protected SelenideElement streamsTab = $x("//nav[@role='navigation']/a[text()='Streams']"); + + @Step + public KsqlDbList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(KSQL_DB).shouldBe(Condition.visible); + return this; + } + + @Step + public KsqlDbList clickExecuteKsqlRequestBtn() { + clickByJavaScript(executeKsqlBtn); + return this; + } + + @Step + public KsqlDbList openDetailsTab(KsqlMenuTabs menu) { + $(By.linkText(menu.toString())).shouldBe(Condition.visible).click(); + waitUntilSpinnerDisappear(); + return this; + } + + private List initTablesItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new KsqlDbList.KsqlTablesGridItem(item))); + return gridItemList; + } + + @Step + public KsqlDbList.KsqlTablesGridItem getTableByName(String tableName) { + return initTablesItems().stream() + .filter(e -> e.getTableName().equals(tableName)) + .findFirst().orElseThrow(); + } + + private List initStreamsItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new KsqlDbList.KsqlStreamsGridItem(item))); + return gridItemList; + } + + @Step + public KsqlDbList.KsqlStreamsGridItem getStreamByName(String streamName) { + return initStreamsItems().stream() + .filter(e -> e.getStreamName().equals(streamName)) + .findFirst().orElseThrow(); + } + + public static class KsqlTablesGridItem extends BasePage { + + private final SelenideElement element; + + public KsqlTablesGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getTableName() { + return element.$x("./td[1]").getText().trim(); + } + + @Step + public String getTopicName() { + return element.$x("./td[2]").getText().trim(); + } + + @Step + public String getKeyFormat() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public String getValueFormat() { + return element.$x("./td[4]").getText().trim(); + } + + @Step + public String getIsWindowed() { + return element.$x("./td[5]").getText().trim(); + } + } + + public static class KsqlStreamsGridItem extends BasePage { + + private final SelenideElement element; + + public KsqlStreamsGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getStreamName() { + return element.$x("./td[1]").getText().trim(); + } + + @Step + public String getTopicName() { + return element.$x("./td[2]").getText().trim(); + } + + @Step + public String getKeyFormat() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public String getValueFormat() { + return element.$x("./td[4]").getText().trim(); + } + + @Step + public String getIsWindowed() { + return element.$x("./td[5]").getText().trim(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java new file mode 100644 index 0000000000..ab24cbe9ab --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java @@ -0,0 +1,153 @@ +package com.provectus.kafka.ui.pages.ksqldb; + +import static com.codeborne.selenide.Condition.visible; +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +public class KsqlQueryForm extends BasePage { + protected SelenideElement clearBtn = $x("//div/button[text()='Clear']"); + protected SelenideElement executeBtn = $x("//div/button[text()='Execute']"); + protected SelenideElement stopQueryBtn = $x("//div/button[text()='Stop query']"); + protected SelenideElement clearResultsBtn = $x("//div/button[text()='Clear results']"); + protected SelenideElement addStreamPropertyBtn = $x("//button[text()='Add Stream Property']"); + protected SelenideElement queryAreaValue = $x("//div[@class='ace_content']"); + protected SelenideElement queryArea = $x("//div[@id='ksql']/textarea[@class='ace_text-input']"); + protected ElementsCollection ksqlGridItems = $$x("//tbody//tr"); + protected ElementsCollection keyField = $$x("//input[@aria-label='key']"); + protected ElementsCollection valueField = $$x("//input[@aria-label='value']"); + + @Step + public KsqlQueryForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + executeBtn.shouldBe(Condition.visible); + return this; + } + + @Step + public KsqlQueryForm clickClearBtn() { + clickByJavaScript(clearBtn); + return this; + } + + @Step + public KsqlQueryForm clickExecuteBtn() { + clickByActions(executeBtn); + if (queryAreaValue.getText().contains("EMIT CHANGES;")) { + loadingSpinner.shouldBe(Condition.visible); + } else { + waitUntilSpinnerDisappear(); + } + return this; + } + + @Step + public KsqlQueryForm clickStopQueryBtn() { + clickByActions(stopQueryBtn); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public KsqlQueryForm clickClearResultsBtn() { + clickByActions(clearResultsBtn); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public KsqlQueryForm clickAddStreamProperty() { + clickByJavaScript(addStreamPropertyBtn); + return this; + } + + @Step + public KsqlQueryForm setQuery(String query) { + queryAreaValue.shouldBe(Condition.visible).click(); + queryArea.setValue(query); + return this; + } + + @Step + public KsqlQueryForm.KsqlResponseGridItem getTableByName(String name) { + return initItems().stream() + .filter(e -> e.getName().equalsIgnoreCase(name)) + .findFirst().orElseThrow(); + } + + @Step + public boolean areResultsVisible() { + boolean visible = false; + try { + visible = initItems().size() > 0; + } catch (Throwable ignored) { + } + return visible; + } + + private List initItems() { + List gridItemList = new ArrayList<>(); + ksqlGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new KsqlQueryForm.KsqlResponseGridItem(item))); + return gridItemList; + } + + public static class KsqlResponseGridItem extends BasePage { + + private final SelenideElement element; + + private KsqlResponseGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getType() { + return element.$x("./td[1]").getText().trim(); + } + + @Step + public String getName() { + return element.$x("./td[2]").scrollTo().getText().trim(); + } + + @Step + public boolean isVisible() { + boolean isVisible = false; + try { + element.$x("./td[2]").shouldBe(visible, Duration.ofMillis(500)); + isVisible = true; + } catch (Throwable ignored) { + } + return isVisible; + } + + @Step + public String getTopic() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public String getKeyFormat() { + return element.$x("./td[4]").getText().trim(); + } + + @Step + public String getValueFormat() { + return element.$x("./td[5]").getText().trim(); + } + + @Step + public String getIsWindowed() { + return element.$x("./td[6]").getText().trim(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlMenuTabs.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlMenuTabs.java new file mode 100644 index 0000000000..016246edb9 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlMenuTabs.java @@ -0,0 +1,17 @@ +package com.provectus.kafka.ui.pages.ksqldb.enums; + +public enum KsqlMenuTabs { + + TABLES("Table"), + STREAMS("Streams"); + + private final String value; + + KsqlMenuTabs(String value) { + this.value = value; + } + + public String toString() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlQueryConfig.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlQueryConfig.java new file mode 100644 index 0000000000..d3cf0ddec2 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlQueryConfig.java @@ -0,0 +1,18 @@ +package com.provectus.kafka.ui.pages.ksqldb.enums; + +public enum KsqlQueryConfig { + + SHOW_TABLES("show tables;"), + SHOW_STREAMS("show streams;"), + SELECT_ALL_FROM("SELECT * FROM %s\n" + "EMIT CHANGES;"); + + private final String query; + + KsqlQueryConfig(String query) { + this.query = query; + } + + public String getQuery() { + return query; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Stream.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Stream.java similarity index 50% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Stream.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Stream.java index 4030a478c4..3583a24366 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Stream.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Stream.java @@ -1,4 +1,4 @@ -package com.provectus.kafka.ui.pages.ksqlDb.models; +package com.provectus.kafka.ui.pages.ksqldb.models; import lombok.Data; import lombok.experimental.Accessors; @@ -7,5 +7,5 @@ import lombok.experimental.Accessors; @Accessors(chain = true) public class Stream { - private String name, topicName, valueFormat, partitions; + private String name, topicName, valueFormat, partitions; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Table.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Table.java similarity index 56% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Table.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Table.java index 1856fffd85..96b3d88ba0 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqlDb/models/Table.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Table.java @@ -1,4 +1,4 @@ -package com.provectus.kafka.ui.pages.ksqlDb.models; +package com.provectus.kafka.ui.pages.ksqldb.models; import lombok.Data; import lombok.experimental.Accessors; @@ -7,5 +7,5 @@ import lombok.experimental.Accessors; @Accessors(chain = true) public class Table { - private String name, streamName; + private String name, streamName; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java index 35490a27d8..ea3cc6ecd1 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java @@ -1,64 +1,63 @@ package com.provectus.kafka.ui.pages.panels; +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.settings.BaseSource.CLUSTER_NAME; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.panels.enums.MenuItem; import io.qameta.allure.Step; - import java.time.Duration; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import static com.codeborne.selenide.Selenide.$x; -import static com.provectus.kafka.ui.settings.BaseSource.CLUSTER_NAME; - public class NaviSideBar extends BasePage { - protected SelenideElement dashboardMenuItem = $x("//a[@title='Dashboard']"); - protected String sideMenuOptionElementLocator = ".//ul/li[contains(.,'%s')]"; - protected String clusterElementLocator = "//aside/ul/li[contains(.,'%s')]"; + protected SelenideElement dashboardMenuItem = $x("//a[@title='Dashboard']"); + protected String sideMenuOptionElementLocator = ".//ul/li[contains(.,'%s')]"; + protected String clusterElementLocator = "//aside/ul/li[contains(.,'%s')]"; - private SelenideElement expandCluster(String clusterName) { - SelenideElement clusterElement = $x(String.format(clusterElementLocator, clusterName)).shouldBe(Condition.visible); - if (clusterElement.parent().$$x(".//ul").size() == 0) { - clickByActions(clusterElement); - } - return clusterElement; + private SelenideElement expandCluster(String clusterName) { + SelenideElement clusterElement = $x(String.format(clusterElementLocator, clusterName)).shouldBe(Condition.visible); + if (clusterElement.parent().$$x(".//ul").size() == 0) { + clickByActions(clusterElement); } + return clusterElement; + } - @Step - public NaviSideBar waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - dashboardMenuItem.shouldBe(Condition.visible, Duration.ofSeconds(30)); - return this; - } + @Step + public NaviSideBar waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + dashboardMenuItem.shouldBe(Condition.visible, Duration.ofSeconds(30)); + return this; + } - @Step - public String getPagePath(MenuItem menuItem) { - return getPagePathFromHeader(menuItem) - .shouldBe(Condition.visible) - .getText().trim(); - } + @Step + public String getPagePath(MenuItem menuItem) { + return getPagePathFromHeader(menuItem) + .shouldBe(Condition.visible) + .getText().trim(); + } - @Step - public NaviSideBar openSideMenu(String clusterName, MenuItem menuItem) { - clickByActions(expandCluster(clusterName).parent() - .$x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle()))); - return this; - } + @Step + public NaviSideBar openSideMenu(String clusterName, MenuItem menuItem) { + clickByActions(expandCluster(clusterName).parent() + .$x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle()))); + return this; + } - @Step - public NaviSideBar openSideMenu(MenuItem menuItem) { - openSideMenu(CLUSTER_NAME, menuItem); - return this; - } + @Step + public NaviSideBar openSideMenu(MenuItem menuItem) { + openSideMenu(CLUSTER_NAME, menuItem); + return this; + } - public List getAllMenuButtons() { - expandCluster(CLUSTER_NAME); - return Stream.of(MenuItem.values()) - .map(menuItem -> $x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle()))) - .collect(Collectors.toList()); - } + public List getAllMenuButtons() { + expandCluster(CLUSTER_NAME); + return Stream.of(MenuItem.values()) + .map(menuItem -> $x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle()))) + .collect(Collectors.toList()); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/TopPanel.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/TopPanel.java index 77cf71a929..805e5b1ee1 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/TopPanel.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/TopPanel.java @@ -1,26 +1,25 @@ package com.provectus.kafka.ui.pages.panels; -import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.pages.BasePage; - -import java.util.Arrays; -import java.util.List; - import static com.codeborne.selenide.Selenide.$x; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import java.util.Arrays; +import java.util.List; + public class TopPanel extends BasePage { - protected SelenideElement kafkaLogo = $x("//a[contains(text(),'UI for Apache Kafka')]"); - protected SelenideElement kafkaVersion = $x("//a[@title='Current commit']"); - protected SelenideElement logOutBtn = $x("//button[contains(text(),'Log out')]"); - protected SelenideElement gitBtn = $x("//a[@href='https://github.com/provectus/kafka-ui']"); - protected SelenideElement discordBtn = $x("//a[contains(@href,'https://discord.com/invite')]"); + protected SelenideElement kafkaLogo = $x("//a[contains(text(),'UI for Apache Kafka')]"); + protected SelenideElement kafkaVersion = $x("//a[@title='Current commit']"); + protected SelenideElement logOutBtn = $x("//button[contains(text(),'Log out')]"); + protected SelenideElement gitBtn = $x("//a[@href='https://github.com/provectus/kafka-ui']"); + protected SelenideElement discordBtn = $x("//a[contains(@href,'https://discord.com/invite')]"); - public List getAllVisibleElements() { - return Arrays.asList(kafkaLogo, kafkaVersion, gitBtn, discordBtn); - } + public List getAllVisibleElements() { + return Arrays.asList(kafkaLogo, kafkaVersion, gitBtn, discordBtn); + } - public List getAllEnabledElements() { - return Arrays.asList(gitBtn, discordBtn, kafkaLogo); - } + public List getAllEnabledElements() { + return Arrays.asList(gitBtn, discordBtn, kafkaLogo); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/enums/MenuItem.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/enums/MenuItem.java index 6610a8293b..993d6070a0 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/enums/MenuItem.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/enums/MenuItem.java @@ -1,28 +1,28 @@ package com.provectus.kafka.ui.pages.panels.enums; public enum MenuItem { - - DASHBOARD("Dashboard", "Dashboard"), - BROKERS("Brokers", "Brokers"), - TOPICS("Topics", "Topics"), - CONSUMERS("Consumers", "Consumers"), - SCHEMA_REGISTRY("Schema Registry", "Schema Registry"), - KAFKA_CONNECT("Kafka Connect", "Connectors"), - KSQL_DB("KSQL DB", "KSQL DB"); - - private final String naviTitle; - private final String pageTitle; - - MenuItem(String naviTitle, String pageTitle) { - this.naviTitle = naviTitle; - this.pageTitle = pageTitle; - } - - public String getNaviTitle() { - return naviTitle; - } - - public String getPageTitle() { - return pageTitle; - } + + DASHBOARD("Dashboard", "Dashboard"), + BROKERS("Brokers", "Brokers"), + TOPICS("Topics", "Topics"), + CONSUMERS("Consumers", "Consumers"), + SCHEMA_REGISTRY("Schema Registry", "Schema Registry"), + KAFKA_CONNECT("Kafka Connect", "Connectors"), + KSQL_DB("KSQL DB", "KSQL DB"); + + private final String naviTitle; + private final String pageTitle; + + MenuItem(String naviTitle, String pageTitle) { + this.naviTitle = naviTitle; + this.pageTitle = pageTitle; + } + + public String getNaviTitle() { + return naviTitle; + } + + public String getPageTitle() { + return pageTitle; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java index 52bfe0971f..374bb42749 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java @@ -1,5 +1,10 @@ package com.provectus.kafka.ui.pages.schemas; +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; +import static org.openqa.selenium.By.id; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.WebDriverRunner; @@ -7,133 +12,130 @@ import com.provectus.kafka.ui.api.model.CompatibilityLevel; import com.provectus.kafka.ui.api.model.SchemaType; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import org.openqa.selenium.Keys; -import org.openqa.selenium.interactions.Actions; - import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; - -import static com.codeborne.selenide.Selenide.*; -import static org.openqa.selenium.By.id; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; public class SchemaCreateForm extends BasePage { - protected SelenideElement schemaNameField = $x("//input[@name='subject']"); - protected SelenideElement pageTitle = $x("//h1['Edit']"); - protected SelenideElement schemaTextArea = $x("//textarea[@name='schema']"); - protected SelenideElement newSchemaInput = $("#newSchema [wrap]"); - protected SelenideElement schemaTypeDdl = $x("//ul[@name='schemaType']"); - protected SelenideElement compatibilityLevelList = $x("//ul[@name='compatibilityLevel']"); - protected SelenideElement newSchemaTextArea = $x("//div[@id='newSchema']"); - protected SelenideElement latestSchemaTextArea = $x("//div[@id='latestSchema']"); - protected SelenideElement leftVersionDdl = $(id("left-select")); - protected SelenideElement rightVersionDdl = $(id("right-select")); - protected List visibleMarkers = $$x("//div[@class='ace_scroller']//div[contains(@class,'codeMarker')]"); - protected List elementsCompareVersionDdl = $$x("//ul[@role='listbox']/ul/li"); - protected String ddlElementLocator = "//li[@value='%s']"; + protected SelenideElement schemaNameField = $x("//input[@name='subject']"); + protected SelenideElement pageTitle = $x("//h1['Edit']"); + protected SelenideElement schemaTextArea = $x("//textarea[@name='schema']"); + protected SelenideElement newSchemaInput = $("#newSchema [wrap]"); + protected SelenideElement schemaTypeDdl = $x("//ul[@name='schemaType']"); + protected SelenideElement compatibilityLevelList = $x("//ul[@name='compatibilityLevel']"); + protected SelenideElement newSchemaTextArea = $x("//div[@id='newSchema']"); + protected SelenideElement latestSchemaTextArea = $x("//div[@id='latestSchema']"); + protected SelenideElement leftVersionDdl = $(id("left-select")); + protected SelenideElement rightVersionDdl = $(id("right-select")); + protected List visibleMarkers = + $$x("//div[@class='ace_scroller']//div[contains(@class,'codeMarker')]"); + protected List elementsCompareVersionDdl = $$x("//ul[@role='listbox']/ul/li"); + protected String ddlElementLocator = "//li[@value='%s']"; - @Step - public SchemaCreateForm waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - pageTitle.shouldBe(Condition.visible); - return this; - } + @Step + public SchemaCreateForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + pageTitle.shouldBe(Condition.visible); + return this; + } - @Step - public SchemaCreateForm setSubjectName(String name) { - schemaNameField.setValue(name); - return this; - } + @Step + public SchemaCreateForm setSubjectName(String name) { + schemaNameField.setValue(name); + return this; + } - @Step - public SchemaCreateForm setSchemaField(String text) { - schemaTextArea.setValue(text); - return this; - } + @Step + public SchemaCreateForm setSchemaField(String text) { + schemaTextArea.setValue(text); + return this; + } - @Step - public SchemaCreateForm selectSchemaTypeFromDropdown(SchemaType schemaType) { - schemaTypeDdl.shouldBe(Condition.enabled).click(); - $x(String.format(ddlElementLocator, schemaType.getValue())).shouldBe(Condition.visible).click(); - return this; - } + @Step + public SchemaCreateForm selectSchemaTypeFromDropdown(SchemaType schemaType) { + schemaTypeDdl.shouldBe(Condition.enabled).click(); + $x(String.format(ddlElementLocator, schemaType.getValue())).shouldBe(Condition.visible).click(); + return this; + } - @Step - public SchemaCreateForm clickSubmitButton() { - clickSubmitBtn(); - return this; - } + @Step + public SchemaCreateForm clickSubmitButton() { + clickSubmitBtn(); + return this; + } - @Step - public SchemaCreateForm selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum level) { - compatibilityLevelList.shouldBe(Condition.enabled).click(); - $x(String.format(ddlElementLocator, level.getValue())).shouldBe(Condition.visible).click(); - return this; - } + @Step + public SchemaCreateForm selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum level) { + compatibilityLevelList.shouldBe(Condition.enabled).click(); + $x(String.format(ddlElementLocator, level.getValue())).shouldBe(Condition.visible).click(); + return this; + } - @Step - public SchemaCreateForm openLeftVersionDdl() { - leftVersionDdl.shouldBe(Condition.enabled).click(); - return this; - } + @Step + public SchemaCreateForm openLeftVersionDdl() { + leftVersionDdl.shouldBe(Condition.enabled).click(); + return this; + } - @Step - public SchemaCreateForm openRightVersionDdl() { - rightVersionDdl.shouldBe(Condition.enabled).click(); - return this; - } + @Step + public SchemaCreateForm openRightVersionDdl() { + rightVersionDdl.shouldBe(Condition.enabled).click(); + return this; + } - @Step - public int getVersionsNumberFromList() { - return elementsCompareVersionDdl.size(); - } + @Step + public int getVersionsNumberFromList() { + return elementsCompareVersionDdl.size(); + } - @Step - public SchemaCreateForm selectVersionFromDropDown(int versionNumberDd) { - $x(String.format(ddlElementLocator, versionNumberDd)).shouldBe(Condition.visible).click(); - return this; - } + @Step + public SchemaCreateForm selectVersionFromDropDown(int versionNumberDd) { + $x(String.format(ddlElementLocator, versionNumberDd)).shouldBe(Condition.visible).click(); + return this; + } - @Step - public int getMarkedLinesNumber() { - return visibleMarkers.size(); - } + @Step + public int getMarkedLinesNumber() { + return visibleMarkers.size(); + } - @Step - public SchemaCreateForm setNewSchemaValue(String configJson) { - newSchemaTextArea.shouldBe(Condition.visible).click(); - newSchemaInput.shouldBe(Condition.enabled); - new Actions(WebDriverRunner.getWebDriver()) - .sendKeys(Keys.PAGE_UP) - .keyDown(Keys.SHIFT) - .sendKeys(Keys.PAGE_DOWN) - .keyUp(Keys.SHIFT) - .sendKeys(Keys.DELETE) - .perform(); - setJsonInputValue(newSchemaInput, configJson); - return this; - } + @Step + public SchemaCreateForm setNewSchemaValue(String configJson) { + newSchemaTextArea.shouldBe(Condition.visible).click(); + newSchemaInput.shouldBe(Condition.enabled); + new Actions(WebDriverRunner.getWebDriver()) + .sendKeys(Keys.PAGE_UP) + .keyDown(Keys.SHIFT) + .sendKeys(Keys.PAGE_DOWN) + .keyUp(Keys.SHIFT) + .sendKeys(Keys.DELETE) + .perform(); + setJsonInputValue(newSchemaInput, configJson); + return this; + } - @Step - public List getAllDetailsPageElements() { - return Stream.of(compatibilityLevelList, newSchemaTextArea, latestSchemaTextArea, submitBtn, schemaTypeDdl) - .collect(Collectors.toList()); - } + @Step + public List getAllDetailsPageElements() { + return Stream.of(compatibilityLevelList, newSchemaTextArea, latestSchemaTextArea, submitBtn, schemaTypeDdl) + .collect(Collectors.toList()); + } - @Step - public boolean isSubmitBtnEnabled() { - return isEnabled(submitBtn); - } + @Step + public boolean isSubmitBtnEnabled() { + return isEnabled(submitBtn); + } - @Step - public boolean isSchemaDropDownEnabled() { - boolean enabled = true; - try { - String attribute = schemaTypeDdl.getAttribute("disabled"); - enabled = false; - } catch (Throwable ignored) { - } - return enabled; + @Step + public boolean isSchemaDropDownEnabled() { + boolean enabled = true; + try { + String attribute = schemaTypeDdl.getAttribute("disabled"); + enabled = false; + } catch (Throwable ignored) { } + return enabled; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java index fc7013d46f..11c2d4a7ba 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java @@ -1,69 +1,69 @@ package com.provectus.kafka.ui.pages.schemas; +import static com.codeborne.selenide.Selenide.$x; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import static com.codeborne.selenide.Selenide.$x; - public class SchemaDetails extends BasePage { - protected SelenideElement actualVersionTextArea = $x("//div[@id='schema']"); - protected SelenideElement compatibilityField = $x("//h4[contains(text(),'Compatibility')]/../p"); - protected SelenideElement editSchemaBtn = $x("//button[contains(text(),'Edit Schema')]"); - protected SelenideElement removeBtn = $x("//*[contains(text(),'Remove')]"); - protected SelenideElement confirmBtn = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); - protected SelenideElement schemaTypeField = $x("//h4[contains(text(),'Type')]/../p"); - protected SelenideElement latestVersionField = $x("//h4[contains(text(),'Latest version')]/../p"); - protected SelenideElement compareVersionBtn = $x("//button[text()='Compare Versions']"); - protected String schemaHeaderLocator = "//h1[contains(text(),'%s')]"; + protected SelenideElement actualVersionTextArea = $x("//div[@id='schema']"); + protected SelenideElement compatibilityField = $x("//h4[contains(text(),'Compatibility')]/../p"); + protected SelenideElement editSchemaBtn = $x("//button[contains(text(),'Edit Schema')]"); + protected SelenideElement removeBtn = $x("//*[contains(text(),'Remove')]"); + protected SelenideElement confirmBtn = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); + protected SelenideElement schemaTypeField = $x("//h4[contains(text(),'Type')]/../p"); + protected SelenideElement latestVersionField = $x("//h4[contains(text(),'Latest version')]/../p"); + protected SelenideElement compareVersionBtn = $x("//button[text()='Compare Versions']"); + protected String schemaHeaderLocator = "//h1[contains(text(),'%s')]"; - @Step - public SchemaDetails waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - actualVersionTextArea.shouldBe(Condition.visible); - return this; - } + @Step + public SchemaDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + actualVersionTextArea.shouldBe(Condition.visible); + return this; + } - @Step - public String getCompatibility() { - return compatibilityField.getText(); - } + @Step + public String getCompatibility() { + return compatibilityField.getText(); + } - @Step - public boolean isSchemaHeaderVisible(String schemaName) { - return isVisible($x(String.format(schemaHeaderLocator, schemaName))); - } + @Step + public boolean isSchemaHeaderVisible(String schemaName) { + return isVisible($x(String.format(schemaHeaderLocator, schemaName))); + } - @Step - public int getLatestVersion() { - return Integer.parseInt(latestVersionField.getText()); - } + @Step + public int getLatestVersion() { + return Integer.parseInt(latestVersionField.getText()); + } - @Step - public String getSchemaType() { - return schemaTypeField.getText(); - } + @Step + public String getSchemaType() { + return schemaTypeField.getText(); + } - @Step - public SchemaDetails openEditSchema() { - editSchemaBtn.shouldBe(Condition.visible).click(); - return this; - } + @Step + public SchemaDetails openEditSchema() { + editSchemaBtn.shouldBe(Condition.visible).click(); + return this; + } - @Step - public SchemaDetails openCompareVersionMenu() { - compareVersionBtn.shouldBe(Condition.enabled).click(); - return this; - } + @Step + public SchemaDetails openCompareVersionMenu() { + compareVersionBtn.shouldBe(Condition.enabled).click(); + return this; + } - @Step - public SchemaDetails removeSchema() { - clickByJavaScript(dotMenuBtn); - removeBtn.shouldBe(Condition.enabled).click(); - confirmBtn.shouldBe(Condition.visible).click(); - confirmBtn.shouldBe(Condition.disappear); - return this; - } + @Step + public SchemaDetails removeSchema() { + clickByJavaScript(dotMenuBtn); + removeBtn.shouldBe(Condition.enabled).click(); + confirmBtn.shouldBe(Condition.visible).click(); + confirmBtn.shouldBe(Condition.disappear); + return this; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java index 4f06bb995b..f2d2f4b98c 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java @@ -1,42 +1,42 @@ package com.provectus.kafka.ui.pages.schemas; +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; -import static com.codeborne.selenide.Selenide.$x; -import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY; - public class SchemaRegistryList extends BasePage { - protected SelenideElement createSchemaBtn = $x("//button[contains(text(),'Create Schema')]"); + protected SelenideElement createSchemaBtn = $x("//button[contains(text(),'Create Schema')]"); - @Step - public SchemaRegistryList waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - getPageTitleFromHeader(SCHEMA_REGISTRY).shouldBe(Condition.visible); - return this; - } + @Step + public SchemaRegistryList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(SCHEMA_REGISTRY).shouldBe(Condition.visible); + return this; + } - @Step - public SchemaRegistryList clickCreateSchema() { - clickByJavaScript(createSchemaBtn); - return this; - } + @Step + public SchemaRegistryList clickCreateSchema() { + clickByJavaScript(createSchemaBtn); + return this; + } - @Step - public SchemaRegistryList openSchema(String schemaName) { - getTableElement(schemaName) - .shouldBe(Condition.enabled).click(); - return this; - } + @Step + public SchemaRegistryList openSchema(String schemaName) { + getTableElement(schemaName) + .shouldBe(Condition.enabled).click(); + return this; + } - @Step - public boolean isSchemaVisible(String schemaName) { - tableGrid.shouldBe(Condition.visible); - return isVisible(getTableElement(schemaName)); - } + @Step + public boolean isSchemaVisible(String schemaName) { + tableGrid.shouldBe(Condition.visible); + return isVisible(getTableElement(schemaName)); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java index 651c514eef..e407d4fe3d 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java @@ -1,57 +1,56 @@ package com.provectus.kafka.ui.pages.topics; +import static com.codeborne.selenide.Selenide.$x; +import static com.codeborne.selenide.Selenide.refresh; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; - import java.util.Arrays; -import static com.codeborne.selenide.Selenide.$x; -import static com.codeborne.selenide.Selenide.refresh; - public class ProduceMessagePanel extends BasePage { - protected SelenideElement keyTextArea = $x("//div[@id='key']/textarea"); - protected SelenideElement contentTextArea = $x("//div[@id='content']/textarea"); - protected SelenideElement headersTextArea = $x("//div[@id='headers']/textarea"); - protected SelenideElement submitBtn = headersTextArea.$x("../../../..//button[@type='submit']"); - protected SelenideElement partitionDdl = $x("//ul[@name='partition']"); - protected SelenideElement keySerdeDdl = $x("//ul[@name='keySerde']"); - protected SelenideElement contentSerdeDdl = $x("//ul[@name='valueSerde']"); + protected SelenideElement keyTextArea = $x("//div[@id='key']/textarea"); + protected SelenideElement contentTextArea = $x("//div[@id='content']/textarea"); + protected SelenideElement headersTextArea = $x("//div[@id='headers']/textarea"); + protected SelenideElement submitBtn = headersTextArea.$x("../../../..//button[@type='submit']"); + protected SelenideElement partitionDdl = $x("//ul[@name='partition']"); + protected SelenideElement keySerdeDdl = $x("//ul[@name='keySerde']"); + protected SelenideElement contentSerdeDdl = $x("//ul[@name='valueSerde']"); - @Step - public ProduceMessagePanel waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - Arrays.asList(partitionDdl, keySerdeDdl, contentSerdeDdl).forEach(element -> element.shouldBe(Condition.visible)); - return this; - } + @Step + public ProduceMessagePanel waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + Arrays.asList(partitionDdl, keySerdeDdl, contentSerdeDdl).forEach(element -> element.shouldBe(Condition.visible)); + return this; + } - @Step - public ProduceMessagePanel setKeyField(String value) { - clearByKeyboard(keyTextArea); - keyTextArea.setValue(value); - return this; - } + @Step + public ProduceMessagePanel setKeyField(String value) { + clearByKeyboard(keyTextArea); + keyTextArea.setValue(value); + return this; + } - @Step - public ProduceMessagePanel setContentFiled(String value) { - clearByKeyboard(contentTextArea); - contentTextArea.setValue(value); - return this; - } + @Step + public ProduceMessagePanel setContentFiled(String value) { + clearByKeyboard(contentTextArea); + contentTextArea.setValue(value); + return this; + } - @Step - public ProduceMessagePanel setHeaderFiled(String value) { - headersTextArea.setValue(value); - return this; - } + @Step + public ProduceMessagePanel setHeaderFiled(String value) { + headersTextArea.setValue(value); + return this; + } - @Step - public ProduceMessagePanel submitProduceMessage() { - clickByActions(submitBtn); - submitBtn.shouldBe(Condition.disappear); - refresh(); - return this; - } + @Step + public ProduceMessagePanel submitProduceMessage() { + clickByActions(submitBtn); + submitBtn.shouldBe(Condition.disappear); + refresh(); + return this; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java index 9c255d5711..57f5930d66 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java @@ -1,6 +1,15 @@ package com.provectus.kafka.ui.pages.topics; -import com.codeborne.selenide.*; +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$; +import static com.codeborne.selenide.Selenide.$x; +import static org.openqa.selenium.By.id; + +import com.codeborne.selenide.ClickOptions; +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue; import com.provectus.kafka.ui.pages.topics.enums.CustomParameterType; @@ -8,267 +17,264 @@ import com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk; import com.provectus.kafka.ui.pages.topics.enums.TimeToRetain; import io.qameta.allure.Step; -import static com.codeborne.selenide.Selenide.*; -import static org.openqa.selenium.By.id; - public class TopicCreateEditForm extends BasePage { - protected SelenideElement timeToRetainField = $x("//input[@id='timeToRetain']"); - protected SelenideElement partitionsField = $x("//input[@name='partitions']"); - protected SelenideElement nameField = $(id("topicFormName")); - protected SelenideElement maxMessageBytesField = $x("//input[@name='maxMessageBytes']"); - protected SelenideElement minInSyncReplicasField = $x("//input[@name='minInSyncReplicas']"); - protected SelenideElement cleanUpPolicyDdl = $x("//ul[@id='topicFormCleanupPolicy']"); - protected SelenideElement maxSizeOnDiscDdl = $x("//ul[@id='topicFormRetentionBytes']"); - protected SelenideElement customParameterDdl = $x("//ul[contains(@name,'customParams')]"); - protected SelenideElement deleteCustomParameterBtn = $x("//span[contains(@title,'Delete customParam')]"); - protected SelenideElement addCustomParameterTypeBtn = $x("//button[contains(text(),'Add Custom Parameter')]"); - protected SelenideElement customParameterValueField = $x("//input[@placeholder='Value']"); - protected SelenideElement validationCustomParameterValueMsg = $x("//p[contains(text(),'Value is required')]"); - protected String ddlElementLocator = "//li[@value='%s']"; - protected String btnTimeToRetainLocator = "//button[@class][text()='%s']"; + protected SelenideElement timeToRetainField = $x("//input[@id='timeToRetain']"); + protected SelenideElement partitionsField = $x("//input[@name='partitions']"); + protected SelenideElement nameField = $(id("topicFormName")); + protected SelenideElement maxMessageBytesField = $x("//input[@name='maxMessageBytes']"); + protected SelenideElement minInSyncReplicasField = $x("//input[@name='minInSyncReplicas']"); + protected SelenideElement cleanUpPolicyDdl = $x("//ul[@id='topicFormCleanupPolicy']"); + protected SelenideElement maxSizeOnDiscDdl = $x("//ul[@id='topicFormRetentionBytes']"); + protected SelenideElement customParameterDdl = $x("//ul[contains(@name,'customParams')]"); + protected SelenideElement deleteCustomParameterBtn = $x("//span[contains(@title,'Delete customParam')]"); + protected SelenideElement addCustomParameterTypeBtn = $x("//button[contains(text(),'Add Custom Parameter')]"); + protected SelenideElement customParameterValueField = $x("//input[@placeholder='Value']"); + protected SelenideElement validationCustomParameterValueMsg = $x("//p[contains(text(),'Value is required')]"); + protected String ddlElementLocator = "//li[@value='%s']"; + protected String btnTimeToRetainLocator = "//button[@class][text()='%s']"; - @Step - public TopicCreateEditForm waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - nameField.shouldBe(Condition.visible); - return this; + @Step + public TopicCreateEditForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + nameField.shouldBe(Condition.visible); + return this; + } + + public boolean isCreateTopicButtonEnabled() { + return isEnabled(submitBtn); + } + + public boolean isDeleteCustomParameterButtonEnabled() { + return isEnabled(deleteCustomParameterBtn); + } + + public boolean isNameFieldEnabled() { + return isEnabled(nameField); + } + + @Step + public TopicCreateEditForm setTopicName(String topicName) { + sendKeysAfterClear(nameField, topicName); + return this; + } + + @Step + public TopicCreateEditForm setMinInsyncReplicas(Integer minInsyncReplicas) { + minInSyncReplicasField.setValue(minInsyncReplicas.toString()); + return this; + } + + @Step + public TopicCreateEditForm setTimeToRetainDataInMs(Long ms) { + timeToRetainField.setValue(ms.toString()); + return this; + } + + @Step + public TopicCreateEditForm setTimeToRetainDataInMs(String ms) { + timeToRetainField.setValue(ms); + return this; + } + + @Step + public TopicCreateEditForm setMaxSizeOnDiskInGB(MaxSizeOnDisk maxSizeOnDisk) { + maxSizeOnDiscDdl.shouldBe(Condition.visible).click(); + $x(String.format(ddlElementLocator, maxSizeOnDisk.getOptionValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicCreateEditForm clickAddCustomParameterTypeButton() { + addCustomParameterTypeBtn.click(); + return this; + } + + @Step + public TopicCreateEditForm openCustomParameterTypeDdl() { + customParameterDdl.shouldBe(Condition.visible).click(); + ddlOptions.shouldHave(CollectionCondition.sizeGreaterThan(0)); + return this; + } + + @Step + public ElementsCollection getAllDdlOptions() { + return getDdlOptions(); + } + + @Step + public TopicCreateEditForm setCustomParameterType(CustomParameterType customParameterType) { + openCustomParameterTypeDdl(); + $x(String.format(ddlElementLocator, customParameterType.getOptionValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicCreateEditForm clearCustomParameterValue() { + clearByKeyboard(customParameterValueField); + return this; + } + + @Step + public TopicCreateEditForm setNumberOfPartitions(int partitions) { + partitionsField.shouldBe(Condition.enabled).clear(); + partitionsField.sendKeys(String.valueOf(partitions)); + return this; + } + + @Step + public TopicCreateEditForm setTimeToRetainDataByButtons(TimeToRetain timeToRetain) { + $x(String.format(btnTimeToRetainLocator, timeToRetain.getButton())).shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicCreateEditForm selectCleanupPolicy(CleanupPolicyValue cleanupPolicyOptionValue) { + cleanUpPolicyDdl.shouldBe(Condition.visible).click(); + $x(String.format(ddlElementLocator, cleanupPolicyOptionValue.getOptionValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicCreateEditForm selectRetentionBytes(String visibleValue) { + return selectFromDropDownByVisibleText("retentionBytes", visibleValue); + } + + @Step + public TopicCreateEditForm selectRetentionBytes(Long optionValue) { + return selectFromDropDownByOptionValue("retentionBytes", optionValue.toString()); + } + + @Step + public TopicCreateEditForm clickSaveTopicBtn() { + clickSubmitBtn(); + return this; + } + + @Step + public TopicCreateEditForm addCustomParameter(String customParameterName, + String customParameterValue) { + ElementsCollection customParametersElements = + $$("ul[role=listbox][name^=customParams][name$=name]"); + KafkaUiSelectElement kafkaUiSelectElement = null; + if (customParametersElements.size() == 1) { + if ("Select".equals(customParametersElements.first().getText())) { + kafkaUiSelectElement = new KafkaUiSelectElement(customParametersElements.first()); + } + } else { + $$("button") + .find(Condition.exactText("Add Custom Parameter")) + .click(); + customParametersElements = $$("ul[role=listbox][name^=customParams][name$=name]"); + kafkaUiSelectElement = new KafkaUiSelectElement(customParametersElements.last()); + } + if (kafkaUiSelectElement != null) { + kafkaUiSelectElement.selectByVisibleText(customParameterName); + } + $(String.format("input[name=\"customParams.%d.value\"]", customParametersElements.size() - 1)) + .setValue(customParameterValue); + return this; + } + + @Step + public TopicCreateEditForm updateCustomParameter(String customParameterName, + String customParameterValue) { + SelenideElement selenideElement = $$("ul[role=listbox][name^=customParams][name$=name]") + .find(Condition.exactText(customParameterName)); + String name = selenideElement.getAttribute("name"); + if (name != null) { + name = name.substring(0, name.lastIndexOf(".")); + } + $(String.format("input[name^=%s]", name)).setValue(customParameterValue); + return this; + } + + @Step + public String getCleanupPolicy() { + return new KafkaUiSelectElement("cleanupPolicy").getCurrentValue(); + } + + @Step + public String getTimeToRetain() { + return timeToRetainField.getValue(); + } + + @Step + public String getMaxSizeOnDisk() { + return new KafkaUiSelectElement("retentionBytes").getCurrentValue(); + } + + @Step + public String getMaxMessageBytes() { + return maxMessageBytesField.getValue(); + } + + @Step + public TopicCreateEditForm setMaxMessageBytes(Long bytes) { + maxMessageBytesField.setValue(bytes.toString()); + return this; + } + + @Step + public TopicCreateEditForm setMaxMessageBytes(String bytes) { + return setMaxMessageBytes(Long.parseLong(bytes)); + } + + @Step + public boolean isValidationMessageCustomParameterValueVisible() { + return isVisible(validationCustomParameterValueMsg); + } + + @Step + public String getCustomParameterValue() { + return customParameterValueField.getValue(); + } + + private TopicCreateEditForm selectFromDropDownByOptionValue(String dropDownElementName, + String optionValue) { + KafkaUiSelectElement select = new KafkaUiSelectElement(dropDownElementName); + select.selectByOptionValue(optionValue); + return this; + } + + private TopicCreateEditForm selectFromDropDownByVisibleText(String dropDownElementName, + String visibleText) { + KafkaUiSelectElement select = new KafkaUiSelectElement(dropDownElementName); + select.selectByVisibleText(visibleText); + return this; + } + + private static class KafkaUiSelectElement { + + private final SelenideElement selectElement; + + public KafkaUiSelectElement(String selectElementName) { + this.selectElement = $("ul[role=listbox][name=" + selectElementName + "]"); } - public boolean isCreateTopicButtonEnabled() { - return isEnabled(submitBtn); + public KafkaUiSelectElement(SelenideElement selectElement) { + this.selectElement = selectElement; } - public boolean isDeleteCustomParameterButtonEnabled() { - return isEnabled(deleteCustomParameterBtn); + public void selectByOptionValue(String optionValue) { + selectElement.click(); + selectElement + .$$x(".//ul/li[@role='option']") + .find(Condition.attribute("value", optionValue)) + .click(ClickOptions.usingJavaScript()); } - public boolean isNameFieldEnabled() { - return isEnabled(nameField); + public void selectByVisibleText(String visibleText) { + selectElement.click(); + selectElement + .$$("ul>li[role=option]") + .find(Condition.exactText(visibleText)) + .click(); } - @Step - public TopicCreateEditForm setTopicName(String topicName) { - sendKeysAfterClear(nameField, topicName); - return this; - } - - @Step - public TopicCreateEditForm setMinInsyncReplicas(Integer minInsyncReplicas) { - minInSyncReplicasField.setValue(minInsyncReplicas.toString()); - return this; - } - - @Step - public TopicCreateEditForm setTimeToRetainDataInMs(Long ms) { - timeToRetainField.setValue(ms.toString()); - return this; - } - - @Step - public TopicCreateEditForm setTimeToRetainDataInMs(String ms) { - timeToRetainField.setValue(ms); - return this; - } - - @Step - public TopicCreateEditForm setMaxSizeOnDiskInGB(MaxSizeOnDisk MaxSizeOnDisk) { - maxSizeOnDiscDdl.shouldBe(Condition.visible).click(); - $x(String.format(ddlElementLocator, MaxSizeOnDisk.getOptionValue())).shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicCreateEditForm clickAddCustomParameterTypeButton() { - addCustomParameterTypeBtn.click(); - return this; - } - - @Step - public TopicCreateEditForm openCustomParameterTypeDdl() { - customParameterDdl.shouldBe(Condition.visible).click(); - ddlOptions.shouldHave(CollectionCondition.sizeGreaterThan(0)); - return this; - } - - @Step - public ElementsCollection getAllDdlOptions() { - return getDdlOptions(); - } - - @Step - public TopicCreateEditForm setCustomParameterType(CustomParameterType customParameterType) { - openCustomParameterTypeDdl(); - $x(String.format(ddlElementLocator, customParameterType.getOptionValue())).shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicCreateEditForm clearCustomParameterValue() { - clearByKeyboard(customParameterValueField); - return this; - } - - @Step - public TopicCreateEditForm setNumberOfPartitions(int partitions) { - partitionsField.shouldBe(Condition.enabled).clear(); - partitionsField.sendKeys(String.valueOf(partitions)); - return this; - } - - @Step - public TopicCreateEditForm setTimeToRetainDataByButtons(TimeToRetain timeToRetain) { - $x(String.format(btnTimeToRetainLocator, timeToRetain.getButton())).shouldBe(Condition.enabled).click(); - return this; - } - - @Step - public TopicCreateEditForm selectCleanupPolicy(CleanupPolicyValue cleanupPolicyOptionValue) { - cleanUpPolicyDdl.shouldBe(Condition.visible).click(); - $x(String.format(ddlElementLocator, cleanupPolicyOptionValue.getOptionValue())).shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicCreateEditForm selectRetentionBytes(String visibleValue) { - return selectFromDropDownByVisibleText("retentionBytes", visibleValue); - } - - @Step - public TopicCreateEditForm selectRetentionBytes(Long optionValue) { - return selectFromDropDownByOptionValue("retentionBytes", optionValue.toString()); - } - - @Step - public TopicCreateEditForm clickCreateTopicBtn() { - clickSubmitBtn(); - return this; - } - - @Step - public TopicCreateEditForm addCustomParameter(String customParameterName, - String customParameterValue) { - ElementsCollection customParametersElements = - $$("ul[role=listbox][name^=customParams][name$=name]"); - KafkaUISelectElement kafkaUISelectElement = null; - if (customParametersElements.size() == 1) { - if ("Select".equals(customParametersElements.first().getText())) { - kafkaUISelectElement = new KafkaUISelectElement(customParametersElements.first()); - } - } else { - $$("button") - .find(Condition.exactText("Add Custom Parameter")) - .click(); - customParametersElements = $$("ul[role=listbox][name^=customParams][name$=name]"); - kafkaUISelectElement = new KafkaUISelectElement(customParametersElements.last()); - } - if (kafkaUISelectElement != null) { - kafkaUISelectElement.selectByVisibleText(customParameterName); - } - $(String.format("input[name=\"customParams.%d.value\"]", customParametersElements.size() - 1)) - .setValue(customParameterValue); - return this; - } - - @Step - public TopicCreateEditForm updateCustomParameter(String customParameterName, - String customParameterValue) { - SelenideElement selenideElement = $$("ul[role=listbox][name^=customParams][name$=name]") - .find(Condition.exactText(customParameterName)); - String name = selenideElement.getAttribute("name"); - if (name != null) { - name = name.substring(0, name.lastIndexOf(".")); - } - $(String.format("input[name^=%s]", name)).setValue(customParameterValue); - return this; - } - - @Step - public String getCleanupPolicy() { - return new KafkaUISelectElement("cleanupPolicy").getCurrentValue(); - } - - @Step - public String getTimeToRetain() { - return timeToRetainField.getValue(); - } - - @Step - public String getMaxSizeOnDisk() { - return new KafkaUISelectElement("retentionBytes").getCurrentValue(); - } - - @Step - public String getMaxMessageBytes() { - return maxMessageBytesField.getValue(); - } - - @Step - public TopicCreateEditForm setMaxMessageBytes(Long bytes) { - maxMessageBytesField.setValue(bytes.toString()); - return this; - } - - @Step - public TopicCreateEditForm setMaxMessageBytes(String bytes) { - return setMaxMessageBytes(Long.parseLong(bytes)); - } - - @Step - public boolean isValidationMessageCustomParameterValueVisible() { - return isVisible(validationCustomParameterValueMsg); - } - - @Step - public String getCustomParameterValue() { - return customParameterValueField.getValue(); - } - - private TopicCreateEditForm selectFromDropDownByOptionValue(String dropDownElementName, - String optionValue) { - KafkaUISelectElement select = new KafkaUISelectElement(dropDownElementName); - select.selectByOptionValue(optionValue); - return this; - } - - private TopicCreateEditForm selectFromDropDownByVisibleText(String dropDownElementName, - String visibleText) { - KafkaUISelectElement select = new KafkaUISelectElement(dropDownElementName); - select.selectByVisibleText(visibleText); - return this; - } - - private static class KafkaUISelectElement { - - private final SelenideElement selectElement; - - public KafkaUISelectElement(String selectElementName) { - this.selectElement = $("ul[role=listbox][name=" + selectElementName + "]"); - } - - public KafkaUISelectElement(SelenideElement selectElement) { - this.selectElement = selectElement; - } - - public void selectByOptionValue(String optionValue) { - selectElement.click(); - selectElement - .$$x(".//ul/li[@role='option']") - .find(Condition.attribute("value", optionValue)) - .click(ClickOptions.usingJavaScript()); - } - - public void selectByVisibleText(String visibleText) { - selectElement.click(); - selectElement - .$$("ul>li[role=option]") - .find(Condition.exactText(visibleText)) - .click(); - } - - public String getCurrentValue() { - return selectElement.$("li").getText(); - } + public String getCurrentValue() { + return selectElement.$("li").getText(); } + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java index d9c0105314..a171c728e4 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java @@ -1,462 +1,479 @@ package com.provectus.kafka.ui.pages.topics; +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; +import static com.codeborne.selenide.Selenide.sleep; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW; +import static org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils.nextInt; + import com.codeborne.selenide.CollectionCondition; import com.codeborne.selenide.Condition; import com.codeborne.selenide.ElementsCollection; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; - import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.util.*; - -import static com.codeborne.selenide.Selenide.*; -import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW; -import static org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils.nextInt; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; public class TopicDetails extends BasePage { - protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]")); - protected SelenideElement recreateTopicBtn = $x("//div[text()='Recreate Topic']"); - protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]"); - protected SelenideElement seekTypeDdl = $x("//ul[@id='selectSeekType']/li"); - protected SelenideElement seekTypeField = $x("//label[text()='Seek Type']//..//div/input"); - protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']"); - protected SelenideElement savedFiltersLink = $x("//div[text()='Saved Filters']"); - protected SelenideElement addFilterCodeModalTitle = $x("//label[text()='Filter code']"); - protected SelenideElement addFilterCodeInput = $x("//div[@id='ace-editor']//textarea"); - protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x("//input[@name='saveFilter']"); - protected SelenideElement displayNameInputAddFilterMdl = $x("//input[@placeholder='Enter Name']"); - protected SelenideElement cancelBtnAddFilterMdl = $x("//button[text()='Cancel']"); - protected SelenideElement addFilterBtnAddFilterMdl = $x("//button[text()='Add filter']"); - protected SelenideElement addFiltersBtnMessages = $x("//button[text()='Add Filters']"); - protected SelenideElement selectFilterBtnAddFilterMdl = $x("//button[text()='Select filter']"); - protected SelenideElement editSettingsMenu = $x("//li[@role][contains(text(),'Edit settings')]"); - protected SelenideElement removeTopicBtn = $x("//ul[@role='menu']//div[contains(text(),'Remove Topic')]"); - protected SelenideElement produceMessageBtn = $x("//div//button[text()='Produce Message']"); - protected SelenideElement contentMessageTab = $x("//html//div[@id='root']/div/main//table//p"); - protected SelenideElement cleanUpPolicyField = $x("//div[contains(text(),'Clean Up Policy')]/../span/*"); - protected SelenideElement partitionsField = $x("//div[contains(text(),'Partitions')]/../span"); - protected SelenideElement backToCreateFiltersLink = $x("//div[text()='Back To create filters']"); - protected ElementsCollection messageGridItems = $$x("//tbody//tr"); - protected SelenideElement actualCalendarDate = $x("//div[@class='react-datepicker__current-month']"); - protected SelenideElement previousMonthButton = $x("//button[@aria-label='Previous Month']"); - protected SelenideElement nextMonthButton = $x("//button[@aria-label='Next Month']"); - protected SelenideElement calendarTimeFld = $x("//input[@placeholder='Time']"); - protected String detailsTabLtr = "//nav//a[contains(text(),'%s')]"; - protected String dayCellLtr = "//div[@role='option'][contains(text(),'%d')]"; - protected String seekFilterDdlLocator = "//ul[@id='selectSeekType']/ul/li[text()='%s']"; - protected String savedFilterNameLocator = "//div[@role='savedFilter']/div[contains(text(),'%s')]"; - protected String consumerIdLocator = "//a[@title='%s']"; - protected String topicHeaderLocator = "//h1[contains(text(),'%s')]"; - protected String activeFilterNameLocator = "//div[@data-testid='activeSmartFilter'][contains(text(),'%s')]"; - protected String settingsGridValueLocator = "//tbody/tr/td/span[text()='%s']//ancestor::tr/td[2]/span"; + protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]")); + protected SelenideElement recreateTopicBtn = $x("//div[text()='Recreate Topic']"); + protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]"); + protected SelenideElement overviewTab = $x("//a[contains(text(),'Overview')]"); + protected SelenideElement messagesTab = $x("//a[contains(text(),'Messages')]"); + protected SelenideElement seekTypeDdl = $x("//ul[@id='selectSeekType']//li"); + protected SelenideElement seekTypeField = $x("//label[text()='Seek Type']//..//div/input"); + protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']"); + protected SelenideElement savedFiltersLink = $x("//div[text()='Saved Filters']"); + protected SelenideElement addFilterCodeModalTitle = $x("//label[text()='Filter code']"); + protected SelenideElement addFilterCodeInput = $x("//div[@id='ace-editor']//textarea"); + protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x("//input[@name='saveFilter']"); + protected SelenideElement displayNameInputAddFilterMdl = $x("//input[@placeholder='Enter Name']"); + protected SelenideElement cancelBtnAddFilterMdl = $x("//button[text()='Cancel']"); + protected SelenideElement addFilterBtnAddFilterMdl = $x("//button[text()='Add filter']"); + protected SelenideElement addFiltersBtnMessages = $x("//button[text()='Add Filters']"); + protected SelenideElement selectFilterBtnAddFilterMdl = $x("//button[text()='Select filter']"); + protected SelenideElement editSettingsMenu = $x("//li[@role][contains(text(),'Edit settings')]"); + protected SelenideElement removeTopicBtn = $x("//ul[@role='menu']//div[contains(text(),'Remove Topic')]"); + protected SelenideElement produceMessageBtn = $x("//div//button[text()='Produce Message']"); + protected SelenideElement contentMessageTab = $x("//html//div[@id='root']/div/main//table//p"); + protected SelenideElement cleanUpPolicyField = $x("//div[contains(text(),'Clean Up Policy')]/../span/*"); + protected SelenideElement partitionsField = $x("//div[contains(text(),'Partitions')]/../span"); + protected SelenideElement backToCreateFiltersLink = $x("//div[text()='Back To create filters']"); + protected ElementsCollection messageGridItems = $$x("//tbody//tr"); + protected SelenideElement actualCalendarDate = $x("//div[@class='react-datepicker__current-month']"); + protected SelenideElement previousMonthButton = $x("//button[@aria-label='Previous Month']"); + protected SelenideElement nextMonthButton = $x("//button[@aria-label='Next Month']"); + protected SelenideElement calendarTimeFld = $x("//input[@placeholder='Time']"); + protected String detailsTabLtr = "//nav//a[contains(text(),'%s')]"; + protected String dayCellLtr = "//div[@role='option'][contains(text(),'%d')]"; + protected String seekFilterDdlLocator = "//ul[@id='selectSeekType']/ul/li[text()='%s']"; + protected String savedFilterNameLocator = "//div[@role='savedFilter']/div[contains(text(),'%s')]"; + protected String consumerIdLocator = "//a[@title='%s']"; + protected String topicHeaderLocator = "//h1[contains(text(),'%s')]"; + protected String activeFilterNameLocator = "//div[@data-testid='activeSmartFilter'][contains(text(),'%s')]"; + protected String settingsGridValueLocator = "//tbody/tr/td/span[text()='%s']//ancestor::tr/td[2]/span"; - @Step - public TopicDetails waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - $x(String.format(detailsTabLtr, OVERVIEW)).shouldBe(Condition.visible); - return this; + @Step + public TopicDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + $x(String.format(detailsTabLtr, OVERVIEW)).shouldBe(Condition.visible); + return this; + } + + @Step + public TopicDetails openDetailsTab(TopicMenu menu) { + $x(String.format(detailsTabLtr, menu.toString())).shouldBe(Condition.enabled).click(); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public String getSettingsGridValueByKey(String key) { + return $x(String.format(settingsGridValueLocator, key)).scrollTo().shouldBe(Condition.visible).getText(); + } + + @Step + public TopicDetails openDotMenu() { + clickByJavaScript(dotMenuBtn); + return this; + } + + @Step + public boolean isAlertWithMessageVisible(AlertHeader header, String message) { + return isAlertVisible(header, message); + } + + @Step + public TopicDetails clickEditSettingsMenu() { + editSettingsMenu.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public boolean isConfirmationMdlVisible() { + return isConfirmationModalVisible(); + } + + @Step + public TopicDetails clickClearMessagesMenu() { + clearMessagesBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public boolean isClearMessagesMenuEnabled() { + return !Objects.requireNonNull(clearMessagesBtn.shouldBe(Condition.visible) + .$x("./..").getAttribute("class")) + .contains("disabled"); + } + + @Step + public TopicDetails clickRecreateTopicMenu() { + recreateTopicBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public String getCleanUpPolicy() { + return cleanUpPolicyField.getText(); + } + + @Step + public int getPartitions() { + return Integer.parseInt(partitionsField.getText().trim()); + } + + @Step + public boolean isTopicHeaderVisible(String topicName) { + return isVisible($x(String.format(topicHeaderLocator, topicName))); + } + + @Step + public TopicDetails clickDeleteTopicMenu() { + removeTopicBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicDetails clickConfirmBtnMdl() { + clickConfirmButton(); + return this; + } + + @Step + public TopicDetails clickProduceMessageBtn() { + clickByJavaScript(produceMessageBtn); + return this; + } + + @Step + public TopicDetails selectSeekTypeDdlMessagesTab(String seekTypeName) { + seekTypeDdl.shouldBe(Condition.enabled).click(); + $x(String.format(seekFilterDdlLocator, seekTypeName)).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicDetails setSeekTypeValueFldMessagesTab(String seekTypeValue) { + seekTypeField.shouldBe(Condition.enabled).sendKeys(seekTypeValue); + return this; + } + + @Step + public TopicDetails clickSubmitFiltersBtnMessagesTab() { + clickByJavaScript(submitBtn); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public TopicDetails clickMessagesAddFiltersBtn() { + addFiltersBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicDetails clickNextButton() { + nextBtn.shouldBe(Condition.enabled).click(); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public TopicDetails openSavedFiltersListMdl() { + savedFiltersLink.shouldBe(Condition.enabled).click(); + backToCreateFiltersLink.shouldBe(Condition.visible); + return this; + } + + @Step + public boolean isFilterVisibleAtSavedFiltersMdl(String filterName) { + return isVisible($x(String.format(savedFilterNameLocator, filterName))); + } + + @Step + public TopicDetails selectFilterAtSavedFiltersMdl(String filterName) { + $x(String.format(savedFilterNameLocator, filterName)).shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicDetails clickSelectFilterBtnAtSavedFiltersMdl() { + selectFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); + addFilterCodeModalTitle.shouldBe(Condition.disappear); + return this; + } + + @Step + public TopicDetails waitUntilAddFiltersMdlVisible() { + addFilterCodeModalTitle.shouldBe(Condition.visible); + return this; + } + + @Step + public TopicDetails setFilterCodeFieldAddFilterMdl(String filterCode) { + addFilterCodeInput.shouldBe(Condition.enabled).sendKeys(filterCode); + return this; + } + + @Step + public TopicDetails selectSaveThisFilterCheckboxMdl(boolean select) { + selectElement(saveThisFilterCheckBoxAddFilterMdl, select); + return this; + } + + @Step + public boolean isSaveThisFilterCheckBoxSelected() { + return isSelected(saveThisFilterCheckBoxAddFilterMdl); + } + + @Step + public TopicDetails setDisplayNameFldAddFilterMdl(String displayName) { + displayNameInputAddFilterMdl.shouldBe(Condition.enabled).sendKeys(displayName); + return this; + } + + @Step + public TopicDetails clickAddFilterBtnAndCloseMdl(boolean closeModal) { + addFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); + if (closeModal) { + addFilterCodeModalTitle.shouldBe(Condition.hidden); + } else { + addFilterCodeModalTitle.shouldBe(Condition.visible); + } + return this; + } + + @Step + public boolean isAddFilterBtnAddFilterMdlEnabled() { + return isEnabled(addFilterBtnAddFilterMdl); + } + + @Step + public boolean isBackButtonEnabled() { + return isEnabled(backBtn); + } + + @Step + public boolean isNextButtonEnabled() { + return isEnabled(nextBtn); + } + + @Step + public boolean isActiveFilterVisible(String activeFilterName) { + return isVisible($x(String.format(activeFilterNameLocator, activeFilterName))); + } + + public List getAllAddFilterModalVisibleElements() { + return Arrays.asList(savedFiltersLink, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl, + cancelBtnAddFilterMdl); + } + + public List getAllAddFilterModalEnabledElements() { + return Arrays.asList(displayNameInputAddFilterMdl, cancelBtnAddFilterMdl); + } + + public List getAllAddFilterModalDisabledElements() { + return Collections.singletonList(addFilterBtnAddFilterMdl); + } + + @Step + public TopicDetails openConsumerGroup(String consumerId) { + $x(String.format(consumerIdLocator, consumerId)).click(); + return this; + } + + @Step + public boolean isKeyMessageVisible(String keyMessage) { + return keyMessage.equals($("td[title]").getText()); + } + + @Step + public boolean isContentMessageVisible(String contentMessage) { + return contentMessage.matches(contentMessageTab.getText().trim()); + } + + private void selectYear(int expectedYear) { + while (getActualCalendarDate().getYear() > expectedYear) { + clickByJavaScript(previousMonthButton); + sleep(1000); + if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { + throw new IllegalArgumentException("Unable to select year"); + } + } + } + + private void selectMonth(int expectedMonth) { + while (getActualCalendarDate().getMonthValue() > expectedMonth) { + clickByJavaScript(previousMonthButton); + sleep(1000); + if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { + throw new IllegalArgumentException("Unable to select month"); + } + } + } + + private void selectDay(int expectedDay) { + Objects.requireNonNull($$x(String.format(dayCellLtr, expectedDay)).stream() + .filter(day -> !Objects.requireNonNull(day.getAttribute("class")).contains("outside-month")) + .findFirst().orElseThrow()).shouldBe(Condition.enabled).click(); + } + + private void setTime(LocalDateTime dateTime) { + calendarTimeFld.shouldBe(Condition.enabled) + .sendKeys(String.valueOf(dateTime.getHour()), String.valueOf(dateTime.getMinute())); + } + + @Step + public TopicDetails selectDateAndTimeByCalendar(LocalDateTime dateTime) { + setTime(dateTime); + selectYear(dateTime.getYear()); + selectMonth(dateTime.getMonthValue()); + selectDay(dateTime.getDayOfMonth()); + return this; + } + + private LocalDate getActualCalendarDate() { + String monthAndYearStr = actualCalendarDate.getText().trim(); + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ofPattern("MMMM yyyy")) + .toFormatter(Locale.ENGLISH); + YearMonth yearMonth = formatter.parse(monthAndYearStr, YearMonth::from); + return yearMonth.atDay(1); + } + + @Step + public TopicDetails openCalendarSeekType() { + seekTypeField.shouldBe(Condition.enabled).click(); + actualCalendarDate.shouldBe(Condition.visible); + return this; + } + + @Step + public int getMessageCountAmount() { + return Integer.parseInt(messageAmountCell.getText().trim()); + } + + private List initItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new TopicDetails.MessageGridItem(item))); + return gridItemList; + } + + @Step + public TopicDetails.MessageGridItem getMessageByOffset(int offset) { + return initItems().stream() + .filter(e -> e.getOffset() == offset) + .findFirst().orElseThrow(); + } + + @Step + public List getAllMessages() { + return initItems(); + } + + @Step + public TopicDetails.MessageGridItem getRandomMessage() { + return getMessageByOffset(nextInt(0, initItems().size() - 1)); + } + + public enum TopicMenu { + OVERVIEW("Overview"), + MESSAGES("Messages"), + CONSUMERS("Consumers"), + SETTINGS("Settings"); + + private final String value; + + TopicMenu(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } + + public static class MessageGridItem extends BasePage { + + private final SelenideElement element; + + private MessageGridItem(SelenideElement element) { + this.element = element; } @Step - public TopicDetails openDetailsTab(TopicMenu menu) { - $x(String.format(detailsTabLtr, menu.toString())).shouldBe(Condition.enabled).click(); - waitUntilSpinnerDisappear(); - return this; + public MessageGridItem clickExpand() { + clickByJavaScript(element.$x("./td[1]/span")); + return this; + } + + private SelenideElement getOffsetElm() { + return element.$x("./td[2]"); } @Step - public String getSettingsGridValueByKey(String key) { - return $x(String.format(settingsGridValueLocator, key)).scrollTo().shouldBe(Condition.visible).getText(); + public int getOffset() { + return Integer.parseInt(getOffsetElm().getText().trim()); } @Step - public TopicDetails openDotMenu() { - clickByJavaScript(dotMenuBtn); - return this; + public int getPartition() { + return Integer.parseInt(element.$x("./td[3]").getText().trim()); } @Step - public boolean isAlertWithMessageVisible(AlertHeader header, String message) { - return isAlertVisible(header, message); + public LocalDateTime getTimestamp() { + String timestampValue = element.$x("./td[4]/div").getText().trim(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M/d/yyyy, HH:mm:ss"); + return LocalDateTime.parse(timestampValue, formatter); } @Step - public TopicDetails clickEditSettingsMenu() { - editSettingsMenu.shouldBe(Condition.visible).click(); - return this; + public String getKey() { + return element.$x("./td[5]").getText().trim(); } @Step - public boolean isConfirmationMdlVisible() { - return isConfirmationModalVisible(); + public String getValue() { + return element.$x("./td[6]/span/p").getText().trim(); } @Step - public TopicDetails clickClearMessagesMenu() { - clearMessagesBtn.shouldBe(Condition.visible).click(); - return this; + public MessageGridItem openDotMenu() { + getOffsetElm().hover(); + element.$x("./td[7]/div/button[@aria-label='Dropdown Toggle']") + .shouldBe(Condition.visible).click(); + return this; } @Step - public TopicDetails clickRecreateTopicMenu() { - recreateTopicBtn.shouldBe(Condition.visible).click(); - return this; + public MessageGridItem clickCopyToClipBoard() { + clickByJavaScript(element.$x("./td[7]//li[text() = 'Copy to clipboard']") + .shouldBe(Condition.visible)); + return this; } @Step - public String getCleanUpPolicy() { - return cleanUpPolicyField.getText(); - } - - @Step - public int getPartitions() { - return Integer.parseInt(partitionsField.getText().trim()); - } - - @Step - public boolean isTopicHeaderVisible(String topicName) { - return isVisible($x(String.format(topicHeaderLocator, topicName))); - } - - @Step - public TopicDetails clickDeleteTopicMenu() { - removeTopicBtn.shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicDetails clickConfirmBtnMdl() { - clickConfirmButton(); - return this; - } - - @Step - public TopicDetails clickProduceMessageBtn() { - clickByJavaScript(produceMessageBtn); - return this; - } - - @Step - public TopicDetails selectSeekTypeDdlMessagesTab(String seekTypeName) { - seekTypeDdl.shouldBe(Condition.enabled).click(); - $x(String.format(seekFilterDdlLocator, seekTypeName)).shouldBe(Condition.visible).click(); - return this; - } - - @Step - public TopicDetails setSeekTypeValueFldMessagesTab(String seekTypeValue) { - seekTypeField.shouldBe(Condition.enabled).sendKeys(seekTypeValue); - return this; - } - - @Step - public TopicDetails clickSubmitFiltersBtnMessagesTab() { - clickByJavaScript(submitBtn); - waitUntilSpinnerDisappear(); - return this; - } - - @Step - public TopicDetails clickMessagesAddFiltersBtn() { - addFiltersBtn.shouldBe(Condition.enabled).click(); - return this; - } - - @Step - public TopicDetails clickNextButton() { - nextBtn.shouldBe(Condition.enabled).click(); - waitUntilSpinnerDisappear(); - return this; - } - - @Step - public TopicDetails openSavedFiltersListMdl() { - savedFiltersLink.shouldBe(Condition.enabled).click(); - backToCreateFiltersLink.shouldBe(Condition.visible); - return this; - } - - @Step - public boolean isFilterVisibleAtSavedFiltersMdl(String filterName) { - return isVisible($x(String.format(savedFilterNameLocator, filterName))); - } - - @Step - public TopicDetails selectFilterAtSavedFiltersMdl(String filterName) { - $x(String.format(savedFilterNameLocator, filterName)).shouldBe(Condition.enabled).click(); - return this; - } - - @Step - public TopicDetails clickSelectFilterBtnAtSavedFiltersMdl() { - selectFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); - addFilterCodeModalTitle.shouldBe(Condition.disappear); - return this; - } - - @Step - public TopicDetails waitUntilAddFiltersMdlVisible() { - addFilterCodeModalTitle.shouldBe(Condition.visible); - return this; - } - - @Step - public TopicDetails setFilterCodeFieldAddFilterMdl(String filterCode) { - addFilterCodeInput.shouldBe(Condition.enabled).sendKeys(filterCode); - return this; - } - - @Step - public TopicDetails selectSaveThisFilterCheckboxMdl(boolean select) { - selectElement(saveThisFilterCheckBoxAddFilterMdl, select); - return this; - } - - @Step - public boolean isSaveThisFilterCheckBoxSelected() { - return isSelected(saveThisFilterCheckBoxAddFilterMdl); - } - - @Step - public TopicDetails setDisplayNameFldAddFilterMdl(String displayName) { - displayNameInputAddFilterMdl.shouldBe(Condition.enabled).sendKeys(displayName); - return this; - } - - @Step - public TopicDetails clickAddFilterBtnAndCloseMdl(boolean closeModal) { - addFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); - if (closeModal) { - addFilterCodeModalTitle.shouldBe(Condition.hidden); - } else { - addFilterCodeModalTitle.shouldBe(Condition.visible); - } - return this; - } - - @Step - public boolean isAddFilterBtnAddFilterMdlEnabled() { - return isEnabled(addFilterBtnAddFilterMdl); - } - - @Step - public boolean isBackButtonEnabled() { - return isEnabled(backBtn); - } - - @Step - public boolean isNextButtonEnabled() { - return isEnabled(nextBtn); - } - - @Step - public boolean isActiveFilterVisible(String activeFilterName) { - return isVisible($x(String.format(activeFilterNameLocator, activeFilterName))); - } - - public List getAllAddFilterModalVisibleElements() { - return Arrays.asList(savedFiltersLink, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl, cancelBtnAddFilterMdl); - } - - public List getAllAddFilterModalEnabledElements() { - return Arrays.asList(displayNameInputAddFilterMdl, cancelBtnAddFilterMdl); - } - - public List getAllAddFilterModalDisabledElements() { - return Collections.singletonList(addFilterBtnAddFilterMdl); - } - - @Step - public TopicDetails openConsumerGroup(String consumerId) { - $x(String.format(consumerIdLocator, consumerId)).click(); - return this; - } - - @Step - public boolean isKeyMessageVisible(String keyMessage) { - return keyMessage.equals($("td[title]").getText()); - } - - @Step - public boolean isContentMessageVisible(String contentMessage) { - return contentMessage.matches(contentMessageTab.getText().trim()); - } - - private void selectYear(int expectedYear) { - while (getActualCalendarDate().getYear() > expectedYear) { - clickByJavaScript(previousMonthButton); - sleep(1000); - if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { - throw new IllegalArgumentException("Unable to select year"); - } - } - } - - private void selectMonth(int expectedMonth) { - while (getActualCalendarDate().getMonthValue() > expectedMonth) { - clickByJavaScript(previousMonthButton); - sleep(1000); - if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { - throw new IllegalArgumentException("Unable to select month"); - } - } - } - - private void selectDay(int expectedDay) { - Objects.requireNonNull($$x(String.format(dayCellLtr, expectedDay)).stream() - .filter(day -> !Objects.requireNonNull(day.getAttribute("class")).contains("outside-month")) - .findFirst().orElseThrow()).shouldBe(Condition.enabled).click(); - } - - private void setTime(LocalDateTime dateTime) { - calendarTimeFld.shouldBe(Condition.enabled) - .sendKeys(String.valueOf(dateTime.getHour()), String.valueOf(dateTime.getMinute())); - } - - @Step - public TopicDetails selectDateAndTimeByCalendar(LocalDateTime dateTime) { - setTime(dateTime); - selectYear(dateTime.getYear()); - selectMonth(dateTime.getMonthValue()); - selectDay(dateTime.getDayOfMonth()); - return this; - } - - private LocalDate getActualCalendarDate() { - String monthAndYearStr = actualCalendarDate.getText().trim(); - DateTimeFormatter formatter = new DateTimeFormatterBuilder() - .parseCaseInsensitive() - .append(DateTimeFormatter.ofPattern("MMMM yyyy")) - .toFormatter(Locale.ENGLISH); - YearMonth yearMonth = formatter.parse(monthAndYearStr, YearMonth::from); - return yearMonth.atDay(1); - } - - @Step - public TopicDetails openCalendarSeekType() { - seekTypeField.shouldBe(Condition.enabled).click(); - actualCalendarDate.shouldBe(Condition.visible); - return this; - } - - @Step - public int getMessageCountAmount() { - return Integer.parseInt(messageAmountCell.getText().trim()); - } - - private List initItems() { - List gridItemList = new ArrayList<>(); - gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new TopicDetails.MessageGridItem(item))); - return gridItemList; - } - - @Step - public TopicDetails.MessageGridItem getMessageByOffset(int offset) { - return initItems().stream() - .filter(e -> e.getOffset() == offset) - .findFirst().orElseThrow(); - } - - @Step - public List getAllMessages() { - return initItems(); - } - - @Step - public TopicDetails.MessageGridItem getRandomMessage() { - return getMessageByOffset(nextInt(0, initItems().size() - 1)); - } - - public enum TopicMenu { - OVERVIEW("Overview"), - MESSAGES("Messages"), - CONSUMERS("Consumers"), - SETTINGS("Settings"); - - private final String value; - - TopicMenu(String value) { - this.value = value; - } - - public String toString() { - return value; - } - } - - public static class MessageGridItem extends BasePage { - - private final SelenideElement element; - - private MessageGridItem(SelenideElement element) { - this.element = element; - } - - @Step - public MessageGridItem clickExpand() { - clickByJavaScript(element.$x("./td[1]/span")); - return this; - } - - private SelenideElement getOffsetElm() { - return element.$x("./td[2]"); - } - - @Step - public int getOffset() { - return Integer.parseInt(getOffsetElm().getText().trim()); - } - - @Step - public int getPartition() { - return Integer.parseInt(element.$x("./td[3]").getText().trim()); - } - - @Step - public LocalDateTime getTimestamp() { - String timestampValue = element.$x("./td[4]/div").getText().trim(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M/d/yyyy, HH:mm:ss"); - return LocalDateTime.parse(timestampValue, formatter); - } - - @Step - public String getKey() { - return element.$x("./td[5]").getText().trim(); - } - - @Step - public String getValue() { - return element.$x("./td[6]/span/p").getText().trim(); - } - - @Step - public MessageGridItem openDotMenu() { - getOffsetElm().hover(); - element.$x("./td[7]/div/button[@aria-label='Dropdown Toggle']") - .shouldBe(Condition.visible).click(); - return this; - } - - @Step - public MessageGridItem clickCopyToClipBoard() { - clickByJavaScript(element.$x("./td[7]//li[text() = 'Copy to clipboard']") - .shouldBe(Condition.visible)); - return this; - } - - @Step - public MessageGridItem clickSaveAsFile() { - clickByJavaScript(element.$x("./td[7]//li[text() = 'Save as a file']") - .shouldBe(Condition.visible)); - return this; - } + public MessageGridItem clickSaveAsFile() { + clickByJavaScript(element.$x("./td[7]//li[text() = 'Save as a file']") + .shouldBe(Condition.visible)); + return this; } + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java index 3c0fcac211..c36e842376 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java @@ -1,66 +1,65 @@ package com.provectus.kafka.ui.pages.topics; +import static com.codeborne.selenide.Selenide.$x; + import com.codeborne.selenide.CollectionCondition; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; - import java.util.ArrayList; import java.util.List; -import static com.codeborne.selenide.Selenide.$x; - public class TopicSettingsTab extends BasePage { - protected SelenideElement defaultValueColumnHeaderLocator = $x("//div[text() = 'Default Value']"); + protected SelenideElement defaultValueColumnHeaderLocator = $x("//div[text() = 'Default Value']"); - @Step - public TopicSettingsTab waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - defaultValueColumnHeaderLocator.shouldBe(Condition.visible); - return this; - } + @Step + public TopicSettingsTab waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + defaultValueColumnHeaderLocator.shouldBe(Condition.visible); + return this; + } - private List initGridItems() { - List gridItemList = new ArrayList<>(); - gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new SettingsGridItem(item))); - return gridItemList; - } + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new SettingsGridItem(item))); + return gridItemList; + } - private TopicSettingsTab.SettingsGridItem getItemByKey(String key) { - return initGridItems().stream() - .filter(e -> e.getKey().equals(key)) - .findFirst().orElseThrow(); + private TopicSettingsTab.SettingsGridItem getItemByKey(String key) { + return initGridItems().stream() + .filter(e -> e.getKey().equals(key)) + .findFirst().orElseThrow(); + } + + @Step + public String getValueByKey(String key) { + return getItemByKey(key).getValue(); + } + + public static class SettingsGridItem extends BasePage { + + private final SelenideElement element; + + public SettingsGridItem(SelenideElement element) { + this.element = element; } @Step - public String getValueByKey(String key) { - return getItemByKey(key).getValue(); + public String getKey() { + return element.$x("./td[1]/span").getText().trim(); } - public static class SettingsGridItem extends BasePage { - - private final SelenideElement element; - - public SettingsGridItem(SelenideElement element) { - this.element = element; - } - - @Step - public String getKey() { - return element.$x("./td[1]/span").getText().trim(); - } - - @Step - public String getValue() { - return element.$x("./td[2]/span").getText().trim(); - } - - @Step - public String getDefaultValue() { - return element.$x("./td[3]/span").getText().trim(); - } + @Step + public String getValue() { + return element.$x("./td[2]/span").getText().trim(); } + + @Step + public String getDefaultValue() { + return element.$x("./td[3]/span").getText().trim(); + } + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java index 538f714e83..184178423d 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java @@ -1,282 +1,283 @@ package com.provectus.kafka.ui.pages.topics; +import static com.codeborne.selenide.Condition.visible; +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS; + import com.codeborne.selenide.CollectionCondition; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.provectus.kafka.ui.pages.BasePage; import io.qameta.allure.Step; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import static com.codeborne.selenide.Condition.visible; -import static com.codeborne.selenide.Selenide.$x; -import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS; - public class TopicsList extends BasePage { - protected SelenideElement addTopicBtn = $x("//button[normalize-space(text()) ='Add a Topic']"); - protected SelenideElement searchField = $x("//input[@placeholder='Search by Topic Name']"); - protected SelenideElement showInternalRadioBtn = $x("//input[@name='ShowInternalTopics']"); - protected SelenideElement deleteSelectedTopicsBtn = $x("//button[text()='Delete selected topics']"); - protected SelenideElement copySelectedTopicBtn = $x("//button[text()='Copy selected topic']"); - protected SelenideElement purgeMessagesOfSelectedTopicsBtn = $x("//button[text()='Purge messages of selected topics']"); - protected SelenideElement clearMessagesBtn = $x("//ul[contains(@class ,'open')]//div[text()='Clear Messages']"); - protected SelenideElement recreateTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Recreate Topic']"); - protected SelenideElement removeTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Remove Topic']"); + protected SelenideElement addTopicBtn = $x("//button[normalize-space(text()) ='Add a Topic']"); + protected SelenideElement searchField = $x("//input[@placeholder='Search by Topic Name']"); + protected SelenideElement showInternalRadioBtn = $x("//input[@name='ShowInternalTopics']"); + protected SelenideElement deleteSelectedTopicsBtn = $x("//button[text()='Delete selected topics']"); + protected SelenideElement copySelectedTopicBtn = $x("//button[text()='Copy selected topic']"); + protected SelenideElement purgeMessagesOfSelectedTopicsBtn = + $x("//button[text()='Purge messages of selected topics']"); + protected SelenideElement clearMessagesBtn = $x("//ul[contains(@class ,'open')]//div[text()='Clear Messages']"); + protected SelenideElement recreateTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Recreate Topic']"); + protected SelenideElement removeTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Remove Topic']"); - @Step - public TopicsList waitUntilScreenReady() { - waitUntilSpinnerDisappear(); - getPageTitleFromHeader(TOPICS).shouldBe(visible); - return this; + @Step + public TopicsList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(TOPICS).shouldBe(visible); + return this; + } + + @Step + public TopicsList clickAddTopicBtn() { + clickByJavaScript(addTopicBtn); + return this; + } + + @Step + public boolean isTopicVisible(String topicName) { + tableGrid.shouldBe(visible); + return isVisible(getTableElement(topicName)); + } + + @Step + public boolean isShowInternalRadioBtnSelected() { + return isSelected(showInternalRadioBtn); + } + + @Step + public TopicsList setShowInternalRadioButton(boolean select) { + if (select) { + if (!showInternalRadioBtn.isSelected()) { + clickByJavaScript(showInternalRadioBtn); + waitUntilSpinnerDisappear(1); + } + } else { + if (showInternalRadioBtn.isSelected()) { + clickByJavaScript(showInternalRadioBtn); + waitUntilSpinnerDisappear(1); + } + } + return this; + } + + @Step + public TopicsList openTopic(String topicName) { + getTopicItem(topicName).openItem(); + return this; + } + + @Step + public TopicsList openDotMenuByTopicName(String topicName) { + getTopicItem(topicName).openDotMenu(); + return this; + } + + @Step + public boolean isCopySelectedTopicBtnEnabled() { + return isEnabled(copySelectedTopicBtn); + } + + @Step + public List getActionButtons() { + return Stream.of(deleteSelectedTopicsBtn, copySelectedTopicBtn, purgeMessagesOfSelectedTopicsBtn) + .collect(Collectors.toList()); + } + + @Step + public TopicsList clickCopySelectedTopicBtn() { + copySelectedTopicBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicsList clickPurgeMessagesOfSelectedTopicsBtn() { + purgeMessagesOfSelectedTopicsBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicsList clickClearMessagesBtn() { + clickByJavaScript(clearMessagesBtn.shouldBe(visible)); + return this; + } + + @Step + public TopicsList clickRecreateTopicBtn() { + clickByJavaScript(recreateTopicBtn.shouldBe(visible)); + return this; + } + + @Step + public TopicsList clickRemoveTopicBtn() { + clickByJavaScript(removeTopicBtn.shouldBe(visible)); + return this; + } + + @Step + public TopicsList clickConfirmBtnMdl() { + clickConfirmButton(); + return this; + } + + @Step + public TopicsList clickCancelBtnMdl() { + clickCancelButton(); + return this; + } + + @Step + public boolean isConfirmationMdlVisible() { + return isConfirmationModalVisible(); + } + + @Step + public boolean isAlertWithMessageVisible(AlertHeader header, String message) { + return isAlertVisible(header, message); + } + + private List getVisibleColumnHeaders() { + return Stream.of("Replication Factor", "Number of messages", "Topic Name", "Partitions", "Out of sync replicas", + "Size") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + private List getEnabledColumnHeaders() { + return Stream.of("Topic Name", "Partitions", "Out of sync replicas", "Size") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllVisibleElements() { + List visibleElements = new ArrayList<>(getVisibleColumnHeaders()); + visibleElements.addAll(Arrays.asList(searchField, addTopicBtn, tableGrid)); + visibleElements.addAll(getActionButtons()); + return visibleElements; + } + + @Step + public List getAllEnabledElements() { + List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); + enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn, addTopicBtn)); + return enabledElements; + } + + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new TopicGridItem(item))); + return gridItemList; + } + + @Step + public TopicGridItem getTopicItem(String name) { + TopicGridItem topicGridItem = initGridItems().stream() + .filter(e -> e.getName().equals(name)) + .findFirst().orElse(null); + if (topicGridItem == null) { + searchItem(name); + topicGridItem = initGridItems().stream() + .filter(e -> e.getName().equals(name)) + .findFirst().orElseThrow(); + } + return topicGridItem; + } + + @Step + public TopicGridItem getAnyNonInternalTopic() { + return getNonInternalTopics().stream() + .findAny().orElseThrow(); + } + + @Step + public List getNonInternalTopics() { + return initGridItems().stream() + .filter(e -> !e.isInternal()) + .collect(Collectors.toList()); + } + + @Step + public List getInternalTopics() { + return initGridItems().stream() + .filter(TopicGridItem::isInternal) + .collect(Collectors.toList()); + } + + public static class TopicGridItem extends BasePage { + + private final SelenideElement element; + + public TopicGridItem(SelenideElement element) { + this.element = element; } @Step - public TopicsList clickAddTopicBtn() { - clickByJavaScript(addTopicBtn); - return this; + public TopicsList selectItem(boolean select) { + selectElement(element.$x("./td[1]/input"), select); + return new TopicsList(); + } + + private SelenideElement getNameElm() { + return element.$x("./td[2]"); } @Step - public boolean isTopicVisible(String topicName) { - tableGrid.shouldBe(visible); - return isVisible(getTableElement(topicName)); + public boolean isInternal() { + boolean internal = false; + try { + internal = getNameElm().$x("./a/span").isDisplayed(); + } catch (Throwable ignored) { + } + return internal; } @Step - public boolean isShowInternalRadioBtnSelected() { - return isSelected(showInternalRadioBtn); + public String getName() { + return getNameElm().$x("./a").getAttribute("title"); } @Step - public TopicsList setShowInternalRadioButton(boolean select) { - if (select) { - if (!showInternalRadioBtn.isSelected()) { - clickByJavaScript(showInternalRadioBtn); - waitUntilSpinnerDisappear(1); - } - } else { - if (showInternalRadioBtn.isSelected()) { - clickByJavaScript(showInternalRadioBtn); - waitUntilSpinnerDisappear(1); - } - } - return this; + public void openItem() { + getNameElm().click(); } @Step - public TopicsList openTopic(String topicName) { - getTopicItem(topicName).openItem(); - return this; + public int getPartition() { + return Integer.parseInt(element.$x("./td[3]").getText().trim()); } @Step - public TopicsList openDotMenuByTopicName(String topicName) { - getTopicItem(topicName).openDotMenu(); - return this; + public int getOutOfSyncReplicas() { + return Integer.parseInt(element.$x("./td[4]").getText().trim()); } @Step - public boolean isCopySelectedTopicBtnEnabled() { - return isEnabled(copySelectedTopicBtn); + public int getReplicationFactor() { + return Integer.parseInt(element.$x("./td[5]").getText().trim()); } @Step - public List getActionButtons() { - return Stream.of(deleteSelectedTopicsBtn, copySelectedTopicBtn, purgeMessagesOfSelectedTopicsBtn) - .collect(Collectors.toList()); + public int getNumberOfMessages() { + return Integer.parseInt(element.$x("./td[6]").getText().trim()); } @Step - public TopicsList clickCopySelectedTopicBtn() { - copySelectedTopicBtn.shouldBe(Condition.enabled).click(); - return this; + public int getSize() { + return Integer.parseInt(element.$x("./td[7]").getText().trim()); } @Step - public TopicsList clickPurgeMessagesOfSelectedTopicsBtn() { - purgeMessagesOfSelectedTopicsBtn.shouldBe(Condition.enabled).click(); - return this; - } - - @Step - public TopicsList clickClearMessagesBtn() { - clickByJavaScript(clearMessagesBtn.shouldBe(visible)); - return this; - } - - @Step - public TopicsList clickRecreateTopicBtn() { - clickByJavaScript(recreateTopicBtn.shouldBe(visible)); - return this; - } - - @Step - public TopicsList clickRemoveTopicBtn() { - clickByJavaScript(removeTopicBtn.shouldBe(visible)); - return this; - } - - @Step - public TopicsList clickConfirmBtnMdl() { - clickConfirmButton(); - return this; - } - - @Step - public TopicsList clickCancelBtnMdl() { - clickCancelButton(); - return this; - } - - @Step - public boolean isConfirmationMdlVisible() { - return isConfirmationModalVisible(); - } - - @Step - public boolean isAlertWithMessageVisible(AlertHeader header, String message) { - return isAlertVisible(header, message); - } - - private List getVisibleColumnHeaders() { - return Stream.of("Replication Factor", "Number of messages", "Topic Name", "Partitions", "Out of sync replicas", "Size") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } - - private List getEnabledColumnHeaders() { - return Stream.of("Topic Name", "Partitions", "Out of sync replicas", "Size") - .map(name -> $x(String.format(columnHeaderLocator, name))) - .collect(Collectors.toList()); - } - - @Step - public List getAllVisibleElements() { - List visibleElements = new ArrayList<>(getVisibleColumnHeaders()); - visibleElements.addAll(Arrays.asList(searchField, addTopicBtn, tableGrid)); - visibleElements.addAll(getActionButtons()); - return visibleElements; - } - - @Step - public List getAllEnabledElements() { - List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); - enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn, addTopicBtn)); - return enabledElements; - } - - private List initGridItems() { - List gridItemList = new ArrayList<>(); - gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) - .forEach(item -> gridItemList.add(new TopicGridItem(item))); - return gridItemList; - } - - @Step - public TopicGridItem getTopicItem(String name) { - TopicGridItem topicGridItem = initGridItems().stream() - .filter(e -> e.getName().equals(name)) - .findFirst().orElse(null); - if (topicGridItem == null) { - searchItem(name); - topicGridItem = initGridItems().stream() - .filter(e -> e.getName().equals(name)) - .findFirst().orElseThrow(); - } - return topicGridItem; - } - - @Step - public TopicGridItem getAnyNonInternalTopic() { - return getNonInternalTopics().stream() - .findAny().orElseThrow(); - } - - @Step - public List getNonInternalTopics() { - return initGridItems().stream() - .filter(e -> !e.isInternal()) - .collect(Collectors.toList()); - } - - @Step - public List getInternalTopics() { - return initGridItems().stream() - .filter(TopicGridItem::isInternal) - .collect(Collectors.toList()); - } - - public static class TopicGridItem extends BasePage { - - private final SelenideElement element; - - public TopicGridItem(SelenideElement element) { - this.element = element; - } - - @Step - public TopicsList selectItem(boolean select) { - selectElement(element.$x("./td[1]/input"), select); - return new TopicsList(); - } - - private SelenideElement getNameElm() { - return element.$x("./td[2]"); - } - - @Step - public boolean isInternal() { - boolean internal = false; - try { - internal = getNameElm().$x("./a/span").isDisplayed(); - } catch (Throwable ignored) { - } - return internal; - } - - @Step - public String getName() { - return getNameElm().$x("./a").getAttribute("title"); - } - - @Step - public void openItem() { - getNameElm().click(); - } - - @Step - public int getPartition() { - return Integer.parseInt(element.$x("./td[3]").getText().trim()); - } - - @Step - public int getOutOfSyncReplicas() { - return Integer.parseInt(element.$x("./td[4]").getText().trim()); - } - - @Step - public int getReplicationFactor() { - return Integer.parseInt(element.$x("./td[5]").getText().trim()); - } - - @Step - public int getNumberOfMessages() { - return Integer.parseInt(element.$x("./td[6]").getText().trim()); - } - - @Step - public int getSize() { - return Integer.parseInt(element.$x("./td[7]").getText().trim()); - } - - @Step - public void openDotMenu() { - element.$x("./td[8]//button").click(); - } + public void openDotMenu() { + element.$x("./td[8]//button").click(); } + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java index 48c0c0fbcb..6e4d31a3a2 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java @@ -2,24 +2,24 @@ package com.provectus.kafka.ui.pages.topics.enums; public enum CleanupPolicyValue { - DELETE("delete", "Delete"), - COMPACT("compact", "Compact"), - COMPACT_DELETE("compact,delete", "Compact,Delete"); + DELETE("delete", "Delete"), + COMPACT("compact", "Compact"), + COMPACT_DELETE("compact,delete", "Compact,Delete"); - private final String optionValue; - private final String visibleText; + private final String optionValue; + private final String visibleText; - CleanupPolicyValue(String optionValue, String visibleText) { - this.optionValue = optionValue; - this.visibleText = visibleText; - } + CleanupPolicyValue(String optionValue, String visibleText) { + this.optionValue = optionValue; + this.visibleText = visibleText; + } - public String getOptionValue() { - return optionValue; - } + public String getOptionValue() { + return optionValue; + } - public String getVisibleText() { - return visibleText; - } + public String getVisibleText() { + return visibleText; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java index f4cb5cb951..4ed3e89966 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java @@ -2,36 +2,36 @@ package com.provectus.kafka.ui.pages.topics.enums; public enum CustomParameterType { - COMPRESSION_TYPE("compression.type"), - DELETE_RETENTION_MS("delete.retention.ms"), - FILE_DELETE_DELAY_MS("file.delete.delay.ms"), - FLUSH_MESSAGES("flush.messages"), - FLUSH_MS("flush.ms"), - FOLLOWER_REPLICATION_THROTTLED_REPLICAS("follower.replication.throttled.replicas"), - INDEX_INTERVAL_BYTES("index.interval.bytes"), - LEADER_REPLICATION_THROTTLED_REPLICAS("leader.replication.throttled.replicas"), - MAX_COMPACTION_LAG_MS("max.compaction.lag.ms"), - MESSAGE_DOWNCONVERSION_ENABLE("message.downconversion.enable"), - MESSAGE_FORMAT_VERSION("message.format.version"), - MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS("message.timestamp.difference.max.ms"), - MESSAGE_TIMESTAMP_TYPE("message.timestamp.type"), - MIN_CLEANABLE_DIRTY_RATIO("min.cleanable.dirty.ratio"), - MIN_COMPACTION_LAG_MS("min.compaction.lag.ms"), - PREALLOCATE("preallocate"), - RETENTION_BYTES("retention.bytes"), - SEGMENT_BYTES("segment.bytes"), - SEGMENT_INDEX_BYTES("segment.index.bytes"), - SEGMENT_JITTER_MS("segment.jitter.ms"), - SEGMENT_MS("segment.ms"), - UNCLEAN_LEADER_ELECTION_ENABLE("unclean.leader.election.enable"); + COMPRESSION_TYPE("compression.type"), + DELETE_RETENTION_MS("delete.retention.ms"), + FILE_DELETE_DELAY_MS("file.delete.delay.ms"), + FLUSH_MESSAGES("flush.messages"), + FLUSH_MS("flush.ms"), + FOLLOWER_REPLICATION_THROTTLED_REPLICAS("follower.replication.throttled.replicas"), + INDEX_INTERVAL_BYTES("index.interval.bytes"), + LEADER_REPLICATION_THROTTLED_REPLICAS("leader.replication.throttled.replicas"), + MAX_COMPACTION_LAG_MS("max.compaction.lag.ms"), + MESSAGE_DOWNCONVERSION_ENABLE("message.downconversion.enable"), + MESSAGE_FORMAT_VERSION("message.format.version"), + MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS("message.timestamp.difference.max.ms"), + MESSAGE_TIMESTAMP_TYPE("message.timestamp.type"), + MIN_CLEANABLE_DIRTY_RATIO("min.cleanable.dirty.ratio"), + MIN_COMPACTION_LAG_MS("min.compaction.lag.ms"), + PREALLOCATE("preallocate"), + RETENTION_BYTES("retention.bytes"), + SEGMENT_BYTES("segment.bytes"), + SEGMENT_INDEX_BYTES("segment.index.bytes"), + SEGMENT_JITTER_MS("segment.jitter.ms"), + SEGMENT_MS("segment.ms"), + UNCLEAN_LEADER_ELECTION_ENABLE("unclean.leader.election.enable"); - private final String optionValue; + private final String optionValue; - CustomParameterType(String optionValue) { - this.optionValue = optionValue; - } + CustomParameterType(String optionValue) { + this.optionValue = optionValue; + } - public String getOptionValue() { - return optionValue; - } + public String getOptionValue() { + return optionValue; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java index 8f459eea75..c77c1a9629 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java @@ -2,26 +2,26 @@ package com.provectus.kafka.ui.pages.topics.enums; public enum MaxSizeOnDisk { - NOT_SET("-1", "Not Set"), - SIZE_1_GB("1073741824", "1 GB"), - SIZE_10_GB("10737418240", "10 GB"), - SIZE_20_GB("21474836480", "20 GB"), - SIZE_50_GB("53687091200", "50 GB"); + NOT_SET("-1", "Not Set"), + SIZE_1_GB("1073741824", "1 GB"), + SIZE_10_GB("10737418240", "10 GB"), + SIZE_20_GB("21474836480", "20 GB"), + SIZE_50_GB("53687091200", "50 GB"); - private final String optionValue; - private final String visibleText; + private final String optionValue; + private final String visibleText; - MaxSizeOnDisk(String optionValue, String visibleText) { - this.optionValue = optionValue; - this.visibleText = visibleText; - } + MaxSizeOnDisk(String optionValue, String visibleText) { + this.optionValue = optionValue; + this.visibleText = visibleText; + } - public String getOptionValue() { - return optionValue; - } + public String getOptionValue() { + return optionValue; + } - public String getVisibleText() { - return visibleText; - } + public String getVisibleText() { + return visibleText; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java index c07abdc175..c2768ca240 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java @@ -2,25 +2,25 @@ package com.provectus.kafka.ui.pages.topics.enums; public enum TimeToRetain { - BTN_12_HOURS("12 hours", "43200000"), - BTN_1_DAY("1 day", "86400000"), - BTN_2_DAYS("2 days", "172800000"), - BTN_7_DAYS("7 days", "604800000"), - BTN_4_WEEKS("4 weeks", "2419200000"); + BTN_12_HOURS("12 hours", "43200000"), + BTN_1_DAY("1 day", "86400000"), + BTN_2_DAYS("2 days", "172800000"), + BTN_7_DAYS("7 days", "604800000"), + BTN_4_WEEKS("4 weeks", "2419200000"); - private final String button; - private final String value; + private final String button; + private final String value; - TimeToRetain(String button, String value) { - this.button = button; - this.value = value; - } + TimeToRetain(String button, String value) { + this.button = button; + this.value = value; + } - public String getButton() { - return button; - } + public String getButton() { + return button; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java index 808f0fdbc8..ea08f57fe4 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java @@ -1,272 +1,282 @@ package com.provectus.kafka.ui.services; +import static com.codeborne.selenide.Selenide.sleep; +import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; + import com.fasterxml.jackson.databind.ObjectMapper; import com.provectus.kafka.ui.api.ApiClient; -import com.provectus.kafka.ui.api.api.*; -import com.provectus.kafka.ui.api.model.*; +import com.provectus.kafka.ui.api.api.KafkaConnectApi; +import com.provectus.kafka.ui.api.api.KsqlApi; +import com.provectus.kafka.ui.api.api.MessagesApi; +import com.provectus.kafka.ui.api.api.SchemasApi; +import com.provectus.kafka.ui.api.api.TopicsApi; +import com.provectus.kafka.ui.api.model.CreateTopicMessage; +import com.provectus.kafka.ui.api.model.KsqlCommandV2; +import com.provectus.kafka.ui.api.model.KsqlCommandV2Response; +import com.provectus.kafka.ui.api.model.KsqlResponse; +import com.provectus.kafka.ui.api.model.NewConnector; +import com.provectus.kafka.ui.api.model.NewSchemaSubject; +import com.provectus.kafka.ui.api.model.TopicCreation; import com.provectus.kafka.ui.models.Connector; import com.provectus.kafka.ui.models.Schema; import com.provectus.kafka.ui.models.Topic; -import com.provectus.kafka.ui.pages.ksqlDb.models.Stream; -import com.provectus.kafka.ui.pages.ksqlDb.models.Table; +import com.provectus.kafka.ui.pages.ksqldb.models.Stream; +import com.provectus.kafka.ui.pages.ksqldb.models.Table; import com.provectus.kafka.ui.settings.BaseSource; import io.qameta.allure.Step; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.reactive.function.client.WebClientResponseException; - import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; - -import static com.codeborne.selenide.Selenide.sleep; -import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.reactive.function.client.WebClientResponseException; @Slf4j public class ApiService extends BaseSource { - @SneakyThrows - private TopicsApi topicApi() { - return new TopicsApi(new ApiClient().setBasePath(BASE_API_URL)); - } + @SneakyThrows + private TopicsApi topicApi() { + return new TopicsApi(new ApiClient().setBasePath(BASE_API_URL)); + } - @SneakyThrows - private SchemasApi schemaApi() { - return new SchemasApi(new ApiClient().setBasePath(BASE_API_URL)); - } + @SneakyThrows + private SchemasApi schemaApi() { + return new SchemasApi(new ApiClient().setBasePath(BASE_API_URL)); + } - @SneakyThrows - private KafkaConnectApi connectorApi() { - return new KafkaConnectApi(new ApiClient().setBasePath(BASE_API_URL)); - } + @SneakyThrows + private KafkaConnectApi connectorApi() { + return new KafkaConnectApi(new ApiClient().setBasePath(BASE_API_URL)); + } - @SneakyThrows - private MessagesApi messageApi() { - return new MessagesApi(new ApiClient().setBasePath(BASE_API_URL)); - } + @SneakyThrows + private MessagesApi messageApi() { + return new MessagesApi(new ApiClient().setBasePath(BASE_API_URL)); + } - @SneakyThrows - private KsqlApi ksqlApi() { - return new KsqlApi(new ApiClient().setBasePath(BASE_API_URL)); - } + @SneakyThrows + private KsqlApi ksqlApi() { + return new KsqlApi(new ApiClient().setBasePath(BASE_API_URL)); + } - @SneakyThrows - private void createTopic(String clusterName, String topicName) { - TopicCreation topic = new TopicCreation(); - topic.setName(topicName); - topic.setPartitions(1); - topic.setReplicationFactor(1); - try { - topicApi().createTopic(clusterName, topic).block(); - sleep(2000); - } catch (WebClientResponseException ex) { - ex.printStackTrace(); - } + @SneakyThrows + private void createTopic(String clusterName, String topicName) { + TopicCreation topic = new TopicCreation(); + topic.setName(topicName); + topic.setPartitions(1); + topic.setReplicationFactor(1); + try { + topicApi().createTopic(clusterName, topic).block(); + sleep(2000); + } catch (WebClientResponseException ex) { + ex.printStackTrace(); } + } - @Step - public ApiService createTopic(Topic topic) { - createTopic(CLUSTER_NAME, topic.getName()); - return this; - } + @Step + public ApiService createTopic(Topic topic) { + createTopic(CLUSTER_NAME, topic.getName()); + return this; + } - @SneakyThrows - private void deleteTopic(String clusterName, String topicName) { - try { - topicApi().deleteTopic(clusterName, topicName).block(); - } catch (WebClientResponseException ignore) { - } + @SneakyThrows + private void deleteTopic(String clusterName, String topicName) { + try { + topicApi().deleteTopic(clusterName, topicName).block(); + } catch (WebClientResponseException ignored) { } + } - @Step - public ApiService deleteTopic(String topicName) { - deleteTopic(CLUSTER_NAME, topicName); - return this; - } + @Step + public ApiService deleteTopic(String topicName) { + deleteTopic(CLUSTER_NAME, topicName); + return this; + } - @SneakyThrows - private void createSchema(String clusterName, Schema schema) { - NewSchemaSubject schemaSubject = new NewSchemaSubject(); - schemaSubject.setSubject(schema.getName()); - schemaSubject.setSchema(fileToString(schema.getValuePath())); - schemaSubject.setSchemaType(schema.getType()); - try { - schemaApi().createNewSchema(clusterName, schemaSubject).block(); - } catch (WebClientResponseException ex) { - ex.printStackTrace(); - } + @SneakyThrows + private void createSchema(String clusterName, Schema schema) { + NewSchemaSubject schemaSubject = new NewSchemaSubject(); + schemaSubject.setSubject(schema.getName()); + schemaSubject.setSchema(fileToString(schema.getValuePath())); + schemaSubject.setSchemaType(schema.getType()); + try { + schemaApi().createNewSchema(clusterName, schemaSubject).block(); + } catch (WebClientResponseException ex) { + ex.printStackTrace(); } + } - @Step - public ApiService createSchema(Schema schema) { - createSchema(CLUSTER_NAME, schema); - return this; - } + @Step + public ApiService createSchema(Schema schema) { + createSchema(CLUSTER_NAME, schema); + return this; + } - @SneakyThrows - private void deleteSchema(String clusterName, String schemaName) { - try { - schemaApi().deleteSchema(clusterName, schemaName).block(); - } catch (WebClientResponseException ignore) { - } + @SneakyThrows + private void deleteSchema(String clusterName, String schemaName) { + try { + schemaApi().deleteSchema(clusterName, schemaName).block(); + } catch (WebClientResponseException ignored) { } + } - @Step - public ApiService deleteSchema(String schemaName) { - deleteSchema(CLUSTER_NAME, schemaName); - return this; - } + @Step + public ApiService deleteSchema(String schemaName) { + deleteSchema(CLUSTER_NAME, schemaName); + return this; + } - @SneakyThrows - private void deleteConnector(String clusterName, String connectName, String connectorName) { - try { - connectorApi().deleteConnector(clusterName, connectName, connectorName).block(); - } catch (WebClientResponseException ignore) { - } + @SneakyThrows + private void deleteConnector(String clusterName, String connectName, String connectorName) { + try { + connectorApi().deleteConnector(clusterName, connectName, connectorName).block(); + } catch (WebClientResponseException ignored) { } + } - @Step - public ApiService deleteConnector(String connectName, String connectorName) { - deleteConnector(CLUSTER_NAME, connectName, connectorName); - return this; - } + @Step + public ApiService deleteConnector(String connectName, String connectorName) { + deleteConnector(CLUSTER_NAME, connectName, connectorName); + return this; + } - @Step - public ApiService deleteConnector(String connectorName) { - deleteConnector(CLUSTER_NAME, CONNECT_NAME, connectorName); - return this; - } + @Step + public ApiService deleteConnector(String connectorName) { + deleteConnector(CLUSTER_NAME, CONNECT_NAME, connectorName); + return this; + } - @SneakyThrows - private void createConnector(String clusterName, String connectName, Connector connector) { - NewConnector connectorProperties = new NewConnector(); - connectorProperties.setName(connector.getName()); - Map configMap = new ObjectMapper().readValue(connector.getConfig(), HashMap.class); - connectorProperties.setConfig(configMap); - try { - connectorApi().deleteConnector(clusterName, connectName, connector.getName()).block(); - } catch (WebClientResponseException ignored) { - } - connectorApi().createConnector(clusterName, connectName, connectorProperties).block(); + @SneakyThrows + private void createConnector(String clusterName, String connectName, Connector connector) { + NewConnector connectorProperties = new NewConnector(); + connectorProperties.setName(connector.getName()); + Map configMap = new ObjectMapper().readValue(connector.getConfig(), HashMap.class); + connectorProperties.setConfig(configMap); + try { + connectorApi().deleteConnector(clusterName, connectName, connector.getName()).block(); + } catch (WebClientResponseException ignored) { } + connectorApi().createConnector(clusterName, connectName, connectorProperties).block(); + } - @Step - public ApiService createConnector(String connectName, Connector connector) { - createConnector(CLUSTER_NAME, connectName, connector); - return this; - } + @Step + public ApiService createConnector(String connectName, Connector connector) { + createConnector(CLUSTER_NAME, connectName, connector); + return this; + } - @Step - public ApiService createConnector(Connector connector) { - createConnector(CLUSTER_NAME, CONNECT_NAME, connector); - return this; - } + @Step + public ApiService createConnector(Connector connector) { + createConnector(CLUSTER_NAME, CONNECT_NAME, connector); + return this; + } - @Step - public String getFirstConnectName(String clusterName) { - return Objects.requireNonNull(connectorApi().getConnects(clusterName).blockFirst()).getName(); - } + @Step + public String getFirstConnectName(String clusterName) { + return Objects.requireNonNull(connectorApi().getConnects(clusterName).blockFirst()).getName(); + } - @SneakyThrows - private void sendMessage(String clusterName, Topic topic) { - CreateTopicMessage createMessage = new CreateTopicMessage(); - createMessage.setPartition(0); - createMessage.setKeySerde("String"); - createMessage.setValueSerde("String"); - createMessage.setKey(topic.getMessageKey()); - createMessage.setContent(topic.getMessageContent()); - try { - messageApi().sendTopicMessages(clusterName, topic.getName(), createMessage).block(); - } catch (WebClientResponseException ex) { - ex.getRawStatusCode(); - } + @SneakyThrows + private void sendMessage(String clusterName, Topic topic) { + CreateTopicMessage createMessage = new CreateTopicMessage(); + createMessage.setPartition(0); + createMessage.setKeySerde("String"); + createMessage.setValueSerde("String"); + createMessage.setKey(topic.getMessageKey()); + createMessage.setContent(topic.getMessageContent()); + try { + messageApi().sendTopicMessages(clusterName, topic.getName(), createMessage).block(); + } catch (WebClientResponseException ex) { + ex.getRawStatusCode(); } + } - @Step - public ApiService sendMessage(Topic topic) { - sendMessage(CLUSTER_NAME, topic); - return this; - } + @Step + public ApiService sendMessage(Topic topic) { + sendMessage(CLUSTER_NAME, topic); + return this; + } - @Step - public ApiService createStream(Stream stream) { - KsqlCommandV2Response pipeIdStream = ksqlApi() - .executeKsql(CLUSTER_NAME, new KsqlCommandV2() - .ksql(String.format("CREATE STREAM %s (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE) ", - stream.getName()) - + String.format("WITH (kafka_topic='%s', value_format='json', partitions=1);", - stream.getTopicName()))) - .block(); - assert pipeIdStream != null; - List responseListStream = ksqlApi() - .openKsqlResponsePipe(CLUSTER_NAME, pipeIdStream.getPipeId()) - .collectList() - .block(); - assert Objects.requireNonNull(responseListStream).size() != 0; - return this; - } + @Step + public ApiService createStream(Stream stream) { + KsqlCommandV2Response pipeIdStream = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql(String.format("CREATE STREAM %s (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE) ", + stream.getName()) + + String.format("WITH (kafka_topic='%s', value_format='json', partitions=1);", + stream.getTopicName()))) + .block(); + assert pipeIdStream != null; + List responseListStream = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdStream.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListStream).size() != 0; + return this; + } - @Step - public ApiService createTables(Table firstTable, Table secondTable) { - KsqlCommandV2Response pipeIdTable1 = ksqlApi() - .executeKsql(CLUSTER_NAME, new KsqlCommandV2() - .ksql(String.format("CREATE TABLE %s AS ", firstTable.getName()) - + " SELECT profileId, " - + " LATEST_BY_OFFSET(latitude) AS la, " - + " LATEST_BY_OFFSET(longitude) AS lo " - + String.format(" FROM %s ", firstTable.getStreamName()) - + " GROUP BY profileId " - + " EMIT CHANGES;")) - .block(); - assert pipeIdTable1 != null; - List responseListTable = ksqlApi() - .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable1.getPipeId()) - .collectList() - .block(); - assert Objects.requireNonNull(responseListTable).size() != 0; - KsqlCommandV2Response pipeIdTable2 = ksqlApi() - .executeKsql(CLUSTER_NAME, new KsqlCommandV2() - .ksql(String.format("CREATE TABLE %s AS ", secondTable.getName()) - + " SELECT ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1) AS distanceInMiles, " - + " COLLECT_LIST(profileId) AS riders, " - + " COUNT(*) AS count " - + String.format(" FROM %s ", firstTable.getName()) - + " GROUP BY ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1);")) - .block(); - assert pipeIdTable2 != null; - List responseListTable2 = ksqlApi() - .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable2.getPipeId()) - .collectList() - .block(); - assert Objects.requireNonNull(responseListTable2).size() != 0; - return this; - } + @Step + public ApiService createTables(Table firstTable, Table secondTable) { + KsqlCommandV2Response pipeIdTable1 = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql(String.format("CREATE TABLE %s AS ", firstTable.getName()) + + " SELECT profileId, " + + " LATEST_BY_OFFSET(latitude) AS la, " + + " LATEST_BY_OFFSET(longitude) AS lo " + + String.format(" FROM %s ", firstTable.getStreamName()) + + " GROUP BY profileId " + + " EMIT CHANGES;")) + .block(); + assert pipeIdTable1 != null; + List responseListTable = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable1.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListTable).size() != 0; + KsqlCommandV2Response pipeIdTable2 = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql(String.format("CREATE TABLE %s AS ", secondTable.getName()) + + " SELECT ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1) AS distanceInMiles, " + + " COLLECT_LIST(profileId) AS riders, " + + " COUNT(*) AS count " + + String.format(" FROM %s ", firstTable.getName()) + + " GROUP BY ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1);")) + .block(); + assert pipeIdTable2 != null; + List responseListTable2 = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable2.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListTable2).size() != 0; + return this; + } - @Step - public ApiService insertInto(Stream stream) { - String streamName = stream.getName(); - KsqlCommandV2Response pipeIdInsert = ksqlApi() - .executeKsql(CLUSTER_NAME, new KsqlCommandV2() - .ksql("INSERT INTO " + streamName + " (profileId, latitude, longitude) VALUES ('c2309eec', 37.7877, -122.4205);" - + "INSERT INTO " + streamName + - " (profileId, latitude, longitude) VALUES ('18f4ea86', 37.3903, -122.0643); " - + "INSERT INTO " + streamName + - " (profileId, latitude, longitude) VALUES ('4ab5cbad', 37.3952, -122.0813); " - + "INSERT INTO " + streamName + - " (profileId, latitude, longitude) VALUES ('8b6eae59', 37.3944, -122.0813); " - + "INSERT INTO " + streamName + - " (profileId, latitude, longitude) VALUES ('4a7c7b41', 37.4049, -122.0822); " - + "INSERT INTO " + streamName + - " (profileId, latitude, longitude) VALUES ('4ddad000', 37.7857, -122.4011);")) - .block(); - assert pipeIdInsert != null; - List responseListInsert = ksqlApi() - .openKsqlResponsePipe(CLUSTER_NAME, pipeIdInsert.getPipeId()) - .collectList() - .block(); - assert Objects.requireNonNull(responseListInsert).size() != 0; - return this; - } + @Step + public ApiService insertInto(Stream stream) { + String streamName = stream.getName(); + KsqlCommandV2Response pipeIdInsert = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql("INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('c2309eec', 37.7877, -122.4205);" + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('18f4ea86', 37.3903, -122.0643); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4ab5cbad', 37.3952, -122.0813); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('8b6eae59', 37.3944, -122.0813); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4a7c7b41', 37.4049, -122.0822); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4ddad000', 37.7857, -122.4011);")) + .block(); + assert pipeIdInsert != null; + List responseListInsert = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdInsert.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListInsert).size() != 0; + return this; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java index cbb1bc2345..821f2ba648 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java @@ -1,29 +1,29 @@ package com.provectus.kafka.ui.settings; +import static com.provectus.kafka.ui.variables.Browser.LOCAL; + import com.provectus.kafka.ui.settings.configs.Config; import org.aeonbits.owner.ConfigFactory; -import static com.provectus.kafka.ui.variables.Browser.LOCAL; - public abstract class BaseSource { - public static final String CLUSTER_NAME = "local"; - public static final String CONNECT_NAME = "first"; - private static final String LOCAL_HOST = "localhost"; - private static Config config; - public static final String BROWSER = config().browser(); - public static final String SUITE_NAME = config().suite(); - public static final String BASE_HOST = BROWSER.equals(LOCAL) - ? LOCAL_HOST - : "host.docker.internal"; - public static final String REMOTE_URL = String.format("http://%s:4444/wd/hub", LOCAL_HOST); - public static final String BASE_API_URL = String.format("http://%s:8080", LOCAL_HOST); - public static final String BASE_UI_URL = String.format("http://%s:8080", BASE_HOST); + public static final String CLUSTER_NAME = "local"; + public static final String CONNECT_NAME = "first"; + private static final String LOCAL_HOST = "localhost"; + public static final String REMOTE_URL = String.format("http://%s:4444/wd/hub", LOCAL_HOST); + public static final String BASE_API_URL = String.format("http://%s:8080", LOCAL_HOST); + private static Config config; + public static final String BROWSER = config().browser(); + public static final String BASE_HOST = BROWSER.equals(LOCAL) + ? LOCAL_HOST + : "host.docker.internal"; + public static final String BASE_UI_URL = String.format("http://%s:8080", BASE_HOST); + public static final String SUITE_NAME = config().suite(); - private static Config config() { - if (config == null) { - config = ConfigFactory.create(Config.class, System.getProperties()); - } - return config; + private static Config config() { + if (config == null) { + config = ConfigFactory.create(Config.class, System.getProperties()); } + return config; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java index ef61d7d770..fb9f9c1b19 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java @@ -1,17 +1,17 @@ package com.provectus.kafka.ui.settings.configs; -import org.aeonbits.owner.Config; - import static com.provectus.kafka.ui.variables.Browser.CONTAINER; import static com.provectus.kafka.ui.variables.Suite.CUSTOM; +import org.aeonbits.owner.Config; + public interface Profiles extends Config { - @Key("browser") - @DefaultValue(CONTAINER) - String browser(); + @Key("browser") + @DefaultValue(CONTAINER) + String browser(); - @Key("suite") - @DefaultValue(CUSTOM) - String suite(); + @Key("suite") + @DefaultValue(CUSTOM) + String suite(); } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java index 2b210e91e1..df1c55bc2f 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java @@ -1,96 +1,101 @@ package com.provectus.kafka.ui.settings.drivers; +import static com.codeborne.selenide.Selenide.clearBrowserCookies; +import static com.codeborne.selenide.Selenide.clearBrowserLocalStorage; +import static com.codeborne.selenide.Selenide.refresh; +import static com.provectus.kafka.ui.settings.BaseSource.BROWSER; +import static com.provectus.kafka.ui.settings.BaseSource.REMOTE_URL; +import static com.provectus.kafka.ui.variables.Browser.CONTAINER; +import static com.provectus.kafka.ui.variables.Browser.LOCAL; + import com.codeborne.selenide.Configuration; import com.codeborne.selenide.Selenide; import com.codeborne.selenide.WebDriverRunner; import com.codeborne.selenide.logevents.SelenideLogger; import io.qameta.allure.Step; import io.qameta.allure.selenide.AllureSelenide; +import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.DesiredCapabilities; -import static com.codeborne.selenide.Selenide.*; -import static com.provectus.kafka.ui.settings.BaseSource.BROWSER; -import static com.provectus.kafka.ui.settings.BaseSource.REMOTE_URL; -import static com.provectus.kafka.ui.variables.Browser.CONTAINER; -import static com.provectus.kafka.ui.variables.Browser.LOCAL; - +@Slf4j public abstract class WebDriver { - @Step - public static void browserSetup() { - Configuration.headless = false; - Configuration.browser = "chrome"; - Configuration.browserSize = "1920x1080"; - /**screenshots and savePageSource config is needed for local debug - * optionally can be set as 'false' to not duplicate Allure report - */ - Configuration.screenshots = true; - Configuration.savePageSource = false; - Configuration.pageLoadTimeout = 120000; - ChromeOptions options = new ChromeOptions() - .addArguments("--no-sandbox") - .addArguments("--verbose") - .addArguments("--remote-allow-origins=*") - .addArguments("--disable-dev-shm-usage") - .addArguments("--disable-gpu") - .addArguments("--lang=en_US"); - switch (BROWSER) { - case (LOCAL) -> Configuration.browserCapabilities = options; - case (CONTAINER) -> { - Configuration.remote = REMOTE_URL; - Configuration.remoteConnectionTimeout = 180000; - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability("enableVNC", true); - capabilities.setCapability("enableVideo", false); - Configuration.browserCapabilities = capabilities.merge(options); - } - default -> throw new IllegalStateException("Unexpected value: " + BROWSER); - } + @Step + public static void browserSetup() { + Configuration.headless = false; + Configuration.browser = "chrome"; + Configuration.browserSize = "1920x1080"; + Configuration.screenshots = true; + Configuration.savePageSource = false; + Configuration.pageLoadTimeout = 120000; + ChromeOptions options = new ChromeOptions() + .addArguments("--no-sandbox") + .addArguments("--verbose") + .addArguments("--remote-allow-origins=*") + .addArguments("--disable-dev-shm-usage") + .addArguments("--disable-gpu") + .addArguments("--lang=en_US"); + switch (BROWSER) { + case (LOCAL) -> Configuration.browserCapabilities = options; + case (CONTAINER) -> { + Configuration.remote = REMOTE_URL; + Configuration.remoteConnectionTimeout = 180000; + DesiredCapabilities capabilities = new DesiredCapabilities(); + capabilities.setCapability("enableVNC", true); + capabilities.setCapability("enableVideo", false); + Configuration.browserCapabilities = capabilities.merge(options); + } + default -> throw new IllegalStateException("Unexpected value: " + BROWSER); } + } - private static org.openqa.selenium.WebDriver getWebDriver() { - try { - return WebDriverRunner.getWebDriver(); - } catch (IllegalStateException ex) { - browserSetup(); - Selenide.open(); - return WebDriverRunner.getWebDriver(); - } + private static org.openqa.selenium.WebDriver getWebDriver() { + try { + return WebDriverRunner.getWebDriver(); + } catch (IllegalStateException ex) { + browserSetup(); + Selenide.open(); + return WebDriverRunner.getWebDriver(); } + } - @Step - public static void openUrl(String url) { - org.openqa.selenium.WebDriver driver = getWebDriver(); - if (!driver.getCurrentUrl().equals(url)) driver.get(url); + @Step + public static void openUrl(String url) { + org.openqa.selenium.WebDriver driver = getWebDriver(); + if (!driver.getCurrentUrl().equals(url)) { + driver.get(url); } + } - @Step - public static void browserInit() { - getWebDriver(); - } + @Step + public static void browserInit() { + getWebDriver(); + } - @Step - public static void browserClear() { - clearBrowserLocalStorage(); - clearBrowserCookies(); - refresh(); - } + @Step + public static void browserClear() { + clearBrowserLocalStorage(); + clearBrowserCookies(); + refresh(); + } - @Step - public static void browserQuit() { - org.openqa.selenium.WebDriver driver = null; - try { - driver = WebDriverRunner.getWebDriver(); - } catch (Throwable ignored) { - } - if (driver != null) driver.quit(); + @Step + public static void browserQuit() { + org.openqa.selenium.WebDriver driver = null; + try { + driver = WebDriverRunner.getWebDriver(); + } catch (Throwable ignored) { } + if (driver != null) { + driver.quit(); + } + } - @Step - public static void loggerSetup() { - SelenideLogger.addListener("AllureSelenide", new AllureSelenide() - .screenshots(true) - .savePageSource(false)); - } + @Step + public static void loggerSetup() { + SelenideLogger.addListener("AllureSelenide", new AllureSelenide() + .screenshots(true) + .savePageSource(false)); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java index 74119f8480..61125408c6 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java @@ -1,35 +1,39 @@ package com.provectus.kafka.ui.settings.listeners; +import static java.nio.file.Files.newInputStream; + import com.codeborne.selenide.Screenshots; import io.qameta.allure.Allure; import io.qameta.allure.testng.AllureTestNg; +import java.io.File; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; import org.testng.ITestListener; import org.testng.ITestResult; -import java.io.File; -import java.io.IOException; -import java.util.Objects; - -import static java.nio.file.Files.newInputStream; - +@Slf4j public class AllureListener extends AllureTestNg implements ITestListener { - private void takeScreenshot() { - File screenshot = Screenshots.takeScreenShotAsFile(); - try { - Allure.addAttachment(Objects.requireNonNull(screenshot).getName(), newInputStream(screenshot.toPath())); - } catch (IOException e) { - throw new RuntimeException(e); - } + private void takeScreenshot() { + File screenshot = Screenshots.takeScreenShotAsFile(); + try { + if (screenshot != null) { + Allure.addAttachment(screenshot.getName(), newInputStream(screenshot.toPath())); + } else { + log.warn("Unable to take screenshot"); + } + } catch (IOException e) { + throw new RuntimeException(e); } + } - @Override - public void onTestFailure(ITestResult result) { - takeScreenshot(); - } + @Override + public void onTestFailure(ITestResult result) { + takeScreenshot(); + } - @Override - public void onTestSkipped(ITestResult result) { - takeScreenshot(); - } + @Override + public void onTestSkipped(ITestResult result) { + takeScreenshot(); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java index ca096cd238..81f510f752 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java @@ -7,31 +7,31 @@ import org.testng.TestListenerAdapter; @Slf4j public class LoggerListener extends TestListenerAdapter { - @Override - public void onTestStart(final ITestResult testResult) { - log.info(String.format("\n------------------------------------------------------------------------ " + - "\nTEST STARTED: %s.%s \n------------------------------------------------------------------------ \n", - testResult.getInstanceName(), testResult.getName())); - } + @Override + public void onTestStart(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST STARTED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } - @Override - public void onTestSuccess(final ITestResult testResult) { - log.info(String.format("\n------------------------------------------------------------------------ " + - "\nTEST PASSED: %s.%s \n------------------------------------------------------------------------ \n", - testResult.getInstanceName(), testResult.getName())); - } + @Override + public void onTestSuccess(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST PASSED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } - @Override - public void onTestFailure(final ITestResult testResult) { - log.info(String.format("\n------------------------------------------------------------------------ " + - "\nTEST FAILED: %s.%s \n------------------------------------------------------------------------ \n", - testResult.getInstanceName(), testResult.getName())); - } + @Override + public void onTestFailure(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST FAILED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } - @Override - public void onTestSkipped(final ITestResult testResult) { - log.info(String.format("\n------------------------------------------------------------------------ " + - "\nTEST SKIPPED: %s.%s \n------------------------------------------------------------------------ \n", - testResult.getInstanceName(), testResult.getName())); - } + @Override + public void onTestSkipped(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST SKIPPED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java index c40481c300..ac3f84eba4 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java @@ -1,14 +1,28 @@ package com.provectus.kafka.ui.settings.listeners; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Status; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Suite; +import static io.qase.api.utils.IntegrationUtils.getCaseTitle; + +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import com.provectus.kafka.ui.utilities.qase.annotations.Status; +import com.provectus.kafka.ui.utilities.qase.annotations.Suite; import io.qase.api.QaseClient; import io.qase.api.StepStorage; import io.qase.api.annotation.QaseId; import io.qase.client.ApiClient; import io.qase.client.api.CasesApi; -import io.qase.client.model.*; +import io.qase.client.model.GetCasesFiltersParameter; +import io.qase.client.model.ResultCreateStepsInner; +import io.qase.client.model.TestCase; +import io.qase.client.model.TestCaseCreate; +import io.qase.client.model.TestCaseCreateStepsInner; +import io.qase.client.model.TestCaseListResponse; +import io.qase.client.model.TestCaseListResponseAllOfResult; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.testng.Assert; @@ -16,108 +30,107 @@ import org.testng.ITestListener; import org.testng.ITestResult; import org.testng.TestListenerAdapter; -import java.lang.reflect.Method; -import java.util.*; - -import static io.qase.api.utils.IntegrationUtils.getCaseTitle; - @Slf4j public class QaseCreateListener extends TestListenerAdapter implements ITestListener { - private static final CasesApi QASE_API = getQaseApi(); + private static final CasesApi QASE_API = getQaseApi(); - private static CasesApi getQaseApi() { - ApiClient apiClient = QaseClient.getApiClient(); - apiClient.setApiKey(System.getProperty("QASEIO_API_TOKEN")); - return new CasesApi(apiClient); + private static CasesApi getQaseApi() { + ApiClient apiClient = QaseClient.getApiClient(); + apiClient.setApiKey(System.getProperty("QASEIO_API_TOKEN")); + return new CasesApi(apiClient); + } + + private static int getStatus(Method method) { + if (method.isAnnotationPresent(Status.class)) { + return method.getDeclaredAnnotation(Status.class).status().getValue(); } + return 1; + } - private static int getStatus(Method method) { - if (method.isAnnotationPresent(Status.class)) - return method.getDeclaredAnnotation(Status.class).status().getValue(); - return 1; + private static int getAutomation(Method method) { + if (method.isAnnotationPresent(Automation.class)) { + return method.getDeclaredAnnotation(Automation.class).state().getValue(); } + return 0; + } - private static int getAutomation(Method method) { - if (method.isAnnotationPresent(Automation.class)) - return method.getDeclaredAnnotation(Automation.class).state().getValue(); - return 0; - } - - @SneakyThrows - private static HashMap getCaseTitlesAndIdsFromQase() { - HashMap cases = new HashMap<>(); - boolean getCases = true; - int offSet = 0; - while (getCases) { - getCases = false; - TestCaseListResponse response = QASE_API.getCases(System.getProperty("QASE_PROJECT_CODE"), - new GetCasesFiltersParameter().status(GetCasesFiltersParameter.SERIALIZED_NAME_STATUS), 100, offSet); - TestCaseListResponseAllOfResult result = response.getResult(); - Assert.assertNotNull(result); - List entities = result.getEntities(); - Assert.assertNotNull(entities); - if (entities.size() > 0) { - for (TestCase testCase : entities) { - cases.put(testCase.getId(), testCase.getTitle()); - } - offSet = offSet + 100; - getCases = true; - } + @SneakyThrows + private static HashMap getCaseTitlesAndIdsFromQase() { + HashMap cases = new HashMap<>(); + boolean getCases = true; + int offSet = 0; + while (getCases) { + getCases = false; + TestCaseListResponse response = QASE_API.getCases(System.getProperty("QASE_PROJECT_CODE"), + new GetCasesFiltersParameter().status(GetCasesFiltersParameter.SERIALIZED_NAME_STATUS), 100, offSet); + TestCaseListResponseAllOfResult result = response.getResult(); + Assert.assertNotNull(result); + List entities = result.getEntities(); + Assert.assertNotNull(entities); + if (entities.size() > 0) { + for (TestCase testCase : entities) { + cases.put(testCase.getId(), testCase.getTitle()); } - return cases; + offSet = offSet + 100; + getCases = true; + } } + return cases; + } - private static boolean isCaseWithTitleExistInQase(Method method) { - HashMap cases = getCaseTitlesAndIdsFromQase(); - String title = getCaseTitle(method); - if (cases.containsValue(title)) { - for (Map.Entry map : cases.entrySet()) { - if (map.getValue().matches(title)) { - long id = map.getKey(); - log.warn(String.format("Test case with @QaseTitle='%s' already exists with @QaseId=%d. " + - "Please verify @QaseTitle annotation", title, id)); - return true; - } - } + private static boolean isCaseWithTitleExistInQase(Method method) { + HashMap cases = getCaseTitlesAndIdsFromQase(); + String title = getCaseTitle(method); + if (cases.containsValue(title)) { + for (Map.Entry map : cases.entrySet()) { + if (map.getValue().matches(title)) { + long id = map.getKey(); + log.warn(String.format("Test case with @QaseTitle='%s' already exists with @QaseId=%d. " + + "Please verify @QaseTitle annotation", title, id)); + return true; } - return false; + } } + return false; + } - @Override - @SneakyThrows - public void onTestSuccess(final ITestResult testResult) { - Method method = testResult.getMethod() - .getConstructorOrMethod() - .getMethod(); - String title = getCaseTitle(method); - if (!method.isAnnotationPresent(QaseId.class)) { - if (title != null) { - if (!isCaseWithTitleExistInQase(method)) { - LinkedList resultSteps = StepStorage.stopSteps(); - LinkedList createSteps = new LinkedList<>(); - resultSteps.forEach(step -> { - TestCaseCreateStepsInner caseStep = new TestCaseCreateStepsInner(); - caseStep.setAction(step.getAction()); - caseStep.setExpectedResult(step.getExpectedResult()); - createSteps.add(caseStep); - }); - TestCaseCreate newCase = new TestCaseCreate(); - newCase.setTitle(title); - newCase.setStatus(getStatus(method)); - newCase.setAutomation(getAutomation(method)); - newCase.setSteps(createSteps); - if (method.isAnnotationPresent(Suite.class)) { - long suiteId = method.getDeclaredAnnotation(Suite.class).id(); - newCase.suiteId(suiteId); - } - Long id = Objects.requireNonNull(QASE_API.createCase(System.getProperty("QASE_PROJECT_CODE"), - newCase).getResult()).getId(); - log.info(String.format("New test case '%s' was created with @QaseId=%d", title, id)); - } - } else - log.warn("To create new test case in Qase.io please add @QaseTitle annotation"); - } else - log.warn("To create new test case in Qase.io please remove @QaseId annotation"); + @Override + @SneakyThrows + public void onTestSuccess(final ITestResult testResult) { + Method method = testResult.getMethod() + .getConstructorOrMethod() + .getMethod(); + String title = getCaseTitle(method); + if (!method.isAnnotationPresent(QaseId.class)) { + if (title != null) { + if (!isCaseWithTitleExistInQase(method)) { + LinkedList resultSteps = StepStorage.stopSteps(); + LinkedList createSteps = new LinkedList<>(); + resultSteps.forEach(step -> { + TestCaseCreateStepsInner caseStep = new TestCaseCreateStepsInner(); + caseStep.setAction(step.getAction()); + caseStep.setExpectedResult(step.getExpectedResult()); + createSteps.add(caseStep); + }); + TestCaseCreate newCase = new TestCaseCreate(); + newCase.setTitle(title); + newCase.setStatus(getStatus(method)); + newCase.setAutomation(getAutomation(method)); + newCase.setSteps(createSteps); + if (method.isAnnotationPresent(Suite.class)) { + long suiteId = method.getDeclaredAnnotation(Suite.class).id(); + newCase.suiteId(suiteId); + } + Long id = Objects.requireNonNull(QASE_API.createCase(System.getProperty("QASE_PROJECT_CODE"), + newCase).getResult()).getId(); + log.info(String.format("New test case '%s' was created with @QaseId=%d", title, id)); + } + } else { + log.warn("To create new test case in Qase.io please add @QaseTitle annotation"); + } + } else { + log.warn("To create new test case in Qase.io please remove @QaseId annotation"); } + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java index 72056db922..d413ca4a28 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java @@ -1,5 +1,12 @@ package com.provectus.kafka.ui.settings.listeners; +import static io.qase.api.utils.IntegrationUtils.getCaseId; +import static io.qase.api.utils.IntegrationUtils.getCaseTitle; +import static io.qase.api.utils.IntegrationUtils.getStacktrace; +import static io.qase.client.model.ResultCreate.StatusEnum.FAILED; +import static io.qase.client.model.ResultCreate.StatusEnum.PASSED; +import static io.qase.client.model.ResultCreate.StatusEnum.SKIPPED; + import io.qase.api.StepStorage; import io.qase.api.config.QaseConfig; import io.qase.api.services.QaseTestCaseListener; @@ -7,6 +14,9 @@ import io.qase.client.model.ResultCreate; import io.qase.client.model.ResultCreateCase; import io.qase.client.model.ResultCreateStepsInner; import io.qase.testng.guice.module.TestNgModule; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.Optional; import lombok.AccessLevel; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -15,88 +25,81 @@ import org.testng.ITestListener; import org.testng.ITestResult; import org.testng.TestListenerAdapter; -import java.lang.reflect.Method; -import java.util.LinkedList; -import java.util.Optional; - -import static io.qase.api.utils.IntegrationUtils.*; -import static io.qase.client.model.ResultCreate.StatusEnum.*; - @Slf4j public class QaseResultListener extends TestListenerAdapter implements ITestListener { - private static final String REPORTER_NAME = "TestNG"; + private static final String REPORTER_NAME = "TestNG"; - static { - System.setProperty(QaseConfig.QASE_CLIENT_REPORTER_NAME_KEY, REPORTER_NAME); - } - - @Getter(lazy = true, value = AccessLevel.PRIVATE) - private final QaseTestCaseListener qaseTestCaseListener = createQaseListener(); - - private static QaseTestCaseListener createQaseListener() { - return TestNgModule.getInjector().getInstance(QaseTestCaseListener.class); - } - - @Override - public void onTestStart(ITestResult tr) { - getQaseTestCaseListener().onTestCaseStarted(); - super.onTestStart(tr); - } - - @Override - public void onTestSuccess(ITestResult tr) { - getQaseTestCaseListener() - .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, PASSED)); - super.onTestSuccess(tr); - } - - @Override - public void onTestSkipped(ITestResult tr) { - getQaseTestCaseListener() - .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, SKIPPED)); - super.onTestSuccess(tr); - } - - @Override - public void onTestFailure(ITestResult tr) { - getQaseTestCaseListener() - .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, FAILED)); - super.onTestFailure(tr); - } - - @Override - public void onFinish(ITestContext testContext) { - getQaseTestCaseListener().onTestCasesSetFinished(); - super.onFinish(testContext); - } - - private void setupResultItem(ResultCreate resultCreate, ITestResult result, ResultCreate.StatusEnum status) { - Optional resultThrowable = Optional.ofNullable(result.getThrowable()); - String comment = resultThrowable - .flatMap(throwable -> Optional.of(throwable.toString())).orElse(null); - Boolean isDefect = resultThrowable - .flatMap(throwable -> Optional.of(throwable instanceof AssertionError)) - .orElse(false); - String stacktrace = resultThrowable - .flatMap(throwable -> Optional.of(getStacktrace(throwable))) - .orElse(null); - Method method = result.getMethod() - .getConstructorOrMethod() - .getMethod(); - Long caseId = getCaseId(method); - String caseTitle = null; - if (caseId == null) { - caseTitle = getCaseTitle(method); - } - LinkedList steps = StepStorage.stopSteps(); - resultCreate - ._case(caseTitle == null ? null : new ResultCreateCase().title(caseTitle)) - .caseId(caseId) - .status(status) - .comment(comment) - .stacktrace(stacktrace) - .steps(steps.isEmpty() ? null : steps) - .defect(isDefect); + static { + System.setProperty(QaseConfig.QASE_CLIENT_REPORTER_NAME_KEY, REPORTER_NAME); + } + + @Getter(lazy = true, value = AccessLevel.PRIVATE) + private final QaseTestCaseListener qaseTestCaseListener = createQaseListener(); + + private static QaseTestCaseListener createQaseListener() { + return TestNgModule.getInjector().getInstance(QaseTestCaseListener.class); + } + + @Override + public void onTestStart(ITestResult tr) { + getQaseTestCaseListener().onTestCaseStarted(); + super.onTestStart(tr); + } + + @Override + public void onTestSuccess(ITestResult tr) { + getQaseTestCaseListener() + .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, PASSED)); + super.onTestSuccess(tr); + } + + @Override + public void onTestSkipped(ITestResult tr) { + getQaseTestCaseListener() + .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, SKIPPED)); + super.onTestSuccess(tr); + } + + @Override + public void onTestFailure(ITestResult tr) { + getQaseTestCaseListener() + .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, FAILED)); + super.onTestFailure(tr); + } + + @Override + public void onFinish(ITestContext testContext) { + getQaseTestCaseListener().onTestCasesSetFinished(); + super.onFinish(testContext); + } + + private void setupResultItem(ResultCreate resultCreate, ITestResult result, ResultCreate.StatusEnum status) { + Optional resultThrowable = Optional.ofNullable(result.getThrowable()); + String comment = resultThrowable + .flatMap(throwable -> Optional.of(throwable.toString())).orElse(null); + Boolean isDefect = resultThrowable + .flatMap(throwable -> Optional.of(throwable instanceof AssertionError)) + .orElse(false); + String stacktrace = resultThrowable + .flatMap(throwable -> Optional.of(getStacktrace(throwable))) + .orElse(null); + Method method = result.getMethod() + .getConstructorOrMethod() + .getMethod(); + Long caseId = getCaseId(method); + String caseTitle = null; + if (caseId == null) { + caseTitle = getCaseTitle(method); } + LinkedList steps = StepStorage.stopSteps(); + resultCreate + ._case(caseTitle == null ? null : new ResultCreateCase().title(caseTitle)) + .caseId(caseId) + .status(status) + .comment(comment) + .stacktrace(stacktrace) + .steps(steps.isEmpty() ? null : steps) + .defect(isDefect); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/FileUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/FileUtils.java index c2489a86c2..8bb57809b8 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/FileUtils.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/FileUtils.java @@ -1,27 +1,26 @@ package com.provectus.kafka.ui.utilities; -import org.testcontainers.shaded.org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - import static org.apache.kafka.common.utils.Utils.readFileAsString; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.testcontainers.shaded.org.apache.commons.io.IOUtils; + public class FileUtils { - public static String getResourceAsString(String resourceFileName) { - try { - return IOUtils.resourceToString("/" + resourceFileName, StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException(e); - } + public static String getResourceAsString(String resourceFileName) { + try { + return IOUtils.resourceToString("/" + resourceFileName, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); } + } - public static String fileToString(String path) { - try { - return readFileAsString(path); - } catch (IOException e) { - throw new RuntimeException(e); - } + public static String fileToString(String path) { + try { + return readFileAsString(path); + } catch (IOException e) { + throw new RuntimeException(e); } + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java index f53e8897e9..7f72f8a4d6 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java @@ -1,17 +1,16 @@ package com.provectus.kafka.ui.utilities; -import lombok.extern.slf4j.Slf4j; +import static com.codeborne.selenide.Selenide.sleep; import java.time.LocalTime; - -import static com.codeborne.selenide.Selenide.sleep; +import lombok.extern.slf4j.Slf4j; @Slf4j public class TimeUtils { - public static void waitUntilNewMinuteStarted() { - int secondsLeft = 60 - LocalTime.now().getSecond(); - log.debug("\nwaitUntilNewMinuteStarted: {}s", secondsLeft); - sleep(secondsLeft * 1000); - } + public static void waitUntilNewMinuteStarted() { + int secondsLeft = 60 - LocalTime.now().getSecond(); + log.debug("\nwaitUntilNewMinuteStarted: {}s", secondsLeft); + sleep(secondsLeft * 1000); + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java index 427662d9d7..fef5ef654a 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java @@ -1,105 +1,110 @@ package com.provectus.kafka.ui.utilities; +import static com.codeborne.selenide.Selenide.executeJavaScript; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.WebDriverRunner; +import java.time.Duration; import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.Keys; import org.openqa.selenium.interactions.Actions; -import java.time.Duration; - -import static com.codeborne.selenide.Selenide.executeJavaScript; - @Slf4j public class WebUtils { - public static int getTimeout(int... timeoutInSeconds) { - return (timeoutInSeconds != null && timeoutInSeconds.length > 0) ? timeoutInSeconds[0] : 4; - } + public static int getTimeout(int... timeoutInSeconds) { + return (timeoutInSeconds != null && timeoutInSeconds.length > 0) ? timeoutInSeconds[0] : 4; + } - public static void sendKeysAfterClear(SelenideElement element, String keys) { - log.debug("\nsendKeysAfterClear: {} \nsend keys '{}'", element.getSearchCriteria(), keys); - element.shouldBe(Condition.enabled).clear(); - if (keys != null) element.sendKeys(keys); + public static void sendKeysAfterClear(SelenideElement element, String keys) { + log.debug("\nsendKeysAfterClear: {} \nsend keys '{}'", element.getSearchCriteria(), keys); + element.shouldBe(Condition.enabled).clear(); + if (keys != null) { + element.sendKeys(keys); } + } - public static void clickByActions(SelenideElement element) { - log.debug("\nclickByActions: {}", element.getSearchCriteria()); - element.shouldBe(Condition.enabled); - new Actions(WebDriverRunner.getWebDriver()) - .moveToElement(element) - .click(element) - .perform(); - } + public static void clickByActions(SelenideElement element) { + log.debug("\nclickByActions: {}", element.getSearchCriteria()); + element.shouldBe(Condition.enabled); + new Actions(WebDriverRunner.getWebDriver()) + .moveToElement(element) + .click(element) + .perform(); + } - public static void sendKeysByActions(SelenideElement element, String keys) { - log.debug("\nsendKeysByActions: {} \nsend keys '{}'", element.getSearchCriteria(), keys); - element.shouldBe(Condition.enabled); - new Actions(WebDriverRunner.getWebDriver()) - .moveToElement(element) - .sendKeys(element, keys) - .perform(); - } + public static void sendKeysByActions(SelenideElement element, String keys) { + log.debug("\nsendKeysByActions: {} \nsend keys '{}'", element.getSearchCriteria(), keys); + element.shouldBe(Condition.enabled); + new Actions(WebDriverRunner.getWebDriver()) + .moveToElement(element) + .sendKeys(element, keys) + .perform(); + } - public static void clickByJavaScript(SelenideElement element) { - log.debug("\nclickByJavaScript: {}", element.getSearchCriteria()); - element.shouldBe(Condition.enabled); - String script = "arguments[0].click();"; - executeJavaScript(script, element); - } + public static void clickByJavaScript(SelenideElement element) { + log.debug("\nclickByJavaScript: {}", element.getSearchCriteria()); + element.shouldBe(Condition.enabled); + String script = "arguments[0].click();"; + executeJavaScript(script, element); + } - public static void clearByKeyboard(SelenideElement field) { - log.debug("\nclearByKeyboard: {}", field.getSearchCriteria()); - field.shouldBe(Condition.enabled).sendKeys(Keys.END); - field.sendKeys(Keys.chord(Keys.CONTROL + "a"), Keys.DELETE); - } + public static void clearByKeyboard(SelenideElement field) { + log.debug("\nclearByKeyboard: {}", field.getSearchCriteria()); + field.shouldBe(Condition.enabled).sendKeys(Keys.END); + field.sendKeys(Keys.chord(Keys.CONTROL + "a"), Keys.DELETE); + } - public static boolean isVisible(SelenideElement element, int... timeoutInSeconds) { - log.debug("\nisVisible: {}", element.getSearchCriteria()); - boolean isVisible = false; - try { - element.shouldBe(Condition.visible, - Duration.ofSeconds(getTimeout(timeoutInSeconds))); - isVisible = true; - } catch (Throwable e) { - log.debug("{} is not visible", element.getSearchCriteria()); - } - return isVisible; + public static boolean isVisible(SelenideElement element, int... timeoutInSeconds) { + log.debug("\nisVisible: {}", element.getSearchCriteria()); + boolean isVisible = false; + try { + element.shouldBe(Condition.visible, + Duration.ofSeconds(getTimeout(timeoutInSeconds))); + isVisible = true; + } catch (Throwable e) { + log.debug("{} is not visible", element.getSearchCriteria()); } + return isVisible; + } - public static boolean isEnabled(SelenideElement element, int... timeoutInSeconds) { - log.debug("\nisEnabled: {}", element.getSearchCriteria()); - boolean isEnabled = false; - try { - element.shouldBe(Condition.enabled, - Duration.ofSeconds(getTimeout(timeoutInSeconds))); - isEnabled = true; - } catch (Throwable e) { - log.debug("{} is not enabled", element.getSearchCriteria()); - } - return isEnabled; + public static boolean isEnabled(SelenideElement element, int... timeoutInSeconds) { + log.debug("\nisEnabled: {}", element.getSearchCriteria()); + boolean isEnabled = false; + try { + element.shouldBe(Condition.enabled, + Duration.ofSeconds(getTimeout(timeoutInSeconds))); + isEnabled = true; + } catch (Throwable e) { + log.debug("{} is not enabled", element.getSearchCriteria()); } + return isEnabled; + } - public static boolean isSelected(SelenideElement element, int... timeoutInSeconds) { - log.debug("\nisSelected: {}", element.getSearchCriteria()); - boolean isSelected = false; - try { - element.shouldBe(Condition.selected, - Duration.ofSeconds(getTimeout(timeoutInSeconds))); - isSelected = true; - } catch (Throwable e) { - log.debug("{} is not selected", element.getSearchCriteria()); - } - return isSelected; + public static boolean isSelected(SelenideElement element, int... timeoutInSeconds) { + log.debug("\nisSelected: {}", element.getSearchCriteria()); + boolean isSelected = false; + try { + element.shouldBe(Condition.selected, + Duration.ofSeconds(getTimeout(timeoutInSeconds))); + isSelected = true; + } catch (Throwable e) { + log.debug("{} is not selected", element.getSearchCriteria()); } + return isSelected; + } - public static boolean selectElement(SelenideElement element, boolean select) { - if (select) { - if (!element.isSelected()) clickByJavaScript(element); - } else { - if (element.isSelected()) clickByJavaScript(element); - } - return true; + public static boolean selectElement(SelenideElement element, boolean select) { + if (select) { + if (!element.isSelected()) { + clickByJavaScript(element); + } + } else { + if (element.isSelected()) { + clickByJavaScript(element); + } } + return true; + } } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/QaseSetup.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/QaseSetup.java new file mode 100644 index 0000000000..f0ed509aad --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/QaseSetup.java @@ -0,0 +1,33 @@ +package com.provectus.kafka.ui.utilities.qase; + +import static com.provectus.kafka.ui.settings.BaseSource.SUITE_NAME; +import static com.provectus.kafka.ui.variables.Suite.MANUAL; +import static org.apache.commons.lang3.BooleanUtils.FALSE; +import static org.apache.commons.lang3.BooleanUtils.TRUE; +import static org.apache.commons.lang3.StringUtils.isEmpty; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class QaseSetup { + + public static void qaseIntegrationSetup() { + String qaseApiToken = System.getProperty("QASEIO_API_TOKEN"); + if (isEmpty(qaseApiToken)) { + log.warn("Integration with Qase is disabled due to run config or token wasn't defined."); + System.setProperty("QASE_ENABLE", FALSE); + } else { + log.warn("Integration with Qase is enabled. Find this run at https://app.qase.io/run/KAFKAUI."); + String automation = SUITE_NAME.equalsIgnoreCase(MANUAL) ? "" : "Automation "; + System.setProperty("QASE_ENABLE", TRUE); + System.setProperty("QASE_PROJECT_CODE", "KAFKAUI"); + System.setProperty("QASE_API_TOKEN", qaseApiToken); + System.setProperty("QASE_USE_BULK", TRUE); + System.setProperty("QASE_RUN_NAME", DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") + .format(OffsetDateTime.now(ZoneOffset.UTC)) + ": " + automation + SUITE_NAME.toUpperCase() + " suite"); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Automation.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Automation.java similarity index 63% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Automation.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Automation.java index 556263c111..1b8d5b65b8 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Automation.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Automation.java @@ -1,7 +1,6 @@ -package com.provectus.kafka.ui.utilities.qaseUtils.annotations; - -import com.provectus.kafka.ui.utilities.qaseUtils.enums.State; +package com.provectus.kafka.ui.utilities.qase.annotations; +import com.provectus.kafka.ui.utilities.qase.enums.State; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -11,5 +10,5 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) public @interface Automation { - State state(); + State state(); } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Status.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Status.java similarity index 65% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Status.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Status.java index 3c31f2345c..df078d0436 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Status.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Status.java @@ -1,4 +1,4 @@ -package com.provectus.kafka.ui.utilities.qaseUtils.annotations; +package com.provectus.kafka.ui.utilities.qase.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -9,5 +9,5 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) public @interface Status { - com.provectus.kafka.ui.utilities.qaseUtils.enums.Status status(); + com.provectus.kafka.ui.utilities.qase.enums.Status status(); } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Suite.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Suite.java similarity index 76% rename from kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Suite.java rename to kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Suite.java index fa1c2c3dd7..fcaff09744 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/annotations/Suite.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Suite.java @@ -1,4 +1,4 @@ -package com.provectus.kafka.ui.utilities.qaseUtils.annotations; +package com.provectus.kafka.ui.utilities.qase.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -9,5 +9,5 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) public @interface Suite { - long id(); + long id(); } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/State.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/State.java new file mode 100644 index 0000000000..11f49d5647 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/State.java @@ -0,0 +1,18 @@ +package com.provectus.kafka.ui.utilities.qase.enums; + +public enum State { + + NOT_AUTOMATED(0), + TO_BE_AUTOMATED(1), + AUTOMATED(2); + + private final int value; + + State(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/Status.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/Status.java new file mode 100644 index 0000000000..a4e7e0cce3 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/Status.java @@ -0,0 +1,18 @@ +package com.provectus.kafka.ui.utilities.qase.enums; + +public enum Status { + + ACTUAL(0), + DRAFT(1), + DEPRECATED(2); + + private final int value; + + Status(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/QaseSetup.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/QaseSetup.java deleted file mode 100644 index 60be014a0a..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/QaseSetup.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.provectus.kafka.ui.utilities.qaseUtils; - -import lombok.extern.slf4j.Slf4j; - -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; - -import static com.provectus.kafka.ui.settings.BaseSource.SUITE_NAME; -import static com.provectus.kafka.ui.variables.Suite.MANUAL; -import static org.apache.commons.lang3.BooleanUtils.FALSE; -import static org.apache.commons.lang3.BooleanUtils.TRUE; -import static org.apache.commons.lang3.StringUtils.isEmpty; - -@Slf4j -public class QaseSetup { - - public static void qaseIntegrationSetup() { - String qaseApiToken = System.getProperty("QASEIO_API_TOKEN"); - if (isEmpty(qaseApiToken)) { - log.warn("Integration with Qase is disabled due to run config or token wasn't defined."); - System.setProperty("QASE_ENABLE", FALSE); - } else { - log.warn("Integration with Qase is enabled. Find this run at https://app.qase.io/run/KAFKAUI."); - String automation = SUITE_NAME.equalsIgnoreCase(MANUAL) ? "" : "Automation "; - System.setProperty("QASE_ENABLE", TRUE); - System.setProperty("QASE_PROJECT_CODE", "KAFKAUI"); - System.setProperty("QASE_API_TOKEN", qaseApiToken); - System.setProperty("QASE_USE_BULK", TRUE); - System.setProperty("QASE_RUN_NAME", DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") - .format(OffsetDateTime.now(ZoneOffset.UTC)) + ": " + automation + SUITE_NAME.toUpperCase() + " suite"); - } - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/State.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/State.java deleted file mode 100644 index cdbbaf38de..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/State.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.provectus.kafka.ui.utilities.qaseUtils.enums; - -public enum State { - - NOT_AUTOMATED(0), - TO_BE_AUTOMATED(1), - AUTOMATED(2); - - private final int value; - - State(int value) { - this.value = value; - } - - public int getValue() { - return value; - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/Status.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/Status.java deleted file mode 100644 index adc8bf24b9..0000000000 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qaseUtils/enums/Status.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.provectus.kafka.ui.utilities.qaseUtils.enums; - -public enum Status { - - ACTUAL(0), - DRAFT(1), - DEPRECATED(2); - - private final int value; - - Status(int value) { - this.value = value; - } - - public int getValue() { - return value; - } -} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java index cb3873cdb5..f435dcbf01 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java @@ -2,6 +2,6 @@ package com.provectus.kafka.ui.variables; public interface Browser { - String CONTAINER = "container"; - String LOCAL = "local"; + String CONTAINER = "container"; + String LOCAL = "local"; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java index 74f60dd0d2..3d337548c9 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java @@ -2,9 +2,9 @@ package com.provectus.kafka.ui.variables; public interface Suite { - String CUSTOM = "custom"; - String MANUAL = "manual"; - String REGRESSION = "regression"; - String SANITY = "sanity"; - String SMOKE = "smoke"; + String CUSTOM = "custom"; + String MANUAL = "manual"; + String REGRESSION = "regression"; + String SANITY = "sanity"; + String SMOKE = "smoke"; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java index 8800463bdc..5b800608f3 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java @@ -2,10 +2,10 @@ package com.provectus.kafka.ui.variables; public interface Url { - String BROKERS_LIST_URL = "http://%s:8080/ui/clusters/local/brokers"; - String TOPICS_LIST_URL = "http://%s:8080/ui/clusters/local/all-topics"; - String CONSUMERS_LIST_URL = "http://%s:8080/ui/clusters/local/consumer-groups"; - String SCHEMA_REGISTRY_LIST_URL = "http://%s:8080/ui/clusters/local/schemas"; - String KAFKA_CONNECT_LIST_URL = "http://%s:8080/ui/clusters/local/connectors"; - String KSQL_DB_LIST_URL = "http://%s:8080/ui/clusters/local/ksqldb/tables"; + String BROKERS_LIST_URL = "http://%s:8080/ui/clusters/local/brokers"; + String TOPICS_LIST_URL = "http://%s:8080/ui/clusters/local/all-topics"; + String CONSUMERS_LIST_URL = "http://%s:8080/ui/clusters/local/consumer-groups"; + String SCHEMA_REGISTRY_LIST_URL = "http://%s:8080/ui/clusters/local/schemas"; + String KAFKA_CONNECT_LIST_URL = "http://%s:8080/ui/clusters/local/connectors"; + String KSQL_DB_LIST_URL = "http://%s:8080/ui/clusters/local/ksqldb/tables"; } diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector.json b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector.json index fbe9fea655..096d4191e4 100644 --- a/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector.json +++ b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector.json @@ -15,4 +15,4 @@ "insert.mode": "upsert", "errors.log.enable": "true", "errors.log.include.messages": "true" -} \ No newline at end of file +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector_via_api.json b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector_via_api.json index 5c5b7aba84..dffd66cae5 100644 --- a/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector_via_api.json +++ b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector_via_api.json @@ -4,4 +4,4 @@ "connection.user": "dev_user", "connection.password": "12345", "topics": "topic_for_connector" -} \ No newline at end of file +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/connectors/delete_connector_config.json b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/delete_connector_config.json index c5adc99444..121613fc1c 100644 --- a/kafka-ui-e2e-checks/src/main/resources/testData/connectors/delete_connector_config.json +++ b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/delete_connector_config.json @@ -15,4 +15,4 @@ "insert.mode": "upsert", "errors.log.enable": "true", "errors.log.include.messages": "true" -} \ No newline at end of file +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_for_update.json b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_for_update.json index 9971559ba7..bbfe011c9f 100644 --- a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_for_update.json +++ b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_for_update.json @@ -14,4 +14,4 @@ "default": null } ] -} \ No newline at end of file +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_value.json b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_value.json index 1bc30f6d1c..d84caf2ea8 100644 --- a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_value.json +++ b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_value.json @@ -12,4 +12,4 @@ "type": "int" } ] -} \ No newline at end of file +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_json_Value.json b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_json_Value.json index 5c5b7aba84..dffd66cae5 100644 --- a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_json_Value.json +++ b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_json_Value.json @@ -4,4 +4,4 @@ "connection.user": "dev_user", "connection.password": "12345", "topics": "topic_for_connector" -} \ No newline at end of file +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/topics/message_content_create_topic.json b/kafka-ui-e2e-checks/src/main/resources/testData/topics/message_content_create_topic.json index 8c8fabe5bf..a4c0d940f4 100644 --- a/kafka-ui-e2e-checks/src/main/resources/testData/topics/message_content_create_topic.json +++ b/kafka-ui-e2e-checks/src/main/resources/testData/topics/message_content_create_topic.json @@ -21,4 +21,4 @@ "id":"1", "value":"kafka" } -} \ No newline at end of file +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java index 424c699f04..45daf6a4b5 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java @@ -1,5 +1,18 @@ package com.provectus.kafka.ui; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.CONSUMERS; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS; +import static com.provectus.kafka.ui.settings.BaseSource.BASE_UI_URL; +import static com.provectus.kafka.ui.settings.drivers.WebDriver.browserClear; +import static com.provectus.kafka.ui.settings.drivers.WebDriver.browserQuit; +import static com.provectus.kafka.ui.settings.drivers.WebDriver.browserSetup; +import static com.provectus.kafka.ui.settings.drivers.WebDriver.loggerSetup; +import static com.provectus.kafka.ui.utilities.qase.QaseSetup.qaseIntegrationSetup; + import com.codeborne.selenide.Condition; import com.codeborne.selenide.Selenide; import com.codeborne.selenide.SelenideElement; @@ -7,136 +20,134 @@ import com.provectus.kafka.ui.settings.listeners.AllureListener; import com.provectus.kafka.ui.settings.listeners.LoggerListener; import com.provectus.kafka.ui.settings.listeners.QaseResultListener; import io.qameta.allure.Step; -import lombok.extern.slf4j.Slf4j; -import org.testng.annotations.*; -import org.testng.asserts.SoftAssert; - import java.util.List; - -import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.*; -import static com.provectus.kafka.ui.settings.BaseSource.BASE_UI_URL; -import static com.provectus.kafka.ui.settings.drivers.WebDriver.*; -import static com.provectus.kafka.ui.utilities.qaseUtils.QaseSetup.qaseIntegrationSetup; +import lombok.extern.slf4j.Slf4j; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.AfterSuite; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Listeners; +import org.testng.asserts.SoftAssert; @Slf4j @Listeners({AllureListener.class, LoggerListener.class, QaseResultListener.class}) public abstract class BaseTest extends Facade { - @BeforeSuite(alwaysRun = true) - public void beforeSuite() { - qaseIntegrationSetup(); - loggerSetup(); - browserSetup(); - } + @BeforeSuite(alwaysRun = true) + public void beforeSuite() { + qaseIntegrationSetup(); + loggerSetup(); + browserSetup(); + } - @AfterSuite(alwaysRun = true) - public void afterSuite() { - browserQuit(); - } + @AfterSuite(alwaysRun = true) + public void afterSuite() { + browserQuit(); + } - @BeforeMethod(alwaysRun = true) - public void beforeMethod() { - Selenide.open(BASE_UI_URL); - naviSideBar.waitUntilScreenReady(); - } + @BeforeMethod(alwaysRun = true) + public void beforeMethod() { + Selenide.open(BASE_UI_URL); + naviSideBar.waitUntilScreenReady(); + } - @AfterMethod(alwaysRun = true) - public void afterMethod() { - browserClear(); - } + @AfterMethod(alwaysRun = true) + public void afterMethod() { + browserClear(); + } - @Step - protected void navigateToBrokers() { - naviSideBar - .openSideMenu(BROKERS); - brokersList - .waitUntilScreenReady(); - } + @Step + protected void navigateToBrokers() { + naviSideBar + .openSideMenu(BROKERS); + brokersList + .waitUntilScreenReady(); + } - @Step - protected void navigateToBrokersAndOpenDetails(int brokerId) { - naviSideBar - .openSideMenu(BROKERS); - brokersList - .waitUntilScreenReady() - .openBroker(brokerId); - brokersDetails - .waitUntilScreenReady(); - } + @Step + protected void navigateToBrokersAndOpenDetails(int brokerId) { + naviSideBar + .openSideMenu(BROKERS); + brokersList + .waitUntilScreenReady() + .openBroker(brokerId); + brokersDetails + .waitUntilScreenReady(); + } - @Step - protected void navigateToTopics() { - naviSideBar - .openSideMenu(TOPICS); - topicsList - .waitUntilScreenReady() - .setShowInternalRadioButton(false); - } + @Step + protected void navigateToTopics() { + naviSideBar + .openSideMenu(TOPICS); + topicsList + .waitUntilScreenReady() + .setShowInternalRadioButton(false); + } - @Step - protected void navigateToTopicsAndOpenDetails(String topicName) { - navigateToTopics(); - topicsList - .openTopic(topicName); - topicDetails - .waitUntilScreenReady(); - } + @Step + protected void navigateToTopicsAndOpenDetails(String topicName) { + navigateToTopics(); + topicsList + .openTopic(topicName); + topicDetails + .waitUntilScreenReady(); + } - @Step - protected void navigateToConsumers() { - naviSideBar - .openSideMenu(CONSUMERS); - consumersList - .waitUntilScreenReady(); - } + @Step + protected void navigateToConsumers() { + naviSideBar + .openSideMenu(CONSUMERS); + consumersList + .waitUntilScreenReady(); + } - @Step - protected void navigateToSchemaRegistry() { - naviSideBar - .openSideMenu(SCHEMA_REGISTRY); - schemaRegistryList - .waitUntilScreenReady(); - } + @Step + protected void navigateToSchemaRegistry() { + naviSideBar + .openSideMenu(SCHEMA_REGISTRY); + schemaRegistryList + .waitUntilScreenReady(); + } - @Step - protected void navigateToSchemaRegistryAndOpenDetails(String schemaName) { - navigateToSchemaRegistry(); - schemaRegistryList - .openSchema(schemaName); - schemaDetails - .waitUntilScreenReady(); - } + @Step + protected void navigateToSchemaRegistryAndOpenDetails(String schemaName) { + navigateToSchemaRegistry(); + schemaRegistryList + .openSchema(schemaName); + schemaDetails + .waitUntilScreenReady(); + } - @Step - protected void navigateToConnectors() { - naviSideBar - .openSideMenu(KAFKA_CONNECT); - kafkaConnectList - .waitUntilScreenReady(); - } + @Step + protected void navigateToConnectors() { + naviSideBar + .openSideMenu(KAFKA_CONNECT); + kafkaConnectList + .waitUntilScreenReady(); + } - @Step - protected void navigateToConnectorsAndOpenDetails(String connectorName) { - navigateToConnectors(); - kafkaConnectList - .openConnector(connectorName); - connectorDetails - .waitUntilScreenReady(); - } + @Step + protected void navigateToConnectorsAndOpenDetails(String connectorName) { + navigateToConnectors(); + kafkaConnectList + .openConnector(connectorName); + connectorDetails + .waitUntilScreenReady(); + } - @Step - protected void navigateToKsqlDb() { - naviSideBar - .openSideMenu(KSQL_DB); - ksqlDbList - .waitUntilScreenReady(); - } + @Step + protected void navigateToKsqlDb() { + naviSideBar + .openSideMenu(KSQL_DB); + ksqlDbList + .waitUntilScreenReady(); + } - @Step - protected void verifyElementsCondition(List elementList, Condition expectedCondition) { - SoftAssert softly = new SoftAssert(); - elementList.forEach(element -> softly.assertTrue(element.is(expectedCondition), - element.getSearchCriteria() + " is " + expectedCondition)); - softly.assertAll(); - } + @Step + protected void verifyElementsCondition(List elementList, Condition expectedCondition) { + SoftAssert softly = new SoftAssert(); + elementList.forEach(element -> softly.assertTrue(element.is(expectedCondition), + element.getSearchCriteria() + " is " + expectedCondition)); + softly.assertAll(); + } } diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java index 67468dbcb0..abc0b0aa6b 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java @@ -1,7 +1,5 @@ package com.provectus.kafka.ui; -import com.provectus.kafka.ui.pages.panels.NaviSideBar; -import com.provectus.kafka.ui.pages.panels.TopPanel; import com.provectus.kafka.ui.pages.brokers.BrokersConfigTab; import com.provectus.kafka.ui.pages.brokers.BrokersDetails; import com.provectus.kafka.ui.pages.brokers.BrokersList; @@ -10,35 +8,41 @@ import com.provectus.kafka.ui.pages.connectors.ConnectorDetails; import com.provectus.kafka.ui.pages.connectors.KafkaConnectList; import com.provectus.kafka.ui.pages.consumers.ConsumersDetails; import com.provectus.kafka.ui.pages.consumers.ConsumersList; -import com.provectus.kafka.ui.pages.ksqlDb.KsqlDbList; -import com.provectus.kafka.ui.pages.ksqlDb.KsqlQueryForm; +import com.provectus.kafka.ui.pages.ksqldb.KsqlDbList; +import com.provectus.kafka.ui.pages.ksqldb.KsqlQueryForm; +import com.provectus.kafka.ui.pages.panels.NaviSideBar; +import com.provectus.kafka.ui.pages.panels.TopPanel; import com.provectus.kafka.ui.pages.schemas.SchemaCreateForm; import com.provectus.kafka.ui.pages.schemas.SchemaDetails; import com.provectus.kafka.ui.pages.schemas.SchemaRegistryList; -import com.provectus.kafka.ui.pages.topics.*; +import com.provectus.kafka.ui.pages.topics.ProduceMessagePanel; +import com.provectus.kafka.ui.pages.topics.TopicCreateEditForm; +import com.provectus.kafka.ui.pages.topics.TopicDetails; +import com.provectus.kafka.ui.pages.topics.TopicSettingsTab; +import com.provectus.kafka.ui.pages.topics.TopicsList; import com.provectus.kafka.ui.services.ApiService; public abstract class Facade { - protected ApiService apiService = new ApiService(); - protected ConnectorCreateForm connectorCreateForm = new ConnectorCreateForm(); - protected KafkaConnectList kafkaConnectList = new KafkaConnectList(); - protected ConnectorDetails connectorDetails = new ConnectorDetails(); - protected SchemaCreateForm schemaCreateForm = new SchemaCreateForm(); - protected SchemaDetails schemaDetails = new SchemaDetails(); - protected SchemaRegistryList schemaRegistryList = new SchemaRegistryList(); - protected ProduceMessagePanel produceMessagePanel = new ProduceMessagePanel(); - protected TopicCreateEditForm topicCreateEditForm = new TopicCreateEditForm(); - protected TopicsList topicsList = new TopicsList(); - protected TopicDetails topicDetails = new TopicDetails(); - protected ConsumersDetails consumersDetails = new ConsumersDetails(); - protected ConsumersList consumersList = new ConsumersList(); - protected NaviSideBar naviSideBar = new NaviSideBar(); - protected TopPanel topPanel = new TopPanel(); - protected BrokersList brokersList = new BrokersList(); - protected BrokersDetails brokersDetails = new BrokersDetails(); - protected BrokersConfigTab brokersConfigTab = new BrokersConfigTab(); - protected TopicSettingsTab topicSettingsTab = new TopicSettingsTab(); - protected KsqlQueryForm ksqlQueryForm = new KsqlQueryForm(); - protected KsqlDbList ksqlDbList = new KsqlDbList(); + protected ApiService apiService = new ApiService(); + protected ConnectorCreateForm connectorCreateForm = new ConnectorCreateForm(); + protected KafkaConnectList kafkaConnectList = new KafkaConnectList(); + protected ConnectorDetails connectorDetails = new ConnectorDetails(); + protected SchemaCreateForm schemaCreateForm = new SchemaCreateForm(); + protected SchemaDetails schemaDetails = new SchemaDetails(); + protected SchemaRegistryList schemaRegistryList = new SchemaRegistryList(); + protected ProduceMessagePanel produceMessagePanel = new ProduceMessagePanel(); + protected TopicCreateEditForm topicCreateEditForm = new TopicCreateEditForm(); + protected TopicsList topicsList = new TopicsList(); + protected TopicDetails topicDetails = new TopicDetails(); + protected ConsumersDetails consumersDetails = new ConsumersDetails(); + protected ConsumersList consumersList = new ConsumersList(); + protected NaviSideBar naviSideBar = new NaviSideBar(); + protected TopPanel topPanel = new TopPanel(); + protected BrokersList brokersList = new BrokersList(); + protected BrokersDetails brokersDetails = new BrokersDetails(); + protected BrokersConfigTab brokersConfigTab = new BrokersConfigTab(); + protected TopicSettingsTab topicSettingsTab = new TopicSettingsTab(); + protected KsqlQueryForm ksqlQueryForm = new KsqlQueryForm(); + protected KsqlDbList ksqlDbList = new KsqlDbList(); } diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/BaseManualTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/BaseManualTest.java deleted file mode 100644 index bf9f0c75ac..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/BaseManualTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.provectus.kafka.ui.manualSuite; - -import com.provectus.kafka.ui.settings.listeners.QaseResultListener; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; -import org.testng.SkipException; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.BeforeSuite; -import org.testng.annotations.Listeners; - -import java.lang.reflect.Method; - -import static com.provectus.kafka.ui.utilities.qaseUtils.QaseSetup.qaseIntegrationSetup; -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.TO_BE_AUTOMATED; - -@Listeners(QaseResultListener.class) -public abstract class BaseManualTest { - - @BeforeSuite - public void beforeSuite() { - qaseIntegrationSetup(); - } - - @BeforeMethod - public void beforeMethod(Method method) { - if (method.getAnnotation(Automation.class).state().equals(NOT_AUTOMATED) - || method.getAnnotation(Automation.class).state().equals(TO_BE_AUTOMATED)) - throw new SkipException("Skip test exception"); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SanityBacklog.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SanityBacklog.java deleted file mode 100644 index f09673c8a0..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SanityBacklog.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.provectus.kafka.ui.manualSuite.backlog; - -import com.provectus.kafka.ui.manualSuite.BaseManualTest; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Suite; -import io.qase.api.annotation.QaseId; -import org.testng.annotations.Test; - -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.TO_BE_AUTOMATED; - -public class SanityBacklog extends BaseManualTest { - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 19) - @QaseId(285) - @Test - public void testCaseA() { - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SmokeBacklog.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SmokeBacklog.java deleted file mode 100644 index 84fa7476cf..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/backlog/SmokeBacklog.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.provectus.kafka.ui.manualSuite.backlog; - -import com.provectus.kafka.ui.manualSuite.BaseManualTest; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Suite; -import io.qase.api.annotation.QaseId; -import org.testng.annotations.Test; - -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.TO_BE_AUTOMATED; - -public class SmokeBacklog extends BaseManualTest { - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 1) - @QaseId(330) - @Test - public void testCaseA() { - } - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 8) - @QaseId(276) - @Test - public void testCaseB() { - } - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 8) - @QaseId(277) - @Test - public void testCaseC() { - } - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 8) - @QaseId(278) - @Test - public void testCaseD() { - } - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 8) - @QaseId(284) - @Test - public void testCaseE() { - } - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 1) - @QaseId(331) - @Test - public void testCaseF() { - } - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 1) - @QaseId(332) - @Test - public void testCaseG() { - } - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 5) - @QaseId(335) - @Test - public void testCaseH() { - } - - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = 5) - @QaseId(336) - @Test - public void testCaseI() { - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/DataMaskingTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/DataMaskingTest.java deleted file mode 100644 index 23b6e6539e..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/DataMaskingTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.provectus.kafka.ui.manualSuite.suite; - -import com.provectus.kafka.ui.manualSuite.BaseManualTest; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; -import io.qase.api.annotation.QaseId; -import org.testng.annotations.Test; - -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; - -public class DataMaskingTest extends BaseManualTest { - - @Automation(state = NOT_AUTOMATED) - @QaseId(262) - @Test - public void testCaseA() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(264) - @Test - public void testCaseB() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(265) - @Test - public void testCaseC() { - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/RbacTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/RbacTest.java deleted file mode 100644 index aad85652f5..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/RbacTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.provectus.kafka.ui.manualSuite.suite; - -import com.provectus.kafka.ui.manualSuite.BaseManualTest; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; -import io.qase.api.annotation.QaseId; -import org.testng.annotations.Test; - -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; - -public class RbacTest extends BaseManualTest { - - @Automation(state = NOT_AUTOMATED) - @QaseId(249) - @Test - public void testCaseA() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(251) - @Test - public void testCaseB() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(257) - @Test - public void testCaseC() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(258) - @Test - public void testCaseD() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(259) - @Test - public void testCaseE() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(260) - @Test - public void testCaseF() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(261) - @Test - public void testCaseG() { - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/TopicsTest.java deleted file mode 100644 index dda8103ffa..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/TopicsTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.provectus.kafka.ui.manualSuite.suite; - -import com.provectus.kafka.ui.manualSuite.BaseManualTest; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; -import io.qase.api.annotation.QaseId; -import org.testng.annotations.Test; - -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; - -public class TopicsTest extends BaseManualTest { - - @Automation(state = NOT_AUTOMATED) - @QaseId(17) - @Test - public void testCaseA() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(18) - @Test - public void testCaseB() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(21) - @Test() - public void testCaseC() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(22) - @Test - public void testCaseD() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(47) - @Test - public void testCaseE() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(48) - @Test - public void testCaseF() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(49) - @Test - public void testCaseG() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(50) - @Test - public void testCaseH() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(57) - @Test - public void testCaseI() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(58) - @Test - public void testCaseJ() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(269) - @Test - public void testCaseK() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(270) - @Test - public void testCaseL() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(271) - @Test - public void testCaseM() { - } - - @Automation(state = NOT_AUTOMATED) - @QaseId(272) - @Test - public void testCaseN() { - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/WizardTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/WizardTest.java deleted file mode 100644 index e7ae52bdb1..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualSuite/suite/WizardTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.provectus.kafka.ui.manualSuite.suite; - -import com.provectus.kafka.ui.manualSuite.BaseManualTest; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; -import io.qase.api.annotation.QaseId; -import org.testng.annotations.Test; - -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; - -public class WizardTest extends BaseManualTest { - - @Automation(state = NOT_AUTOMATED) - @QaseId(333) - @Test - public void testCaseA() { - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/BaseManualTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/BaseManualTest.java new file mode 100644 index 0000000000..827dc1ce43 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/BaseManualTest.java @@ -0,0 +1,30 @@ +package com.provectus.kafka.ui.manualsuite; + +import static com.provectus.kafka.ui.utilities.qase.QaseSetup.qaseIntegrationSetup; +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; +import static com.provectus.kafka.ui.utilities.qase.enums.State.TO_BE_AUTOMATED; + +import com.provectus.kafka.ui.settings.listeners.QaseResultListener; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import java.lang.reflect.Method; +import org.testng.SkipException; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Listeners; + +@Listeners(QaseResultListener.class) +public abstract class BaseManualTest { + + @BeforeSuite + public void beforeSuite() { + qaseIntegrationSetup(); + } + + @BeforeMethod + public void beforeMethod(Method method) { + if (method.getAnnotation(Automation.class).state().equals(NOT_AUTOMATED) + || method.getAnnotation(Automation.class).state().equals(TO_BE_AUTOMATED)) { + throw new SkipException("Skip test exception"); + } + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SanityBacklog.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SanityBacklog.java new file mode 100644 index 0000000000..4021c8ec94 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SanityBacklog.java @@ -0,0 +1,7 @@ +package com.provectus.kafka.ui.manualsuite.backlog; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; + +public class SanityBacklog extends BaseManualTest { + +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java new file mode 100644 index 0000000000..b89a1d0cf7 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java @@ -0,0 +1,78 @@ +package com.provectus.kafka.ui.manualsuite.backlog; + +import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.BROKERS_SUITE_ID; +import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.KSQL_DB_SUITE_ID; +import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_PROFILE_SUITE_ID; +import static com.provectus.kafka.ui.utilities.qase.enums.State.TO_BE_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import com.provectus.kafka.ui.utilities.qase.annotations.Suite; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class SmokeBacklog extends BaseManualTest { + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = BROKERS_SUITE_ID) + @QaseId(330) + @Test + public void testCaseA() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = KSQL_DB_SUITE_ID) + @QaseId(276) + @Test + public void testCaseB() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = KSQL_DB_SUITE_ID) + @QaseId(277) + @Test + public void testCaseC() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = KSQL_DB_SUITE_ID) + @QaseId(278) + @Test + public void testCaseD() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = KSQL_DB_SUITE_ID) + @QaseId(284) + @Test + public void testCaseE() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = BROKERS_SUITE_ID) + @QaseId(331) + @Test + public void testCaseF() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = BROKERS_SUITE_ID) + @QaseId(332) + @Test + public void testCaseG() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = TOPICS_PROFILE_SUITE_ID) + @QaseId(335) + @Test + public void testCaseH() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = TOPICS_PROFILE_SUITE_ID) + @QaseId(336) + @Test + public void testCaseI() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/DataMaskingTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/DataMaskingTest.java new file mode 100644 index 0000000000..540511d0c4 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/DataMaskingTest.java @@ -0,0 +1,29 @@ +package com.provectus.kafka.ui.manualsuite.suite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class DataMaskingTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(262) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(264) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(265) + @Test + public void testCaseC() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/RbacTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/RbacTest.java new file mode 100644 index 0000000000..7c7ac3153b --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/RbacTest.java @@ -0,0 +1,53 @@ +package com.provectus.kafka.ui.manualsuite.suite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class RbacTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(249) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(251) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(257) + @Test + public void testCaseC() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(258) + @Test + public void testCaseD() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(259) + @Test + public void testCaseE() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(260) + @Test + public void testCaseF() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(261) + @Test + public void testCaseG() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java new file mode 100644 index 0000000000..76f8506deb --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java @@ -0,0 +1,95 @@ +package com.provectus.kafka.ui.manualsuite.suite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class TopicsTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(17) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(18) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(21) + @Test() + public void testCaseC() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(22) + @Test + public void testCaseD() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(47) + @Test + public void testCaseE() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(48) + @Test + public void testCaseF() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(49) + @Test + public void testCaseG() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(50) + @Test + public void testCaseH() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(57) + @Test + public void testCaseI() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(58) + @Test + public void testCaseJ() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(269) + @Test + public void testCaseK() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(270) + @Test + public void testCaseL() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(271) + @Test + public void testCaseM() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(272) + @Test + public void testCaseN() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java new file mode 100644 index 0000000000..9621104b1a --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java @@ -0,0 +1,17 @@ +package com.provectus.kafka.ui.manualsuite.suite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class WizardTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(333) + @Test + public void testCaseA() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java deleted file mode 100644 index 7b4c34ac05..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.provectus.kafka.ui.qaseSuite; - -import com.provectus.kafka.ui.settings.listeners.QaseCreateListener; -import org.testng.annotations.BeforeSuite; -import org.testng.annotations.Listeners; - -import static com.provectus.kafka.ui.utilities.qaseUtils.QaseSetup.qaseIntegrationSetup; - -@Listeners(QaseCreateListener.class) -public abstract class BaseQaseTest { - - protected static final long BROKERS_SUITE_ID = 1; - protected static final long CONNECTORS_SUITE_ID = 10; - protected static final long KSQL_DB_SUITE_ID = 8; - protected static final long SANITY_SUITE_ID = 19; - protected static final long SCHEMAS_SUITE_ID = 11; - protected static final long TOPICS_SUITE_ID = 2; - - @BeforeSuite - public void beforeSuite() { - qaseIntegrationSetup(); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java deleted file mode 100644 index ca987650dc..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.provectus.kafka.ui.qaseSuite; - -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Automation; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Status; -import com.provectus.kafka.ui.utilities.qaseUtils.annotations.Suite; -import io.qase.api.annotation.QaseTitle; -import io.qase.api.annotation.Step; - -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.State.NOT_AUTOMATED; -import static com.provectus.kafka.ui.utilities.qaseUtils.enums.Status.DRAFT; - -public class Template extends BaseQaseTest { - - /** - * this class is a kind of placeholder or example, use is as template to create new one - * copy Template into kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/ - * place it into regarding folder and rename according to test case summary from Qase.io - * uncomment @Test and set all annotations according to kafka-ui-e2e-checks/QASE.md - */ - - @Automation(state = NOT_AUTOMATED) - @QaseTitle("testCaseA title") - @Status(status = DRAFT) - @Suite(id = 0) -// @org.testng.annotations.Test - public void testCaseA() { - stepA(); - stepB(); - stepC(); - stepD(); - stepE(); - stepF(); - } - - @Step("stepA action") - private void stepA() { - } - - @Step("stepB action") - private void stepB() { - } - - @Step("stepC action") - private void stepC() { - } - - @Step("stepD action") - private void stepD() { - } - - @Step("stepE action") - private void stepE() { - } - - @Step("stepF action") - private void stepF() { - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/BaseQaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/BaseQaseTest.java new file mode 100644 index 0000000000..1a195a3631 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/BaseQaseTest.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.qasesuite; + +import static com.provectus.kafka.ui.utilities.qase.QaseSetup.qaseIntegrationSetup; + +import com.provectus.kafka.ui.settings.listeners.QaseCreateListener; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Listeners; + +@Listeners(QaseCreateListener.class) +public abstract class BaseQaseTest { + + public static final long BROKERS_SUITE_ID = 1; + public static final long CONNECTORS_SUITE_ID = 10; + public static final long KSQL_DB_SUITE_ID = 8; + public static final long SANITY_SUITE_ID = 19; + public static final long SCHEMAS_SUITE_ID = 11; + public static final long TOPICS_SUITE_ID = 2; + public static final long TOPICS_CREATE_SUITE_ID = 4; + public static final long TOPICS_PROFILE_SUITE_ID = 5; + + @BeforeSuite + public void beforeSuite() { + qaseIntegrationSetup(); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/Template.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/Template.java new file mode 100644 index 0000000000..d5a47eccdf --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/Template.java @@ -0,0 +1,58 @@ +package com.provectus.kafka.ui.qasesuite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; +import static com.provectus.kafka.ui.utilities.qase.enums.Status.DRAFT; + +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import com.provectus.kafka.ui.utilities.qase.annotations.Status; +import com.provectus.kafka.ui.utilities.qase.annotations.Suite; +import io.qase.api.annotation.QaseTitle; +import io.qase.api.annotation.Step; + +public class Template extends BaseQaseTest { + + /** + * this class is a kind of placeholder or example, use is as template to create new one + * copy Template into kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/ + * place it into regarding folder and rename according to test case summary from Qase.io + * uncomment @Test and set all annotations according to kafka-ui-e2e-checks/QASE.md + */ + + @Automation(state = NOT_AUTOMATED) + @QaseTitle("testCaseA title") + @Status(status = DRAFT) + @Suite(id = 0) + // @org.testng.annotations.Test + public void testCaseA() { + stepA(); + stepB(); + stepC(); + stepD(); + stepE(); + stepF(); + } + + @Step("stepA action") + private void stepA() { + } + + @Step("stepB action") + private void stepB() { + } + + @Step("stepC action") + private void stepC() { + } + + @Step("stepD action") + private void stepD() { + } + + @Step("stepE action") + private void stepE() { + } + + @Step("stepF action") + private void stepF() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitySuite/TestClass.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitySuite/TestClass.java deleted file mode 100644 index 89402f3fc8..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitySuite/TestClass.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.provectus.kafka.ui.sanitySuite; - -public class TestClass { -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitysuite/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitysuite/TopicsTest.java new file mode 100644 index 0000000000..40a6d67800 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitysuite/TopicsTest.java @@ -0,0 +1,66 @@ +package com.provectus.kafka.ui.sanitysuite; + +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.COMPACT; +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.DELETE; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Topic; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +public class TopicsTest extends BaseTest { + + private static final List TOPIC_LIST = new ArrayList<>(); + + @QaseId(285) + @Test() + public void verifyClearMessagesMenuStateAfterTopicUpdate() { + Topic topic = new Topic() + .setName("topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setCleanupPolicyValue(DELETE); + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(topic.getName()) + .setNumberOfPartitions(topic.getNumberOfPartitions()) + .selectCleanupPolicy(topic.getCleanupPolicyValue()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(topic); + topicDetails + .openDotMenu(); + Assert.assertTrue(topicDetails.isClearMessagesMenuEnabled(), "isClearMessagesMenuEnabled"); + topic.setCleanupPolicyValue(COMPACT); + editCleanUpPolicyAndOpenDotMenu(topic); + Assert.assertFalse(topicDetails.isClearMessagesMenuEnabled(), "isClearMessagesMenuEnabled"); + topic.setCleanupPolicyValue(DELETE); + editCleanUpPolicyAndOpenDotMenu(topic); + Assert.assertTrue(topicDetails.isClearMessagesMenuEnabled(), "isClearMessagesMenuEnabled"); + } + + private void editCleanUpPolicyAndOpenDotMenu(Topic topic) { + topicDetails + .clickEditSettingsMenu(); + topicCreateEditForm + .waitUntilScreenReady() + .selectCleanupPolicy(topic.getCleanupPolicyValue()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady() + .openDotMenu(); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java deleted file mode 100644 index c8439e70dd..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/SmokeTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.provectus.kafka.ui.smokeSuite; - -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.WebDriverRunner; -import com.provectus.kafka.ui.BaseTest; -import com.provectus.kafka.ui.models.Connector; -import com.provectus.kafka.ui.models.Schema; -import com.provectus.kafka.ui.models.Topic; -import com.provectus.kafka.ui.pages.panels.enums.MenuItem; -import io.qameta.allure.Step; -import io.qase.api.annotation.QaseId; -import org.testng.Assert; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; - -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.*; -import static com.provectus.kafka.ui.settings.BaseSource.BASE_HOST; -import static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString; -import static com.provectus.kafka.ui.variables.Url.*; -import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; - -public class SmokeTest extends BaseTest { - - private static final int BROKER_ID = 1; - private static final Schema TEST_SCHEMA = Schema.createSchemaAvro(); - private static final Topic TEST_TOPIC = new Topic() - .setName("new-topic-" + randomAlphabetic(5)) - .setNumberOfPartitions(1); - private static final Connector TEST_CONNECTOR = new Connector() - .setName("new-connector-" + randomAlphabetic(5)) - .setConfig(getResourceAsString("testData/connectors/config_for_create_connector_via_api.json")); - - @BeforeClass(alwaysRun = true) - public void beforeClass() { - apiService - .createTopic(TEST_TOPIC) - .createSchema(TEST_SCHEMA) - .createConnector(TEST_CONNECTOR); - } - - @QaseId(198) - @Test - public void checkBasePageElements() { - verifyElementsCondition( - Stream.concat(topPanel.getAllVisibleElements().stream(), naviSideBar.getAllMenuButtons().stream()) - .collect(Collectors.toList()), Condition.visible); - verifyElementsCondition( - Stream.concat(topPanel.getAllEnabledElements().stream(), naviSideBar.getAllMenuButtons().stream()) - .collect(Collectors.toList()), Condition.enabled); - } - - @QaseId(45) - @Test - public void checkUrlWhileNavigating() { - navigateToBrokers(); - verifyCurrentUrl(BROKERS_LIST_URL); - navigateToTopics(); - verifyCurrentUrl(TOPICS_LIST_URL); - navigateToConsumers(); - verifyCurrentUrl(CONSUMERS_LIST_URL); - navigateToSchemaRegistry(); - verifyCurrentUrl(SCHEMA_REGISTRY_LIST_URL); - navigateToConnectors(); - verifyCurrentUrl(KAFKA_CONNECT_LIST_URL); - navigateToKsqlDb(); - verifyCurrentUrl(KSQL_DB_LIST_URL); - } - - @QaseId(46) - @Test - public void checkPathWhileNavigating() { - navigateToBrokersAndOpenDetails(BROKER_ID); - verifyComponentsPath(BROKERS, String.format("Broker %d", BROKER_ID)); - navigateToTopicsAndOpenDetails(TEST_TOPIC.getName()); - verifyComponentsPath(TOPICS, TEST_TOPIC.getName()); - navigateToSchemaRegistryAndOpenDetails(TEST_SCHEMA.getName()); - verifyComponentsPath(SCHEMA_REGISTRY, TEST_SCHEMA.getName()); - navigateToConnectorsAndOpenDetails(TEST_CONNECTOR.getName()); - verifyComponentsPath(KAFKA_CONNECT, TEST_CONNECTOR.getName()); - } - - @Step - private void verifyCurrentUrl(String expectedUrl) { - String urlWithoutParameters = WebDriverRunner.getWebDriver().getCurrentUrl(); - if (urlWithoutParameters.contains("?")) - urlWithoutParameters = urlWithoutParameters.substring(0, urlWithoutParameters.indexOf("?")); - Assert.assertEquals(urlWithoutParameters, String.format(expectedUrl, BASE_HOST), "getCurrentUrl()"); - } - - @Step - private void verifyComponentsPath(MenuItem menuItem, String expectedPath) { - Assert.assertEquals(naviSideBar.getPagePath(menuItem), expectedPath, - String.format("getPagePath() for %s", menuItem.getPageTitle().toUpperCase())); - } - - @AfterClass(alwaysRun = true) - public void afterClass() { - apiService - .deleteTopic(TEST_TOPIC.getName()) - .deleteSchema(TEST_SCHEMA.getName()) - .deleteConnector(TEST_CONNECTOR.getName()); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/brokers/BrokersTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/brokers/BrokersTest.java deleted file mode 100644 index c9029e30ae..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/brokers/BrokersTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.provectus.kafka.ui.smokeSuite.brokers; - -import com.codeborne.selenide.Condition; -import com.provectus.kafka.ui.BaseTest; -import io.qase.api.annotation.QaseId; -import org.testng.Assert; -import org.testng.annotations.Test; - -import static com.provectus.kafka.ui.pages.brokers.BrokersDetails.DetailsTab.CONFIGS; - -public class BrokersTest extends BaseTest { - - @QaseId(1) - @Test - public void checkBrokersOverview() { - navigateToBrokers(); - Assert.assertTrue(brokersList.getAllBrokers().size() > 0, "getAllBrokers()"); - verifyElementsCondition(brokersList.getAllVisibleElements(), Condition.visible); - verifyElementsCondition(brokersList.getAllEnabledElements(), Condition.enabled); - } - - @QaseId(85) - @Test - public void checkExistingBrokersInCluster() { - navigateToBrokers(); - Assert.assertTrue(brokersList.getAllBrokers().size() > 0, "getAllBrokers()"); - brokersList - .openBroker(1); - brokersDetails - .waitUntilScreenReady(); - verifyElementsCondition(brokersDetails.getAllVisibleElements(), Condition.visible); - verifyElementsCondition(brokersDetails.getAllEnabledElements(), Condition.enabled); - brokersDetails - .openDetailsTab(CONFIGS); - brokersConfigTab - .waitUntilScreenReady(); - verifyElementsCondition(brokersConfigTab.getColumnHeaders(), Condition.visible); - verifyElementsCondition(brokersConfigTab.getEditButtons(), Condition.enabled); - Assert.assertTrue(brokersConfigTab.isSearchByKeyVisible(), "isSearchByKeyVisible()"); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/connectors/ConnectorsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/connectors/ConnectorsTest.java deleted file mode 100644 index bdce29e153..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/connectors/ConnectorsTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.provectus.kafka.ui.smokeSuite.connectors; - -import com.provectus.kafka.ui.BaseTest; -import com.provectus.kafka.ui.models.Connector; -import com.provectus.kafka.ui.models.Topic; -import io.qase.api.annotation.QaseId; -import org.testng.Assert; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; - -import java.util.ArrayList; -import java.util.List; - -import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; -import static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString; -import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; - -public class ConnectorsTest extends BaseTest { - - private static final List TOPIC_LIST = new ArrayList<>(); - private static final List CONNECTOR_LIST = new ArrayList<>(); - private static final String MESSAGE_CONTENT = "testData/topics/message_content_create_topic.json"; - private static final String MESSAGE_KEY = " "; - private static final Topic TOPIC_FOR_CREATE = new Topic() - .setName("topic-for-create-connector-" + randomAlphabetic(5)) - .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); - private static final Topic TOPIC_FOR_DELETE = new Topic() - .setName("topic-for-delete-connector-" + randomAlphabetic(5)) - .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); - private static final Topic TOPIC_FOR_UPDATE = new Topic() - .setName("topic-for-update-connector-" + randomAlphabetic(5)) - .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); - private static final Connector CONNECTOR_FOR_DELETE = new Connector() - .setName("connector-for-delete-" + randomAlphabetic(5)) - .setConfig(getResourceAsString("testData/connectors/delete_connector_config.json")); - private static final Connector CONNECTOR_FOR_UPDATE = new Connector() - .setName("connector-for-update-and-delete-" + randomAlphabetic(5)) - .setConfig(getResourceAsString("testData/connectors/config_for_create_connector_via_api.json")); - - @BeforeClass(alwaysRun = true) - public void beforeClass() { - TOPIC_LIST.addAll(List.of(TOPIC_FOR_CREATE, TOPIC_FOR_DELETE, TOPIC_FOR_UPDATE)); - TOPIC_LIST.forEach(topic -> apiService - .createTopic(topic) - .sendMessage(topic) - ); - CONNECTOR_LIST.addAll(List.of(CONNECTOR_FOR_DELETE, CONNECTOR_FOR_UPDATE)); - CONNECTOR_LIST.forEach(connector -> apiService.createConnector(connector)); - } - - @QaseId(42) - @Test - public void createConnector() { - Connector connectorForCreate = new Connector() - .setName("connector-for-create-" + randomAlphabetic(5)) - .setConfig(getResourceAsString("testData/connectors/config_for_create_connector.json")); - navigateToConnectors(); - kafkaConnectList - .clickCreateConnectorBtn(); - connectorCreateForm - .waitUntilScreenReady() - .setConnectorDetails(connectorForCreate.getName(), connectorForCreate.getConfig()) - .clickSubmitButton(); - connectorDetails - .waitUntilScreenReady(); - navigateToConnectorsAndOpenDetails(connectorForCreate.getName()); - Assert.assertTrue(connectorDetails.isConnectorHeaderVisible(connectorForCreate.getName()), "isConnectorTitleVisible()"); - navigateToConnectors(); - Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); - CONNECTOR_LIST.add(connectorForCreate); - } - - @QaseId(196) - @Test - public void updateConnector() { - navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_UPDATE.getName()); - connectorDetails - .openConfigTab() - .setConfig(CONNECTOR_FOR_UPDATE.getConfig()) - .clickSubmitButton(); - Assert.assertTrue(connectorDetails.isAlertWithMessageVisible(SUCCESS, "Config successfully updated."), "isAlertWithMessageVisible()"); - navigateToConnectors(); - Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()), "isConnectorVisible()"); - } - - @QaseId(195) - @Test - public void deleteConnector() { - navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_DELETE.getName()); - connectorDetails - .openDotMenu() - .clickDeleteBtn() - .clickConfirmBtn(); - navigateToConnectors(); - Assert.assertFalse(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); - CONNECTOR_LIST.remove(CONNECTOR_FOR_DELETE); - } - - @AfterClass(alwaysRun = true) - public void afterClass() { - CONNECTOR_LIST.forEach(connector -> - apiService.deleteConnector(connector.getName())); - TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java deleted file mode 100644 index 210e5549f4..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/ksqlDb/KsqlDbTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.provectus.kafka.ui.smokeSuite.ksqlDb; - -import com.provectus.kafka.ui.BaseTest; -import com.provectus.kafka.ui.pages.ksqlDb.models.Stream; -import com.provectus.kafka.ui.pages.ksqlDb.models.Table; -import io.qase.api.annotation.QaseId; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; -import org.testng.asserts.SoftAssert; - -import java.util.ArrayList; -import java.util.List; - -import static com.provectus.kafka.ui.pages.ksqlDb.enums.KsqlQueryConfig.SHOW_TABLES; -import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; - -public class KsqlDbTest extends BaseTest { - - private static final Stream STREAM_FOR_CHECK_TABLES = new Stream() - .setName("STREAM_FOR_CHECK_TABLES_" + randomAlphabetic(4).toUpperCase()) - .setTopicName("TOPIC_FOR_STREAM_" + randomAlphabetic(4).toUpperCase()); - private static final Table FIRST_TABLE = new Table() - .setName("FIRST_TABLE" + randomAlphabetic(4).toUpperCase()) - .setStreamName(STREAM_FOR_CHECK_TABLES.getName()); - private static final Table SECOND_TABLE = new Table() - .setName("SECOND_TABLE" + randomAlphabetic(4).toUpperCase()) - .setStreamName(STREAM_FOR_CHECK_TABLES.getName()); - private static final List TOPIC_NAMES_LIST = new ArrayList<>(); - - @BeforeClass(alwaysRun = true) - public void beforeClass() { - apiService - .createStream(STREAM_FOR_CHECK_TABLES) - .createTables(FIRST_TABLE, SECOND_TABLE); - TOPIC_NAMES_LIST.addAll(List.of(STREAM_FOR_CHECK_TABLES.getTopicName(), - FIRST_TABLE.getName(), SECOND_TABLE.getName())); - } - - @QaseId(41) - @Test(priority = 1) - public void checkShowTablesRequestExecution() { - navigateToKsqlDb(); - ksqlDbList - .clickExecuteKsqlRequestBtn(); - ksqlQueryForm - .waitUntilScreenReady() - .setQuery(SHOW_TABLES.getQuery()) - .clickExecuteBtn(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); - softly.assertTrue(ksqlQueryForm.getTableByName(FIRST_TABLE.getName()).isVisible(), "getTableName()"); - softly.assertTrue(ksqlQueryForm.getTableByName(SECOND_TABLE.getName()).isVisible(), "getTableName()"); - softly.assertAll(); - } - - @QaseId(86) - @Test(priority = 2) - public void clearResultsForExecutedRequest() { - navigateToKsqlDb(); - ksqlDbList - .clickExecuteKsqlRequestBtn(); - ksqlQueryForm - .waitUntilScreenReady() - .setQuery(SHOW_TABLES.getQuery()) - .clickExecuteBtn(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); - softly.assertAll(); - ksqlQueryForm - .clickClearResultsBtn(); - softly.assertFalse(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); - softly.assertAll(); - } - - @AfterClass(alwaysRun = true) - public void afterClass() { - TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName)); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/schemas/SchemasTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/schemas/SchemasTest.java deleted file mode 100644 index 9b2a1618c2..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/schemas/SchemasTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.provectus.kafka.ui.smokeSuite.schemas; - -import com.codeborne.selenide.Condition; -import com.provectus.kafka.ui.BaseTest; -import com.provectus.kafka.ui.api.model.CompatibilityLevel; -import com.provectus.kafka.ui.models.Schema; -import io.qase.api.annotation.QaseId; -import org.testng.Assert; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; -import org.testng.asserts.SoftAssert; - -import java.util.ArrayList; -import java.util.List; - -import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; - -public class SchemasTest extends BaseTest { - - private static final List SCHEMA_LIST = new ArrayList<>(); - private static final Schema AVRO_API = Schema.createSchemaAvro(); - private static final Schema JSON_API = Schema.createSchemaJson(); - private static final Schema PROTOBUF_API = Schema.createSchemaProtobuf(); - - @BeforeClass(alwaysRun = true) - public void beforeClass() { - SCHEMA_LIST.addAll(List.of(AVRO_API, JSON_API, PROTOBUF_API)); - SCHEMA_LIST.forEach(schema -> apiService.createSchema(schema)); - } - - @QaseId(43) - @Test(priority = 1) - public void createSchemaAvro() { - Schema schemaAvro = Schema.createSchemaAvro(); - navigateToSchemaRegistry(); - schemaRegistryList - .clickCreateSchema(); - schemaCreateForm - .setSubjectName(schemaAvro.getName()) - .setSchemaField(fileToString(schemaAvro.getValuePath())) - .selectSchemaTypeFromDropdown(schemaAvro.getType()) - .clickSubmitButton(); - schemaDetails - .waitUntilScreenReady(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaAvro.getName()), "isSchemaHeaderVisible()"); - softly.assertEquals(schemaDetails.getSchemaType(), schemaAvro.getType().getValue(), "getSchemaType()"); - softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), - "getCompatibility()"); - softly.assertAll(); - navigateToSchemaRegistry(); - Assert.assertTrue(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), "isSchemaVisible()"); - SCHEMA_LIST.add(schemaAvro); - } - - @QaseId(186) - @Test(priority = 2) - public void updateSchemaAvro() { - AVRO_API.setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_avro_for_update.json"); - navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); - schemaDetails - .openEditSchema(); - schemaCreateForm - .waitUntilScreenReady(); - verifyElementsCondition(schemaCreateForm.getAllDetailsPageElements(), Condition.visible); - SoftAssert softly = new SoftAssert(); - softly.assertFalse(schemaCreateForm.isSubmitBtnEnabled(), "isSubmitBtnEnabled()"); - softly.assertFalse(schemaCreateForm.isSchemaDropDownEnabled(), "isSchemaDropDownEnabled()"); - softly.assertAll(); - schemaCreateForm - .selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum.NONE) - .setNewSchemaValue(fileToString(AVRO_API.getValuePath())) - .clickSubmitButton(); - schemaDetails - .waitUntilScreenReady(); - Assert.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.NONE.toString(), - "getCompatibility()"); - } - - @QaseId(44) - @Test(priority = 3) - public void compareVersionsOperation() { - navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); - int latestVersion = schemaDetails - .waitUntilScreenReady() - .getLatestVersion(); - schemaDetails - .openCompareVersionMenu(); - int versionsNumberFromDdl = schemaCreateForm - .waitUntilScreenReady() - .openLeftVersionDdl() - .getVersionsNumberFromList(); - Assert.assertEquals(versionsNumberFromDdl, latestVersion, "Versions number is not matched"); - schemaCreateForm - .selectVersionFromDropDown(1); - Assert.assertEquals(schemaCreateForm.getMarkedLinesNumber(), 42, "getAllMarkedLines()"); - } - - @QaseId(187) - @Test(priority = 4) - public void deleteSchemaAvro() { - navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); - schemaDetails - .removeSchema(); - schemaRegistryList - .waitUntilScreenReady(); - Assert.assertFalse(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), "isSchemaVisible()"); - SCHEMA_LIST.remove(AVRO_API); - } - - @QaseId(89) - @Test(priority = 5) - public void createSchemaJson() { - Schema schemaJson = Schema.createSchemaJson(); - navigateToSchemaRegistry(); - schemaRegistryList - .clickCreateSchema(); - schemaCreateForm - .setSubjectName(schemaJson.getName()) - .setSchemaField(fileToString(schemaJson.getValuePath())) - .selectSchemaTypeFromDropdown(schemaJson.getType()) - .clickSubmitButton(); - schemaDetails - .waitUntilScreenReady(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaJson.getName()), "isSchemaHeaderVisible()"); - softly.assertEquals(schemaDetails.getSchemaType(), schemaJson.getType().getValue(), "getSchemaType()"); - softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), - "getCompatibility()"); - softly.assertAll(); - navigateToSchemaRegistry(); - Assert.assertTrue(schemaRegistryList.isSchemaVisible(JSON_API.getName()), "isSchemaVisible()"); - SCHEMA_LIST.add(schemaJson); - } - - @QaseId(189) - @Test(priority = 6) - public void deleteSchemaJson() { - navigateToSchemaRegistryAndOpenDetails(JSON_API.getName()); - schemaDetails - .removeSchema(); - schemaRegistryList - .waitUntilScreenReady(); - Assert.assertFalse(schemaRegistryList.isSchemaVisible(JSON_API.getName()), "isSchemaVisible()"); - SCHEMA_LIST.remove(JSON_API); - } - - @QaseId(91) - @Test(priority = 7) - public void createSchemaProtobuf() { - Schema schemaProtobuf = Schema.createSchemaProtobuf(); - navigateToSchemaRegistry(); - schemaRegistryList - .clickCreateSchema(); - schemaCreateForm - .setSubjectName(schemaProtobuf.getName()) - .setSchemaField(fileToString(schemaProtobuf.getValuePath())) - .selectSchemaTypeFromDropdown(schemaProtobuf.getType()) - .clickSubmitButton(); - schemaDetails - .waitUntilScreenReady(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaProtobuf.getName()), "isSchemaHeaderVisible()"); - softly.assertEquals(schemaDetails.getSchemaType(), schemaProtobuf.getType().getValue(), "getSchemaType()"); - softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), - "getCompatibility()"); - softly.assertAll(); - navigateToSchemaRegistry(); - Assert.assertTrue(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), "isSchemaVisible()"); - SCHEMA_LIST.add(schemaProtobuf); - } - - @QaseId(223) - @Test(priority = 8) - public void deleteSchemaProtobuf() { - navigateToSchemaRegistryAndOpenDetails(PROTOBUF_API.getName()); - schemaDetails - .removeSchema(); - schemaRegistryList - .waitUntilScreenReady(); - Assert.assertFalse(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), "isSchemaVisible()"); - SCHEMA_LIST.remove(PROTOBUF_API); - } - - @AfterClass(alwaysRun = true) - public void afterClass() { - SCHEMA_LIST.forEach(schema -> apiService.deleteSchema(schema.getName())); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/MessagesTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/MessagesTest.java deleted file mode 100644 index 00a0413e7d..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/MessagesTest.java +++ /dev/null @@ -1,283 +0,0 @@ -package com.provectus.kafka.ui.smokeSuite.topics; - -import com.provectus.kafka.ui.BaseTest; -import com.provectus.kafka.ui.models.Topic; -import com.provectus.kafka.ui.pages.topics.TopicDetails; -import io.qameta.allure.Issue; -import io.qameta.allure.Step; -import io.qase.api.annotation.QaseId; -import org.testng.Assert; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; -import org.testng.asserts.SoftAssert; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.stream.IntStream; - -import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; -import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES; -import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW; -import static com.provectus.kafka.ui.utilities.TimeUtils.waitUntilNewMinuteStarted; -import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; - -public class MessagesTest extends BaseTest { - - private static final Topic TOPIC_FOR_MESSAGES = new Topic() - .setName("topic-with-clean-message-attribute-" + randomAlphabetic(5)) - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final Topic TOPIC_TO_CLEAR_AND_PURGE_MESSAGES = new Topic() - .setName("topic-to-clear-and-purge-messages-" + randomAlphabetic(5)) - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic() - .setName("topic-for-check-filters-" + randomAlphabetic(5)) - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final Topic TOPIC_TO_RECREATE = new Topic() - .setName("topic-to-recreate-attribute-" + randomAlphabetic(5)) - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final Topic TOPIC_FOR_CHECK_MESSAGES_COUNT = new Topic() - .setName("topic-for-check-messages-count" + randomAlphabetic(5)) - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final List TOPIC_LIST = new ArrayList<>(); - - @BeforeClass(alwaysRun = true) - public void beforeClass() { - TOPIC_LIST.addAll(List.of(TOPIC_FOR_MESSAGES, TOPIC_FOR_CHECK_FILTERS, TOPIC_TO_CLEAR_AND_PURGE_MESSAGES, - TOPIC_TO_RECREATE, TOPIC_FOR_CHECK_MESSAGES_COUNT)); - TOPIC_LIST.forEach(topic -> apiService.createTopic(topic)); - IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS)); - waitUntilNewMinuteStarted(); - IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS)); - IntStream.range(1, 110).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_MESSAGES_COUNT)); - } - - @QaseId(222) - @Test(priority = 1) - public void produceMessage() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); - topicDetails - .openDetailsTab(MESSAGES); - produceMessage(TOPIC_FOR_MESSAGES); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicDetails.isKeyMessageVisible((TOPIC_FOR_MESSAGES.getMessageKey())), - "isKeyMessageVisible()"); - softly.assertTrue(topicDetails.isContentMessageVisible((TOPIC_FOR_MESSAGES.getMessageContent()).trim()), - "isContentMessageVisible()"); - softly.assertAll(); - } - - @QaseId(19) - @Test(priority = 2) - public void clearMessage() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); - topicDetails - .openDetailsTab(OVERVIEW); - int messageAmount = topicDetails.getMessageCountAmount(); - produceMessage(TOPIC_FOR_MESSAGES); - Assert.assertEquals(topicDetails.getMessageCountAmount(), messageAmount + 1, "getMessageCountAmount()"); - topicDetails - .openDotMenu() - .clickClearMessagesMenu() - .clickConfirmBtnMdl() - .waitUntilScreenReady(); - Assert.assertEquals(topicDetails.getMessageCountAmount(), 0, "getMessageCountAmount()"); - } - - @QaseId(239) - @Test(priority = 3) - public void checkClearTopicMessage() { - navigateToTopicsAndOpenDetails(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()); - topicDetails - .openDetailsTab(OVERVIEW); - produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES); - navigateToTopics(); - Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 1, - "getNumberOfMessages()"); - topicsList - .openDotMenuByTopicName(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()) - .clickClearMessagesBtn() - .clickConfirmBtnMdl(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, - String.format("%s messages have been successfully cleared!", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())), - "isAlertWithMessageVisible()"); - softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0, - "getNumberOfMessages()"); - softly.assertAll(); - } - - @QaseId(10) - @Test(priority = 4) - public void checkPurgeMessagePossibility() { - navigateToTopics(); - int messageAmount = topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(); - topicsList - .openTopic(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()); - topicDetails - .openDetailsTab(OVERVIEW); - produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES); - navigateToTopics(); - Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), - messageAmount + 1, "getNumberOfMessages()"); - topicsList - .getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()) - .selectItem(true) - .clickPurgeMessagesOfSelectedTopicsBtn(); - Assert.assertTrue(topicsList.isConfirmationMdlVisible(), "isConfirmationMdlVisible()"); - topicsList - .clickCancelBtnMdl() - .clickPurgeMessagesOfSelectedTopicsBtn() - .clickConfirmBtnMdl(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, - String.format("%s messages have been successfully cleared!", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())), - "isAlertWithMessageVisible()"); - softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0, - "getNumberOfMessages()"); - softly.assertAll(); - } - - @Ignore - @Issue("https://github.com/provectus/kafka-ui/issues/2394") - @QaseId(15) - @Test(priority = 6) - public void checkMessageFilteringByOffset() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); - topicDetails - .openDetailsTab(MESSAGES); - TopicDetails.MessageGridItem secondMessage = topicDetails.getMessageByOffset(1); - topicDetails - .selectSeekTypeDdlMessagesTab("Offset") - .setSeekTypeValueFldMessagesTab(String.valueOf(secondMessage.getOffset())) - .clickSubmitFiltersBtnMessagesTab(); - SoftAssert softly = new SoftAssert(); - topicDetails.getAllMessages().forEach(message -> - softly.assertTrue(message.getOffset() == secondMessage.getOffset() - || message.getOffset() > secondMessage.getOffset(), - String.format("Expected offset is: %s, but found: %s", secondMessage.getOffset(), message.getOffset()))); - softly.assertAll(); - } - - @Ignore - @Issue("https://github.com/provectus/kafka-ui/issues/3215") - @Issue("https://github.com/provectus/kafka-ui/issues/2345") - @QaseId(16) - @Test(priority = 7) - public void checkMessageFilteringByTimestamp() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); - topicDetails - .openDetailsTab(MESSAGES); - LocalDateTime firstTimestamp = topicDetails.getMessageByOffset(0).getTimestamp(); - List nextMessages = topicDetails.getAllMessages().stream() - .filter(message -> message.getTimestamp().getMinute() != firstTimestamp.getMinute()) - .toList(); - LocalDateTime nextTimestamp = Objects.requireNonNull(nextMessages.stream() - .findFirst().orElseThrow()).getTimestamp(); - topicDetails - .selectSeekTypeDdlMessagesTab("Timestamp") - .openCalendarSeekType() - .selectDateAndTimeByCalendar(nextTimestamp) - .clickSubmitFiltersBtnMessagesTab(); - SoftAssert softly = new SoftAssert(); - topicDetails.getAllMessages().forEach(message -> - softly.assertTrue(message.getTimestamp().isEqual(nextTimestamp) - || message.getTimestamp().isAfter(nextTimestamp), - String.format("Expected timestamp is: %s, but found: %s", nextTimestamp, message.getTimestamp()))); - softly.assertAll(); - } - - @Ignore - @Issue("https://github.com/provectus/kafka-ui/issues/2778") - @QaseId(246) - @Test(priority = 8) - public void checkClearTopicMessageFromOverviewTab() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); - topicDetails - .openDetailsTab(OVERVIEW) - .openDotMenu() - .clickClearMessagesMenu() - .clickConfirmBtnMdl(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, - String.format("%s messages have been successfully cleared!", TOPIC_FOR_CHECK_FILTERS.getName())), - "isAlertWithMessageVisible()"); - softly.assertEquals(topicDetails.getMessageCountAmount(), 0, - "getMessageCountAmount()= " + topicDetails.getMessageCountAmount()); - softly.assertAll(); - } - - @QaseId(240) - @Test(priority = 9) - public void checkRecreateTopic() { - navigateToTopicsAndOpenDetails(TOPIC_TO_RECREATE.getName()); - topicDetails - .openDetailsTab(OVERVIEW); - produceMessage(TOPIC_TO_RECREATE); - navigateToTopics(); - Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 1, - "getNumberOfMessages()"); - topicsList - .openDotMenuByTopicName(TOPIC_TO_RECREATE.getName()) - .clickRecreateTopicBtn() - .clickConfirmBtnMdl(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, - String.format("Topic %s successfully recreated!", TOPIC_TO_RECREATE.getName())), - "isAlertWithMessageVisible()"); - softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 0, - "getNumberOfMessages()"); - softly.assertAll(); - } - - @Ignore - @Issue("https://github.com/provectus/kafka-ui/issues/3129") - @QaseId(267) - @Test(priority = 10) - public void CheckMessagesCountPerPageWithinTopic() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_MESSAGES_COUNT.getName()); - topicDetails - .openDetailsTab(MESSAGES); - int messagesPerPage = topicDetails.getAllMessages().size(); - SoftAssert softly = new SoftAssert(); - softly.assertEquals(messagesPerPage, 100, "getAllMessages()"); - softly.assertFalse(topicDetails.isBackButtonEnabled(), "isBackButtonEnabled()"); - softly.assertTrue(topicDetails.isNextButtonEnabled(), "isNextButtonEnabled()"); - softly.assertAll(); - int lastOffsetOnPage = topicDetails.getAllMessages() - .get(messagesPerPage - 1).getOffset(); - topicDetails - .clickNextButton(); - softly.assertEquals(topicDetails.getAllMessages().stream().findFirst().orElseThrow().getOffset(), - lastOffsetOnPage + 1, "findFirst().getOffset()"); - softly.assertTrue(topicDetails.isBackButtonEnabled(), "isBackButtonEnabled()"); - softly.assertFalse(topicDetails.isNextButtonEnabled(), "isNextButtonEnabled()"); - softly.assertAll(); - } - - @Step - protected void produceMessage(Topic topic) { - topicDetails - .clickProduceMessageBtn(); - produceMessagePanel - .waitUntilScreenReady() - .setKeyField(topic.getMessageKey()) - .setContentFiled(topic.getMessageContent()) - .submitProduceMessage(); - topicDetails - .waitUntilScreenReady(); - } - - @AfterClass(alwaysRun = true) - public void afterClass() { - TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java deleted file mode 100644 index 0ce4d6c718..0000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokeSuite/topics/TopicsTest.java +++ /dev/null @@ -1,496 +0,0 @@ -package com.provectus.kafka.ui.smokeSuite.topics; - -import com.codeborne.selenide.Condition; -import com.provectus.kafka.ui.BaseTest; -import com.provectus.kafka.ui.models.Topic; -import io.qameta.allure.Issue; -import io.qase.api.annotation.QaseId; -import org.testng.Assert; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; -import org.testng.asserts.SoftAssert; - -import java.util.ArrayList; -import java.util.List; - -import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; -import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.*; -import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.COMPACT; -import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.DELETE; -import static com.provectus.kafka.ui.pages.topics.enums.CustomParameterType.COMPRESSION_TYPE; -import static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.*; -import static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_2_DAYS; -import static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_7_DAYS; -import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; -import static org.apache.commons.lang3.RandomUtils.nextInt; - -public class TopicsTest extends BaseTest { - - private static final Topic TOPIC_TO_CREATE = new Topic() - .setName("new-topic-" + randomAlphabetic(5)) - .setNumberOfPartitions(1) - .setCustomParameterType(COMPRESSION_TYPE) - .setCustomParameterValue("producer") - .setCleanupPolicyValue(DELETE); - private static final Topic TOPIC_TO_UPDATE_AND_DELETE = new Topic() - .setName("topic-to-update-and-delete-" + randomAlphabetic(5)) - .setNumberOfPartitions(1) - .setCleanupPolicyValue(DELETE) - .setTimeToRetain(BTN_7_DAYS) - .setMaxSizeOnDisk(NOT_SET) - .setMaxMessageBytes("1048588") - .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); - private static final Topic TOPIC_TO_CHECK_SETTINGS = new Topic() - .setName("new-topic-" + randomAlphabetic(5)) - .setNumberOfPartitions(1) - .setMaxMessageBytes("1000012") - .setMaxSizeOnDisk(NOT_SET); - private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic() - .setName("topic-for-check-filters-" + randomAlphabetic(5)); - private static final Topic TOPIC_FOR_DELETE = new Topic() - .setName("topic-to-delete-" + randomAlphabetic(5)); - private static final List TOPIC_LIST = new ArrayList<>(); - - @BeforeClass(alwaysRun = true) - public void beforeClass() { - TOPIC_LIST.addAll(List.of(TOPIC_TO_UPDATE_AND_DELETE, TOPIC_FOR_DELETE, TOPIC_FOR_CHECK_FILTERS)); - TOPIC_LIST.forEach(topic -> apiService.createTopic(topic)); - } - - @QaseId(199) - @Test(priority = 1) - public void createTopic() { - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(TOPIC_TO_CREATE.getName()) - .setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions()) - .selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue()) - .clickCreateTopicBtn(); - navigateToTopicsAndOpenDetails(TOPIC_TO_CREATE.getName()); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_CREATE.getName()), "isTopicHeaderVisible()"); - softly.assertEquals(topicDetails.getCleanUpPolicy(), TOPIC_TO_CREATE.getCleanupPolicyValue().toString(), "getCleanUpPolicy()"); - softly.assertEquals(topicDetails.getPartitions(), TOPIC_TO_CREATE.getNumberOfPartitions(), "getPartitions()"); - softly.assertAll(); - navigateToTopics(); - Assert.assertTrue(topicsList.isTopicVisible(TOPIC_TO_CREATE.getName()), "isTopicVisible()"); - TOPIC_LIST.add(TOPIC_TO_CREATE); - } - - @QaseId(7) - @Test(priority = 2) - void checkAvailableOperations() { - navigateToTopics(); - topicsList - .getTopicItem(TOPIC_TO_UPDATE_AND_DELETE.getName()) - .selectItem(true); - verifyElementsCondition(topicsList.getActionButtons(), Condition.enabled); - topicsList - .getTopicItem(TOPIC_FOR_CHECK_FILTERS.getName()) - .selectItem(true); - Assert.assertFalse(topicsList.isCopySelectedTopicBtnEnabled(), "isCopySelectedTopicBtnEnabled()"); - } - - @Ignore - @Issue("https://github.com/provectus/kafka-ui/issues/3071") - @QaseId(268) - @Test(priority = 3) - public void checkCustomParametersWithinEditExistingTopic() { - navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); - topicDetails - .openDotMenu() - .clickEditSettingsMenu(); - SoftAssert softly = new SoftAssert(); - topicCreateEditForm - .waitUntilScreenReady() - .clickAddCustomParameterTypeButton() - .openCustomParameterTypeDdl() - .getAllDdlOptions() - .forEach(option -> - softly.assertTrue(!option.is(Condition.attribute("disabled")), - option.getText() + " is enabled:")); - softly.assertAll(); - } - - @QaseId(197) - @Test(priority = 4) - public void updateTopic() { - navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); - topicDetails - .openDotMenu() - .clickEditSettingsMenu(); - topicCreateEditForm - .waitUntilScreenReady(); - SoftAssert softly = new SoftAssert(); - softly.assertEquals(topicCreateEditForm.getCleanupPolicy(), - TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), "getCleanupPolicy()"); - softly.assertEquals(topicCreateEditForm.getTimeToRetain(), - TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), "getTimeToRetain()"); - softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(), - TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), "getMaxSizeOnDisk()"); - softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(), - TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), "getMaxMessageBytes()"); - softly.assertAll(); - TOPIC_TO_UPDATE_AND_DELETE - .setCleanupPolicyValue(COMPACT) - .setTimeToRetain(BTN_2_DAYS) - .setMaxSizeOnDisk(SIZE_50_GB).setMaxMessageBytes("1048589"); - topicCreateEditForm - .selectCleanupPolicy((TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue())) - .setTimeToRetainDataByButtons(TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain()) - .setMaxSizeOnDiskInGB(TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk()) - .setMaxMessageBytes(TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes()) - .clickCreateTopicBtn(); - softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, "Topic successfully updated."), - "isAlertWithMessageVisible()"); - softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_UPDATE_AND_DELETE.getName()), - "isTopicHeaderVisible()"); - softly.assertAll(); - topicDetails - .waitUntilScreenReady(); - navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); - topicDetails - .openDotMenu() - .clickEditSettingsMenu(); - softly.assertFalse(topicCreateEditForm.isNameFieldEnabled(), "isNameFieldEnabled()"); - softly.assertEquals(topicCreateEditForm.getCleanupPolicy(), - TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), "getCleanupPolicy()"); - softly.assertEquals(topicCreateEditForm.getTimeToRetain(), - TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), "getTimeToRetain()"); - softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(), - TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), "getMaxSizeOnDisk()"); - softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(), - TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), "getMaxMessageBytes()"); - softly.assertAll(); - } - - @QaseId(242) - @Test(priority = 5) - public void removeTopicFromTopicList() { - navigateToTopics(); - topicsList - .openDotMenuByTopicName(TOPIC_TO_UPDATE_AND_DELETE.getName()) - .clickRemoveTopicBtn() - .clickConfirmBtnMdl(); - Assert.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, - String.format("Topic %s successfully deleted!", TOPIC_TO_UPDATE_AND_DELETE.getName())), - "isAlertWithMessageVisible()"); - TOPIC_LIST.remove(TOPIC_TO_UPDATE_AND_DELETE); - } - - @QaseId(207) - @Test(priority = 6) - public void deleteTopic() { - navigateToTopicsAndOpenDetails(TOPIC_FOR_DELETE.getName()); - topicDetails - .openDotMenu() - .clickDeleteTopicMenu() - .clickConfirmBtnMdl(); - navigateToTopics(); - Assert.assertFalse(topicsList.isTopicVisible(TOPIC_FOR_DELETE.getName()), "isTopicVisible"); - TOPIC_LIST.remove(TOPIC_FOR_DELETE); - } - - @QaseId(20) - @Test(priority = 7) - public void redirectToConsumerFromTopic() { - String topicName = "source-activities"; - String consumerGroupId = "connect-sink_postgres_activities"; - navigateToTopicsAndOpenDetails(topicName); - topicDetails - .openDetailsTab(CONSUMERS) - .openConsumerGroup(consumerGroupId); - consumersDetails - .waitUntilScreenReady(); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(consumersDetails.isRedirectedConsumerTitleVisible(consumerGroupId), - "isRedirectedConsumerTitleVisible()"); - softly.assertTrue(consumersDetails.isTopicInConsumersDetailsVisible(topicName), - "isTopicInConsumersDetailsVisible()"); - softly.assertAll(); - } - - @QaseId(4) - @Test(priority = 8) - public void checkTopicCreatePossibility() { - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady(); - Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); - topicCreateEditForm - .setTopicName("testName"); - Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); - topicCreateEditForm - .setTopicName(null) - .setNumberOfPartitions(nextInt(1, 10)); - Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); - topicCreateEditForm - .setTopicName("testName"); - Assert.assertTrue(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); - } - - @QaseId(266) - @Test(priority = 9) - public void checkTimeToRetainDataCustomValueWithEditingTopic() { - Topic topicToRetainData = new Topic() - .setName("topic-to-retain-data-" + randomAlphabetic(5)) - .setTimeToRetainData("86400000"); - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(topicToRetainData.getName()) - .setNumberOfPartitions(1) - .setTimeToRetainDataInMs("604800000"); - Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), "604800000", "getTimeToRetain()"); - topicCreateEditForm - .setTimeToRetainDataInMs(topicToRetainData.getTimeToRetainData()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady() - .openDotMenu() - .clickEditSettingsMenu(); - Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), topicToRetainData.getTimeToRetainData(), - "getTimeToRetain()"); - topicDetails - .openDetailsTab(SETTINGS); - Assert.assertEquals(topicDetails.getSettingsGridValueByKey("retention.ms"), topicToRetainData.getTimeToRetainData(), - "getSettingsGridValueByKey()"); - TOPIC_LIST.add(topicToRetainData); - } - - @QaseId(6) - @Test(priority = 10) - public void checkCustomParametersWithinCreateNewTopic() { - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(TOPIC_TO_CREATE.getName()) - .clickAddCustomParameterTypeButton() - .setCustomParameterType(TOPIC_TO_CREATE.getCustomParameterType()); - Assert.assertTrue(topicCreateEditForm.isDeleteCustomParameterButtonEnabled(), - "isDeleteCustomParameterButtonEnabled()"); - topicCreateEditForm - .clearCustomParameterValue(); - Assert.assertTrue(topicCreateEditForm.isValidationMessageCustomParameterValueVisible(), - "isValidationMessageCustomParameterValueVisible()"); - } - - @QaseId(2) - @Test(priority = 11) - public void checkTopicListElements() { - navigateToTopics(); - verifyElementsCondition(topicsList.getAllVisibleElements(), Condition.visible); - verifyElementsCondition(topicsList.getAllEnabledElements(), Condition.enabled); - } - - @QaseId(12) - @Test(priority = 12) - public void addNewFilterWithinTopic() { - String filterName = randomAlphabetic(5); - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); - topicDetails - .openDetailsTab(MESSAGES) - .clickMessagesAddFiltersBtn() - .waitUntilAddFiltersMdlVisible(); - verifyElementsCondition(topicDetails.getAllAddFilterModalVisibleElements(), Condition.visible); - verifyElementsCondition(topicDetails.getAllAddFilterModalEnabledElements(), Condition.enabled); - verifyElementsCondition(topicDetails.getAllAddFilterModalDisabledElements(), Condition.disabled); - Assert.assertFalse(topicDetails.isSaveThisFilterCheckBoxSelected(), "isSaveThisFilterCheckBoxSelected()"); - topicDetails - .setFilterCodeFieldAddFilterMdl(filterName); - Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(), "isAddFilterBtnAddFilterMdlEnabled()"); - topicDetails.clickAddFilterBtnAndCloseMdl(true); - Assert.assertTrue(topicDetails.isActiveFilterVisible(filterName), "isActiveFilterVisible()"); - } - - @QaseId(13) - @Test(priority = 13) - public void checkFilterSavingWithinSavedFilters() { - String displayName = randomAlphabetic(5); - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); - topicDetails - .openDetailsTab(MESSAGES) - .clickMessagesAddFiltersBtn() - .waitUntilAddFiltersMdlVisible() - .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4)) - .selectSaveThisFilterCheckboxMdl(true) - .setDisplayNameFldAddFilterMdl(displayName); - Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(), - "isAddFilterBtnAddFilterMdlEnabled()"); - topicDetails - .clickAddFilterBtnAndCloseMdl(false) - .openSavedFiltersListMdl(); - Assert.assertTrue(topicDetails.isFilterVisibleAtSavedFiltersMdl(displayName), - "isFilterVisibleAtSavedFiltersMdl()"); - } - - @QaseId(14) - @Test(priority = 14) - public void checkApplyingSavedFilterWithinTopicMessages() { - String displayName = randomAlphabetic(5); - navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); - topicDetails - .openDetailsTab(MESSAGES) - .clickMessagesAddFiltersBtn() - .waitUntilAddFiltersMdlVisible() - .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4)) - .selectSaveThisFilterCheckboxMdl(true) - .setDisplayNameFldAddFilterMdl(displayName) - .clickAddFilterBtnAndCloseMdl(false) - .openSavedFiltersListMdl() - .selectFilterAtSavedFiltersMdl(displayName) - .clickSelectFilterBtnAtSavedFiltersMdl(); - Assert.assertTrue(topicDetails.isActiveFilterVisible(displayName), "isActiveFilterVisible()"); - } - - @QaseId(11) - @Test(priority = 15) - public void checkShowInternalTopicsButton() { - navigateToTopics(); - topicsList - .setShowInternalRadioButton(true); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicsList.getInternalTopics().size() > 0, "getInternalTopics()"); - softly.assertTrue(topicsList.getNonInternalTopics().size() > 0, "getNonInternalTopics()"); - softly.assertAll(); - topicsList - .setShowInternalRadioButton(false); - softly.assertEquals(topicsList.getInternalTopics().size(), 0, "getInternalTopics()"); - softly.assertTrue(topicsList.getNonInternalTopics().size() > 0, "getNonInternalTopics()"); - softly.assertAll(); - } - - @QaseId(334) - @Test(priority = 16) - public void checkInternalTopicsNaming() { - navigateToTopics(); - SoftAssert softly = new SoftAssert(); - topicsList - .setShowInternalRadioButton(true) - .getInternalTopics() - .forEach(topic -> softly.assertTrue(topic.getName().startsWith("_"), - String.format("'%s' starts with '_'", topic.getName()))); - softly.assertAll(); - } - - @QaseId(56) - @Test(priority = 17) - public void checkRetentionBytesAccordingToMaxSizeOnDisk() { - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(TOPIC_TO_CHECK_SETTINGS.getName()) - .setNumberOfPartitions(TOPIC_TO_CHECK_SETTINGS.getNumberOfPartitions()) - .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady(); - TOPIC_LIST.add(TOPIC_TO_CHECK_SETTINGS); - topicDetails - .openDetailsTab(SETTINGS); - topicSettingsTab - .waitUntilScreenReady(); - SoftAssert softly = new SoftAssert(); - softly.assertEquals(topicSettingsTab.getValueByKey("retention.bytes"), - TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), "getValueOfKey(retention.bytes)"); - softly.assertEquals(topicSettingsTab.getValueByKey("max.message.bytes"), - TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), "getValueOfKey(max.message.bytes)"); - softly.assertAll(); - TOPIC_TO_CHECK_SETTINGS - .setMaxSizeOnDisk(SIZE_1_GB) - .setMaxMessageBytes("1000056"); - topicDetails - .openDotMenu() - .clickEditSettingsMenu(); - topicCreateEditForm - .waitUntilScreenReady() - .setMaxSizeOnDiskInGB(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk()) - .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady() - .openDetailsTab(SETTINGS); - topicSettingsTab - .waitUntilScreenReady(); - softly.assertEquals(topicSettingsTab.getValueByKey("retention.bytes"), - TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), "getValueOfKey(retention.bytes)"); - softly.assertEquals(topicSettingsTab.getValueByKey("max.message.bytes"), - TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), "getValueOfKey(max.message.bytes)"); - softly.assertAll(); - } - - @QaseId(247) - @Test(priority = 18) - public void recreateTopicFromTopicProfile() { - Topic topicToRecreate = new Topic() - .setName("topic-to-recreate-" + randomAlphabetic(5)) - .setNumberOfPartitions(1); - navigateToTopics(); - topicsList - .clickAddTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady() - .setTopicName(topicToRecreate.getName()) - .setNumberOfPartitions(topicToRecreate.getNumberOfPartitions()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady(); - TOPIC_LIST.add(topicToRecreate); - topicDetails - .openDotMenu() - .clickRecreateTopicMenu(); - Assert.assertTrue(topicDetails.isConfirmationMdlVisible(), "isConfirmationMdlVisible()"); - topicDetails - .clickConfirmBtnMdl(); - Assert.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, - String.format("Topic %s successfully recreated!", topicToRecreate.getName())), - "isAlertWithMessageVisible()"); - } - - @QaseId(8) - @Test(priority = 19) - public void checkCopyTopicPossibility() { - Topic topicToCopy = new Topic() - .setName("topic-to-copy-" + randomAlphabetic(5)) - .setNumberOfPartitions(1); - navigateToTopics(); - topicsList - .getAnyNonInternalTopic() - .selectItem(true) - .clickCopySelectedTopicBtn(); - topicCreateEditForm - .waitUntilScreenReady(); - Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); - topicCreateEditForm - .setTopicName(topicToCopy.getName()) - .setNumberOfPartitions(topicToCopy.getNumberOfPartitions()) - .clickCreateTopicBtn(); - topicDetails - .waitUntilScreenReady(); - TOPIC_LIST.add(topicToCopy); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, "Topic successfully created."), - "isAlertWithMessageVisible()"); - softly.assertTrue(topicDetails.isTopicHeaderVisible(topicToCopy.getName()), "isTopicHeaderVisible()"); - softly.assertAll(); - } - - @AfterClass(alwaysRun = true) - public void afterClass() { - TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/SmokeTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/SmokeTest.java new file mode 100644 index 0000000000..5193ecb25e --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/SmokeTest.java @@ -0,0 +1,115 @@ +package com.provectus.kafka.ui.smokesuite; + +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS; +import static com.provectus.kafka.ui.settings.BaseSource.BASE_HOST; +import static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString; +import static com.provectus.kafka.ui.variables.Url.BROKERS_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.CONSUMERS_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.KAFKA_CONNECT_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.KSQL_DB_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.SCHEMA_REGISTRY_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.TOPICS_LIST_URL; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.WebDriverRunner; +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Connector; +import com.provectus.kafka.ui.models.Schema; +import com.provectus.kafka.ui.models.Topic; +import com.provectus.kafka.ui.pages.panels.enums.MenuItem; +import io.qameta.allure.Step; +import io.qase.api.annotation.QaseId; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class SmokeTest extends BaseTest { + + private static final int BROKER_ID = 1; + private static final Schema TEST_SCHEMA = Schema.createSchemaAvro(); + private static final Topic TEST_TOPIC = new Topic() + .setName("new-topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1); + private static final Connector TEST_CONNECTOR = new Connector() + .setName("new-connector-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("testData/connectors/config_for_create_connector_via_api.json")); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + apiService + .createTopic(TEST_TOPIC) + .createSchema(TEST_SCHEMA) + .createConnector(TEST_CONNECTOR); + } + + @QaseId(198) + @Test + public void checkBasePageElements() { + verifyElementsCondition( + Stream.concat(topPanel.getAllVisibleElements().stream(), naviSideBar.getAllMenuButtons().stream()) + .collect(Collectors.toList()), Condition.visible); + verifyElementsCondition( + Stream.concat(topPanel.getAllEnabledElements().stream(), naviSideBar.getAllMenuButtons().stream()) + .collect(Collectors.toList()), Condition.enabled); + } + + @QaseId(45) + @Test + public void checkUrlWhileNavigating() { + navigateToBrokers(); + verifyCurrentUrl(BROKERS_LIST_URL); + navigateToTopics(); + verifyCurrentUrl(TOPICS_LIST_URL); + navigateToConsumers(); + verifyCurrentUrl(CONSUMERS_LIST_URL); + navigateToSchemaRegistry(); + verifyCurrentUrl(SCHEMA_REGISTRY_LIST_URL); + navigateToConnectors(); + verifyCurrentUrl(KAFKA_CONNECT_LIST_URL); + navigateToKsqlDb(); + verifyCurrentUrl(KSQL_DB_LIST_URL); + } + + @QaseId(46) + @Test + public void checkPathWhileNavigating() { + navigateToBrokersAndOpenDetails(BROKER_ID); + verifyComponentsPath(BROKERS, String.format("Broker %d", BROKER_ID)); + navigateToTopicsAndOpenDetails(TEST_TOPIC.getName()); + verifyComponentsPath(TOPICS, TEST_TOPIC.getName()); + navigateToSchemaRegistryAndOpenDetails(TEST_SCHEMA.getName()); + verifyComponentsPath(SCHEMA_REGISTRY, TEST_SCHEMA.getName()); + navigateToConnectorsAndOpenDetails(TEST_CONNECTOR.getName()); + verifyComponentsPath(KAFKA_CONNECT, TEST_CONNECTOR.getName()); + } + + @Step + private void verifyCurrentUrl(String expectedUrl) { + String urlWithoutParameters = WebDriverRunner.getWebDriver().getCurrentUrl(); + if (urlWithoutParameters.contains("?")) { + urlWithoutParameters = urlWithoutParameters.substring(0, urlWithoutParameters.indexOf("?")); + } + Assert.assertEquals(urlWithoutParameters, String.format(expectedUrl, BASE_HOST), "getCurrentUrl()"); + } + + @Step + private void verifyComponentsPath(MenuItem menuItem, String expectedPath) { + Assert.assertEquals(naviSideBar.getPagePath(menuItem), expectedPath, + String.format("getPagePath() for %s", menuItem.getPageTitle().toUpperCase())); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + apiService + .deleteTopic(TEST_TOPIC.getName()) + .deleteSchema(TEST_SCHEMA.getName()) + .deleteConnector(TEST_CONNECTOR.getName()); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java new file mode 100644 index 0000000000..8fd1ffde15 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java @@ -0,0 +1,41 @@ +package com.provectus.kafka.ui.smokesuite.brokers; + +import static com.provectus.kafka.ui.pages.brokers.BrokersDetails.DetailsTab.CONFIGS; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.BaseTest; +import io.qase.api.annotation.QaseId; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class BrokersTest extends BaseTest { + + @QaseId(1) + @Test + public void checkBrokersOverview() { + navigateToBrokers(); + Assert.assertTrue(brokersList.getAllBrokers().size() > 0, "getAllBrokers()"); + verifyElementsCondition(brokersList.getAllVisibleElements(), Condition.visible); + verifyElementsCondition(brokersList.getAllEnabledElements(), Condition.enabled); + } + + @QaseId(85) + @Test + public void checkExistingBrokersInCluster() { + navigateToBrokers(); + Assert.assertTrue(brokersList.getAllBrokers().size() > 0, "getAllBrokers()"); + brokersList + .openBroker(1); + brokersDetails + .waitUntilScreenReady(); + verifyElementsCondition(brokersDetails.getAllVisibleElements(), Condition.visible); + verifyElementsCondition(brokersDetails.getAllEnabledElements(), Condition.enabled); + brokersDetails + .openDetailsTab(CONFIGS); + brokersConfigTab + .waitUntilScreenReady(); + verifyElementsCondition(brokersConfigTab.getColumnHeaders(), Condition.visible); + verifyElementsCondition(brokersConfigTab.getEditButtons(), Condition.enabled); + Assert.assertTrue(brokersConfigTab.isSearchByKeyVisible(), "isSearchByKeyVisible()"); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java new file mode 100644 index 0000000000..72fe769e31 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java @@ -0,0 +1,107 @@ +package com.provectus.kafka.ui.smokesuite.connectors; + +import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; +import static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Connector; +import com.provectus.kafka.ui.models.Topic; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class ConnectorsTest extends BaseTest { + + private static final List TOPIC_LIST = new ArrayList<>(); + private static final List CONNECTOR_LIST = new ArrayList<>(); + private static final String MESSAGE_CONTENT = "testData/topics/message_content_create_topic.json"; + private static final String MESSAGE_KEY = " "; + private static final Topic TOPIC_FOR_CREATE = new Topic() + .setName("topic-for-create-connector-" + randomAlphabetic(5)) + .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + private static final Topic TOPIC_FOR_DELETE = new Topic() + .setName("topic-for-delete-connector-" + randomAlphabetic(5)) + .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + private static final Topic TOPIC_FOR_UPDATE = new Topic() + .setName("topic-for-update-connector-" + randomAlphabetic(5)) + .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + private static final Connector CONNECTOR_FOR_DELETE = new Connector() + .setName("connector-for-delete-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("testData/connectors/delete_connector_config.json")); + private static final Connector CONNECTOR_FOR_UPDATE = new Connector() + .setName("connector-for-update-and-delete-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("testData/connectors/config_for_create_connector_via_api.json")); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + TOPIC_LIST.addAll(List.of(TOPIC_FOR_CREATE, TOPIC_FOR_DELETE, TOPIC_FOR_UPDATE)); + TOPIC_LIST.forEach(topic -> apiService + .createTopic(topic) + .sendMessage(topic) + ); + CONNECTOR_LIST.addAll(List.of(CONNECTOR_FOR_DELETE, CONNECTOR_FOR_UPDATE)); + CONNECTOR_LIST.forEach(connector -> apiService.createConnector(connector)); + } + + @QaseId(42) + @Test + public void createConnector() { + Connector connectorForCreate = new Connector() + .setName("connector-for-create-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("testData/connectors/config_for_create_connector.json")); + navigateToConnectors(); + kafkaConnectList + .clickCreateConnectorBtn(); + connectorCreateForm + .waitUntilScreenReady() + .setConnectorDetails(connectorForCreate.getName(), connectorForCreate.getConfig()) + .clickSubmitButton(); + connectorDetails + .waitUntilScreenReady(); + navigateToConnectorsAndOpenDetails(connectorForCreate.getName()); + Assert.assertTrue(connectorDetails.isConnectorHeaderVisible(connectorForCreate.getName()), + "isConnectorTitleVisible()"); + navigateToConnectors(); + Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); + CONNECTOR_LIST.add(connectorForCreate); + } + + @QaseId(196) + @Test + public void updateConnector() { + navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_UPDATE.getName()); + connectorDetails + .openConfigTab() + .setConfig(CONNECTOR_FOR_UPDATE.getConfig()) + .clickSubmitButton(); + Assert.assertTrue(connectorDetails.isAlertWithMessageVisible(SUCCESS, "Config successfully updated."), + "isAlertWithMessageVisible()"); + navigateToConnectors(); + Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()), "isConnectorVisible()"); + } + + @QaseId(195) + @Test + public void deleteConnector() { + navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_DELETE.getName()); + connectorDetails + .openDotMenu() + .clickDeleteBtn() + .clickConfirmBtn(); + navigateToConnectors(); + Assert.assertFalse(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); + CONNECTOR_LIST.remove(CONNECTOR_FOR_DELETE); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + CONNECTOR_LIST.forEach(connector -> + apiService.deleteConnector(connector.getName())); + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java new file mode 100644 index 0000000000..d8bda606dc --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java @@ -0,0 +1,79 @@ +package com.provectus.kafka.ui.smokesuite.ksqldb; + +import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.pages.ksqldb.models.Stream; +import com.provectus.kafka.ui.pages.ksqldb.models.Table; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +public class KsqlDbTest extends BaseTest { + + private static final Stream STREAM_FOR_CHECK_TABLES = new Stream() + .setName("STREAM_FOR_CHECK_TABLES_" + randomAlphabetic(4).toUpperCase()) + .setTopicName("TOPIC_FOR_STREAM_" + randomAlphabetic(4).toUpperCase()); + private static final Table FIRST_TABLE = new Table() + .setName("FIRST_TABLE" + randomAlphabetic(4).toUpperCase()) + .setStreamName(STREAM_FOR_CHECK_TABLES.getName()); + private static final Table SECOND_TABLE = new Table() + .setName("SECOND_TABLE" + randomAlphabetic(4).toUpperCase()) + .setStreamName(STREAM_FOR_CHECK_TABLES.getName()); + private static final List TOPIC_NAMES_LIST = new ArrayList<>(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + apiService + .createStream(STREAM_FOR_CHECK_TABLES) + .createTables(FIRST_TABLE, SECOND_TABLE); + TOPIC_NAMES_LIST.addAll(List.of(STREAM_FOR_CHECK_TABLES.getTopicName(), + FIRST_TABLE.getName(), SECOND_TABLE.getName())); + } + + @QaseId(41) + @Test(priority = 1) + public void checkShowTablesRequestExecution() { + navigateToKsqlDb(); + ksqlDbList + .clickExecuteKsqlRequestBtn(); + ksqlQueryForm + .waitUntilScreenReady() + .setQuery(SHOW_TABLES.getQuery()) + .clickExecuteBtn(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertTrue(ksqlQueryForm.getTableByName(FIRST_TABLE.getName()).isVisible(), "getTableName()"); + softly.assertTrue(ksqlQueryForm.getTableByName(SECOND_TABLE.getName()).isVisible(), "getTableName()"); + softly.assertAll(); + } + + @QaseId(86) + @Test(priority = 2) + public void clearResultsForExecutedRequest() { + navigateToKsqlDb(); + ksqlDbList + .clickExecuteKsqlRequestBtn(); + ksqlQueryForm + .waitUntilScreenReady() + .setQuery(SHOW_TABLES.getQuery()) + .clickExecuteBtn(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertAll(); + ksqlQueryForm + .clickClearResultsBtn(); + softly.assertFalse(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertAll(); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName)); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/schemas/SchemasTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/schemas/SchemasTest.java new file mode 100644 index 0000000000..0fc77e1f4b --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/schemas/SchemasTest.java @@ -0,0 +1,190 @@ +package com.provectus.kafka.ui.smokesuite.schemas; + +import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.api.model.CompatibilityLevel; +import com.provectus.kafka.ui.models.Schema; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +public class SchemasTest extends BaseTest { + + private static final List SCHEMA_LIST = new ArrayList<>(); + private static final Schema AVRO_API = Schema.createSchemaAvro(); + private static final Schema JSON_API = Schema.createSchemaJson(); + private static final Schema PROTOBUF_API = Schema.createSchemaProtobuf(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + SCHEMA_LIST.addAll(List.of(AVRO_API, JSON_API, PROTOBUF_API)); + SCHEMA_LIST.forEach(schema -> apiService.createSchema(schema)); + } + + @QaseId(43) + @Test(priority = 1) + public void createSchemaAvro() { + Schema schemaAvro = Schema.createSchemaAvro(); + navigateToSchemaRegistry(); + schemaRegistryList + .clickCreateSchema(); + schemaCreateForm + .setSubjectName(schemaAvro.getName()) + .setSchemaField(fileToString(schemaAvro.getValuePath())) + .selectSchemaTypeFromDropdown(schemaAvro.getType()) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaAvro.getName()), "isSchemaHeaderVisible()"); + softly.assertEquals(schemaDetails.getSchemaType(), schemaAvro.getType().getValue(), "getSchemaType()"); + softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), + "getCompatibility()"); + softly.assertAll(); + navigateToSchemaRegistry(); + Assert.assertTrue(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.add(schemaAvro); + } + + @QaseId(186) + @Test(priority = 2) + public void updateSchemaAvro() { + AVRO_API.setValuePath( + System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_avro_for_update.json"); + navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); + schemaDetails + .openEditSchema(); + schemaCreateForm + .waitUntilScreenReady(); + verifyElementsCondition(schemaCreateForm.getAllDetailsPageElements(), Condition.visible); + SoftAssert softly = new SoftAssert(); + softly.assertFalse(schemaCreateForm.isSubmitBtnEnabled(), "isSubmitBtnEnabled()"); + softly.assertFalse(schemaCreateForm.isSchemaDropDownEnabled(), "isSchemaDropDownEnabled()"); + softly.assertAll(); + schemaCreateForm + .selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum.NONE) + .setNewSchemaValue(fileToString(AVRO_API.getValuePath())) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + Assert.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.NONE.toString(), + "getCompatibility()"); + } + + @QaseId(44) + @Test(priority = 3) + public void compareVersionsOperation() { + navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); + int latestVersion = schemaDetails + .waitUntilScreenReady() + .getLatestVersion(); + schemaDetails + .openCompareVersionMenu(); + int versionsNumberFromDdl = schemaCreateForm + .waitUntilScreenReady() + .openLeftVersionDdl() + .getVersionsNumberFromList(); + Assert.assertEquals(versionsNumberFromDdl, latestVersion, "Versions number is not matched"); + schemaCreateForm + .selectVersionFromDropDown(1); + Assert.assertEquals(schemaCreateForm.getMarkedLinesNumber(), 42, "getAllMarkedLines()"); + } + + @QaseId(187) + @Test(priority = 4) + public void deleteSchemaAvro() { + navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); + schemaDetails + .removeSchema(); + schemaRegistryList + .waitUntilScreenReady(); + Assert.assertFalse(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.remove(AVRO_API); + } + + @QaseId(89) + @Test(priority = 5) + public void createSchemaJson() { + Schema schemaJson = Schema.createSchemaJson(); + navigateToSchemaRegistry(); + schemaRegistryList + .clickCreateSchema(); + schemaCreateForm + .setSubjectName(schemaJson.getName()) + .setSchemaField(fileToString(schemaJson.getValuePath())) + .selectSchemaTypeFromDropdown(schemaJson.getType()) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaJson.getName()), "isSchemaHeaderVisible()"); + softly.assertEquals(schemaDetails.getSchemaType(), schemaJson.getType().getValue(), "getSchemaType()"); + softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), + "getCompatibility()"); + softly.assertAll(); + navigateToSchemaRegistry(); + Assert.assertTrue(schemaRegistryList.isSchemaVisible(JSON_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.add(schemaJson); + } + + @QaseId(189) + @Test(priority = 6) + public void deleteSchemaJson() { + navigateToSchemaRegistryAndOpenDetails(JSON_API.getName()); + schemaDetails + .removeSchema(); + schemaRegistryList + .waitUntilScreenReady(); + Assert.assertFalse(schemaRegistryList.isSchemaVisible(JSON_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.remove(JSON_API); + } + + @QaseId(91) + @Test(priority = 7) + public void createSchemaProtobuf() { + Schema schemaProtobuf = Schema.createSchemaProtobuf(); + navigateToSchemaRegistry(); + schemaRegistryList + .clickCreateSchema(); + schemaCreateForm + .setSubjectName(schemaProtobuf.getName()) + .setSchemaField(fileToString(schemaProtobuf.getValuePath())) + .selectSchemaTypeFromDropdown(schemaProtobuf.getType()) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaProtobuf.getName()), "isSchemaHeaderVisible()"); + softly.assertEquals(schemaDetails.getSchemaType(), schemaProtobuf.getType().getValue(), "getSchemaType()"); + softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), + "getCompatibility()"); + softly.assertAll(); + navigateToSchemaRegistry(); + Assert.assertTrue(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.add(schemaProtobuf); + } + + @QaseId(223) + @Test(priority = 8) + public void deleteSchemaProtobuf() { + navigateToSchemaRegistryAndOpenDetails(PROTOBUF_API.getName()); + schemaDetails + .removeSchema(); + schemaRegistryList + .waitUntilScreenReady(); + Assert.assertFalse(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.remove(PROTOBUF_API); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + SCHEMA_LIST.forEach(schema -> apiService.deleteSchema(schema.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java new file mode 100644 index 0000000000..2404838698 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java @@ -0,0 +1,279 @@ +package com.provectus.kafka.ui.smokesuite.topics; + +import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW; +import static com.provectus.kafka.ui.utilities.TimeUtils.waitUntilNewMinuteStarted; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Topic; +import com.provectus.kafka.ui.pages.topics.TopicDetails; +import io.qameta.allure.Issue; +import io.qameta.allure.Step; +import io.qase.api.annotation.QaseId; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +public class MessagesTest extends BaseTest { + + private static final Topic TOPIC_FOR_MESSAGES = new Topic() + .setName("topic-with-clean-message-attribute-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_TO_CLEAR_AND_PURGE_MESSAGES = new Topic() + .setName("topic-to-clear-and-purge-messages-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic() + .setName("topic-for-check-filters-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_TO_RECREATE = new Topic() + .setName("topic-to-recreate-attribute-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_FOR_CHECK_MESSAGES_COUNT = new Topic() + .setName("topic-for-check-messages-count" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final List TOPIC_LIST = new ArrayList<>(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + TOPIC_LIST.addAll(List.of(TOPIC_FOR_MESSAGES, TOPIC_FOR_CHECK_FILTERS, TOPIC_TO_CLEAR_AND_PURGE_MESSAGES, + TOPIC_TO_RECREATE, TOPIC_FOR_CHECK_MESSAGES_COUNT)); + TOPIC_LIST.forEach(topic -> apiService.createTopic(topic)); + IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS)); + waitUntilNewMinuteStarted(); + IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS)); + IntStream.range(1, 110).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_MESSAGES_COUNT)); + } + + @QaseId(222) + @Test(priority = 1) + public void produceMessageCheck() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); + topicDetails + .openDetailsTab(MESSAGES); + produceMessage(TOPIC_FOR_MESSAGES); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isKeyMessageVisible((TOPIC_FOR_MESSAGES.getMessageKey())), + "isKeyMessageVisible()"); + softly.assertTrue(topicDetails.isContentMessageVisible((TOPIC_FOR_MESSAGES.getMessageContent()).trim()), + "isContentMessageVisible()"); + softly.assertAll(); + } + + @QaseId(19) + @Test(priority = 2) + public void clearMessageCheck() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + int messageAmount = topicDetails.getMessageCountAmount(); + produceMessage(TOPIC_FOR_MESSAGES); + Assert.assertEquals(topicDetails.getMessageCountAmount(), messageAmount + 1, "getMessageCountAmount()"); + topicDetails + .openDotMenu() + .clickClearMessagesMenu() + .clickConfirmBtnMdl() + .waitUntilScreenReady(); + Assert.assertEquals(topicDetails.getMessageCountAmount(), 0, "getMessageCountAmount()"); + } + + @QaseId(239) + @Test(priority = 3) + public void checkClearTopicMessage() { + navigateToTopicsAndOpenDetails(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES); + navigateToTopics(); + Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 1, + "getNumberOfMessages()"); + topicsList + .openDotMenuByTopicName(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()) + .clickClearMessagesBtn() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, + String.format("%s messages have been successfully cleared!", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0, + "getNumberOfMessages()"); + softly.assertAll(); + } + + @QaseId(10) + @Test(priority = 4) + public void checkPurgeMessagePossibility() { + navigateToTopics(); + int messageAmount = topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(); + topicsList + .openTopic(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES); + navigateToTopics(); + Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), + messageAmount + 1, "getNumberOfMessages()"); + topicsList + .getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()) + .selectItem(true) + .clickPurgeMessagesOfSelectedTopicsBtn(); + Assert.assertTrue(topicsList.isConfirmationMdlVisible(), "isConfirmationMdlVisible()"); + topicsList + .clickCancelBtnMdl() + .clickPurgeMessagesOfSelectedTopicsBtn() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, + String.format("%s messages have been successfully cleared!", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0, + "getNumberOfMessages()"); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/2394") + @QaseId(15) + @Test(priority = 6) + public void checkMessageFilteringByOffset() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES); + TopicDetails.MessageGridItem secondMessage = topicDetails.getMessageByOffset(1); + topicDetails + .selectSeekTypeDdlMessagesTab("Offset") + .setSeekTypeValueFldMessagesTab(String.valueOf(secondMessage.getOffset())) + .clickSubmitFiltersBtnMessagesTab(); + SoftAssert softly = new SoftAssert(); + topicDetails.getAllMessages().forEach(message -> + softly.assertTrue(message.getOffset() == secondMessage.getOffset() + || message.getOffset() > secondMessage.getOffset(), + String.format("Expected offset is: %s, but found: %s", secondMessage.getOffset(), message.getOffset()))); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3215") + @Issue("https://github.com/provectus/kafka-ui/issues/2345") + @QaseId(16) + @Test(priority = 7) + public void checkMessageFilteringByTimestamp() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES); + LocalDateTime firstTimestamp = topicDetails.getMessageByOffset(0).getTimestamp(); + List nextMessages = topicDetails.getAllMessages().stream() + .filter(message -> message.getTimestamp().getMinute() != firstTimestamp.getMinute()) + .toList(); + LocalDateTime nextTimestamp = nextMessages.stream() + .findFirst().orElseThrow().getTimestamp(); + topicDetails + .selectSeekTypeDdlMessagesTab("Timestamp") + .openCalendarSeekType() + .selectDateAndTimeByCalendar(nextTimestamp) + .clickSubmitFiltersBtnMessagesTab(); + SoftAssert softly = new SoftAssert(); + topicDetails.getAllMessages().forEach(message -> + softly.assertTrue(message.getTimestamp().isEqual(nextTimestamp) + || message.getTimestamp().isAfter(nextTimestamp), + String.format("Expected that %s is not before %s.", message.getTimestamp(), nextTimestamp))); + softly.assertAll(); + } + + @QaseId(246) + @Test(priority = 8) + public void checkClearTopicMessageFromOverviewTab() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(OVERVIEW) + .openDotMenu() + .clickClearMessagesMenu() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, + String.format("%s messages have been successfully cleared!", TOPIC_FOR_CHECK_FILTERS.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicDetails.getMessageCountAmount(), 0, + "getMessageCountAmount()= " + topicDetails.getMessageCountAmount()); + softly.assertAll(); + } + + @QaseId(240) + @Test(priority = 9) + public void checkRecreateTopic() { + navigateToTopicsAndOpenDetails(TOPIC_TO_RECREATE.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + produceMessage(TOPIC_TO_RECREATE); + navigateToTopics(); + Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 1, + "getNumberOfMessages()"); + topicsList + .openDotMenuByTopicName(TOPIC_TO_RECREATE.getName()) + .clickRecreateTopicBtn() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, + String.format("Topic %s successfully recreated!", TOPIC_TO_RECREATE.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 0, + "getNumberOfMessages()"); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3129") + @QaseId(267) + @Test(priority = 10) + public void checkMessagesCountPerPageWithinTopic() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_MESSAGES_COUNT.getName()); + topicDetails + .openDetailsTab(MESSAGES); + int messagesPerPage = topicDetails.getAllMessages().size(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(messagesPerPage, 100, "getAllMessages()"); + softly.assertFalse(topicDetails.isBackButtonEnabled(), "isBackButtonEnabled()"); + softly.assertTrue(topicDetails.isNextButtonEnabled(), "isNextButtonEnabled()"); + softly.assertAll(); + int lastOffsetOnPage = topicDetails.getAllMessages() + .get(messagesPerPage - 1).getOffset(); + topicDetails + .clickNextButton(); + softly.assertEquals(topicDetails.getAllMessages().stream().findFirst().orElseThrow().getOffset(), + lastOffsetOnPage + 1, "findFirst().getOffset()"); + softly.assertTrue(topicDetails.isBackButtonEnabled(), "isBackButtonEnabled()"); + softly.assertFalse(topicDetails.isNextButtonEnabled(), "isNextButtonEnabled()"); + softly.assertAll(); + } + + @Step + private void produceMessage(Topic topic) { + topicDetails + .clickProduceMessageBtn(); + produceMessagePanel + .waitUntilScreenReady() + .setKeyField(topic.getMessageKey()) + .setContentFiled(topic.getMessageContent()) + .submitProduceMessage(); + topicDetails + .waitUntilScreenReady(); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java new file mode 100644 index 0000000000..319c5f74f9 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java @@ -0,0 +1,500 @@ +package com.provectus.kafka.ui.smokesuite.topics; + +import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.CONSUMERS; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.SETTINGS; +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.COMPACT; +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.DELETE; +import static com.provectus.kafka.ui.pages.topics.enums.CustomParameterType.COMPRESSION_TYPE; +import static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.NOT_SET; +import static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.SIZE_1_GB; +import static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.SIZE_50_GB; +import static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_2_DAYS; +import static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_7_DAYS; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang3.RandomUtils.nextInt; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Topic; +import io.qameta.allure.Issue; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +public class TopicsTest extends BaseTest { + + private static final Topic TOPIC_TO_CREATE = new Topic() + .setName("new-topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setCustomParameterType(COMPRESSION_TYPE) + .setCustomParameterValue("producer") + .setCleanupPolicyValue(DELETE); + private static final Topic TOPIC_TO_UPDATE_AND_DELETE = new Topic() + .setName("topic-to-update-and-delete-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setCleanupPolicyValue(DELETE) + .setTimeToRetain(BTN_7_DAYS) + .setMaxSizeOnDisk(NOT_SET) + .setMaxMessageBytes("1048588") + .setMessageKey(randomAlphabetic(5)) + .setMessageContent(randomAlphabetic(10)); + private static final Topic TOPIC_TO_CHECK_SETTINGS = new Topic() + .setName("new-topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setMaxMessageBytes("1000012") + .setMaxSizeOnDisk(NOT_SET); + private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic() + .setName("topic-for-check-filters-" + randomAlphabetic(5)); + private static final Topic TOPIC_FOR_DELETE = new Topic() + .setName("topic-to-delete-" + randomAlphabetic(5)); + private static final List TOPIC_LIST = new ArrayList<>(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + TOPIC_LIST.addAll(List.of(TOPIC_TO_UPDATE_AND_DELETE, TOPIC_FOR_DELETE, TOPIC_FOR_CHECK_FILTERS)); + TOPIC_LIST.forEach(topic -> apiService.createTopic(topic)); + } + + @QaseId(199) + @Test(priority = 1) + public void createTopic() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(TOPIC_TO_CREATE.getName()) + .setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions()) + .selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue()) + .clickSaveTopicBtn(); + navigateToTopicsAndOpenDetails(TOPIC_TO_CREATE.getName()); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_CREATE.getName()), "isTopicHeaderVisible()"); + softly.assertEquals(topicDetails.getCleanUpPolicy(), TOPIC_TO_CREATE.getCleanupPolicyValue().toString(), + "getCleanUpPolicy()"); + softly.assertEquals(topicDetails.getPartitions(), TOPIC_TO_CREATE.getNumberOfPartitions(), "getPartitions()"); + softly.assertAll(); + navigateToTopics(); + Assert.assertTrue(topicsList.isTopicVisible(TOPIC_TO_CREATE.getName()), "isTopicVisible()"); + TOPIC_LIST.add(TOPIC_TO_CREATE); + } + + @QaseId(7) + @Test(priority = 2) + void checkAvailableOperations() { + navigateToTopics(); + topicsList + .getTopicItem(TOPIC_TO_UPDATE_AND_DELETE.getName()) + .selectItem(true); + verifyElementsCondition(topicsList.getActionButtons(), Condition.enabled); + topicsList + .getTopicItem(TOPIC_FOR_CHECK_FILTERS.getName()) + .selectItem(true); + Assert.assertFalse(topicsList.isCopySelectedTopicBtnEnabled(), "isCopySelectedTopicBtnEnabled()"); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3071") + @QaseId(268) + @Test(priority = 3) + public void checkCustomParametersWithinEditExistingTopic() { + navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + SoftAssert softly = new SoftAssert(); + topicCreateEditForm + .waitUntilScreenReady() + .clickAddCustomParameterTypeButton() + .openCustomParameterTypeDdl() + .getAllDdlOptions() + .forEach(option -> + softly.assertTrue(!option.is(Condition.attribute("disabled")), + option.getText() + " is enabled:")); + softly.assertAll(); + } + + @QaseId(197) + @Test(priority = 4) + public void updateTopic() { + navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + topicCreateEditForm + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(topicCreateEditForm.getCleanupPolicy(), + TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), "getCleanupPolicy()"); + softly.assertEquals(topicCreateEditForm.getTimeToRetain(), + TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), "getTimeToRetain()"); + softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), "getMaxSizeOnDisk()"); + softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), "getMaxMessageBytes()"); + softly.assertAll(); + TOPIC_TO_UPDATE_AND_DELETE + .setCleanupPolicyValue(COMPACT) + .setTimeToRetain(BTN_2_DAYS) + .setMaxSizeOnDisk(SIZE_50_GB).setMaxMessageBytes("1048589"); + topicCreateEditForm + .selectCleanupPolicy((TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue())) + .setTimeToRetainDataByButtons(TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain()) + .setMaxSizeOnDiskInGB(TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk()) + .setMaxMessageBytes(TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes()) + .clickSaveTopicBtn(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, "Topic successfully updated."), + "isAlertWithMessageVisible()"); + softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_UPDATE_AND_DELETE.getName()), + "isTopicHeaderVisible()"); + softly.assertAll(); + topicDetails + .waitUntilScreenReady(); + navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + softly.assertFalse(topicCreateEditForm.isNameFieldEnabled(), "isNameFieldEnabled()"); + softly.assertEquals(topicCreateEditForm.getCleanupPolicy(), + TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), "getCleanupPolicy()"); + softly.assertEquals(topicCreateEditForm.getTimeToRetain(), + TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), "getTimeToRetain()"); + softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), "getMaxSizeOnDisk()"); + softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), "getMaxMessageBytes()"); + softly.assertAll(); + } + + @QaseId(242) + @Test(priority = 5) + public void removeTopicFromTopicList() { + navigateToTopics(); + topicsList + .openDotMenuByTopicName(TOPIC_TO_UPDATE_AND_DELETE.getName()) + .clickRemoveTopicBtn() + .clickConfirmBtnMdl(); + Assert.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, + String.format("Topic %s successfully deleted!", TOPIC_TO_UPDATE_AND_DELETE.getName())), + "isAlertWithMessageVisible()"); + TOPIC_LIST.remove(TOPIC_TO_UPDATE_AND_DELETE); + } + + @QaseId(207) + @Test(priority = 6) + public void deleteTopic() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_DELETE.getName()); + topicDetails + .openDotMenu() + .clickDeleteTopicMenu() + .clickConfirmBtnMdl(); + navigateToTopics(); + Assert.assertFalse(topicsList.isTopicVisible(TOPIC_FOR_DELETE.getName()), "isTopicVisible"); + TOPIC_LIST.remove(TOPIC_FOR_DELETE); + } + + @QaseId(20) + @Test(priority = 7) + public void redirectToConsumerFromTopic() { + String topicName = "source-activities"; + String consumerGroupId = "connect-sink_postgres_activities"; + navigateToTopicsAndOpenDetails(topicName); + topicDetails + .openDetailsTab(CONSUMERS) + .openConsumerGroup(consumerGroupId); + consumersDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(consumersDetails.isRedirectedConsumerTitleVisible(consumerGroupId), + "isRedirectedConsumerTitleVisible()"); + softly.assertTrue(consumersDetails.isTopicInConsumersDetailsVisible(topicName), + "isTopicInConsumersDetailsVisible()"); + softly.assertAll(); + } + + @QaseId(4) + @Test(priority = 8) + public void checkTopicCreatePossibility() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady(); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName("testName"); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName(null) + .setNumberOfPartitions(nextInt(1, 10)); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName("testName"); + Assert.assertTrue(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + } + + @QaseId(266) + @Test(priority = 9) + public void checkTimeToRetainDataCustomValueWithEditingTopic() { + Topic topicToRetainData = new Topic() + .setName("topic-to-retain-data-" + randomAlphabetic(5)) + .setTimeToRetainData("86400000"); + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(topicToRetainData.getName()) + .setNumberOfPartitions(1) + .setTimeToRetainDataInMs("604800000"); + Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), "604800000", "getTimeToRetain()"); + topicCreateEditForm + .setTimeToRetainDataInMs(topicToRetainData.getTimeToRetainData()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady() + .openDotMenu() + .clickEditSettingsMenu(); + Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), topicToRetainData.getTimeToRetainData(), + "getTimeToRetain()"); + topicDetails + .openDetailsTab(SETTINGS); + Assert.assertEquals(topicDetails.getSettingsGridValueByKey("retention.ms"), topicToRetainData.getTimeToRetainData(), + "getSettingsGridValueByKey()"); + TOPIC_LIST.add(topicToRetainData); + } + + @QaseId(6) + @Test(priority = 10) + public void checkCustomParametersWithinCreateNewTopic() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(TOPIC_TO_CREATE.getName()) + .clickAddCustomParameterTypeButton() + .setCustomParameterType(TOPIC_TO_CREATE.getCustomParameterType()); + Assert.assertTrue(topicCreateEditForm.isDeleteCustomParameterButtonEnabled(), + "isDeleteCustomParameterButtonEnabled()"); + topicCreateEditForm + .clearCustomParameterValue(); + Assert.assertTrue(topicCreateEditForm.isValidationMessageCustomParameterValueVisible(), + "isValidationMessageCustomParameterValueVisible()"); + } + + @QaseId(2) + @Test(priority = 11) + public void checkTopicListElements() { + navigateToTopics(); + verifyElementsCondition(topicsList.getAllVisibleElements(), Condition.visible); + verifyElementsCondition(topicsList.getAllEnabledElements(), Condition.enabled); + } + + @QaseId(12) + @Test(priority = 12) + public void addNewFilterWithinTopic() { + String filterName = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible(); + verifyElementsCondition(topicDetails.getAllAddFilterModalVisibleElements(), Condition.visible); + verifyElementsCondition(topicDetails.getAllAddFilterModalEnabledElements(), Condition.enabled); + verifyElementsCondition(topicDetails.getAllAddFilterModalDisabledElements(), Condition.disabled); + Assert.assertFalse(topicDetails.isSaveThisFilterCheckBoxSelected(), "isSaveThisFilterCheckBoxSelected()"); + topicDetails + .setFilterCodeFieldAddFilterMdl(filterName); + Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(), "isAddFilterBtnAddFilterMdlEnabled()"); + topicDetails.clickAddFilterBtnAndCloseMdl(true); + Assert.assertTrue(topicDetails.isActiveFilterVisible(filterName), "isActiveFilterVisible()"); + } + + @QaseId(13) + @Test(priority = 13) + public void checkFilterSavingWithinSavedFilters() { + String displayName = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible() + .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4)) + .selectSaveThisFilterCheckboxMdl(true) + .setDisplayNameFldAddFilterMdl(displayName); + Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(), + "isAddFilterBtnAddFilterMdlEnabled()"); + topicDetails + .clickAddFilterBtnAndCloseMdl(false) + .openSavedFiltersListMdl(); + Assert.assertTrue(topicDetails.isFilterVisibleAtSavedFiltersMdl(displayName), + "isFilterVisibleAtSavedFiltersMdl()"); + } + + @QaseId(14) + @Test(priority = 14) + public void checkApplyingSavedFilterWithinTopicMessages() { + String displayName = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible() + .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4)) + .selectSaveThisFilterCheckboxMdl(true) + .setDisplayNameFldAddFilterMdl(displayName) + .clickAddFilterBtnAndCloseMdl(false) + .openSavedFiltersListMdl() + .selectFilterAtSavedFiltersMdl(displayName) + .clickSelectFilterBtnAtSavedFiltersMdl(); + Assert.assertTrue(topicDetails.isActiveFilterVisible(displayName), "isActiveFilterVisible()"); + } + + @QaseId(11) + @Test(priority = 15) + public void checkShowInternalTopicsButton() { + navigateToTopics(); + topicsList + .setShowInternalRadioButton(true); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicsList.getInternalTopics().size() > 0, "getInternalTopics()"); + softly.assertTrue(topicsList.getNonInternalTopics().size() > 0, "getNonInternalTopics()"); + softly.assertAll(); + topicsList + .setShowInternalRadioButton(false); + softly.assertEquals(topicsList.getInternalTopics().size(), 0, "getInternalTopics()"); + softly.assertTrue(topicsList.getNonInternalTopics().size() > 0, "getNonInternalTopics()"); + softly.assertAll(); + } + + @QaseId(334) + @Test(priority = 16) + public void checkInternalTopicsNaming() { + navigateToTopics(); + SoftAssert softly = new SoftAssert(); + topicsList + .setShowInternalRadioButton(true) + .getInternalTopics() + .forEach(topic -> softly.assertTrue(topic.getName().startsWith("_"), + String.format("'%s' starts with '_'", topic.getName()))); + softly.assertAll(); + } + + @QaseId(56) + @Test(priority = 17) + public void checkRetentionBytesAccordingToMaxSizeOnDisk() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(TOPIC_TO_CHECK_SETTINGS.getName()) + .setNumberOfPartitions(TOPIC_TO_CHECK_SETTINGS.getNumberOfPartitions()) + .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(TOPIC_TO_CHECK_SETTINGS); + topicDetails + .openDetailsTab(SETTINGS); + topicSettingsTab + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(topicSettingsTab.getValueByKey("retention.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), "getValueOfKey(retention.bytes)"); + softly.assertEquals(topicSettingsTab.getValueByKey("max.message.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), "getValueOfKey(max.message.bytes)"); + softly.assertAll(); + TOPIC_TO_CHECK_SETTINGS + .setMaxSizeOnDisk(SIZE_1_GB) + .setMaxMessageBytes("1000056"); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + topicCreateEditForm + .waitUntilScreenReady() + .setMaxSizeOnDiskInGB(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk()) + .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady() + .openDetailsTab(SETTINGS); + topicSettingsTab + .waitUntilScreenReady(); + softly.assertEquals(topicSettingsTab.getValueByKey("retention.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), "getValueOfKey(retention.bytes)"); + softly.assertEquals(topicSettingsTab.getValueByKey("max.message.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), "getValueOfKey(max.message.bytes)"); + softly.assertAll(); + } + + @QaseId(247) + @Test(priority = 18) + public void recreateTopicFromTopicProfile() { + Topic topicToRecreate = new Topic() + .setName("topic-to-recreate-" + randomAlphabetic(5)) + .setNumberOfPartitions(1); + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(topicToRecreate.getName()) + .setNumberOfPartitions(topicToRecreate.getNumberOfPartitions()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(topicToRecreate); + topicDetails + .openDotMenu() + .clickRecreateTopicMenu(); + Assert.assertTrue(topicDetails.isConfirmationMdlVisible(), "isConfirmationMdlVisible()"); + topicDetails + .clickConfirmBtnMdl(); + Assert.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, + String.format("Topic %s successfully recreated!", topicToRecreate.getName())), + "isAlertWithMessageVisible()"); + } + + @QaseId(8) + @Test(priority = 19) + public void checkCopyTopicPossibility() { + Topic topicToCopy = new Topic() + .setName("topic-to-copy-" + randomAlphabetic(5)) + .setNumberOfPartitions(1); + navigateToTopics(); + topicsList + .getAnyNonInternalTopic() + .selectItem(true) + .clickCopySelectedTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady(); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName(topicToCopy.getName()) + .setNumberOfPartitions(topicToCopy.getNumberOfPartitions()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(topicToCopy); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, "Topic successfully created."), + "isAlertWithMessageVisible()"); + softly.assertTrue(topicDetails.isTopicHeaderVisible(topicToCopy.getName()), "isTopicHeaderVisible()"); + softly.assertAll(); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/resources/manual.xml b/kafka-ui-e2e-checks/src/test/resources/manual.xml index 89dbd7e172..f9ea5e5b0f 100644 --- a/kafka-ui-e2e-checks/src/test/resources/manual.xml +++ b/kafka-ui-e2e-checks/src/test/resources/manual.xml @@ -2,7 +2,7 @@ - + diff --git a/kafka-ui-e2e-checks/src/test/resources/qase.xml b/kafka-ui-e2e-checks/src/test/resources/qase.xml index 8dd1dd1527..df31931718 100644 --- a/kafka-ui-e2e-checks/src/test/resources/qase.xml +++ b/kafka-ui-e2e-checks/src/test/resources/qase.xml @@ -2,7 +2,7 @@ - + diff --git a/kafka-ui-e2e-checks/src/test/resources/regression.xml b/kafka-ui-e2e-checks/src/test/resources/regression.xml index 6cc4c5bd49..fe102bae3e 100644 --- a/kafka-ui-e2e-checks/src/test/resources/regression.xml +++ b/kafka-ui-e2e-checks/src/test/resources/regression.xml @@ -2,9 +2,9 @@ - - - + + + diff --git a/kafka-ui-e2e-checks/src/test/resources/sanity.xml b/kafka-ui-e2e-checks/src/test/resources/sanity.xml index 01aa279c0f..c6b9b06024 100644 --- a/kafka-ui-e2e-checks/src/test/resources/sanity.xml +++ b/kafka-ui-e2e-checks/src/test/resources/sanity.xml @@ -2,7 +2,7 @@ - + diff --git a/kafka-ui-e2e-checks/src/test/resources/smoke.xml b/kafka-ui-e2e-checks/src/test/resources/smoke.xml index b370468e4f..ab2929ff34 100644 --- a/kafka-ui-e2e-checks/src/test/resources/smoke.xml +++ b/kafka-ui-e2e-checks/src/test/resources/smoke.xml @@ -2,7 +2,7 @@ - + From 83f94325691554ec783e84fb3fb0c912fd936719 Mon Sep 17 00:00:00 2001 From: Nail Badiullin Date: Mon, 10 Apr 2023 13:08:26 +0400 Subject: [PATCH 13/17] [FE] Messages filtering by offset & timestamp doesn't work (#3582) --- .../components/Topics/Topic/Messages/Filters/Filters.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx index 2941536f8b..b3f355f569 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx @@ -235,9 +235,13 @@ const Filters: React.FC = ({ props.seekType = SeekType.TIMESTAMP; } + const isSeekTypeWithSeekTo = + props.seekType === SeekType.TIMESTAMP || + props.seekType === SeekType.OFFSET; + if ( selectedPartitions.length !== partitions.length || - currentSeekType === SeekType.TIMESTAMP + isSeekTypeWithSeekTo ) { // not everything in the partition is selected props.seekTo = selectedPartitions.map(({ value }) => { From b3240d9057d5470dda93d56bb3e0145a768662eb Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Mon, 10 Apr 2023 19:06:15 +0800 Subject: [PATCH 14/17] BE: Exempt appconfig from rbac check (#3647) --- .../java/com/provectus/kafka/ui/model/rbac/Permission.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java index 837f9008f3..afdcf0ca15 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java @@ -1,5 +1,6 @@ package com.provectus.kafka.ui.model.rbac; +import static com.provectus.kafka.ui.model.rbac.Resource.APPLICATIONCONFIG; import static com.provectus.kafka.ui.model.rbac.Resource.CLUSTERCONFIG; import static com.provectus.kafka.ui.model.rbac.Resource.KSQL; @@ -25,6 +26,8 @@ import org.springframework.util.Assert; @EqualsAndHashCode public class Permission { + private static final List RBAC_ACTION_EXEMPT_LIST = List.of(KSQL, CLUSTERCONFIG, APPLICATIONCONFIG); + Resource resource; List actions; @@ -50,7 +53,7 @@ public class Permission { public void validate() { Assert.notNull(resource, "resource cannot be null"); - if (!List.of(KSQL, CLUSTERCONFIG).contains(this.resource)) { + if (!RBAC_ACTION_EXEMPT_LIST.contains(this.resource)) { Assert.notNull(value, "permission value can't be empty for resource " + resource); } } From 94da2f4e7f98f969df7a3509bfff41850fef71e3 Mon Sep 17 00:00:00 2001 From: Hrant Abrahamyan <113341474+habrahamyanpro@users.noreply.github.com> Date: Mon, 10 Apr 2023 15:53:31 +0400 Subject: [PATCH 15/17] [FE] SR: Display a warning in case of invalid syntax (#2599) * No warning about filling the invalid data in case of editing the Schema / Producing the Message * fixed test errors * pull master * fixed test problems * use isJsonObject for validation * fixed protobuf format bug * fix setNewSchemaValue() * test commit * fix BaseTest * upd global * upd global * upd global * add local browser VM option * fix TopicsList column header locator * fix withStartupTimeout() * switch e2e to TestNG * upd pom * upd page classes * upd -pl kafka-ui-e2e-checks * test commit * Revert "test commit" This reverts commit 4b505321ac5e164986a7a1886ac40c6744b8ecb1. * fix workflow module * upd test -f 'kafka-ui-e2e-checks' * crt firstCase * upd QaseUtils * add -Dsuite * add -Dsuite * add -Dsuite * add -Dsuite * add isSuiteEnabled * add isSuiteEnabled * upd workflow * upd readMe * upd readMe * upd readMe * upd qaseSetup * upd qaseSetup * add schedule * add schedule * upd suites * upd suites * upd suites * upd json input * upd onTestStart * Revert "fix setNewSchemaValue()" This reverts commit 67d12d113409769ebe52c4decc4bc3b842fe4c9c. * resolve conflicts * upd localWebDriver * added error message * added ability to check Valid Enum * swapped key Serde and Value Serde * replace 'e' with 'enum', also added test cases --------- Co-authored-by: VladSenyuta Co-authored-by: Vlad Senyuta <66071557+VladSenyuta@users.noreply.github.com> Co-authored-by: davitbejanyan Co-authored-by: David <58771979+David-DB88@users.noreply.github.com> Co-authored-by: Roman Zabaluev --- .../src/components/Schemas/Edit/Form.tsx | 20 +++++++++-- .../src/lib/__test__/yupExtended.spec.ts | 33 ++++++++++++++++++- kafka-ui-react-app/src/lib/yupExtended.ts | 33 +++++++++++++++++-- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx b/kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx index 9ce7f280f4..2fce1ad7d7 100644 --- a/kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx @@ -10,6 +10,7 @@ import { clusterSchemasPath, ClusterSubjectParam, } from 'lib/paths'; +import yup from 'lib/yupExtended'; import { NewSchemaSubjectRaw } from 'redux/interfaces'; import Editor from 'components/common/Editor/Editor'; import Select from 'components/common/Select/Select'; @@ -28,6 +29,9 @@ import { import PageLoader from 'components/common/PageLoader/PageLoader'; import { schemasApiClient } from 'lib/api'; import { showServerError } from 'lib/errorHandling'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { FormError } from 'components/common/Input/Input.styled'; +import { ErrorMessage } from '@hookform/error-message'; import * as S from './Edit.styled'; @@ -47,8 +51,16 @@ const Form: React.FC = () => { : JSON.stringify(JSON.parse(schema?.schema || '{}'), null, '\t'); }, [schema]); + const validationSchema = () => + yup.object().shape({ + newSchema: + schema?.schemaType === SchemaType.PROTOBUF + ? yup.string().required().isEnum('Schema syntax is not valid') + : yup.string().required().isJsonObject('Schema syntax is not valid'), + }); const methods = useForm({ mode: 'onChange', + resolver: yupResolver(validationSchema()), defaultValues: { schemaType: schema?.schemaType, compatibilityLevel: @@ -58,11 +70,10 @@ const Form: React.FC = () => { }); const { - formState: { isDirty, isSubmitting, dirtyFields }, + formState: { isDirty, isSubmitting, dirtyFields, errors }, control, handleSubmit, } = methods; - const onSubmit = async (props: NewSchemaSubjectRaw) => { if (!schema) return; @@ -191,11 +202,14 @@ const Form: React.FC = () => { )} /> + + + diff --git a/kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts b/kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts index bd43dd3f72..8100b9a326 100644 --- a/kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts +++ b/kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts @@ -1,5 +1,19 @@ -import { isValidJsonObject } from 'lib/yupExtended'; +import { isValidEnum, isValidJsonObject } from 'lib/yupExtended'; +const invalidEnum = ` +ennum SchemType { + AVRO = 0; + JSON = 1; + PROTOBUF = 3; +} +`; +const validEnum = ` +enum SchemType { + AVRO = 0; + JSON = 1; + PROTOBUF = 3; +} +`; describe('yup extended', () => { describe('isValidJsonObject', () => { it('returns false for no value', () => { @@ -21,4 +35,21 @@ describe('yup extended', () => { expect(isValidJsonObject('{ "foo": "bar" }')).toBeTruthy(); }); }); + + describe('isValidEnum', () => { + it('returns false for invalid enum', () => { + expect(isValidEnum(invalidEnum)).toBeFalsy(); + }); + it('returns false for no value', () => { + expect(isValidEnum()).toBeFalsy(); + }); + it('returns true should trim value', () => { + expect( + isValidEnum(` enum SchemType {AVRO = 0; PROTOBUF = 3;} `) + ).toBeTruthy(); + }); + it('returns true for valid enum', () => { + expect(isValidEnum(validEnum)).toBeTruthy(); + }); + }); }); diff --git a/kafka-ui-react-app/src/lib/yupExtended.ts b/kafka-ui-react-app/src/lib/yupExtended.ts index 9c96e073db..4c662ca822 100644 --- a/kafka-ui-react-app/src/lib/yupExtended.ts +++ b/kafka-ui-react-app/src/lib/yupExtended.ts @@ -9,7 +9,8 @@ declare module 'yup' { TDefault = undefined, TFlags extends yup.Flags = '' > extends yup.Schema { - isJsonObject(): StringSchema; + isJsonObject(message?: string): StringSchema; + isEnum(message?: string): StringSchema; } } @@ -31,15 +32,40 @@ export const isValidJsonObject = (value?: string) => { return false; }; -const isJsonObject = () => { +const isJsonObject = (message?: string) => { return yup.string().test( 'isJsonObject', // eslint-disable-next-line no-template-curly-in-string - '${path} is not JSON object', + message || '${path} is not JSON object', isValidJsonObject ); }; +export const isValidEnum = (value?: string) => { + try { + if (!value) return false; + const trimmedValue = value.trim(); + if ( + trimmedValue.indexOf('enum') === 0 && + trimmedValue.lastIndexOf('}') === trimmedValue.length - 1 + ) { + return true; + } + } catch { + // do nothing + } + return false; +}; + +const isEnum = (message?: string) => { + return yup.string().test( + 'isEnum', + // eslint-disable-next-line no-template-curly-in-string + message || '${path} is not Enum object', + isValidEnum + ); +}; + /** * due to yup rerunning all the object validiation during any render, * it makes sense to cache the async results @@ -62,6 +88,7 @@ export function cacheTest( } yup.addMethod(yup.StringSchema, 'isJsonObject', isJsonObject); +yup.addMethod(yup.StringSchema, 'isEnum', isEnum); export const topicFormValidationSchema = yup.object().shape({ name: yup From dbdced5babfc5787c4e242348b8142f8b9a03252 Mon Sep 17 00:00:00 2001 From: Ilya Kuramshin Date: Mon, 10 Apr 2023 16:40:15 +0400 Subject: [PATCH 16/17] BE: Fix loading freezes in case one of the brokers is down (#3618) Co-authored-by: iliax Co-authored-by: Roman Zabaluev --- .../kafka/ui/service/ReactiveAdminClient.java | 39 +++++++++---- .../kafka/ui/service/TopicsService.java | 21 +++---- .../ui/service/ReactiveAdminClientTest.java | 57 +++++++++++++++++++ 3 files changed, 93 insertions(+), 24 deletions(-) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java index 7cabb79f2d..39332da39e 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java @@ -4,6 +4,7 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableTable; import com.google.common.collect.Iterables; import com.google.common.collect.Table; @@ -498,6 +499,14 @@ public class ReactiveAdminClient implements Closeable { .flatMap(parts -> listOffsetsUnsafe(parts, offsetSpec)); } + /** + * List offset for the specified topics, skipping no-leader partitions. + */ + public Mono> listOffsets(Collection topicDescriptions, + OffsetSpec offsetSpec) { + return listOffsetsUnsafe(filterPartitionsWithLeaderCheck(topicDescriptions, p -> true, false), offsetSpec); + } + private Mono> filterPartitionsWithLeaderCheck(Collection partitions, boolean failOnUnknownLeader) { var targetTopics = partitions.stream().map(TopicPartition::topic).collect(Collectors.toSet()); @@ -507,34 +516,44 @@ public class ReactiveAdminClient implements Closeable { descriptions.values(), partitions::contains, failOnUnknownLeader)); } - private Set filterPartitionsWithLeaderCheck(Collection topicDescriptions, + @VisibleForTesting + static Set filterPartitionsWithLeaderCheck(Collection topicDescriptions, Predicate partitionPredicate, boolean failOnUnknownLeader) { var goodPartitions = new HashSet(); for (TopicDescription description : topicDescriptions) { + var goodTopicPartitions = new ArrayList(); for (TopicPartitionInfo partitionInfo : description.partitions()) { TopicPartition topicPartition = new TopicPartition(description.name(), partitionInfo.partition()); - if (!partitionPredicate.test(topicPartition)) { - continue; + if (partitionInfo.leader() == null) { + if (failOnUnknownLeader) { + throw new ValidationException(String.format("Topic partition %s has no leader", topicPartition)); + } else { + // if ANY of topic partitions has no leader - we have to skip all topic partitions + goodTopicPartitions.clear(); + break; + } } - if (partitionInfo.leader() != null) { - goodPartitions.add(topicPartition); - } else if (failOnUnknownLeader) { - throw new ValidationException(String.format("Topic partition %s has no leader", topicPartition)); + if (partitionPredicate.test(topicPartition)) { + goodTopicPartitions.add(topicPartition); } } + goodPartitions.addAll(goodTopicPartitions); } return goodPartitions; } - // 1. NOTE(!): should only apply for partitions with existing leader, + // 1. NOTE(!): should only apply for partitions from topics where all partitions have leaders, // otherwise AdminClient will try to fetch topic metadata, fail and retry infinitely (until timeout) // 2. NOTE(!): Skips partitions that were not initialized yet // (UnknownTopicOrPartitionException thrown, ex. after topic creation) // 3. TODO: check if it is a bug that AdminClient never throws LeaderNotAvailableException and just retrying instead @KafkaClientInternalsDependant - public Mono> listOffsetsUnsafe(Collection partitions, - OffsetSpec offsetSpec) { + @VisibleForTesting + Mono> listOffsetsUnsafe(Collection partitions, OffsetSpec offsetSpec) { + if (partitions.isEmpty()) { + return Mono.just(Map.of()); + } Function, Mono>> call = parts -> { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java index 9aaff3e9ef..797c30167b 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java @@ -3,6 +3,7 @@ package com.provectus.kafka.ui.service; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; +import com.google.common.collect.Sets; import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.exception.TopicMetadataException; import com.provectus.kafka.ui.exception.TopicNotFoundException; @@ -136,22 +137,14 @@ public class TopicsService { } private Mono getPartitionOffsets(Map - descriptions, + descriptionsMap, ReactiveAdminClient ac) { - var topicPartitions = descriptions.values().stream() - .flatMap(desc -> - desc.partitions().stream() - // list offsets should only be applied to partitions with existing leader - // (see ReactiveAdminClient.listOffsetsUnsafe(..) docs) - .filter(tp -> tp.leader() != null) - .map(p -> new TopicPartition(desc.name(), p.partition()))) - .collect(toList()); - - return ac.listOffsetsUnsafe(topicPartitions, OffsetSpec.earliest()) - .zipWith(ac.listOffsetsUnsafe(topicPartitions, OffsetSpec.latest()), + var descriptions = descriptionsMap.values(); + return ac.listOffsets(descriptions, OffsetSpec.earliest()) + .zipWith(ac.listOffsets(descriptions, OffsetSpec.latest()), (earliest, latest) -> - topicPartitions.stream() - .filter(tp -> earliest.containsKey(tp) && latest.containsKey(tp)) + Sets.intersection(earliest.keySet(), latest.keySet()) + .stream() .map(tp -> Map.entry(tp, new InternalPartitionsOffsets.Offsets( diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ReactiveAdminClientTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ReactiveAdminClientTest.java index 2e302009ac..061c0bea66 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ReactiveAdminClientTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ReactiveAdminClientTest.java @@ -4,8 +4,11 @@ import static com.provectus.kafka.ui.service.ReactiveAdminClient.toMonoWithExcep import static java.util.Objects.requireNonNull; import static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.ThrowableAssert.ThrowingCallable; import com.provectus.kafka.ui.AbstractIntegrationTest; +import com.provectus.kafka.ui.exception.ValidationException; import com.provectus.kafka.ui.producer.KafkaTestProducer; import java.time.Duration; import java.util.ArrayList; @@ -22,16 +25,20 @@ import org.apache.kafka.clients.admin.Config; import org.apache.kafka.clients.admin.ConfigEntry; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.clients.admin.TopicDescription; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.KafkaFuture; +import org.apache.kafka.common.Node; import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.TopicPartitionInfo; import org.apache.kafka.common.config.ConfigResource; import org.apache.kafka.common.errors.UnknownTopicOrPartitionException; import org.apache.kafka.common.internals.KafkaFutureImpl; import org.apache.kafka.common.serialization.StringDeserializer; +import org.assertj.core.api.ThrowableAssert; import org.junit.function.ThrowingRunnable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -133,6 +140,56 @@ class ReactiveAdminClientTest extends AbstractIntegrationTest { .verifyComplete(); } + @Test + void filterPartitionsWithLeaderCheckSkipsPartitionsFromTopicWhereSomePartitionsHaveNoLeader() { + var filteredPartitions = ReactiveAdminClient.filterPartitionsWithLeaderCheck( + List.of( + // contains partitions with no leader + new TopicDescription("noLeaderTopic", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()), + new TopicPartitionInfo(1, null, List.of(), List.of()))), + // should be skipped by predicate + new TopicDescription("skippingByPredicate", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()))), + // good topic + new TopicDescription("good", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()), + new TopicPartitionInfo(1, new Node(2, "n2", 9092), List.of(), List.of())) + )), + p -> !p.topic().equals("skippingByPredicate"), + false + ); + + assertThat(filteredPartitions) + .containsExactlyInAnyOrder( + new TopicPartition("good", 0), + new TopicPartition("good", 1) + ); + } + + @Test + void filterPartitionsWithLeaderCheckThrowExceptionIfThereIsSomePartitionsWithoutLeaderAndFlagSet() { + ThrowingCallable call = () -> ReactiveAdminClient.filterPartitionsWithLeaderCheck( + List.of( + // contains partitions with no leader + new TopicDescription("t1", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()), + new TopicPartitionInfo(1, null, List.of(), List.of()))), + new TopicDescription("t2", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of())) + )), + p -> true, + // setting failOnNoLeader flag + true + ); + assertThatThrownBy(call).isInstanceOf(ValidationException.class); + } + @Test void testListOffsetsUnsafe() { String topic = UUID.randomUUID().toString(); From 005e74f2480f3ea961ec0c3d9b4a633b666df552 Mon Sep 17 00:00:00 2001 From: David Bejanyan <58771979+David-DB88@users.noreply.github.com> Date: Mon, 10 Apr 2023 19:35:14 +0400 Subject: [PATCH 17/17] FE: Format field previews one per line (#3623) * new properties are added on a new line * added test cases for key and Value Preview filters * upd produceMessageCheck --------- Co-authored-by: Roman Zabaluev Co-authored-by: VladSenyuta --- .../com/provectus/kafka/ui/models/Topic.java | 2 +- .../ui/pages/topics/ProduceMessagePanel.java | 10 ++--- .../kafka/ui/pages/topics/TopicDetails.java | 20 ++++----- .../kafka/ui/services/ApiService.java | 2 +- .../smokesuite/connectors/ConnectorsTest.java | 6 +-- .../ui/smokesuite/topics/MessagesTest.java | 20 ++++----- .../ui/smokesuite/topics/TopicsTest.java | 2 +- .../Topics/Topic/Messages/Message.tsx | 19 ++++---- .../MessageContent/MessageContent.styled.ts | 2 +- .../Topic/Messages/__test__/Message.spec.tsx | 43 +++++++++++++++++-- 10 files changed, 77 insertions(+), 49 deletions(-) diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java index ece16b4cc1..8fb3df086e 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java @@ -11,7 +11,7 @@ import lombok.experimental.Accessors; @Accessors(chain = true) public class Topic { - private String name, timeToRetainData, maxMessageBytes, messageKey, messageContent, customParameterValue; + private String name, timeToRetainData, maxMessageBytes, messageKey, messageValue, customParameterValue; private int numberOfPartitions; private CustomParameterType customParameterType; private CleanupPolicyValue cleanupPolicyValue; diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java index e407d4fe3d..c4e65c65be 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java @@ -12,7 +12,7 @@ import java.util.Arrays; public class ProduceMessagePanel extends BasePage { protected SelenideElement keyTextArea = $x("//div[@id='key']/textarea"); - protected SelenideElement contentTextArea = $x("//div[@id='content']/textarea"); + protected SelenideElement valueTextArea = $x("//div[@id='content']/textarea"); protected SelenideElement headersTextArea = $x("//div[@id='headers']/textarea"); protected SelenideElement submitBtn = headersTextArea.$x("../../../..//button[@type='submit']"); protected SelenideElement partitionDdl = $x("//ul[@name='partition']"); @@ -34,14 +34,14 @@ public class ProduceMessagePanel extends BasePage { } @Step - public ProduceMessagePanel setContentFiled(String value) { - clearByKeyboard(contentTextArea); - contentTextArea.setValue(value); + public ProduceMessagePanel setValueFiled(String value) { + clearByKeyboard(valueTextArea); + valueTextArea.setValue(value); return this; } @Step - public ProduceMessagePanel setHeaderFiled(String value) { + public ProduceMessagePanel setHeadersFld(String value) { headersTextArea.setValue(value); return this; } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java index a171c728e4..b7d03dcf3a 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java @@ -1,6 +1,5 @@ package com.provectus.kafka.ui.pages.topics; -import static com.codeborne.selenide.Selenide.$; import static com.codeborne.selenide.Selenide.$$x; import static com.codeborne.selenide.Selenide.$x; import static com.codeborne.selenide.Selenide.sleep; @@ -296,16 +295,6 @@ public class TopicDetails extends BasePage { return this; } - @Step - public boolean isKeyMessageVisible(String keyMessage) { - return keyMessage.equals($("td[title]").getText()); - } - - @Step - public boolean isContentMessageVisible(String contentMessage) { - return contentMessage.matches(contentMessageTab.getText().trim()); - } - private void selectYear(int expectedYear) { while (getActualCalendarDate().getYear() > expectedYear) { clickByJavaScript(previousMonthButton); @@ -382,6 +371,13 @@ public class TopicDetails extends BasePage { .findFirst().orElseThrow(); } + @Step + public TopicDetails.MessageGridItem getMessageByKey(String key) { + return initItems().stream() + .filter(e -> e.getKey().equals(key)) + .findFirst().orElseThrow(); + } + @Step public List getAllMessages() { return initItems(); @@ -451,7 +447,7 @@ public class TopicDetails extends BasePage { @Step public String getValue() { - return element.$x("./td[6]/span/p").getText().trim(); + return element.$x("./td[6]").getAttribute("title"); } @Step diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java index ea08f57fe4..a041defc93 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java @@ -185,7 +185,7 @@ public class ApiService extends BaseSource { createMessage.setKeySerde("String"); createMessage.setValueSerde("String"); createMessage.setKey(topic.getMessageKey()); - createMessage.setContent(topic.getMessageContent()); + createMessage.setContent(topic.getMessageValue()); try { messageApi().sendTopicMessages(clusterName, topic.getName(), createMessage).block(); } catch (WebClientResponseException ex) { diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java index 72fe769e31..9ca3526c71 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java @@ -23,13 +23,13 @@ public class ConnectorsTest extends BaseTest { private static final String MESSAGE_KEY = " "; private static final Topic TOPIC_FOR_CREATE = new Topic() .setName("topic-for-create-connector-" + randomAlphabetic(5)) - .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + .setMessageValue(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); private static final Topic TOPIC_FOR_DELETE = new Topic() .setName("topic-for-delete-connector-" + randomAlphabetic(5)) - .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + .setMessageValue(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); private static final Topic TOPIC_FOR_UPDATE = new Topic() .setName("topic-for-update-connector-" + randomAlphabetic(5)) - .setMessageContent(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + .setMessageValue(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); private static final Connector CONNECTOR_FOR_DELETE = new Connector() .setName("connector-for-delete-" + randomAlphabetic(5)) .setConfig(getResourceAsString("testData/connectors/delete_connector_config.json")); diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java index 2404838698..3bbc7e7cd3 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java @@ -28,23 +28,23 @@ public class MessagesTest extends BaseTest { private static final Topic TOPIC_FOR_MESSAGES = new Topic() .setName("topic-with-clean-message-attribute-" + randomAlphabetic(5)) .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); + .setMessageValue(randomAlphabetic(10)); private static final Topic TOPIC_TO_CLEAR_AND_PURGE_MESSAGES = new Topic() .setName("topic-to-clear-and-purge-messages-" + randomAlphabetic(5)) .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); + .setMessageValue(randomAlphabetic(10)); private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic() .setName("topic-for-check-filters-" + randomAlphabetic(5)) .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); + .setMessageValue(randomAlphabetic(10)); private static final Topic TOPIC_TO_RECREATE = new Topic() .setName("topic-to-recreate-attribute-" + randomAlphabetic(5)) .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); + .setMessageValue(randomAlphabetic(10)); private static final Topic TOPIC_FOR_CHECK_MESSAGES_COUNT = new Topic() .setName("topic-for-check-messages-count" + randomAlphabetic(5)) .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); + .setMessageValue(randomAlphabetic(10)); private static final List TOPIC_LIST = new ArrayList<>(); @BeforeClass(alwaysRun = true) @@ -65,12 +65,8 @@ public class MessagesTest extends BaseTest { topicDetails .openDetailsTab(MESSAGES); produceMessage(TOPIC_FOR_MESSAGES); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicDetails.isKeyMessageVisible((TOPIC_FOR_MESSAGES.getMessageKey())), - "isKeyMessageVisible()"); - softly.assertTrue(topicDetails.isContentMessageVisible((TOPIC_FOR_MESSAGES.getMessageContent()).trim()), - "isContentMessageVisible()"); - softly.assertAll(); + Assert.assertEquals(topicDetails.getMessageByKey(TOPIC_FOR_MESSAGES.getMessageKey()).getValue(), + TOPIC_FOR_MESSAGES.getMessageValue(), "message.getValue()"); } @QaseId(19) @@ -266,7 +262,7 @@ public class MessagesTest extends BaseTest { produceMessagePanel .waitUntilScreenReady() .setKeyField(topic.getMessageKey()) - .setContentFiled(topic.getMessageContent()) + .setValueFiled(topic.getMessageValue()) .submitProduceMessage(); topicDetails .waitUntilScreenReady(); diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java index 319c5f74f9..ad20f595a4 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java @@ -45,7 +45,7 @@ public class TopicsTest extends BaseTest { .setMaxSizeOnDisk(NOT_SET) .setMaxMessageBytes("1048588") .setMessageKey(randomAlphabetic(5)) - .setMessageContent(randomAlphabetic(10)); + .setMessageValue(randomAlphabetic(10)); private static final Topic TOPIC_TO_CHECK_SETTINGS = new Topic() .setName("new-topic-" + randomAlphabetic(5)) .setNumberOfPartitions(1) diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx index 7a9df2c16f..60f09b8293 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx @@ -80,19 +80,20 @@ const Message: React.FC = ({ filters?: PreviewFilter[] ) => { if (!filters?.length || !jsonValue) return jsonValue; - const parsedJson = getParsedJson(jsonValue); return ( <> - {filters.map((item) => ( - - {item.field}:{' '} - {JSON.stringify( - JSONPath({ path: item.path, json: parsedJson, wrap: false }) - )} - - ))} + {filters.map((item) => { + return ( +
+ {item.field}:{' '} + {JSON.stringify( + JSONPath({ path: item.path, json: parsedJson, wrap: false }) + )} +
+ ); + })} ); }; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.styled.ts b/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.styled.ts index 1c190ba9ca..9eb5e6b062 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.styled.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.styled.ts @@ -58,7 +58,7 @@ export const MetadataLabel = styled.p` width: 80px; `; -export const MetadataValue = styled.p` +export const MetadataValue = styled.div` color: ${({ theme }) => theme.topicMetaData.color.value}; font-size: 14px; `; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Message.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Message.spec.tsx index c01f4af3d4..c96b395f4c 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Message.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Message.spec.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { TopicMessage, TopicMessageTimestampTypeEnum } from 'generated-sources'; -import Message, { Props } from 'components/Topics/Topic/Messages/Message'; +import Message, { + PreviewFilter, + Props, +} from 'components/Topics/Topic/Messages/Message'; import { screen } from '@testing-library/react'; import { render } from 'lib/testHelpers'; import userEvent from '@testing-library/user-event'; @@ -8,6 +11,9 @@ import { formatTimestamp } from 'lib/dateTimeHelpers'; const messageContentText = 'messageContentText'; +const keyTest = '{"payload":{"subreddit":"learnprogramming"}}'; +const contentTest = + '{"payload":{"author":"DwaywelayTOP","archived":false,"name":"t3_11jshwd","id":"11jshwd"}}'; jest.mock( 'components/Topics/Topic/Messages/MessageContent/MessageContent', () => () => @@ -28,10 +34,19 @@ describe('Message component', () => { content: '{"data": "test"}', headers: { header: 'test' }, }; - + const mockKeyFilters: PreviewFilter = { + field: 'sub', + path: '$.payload.subreddit', + }; + const mockContentFilters: PreviewFilter = { + field: 'author', + path: '$.payload.author', + }; const renderComponent = ( props: Partial = { message: mockMessage, + keyFilters: [], + contentFilters: [], } ) => render( @@ -39,8 +54,8 @@ describe('Message component', () => {
@@ -88,4 +103,24 @@ describe('Message component', () => { await userEvent.click(messageToggleIcon); expect(screen.getByText(messageContentText)).toBeInTheDocument(); }); + + it('should check if Preview filter showing for key', () => { + const props = { + message: { ...mockMessage, key: keyTest as string }, + keyFilters: [mockKeyFilters], + }; + renderComponent(props); + const keyFiltered = screen.getByText('sub: "learnprogramming"'); + expect(keyFiltered).toBeInTheDocument(); + }); + + it('should check if Preview filter showing for Value', () => { + const props = { + message: { ...mockMessage, content: contentTest as string }, + contentFilters: [mockContentFilters], + }; + renderComponent(props); + const keyFiltered = screen.getByText('author: "DwaywelayTOP"'); + expect(keyFiltered).toBeInTheDocument(); + }); });