9JKztZWySxTk*gwcQ1(I2WX|8TJc
zM53}p5G=m3NpDf6wgUw!XlY5kS5=A17)~U;_j&KS5*?}a^(4A?4%e)yX_`1MY`R8~
z5VII!DZBWfUiR_DxRQG{EfV%MtKiTJt|Ad5OL2_>KQhotbb1&vJ2@%_z~L8BxYbB`
zsG|QO`j~<7RMvpLCZBOQ(~bIihU{z+1tj{ZuU
zW^V5Qg|5gj;`|GO$*(;1wO7TH7Vf8hls{j6l`lJLl!5NRqGR^?EdXKPCeW~>aN@5;
zLdW6xtjh&|#e~Y#Z?WVZ@mHsz;jnK#Q5pcN6<
zP(9oBv4#LzmmvQF3V8i;5E-6+ze&BGA7VEcN`K8jhjy-$Q6Ky+g)fA6GVV@6tB8GU
z9?}MGZ5M+0WCdrSdGIzjsl<=A8rn4%ypTQ1)k$6L{{Db#3oT~H;|75LV#s|XtWM@yT8VKejMIe$mavrkB1Z_(3bH>)8hL<0;_taUo
zsQ^J=jl6y^Q<;uPTCTZp7L&x|J$Jym+e`R@$}CBO*EYo~UaGM@Pq)Q%&7I|^iRt^#
zZfeICcg}8hGX37J=I_X|9HtVNiNzDUmdR(@GUBZ$loK?B+L;ngxW-!!`)s#DME
zz!_OC$W2r{Y?)X{Gi#oBxQS|&sl!FkI|!meKNC2=z_MMEUv^r^sGI;U7;$HLag$=z
zT5VTkXczuy-q%+wC2F4jyQY=dy9>Eg5ADOZ{?XRLb7UjrGsFpxXe9mUAnd-q@(SsX
z^4P`^?$EcB&)>dM=J={%;A}n_x4Dx!e!p$oCtpBDEdhl1Y}4FUPyaZ)UtWyHT6F2W
zOK~hsgd8m7Rgq#x5>K96i8o!bH?ogY`}Kvkg_3OAa%_)tjz5laQ^~3rqSSJ+`~KE0
z)7Fjnmk;3XkhdJB+4P!jWq=8pi(+i3p5d`-)7}2?Ld(P4#W2i#2uK_K8>dm2KS3+)
z7u$07YOFad9oMit`9P&4plbS}w#Hk=r#Y*9xrHs~;|S1^I%=MW00ZF=Zd>qXa*`o(
z=>2ON8k)jDe<9kCWfGQF0Yb=>G@eo1ZlkbRe6fsSQ_VbhVDHuv)iUls#jo&qUB`S3
z?XiAusmcCyGV1%A-`7C4kr8%P+5=wPo!hdyLf)}o$6Z`+=U%{FG)wDDPYZ0i6w93H
zR#JAIj<5vlX1O`-t@UyScXx4E(>R8=aCAk&+GJ3^s0Lkb3Qulo^2?@ZBU_IkO=k@G
zJkrkl0A&RP5tuSRLOj}{1A&aUwIrLWwuX#+Dkv7c8NXC*(?MvEh~}ikt4q#M_gftL
zxP%X|Cf}{XrLw5i%vGl}X7$5?YF(uz(Q909j@kAuDsL=r;a38zv%=kI+BQz{c3G^>
z0%L-#(~jyTjln;&28rmLp2xQJgO&?5-^458YU8EDKQ_HVWc&+55qIEw3|Amc7Ea=5Jyzq=MypK0&bPC$jAXCl9U=#FElvAR
zjqN{6Trd_G(S2nC!~;0oz9zcl|r_zNI%P;e>s4kQhE06(~20sm3RbXy$G*IF1#O
zH~Y0HiDm!shB1^5@QcdBwR;dfSw#*4YpxU%0q|#|)sv$bRPgm7E%qXyYd=r^I-O+G
z+ra+JEAbEjFbwHfVC3`U#`{s4E?-Xa_U&-u>i02;13z_0)=zvwee=W*{A(W
ze?A&HnAi2{Iwv&STs4pSM$D!KWdE>3i!mZmi2N$(b9P!K@TS(%&D+*95O_Zecsy(S
zxQ~GcBFUItysOJsa)+Z(_QQmzs^Ul_XcFE+mmYyYjTE*klJcl^tnHWa%h&+VR-e$^(&UsBVXX%
zwE!NIhAacVTv^!73aGF&8T|Cb*6vk9Zl8)0V|-iH;Z#PW2)DP`F!81bI7X6B8(^2|4~|lTV_Y?0*^dUuyM*Z
zvU*eD+^z*@L_7Zne~ub`M%eVi&m^)oLMQ$=E()cI*J(s<-@^8D7cSNAG>E1n!F$yS
znfln@NE&*KoFtc^d_*X{z4YBYH~kDwGSL7^|AM7x*qY|kN87Ok>ojX~K#YCq?i{=kuH#0wgGaDGXbEcY@>p%HU|!GDd{
z?4fR3=nT=HD8AyO>G4vaq+`04eFkR+#`srT_J=dT@3YFlHBsXG%Ojj~gLwgo{0m
zb!*)!M6kNpKiz~%;vBB`hTm<{#tX2=qL14ie)CSe37Y^8|2C-r|9JLqlIwnH?kjff
zcE;N_fp6(U(=M@FTh`z0UaAe(%GXrOX=Q#p8&p_jgs_A$I+NA={*5>BSzl02)a7s>
zz}&T@0OH-n0dE^-q&FYIyz#$gD|RYdDX=R4FDb^HR|h6G&Bvq7t68$Q`N0M%H{SB`5naJx>X4QGXy)}@dG9PgBcZDKszz+F3m5cZT
zlp_*^%_t`r-+kyBv8>jh?AgN8u3ScdX}t4Mn9{abtLwCk-+LyYF9GWxc9xU}-@)x>(n@h$j1N1rzTUW1gBYULx-YvtpJhtug{(4fQAI#{yV
zLK48>v^+|rxh}>xWBnVVdXZS_nRCB<(T0)Rz#;MlF?T`1q!yG?rM!ZK@UsDGrbMC=
zSrwR8IejEDS`*N2trHq;29>+_;tF%t%BVW@SL>_qd6cIt>mYLUeSK2d;{8F7kO7#O
zXou1i79!kY{KWuoWA=Z9t_9kvcyD;x785lMoj?pOthqLsjD_11Y!V+1?5O2Z>+Z7S
zXqfL(Eo6`47VAEkA+2aSy8
z|9t3q7PkoK>0PMgVZiLz`;~^G$;O3W4u_6^m8r}EnF+>(i`eIcX$wYsbUGKGr>36j
zN5i$3v2y7L)a5DD?ECA3M5{htTuDie+l7v~q8`PxY^vp__&3iC{1x?=2wbqmztLVE
zkapL|P$yC*96%FcL)~NGqPB2oREw?SktU#7V}KqJ`Jf8C7d1AGN3DxTB>Nu!<@j^<
zs+oJG;xigHDkaDQ(u$!E8)CtoHfBuyNGzJQv>(s3V`F@hT!#Rm(_7DdBP&IXy6BM?E@px<58j>^RLnKfiD%Pb%MN_iD#d+QD&+PmapV|
zW`W`pEKGi1m^#iI#c}I|n5y_uf9(7B?~%mk;NS1Z(#tG`GFlZCiNN!8(=g^@c22e7
zEvUHOd7EM;LsO&;Wykbg=R5k9h+9{^`9A5nR_5J8hN*>!>hz&B`yDPpn({0&I#gq$
z-{Ml&eDt2}w;CZ`sXW{0K_z@dmcsf{iQMMT{}yBA-M&Bg7Z?=*jwq=;-gikH^&M(b
zIzt~((gAUpB<~-CG>qF~nWc}(ww>jzOg-GFexj1K#$3qacf5LtEYOqJO2IHlR^MMv
zo~raFsywIVjHMtypU}pi0JT)iG8pOc#cJTkwhB&sx|Nl8J)eZ_R5=X`ldjRz`0%m@
z+I1?)W}J3x5*LSl-1kTlnzV|iN~i;*l6NOIz>0A#q|wos9e#|Q)*-QtOxw!I<290T
zCV(q8{?77Nl#h0;*9>LTX<9a7)9RPDB(1ACRZe*$dmTnB&)-<7l3-=nn@{$2gJXP;
z&S0fk2v>N#Ju|$E-1?`F;V~{=6h94t7&XcgY>p8`&+QEzQMpY&WmP&gz5)lPXN!p(
zSxH-SsF(A^QM%DnqVTl_oTLF~N`s3fX7Ncf>}j-7ml9%zmYviecF`O_g{l>LjZ#Gpf&37C|&
z#xBhC#TH%xf{KteLlOEe3#EEdHw-UQ&R)30|>243!FY`QQQ{Wy{eoU2qM#ch!R
z&@<43yj{Y25P~}&c*x?Uf!{ZzUrSli8R=ji>IpYdSrWtH=a0>yiEFjiKnrFV9p$|Ji7$Bc>UM{0=2gck4qH!=c
zt62&C;HN+-)0W)t$>17WJomy&VGp7bb0aunm16yxqFbzidl3Th>0?neY(G6PedDko
zvudr84CM!q6gDTlGl=u{+qhu`oJR=wTAY_PCGusr^?aQ~XDBk|zmP#=ilfQ|r%W*Z6&gllb25uil(+l2ztTswC&d
z=8}o!dUS+1cU#G~6Zo3z{u|B%!lIS(TSZWws?p$&!5_@(SnDKQS)!^MgzcPAl8M#0GVqE=?w+
z2Hd!y0SswtrTS8j;g;@CSAfYjbNxm#G=}I!&s;u%927S`ReG!Lgp9)Cv?cj
zpWr#OwpYmc_vbjE5Vp@++h=DiKYsl5kQgT!F2&y>%EJz%Mh|rjF)aYy$tcHQYZR8u
zV72@N4vVdnIDcj-7&q(U^(og4=m^0(hQIjH9ZJS)@(1&es+pOwc%goxjY72WVJsI9
z45BvYJ6wa29o{hMGYtkLBC>wt*W@|LP@#ND8?xR}Y=uLfbFKRk!l1YJB9m;7>y&uX
z;Ta3cGKR;z(=F3cjM>t|NSA|FRJPtfH46JPOvM~rGsfR0pA6DeW#8d!qbs%G-1{4K
z)8jX8X4#r?n`D_e*XwHk1%*1D;DvX0DNH@
z4p>OE(B`w~9cq+fq~ZZR_!ja-lXXOP@@4AIfVv8tun0h1$uC3hIP^G)9oy23A%}F!
zPaN@zOsoc?;Zd%U-8tCl>AoMNlnnJy9OjzfYy|rF%l#(95Z%zAn@2*SIc~!$#=}
zoy~EXwqI#6VU*jPd1M)b1VU>QCYvL&=MSU(vu9osdGGW7pfMQBrJ3e&2S6=_B;D2^qBM2S)jp$);o?XZAdyMc4mRPXah0T
zG9QisfeTM2=4na92Pa(@6f4G8#O92F#L13`7{6ddYcNEf(Q|@)=@x=ff}-;wuaaQU
zp~0Gn_aUT#v`oiAmr|ih`b+iBaWGB7r~qWg^_9oTS_wtAgfu!*xRKE|$%h`hu@&(K
z{fE`5LD0V%O&XmcN3J?WiEj#rZ)cW~Jw{f22FHT4s8t7)a>qLDIG)9>7Mlg>R!Q*j
z7sQ9XQtk81+T83_`k)sG62z7J4f>`L
ziXQr62^2ww01@IAOpgp6ga{?m8#-W(CQcE+hl!4%r<3RVGDaceBI%%ssw>z%R`WV9
zJ)&{e6iTU}74~Oe$LKjXmjMT+82`&VyVJ@}1j-$PeI9tOu6`!sigv^_&IJ5k?&4N)
zu%;|{HbF12zu0Ynn(gmK+rHHa8TmLfJNfE|&KfJ_s
z=i4d``&+RVcfzB3P5=nOLau%Oj5h=fOuBg&;>*bg!?_YC$EwjonF>GHLT(=EeCyGu
z9ppCYiK-lw@6-
z4SR5S$&zAAp~w`eh8hfFTU4neDS48jY|y&93YeF_gH0QdtWw0TLZT4!)21ia@+b&;
zHt4;3Mr;f1Wa(Czf=loFMJc^DBy;wl3N+G(MYg^89Ch~HmK^r?rOkmo&(og`%wqz8
zIcEQ7K3pdEh7m5^kkQ_+73hQpj*I1Js2*r$hs`@e-0844qL17R7dS#S0M}^zF$JGJ
z$pEyMe8WaEOKrRkV=28+35_Gj@!P;B_B47Kn+qQM_yI<~#&2=+fW`L`a)!+}njAl~
z*;GQQ(yMphe{CtsxSQKO!Osw3r;g#xM6C#0U2s;KnRQP3vCa4wcDTFR>^vmP^s0E{
zH40J5e6R&IA=gI+gC-GI!-N>u6bwT?tqUQzNN2GgLnLpg!1a1sC0{g1iYJsXH0E&^
z_OJiHa^8?@-dyyJz+j|o=)ot#rcPBM^yh!s225{tj`Kobo#hZ|K2hZit
z1)L8wVA#!%-yaVCrFzFMn{w)vpInj)S4my{Cv+hCy~yOi5TvqK?5H!yb?g+1D!4mb
z*zj=-N_S|0d8#&2Ve2(2XL$HG9J;!c!W9n{UkLQJ$U(miiq9-VCB5J21%!ceOI;db
zq1vb*)N}Uu#Xo(W=tJR|8Qa9ffaUASJiWJ1T;7z)PF8EAS3A)n5JKI>0c4{R5!Q`htUZ+kQ;OMTd^3xQF$?_OZZtr1?K^#EweaYuDO
zPvR_5gROvlCm|%Y7D^VxLGi3w^%qzj5H|Nl=HSrhrYmN^7QC1R@h)YddTGXH?_b7g
zdGyV45P})QH@#uqkIbufDmhnLr9nT$ATvrV_M*2d0Wc@VIc
zV_nexvmC&+L?|7`T}SW>gg)YGiIo~Qsta>1dz8Ag|AiwYp@6>erD7@MJ8~H1Mjns{
zsscH<$8FF`y3jj-6b4F>N^5YpGb{(4;-HzeFQgoESbSxWmPm-`d)M%xYc4A**SUU0
z2uF*&t|&~O`IrYn!?gTO++nL=$J7mbL{#JB)-!&I8#uMiY~Oer<5#B;nXv(ZkJ0IE
z3z#1aewwg+#y%$kNaKiL_Q2lp!Lkq$CZR`b*$^1(fPY3c!UnYl4i!ay+!DC{(~Hdt
z(Wo4DO#y{Aaw&~U>@u-MT
zCy3iO0=jY)X2(Q$wsw*-u4F)r=j@qmnNnu#tXvu7^@`T~HWf|2`sut`y1Bt|9`ov@
zGXDbH^>|dwrBD5cBOIu8C!u;(f#AEM1FqC}s+-{f18WIfuSHh8C&3h-Xc6JvSp`^Mccy4OuBL
zAOXDea{CREc8&`0NWL%rK*~=d+fFlUS@B~nu*8pIP<8_aNb$_{$6W`
zW??J%`^WC|aZ;|gL%Y)nQ{{R%@?+}w(G$wQEZ`f#d+kSP*O|(&tv*o{C!$X{?1z}*UifyOo?9i*bmwX2W3I7wI^SQL-$`h
zmP3RVKGe`0*lf;RGEPJ1ACz15DjeA7Tpw)twt6~)xHw&iS<|X27tH(l!Tf>>|_egI)wPUO(bX@ZX{@73#v+a02mMShGuLi*TRhE~*D&uapof
zSDh&GrF%=^%4Z{pSJP*g<#Qe%+C4*PL%7@bgeyUmWFTEanP0ljPCncXItjAYSosFMr20f8LsTkTEJ$Gd+=
z>hLjUJVxS7s8e*hBgbR#Gre*;3=P2>{%qZi54+EWVuW?89$Q1GC1@@gD~8Na#@i+N
z7VRF>oh$?mDLSC|xQO7K-*B-|LkqtI&n
ze`PVi9EI9!mq|tVZp;{An`x2y68ykxeSkPLnAG$Dro}Zr#kwa=^^g!tj{0ih;Ger{@gkKye-Z=_9ecN;iT@O86O2R(ul9N<*?tDE554S9Cr*Z2%#S*JIMGp=
zbyd0nZ_6gLNv%I$6FJfCmAYT#TON5;ID{nH-ZojFAK@O
z1}MP$6_`mt>s8X9Jabv+${Ae`&sN$T0nuvc?^-u1uC%IHmOk!TLn5xKd^&FVvrYQ(
zF0uG4=c^}cOqM^4v#v7YM=U2$WJ9EZ!CTUbYhQ7&v+g&R9&YluSZ56KKknvDDfWyC
zG3HWOZ{+ZuJ|$L)rOK^lsq~eU-4~^vzR`}+j{uh{V(3lU%fT$qdM-bB@8!|OsI|Ud
z)KPgUwnV7tw^#)&NPHLUCY&pF
z%#Yius){PL!$92s@(ar&E>FKlLUWQ{zICrp@-)#vf^n
zLzUD|k#g`{#tDo7b^sUp)kN!Xu67=_QJxQ%>-zk%MDy?;05c9H4#V5EB+_Th-UKSb
zkJO{(F-tU5WemD?iJD)?LdHbs%osKl2p`I!aQ~Cm*95jI_kv~?I2g1Zo#7l
zzu2m-UYFInFQykr(O#WQzYo>k6GruK=a{O8Y>{zRq*G;q{rsG&Q
zCdfYZv?b9r)*GGTuT(-hHB7RceU%_`q_^DHkl^RUqrzw-FT|)^>JH%0F0H8#On&$J
zv_9dmBZKUB%|YYp#nH;0AO43d{};5z-SZZfIi4vEKd}u;d-^Uc*XZ&0@t-FPw`$-
zIT%9%c^wZLE`Dli@0eR(=1=fglUAx!x)3Xpy
z?U!{;uTanVxNB(yYKX8FUwmC!mqnCEL0P8e68#;(?p)Vo^lv6R3aUUDGqk8Y1;O=D
z!tv0Dcxb5Hgr%ORYHy;7H~$=DFF3M2!|nZ%`8{ZE^_^-Bp@D{RjE+%_y}q!K&$iz_
z#QDs?`OM7r``z-A5^DRE^R591SKr=%t;1ogvKv|TuGeS%%1MZquwXVk$fHm5t4NZq
z_W{&tPf9kKcc6eP1i#?ui8HrGGs5=TYzbKF@RS
zePW}sST14SDVCnDRAelQ`R^5z7%QvYdzXNhVM#hacgXXu*Q?y~>6mKUhK-y7rI9Zd
zD9^ruB}-L$2()bFg)-%pw4=LKV#wgBd>kC+$#QBB#7v|G7U8FP87wpRO6O7xAx;0r
z-==@t2!}|9L@_(n&UXnCwhEv|h>qO4z+kb>yZ!GLayf@R-i))L%JsMTQF#Da^+%Ni
zP^)t0M9M>3mYr_FTQEn7wRJqg=6VGALLC*&6*X3+=oDP{rTVcBH>CkdhbV|_@ML}*
z5xVniQoOhi_rnzSQa>;C0AYS(uN(n}#!z({V(!HFyN|Pbx<1XU0K-95agmuRQ
zDn%I;Q%4Ml7r+dK|M{(^@``~GgLs(ythV;Ac##U&k`w+pPZ>3Yn=J6(c-k@6
z9Kdm_@j+Mhu#We;&Qi5k2Al9)-}m|YYNkxn!%Y`haYn3KcnHew+SVWq=d-@)KgECQ
zdsqjfqlbrvh*Iau4||^Qm9}+m7Umt|CWNkekimpkCE;lO=dDZ@)=N1X%ii64B3CgX
z9wCGhf8PW3N_-FzH@HhXw>8KN7S4{1FpLH@gvy2b)s@LFKNel|=et}M;>m1d*OoC#
z$C5wM|Lx7>fqty*h>5WZ;YG+(`+U_oj=()W@+MPz^s41kp01MeQGnDhELT@oj{s<&
zPeVWgY1*_n3`qbP=d&Wtixq%On4R`mcMkCts%Vmv1?46WQ4|)5CHEBdF0OR$W!SLs
z$PJe;)*yRV^m?!IRP0nj)1*OU?iH%mGrq}-&1ghUlhaB&S0>N$de8tq>R18`6aK+G
z1)3l;v!1)kZeMV6R{6sz4Sza*a@qE(?8aZY@>vAA^mm23&+)iE76WZ*^<%+
z4sC_;N3f;jdK{AX`d0vP+tuK&Y)u#!ciRFX>cH@nOp*HpVkoC4Hp*kOg$B_9n5T7B
zJgUojD$@Tu3OW2VK&gXu1ly2F=)yJf!64y8wClo>R=Q|t+pv`D3_^95C~yf|xNM5_
za%DWd)C_q@V|~u%@aCU_k!=Z)*z#vy|26JM>A*j&&epcIbrx!lwLEog8Ex(8A@w8<
zVpc~WvB0XRon*$*rlExtbQNKmcgA=2th{X4cp3U=_xPb3iVwAGP7-br
zuQB>9M=T=AQYpfYs_1(;A}NSnuO-#qF!qiTse3B4ZgA-y(fyr2mdrZx*vvNV@^^K5
zm2nuOcx{2Uu*}J<&oPveeku>~Lv@nPHz%=r=8vP{-jyW}Bo_6%0f&dE&S_ujX`l9K
zeYzDhW}TDZuU11kliaAy@PVoqKff3mch%nq3}<5xFR@u<4AraCyGfnw%v=bnOt`3c
zKj<%x{VaxW`E7xk1Q#+FvIkG?52L*lN-De0Xn$wV=6ecVwRa!n9d0Z!C!f+>+y(WR
z(^XIIBn6plO^zV#592HdL!)?Hdxd=qKo{iuwS0)+4-CH|Io;DJ6I_eZuMgdtr~XiK
zO_TTyi}p(gXMV%uui`~vBP*90BkRZ(<%!yvwU)!IfvJoILaw?EpRo%AWj{`M4^yNV
z15BKhf1zY<_U?4|^{y8q(!gUVIUTGm{S5ItczEW=oId8YmRM6mTgIf?J5i6
zR{7zE1fa8JY>9t}VfqFJNP9|X{5Op=ta^r!TQk|qm3X2)&>U2OD>+S6Yg(o5{C(A+
zyH%|K?*KF>2>WPy0sSk{7^DZ#93UVz+mTxIU)g9PY}cw{xaS0*wu&4GhD6x%A*}wS
zA@9!X@_!(6x7wfYnYxplqN@wz3eB@kNvh8sU9Gq}3E6VWsP=tM%>Fb|xmbd$boM}>
z{&W~_o?b@w&;RTCswW)ql@!IX$S9Ml<=WCA1Y7owfhUXcs<_xpy>Rt_U
zuiRi$C~+NHF6Zxk;wZk9pOQ5Sba4^{XioOsX0P;lm-*-6T#!&pO->rb&O7uF%rSQ_
zL@K16YyYNN>9aC+RBIQoT7h@8C<|2pJ{C%qWa#5@+~p}jN}^b2^XVgwwPMwv(4?Ab
z|Ndh;G=@Vi_@OGpVJi3~fPdi+(161>R=&4NmQgpambR_M@=t+ST^pYD_rW=*mSbLJ
z)Lz@J+qPwDtuIP7vLYV-|7+#k|C#>ZKQ2*9a!QKcPDPC*(G*&895rNfN=rFqcv%i}
zY&toF9CCQ06%lh@hz+X=HKHP?v6*4WVa%B}^zr`w4c}j{pPtusdp@7n^>{w7>t3e8
zZEBlO?%k$~%#aAib
zC*w21PYxfh)^`wj+5CfAS{rrCzW-Ul+02u)NV`(m!iq@R
z@(PD;9dqA3&oQMk-u_Q|_m7`(T)Yobx
z+@=sy!OSdRJ`(9OYY;M@eN7m-M`971DC>;)NUpuuVHvf
zNAf``yYB&0Q8Ju~zrSz3!FKn5;^8!1IL1LeYc-?EHg}X
zi37Wn?)pM!FqmJh#G>bT8$Dj5Qqd`R`1m$bNPZz@CCpfOmal;*S<ObYn`|BCpUK3%_+R#7j>FqH}_Fd7cq49Smza98Nhh)nB
zLiWh>nhrRKG0t7977HkBTmwA6@Y&~P@qu!zcxb~lCErTT*KZI#ya^j4JA~@#NcbrJl*^a@XZ|l(au#|1CBo@+
z?Xl{PLy8pQ=xL+gn^(_@dY|qf1iKd7Y*^I`h?g-fdQmKCA1EH;{}`<(1sLjrF>J;i
zx1|8>nWBt{*)VQFAJn9Y^)TJddUED^nk)iJqxE2yviG9bauJu8-a3?6
zwQf_+Ud449tdU;MfHq^dPWGYs=7z@~IF+nq=&O+1m&F>m+&pdpl}x#%ISs3_x2hr-)J
zL9XIxhwP`Y(3A&z19}hGKYb)%1I`s_-7P4oB9>|wI~1k-JG3qudZlamydE&?Ia;Bg
zHR|bX-HeR&8Mme8q=0q#T?fQcu0|am^}QpCYL4w&>D>SDN;0>Tgn4Xv!B|%Fv``{q!ldV9749
z2gA$@@Hubl&0G4@!dEi;iM*LhM8WHsMLw+|bwFDiQS$CX4t|x;Rlj3!SLd@m?me*u
zrq-vO{d`O5NYzM8az84;6kc}pT5hVyc8Yvgg%4a02pIsAQ58pJvYqmtnB>Wfme%S#
z7#-1e$|sdd2N!Z*aq(jXKg-L9Vd@`JtYvvyBhVjavrk4JrE|qp^oCE
z$`pr^nL5T{fmSj7i7Q{x&8bdsowqH&cEF(^rAoKf)i4Cap^h?te0W)Ql;w9wt-lLOm)o7`e|9qVrv&S^HJ~wpriJO0v-JLNV-pjt~*9v8tM{JR#DHGc2yV|x~dD*Dr3ax
zgqsgf6kw4kq-RkFv#@~o3UoN8JW4wzZznRVW`5;UNCI0I9)T;w(7VD`Z?$V#}prWNEZ_AX-vr>Q9yP}-P-r6`gckm+R9;2}a
zV6qdHT1*PGo@^-RpX)V#sJWrsM`Y92HS`;Nft-9Rt}G+OORrcKmH4
zDq4Jc16kt4bAZPgAwg|gjuk|8zVZIcCl?LbR%a!$e
zMSKYM^SRCfX(|6MFWA_YcMz
z@+XF3CwOVWS%9wnl$sl-Bg?K4sZ|d7vS6{@nthxmr5A78da)B80uy3r)B8NEgbVw$
zp4JYEX;>#(aJH*)jT@jypW$RF!^8>5Y&Qz$Ujbg{pv7xe+dDDp2kb~9D5+-eLCJrhWbS=G-bDZ
z*_E0pAIBi3Gu5E{ALjBJm&Cco%PevTzZ8#cG{VaK_AGHuL0lQDv)mp)ZWys1D$|ou
zoqPR=H>rdFcDKv}8Fz+>KnMEd2%#2{!#L9oTPq|USUc3>8;>(C0ohm0x2CFeO90|w
zH$t?-sclpuyh$r0&L?uwy(nok%y=7DCyAQf;z`(Na|@0bxiS{i#%EO61osy37tb$R
zEC}tbxN~r3oo0idKHg>rVoEFK5Vsk`hB-3p@FQ~KVOWE#zhFKU>3;XkF*_Ff0x!)p
z!IT|R*sf&=8Ieu*2)p4LBbW6d=|a-CQ<-QNPdNpeeaQ1oy+L~>zGE4M>)@C;;h_0w
zXJj}}f&4=|PNf0j89K@OZT8Dzk1M0Obk_|W!rpO1>$^#OE#e9i5J>^xS=C}?m63`>
zNvB7ADn9neyY~Wh!BnB9um_#Z`bO8GS$dMBQC_Sg}U@
z+G3l3AeyRf1hL(f3!+rK;U%S8_byl8dLe^KaAF$FiS
zl2jYg+n|ZC1$fV?u3mG+3_3(rEVB8h7N*d>{{kd9pC|FaB?p;MHjQYFxJ;NEH^2^gEQP||U4ps;u&LIQYwgI@Gy)|Cz`)tm-1pC}G
zUByy4s>Ha)SF}i$q~TB$;L@n_r#Qo>?BgXHCyNR{l4=8zQ*>`z*-k4K>=!Y}?|bAO
z(?il^q)rfhFtRPX+PbL?UqZofP5Ky=dOJQCcB39#zYtB2wM7*+K0tas?t6B;?XuWP
zoSvI(19h@(r!1i1`FfmA4IQd~?){Hi2?g4~IuiXI;t?X!GUtOXJ6-cb)Kh+$F_{gC
zrfqCO)X}|BO-yj=hZNp$j1fQA!ZG(+{=#F3PjR9X+!M2Hv{Z%%r5m}rxL44}<&_}j
z7X0yTEV-eSJr2er7VwRB7=>3A?7-bmX`I?RBi;df%UQ>?ZuTG_9%hpVF)dM|rU10OVi;K-R#@XOa&vD2KGfXgRhF
zXqDKK*@(x%=bg;e*20V`8*Z5aK?CzF_8>9oJK3LtpnzBBYc?7busz^R@{dH6A}0`n
zyo5dU5+QgJ6UERx$q1V9ZWLF5oJ6FT(Z1b$QD*`?E+|NIEmfQI%C^p+9}kUk(Y_$JjZnizi@{4(8!8`QgIUyXwj#IW{`E-vH@
zfYEth-XPfJ3Ub;c0vAO(k5<&N&Wq5S9CDQLS1-14jKIm7X9c1)<}N^2B``?2-Ao(7
zv8otb+EMmBnVx!yKFe^>u9Ni2i~BV1l(!+V_Y$^y%_#IGN7=$F)EVR{M2d-|uao)Kc_88$I%2bNWD6iN!I7YXt|G-ZZ2mgckUbu3ogix7hm9>Q#BS=#F*N$uFKo3hNYcKib=D8?Jg#?-GvF
zqwpCa?^iuW>C*;KIJ#^W8Mv&?%N7f%OY*mCM;0=QWxMUIvlVDL`dz@C
zBV$WT95RszG{%-<3R?m}9iiTbtb+IM-na?}<4z60Fom--7gYT&$OiZK#%|^_`cI@q
zw8O~+xUvnoV&$VlbkgR9TT_5YlXtE
zVB7U#D1d)tw_O4reVIH(h6c5QY2geWKiip?K?b_mH3z(vy7rF!w@-;a{}w9gBt8#8HD_Z?)U$
zV-dTJiuBA5>;}{2wV17ro0E{Ewk%^dAv$7w>oY|JAVhchB@si174`ZrUzVQoz#8
L+LT~?_v!xt2^mxi
literal 0
HcmV?d00001
diff --git a/apollo-portal/src/main/resources/static/scripts/app.js b/apollo-portal/src/main/resources/static/scripts/app.js
index bac2dd5fde8..88ecb81e522 100644
--- a/apollo-portal/src/main/resources/static/scripts/app.js
+++ b/apollo-portal/src/main/resources/static/scripts/app.js
@@ -64,6 +64,8 @@ var diff_item_module = angular.module('diff_item', ['app.service', 'apollo.direc
var namespace_module = angular.module('namespace', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']);
//server config
var server_config_manage_module = angular.module('server_config_manage', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']);
+// Value的全局检索
+var global_search_value_module = angular.module('global_search_value', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'ngSanitize']);
//setting
var setting_module = angular.module('setting', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar', 'valdr']);
//role
diff --git a/apollo-portal/src/main/resources/static/scripts/controller/GlobalSearchValueController.js b/apollo-portal/src/main/resources/static/scripts/controller/GlobalSearchValueController.js
new file mode 100644
index 00000000000..ee720aacc58
--- /dev/null
+++ b/apollo-portal/src/main/resources/static/scripts/controller/GlobalSearchValueController.js
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+global_search_value_module.controller('GlobalSearchValueController',
+ ['$scope', '$window', '$translate', 'toastr', 'AppUtil', 'GlobalSearchValueService', 'PermissionService', GlobalSearchValueController]);
+
+function GlobalSearchValueController($scope, $window, $translate, toastr, AppUtil, GlobalSearchValueService, PermissionService) {
+
+ $scope.allItemInfo = [];
+ $scope.pageItemInfo = [];
+ $scope.itemInfoSearchKey = '';
+ $scope.itemInfoSearchValue = '';
+ $scope.needToBeHighlightedKey = '';
+ $scope.needToBeHighlightedValue = '';
+ $scope.isShowHighlightKeyword = [];
+ $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+ $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+ $scope.isAllItemInfoDisplayValueInARow = [];
+ $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+ $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+ $scope.isPageItemInfoDisplayValueInARow = [];
+ $scope.currentPage = 1;
+ $scope.pageSize = '10';
+ $scope.totalItems = 0;
+ $scope.totalPages = 0;
+ $scope.pagesArray = [];
+ $scope.tempKey = '';
+ $scope.tempValue = '';
+
+ $scope.getItemInfoByKeyAndValue = getItemInfoByKeyAndValue;
+ $scope.highlightKeyword = highlightKeyword;
+ $scope.jumpToTheEditingPage = jumpToTheEditingPage;
+ $scope.isShowAllValue = isShowAllValue;
+ $scope.convertPageSizeToInt = convertPageSizeToInt;
+ $scope.changePage = changePage;
+ $scope.getPagesArray = getPagesArray;
+ $scope.determineDisplayKeyOrValueWithoutShowHighlightKeyword = determineDisplayKeyOrValueWithoutShowHighlightKeyword;
+ $scope.determineDisplayValueInARow = determineDisplayValueInARow;
+
+ init();
+ function init() {
+ initPermission();
+ }
+
+ function initPermission() {
+ PermissionService.has_root_permission()
+ .then(function (result) {
+ $scope.isRootUser = result.hasPermission;
+ });
+ }
+
+ function getItemInfoByKeyAndValue(itemInfoSearchKey, itemInfoSearchValue) {
+ $scope.currentPage = 1;
+ $scope.itemInfoSearchKey = itemInfoSearchKey || '';
+ $scope.itemInfoSearchValue = itemInfoSearchValue || '';
+ $scope.allItemInfo = [];
+ $scope.pageItemInfo = [];
+ $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+ $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+ $scope.isAllItemInfoDisplayValueInARow = [];
+ $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+ $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+ $scope.isPageItemInfoDisplayValueInARow = [];
+ $scope.tempKey = itemInfoSearchKey || '';
+ $scope.tempValue = itemInfoSearchValue || '';
+ $scope.isShowHighlightKeyword = [];
+ GlobalSearchValueService.findItemInfoByKeyAndValue($scope.itemInfoSearchKey, $scope.itemInfoSearchValue)
+ .then(handleSuccess).catch(handleError);
+ function handleSuccess(result) {
+ let allItemInfo = [];
+ let isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+ let isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+ let isAllItemInfoDisplayValueInARow = [];
+ if(($scope.itemInfoSearchKey === '') && !($scope.itemInfoSearchValue === '')){
+ $scope.needToBeHighlightedValue = $scope.itemInfoSearchValue;
+ $scope.needToBeHighlightedKey = '';
+ result.body.forEach((itemInfo, index) => {
+ allItemInfo.push(itemInfo);
+ isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = '0';
+ isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.value, itemInfoSearchValue);
+ isAllItemInfoDisplayValueInARow[index] = determineDisplayValueInARow(itemInfo.value, itemInfoSearchValue);
+ });
+ }else if(!($scope.itemInfoSearchKey === '') && ($scope.itemInfoSearchValue === '')){
+ $scope.needToBeHighlightedKey = $scope.itemInfoSearchKey;
+ $scope.needToBeHighlightedValue = '';
+ result.body.forEach((itemInfo, index) => {
+ allItemInfo.push(itemInfo);
+ isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.key, itemInfoSearchKey);
+ isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = '0';
+ });
+ }else{
+ $scope.needToBeHighlightedKey = $scope.itemInfoSearchKey;
+ $scope.needToBeHighlightedValue = $scope.itemInfoSearchValue;
+ result.body.forEach((itemInfo, index) => {
+ allItemInfo.push(itemInfo);
+ isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.value, itemInfoSearchValue);
+ isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword[index] = determineDisplayKeyOrValueWithoutShowHighlightKeyword(itemInfo.key, itemInfoSearchKey);
+ isAllItemInfoDisplayValueInARow[index] = determineDisplayValueInARow(itemInfo.value, itemInfoSearchValue);
+ });
+ }
+ $scope.totalItems = allItemInfo.length;
+ $scope.allItemInfo = allItemInfo;
+ $scope.totalPages = Math.ceil($scope.totalItems / parseInt($scope.pageSize, 10));
+ const startIndex = ($scope.currentPage - 1) * parseInt($scope.pageSize, 10);
+ const endIndex = Math.min(startIndex + parseInt($scope.pageSize, 10), allItemInfo.length);
+ $scope.pageItemInfo = allItemInfo.slice(startIndex, endIndex);
+ $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword;
+ $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword;
+ $scope.isAllItemInfoDisplayValueInARow = isAllItemInfoDisplayValueInARow;
+ $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword.slice(startIndex, endIndex);
+ $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword.slice(startIndex, endIndex);
+ $scope.isPageItemInfoDisplayValueInARow = isAllItemInfoDisplayValueInARow.slice(startIndex, endIndex);
+ getPagesArray();
+ if(result.hasMoreData){
+ toastr.warning(result.message, $translate.instant('Item.GlobalSearch.Tips'));
+ }
+ }
+
+ function handleError(error) {
+ $scope.itemInfo = [];
+ toastr.error(AppUtil.errorMsg(error), $translate.instant('Item.GlobalSearchSystemError'));
+ }
+ }
+
+ function convertPageSizeToInt() {
+ getItemInfoByKeyAndValue($scope.tempKey, $scope.tempValue);
+ }
+
+ function changePage(page) {
+ if (page >= 1 && page <= $scope.totalPages) {
+ $scope.currentPage = page;
+ $scope.isShowHighlightKeyword = [];
+ $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = [];
+ $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = [];
+ $scope.isPageItemInfoDisplayValueInARow = [];
+ $scope.itemInfoSearchKey = $scope.tempKey;
+ $scope.itemInfoSearchValue = $scope.tempValue;
+ const startIndex = ($scope.currentPage - 1)* parseInt($scope.pageSize, 10);
+ const endIndex = Math.min(startIndex + parseInt($scope.pageSize, 10), $scope.totalItems);
+ $scope.pageItemInfo = $scope.allItemInfo.slice(startIndex, endIndex);
+ $scope.isPageItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword = $scope.isAllItemInfoDirectlyDisplayValueWithoutShowHighlightKeyword.slice(startIndex, endIndex);
+ $scope.isPageItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword = $scope.isAllItemInfoDirectlyDisplayKeyWithoutShowHighlightKeyword.slice(startIndex, endIndex);
+ $scope.isPageItemInfoDisplayValueInARow = $scope.isAllItemInfoDisplayValueInARow.slice(startIndex, endIndex);
+ getPagesArray();
+ }
+ }
+
+ function getPagesArray() {
+ const pageRange = 2;
+ let pagesArray = [];
+ let currentPage = $scope.currentPage;
+ let totalPages = $scope.totalPages;
+ if (totalPages <= (pageRange * 2) + 4) {
+ for (let i = 1; i <= totalPages; i++) {
+ pagesArray.push(i);
+ }
+ } else {
+ if (currentPage <= (pageRange + 2)) {
+ for (let i = 1; i <= pageRange * 2 + 2; i++) {
+ pagesArray.push(i);
+ }
+ pagesArray.push('...');
+ pagesArray.push(totalPages);
+ } else if (currentPage >= (totalPages - (pageRange + 1))) {
+ for (let i = totalPages - pageRange * 2 - 1 ; i <= totalPages; i++) {
+ pagesArray.push(i);
+ }
+ pagesArray.unshift('...');
+ pagesArray.unshift(1);
+ } else {
+ for (let i = (currentPage - pageRange); i <= currentPage + pageRange; i++) {
+ pagesArray.push(i);
+ }
+ pagesArray.unshift('...');
+ pagesArray.unshift(1);
+ pagesArray.push('...');
+ pagesArray.push(totalPages);
+ }
+ }
+ $scope.pagesArray = pagesArray;
+ }
+
+ function determineDisplayValueInARow(value, highlight) {
+ var valueColumn = document.getElementById('valueColumn');
+ var testElement = document.createElement('span');
+ setupTestElement(testElement, valueColumn);
+ testElement.innerText = value;
+ document.body.appendChild(testElement);
+ const position = determinePosition(value, highlight);
+ let displayValue = '0';
+ if (testElement.scrollWidth > testElement.offsetWidth) {
+ displayValue = position;
+ } else {
+ if (testElement.scrollWidth === testElement.offsetWidth) {
+ return '0';
+ }
+ switch (position) {
+ case '1':
+ testElement.innerText = value + '...' + '| ' + $translate.instant('Global.Expand');
+ break;
+ case '2':
+ testElement.innerText = '...' + value + '| ' + $translate.instant('Global.Expand');
+ break;
+ case '3':
+ testElement.innerText = '...' + value + '...' + '| ' + $translate.instant('Global.Expand');
+ break;
+ default:
+ return '0';
+ }
+ if (testElement.scrollWidth === testElement.offsetWidth) {
+ displayValue = '0';
+ } else {
+ displayValue = position;
+ }
+ }
+ document.body.removeChild(testElement);
+ return displayValue;
+ }
+
+ function setupTestElement(element, valueColumn) {
+ element.style.visibility = 'hidden';
+ element.style.position = 'absolute';
+ element.style.whiteSpace = 'nowrap';
+ element.style.display = 'inline-block';
+ element.style.fontFamily = '"Open Sans", sans-serif';
+ const devicePixelRatio = window.devicePixelRatio;
+ const zoomLevel = Math.round((window.outerWidth / window.innerWidth) * 100) / 100;
+ element.style.fontSize = 13 * devicePixelRatio * zoomLevel + 'px';
+ element.style.padding = 8 * devicePixelRatio * zoomLevel + 'px';
+ element.style.width = valueColumn.offsetWidth * devicePixelRatio * zoomLevel + 'px';
+ }
+
+ function determinePosition(value, highlight) {
+ const position = value.indexOf(highlight);
+ if (position === -1) return '-1';
+ if (position === 0) return '1';
+ if (position + highlight.length === value.length) return '2';
+ return "3";
+ }
+
+ function determineDisplayKeyOrValueWithoutShowHighlightKeyword(keyorvalue, highlight) {
+ return keyorvalue === highlight ? '0' : '-1';
+ }
+
+ function jumpToTheEditingPage(appid,env,cluster){
+ let url = AppUtil.prefixPath() + "/config.html#/appid=" + appid + "&" +"env=" + env + "&" + "cluster=" + cluster;
+ window.open(url, '_blank');
+ }
+
+ function highlightKeyword(fulltext,keyword) {
+ if (!keyword || keyword.length === 0) return fulltext;
+ let regex = new RegExp("(" + keyword + ")", "g");
+ return fulltext.replace(regex, '$1');
+ }
+
+ function isShowAllValue(index){
+ $scope.isShowHighlightKeyword[index] = !$scope.isShowHighlightKeyword[index];
+ }
+
+}
diff --git a/apollo-portal/src/main/resources/static/scripts/services/GlobalSearchValueService.js b/apollo-portal/src/main/resources/static/scripts/services/GlobalSearchValueService.js
new file mode 100644
index 00000000000..52a345449b7
--- /dev/null
+++ b/apollo-portal/src/main/resources/static/scripts/services/GlobalSearchValueService.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+appService.service('GlobalSearchValueService', ['$resource', '$q', 'AppUtil', function ($resource, $q, AppUtil) {
+ let global_search_resource = $resource('', {}, {
+ get_item_Info_by_key_and_Value: {
+ isArray: false,
+ method: 'GET',
+ url: AppUtil.prefixPath() + '/global-search/item-info/by-key-or-value',
+ params: {
+ key: 'key',
+ value: 'value'
+ }
+ }
+ });
+ return {
+ findItemInfoByKeyAndValue:function (key,value){
+ let d = $q.defer();
+ global_search_resource.get_item_Info_by_key_and_Value({key: key,value: value},function (result) {
+ d.resolve(result);
+ }, function (error) {
+ d.reject(error);
+ });
+ return d.promise;
+ }
+ }
+}]);
diff --git a/apollo-portal/src/main/resources/static/views/common/nav.html b/apollo-portal/src/main/resources/static/views/common/nav.html
index c2f73b213dd..a78f2b68dd0 100644
--- a/apollo-portal/src/main/resources/static/views/common/nav.html
+++ b/apollo-portal/src/main/resources/static/views/common/nav.html
@@ -66,6 +66,7 @@
{{'Common.Nav.SystemInfo' | translate }}
{{'Common.Nav.ConfigExport' | translate }}
{{'ApolloAuditLog.Title' | translate }}
+ {{'Global.Title' | translate }}
diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchControllerTest.java
new file mode 100644
index 00000000000..03231dcc59e
--- /dev/null
+++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/GlobalSearchControllerTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.ctrip.framework.apollo.portal.controller;
+
+/**
+ * @author hujiyuan 2024-08-10
+ */
+
+import com.ctrip.framework.apollo.common.http.SearchResponseEntity;
+import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
+import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo;
+import com.ctrip.framework.apollo.portal.service.GlobalSearchService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.*;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GlobalSearchControllerTest {
+
+ private MockMvc mockMvc;
+
+ @Mock
+ private PortalConfig portalConfig;
+
+ @Mock
+ private GlobalSearchService globalSearchService;
+
+ @InjectMocks
+ private GlobalSearchController globalSearchController;
+
+ private final int perEnvSearchMaxResults = 200;
+
+ @Before
+ public void setUp() {
+ when(portalConfig.getPerEnvSearchMaxResults()).thenReturn(perEnvSearchMaxResults);
+ mockMvc = MockMvcBuilders.standaloneSetup(globalSearchController).build();
+ }
+
+ @Test
+ public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnEmptyItemInfos() throws Exception {
+ when(globalSearchService.getAllEnvItemInfoBySearch(anyString(), anyString(),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.ok(new ArrayList<>()));
+ mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param("key", "query-key")
+ .param("value", "query-value"))
+ .andExpect(status().isOk())
+ .andExpect(content().json("{\"body\":[],\"hasMoreData\":false,\"message\":\"OK\",\"code\":200}"));
+ verify(portalConfig,times(1)).getPerEnvSearchMaxResults();
+ verify(globalSearchService,times(1)).getAllEnvItemInfoBySearch(anyString(), anyString(),eq(0),eq(perEnvSearchMaxResults));
+ }
+
+ @Test
+ public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnExpectedItemInfos_ButOverPerEnvLimit() throws Exception {
+ List allEnvMockItemInfos = new ArrayList<>();
+ allEnvMockItemInfos.add(new ItemInfo("appid1","env1","cluster1","namespace1","query-key","query-value"));
+ allEnvMockItemInfos.add(new ItemInfo("appid2","env2","cluster2","namespace2","query-key","query-value"));
+ when(globalSearchService.getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.okWithMessage(allEnvMockItemInfos,"In DEV , PRO , more than "+perEnvSearchMaxResults+" items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope."));
+ mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param("key", "query-key")
+ .param("value", "query-value"))
+ .andExpect(status().isOk())
+ .andExpect(content().json("{\"body\":[" +
+ " { \"appId\": \"appid1\",\n" +
+ " \"envName\": \"env1\",\n" +
+ " \"clusterName\": \"cluster1\",\n" +
+ " \"namespaceName\": \"namespace1\",\n" +
+ " \"key\": \"query-key\",\n" +
+ " \"value\": \"query-value\"}," +
+ " { \"appId\": \"appid2\",\n" +
+ " \"envName\": \"env2\",\n" +
+ " \"clusterName\": \"cluster2\",\n" +
+ " \"namespaceName\": \"namespace2\",\n" +
+ " \"key\": \"query-key\",\n" +
+ " \"value\": \"query-value\"}],\"hasMoreData\":true,\"message\":\"In DEV , PRO , more than 200 items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.\",\"code\":200}"));
+ verify(portalConfig,times(1)).getPerEnvSearchMaxResults();
+ verify(globalSearchService, times(1)).getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults));
+ }
+
+ @Test
+ public void testGet_ItemInfo_BySearch_WithKeyAndValueAndActiveEnvs_ReturnExpectedItemInfos() throws Exception {
+ List allEnvMockItemInfos = new ArrayList<>();
+ allEnvMockItemInfos.add(new ItemInfo("appid1","env1","cluster1","namespace1","query-key","query-value"));
+ allEnvMockItemInfos.add(new ItemInfo("appid2","env2","cluster2","namespace2","query-key","query-value"));
+ when(globalSearchService.getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults))).thenReturn(SearchResponseEntity.ok(allEnvMockItemInfos));
+
+ mockMvc.perform(MockMvcRequestBuilders.get("/global-search/item-info/by-key-or-value")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param("key", "query-key")
+ .param("value", "query-value"))
+ .andExpect(status().isOk())
+ .andExpect(content().json("{\"body\":[" +
+ " { \"appId\": \"appid1\",\n" +
+ " \"envName\": \"env1\",\n" +
+ " \"clusterName\": \"cluster1\",\n" +
+ " \"namespaceName\": \"namespace1\",\n" +
+ " \"key\": \"query-key\",\n" +
+ " \"value\": \"query-value\"}," +
+ " { \"appId\": \"appid2\",\n" +
+ " \"envName\": \"env2\",\n" +
+ " \"clusterName\": \"cluster2\",\n" +
+ " \"namespaceName\": \"namespace2\",\n" +
+ " \"key\": \"query-key\",\n" +
+ " \"value\": \"query-value\"}],\"hasMoreData\":false,\"message\":\"OK\",\"code\":200}"));
+ verify(globalSearchService, times(1)).getAllEnvItemInfoBySearch(eq("query-key"), eq("query-value"),eq(0),eq(perEnvSearchMaxResults));
+ }
+
+}
diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/GlobalSearchServiceTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/GlobalSearchServiceTest.java
new file mode 100644
index 00000000000..661a692447d
--- /dev/null
+++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/GlobalSearchServiceTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2024 Apollo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.ctrip.framework.apollo.portal.service;
+
+/**
+ * @author hujiyuan 2024-08-10
+ */
+
+import com.ctrip.framework.apollo.common.dto.ItemInfoDTO;
+import com.ctrip.framework.apollo.common.dto.PageDTO;
+import com.ctrip.framework.apollo.common.http.SearchResponseEntity;
+import com.ctrip.framework.apollo.portal.api.AdminServiceAPI;
+import com.ctrip.framework.apollo.portal.component.PortalSettings;
+import com.ctrip.framework.apollo.portal.entity.vo.ItemInfo;
+import com.ctrip.framework.apollo.portal.environment.Env;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.data.domain.PageRequest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GlobalSearchServiceTest {
+
+ @Mock
+ private AdminServiceAPI.ItemAPI itemAPI;
+
+ @Mock
+ private PortalSettings portalSettings;
+
+ @InjectMocks
+ private GlobalSearchService globalSearchService;
+
+ private final List activeEnvs = new ArrayList<>();
+
+ @Before
+ public void setUp() {
+ when(portalSettings.getActiveEnvs()).thenReturn(activeEnvs);
+ }
+
+ @Test
+ public void testGet_PerEnv_ItemInfo_BySearch_withKeyAndValue_ReturnExpectedItemInfos() {
+ activeEnvs.add(Env.DEV);
+ activeEnvs.add(Env.PRO);
+
+ ItemInfoDTO itemInfoDTO = new ItemInfoDTO("TestApp","TestCluster","TestNamespace","TestKey","TestValue");
+ List mockItemInfoDTOs = new ArrayList<>();
+ mockItemInfoDTOs.add(itemInfoDTO);
+ Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1))).thenReturn(new PageDTO<>(mockItemInfoDTOs, PageRequest.of(0, 1), 1L));
+ SearchResponseEntity> mockItemInfos = globalSearchService.getAllEnvItemInfoBySearch("TestKey", "TestValue", 0, 1);
+ assertEquals(2, mockItemInfos.getBody().size());
+
+ List devMockItemInfos = new ArrayList<>();
+ List proMockItemInfos = new ArrayList<>();
+ List allEnvMockItemInfos = new ArrayList<>();
+ devMockItemInfos.add(new ItemInfo("TestApp", Env.DEV.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue"));
+ proMockItemInfos.add(new ItemInfo("TestApp", Env.PRO.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue"));
+ allEnvMockItemInfos.addAll(devMockItemInfos);
+ allEnvMockItemInfos.addAll(proMockItemInfos);
+
+ verify(itemAPI,times(2)).getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1));
+ verify(portalSettings,times(1)).getActiveEnvs();
+ assertEquals(allEnvMockItemInfos.toString(), mockItemInfos.getBody().toString());
+ }
+
+ @Test
+ public void testGet_PerEnv_ItemInfo_withKeyAndValue_BySearch_ReturnEmptyItemInfos() {
+ activeEnvs.add(Env.DEV);
+ activeEnvs.add(Env.PRO);
+ Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), anyString(), anyString(), eq(0), eq(1)))
+ .thenReturn(new PageDTO<>(new ArrayList<>(), PageRequest.of(0, 1), 0L));
+ SearchResponseEntity> result = globalSearchService.getAllEnvItemInfoBySearch("NonExistentKey", "NonExistentValue", 0, 1);
+ assertEquals(0, result.getBody().size());
+ }
+
+ @Test
+ public void testGet_PerEnv_ItemInfo_BySearch_withKeyAndValue_ReturnExpectedItemInfos_ButOverPerEnvLimit() {
+ activeEnvs.add(Env.DEV);
+ activeEnvs.add(Env.PRO);
+
+ ItemInfoDTO itemInfoDTO = new ItemInfoDTO("TestApp","TestCluster","TestNamespace","TestKey","TestValue");
+ List mockItemInfoDTOs = new ArrayList<>();
+ mockItemInfoDTOs.add(itemInfoDTO);
+ Mockito.when(itemAPI.getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1))).thenReturn(new PageDTO<>(mockItemInfoDTOs, PageRequest.of(0, 1), 2L));
+ SearchResponseEntity> mockItemInfos = globalSearchService.getAllEnvItemInfoBySearch("TestKey", "TestValue", 0, 1);
+ assertEquals(2, mockItemInfos.getBody().size());
+
+ List devMockItemInfos = new ArrayList<>();
+ List proMockItemInfos = new ArrayList<>();
+ List allEnvMockItemInfos = new ArrayList<>();
+ devMockItemInfos.add(new ItemInfo("TestApp", Env.DEV.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue"));
+ proMockItemInfos.add(new ItemInfo("TestApp", Env.PRO.getName(), "TestCluster", "TestNamespace", "TestKey", "TestValue"));
+ allEnvMockItemInfos.addAll(devMockItemInfos);
+ allEnvMockItemInfos.addAll(proMockItemInfos);
+ String message = "In DEV , PRO , more than 1 items found (Exceeded the maximum search quantity for a single environment). Please enter more precise criteria to narrow down the search scope.";
+ verify(itemAPI,times(2)).getPerEnvItemInfoBySearch(any(Env.class), eq("TestKey"), eq("TestValue"), eq(0), eq(1));
+ verify(portalSettings,times(1)).getActiveEnvs();
+ assertEquals(allEnvMockItemInfos.toString(), mockItemInfos.getBody().toString());
+ assertEquals(message, mockItemInfos.getMessage());
+ }
+
+}
diff --git a/changes/changes-2.4.0.md b/changes/changes-2.4.0.md
new file mode 100644
index 00000000000..615d7c1d7a4
--- /dev/null
+++ b/changes/changes-2.4.0.md
@@ -0,0 +1,13 @@
+Changes by Version
+==================
+Release Notes.
+
+Apollo 2.4.0
+
+------------------
+* [Update the server config link in system info page](https://github.com/apolloconfig/apollo/pull/5204)
+* [Feature support portal restTemplate Client connection pool config](https://github.com/apolloconfig/apollo/pull/5200)
+* [Feature added the ability for administrators to globally search for Value](https://github.com/apolloconfig/apollo/pull/5182)
+
+------------------
+All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)
diff --git a/docs/en/README.md b/docs/en/README.md
index 46fb251756f..d76c4540aa2 100644
--- a/docs/en/README.md
+++ b/docs/en/README.md
@@ -37,6 +37,10 @@ Demo Environment:
* **Grayscale release**
* Support grayscale configuration release, for example, after clicking release, it will only take effect for some application instances. After a period of observation, we could push the configurations to all application instances if there is no problem
+- **Global Search Configuration Items**
+ - A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used
+ - It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations
+
* **Authorization management, release approval and operation audit**
* Great authorization mechanism is designed for applications and configurations management, and the management of configurations is divided into two operations: editing and publishing, therefore greatly reducing human errors
* All operations have audit logs for easy tracking of problems
diff --git a/docs/en/deployment/distributed-deployment-guide.md b/docs/en/deployment/distributed-deployment-guide.md
index 3251d4c5d5e..4dbe8dd7586 100644
--- a/docs/en/deployment/distributed-deployment-guide.md
+++ b/docs/en/deployment/distributed-deployment-guide.md
@@ -769,9 +769,9 @@ apollo.service.registry.cluster=same name with apollo Cluster
```
2. (optional) If you want to customize Config Service and Admin Service's uri for Client,
-for example when deploying on the intranet,
-if you don't want to expose the intranet ip,
-you can add a property in `config/application-github.properties` of the Config Service and Admin Service installation package
+ for example when deploying on the intranet,
+ if you don't want to expose the intranet ip,
+ you can add a property in `config/application-github.properties` of the Config Service and Admin Service installation package
```properties
apollo.service.registry.uri=http://your-ip-or-domain:${server.port}/
```
@@ -1447,6 +1447,14 @@ The default is true, which makes it easy to quickly search for configurations by
If set to false, this feature is disabled
+### 3.1.14 apollo.portal.search.perEnvMaxResults - set the Administrator Tool-Global Search for Value function's maximum number of search results for a single individual environment
+
+> For versions 2.4.0 and above
+
+Default is 200, which means that each environment will return up to 200 results in a single search operation.
+
+Modifying this parameter may affect the performance of the search function, so before modifying it, you should conduct sufficient testing and adjust the value of `apollo.portal.search.perEnvMaxResults` appropriately according to the actual business requirements and system resources to balance the performance and the number of search results.
+
## 3.2 Adjusting ApolloConfigDB configuration
Configuration items are uniformly stored in the ApolloConfigDB.ServerConfig table. It should be noted that each environment's ApolloConfigDB.ServerConfig needs to be configured separately, and the modification takes effect in real time for one minute afterwards.
diff --git a/docs/en/design/apollo-design.md b/docs/en/design/apollo-design.md
index f078c1881d2..0061f696f20 100644
--- a/docs/en/design/apollo-design.md
+++ b/docs/en/design/apollo-design.md
@@ -130,7 +130,7 @@ Why do we use Eureka as a service registry instead of the traditional zk and etc
### 1.3.2 Admin Service
* Provide configuration management interface
-* Provides interfaces for configuration modification, publishing, etc.
+* Provides interfaces for configuration modification, publishing, retrieval, etc.
* Interface service object is Portal
### 1.3.3 Meta Server
diff --git a/docs/en/design/apollo-introduction.md b/docs/en/design/apollo-introduction.md
index a001f5a3a81..f846e6be867 100644
--- a/docs/en/design/apollo-introduction.md
+++ b/docs/en/design/apollo-introduction.md
@@ -78,7 +78,13 @@ It is precisely based on the particularity of configuration that Apollo has been
* **Client configuration information monitoring**
* You can easily see which instances the configuration is being used on the interface
+**Global Search Configuration Items**
+
+- A fuzzy search of the key and value of a configuration item finds in which application, environment, cluster, namespace the configuration item with the corresponding value is used
+- It is easy for administrators and SRE roles to quickly and easily find and change the configuration values of resources by highlighting, paging and jumping through configurations
+
**Java and .Net native clients available**
+
* Provides native clients of Java and .Net for easy application integration
* Support Spring Placeholder, Annotation and Spring Boot's ConfigurationProperties for easy application use (requires Spring 3.1.1+)
* Also provides Http interface, non-Java and .Net applications can also be easily used
diff --git a/docs/en/images/Configuration query-Non properties.png b/docs/en/images/Configuration query-Non properties.png
new file mode 100644
index 0000000000000000000000000000000000000000..1a355074cbc6c388f74598aab746a66592d59737
GIT binary patch
literal 35238
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