From 7c69c4b6b53319ef7fb89b3ecd3ca0cd6ce7924c Mon Sep 17 00:00:00 2001 From: Ayu Date: Fri, 30 Aug 2024 15:44:46 +0300 Subject: [PATCH] feat: add percentages to custom event data (#130) * feat(core): add events percentage to properties endpoint * fix(dashboard): error page styles relying on mantine preset * fix(core): return percentages with materialised table * feat(dashboard): show percentage on home and expanded page --- bun.lockb | Bin 319408 -> 318976 bytes core/api/oas_json_gen.go | 21 ++++++++- core/api/oas_schemas_gen.go | 36 ++++++++++------ core/api/oas_validators_gen.go | 40 ++++++++++++++++++ core/db/duckdb/properties.go | 20 +++++++-- core/db/duckdb/query/qb.go | 11 +++++ core/model/stats.go | 7 +-- core/openapi.yaml | 29 +++++++------ core/services/properties.go | 3 +- dashboard/app/api/types.d.ts | 29 +++++++------ .../app/components/DropdownSelect.module.css | 2 +- .../app/components/layout/Error.module.css | 16 ++++--- dashboard/app/components/layout/Error.tsx | 1 + dashboard/app/components/stats/StatsItem.tsx | 8 +++- dashboard/app/components/stats/Table.tsx | 14 +++++- dashboard/app/components/stats/Tabs.tsx | 2 +- dashboard/app/components/stats/types.ts | 2 + dashboard/package.json | 1 - 18 files changed, 184 insertions(+), 58 deletions(-) diff --git a/bun.lockb b/bun.lockb index 8f7af2aa6254ad79e3c5a556fb18396b8ecc339a..2cc50fb1644476ee5d4403de541251881a833f5e 100644 GIT binary patch delta 45512 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 D%WSwC delta 45817 zcmeIbcYIXE7dC$HZWeNZKtfAGfY6JWKoY_RLbCMUMS4phKp+K3Xi^do5Kv0E;DEGH ztaL?!0#Xz)6d?#G9Yj%V2#AOx{GR8|Y!Wp3ZNK;ZynncQ^2|ANX6DS9Gv&_Sko4W9 zw(c#pFt~QwZlCW5agZ9OHFDG7~*Y)h$;OgT)-M8w@v|+(LFMn0(vWLb`nT(!+ zwx-20CJbtNuaKr0nwCB;CLumHIaM1IH+Xo=nADVam3&NG%-~cM+2E;Z#h}+6%G6&A zo_NFb*8}x#WFM5@hX2#|LqKC2`e+(-)2&Vf#iy(U?*+ap zuq<$PNlhyX9GsGpI&oB-_6&F$(6W@K;s10$=rP}2J|THXOv-Rg+m8BK&)|fV3H0Z2 zILlv4)6(BSf(8tUNsS8{nv^mXJPj$1)>v^2_zJ+S7zb7`G9fuB1{DZXzY2ZO1 zT{|A7IW$QMH$%<_l2twZfpAKCrHT@Z0O`4%K-S|CC=E`HiH%K*iA{)$Phk{kT5I%# z6+3}c9A8ORa23cQIRm6?Iao&!As^ul@wJrJ zEmddG6Vmu}bB#w}+JriiON>v93mOuuC4y%gqf<2N6Z52O`zVkm?*-D-;V~)6BEvo4Pi??kx?2MU%^cJ_Pctr zf@@}s!#C}8h?EXaP97PL&W(#1Gm)iwRFX0GQ>dn4G}F%m5k~2she?m;DEt6Ovv&fE z0k5JQTR8-MwwgGG4$!m<;c|qJ0y$DU8d&wDrDs9FSnK7K1y+M+Ka2=j;nP4?G&VIp z0d{F68%hIa06Fc4CXd0oF?hI7BjqQ^8H1a_BV5xxz|+4sQ4ia>rs{nG?Ud4*Yv~iM zL|zQ-g5~VV*rXxxgX1-=J$l3ecrHrH-2>{5!71iDh3cjqaY?Us1hTpKl%Qb=$+6n( zCej}+B`?`jnqMD0YmFTfGdM0q(=J0!zg2H0%Z~?gm}8PACdQ-=9-f#yW>`!Ty03z+ zW^<`uQ_i5Y^t%wyu{VG;-xC>uz;9c~ft%h^`tbsgi^463oXgc)Yg!HPZv)ZObT6PE za9SHpL&T&{g&zHRua(5}ZKZ+l1F5&cLac6^puol9MF{BVFTs}tegI@eciST>fiHnC z1-yugIk4qBN`ve`fAI5x9JoWO;p?4b{$Yh%fLO26R|DDM#lWIzNNhv_W-JEMvSC0P z)D1|3q7+sFa+w|3O&U-HJbQQ>%PGr!0;K2m04aBLmpxt#o)Py9kOn3IivxY0M*Zy3 zVkFq$b3o3C-sXKzXIhb7lGA`3;iQ7OXUUWFThGTmtO&&i0dM}lj}#aKfa^l%iA9hfu%Q-P6@hy+J!FpvtpN6JaqM&U0&hEhAM zFf;&%oCJup5{c54?($fhgN7!=4D*1zDD;AoWW$qzthX2Hr@T3kLzR`RX)PHtn~^Yp z>w#2!7H%VE18HgMnEDOSp0-@+WsH`gb{$B==b596`qoZEF1aKicfDCyH&{!YD%As6 z0(?7jTT$P%g{g9wCj%KTA@B+phMVJLn4bdDds#qE2La@$Oajuou|R)dOO^iwkow-h za=`mzW!(M%WT&gE`sYrv;y5imE@f~`Ld=l3DG+dY;(;`zKahr$K>>Q*q42(4%8yTx z-Mayv`R9S`c2v5iRR?|rWH&DY+5J;MI(N@BiKAjthsSB=hsCNG6_Le=*=^n`7MNCJ zhV=Husj{KWnUbFg41iVz#0o3s!X1;4qWMCf{9Wj?1HF+?gGQvFYnrC*d`^03%xqb{ zFOc(p4dld>IWi0k)PoMBrGN9RG-vi)Ss(#OS7$?B8F=G)*@KBO2?_BjsoD|noFMN2 z87AAI$GD0GatLhb9?OrIuW2=b?SbsB6G%^2RakE!hrFtmUJ(gaR7|0}K|6T5@QxW$ zJg~_11+u^8=9_(rS2PZpa)5W6(~A41?OrUo|4Yl)A6Kum^#67Fio?SU22WeDTt>mR zp0Y!gR!IJ9j53{j0?6$kYNe*t0tNu9@EAXHm8OM4&=>dw(2*%e;jPu0_9Xbrz#70P zRzKnJ{Ah8z-hC%o% zG9GIH8K-YSUJMw50_5GJTO0D?;8Wrf$Hk3-ameS49b?9L`!pnSE;`rh5gNMW0ze3L1 zGB(TD;v@|k88`72l%p+)@k#NcVp3AvE8t4V8IezHmGxEtat@Z=CgnH47vY@R0Ra-} zJ0K_qZ2hL3e2syu_^;OLKUDOmdNQ|5r~R>>Nf;gOp1a?sA$4$mjFH0@j|}>4Y*PHt z__!gOdsrXrkcQnc@B0L%4c;jSwFi*>iH(uR+Lh1>1pn1La_~eB7sA(g1OMlt-$Z|h}XFHvM<>{|h@5yEUV<62f0}KH+ zf&!y%k}|g@c=ppD$lz&*?r{KL-!J`Y0%<^XApJiTNP|xukp6G_zAQHY@=B2Z2EH_> zj|&3!v@Xya=m(^QoexSsMXH9LR9FH?14e!*N1lsIm_KZq;cR=hmT1fJ8XF9 zgw(jCA@J0}L(<@ck7W5?z|vHVPl*{bCT616!CX_SX4;)>SEERv%USMoT>57^c={~~2!B^e z?~4Q-{S^wZqORcCP$BR%H_(hP?Mz#mBhN?+fHbq@DY;7R|3p^z3y`bUr@+d`E5RiWC45UHLuE}_)51xh$9-KTj2{Wk+8eoTR!*J^F z{X!bP7d!{@GO!e4E&V7Hz+Ts7MVo-+n*qxLgMpku@rxf-dMg!jf0C}e*Xv3uGT;f(Zk1Ka}N$ro@elOG-^S1fD)$AD;2q^#kkQs_9?b&+~Yv%Q>q? ze%iw=uZ2fadh?%sCBQ#zwd_ z0!8d*&eaIhaJp>eikj`5(RyEVj?<+tF|(a6;}|^Dz*6dZIzlgIwu^{1k}x(ctd@P8 zPRE<%%wr8Ajh~T{6=v*>@IVkWGQFIUj^Ri(uoPAy<&u>}IE{9mzAZnkR_ z?HGpfk76->y_wy}rC&3RNS7XEwvBWdlQ9aiK-UPT{*IZAoL@|%u?voGi=PY4IgMS$ zXBfjKmUTG|ocbWsh;r#~m~HX%3v*7C%cy}7>2BqBZR|8maM9o#W=^Ejkpr%Q*)cNG zR=AWoH#%DHWoAdaj90NTNTrO=BFwffmtN7FgP+~aY?sS;5$i-fRADnSqMZ7NX4@t% zqf;48YXg~K<}`BZFPhm+T>8hR(bQ!tU)F5bG};)12#vK0(aK|HHnPh4nnjvLJKA6g zZ$%^Y`R1HvF2`5k8=J?PM%t?SnTMN3>u;NFo4fQM%{k3o#vsH}W7MP5eq$%NK^Et; zFhVb9wrdd$56o%da@;M4E|^{|BJ}~L(b8os43J}}n;1j=lsN~o^5r$Ho3sgarGsO9 z+03p^r}3`hbhB&!2y;#=m)_3I#?NfiXzkKN&9<#w#^{Q;47N(p5g(b^tzEVa=7TV=$;4T*eJZILx}4fgabaF3m%aBb<&Ha1o|g<4Ap(*|ww0I1HXqCjDNd zhAe3_b6Po#HsCl9P*0T8kC??@{M>6A-Cf4d$Y-sZ*%kTq%{h=I!Xo8|W={Q_X*}&R0)yP6 z!DyQfj`qQ9_b-_@>+4MI&h_3fEjb51Z5O89ln=Yq**PH`c-4NNJ#xQckgKJ<-snurN z-Y)&JIj6VF2ndy?L#K(;(HUGl>NwJnYRnKeN`~P^-rcriIJhp9JMJLW&h%;-$>pxE z%a{TWL_*K1$=C^wPJ>I}NGyB(TySUGelFvOaQ9%sdPfz+N(XDTO+u;-O4vESj6>i! zJ+ROr-h7;Ls#??ENC4Nx()7WSxEowYi_3_Q&?C%t1ETeKbIt&l{-&9YpH)p`pvy2D zVwHlH^ygV{3<}L;4dpSduzbnlg3Bj`pI$#y=vy1K`zvuJ;-J3MrUPM zaN0Zo*WB7aTo?u`WlTqkJ(82|FgWU?%@$5uktnlWY_#FR1|ggF>F3lgmn?xIZv2n?;M-OeL%MzY~kV_Z_ z1zjr+Yg**i(hVGZj{TXd$s%xU&)Ou6kCi64Q4yX^<;YkgYb1ipGjjttP6WGE$qjI< zq%aLL!kWqE1(^TzPGd7G{z7 zXh$dXpsDEJl5xsq>_w#H$omY4@C4H%SE&=^9juw!9vi_3x7;`l zZh)J!g|#scC%8N@l7lQ+#*Y!6ZQYAEd)6VB>-v6#2bf0Yyituk5x$+=l^CNRgw(`-If53(*x@?G`A}b6ZD-3Pguy+PljI_ICf2P2f@)~C@zoie9B!vYwQm$ zF9hBJhoz-yq@xrJvW_H<&PYX5rq46mrn>ag=A2ZQqbZJ}t;~6;k&X>W^)S79M%wfq zX1lS`Mk9EU9gtBgz;RX~zR}!qa1Fp&Gum+Ul#>z@2%D-C+>^+Yn@|$CdR8r6G0kxZ z8SgTD(Lp)RIp}_GaEx)dbZ3N_J>I2%ZWMtGQ+sk# zc0_CoORsfq>iOK%@!V9Y80vL=rm<;Ebvfn_*0jE+*VIVI4WxQosb)hcn>Veo2NF+N zq92j!Z+cB{>=CDF@s?-@QZZJ_cc_(#I#Q7uX35SY6=$WI4AZoMR%&%_>LF674kh{y zcjvx~lx)9LJif!Waz`K)X&&nuX>3EPu{D+%?VQFvaF}^q$8gwqZUj~%aAmCc_y}AJ za5zN} zQ8m#$k-N@~@C3u!u|aIH7#WycPH!R$QH~>dBh#4WGG-;YgA2zh;{dp56vUGlI4#*d zoqVS9alm-aWq6H}i?gS-IJX9eqd?0@V;)mhlWgTYIJtG#94*6JvzGmy;JCCq%&v`a zzyl{IFHYi~U}&@4g(JpD4u&*!8gs#MHhEaf!9H;8kUVexqBKzsRW?qMCKonyx;l-8 z;AoV!%Q%jLYh)e^k2DIU%5K8N!<|M8aI6pB@9s3#fvaz|#RKrCDo<{UCC17b4-O}< z{@}Vm6D?!^-(=2t!R07Cp7Am7g-FLBqy}24y+{qVQdK4}E}8p0QZeSd`Hel0Ko~>R zdZJtH4WwjI;~9n^bB7|;(@Nza6>ZKt+E|}t&Rq~~WKPOkRUwG(M&r9Oi9v zr=#j~3~^}b1I@NeUB)@^p~$r!8f?X8n}?T18}nwncTR@uWpJFq@=2n$IcJ&6m_0|j zNgnt<2FH0OnCPlsi&XUv?0tjZX-n}TBkYV$rq%naVQ=W z;Q?lV=_Mi^`;fwhgf`Rj1EID))Tqkg>-_o?rT5J|^MjMBe3f2PL-8hz*hu20s8ZY72 zdTec^V=huy$&fmeo2v4nTb7iYdKam7mY#>;M#&N#a#M?QQx|em)t0*T5^_`9b5jp; zQ!SQpKrLHxQzvp$WnXgV_CpG{S7g95mz_kJAVLctDte3}PXkv9b4X!)#7yuC-FI$UAWTfL1Qr)Zv&6cld zT3cCFs?(Sct`#`MG+c8ETodz{Gtwyis+1SvR<8Fkv$yb$(b$T`0Xg+i0vk?mJVm`G zBNNBIMiHK1xF{eLaS`wpxKK9f@PC~tK8>_TiW=Bjp>Z5sAIqrPZ^(Iuk~l|<1=ku{ zMXX738XUbW&!BZT$gz}u83vA?uVwf-$oSb7ko$pp$VbiBagy&}W>c~E{2Z#GV^lcSV%}|}R z>oOM>V`E4=V?T9u;Z;?XOgEn3V*9a;&AYmiFrZ`;iu5ucUw#v%T18mrn!BG?2 z(+d%v+a!ai!-RhZTw`S6t`1AcKE>fww9Qgv2q8?CIE?{s%BLG?-ga>8pLNDDZYvI} z7?#m`Z_$lj=Nfw;fum2aNMkcn(twP$5uOyIEP4UoRNJ24fI;BcrhIPS46X$@>k7uW z4UTOZrcY0&QSWW{x|M^=t|V}s%#IO}#zCaml(n-MkH9hTt@t$R?vP<31AjL-C-M*` z@TO;$Y}q21umdtAnJ$XFLPGI)45xcZWV-9IW0{=l@Uwo?u&f^0l)Q^CbTQ$EOl z4o(iNPcLY`lWP&mbq6;TnwY*jB0Rx#kqjDW|E_z(q?=ZP>t*HTfV&GW6r4QY)!Zcw z!}{FQY4ioxU20-P59V^*$)4Hm?t%{vubaU!`q5=LAYhMd0W%+YDd1={It1=*aD%Km zY0J>}G;O%WvFlgBagUQHz9;u;+FZA$V+Xh{To7!9_L+0vk2ad@lgC4NW_W}L7(D4< zVx2^a%ZS{Bw(plii;0D{Z-Zlh5L}4YRtIFbAV9F@P6vl&2{n9+R1d2L_NDXtGH_4> zEHuHn?{kw zRi+@vR*f@R%@1X(R?S8#I0lb3Vf4-BoR3`kbu;@Tmr?tWtPR%Tv1A^&2>CTtwwU)B+rBg-sZ%8M%2GShC&+C#M5u zyF)t+&N}VaLyDno`AZ*S8b@8mE09V1eHJ^7@4)5N`s9)PBQyeB16dj)lnIX0TUv7g z96f~C>}6fXAH$jSsMJT#uv|nRvqk`W)_SCxLszcb9v{n_$zs-m`WCnWR;?H{qsH<4 zb7KNHMu=QAj)KecP5Bct;1J_CaPfb_|t~2lz)+MBx%J%j~c;>h#7*Arm%(glB zjsUWHR--;}y8lV{0h}hZ2FEFg02$ykCW50TrVE1Vb#V2q9pfTWNwNmW+kXO=TH*TS zseE@W#%yrxfVGJm7xL#hB2MFO%{-RS*b@mxIFxV?1*T7KiJ#B7rzb7z@oE0sxP9OT z%YRMB_3zxiTU49C26BqBXPj zBSkqZ!TJ(?Rdz~lY)irQLY{2-M{t~Va>JPLdH%L`gKKPQa_9U79D5Br`bBtNb5C7n z32+QuY2saQ_1&DK_7`Z}oQK`917aR`8B@vmf%$#psw96f~<7yH7SijyYVzIGdoa*jdZx-*y^ z+mXWE*zv}mH{26G1F`P{*9}#{gq}`4!<=)~WgLQxgM>>4ctHQgy~O#T0}a734~<1S z@%_=9&t1m5kTE#q)GzX_?7lRo9k>YOSc&GDj5h@z(s?TnzetBoVAQOhJnL#1cG{l zlFOa`1~`r_I5@n*&D<3N_dNr^;rb67&2FTc<<|TV9NWRLjF0gA(S5+=97zNhj4YhZ znp;^?k3Kw$EcOaM$7bboOLEqs-4hIrz&e5X_Z&Ive7Q@>cs zvozM4q>fJHEUo!S(PKCfV=*`ij`K?%bUp9o#vtOe8Mp?>lN-ncaLTvn?pAO#2%X;@ z!D21IAgqy9;}@xDg@|JSxM1^Gt4QNGQeB{BJue%9_hlLB%faC2S&RZ2UI$JF89jRy zoLU0*M0otl!C<^3BSq`1hZN&waJ0@EUgIJ-S|_;@zsZ0>9N?XaDATy(TomeQOozA^ zlCP?w-p$ zfGq*X_R%>+-Z5~p1I#P>(4B|)w{?7I9=;oGn`?n_M&+U7EuFUFkIcD0&+T1Tv#Q2V zi8?S6R0Px#WCvk_YLvHCh^cLTh?KVn6$kYO@k7+jDffK+ncI&de25NE3`jSR-SZ7W zLL1B<_!NXJJ;chDsGDBD_@+@XR7vwAwTCG=k!>Y_*n?ycKl!l`_^}`xXgr7?BI}tz z2A=}ZYQLZitDkNIo4z!=454lOH|6KL?^Avq1(Z1H?}T-saP& zumr@41c-`D6)po724#Y%w;se#eq_V1g6N@*AeP?*;)lp`+d!=UEfDo~g81PJ*_P$I zfK=E6Vg>Jk_$dfIA^#9W{f|KW#`Y%YoEi38em7g|7lRYHz6gjX-bkyOn&elJ94Har_S{ zK{k;5aUd7+i$H#etniA$Yd|*qC6En&qvYQMxk~-4`1?S9h@1cp6ynq?31s=weE*z1 zDi6*d7_Ks$3L66H>Lx&bh^(_IkQH?T(%|mEQovzA8k7K}ULvq4a00L_a6YgkFcV0F zHg-UQ1-1cMQ5KK|cPjZFAU{NUV4sp71XAxql~43AJ3jKY2K1(NK zJ{@v4FiYj%FmBnyxvf9B9oi& zgFV{}q#o%-^?e!PvHS2KLlh$9|8F(2wCp1$|rJF z{Zy6v42b;V+Bu~_EGGJU=)PjIhhE7F%&%2mK}h4iSNZvojowspB8~h};Vp%?RX&kR zwXFJaz!c$WS`0^zC4hQsR-nr zSObXvwI`JPNg&GyDGXNfdO-ZI)#nd|)C*TUP&$eN#$#iZL1eNCez4(Y3R_T!Pkv;6 zE67=KYn4wV-$wC78s0_8yDIGFA=}rwL4g0Y9{iz@74%f{zDiyY(t!RdKR?owgO!{} zy*R}a8AZd>kYGdc3P&h~{K$g**9iC_Qa=$$d9sod+2B|pE1m$P-b9s8q;$!nTjt6nKeh{=SPa>;RlDB|J(vTY3{_|B6XMI2Mt{YWY3q& z3M^!Dwc_(5_0~Yna%)vSksWv$=*jQ)HmM9EGx+cGu;80O7JLhc|FtZ|?<6C>_tbs6 zGJ6+Ts_&s#Ay1kgDxS#vkAT$AR{Rl#$AJ70Jw$&)?W&(BaY0Bc%@kb!Uk`E2%* z(j(IEUjk{ybtNwV(=0dKfC4M{Ru%Y86(F*L?|~HEQgR{<`$_Tnk@|O)oah6-5SC!d z3j>)|6v#PR%7Hn_ipp}0R!H7o@kACZr!YXtiPWnI zK!(yZ#m}LFm{%0bHp|Ti%S*7ODmxR%50S-R15)pGAhR|rd<#ei?FF*E{R-a)@{=D~ z&k-fhhwZF_$5ci^$gGq2K}Vbh(vUL>KV>F9#1dj&F|`eSrNsG>#lBW@BCGpW@kH`J zD4t0Dn~E<8sefDL6WRZt9Q;LpnaD*th$|{9)D_w|u|$xU?rTu&u-J}XYQ`F1SzS?z z|A1642061z;s^OsKc5VBk|RjxUZd`rc* zDul~>R@hoqkRPeoRw=dv(mS1j^wv{KpJ)?7CG;E z-~GAIKX=ZSQ+U_R50TC);I5h3l*@BeK??NlKX=am+&MpX2hG*BEJBJ=#k*#Hh@8Ry z+&TYq=gfHc=g#?`J7>nh|8n;%x21pXobz9L^Q2RR2gyGmMP5oyK?5S$|+xGtHlD_;bJ6+Fz%PdwyQzwdz}~OSOk39sg^$zLUhE0A`H3l^w)VOqvO{e>#e^`M zmk0`jps1J_20^X*5L~3dOVq9p!DkfAs}Dg5ah8JF;SfZHL*OIkgxh-RCB;=jDbcV2 zpmbJ71DmNktiKc^qEc;vuU2x}^n!i}WL#Jj>QUrgp#0YsZI!a3``FsqAj?W`V%u0$ zpDltq*gETj#S}o9CB5Xo0m&hE|FhJeW?k)IyR7Ryv-WqgrRYW!j*fWCEB*Vds4lhu zn`81Al*ww^#kNaFwPoD@i1{8|M3;sdtEw(Sy4y-QtbfnkvY3WtLR%Dj+UDu7Rf2T_ zHLRm(A4LnN<2wxz(!thTc=xb*mT5V|dMU-KjsIw8IBK0Gf_m6CIEp`q?{%meh|(|i zMByArjI5@8Y+>xEfxkLq{VU^jM91E?Ft2j+t*=Yc&n-fg?pBS*dizN}Z_?CWw2O8) zEIUNk-nP0%&TH0Jg6ZeQwgI-AdOxvdpslZ$TWV~^7nQUj&~o;Zv9kq}YMB8{r3f^umK?|OuneKmkbbr>oB-=V&Z<`e|%2r3u>y1^D7?oo4w0S-d(Oqn> zpjcV=KbSc~K4w3BBtI0N!I<%)xP$tl#?5mm%TNKIPn8ZUa(~go7aUoR&lc@v3))#F_EWA(9+S¨TU{DwD6j9su!kU&;737{11|KEFZ6_V|_b10{P1 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 diff --git a/core/api/oas_json_gen.go b/core/api/oas_json_gen.go index 0c636f68..f478c3ea 100644 --- a/core/api/oas_json_gen.go +++ b/core/api/oas_json_gen.go @@ -3538,12 +3538,17 @@ func (s *StatsPropertiesItem) encodeFields(e *jx.Encoder) { e.FieldStart("events") e.Int(s.Events) } + { + e.FieldStart("events_percentage") + e.Float32(s.EventsPercentage) + } } -var jsonFieldsNameOfStatsPropertiesItem = [3]string{ +var jsonFieldsNameOfStatsPropertiesItem = [4]string{ 0: "name", 1: "value", 2: "events", + 3: "events_percentage", } // Decode decodes StatsPropertiesItem from json. @@ -3587,6 +3592,18 @@ func (s *StatsPropertiesItem) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "decode field \"events\"") } + case "events_percentage": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + v, err := d.Float32() + s.EventsPercentage = float32(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"events_percentage\"") + } default: return d.Skip() } @@ -3597,7 +3614,7 @@ func (s *StatsPropertiesItem) Decode(d *jx.Decoder) error { // Validate required fields. var failures []validate.FieldError for i, mask := range [1]uint8{ - 0b00000100, + 0b00001100, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. diff --git a/core/api/oas_schemas_gen.go b/core/api/oas_schemas_gen.go index d4d5f3e2..b2955030 100644 --- a/core/api/oas_schemas_gen.go +++ b/core/api/oas_schemas_gen.go @@ -1488,7 +1488,7 @@ type StatsBrowsersItem struct { Browser string `json:"browser"` // Number of unique visitors from browser. Visitors int `json:"visitors"` - // Percentage of unique visitors from browser. + // Percentage of unique visitors from browser relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Bounce rate percentage from browser. BouncePercentage OptFloat32 `json:"bounce_percentage"` @@ -1555,7 +1555,7 @@ type StatsCountriesItem struct { Country string `json:"country"` // Number of unique visitors from country. Visitors int `json:"visitors"` - // Percentage of unique visitors from country. + // Percentage of unique visitors from country relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Bounce rate percentage from country. BouncePercentage OptFloat32 `json:"bounce_percentage"` @@ -1622,7 +1622,7 @@ type StatsDevicesItem struct { Device string `json:"device"` // Number of unique visitors from device. Visitors int `json:"visitors"` - // Percentage of unique visitors from device. + // Percentage of unique visitors from device relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Bounce rate percentage from device. BouncePercentage OptFloat32 `json:"bounce_percentage"` @@ -1689,7 +1689,7 @@ type StatsLanguagesItem struct { Language string `json:"language"` // Number of unique visitors for language. Visitors int `json:"visitors"` - // Percentage of unique visitors for language. + // Percentage of unique visitors for language relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Bounce rate percentage for language. BouncePercentage OptFloat32 `json:"bounce_percentage"` @@ -1756,7 +1756,7 @@ type StatsOSItem struct { Os string `json:"os"` // Number of unique visitors from OS. Visitors int `json:"visitors"` - // Percentage of unique visitors from OS. + // Percentage of unique visitors from OS relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Bounce rate percentage from OS. BouncePercentage OptFloat32 `json:"bounce_percentage"` @@ -1823,11 +1823,11 @@ type StatsPagesItem struct { Path string `json:"path"` // Number of unique visitors for given page. Visitors int `json:"visitors"` - // Percentage of unique visitors for given page. + // Percentage of unique visitors for given page relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Number of page views. Pageviews OptInt `json:"pageviews"` - // Percentage of page views. + // Percentage of page views relative to all pages. PageviewsPercentage OptFloat32 `json:"pageviews_percentage"` // Bounce rate percentage. BouncePercentage OptFloat32 `json:"bounce_percentage"` @@ -1916,6 +1916,8 @@ type StatsPropertiesItem struct { Value OptString `json:"value"` // Number of events for custom property. Events int `json:"events"` + // Percentage of events for custom property relative to all events. + EventsPercentage float32 `json:"events_percentage"` } // GetName returns the value of Name. @@ -1933,6 +1935,11 @@ func (s *StatsPropertiesItem) GetEvents() int { return s.Events } +// GetEventsPercentage returns the value of EventsPercentage. +func (s *StatsPropertiesItem) GetEventsPercentage() float32 { + return s.EventsPercentage +} + // SetName sets the value of Name. func (s *StatsPropertiesItem) SetName(val OptString) { s.Name = val @@ -1948,6 +1955,11 @@ func (s *StatsPropertiesItem) SetEvents(val int) { s.Events = val } +// SetEventsPercentage sets the value of EventsPercentage. +func (s *StatsPropertiesItem) SetEventsPercentage(val float32) { + s.EventsPercentage = val +} + type StatsReferrers []StatsReferrersItem func (*StatsReferrers) getWebsiteIDReferrersRes() {} @@ -1957,7 +1969,7 @@ type StatsReferrersItem struct { Referrer string `json:"referrer"` // Number of unique visitors from referrer. Visitors int `json:"visitors"` - // Percentage of unique visitors from referrer. + // Percentage of unique visitors from referrer relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Bounce rate percentage from referrer. BouncePercentage OptFloat32 `json:"bounce_percentage"` @@ -2221,7 +2233,7 @@ type StatsTimeItem struct { DurationUpperQuartile OptInt `json:"duration_upper_quartile"` // Total time spent on page in milliseconds for the lower quartile (25%). DurationLowerQuartile OptInt `json:"duration_lower_quartile"` - // Percentage of time contributing to the total time spent on the website. + // Percentage of time contributing to the total time spent on the website relative to all pages. DurationPercentage float32 `json:"duration_percentage"` } @@ -2294,7 +2306,7 @@ type StatsUTMCampaignsItem struct { Campaign string `json:"campaign"` // Number of unique visitors from UTM campaign. Visitors int `json:"visitors"` - // Percentage of unique visitors from UTM campaign. + // Percentage of unique visitors from UTM campaign relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Bounce rate percentage from UTM campaign. BouncePercentage OptFloat32 `json:"bounce_percentage"` @@ -2361,7 +2373,7 @@ type StatsUTMMediumsItem struct { Medium string `json:"medium"` // Number of unique visitors from UTM medium. Visitors int `json:"visitors"` - // Percentage of unique visitors from UTM medium. + // Percentage of unique visitors from UTM medium relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Bounce rate percentage from UTM medium. BouncePercentage OptFloat32 `json:"bounce_percentage"` @@ -2428,7 +2440,7 @@ type StatsUTMSourcesItem struct { Source string `json:"source"` // Number of unique visitors from UTM source. Visitors int `json:"visitors"` - // Percentage of unique visitors from UTM source. + // Percentage of unique visitors from UTM source relative to all visitors. VisitorsPercentage float32 `json:"visitors_percentage"` // Bounce rate percentage from UTM source. BouncePercentage OptFloat32 `json:"bounce_percentage"` diff --git a/core/api/oas_validators_gen.go b/core/api/oas_validators_gen.go index e2953efb..cf053d17 100644 --- a/core/api/oas_validators_gen.go +++ b/core/api/oas_validators_gen.go @@ -569,6 +569,46 @@ func (s StatsProperties) Validate() error { if alias == nil { return errors.New("nil is invalid value") } + var failures []validate.FieldError + for i, elem := range alias { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *StatsPropertiesItem) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := (validate.Float{}).Validate(float64(s.EventsPercentage)); err != nil { + return errors.Wrap(err, "float") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "events_percentage", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } return nil } diff --git a/core/db/duckdb/properties.go b/core/db/duckdb/properties.go index 7d5f0f2c..836c803e 100644 --- a/core/db/duckdb/properties.go +++ b/core/db/duckdb/properties.go @@ -9,6 +9,11 @@ import ( "github.com/medama-io/medama/model" ) +const ( + EventsCountStmt = "COUNT(*) AS events" + EventsPercentageStmt = "ifnull(ROUND(COUNT(*) / (SELECT total_events FROM total), 4), 0) AS events_percentage" +) + // GetWebsiteReferrersSummary returns a summary of the referrers for the given filters. func (c *Client) GetWebsiteCustomProperties(ctx context.Context, filter *db.Filters) ([]*model.StatsCustomProperties, error) { var properties []*model.StatsCustomProperties @@ -21,8 +26,13 @@ func (c *Client) GetWebsiteCustomProperties(ctx context.Context, filter *db.Filt // // Events is the number of events for the custom property // - // Visitors is the number of unique visitors for the custom property. - query := qb.New() + // Events percentage is the percentage of events for the custom property + query := qb.New().WithMaterialized( + qb.NewCTE("total", qb.New(). + Select("COUNT(*) FILTER (WHERE name IS NOT NULL) AS total_events"). + From("views"). + LeftJoin(EventsJoinStmt). + Where(filter.WhereString()))) // If the property name is empty, return only the property names with their // aggregated events and visitors. No values. @@ -30,7 +40,8 @@ func (c *Client) GetWebsiteCustomProperties(ctx context.Context, filter *db.Filt query = query.Select( "name", "'' AS value", - "COUNT(*) AS events", + EventsCountStmt, + EventsPercentageStmt, ). From("views"). LeftJoin(EventsJoinStmt). @@ -44,7 +55,8 @@ func (c *Client) GetWebsiteCustomProperties(ctx context.Context, filter *db.Filt query = query.Select( "'' AS name", "value", - "COUNT(*) AS events", + EventsCountStmt, + EventsPercentageStmt, ). From("views"). LeftJoin(EventsJoinStmt). diff --git a/core/db/duckdb/query/qb.go b/core/db/duckdb/query/qb.go index d726a447..9cf52fca 100644 --- a/core/db/duckdb/query/qb.go +++ b/core/db/duckdb/query/qb.go @@ -13,6 +13,7 @@ type QueryBuilder struct { cteClauses []CTE selectClauses []string fromClause []string + joinClause string leftJoinClause string whereClause string groupByClause []string @@ -47,6 +48,11 @@ func (qb *QueryBuilder) From(tables ...string) *QueryBuilder { return qb } +func (qb *QueryBuilder) Join(query string) *QueryBuilder { + qb.joinClause = query + return qb +} + func (qb *QueryBuilder) LeftJoin(query string) *QueryBuilder { qb.leftJoinClause = query return qb @@ -100,6 +106,11 @@ func (qb *QueryBuilder) Build() string { query.WriteString(" FROM ") query.WriteString(strings.Join(qb.fromClause, ", ")) + if qb.joinClause != "" { + query.WriteString(" JOIN ") + query.WriteString(qb.joinClause) + } + if qb.leftJoinClause != "" { query.WriteString(" LEFT JOIN ") query.WriteString(qb.leftJoinClause) diff --git a/core/model/stats.go b/core/model/stats.go index b59775a4..0965bb58 100644 --- a/core/model/stats.go +++ b/core/model/stats.go @@ -168,7 +168,8 @@ type StatsLanguages struct { } type StatsCustomProperties struct { - Name string `db:"name"` - Value string `db:"value"` - Events int `db:"events"` + Name string `db:"name"` + Value string `db:"value"` + Events int `db:"events"` + EventsPercentage float32 `db:"events_percentage"` } diff --git a/core/openapi.yaml b/core/openapi.yaml index b70a3ea5..827b914f 100644 --- a/core/openapi.yaml +++ b/core/openapi.yaml @@ -1702,14 +1702,14 @@ components: description: Number of unique visitors for given page. visitors_percentage: type: number - description: Percentage of unique visitors for given page. + description: Percentage of unique visitors for given page relative to all visitors. format: float pageviews: type: integer description: Number of page views. pageviews_percentage: type: number - description: Percentage of page views. + description: Percentage of page views relative to all pages. format: float bounce_percentage: type: number @@ -1745,7 +1745,7 @@ components: description: Total time spent on page in milliseconds for the lower quartile (25%). duration_percentage: type: number - description: Percentage of time contributing to the total time spent on the website. + description: Percentage of time contributing to the total time spent on the website relative to all pages. format: float required: - path @@ -1765,7 +1765,7 @@ components: description: Number of unique visitors from referrer. visitors_percentage: type: number - description: Percentage of unique visitors from referrer. + description: Percentage of unique visitors from referrer relative to all visitors. format: float bounce_percentage: type: number @@ -1792,7 +1792,7 @@ components: description: Number of unique visitors from UTM source. visitors_percentage: type: number - description: Percentage of unique visitors from UTM source. + description: Percentage of unique visitors from UTM source relative to all visitors. format: float bounce_percentage: type: number @@ -1819,7 +1819,7 @@ components: description: Number of unique visitors from UTM medium. visitors_percentage: type: number - description: Percentage of unique visitors from UTM medium. + description: Percentage of unique visitors from UTM medium relative to all visitors. format: float bounce_percentage: type: number @@ -1846,7 +1846,7 @@ components: description: Number of unique visitors from UTM campaign. visitors_percentage: type: number - description: Percentage of unique visitors from UTM campaign. + description: Percentage of unique visitors from UTM campaign relative to all visitors. format: float bounce_percentage: type: number @@ -1873,7 +1873,7 @@ components: description: Number of unique visitors from browser. visitors_percentage: type: number - description: Percentage of unique visitors from browser. + description: Percentage of unique visitors from browser relative to all visitors. format: float bounce_percentage: type: number @@ -1900,7 +1900,7 @@ components: description: Number of unique visitors from OS. visitors_percentage: type: number - description: Percentage of unique visitors from OS. + description: Percentage of unique visitors from OS relative to all visitors. format: float bounce_percentage: type: number @@ -1927,7 +1927,7 @@ components: description: Number of unique visitors from device. visitors_percentage: type: number - description: Percentage of unique visitors from device. + description: Percentage of unique visitors from device relative to all visitors. format: float bounce_percentage: type: number @@ -1954,7 +1954,7 @@ components: description: Number of unique visitors from country. visitors_percentage: type: number - description: Percentage of unique visitors from country. + description: Percentage of unique visitors from country relative to all visitors. format: float bounce_percentage: type: number @@ -1983,7 +1983,7 @@ components: description: Number of unique visitors for language. visitors_percentage: type: number - description: Percentage of unique visitors for language. + description: Percentage of unique visitors for language relative to all visitors. format: float bounce_percentage: type: number @@ -2012,5 +2012,10 @@ components: events: type: integer description: Number of events for custom property. + events_percentage: + type: number + description: Percentage of events for custom property relative to all events. + format: float required: - events + - events_percentage diff --git a/core/services/properties.go b/core/services/properties.go index 87fa0182..04b30190 100644 --- a/core/services/properties.go +++ b/core/services/properties.go @@ -33,7 +33,8 @@ func (h *Handler) GetWebsiteIDProperties(ctx context.Context, params api.GetWebs resp := make(api.StatsProperties, 0, len(properties)) for _, p := range properties { item := api.StatsPropertiesItem{ - Events: p.Events, + Events: p.Events, + EventsPercentage: p.EventsPercentage, } if p.Name != "" { diff --git a/dashboard/app/api/types.d.ts b/dashboard/app/api/types.d.ts index 42f6063c..0ac4ecf3 100644 --- a/dashboard/app/api/types.d.ts +++ b/dashboard/app/api/types.d.ts @@ -662,14 +662,14 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors for given page. + * @description Percentage of unique visitors for given page relative to all visitors. */ visitors_percentage: number; /** @description Number of page views. */ pageviews?: number; /** * Format: float - * @description Percentage of page views. + * @description Percentage of page views relative to all pages. */ pageviews_percentage?: number; /** @@ -694,7 +694,7 @@ export interface components { duration_lower_quartile?: number; /** * Format: float - * @description Percentage of time contributing to the total time spent on the website. + * @description Percentage of time contributing to the total time spent on the website relative to all pages. */ duration_percentage: number; }[]; @@ -706,7 +706,7 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors from referrer. + * @description Percentage of unique visitors from referrer relative to all visitors. */ visitors_percentage: number; /** @@ -725,7 +725,7 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors from UTM source. + * @description Percentage of unique visitors from UTM source relative to all visitors. */ visitors_percentage: number; /** @@ -744,7 +744,7 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors from UTM medium. + * @description Percentage of unique visitors from UTM medium relative to all visitors. */ visitors_percentage: number; /** @@ -763,7 +763,7 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors from UTM campaign. + * @description Percentage of unique visitors from UTM campaign relative to all visitors. */ visitors_percentage: number; /** @@ -782,7 +782,7 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors from browser. + * @description Percentage of unique visitors from browser relative to all visitors. */ visitors_percentage: number; /** @@ -801,7 +801,7 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors from OS. + * @description Percentage of unique visitors from OS relative to all visitors. */ visitors_percentage: number; /** @@ -820,7 +820,7 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors from device. + * @description Percentage of unique visitors from device relative to all visitors. */ visitors_percentage: number; /** @@ -839,7 +839,7 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors from country. + * @description Percentage of unique visitors from country relative to all visitors. */ visitors_percentage: number; /** @@ -864,7 +864,7 @@ export interface components { visitors: number; /** * Format: float - * @description Percentage of unique visitors for language. + * @description Percentage of unique visitors for language relative to all visitors. */ visitors_percentage: number; /** @@ -886,6 +886,11 @@ export interface components { value?: string; /** @description Number of events for custom property. */ events: number; + /** + * Format: float + * @description Percentage of events for custom property relative to all events. + */ + events_percentage: number; }[]; }; responses: { diff --git a/dashboard/app/components/DropdownSelect.module.css b/dashboard/app/components/DropdownSelect.module.css index 45580448..61cf2588 100644 --- a/dashboard/app/components/DropdownSelect.module.css +++ b/dashboard/app/components/DropdownSelect.module.css @@ -20,7 +20,7 @@ } &:focus-visible { - outline: 2px solid var(--mantine-color-blue-outline); + outline: 2px solid var(--focus-outline); } &:where([data-left="true"]) { diff --git a/dashboard/app/components/layout/Error.module.css b/dashboard/app/components/layout/Error.module.css index 826b079d..a9cb86f4 100644 --- a/dashboard/app/components/layout/Error.module.css +++ b/dashboard/app/components/layout/Error.module.css @@ -5,29 +5,31 @@ .label { text-align: center; font-weight: 900; - font-size: rem(38px); + font-size: 38px; line-height: 1; - margin-bottom: var(--mantine-spacing-xs); + margin-bottom: 4px; color: var(--border-light); @media (--lt-sm) { - font-size: rem(32px); + font-size: 32px; } } .description { - max-width: rem(500px); + max-width: 500px; margin: auto; - margin-bottom: calc(1.5 * var(--mantine-spacing-xl)); + margin-bottom: 30px; } .title { text-align: center; font-weight: 900; - font-size: rem(38px); + font-size: 38px; @media (--lt-sm) { - font-size: rem(32px); + margin: 12px 0; + font-size: 28px; + line-height: 1.4; } } diff --git a/dashboard/app/components/layout/Error.tsx b/dashboard/app/components/layout/Error.tsx index 51d3385c..9102e1ff 100644 --- a/dashboard/app/components/layout/Error.tsx +++ b/dashboard/app/components/layout/Error.tsx @@ -1,6 +1,7 @@ import { Anchor, Container, Text } from '@mantine/core'; import { Link } from '@remix-run/react'; import type { ReactNode } from 'react'; + import classes from './Error.module.css'; interface ErrorPageProps { diff --git a/dashboard/app/components/stats/StatsItem.tsx b/dashboard/app/components/stats/StatsItem.tsx index 0f3ea8bf..f71e4e70 100644 --- a/dashboard/app/components/stats/StatsItem.tsx +++ b/dashboard/app/components/stats/StatsItem.tsx @@ -36,7 +36,7 @@ const FILTER_MAP: Record = { const StatsItem = ({ label, count = 0, - percentage = 0, + percentage, tab, bar = true, }: StatsItemProps) => { @@ -59,6 +59,12 @@ const StatsItem = ({ addFilter(filter, 'eq', label); }; + // If percentage is not defined, don't show the percentage bar + if (percentage === undefined) { + bar = false; + percentage = 0; + } + return (