From 968574dda19d2a042984aa59767632c2b6e74e09 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 25 May 2023 20:07:58 -0700 Subject: [PATCH 001/357] add ownerId to non-cache. --- lib/session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/session.ts b/lib/session.ts index 32f3bdc86..1fedb91b8 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -63,6 +63,7 @@ export async function findSession(req: NextApiRequestCollect) { subdivision1, subdivision2, city, + ownerId: website.userId, }; } From 7e587198dddac20fbb0744adb490797c15f1cafc Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 1 Jun 2023 12:49:28 -0700 Subject: [PATCH 002/357] Add incr on block. --- lib/cache.ts | 6 ++++++ lib/session.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/lib/cache.ts b/lib/cache.ts index e63a53bbd..7ee7bf287 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -54,6 +54,11 @@ async function fetchUserBlock(userId: string) { return redis.get(key); } +async function incrementUserBlock(userId: string) { + const key = `user:block:${userId}`; + return redis.incr(key); +} + export default { fetchWebsite, storeWebsite, @@ -65,5 +70,6 @@ export default { storeSession, deleteSession, fetchUserBlock, + incrementUserBlock, enabled: redis.enabled, }; diff --git a/lib/session.ts b/lib/session.ts index 1fedb91b8..7fa062153 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -99,6 +99,8 @@ export async function findSession(req: NextApiRequestCollect) { async function checkUserBlock(userId: string) { if (process.env.ENABLE_BLOCKER && (await cache.fetchUserBlock(userId))) { + await cache.incrementUserBlock(userId); + throw new Error('Usage Limit.'); } } From 499ba9634891e88fbca5e8b396c9a71781684107 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 26 Jul 2023 10:49:19 -0700 Subject: [PATCH 003/357] Update reset website logic. --- pages/api/websites/[id]/reset.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index dc98c5913..23b5305db 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -1,5 +1,5 @@ import { NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; +import { canUpdateWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -19,7 +19,7 @@ export default async ( const { id: websiteId } = req.query; if (req.method === 'POST') { - if (!(await canViewWebsite(req.auth, websiteId))) { + if (!(await canUpdateWebsite(req.auth, websiteId))) { return unauthorized(res); } From d9f1cbfc23220273df508675b8effccb5bea78d3 Mon Sep 17 00:00:00 2001 From: ImgBotApp Date: Wed, 2 Aug 2023 00:00:06 +0000 Subject: [PATCH 004/357] [ImgBot] Optimize images *Total -- 137.06kb -> 101.09kb (26.24%) /public/images/browsers/aol.png -- 3.08kb -> 0.41kb (86.68%) /public/images/os/ios.png -- 3.37kb -> 0.64kb (80.96%) /public/images/browsers/curl.png -- 4.11kb -> 1.17kb (71.6%) /public/images/os/os-2.png -- 4.15kb -> 1.20kb (71.23%) /public/images/browsers/ios.png -- 4.30kb -> 1.25kb (71.03%) /public/images/browsers/ios-webview.png -- 4.30kb -> 1.25kb (71.03%) /public/images/browsers/beaker.png -- 3.41kb -> 1.03kb (69.74%) /public/images/os/mac-os.png -- 4.62kb -> 1.43kb (69.07%) /public/images/os/windows-8-1.png -- 3.36kb -> 1.38kb (58.92%) /public/images/os/windows-8.png -- 3.36kb -> 1.38kb (58.92%) /public/images/os/blackberry-os.png -- 3.07kb -> 1.89kb (38.37%) /public/images/browsers/blackberry.png -- 2.06kb -> 1.40kb (31.85%) /public/images/browsers/searchbot.png -- 1.56kb -> 1.23kb (21.37%) /public/images/os/sun-os.png -- 2.66kb -> 2.24kb (15.79%) /public/images/os/amazon-os.png -- 2.33kb -> 1.98kb (15%) /public/images/os/windows-3-11.png -- 3.18kb -> 2.75kb (13.49%) /public/images/os/windows-98.png -- 3.18kb -> 2.75kb (13.49%) /public/images/os/windows-2000.png -- 3.18kb -> 2.75kb (13.49%) /public/images/os/windows-95.png -- 3.18kb -> 2.75kb (13.49%) /public/mstile-150x150.png -- 2.93kb -> 2.54kb (13.35%) /public/images/os/windows-server-2003.png -- 2.66kb -> 2.37kb (10.87%) /public/images/os/windows-me.png -- 2.66kb -> 2.37kb (10.87%) /public/images/os/windows-7.png -- 2.66kb -> 2.37kb (10.87%) /public/images/os/windows-vista.png -- 2.66kb -> 2.37kb (10.87%) /public/images/os/windows-xp.png -- 2.66kb -> 2.37kb (10.87%) /public/images/os/beos.png -- 2.86kb -> 2.57kb (10.24%) /public/images/os/open-bsd.png -- 3.33kb -> 3.03kb (8.99%) /public/images/browsers/android.png -- 1.61kb -> 1.47kb (8.26%) /public/images/os/linux.png -- 1.90kb -> 1.74kb (8.18%) /public/images/browsers/miui.png -- 1.77kb -> 1.65kb (6.89%) /public/images/os/android-os.png -- 1.86kb -> 1.76kb (5.67%) /public/images/browsers/edge.png -- 2.10kb -> 2.00kb (4.92%) /public/images/os/qnx.png -- 2.39kb -> 2.28kb (4.81%) /public/favicon-32x32.png -- 0.87kb -> 0.83kb (4.17%) /public/apple-touch-icon.png -- 2.03kb -> 1.95kb (4%) /public/safari-pinned-tab.svg -- 5.02kb -> 4.84kb (3.62%) /public/android-chrome-512x512.png -- 22.16kb -> 21.47kb (3.09%) /public/favicon-16x16.png -- 0.58kb -> 0.57kb (2.85%) /public/android-chrome-192x192.png -- 7.71kb -> 7.53kb (2.38%) /public/images/browsers/instagram.png -- 2.15kb -> 2.13kb (1.14%) Signed-off-by: ImgBotApp --- public/android-chrome-192x192.png | Bin 7895 -> 7707 bytes public/android-chrome-512x512.png | Bin 22690 -> 21989 bytes public/apple-touch-icon.png | Bin 2075 -> 1992 bytes public/favicon-16x16.png | Bin 597 -> 580 bytes public/favicon-32x32.png | Bin 888 -> 851 bytes public/images/browsers/android.png | Bin 1646 -> 1510 bytes public/images/browsers/aol.png | Bin 3154 -> 420 bytes public/images/browsers/beaker.png | Bin 3496 -> 1058 bytes public/images/browsers/blackberry.png | Bin 2110 -> 1438 bytes public/images/browsers/curl.png | Bin 4208 -> 1195 bytes public/images/browsers/edge.png | Bin 2154 -> 2048 bytes public/images/browsers/instagram.png | Bin 2202 -> 2177 bytes public/images/browsers/ios-webview.png | Bin 4405 -> 1276 bytes public/images/browsers/ios.png | Bin 4405 -> 1276 bytes public/images/browsers/miui.png | Bin 1813 -> 1688 bytes public/images/browsers/searchbot.png | Bin 1596 -> 1255 bytes public/images/os/amazon-os.png | Bin 2386 -> 2028 bytes public/images/os/android-os.png | Bin 1906 -> 1798 bytes public/images/os/beos.png | Bin 2930 -> 2630 bytes public/images/os/blackberry-os.png | Bin 3146 -> 1939 bytes public/images/os/ios.png | Bin 3450 -> 657 bytes public/images/os/linux.png | Bin 1943 -> 1784 bytes public/images/os/mac-os.png | Bin 4736 -> 1465 bytes public/images/os/open-bsd.png | Bin 3414 -> 3107 bytes public/images/os/os-2.png | Bin 4254 -> 1224 bytes public/images/os/qnx.png | Bin 2452 -> 2334 bytes public/images/os/sun-os.png | Bin 2723 -> 2293 bytes public/images/os/windows-2000.png | Bin 3261 -> 2821 bytes public/images/os/windows-3-11.png | Bin 3261 -> 2821 bytes public/images/os/windows-7.png | Bin 2723 -> 2427 bytes public/images/os/windows-8-1.png | Bin 3437 -> 1412 bytes public/images/os/windows-8.png | Bin 3437 -> 1412 bytes public/images/os/windows-95.png | Bin 3261 -> 2821 bytes public/images/os/windows-98.png | Bin 3261 -> 2821 bytes public/images/os/windows-me.png | Bin 2723 -> 2427 bytes public/images/os/windows-server-2003.png | Bin 2723 -> 2427 bytes public/images/os/windows-vista.png | Bin 2723 -> 2427 bytes public/images/os/windows-xp.png | Bin 2723 -> 2427 bytes public/mstile-150x150.png | Bin 3003 -> 2602 bytes public/safari-pinned-tab.svg | 76 +---------------------- 40 files changed, 1 insertion(+), 75 deletions(-) diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png index 36a3bb8c6456a01992cf90568574fb55cda04eb1..29d134eef86f26171befbbd80aadf4fa8c54f48b 100644 GIT binary patch delta 7638 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 delta 7810 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 U@p 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 literal 22690 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 Y&k4T3Eq%2NunOR+k%i$$ednkD19u^)=l}o! diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 16842d3c183298ee7d763d39b139da041c0c68b9..575bba77b63eef82217cebfb77039f01256014d5 100644 GIT binary patch delta 1897 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 H&ZqnX%MhI1 diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png index 9d08d9963a0571252bdf0cfcf0141f4532d3af70..549642460f3f8c1d7989938a389eb4e4c42cb0d9 100644 GIT binary patch delta 455 zcmcc0a)f0ty$tFnMYjYLWauvlV4d#A$|~;IQU7td_(Xvcow-5NpDpLBw0h#b zFX7U6p8LP&elB=m+f>l#eX4$Gg+#{%^#?PYd=6gyAJ0^??a8jMibq<#D^d>0e)oyo zq}}M#^}WCHYF}lN+cFN;H5DuL(;CCs<2Gz4+!6TH;J|}OX1+^7*QIvkvbIT8JYQn? z?$fG9mfegoD}A);kMd71S(V5>d4tzPk+j1z9nWkNdTZc#3XGWk722WQ%mvv4FO#nWQsXzb# delta 491 zcmX@Ya+PI5N&ORUPEl^H%<4ay3=9mvJY5_^G|rb!*z3idDByZtTtrbZfot*H^9o^H zUP&7^3b3qQqoZ5PeDGHJ2i+zCS5A&?W^D;tf`LawvaY$F*m%QN>`uk?dZtf z*7K+Pgm-q+>#{%mv9E8OdbLmXeQo)S!0?0mT?~T0RUsT-B2((Sg&2-FGS*onn=NIZ z^Z5{?F@wmYW6{fQ`Y!K!6Yxw;a0XLG(J7{Kp5RRfr2A_&@GhuQ5q=bKmFa-YL7=A diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png index 3f2e4c1f71c55068edac3e9703fc988ce1354517..ec5ecedae5d5af5b796a96cffa4fa6539c2004ea 100644 GIT binary patch delta 728 zcmeytcA0HLiNJFXMsaKP^Bqnz7#JAhN?a%NGfUKSWfqhqb18sZH7T8lz!U%fguW{{o4P ztk*i9yY+DHFL^CkfA2zrxV+sSn3Ii-CJvg}<(R^!1E!V8%eEh4sAwL$F*XK3GhRsZ#Cc5as?oFq_4>A@|Wl-%W>E)=pXS?cS_01Me6ltBm(s%NeRU3tT7fn=( z?U2@-w&=wnC9b+_3lhw)&Gr7v*!16Fmu75)>%^uggNRK*E&{I)`3v0DC{5&U3&@@& zqU@R5*S*AaX1SVYahma+%JY@)pT{t?+$srqJS))g>xLzfwioIPD_K*GZY@hZdwpq` z(ptgRR*j2~wbpgUIXMY7+DPR!#T;6(=f#XVGu;W-W`?!hXi~15oFaF_X9n*%ccz$4 zU*^tzHa%1BQxlI|LMI=e;fXw*&k3i382z{Ep0~}r%(GJR&KVPQo8Jb+h)(0hsp_DIjQc4nG)-rmN_*1 z<>me&WWt(oAop3$2jSIALj4Z1UOZ476uCXDZhdU&%iE@**zuw6POGyzGwDp^uJa3m zH+C)YoaC6J+at=#EmvZ~c|;%2Gf zkU#e?*v$nxoNZc8VDYRFpMOGscps|0DN$F5`bqh0TSI>M_$B1(c1 z%M}WW^3yVNQWZ)n3sMy_3rdn17%JvG{=~yk7^b0d%K!8k&!<5Q%*xz)$=t%q!rqfb zn1vNw8cYtSFe`5kQ8<0$%84Uqj>sHgKi%N5z)O$emAGKZCnwXX3=CWh44$rjF6*2U FngEDwN+bXP diff --git a/public/images/browsers/android.png b/public/images/browsers/android.png index 6e28498d2c1a737eb1cd4554a5e8ec631562d584..393001a017e08b8e378bc3ec8ef5acd6c404b2e4 100644 GIT binary patch literal 1510 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>lbcxo13z8 z&b&SIe>XONU&0fY%IkRQ>G5NyXY1+c=ZOdwURdQ^0yK*;$=lt9p@UV{1IXbl@Q5sC zU=XwfVMghjlih%V>?NMQuI$e_n1G74UK;_0m}RO$B1(c1%M}WW^3yVNQWZ)n3sMy- zatjz3JUWktd6(Tb;JNcy#I}iLLPLej%}HgI>rcz=H(pRb>8(nxcyFA>EEe`=ebxPT zb*K3M^hU95+?;E8^uglU^Y^%2Ja~0=yLo(WVC?qGFSo2r+FSY1i+6SE#)UeDBF%4} z_?{9eevq(r#_6c;$Gi^4GC^CnS9pJlJA#Os@96~rDm>9*4JH7~Ok>pd`j&=|(O?q`N@kJgXSvtN2jP5;f# zt0)dR$bC(!#OZ|LtX95ucYQXk3WsG`z@R^NGIcVWU*HEGv%?E?Klxv85V4q8?C?3G z*WW16=->_k=R1ZQ9a>&Yo{=D7Qc!=MNkl-W#cA)-^-?t~ov-&DxX4vopc`3w-OjC9 zukP9GQ`f2|l!&IBf5z=I^FX>nrA5sSx7U1~%HIw#GHa}u&snl@UXz42Ul`l`Wo@wy z$I}y1uC#1UU}-j-{9bVXM?0DQw-xkDmd2lIQu@IxTJ(jNVY>JOv87i&$W01=@aa+m zvqiRvK*@=mP-EV0B}QD+K73iR;MUt!^R2jymojzKUGP$!J%9eI*C)d#l?E@6)Lb8Y z_Sr#ZkL=6e++1~+?q9j!`rWP9l#YjScQM#9%;r_*+WJ1J&*nAC-(BR;uD)Zw z;SF2ywV%-uFKdrmU;Q}qQ~AQDU2|0pcBe1XZ#nH?e(qvfw%X$^DTRLZaORF&-kVL2 z#aCO;ZudybI@kL5;H1B^65G=?Zl=y~*Ny(w<69!apx7pJ(N838!)u)va%{(ouNgT1 zOE5cl#6-5}gtdyw5%oV-MhWL4?r{A$p`iTD!QtPugA*I>NL)C%_I{dx^?!D^y{f0r z>F+Q!HcAkGTTrYJWccp7z+1M%n~XiSwapa&wsm3pKbtF}`FB?TaQ)?~b>iH+dJoo0 z=}vYw!4vmy?cAp=^K;QP`G*nAGZ@R3JXG~=7hKBpb5CsT&9!>*+HY>^FPe1r+vSK? zWqTS^>sjTG{(62$_TP`GSIU2^RBO-Q>h(&}^X>e|Ptpgrx_b8gsFMd~ohhC!jv*44 zW6xg~YBCUExxjL?FSaRmQNsHhxBvYQuRN<_vCF^w+?&I($1IL|sl1&sby>F|t7J;? ziO&*Teu5fPwb^nkuJSKfw(wp->vPWSH-j#`E2*xX+Nj^oqReuEQ-OoUiCOU&*Am8+ z57!&cJjZiJ+`CCMUi!v|wZD?L_ZL_wS^fMt%~&tI<-RV<_V^u-KOXTuD1AXy_SncptHiCrBkVIHPy>UftDnm{r-UW|zXqUO literal 1646 zcmbVN3uxSA98c+NYs-mK!P+VdsoGVWOD?&)USe;vy=&J~)Aez6SBJ3glJD+f?~*i0 z@6kyWb;zup_&_JrQe+|gPQ^~=4SQXMXkm){SWmh(plC!!Rlf5x&pgSulonULt0i z8c$KFRLY)W?W!K79300{G(#~Ai4dfbQB05~6{Df3!4C~lmo-yX72MJY!s>|WC6KFm z4hgNKRxt{3LJ_0VK%*RX+TxT0N@5A8jp*^5aY>|L944S*8VKtsVYQfQszyxx7uBWX zzZpQW6@-$FReeb$N+t}mBZ+7f0E0l=BXZeyNkXM_EL8SCQ z(w&DHYQ#(d9kxYLv@Tjs6mp)j`Y_Lf`X|NQ*#$jYgrh-VElVQ^ZAwE7Drj=)XhgX^ z)@&@hIpA*#dL|}5-F)jKz1Mv3)6psj=Gynq9-M6-46VPdzP|pZCwK2yoPX-|Hyhpk z`lE z+%uEEx4w6_EcAZW$>GZJ>|uOr>A||O`F%UGY{ks@ck#-a-E-G|f6sxbg@$M8$6h$_ zaQ2UvosEn8rWcMJx$mQ$Wp7`B30%|kE!+bymCqiMW=;;?-p0@^H(;laJ^DuTS7f%@ ze&f-KsK+*Dd+sgU@?tYSu=X2I)$+hAYgW2`e&YIQ&1c@NpWivLiCY+$`Sskcu<+Yu XL8X1KGWG4a^+yPFbo*ax-}ca-;utKH diff --git a/public/images/browsers/aol.png b/public/images/browsers/aol.png index 66dc4288e69dfe25867c511f168d82755333efe9..3b6f962f792ca91c8b64202ca7e1fb76034aa787 100644 GIT binary patch delta 363 zcmca4v4nYogd__y0|Ud`yN`l^6id3JuOkD)#(wTUiL8^`xb*8)0(?ST|NsC0?Afzb zrylLQ{_@e=Z`ErrPuX+ZqwiqSyi;1$+b=wN&y%uh($mwaKob~~yxm;O zkH}&M25w;xW@MN(M*=9wUgGKN%Kn^#3CNCp!*l^iWq7(chHzX@PDn@)5D{USCov0b8+ZlSI=aO`Vr9ZB0B)w>L*zn(5QfGuQXn#GI23Z6%o* z=I#p=nkRfY_lcL&q1LJC(r(9X1_ct%yea&>4YO=ywU~<87^-vxURf+KGzD6!TH+c} zl9E`GYL#4+3Zxi}42+C*4J>pG3`2~JtxPSgj7_xN3=FIc3@o|5_oC>?%}>cptHi1U VtidDfGb2y~gQu&X%Q~loCIAfedj9|b delta 3118 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 IM6N<$f)?-j4gdfE diff --git a/public/images/browsers/beaker.png b/public/images/browsers/beaker.png index fbc997cd886840a25d6b85d4c7803263a7704cd7..8d4e61d6905db4b6ee1b784603d4e56835efcc4e 100644 GIT binary patch literal 1058 zcmZ`%Yf#j66#Xp=!tzK7WeGu-Hv$XGo81L*mwmFk?6T~WKo;0#(ZBML)u1$yWeOY2 zGTJa9QG%K}v;0PvffP@?+9Hk-M^t25+ZGBO>JF!GNV|ADVM&6?zRJ&&Ctd-bu(~T56vs zWG9c11sfrPUP9PSedkK^31QsPsSQ#9^bBDzI8PbnIWrc?A66+Wl0OxB7h~2|_;?4) zb7z#fm;8Jqv<2UpB@0X@D=V>Iz#R)@MXd@<+rVi}jqT#63~l6{%}*T@XUt?L9u}kw zN3u<^Yn@&UAA{(TH!~#_Gl+hSi>RkJM~14>V)texzQT*^ zSRbaPBW*21bunaV*q8umDykH4YSCmt5f|bVG*#inHn?gb<3W^!8U>oI5XQo$!M8W* zG`KsDFE8TNP8bESsZg8=a}oY}j5-7MG+^;QP7fn19Ci)rb>K(g=GQ2fBPkf^VR*2J z@8_^72^B?nT8`;)a96`BLmC@Ld*IN+QV3H4oJRB?LPj|Ly%hw}*tY{MH7LzPwE~(v z-1-J7At)_CxeO*T5`th9W3v=_9K6#9&r9I00gsL68!tndu4OWygy;F*&%eIu0({!1Y>uAUQ3 zzus#LsOTB;R0j`Dy|=LZ88s@ogmn2tDrmxLQdnCE2?$v(26?kDj23-eN{!U_7e5AD zD4g5B?glO0S4u>Nu-RJMdD6Y<{N<0o`1I-pR!xWZ#6-}6ZjJiN<+j!fUh3VU9@F)l z@m-IeSn}d9MDheD+iW)HKC@CAHPAlx#)k(#7&&lsZ#!kvjrlR%^@{pls2`^ZQKikwE!MV4N;(+G zbZ=7_&r+K0mCaLBQSECHO!uUCup?-We!8T~sXzT}i{g|O1G z*fTskbnUaleASwt*s|=0w_1L_dgT3MtcMXXCp_aXkG?g{=wgw5X9V;1*ITS~_BfZ+ z;<8t}9D>>%4jM=XFC&x7%j5FW4H=n&Y`!2fE1Aa=@Obe~L_pfEjP P&<3KEmPkg$wvK-Q-V(PE delta 3445 zcmV-*4T|!j2&fy78Gi-<001BJ|6u?C00d`2O+f$vv5yP$P@s`7yz(Svt$YYlmGy1d3-`5 z0ICfD?DR=K1%Ck8sgv9n0NA1&sR#g#0RWjOMDC0@ZjPh;=*jPLSYvv5M~MFBAl0-BYzV}=L1a63;+Nc`O(4tI6si* z=H%h#X6J10^u?n7Yw&L(J|Xen{=AF=1OO0D&+pn_<>l4`aK{0#b-!z=TL9Wt0BGO& zT{GJWpjryhdijfaIQ&2!o}p04JRKYg3k&TfVxhe-d$gBmT#QfBlXr(c(0*Tr3re@mPttP$EsodAU-N zL?OwQ;u7h9GVvdl{RxwI4FIf$Pry#L2er#=z<%xl0*ek<(slqqe)BDi8VivC5N9+p zdG`PSlfU_oKq~Co|$RIL`{EECsF_eL_Q3KQhbwIhO9~z3rpmWi5G!I>XmZEFX8nhlgfVQHi z(M#xcbO3#dj$?q)F%D*o*1Pf{>6$SWH+$s3q(pv=X`qSAPkEgfYS=B9o|3v?Y2H`NVi)In3rTB8+ej^>Q=~r95NVuDChL%G$=>7$vVg20 zmyx%S50Foi`^m%Pw-h?Xh~i8Mq9jtJloCocWk2NvrJpiFnV_ms&8eQ$2&#xWpMP3O zZJ>5gFH?u96Et<2CC!@_L(8Nsqt(!wX=iEoXfNq>x(VHb9z~bXm(pwK2kGbOgYnv-SO=4TJ`Rq(~1^XLzFMCW= zLvyNTtY(pBo#t`P0S?Bo;P5%woJ!6i&JE6cEdwn-EwR>Wt!Ax$tvA|w+JC;G2sV%zY%w@v@XU~7=xdm1xY6*0;iwVIXu6TaXrs|dqbIl~?uTdNHFy_3 zW~^@n!VS)>mv$8&{hQ zn>w4zwy3R}t;BYlZQm5)6pty=DfLrs+A-|>>;~;Q_F?uV_J3{m&mGJh5*^k% zbUS=lFJ5+D zSzi0S9#6BJCZ5(XZGXty#9QFK%X?rtK0Rgn&gla_#y$d{dY^~BroJNIJ-#D;)_$3O z2mGGIT7>CCnWh~P(T zh`1kV8JQRPeH1AwGO8}>QM6NZadh`A)~w`N`)9q5@sFvDxjWlxwsLl7tZHmhY-8-3 zxPZ8-xPf?w_(k!T5_A(J3GIpG#Ms0=iQ{tu=WLoYoaCBRmULsT<=mpV7v|~C%bs^U zSv6UZd^m-e5`UMnKjniULQpRlPvxg>O&t^Rgqwv=MZThqqEWH8xJo>d=ABlR_Bh=; zeM9Tw|Ih34~oTE|=X_mAr*D$vzw@+p( zE0Yc6dFE}(8m z`6CO07JR*suu!$(^sg%jfZm#rNxnmV!m1I@#YM0epR(~oNm0zrItf;Q|utvD% z;#W>z)qi~Td2QO--b%O1?dwSEr0Z_1_gTNMO1)}9)zF6U4XqpTjpZ9(ZA#vBp?Yfd zj?J{q%FP2cVKwbr%(krC@}V}P_IjOvUCUPet*f`b*(Tc7zuk9x^A3X@6+7PVl%($2E^vo}is5J@NVEf|KK?WT&2;PCq@=ncR8z zOn>~DYh6)Yy=Ozuo6w@a-(u02P7aQ)#(uUl{H zW%tYNS3ItC^iAtK(eKlL`f9+{bJzISE?u8_z3;~C8@FyI-5j_jy7l;W_U#vU3hqqY zU3!mrul&B+{ptt$59)uk{;_4iZQ%G|z<*%R!&wjS4he^z{*?dIhvCvk%tzHDMk9@n zogW_?4H~`jWX_Y}r?RIL&&qyQ|9R_ktLNYS;`>X_Sp3-V3;B!Bzpi{t)Gt1g7tuwQlrfu3-&M2ore-<^! zxhobD(SrhMFo=kr1wL2=QEWX4S_Bn~5~3nlS(N4GTs0{ouUeLId3AT1srS?Ky$8Q7 zlzO%|d>?#1-}mRum(TnCH6-zW;07=ZV_b9Q2embSb8@$avN9J{7>0L>WXBTyf3$W! z{prNR?qlc2at%pu1w8JoiC>hRK0a&u(lvvF(A^C~L*Vs7AOLAz=!&#nY`$H)ekl1Y zHeSFmSX241BluykxZ^HJP*Mg{1MsQ`{%VI|8-jW8W+eLLRP&6&RBF)Xza5m%J^bT$ zA?fpf0;ztr6uz%<`hBm1?Xshhth?Rl@0Ss;bjk;ItnujVD?<-O@QB; zUt$8d%yi4P!OC>3c^n4M1mNF@jHbZngRVzTo#}^U65M9G&jd)?FQPslJb&sCDyzXx zIu^QIFggmaqOiEex$pWLGBRMu?s;hfMB#B<^AgK~p7`Z!NWwHc>FcTnMm8>UY`pfXv+)P_q+gCm?KhqJ)^Ac}p%ki;Ldzt# zDl!D=5 zCl{s*VtrllJDrU`bUr)O_}!RbNfYEdR{o&YCom>Py65x3@isoNI!S_*mjU X-qUVcJWZ#?00000NkvXXu0mjf3UsQ! diff --git a/public/images/browsers/blackberry.png b/public/images/browsers/blackberry.png index 74f255cb90060cf8a4350dd395283f4d5b4e5625..11ccd8364634ec1bbe54c659ee7c7c566c44335c 100644 GIT binary patch delta 1411 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 DJ+aiG literal 4208 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 Moh_AAV#A307dgCg^#A|> literal 2154 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 Q3H{k&Ywch~I*v{JC!%pf>i_@% literal 2202 zcmbVO2~ZPP7>>XwB4`1xp%vE^14Wa~K_FR)77}7m0!)aCc(Lp*u##lg-35Y(2&gS~ zq!la(*0z90X|00TQpKZ#inP;=)T?T#x78}50!^)obign-F1vxr4tW+o3ZsBs3R35q31Ghk;#lE`#c z3396T+hC#`v}R_&Pn^df3qnC+p~!Ai9}q(wI4Yes_6f&O2sh#;+{`i@R_wr1sU%A> zspKoD9mg*S;C!poIb^(47n8{$f?*>vI5P$u@=`Qo$f9sak27RCjp7j*oSKvE)=;pD z#u1jJ4J2tCj8yEP%77$D7z_l=C(Ia`$-MpC1ze4=xDvESO&|&ph!h5~43^5^AThUv zMWTMFj>L%MtXDu~!7o6$oWT$lc`X=2;bf9FA)LX42}!{r%A5iM4mZLo(n!*rU`{(} zpB6eD9BF1)#EjyRY9+|oEF=gF4n{C>h$KiVkc!0;flL&n5Gc@OOn^y(g5=^5ksK4F zgZI@Wnr@Hm;C<{r-jAUPuCx&2|K_n5oINFAEx~ZX${MJMIDGlQ%18ixSpXxbJ-d}4 zY7Z-pfdik3|LuW(+RRj(BmI|f_rn-6naxCKJS>It)@#!V;*^K%b=YqQ^iLD}&Yr8; zK{(e4>`O-r;x>*ZhMT$0MRP4`YTLIpJf5>YQXOWHZ| z{K5tNqMD1XhGnO#1ZTWtN^R+~fX@TG!rsW<7(Okq`&2>r=mb}{%3*bSvN(T=cFy*s zaka4x33@ANJvH9Pe`_>fdin7u!*a_$y{(jgP%>iF_uG7Av5Nx@!+)RUchuE?Jry1y zG5Dd>KEU(3zmqa7sc91P8PFMEE6=t6FVW<-0frlT-)-MZYJy@@C4JoC^0 zcIie?W3#PpRmU8!%7XIFz%L$7D!jS5t;bw3vQfiiufMnR(xgAz=e<*JvqFy=N)vyo z-gl?uZuaN~E59>scCd@gNBv$WTD)olTdO%)?pZO?h}y2myPyrHJDYt^#iUDeS32$V zZLL@{208AVJdeDR;Hy7&CSoevzT3b4$ot3V&uD72IOkcfwGMZSyZ_VS^kHpK!Myk; zRhoDG*+X-LcPDKJZ;ll&EE}TTpVo42L6v^!FBeCy-tO!^#>pq{@iDO3_+yVMhac`5 zu{WY}-Vy%;SzDV6m;1JO8f1CdkL6!aB)zRYMY0Eeykb|?vCTxonvz}Mv=P(qR!_7& zj5$~o+Nrp3GCFVd?VWQBMMa2z(zZ&^Z`M8tbtlks`{y40;%T8RFh1tU>cib{H$B>t zXyldWzV$WX)wcPldqu5befNWCLquii&mS&?x0O?>U2xt3%jmANd+KJLtSZ}Gsakt@ zsm2&stn4|qN?Eft=lr3@_?&xwO{2BL`TM(*_I_1AUARQGSef7UWJXovs7*s3^#+}`vbyvw==Lde-=hi+0z3hJvkrC1A-QkJ(e***pKSlrm diff --git a/public/images/browsers/ios-webview.png b/public/images/browsers/ios-webview.png index 5f2dd40196c7b37b43ac91b9604f7ba3097c585b..bf7c362e534dbf87c6585df9bf9c1333afdf2744 100644 GIT binary patch delta 1225 zcmV;)1UCD%BK!%E85jfr006c6H|hWY00d`2O+f$vv5yPGVoOIv0RM-N z%)bBt010qNS#tmY4c7nw4c7reD4Tcy000McNliru=Li88J3HHn9@hW>0~>l&Saech zcOY*wjN*-ehY%CU2UP^X+0ceIzsaq7NCG&Ar$Jh`KrQ4khrZ29Iem?*OFCt z?Q=Bf;xpf-Y{_q0OuFr7CD#j9+Fq!^3s3< zIBPTzbjYklTNl+m%=l-RFUU#=zi}tQ1qX<}c0B_Z|tzwKa-oeh!l&S7f2`hWt6D((^4a&g$ zEv)pu8nOl3H>*3Fr%1Nc zoHF}^HTHi?3p+eR$u7e@s;#9@a4x7@c76-{B#0NlCXA3NSZ9kFct~&#U6U;u{mE%8 zP7zpxDL*k_&RE+|>%Hw@7NgX31}x%tblVL0s+t0078IL_t(2&xMgO3c^4TML(zrDi&5+*ob-! zdvkvXYax)%8`xQS06SY7J3BpvIe=J%C`JMz`)94pW)i`jW`}vqXZ{C(03i~@g-Q@2 z001AhSQYp8fk+QKvNsdS`UprBASsQ&0VxoFn;yrka%H9*IV8@28K1Or;F4FdaxzU( zz>tR!g4{NSAG8^^dTi!uk{U2kRp+OgJLOG5`PoC3HntbYx+4WjbSWWnpw>05UK# zGc7PUEif@uGBY|gIXW{mD=;uRFfcg_OT7R903~!qSaf7zbY(hiZ)9m^c>ppnGBYJD nFgPtRF;p@$IyE^uGc+qOFgh?WNLKR!00000NkvXXu0mjfUvU!* literal 4405 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 diff --git a/public/images/browsers/ios.png b/public/images/browsers/ios.png index 5f2dd40196c7b37b43ac91b9604f7ba3097c585b..114bf554c5560ac60ca7fe1ab8a2e52e631fe65b 100644 GIT binary patch delta 1225 zcmV;)1UCD%BK!%E85jfr006c6H|hWY00d`2O+f$vv5yPGVoOIv0RM-N z%)bBt010qNS#tmY4c7nw4c7reD4Tcy000McNliru=Li88J2%olQv3h_0~>l&Saech zcOY*wjN*-ehY%CU2UP^X+0ceIzsaq7NCG&Ar$Jh`KrQ4khrZ29Iem?*OFCt z?Q=Bf;xpf-Y{_q0OuFr7CD#j9+Fq!^3s3< zIBPTzbjYklTNl+m%=l-RFUU#=zi}tQ1qX<}c0B_Z|tzwKa-oeh!l&S7f2`hWt6D((^4a&g$ zEv)pu8nOl3H>*3Fr%1Nc zoHF}^HTHi?3p+eR$u7e@s;#9@a4x7@c76-{B#0NlCXA3NSZ9kFct~&#U6U;u{mE%8 zP7zpxDL*k_&RE+|>%Hw@7NgX31}x%tblVL0s+t0078IL_t(2&xMgO3c^4TML(zrDi&5+*ob-! zdvkvXYax)%8`xQS06SY7J3BpvIe=J%C`JMz`)94pW)i`jW`}vqXZ{C(03i~@g-Q@2 z001AhSQYp8fk+QKvNsdS`UprBASsQ&0VxoFn;yrka%H9*IV8@28K1Or;F4FdaxzU( zz>tR!g4{NSAG8^^dTi!uk{U2kRp+OgJLOG5`PoC3HntbYx+4WjbSWWnpw>05UK# zGc7PUEif@uGBY|gIXW{mD=;uRFfcg_OT7R903~!qSaf7zbY(hiZ)9m^c>ppnGBYJD nFgPtRF;p@$IyE^uGc+qOFgh?WNLKR!00000NkvXXu0mjfXUr20 literal 4405 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 diff --git a/public/images/browsers/miui.png b/public/images/browsers/miui.png index 5f929510f987d9293ae617a31a017b2c3d9f8487..1534a0ca129902c559bd231ee72607857df9f3b4 100644 GIT binary patch literal 1688 zcmcIjdo$WT<)vSlqo1Xh3##~otO{}|E#dOQ}vE(ALf@DRi!zy%@-BEfR#*DB)alhU+2H8Jl6}|C;Ce!YwNhmAnzM9QBq=LOuWDh zOBD&s#6l55>>PQJ_w`eBa(SG=&aA@t?p@R&M*0Z$Xn6XoLrZlhpT)UZm7y!cH8q!g z+t=@VpcAS-!gA=2?JJ^Mrzs*%6xT0YEo`}SPgdLbR4C{YD@HBkRljosf3Ou~#`k$N zKZy}k?(A~ei#PtuWZ@S# zEuw}aS zQGJW5Ipbb_CRG3Qt0n~@=7v=E=?+(yUn5&dFSvDe;_*RFF-0sUdU$NBC0ZCAS*0?W za2r!h9;RTGHnT-h`Zr9^pKeG*&S4pO!=XOu>`NW5A7tn?^`HMzS->TE8rKf-^=ofR zJ?Hn?N%!>^uG^}eF`yF~+I>Ocy|?va?wJG!M~mU+l4z4Q6k|K^?7WrBbfb+jrLq5{ zyy4gp^Nl(iGdmd{AFj_!luQu54)Q-y9s3LKb@t4PmEL=Gyj#bj&$-G1KiN6Hd9+3C z$5_5_zviFayh5V!iS^pw$ijtnF zEZpFqGVJ!&uFWf~N^QyLr^B4njfLWf@I$k<<$p>$D_6zNE$WFwVHryX#uuM`NlPJ4 zrAk%xJA~&9#vU7P$%&kaS(&W!Fd8qe5JQKULz+NZ>VZ2Z=lch8l&w-fe1 z^H^87laPgfoL62fCdWwO+Y`JwD*zoD^4HrDC@G>edYa`Pk*J>j8TJpK=yK_#BlIz zZryF#<+QstON09#I<0BEFkgA-_M7s-*1aZ+>mz+O9y7^5>hISp-p34ER{2%_@nBgS zZ?2@xXVY<$F>Bd(zrEDk`;$e20(QoRijti>6geC+TtBsV(uXrpN9!t zcP9yOKn`>VhArLMmTu2+V7RV!ab-B#(CMypx(ms>^&bXdB0nZF^}h}5pmzk^fV|xO KSeG`1XZ;Q6cLF~E literal 1813 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 diff --git a/public/images/os/beos.png b/public/images/os/beos.png index 6bc4a8a5c58eb439ac01191b0ca8df4c0f1839ed..8e852b0fc82b3096588447ea7b8d2c3671e9ea58 100644 GIT binary patch delta 2601 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 diff --git a/public/images/os/blackberry-os.png b/public/images/os/blackberry-os.png index c77db52541db458d831df2b13afbe0113a31c0d8..093a8a40032757fe7f8693921fa47a2d87fa2299 100644 GIT binary patch delta 1916 zcmV-?2ZQ*^7?Tf>8BzoQ007x@vVQ;o00d`2O+f$vv5yP zfP?@5`Tzg`fam}Kbua(`>RI+y?e7jT@qQ9J+u00Lr5M??VshmXv^ks%j<00(qQ zO+^Ri2mu#6I;@T^K>z>)8hTV%bW&k=AaHVTW@&6?Aar?fWgvKMZ~y>EiN#h+mYgsQ z-0KuMf{|}|cHs}mK1rM}ZaLxxCVN}JhW~NuntMT7-#harFUa;1n6GD#BEVLYd9=#c;-~pMY zwN~F7C$~i!I7Y=&Y=S{+xZ}eD5u#6GgkjLY^*swv zK(G)B^`5@!@DU{L>jFnB^ue`cwYl~+8g%hH-&D3dgQSZfdsd%e$_PG%)5h+a0AlXw11$Y)w;c@V5_@(twoN1y;T$eD-NZIQE&5+ zGi3)mJ5#2+M<=Z8aZg|)0a?_j|Bqp1>pQ+t9o{MbF0|aSvjish%=DRX8R*V$wB44; zxS$5eXmV9r=P3i!w%Blbc-{+7X!fIJw`$$dM` z;D_sfV6?Sl+4%)0W=00=q90BUL@I{;9HQM}Sat@<)-dtFzBlyDpO?DgiF+5nNuQ*G z?Ck!BRFDrN@xmYi`P(+G9~F*#QR}a8tO0xhcA()kONSAEr2_f9U47ZDZnqWo?31Wu zv;8!dx*ktp*&o42vF*C>gg@SrwiRC0hQZ?2m;vuZc&mff-&W;K|%*ZqE9o_tw38MUkWP<=H4N@l_C zTAIb-u-IL@P%Iq*@a4(QIP9U~u$Y-c0midymSf0&1Lqw8NWrtE9mlZ%L_4Oe2B1S7 zIz1Hr9Ujx%;s_u245gG(N=U;RmRpaoSPTIHrnYdu)+{06`iMp~Dx~w8xa%;KD3T;e zl9O~3QRYldZ)1@1K7hedx;vprNMXJjd!7=Xu*@3&)zjy5feIBWlrG2uLVOi#k4SQ!54guY-s38nMse-d z00!|zY_Lt9fhOD%Sx%vvbP*A@0s{J(V}%vIW0498F7X2^tndwY808sW<~Fx^hnwVo znk!-xshW@yLuAR4AxV~VjKn)87^R)BV}EApte-&j5F2SMQlyB%F~&KraD^9{Aj25L zY{Y>9`C`bnRin`zprr>4gj72h|Ma?7-_|M0000bbVXQn zWMOn=I%9HWVRU5xGB7eTEigDOFfmjzGdeXnIx{pYFfckWFgXiLy#N3JC3HntbYx+4 zWjbwdWNBu305UK#Gc7PUEif@3R5CL0000 C>3d%Q literal 3146 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 G%D(_=H5YpT diff --git a/public/images/os/ios.png b/public/images/os/ios.png index 1c129ae84c1753b45092a525cecdd49d225c41bf..7e803a6906ee9785aec3970ad60a8f677cbe67b0 100644 GIT binary patch delta 601 zcmew*HIa3Kqy!5C1H;YYP4z&ECEd~2k%3`jKlh(R*2!&L`t?jn-tI08|J(b|><7wo z7I;J!GcfR82Vq7hjoB4ILG}_)Usv|$983%hOq(Ai|6yQYZ1Hq)4DmR=cB;KMv!lS# zUOJLn%fucm_=o#UT@eNZytYPV`xU#8U@3p0fiB#cw;}a}v z$|il&UHozVp(VQ-0&BD`r;5rpb8BRu?~L6QW@yi{ySdnInbG79arO^wH+~i#SUtJ% zyB))~7i|W!W7)&+NpF}o)8*Z+^$x3B1dACfFK{wWKDOtjN*jLC3P+BaEq@-&6N5%Yk;w&TH+c}l9E`G zYL#4+3Zxi}42+C*4J>pG3`2~JtxPSgj7_u+46F!x!WTsW(*5MKM RnGvXi!PC{xWt~$(69A)n^+W&w delta 3415 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 t6PT#2fxLz#e5Qy$0Dy!50Qvv`0D$NK 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 diff --git a/public/images/os/open-bsd.png b/public/images/os/open-bsd.png index 806887e83f48b210c54c554ca86914ec5a2d2917..b3423ccee06b18b427fdb7db3c1b43381521d9eb 100644 GIT binary patch delta 3082 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 diff --git a/public/images/os/os-2.png b/public/images/os/os-2.png index 5f88105dacb2e27b9b4fb36837699a7ea14bece3..8f51e6183f738c2d0541caa225b46081714958e2 100644 GIT binary patch delta 1173 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 AO8@`> diff --git a/public/images/os/qnx.png b/public/images/os/qnx.png index 59d9a44c4363de57e873cbee62ddd0018c8d207a..1cf10fe57dbd525b4534fee128dec06e0603c10a 100644 GIT binary patch literal 2334 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 m0-9*4^;CY#Od0ngjqLHT;r<)TD-Twi2Fu$Sz diff --git a/public/images/os/sun-os.png b/public/images/os/sun-os.png index c19f0eb39924ae56743784b3c5efe675667456da..648eb24175e8e8ff4680421f02e8241ecedc42af 100644 GIT binary patch delta 2261 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 diff --git a/public/images/os/windows-2000.png b/public/images/os/windows-2000.png index 3bccae3fb1731eb95029719ac7eb8b166a2f2e54..8ec7db1876bfc3fe0bcb0662f93271403d30e76c 100644 GIT binary patch delta 2794 zcmV0Dy!50Qvv`0D$NK z0Cg|`0P0`>06Lfe02gqax=}m;000JJOGiWi{{a60|De66laV18e*+YHR9JLUVRs;K za&Km7Y-J#Hd2nSQcx`Y107!|&R!0)sFburo6?p;$kbw9c*^+LV$uZB*F6b)TsfSo& z6%t%5DkcwOs?^5Sj0t=hPe^R$r9Zvw7VUeoswrR0*%(lerSm-O2`J{#4e)cVlb3zSZQ++27 zTgJpqJek~KFm~dKdRw8T&2?=>l;rlvA?v!?q$~IoT+nL4F`sOPQ59d!OwSlsf5dOR56ka1$Qxbhm%5?|{y>EseFMPt4Y)3=@s(uVRA@%Z>)f(=Z~@ZVw0}_1 zY8_817%ceLG>$+)2g$m7M;0AgE1wqW_i>cQqAl9`%IWq+__zh&6r z8A=Wr=26dKf7k;7`CA4@0&-(OC-X+T>a#~@}eiM~!wm*%f?#CTif1bDCqu4ku+~JR>q%p&znjFq*jnp%e zI*wWL1LuH6uhS8M4FCWJ=Sf6CR9M61muXOy*B!=xclClBic!F&f}rT-%9V?|JW27y0Ui@2Kq}vLY->a%Pk9j&>;qs1 zb^~eO8GyRB`LeqCqS0L^Xi;>GtBn^x9uN=o0Xlu_0M*Y%JXC%Jx8}LHkrO^xxvsF< z{OC?=q5=R$0ylsufE|eb_C{c4mxr3wf0?^E-F&W3wo{!QhUWI(r5sn%$dJ|qm<(RP zvw(oVc|ZmOE>Bf{Ku4f0;15{89zdSsq2@YJUSwWPdQ8I2EIU5>;?FtC!>ElyGiNa&S96tjE7|dtKAasP(t9oKIIi(`#ONa*#i; z9C)vp*|@AEuKoVC?hab)^3!x@e_OfQdN&4g2Y9BwMT60V(Q2!^d>)ANHkoX*Bif>Q zeR?U^_3`aAd!MRl&L+3gx>;&c1JSIAN-c3~najTfr~&Ry>ehO*R>WUdew36SB`uHF z_#htva%OaweN4!sAU^}ZwP-iJm;QmMCHv?QpN_{o$AN^mQi8aaVRtB_f9D|0jnuty z_BX3jL#Uk-5%+m!n7`Rzm=Bx#){qqV@uFOs=x3mOuflq;{z~mM-vwL+Pf3DAp z$iF((IU~Ha*QUCj{3f|`xn6MBYPwU(jnMsRPPH-_1StKTq58>%e@V*zV65^#o1*j` z!(=oY>i{Ovck?I2t~wqaTXeK#%&Mc-erqb;h}}@prE6LN*5E{drJk1$05h7|Ou61h zLnN>Y*z038E{Wa&&_6EC(k?uu6_C)Ynl8fKNau_OwD$UcF`RUtIBa zOwqBB=z`d6Uk1H#XNh0JIE!y+!oU`tf5Q6!%YX`Cr*}Z;9@>Ws z-(X?(5g6)CX7+2T*f|lJ`B8>m(gMZIayB$z_qwIG3UJvo)-vFgn`2{NJm=`U^vLMg zmyQ!ve4>5Avg7OH)}8uMV(F!KM()%;M|PPD_&KnzX+Y?rfu)rd12%rH#Nx9WRCZnd zapMK3{ZkK@e_V?|AC3Ff(`x5vQ+Mw~!{n0=bO91ucI(T4t#=3haNE7p@r578#4kTG zG_LSu*Vw`nB(DDRuB5WcUp5WtU6=V3JZkZtlsEOCgjF9YspO&tmi|+gRuAU?@-DN^ zAFwFJZD=eRWj>k#yLe0%7!Wyvu&J9EwEbSnkS#Tee}DP>rkKJbVaa7TlEQPh*?`r+ zdq%5QfxGb7AN#MnAa}u0CBJ@0VOb>t6c>hc*(zPNN>{8ZU1(Ah!7E|00L}r$?IMN|Ip^Im-80ri7_44Jz-6PQ z$@}i2Q@XMRKn=f2iO`x$GLSCo%yP5ck=?1^bf04`9OzC%?+PteN%vzfHz;#XRtx4E}V+I)H=UadM3g1I?0SbY8jf{+) zfA28@fjtcxKh6`1bsm7}Ji#agg1`D${{Q}W{9lg1o)-Lms^S0u03~!qSaf7zbY(hY za%Ew3WdJfTGBYhOI4v+SR5CLN#&r!&dO@tOuCf=(kN-Hn|vPHZbMfNmeb0ry2XyHO*8 zs6-mld?&(8K!ON@K^_Gm2xc(3Bmo(@%u9mS;?HO#Vj02pt#{o9z}p+ay7p&Sc6bk=<&lw)s-J8KX0OmPxu5+6c?>O}u+dlpjM%1SAdR|$V~27W%fe|?G{^vhK_-t2 zVX-S%b^wdV;s&t(3-!wJUj#tDb#hvf@wd7J2d{|W@@&E&Gd?@yZ_!-0a5ji`1-YzH z4i&TsgVfX%Tf-(#c^Y?7+)gg@eSTwp{_}@Z}NXEFoK%tzWQFxSp1k$#O1R;)$A8I)Ck1Ol@dggO@oYEeP4 z_oe_qI@H11(#<38WkxuA|I1Cs&t4kbu0^;8*U)g3(30HMuNgp!d1e2sG%l7mJ zs+T*5R`#~aufLMzs_!Cop$myXrXBdX#lrHzTCJAtF*-r3XmxPH+%R(_ms`IW!qv?; z8U69jwuSau!zY}M+RiIy=Ax3cVxRt=`mrveA%0QDY$Dh+xp%|uKKaNuQ=hC_g@5z|j**8#7#r?Fs`6W+k2x0@>JI%XF)K5NG*ap{n zlkG+83|jg0q)-_sCCI2qWTBA;8FZK_E-`O64Q8DEks zVpu3Ol+Q)o_VteG5se?ME(ZF@GdgVAQsu|iXLXznV^#C>MZWu~o-NAvnxFN~>Q=mc zJ3BktQM6C5)U5CFZ0_5UXjtm@;;HKy--oKxce{o=_KSkNwg`g?g}OYledhZ^UTc2d zU2*JuphtuxLe6D;z+>e7gP74W)uea$mJRqllHN#eyHUFe6AoUuq3YPdIdW>yVUwMn znrzH*Sa=}iVODeVZ=;Tjs>CHempP^`@x-JjbqG zV=3{PeR_TKVei3>DtUQ%I|ahV;Ifvq{5k`h+m(g%vyK|Yh4gr*Kw;FkR#p=PmCI+x zDVB;pOG4ABu9}BE(Tlw@VK&3NPM$r!v;M?=2kPwbr2JxtZs`CmQ1L!BRv|{iBL2*o z!3-@vzpF+eBCpS?`1@*BPKjgAmTn;YVK29HeZ!nhJ9e=D!G;s$qH8&hDHYw$j=G6a zkAfb)!^Lh~Qz_rZjwW42RQc=wtf?uwP%(90HKtxgcCWf#h9UvS4%=2xs-E!5MWq?* zmGj3@fsF z;a2NG<>Q0nUA2~v@-OjU=6@{bPFC97Vc2NO9X0bwGxx2Xy8p|3^R#=!u@A*&>vMbA zK!29sDb)@6q=O2o56}0WfNydRJ2=5@T1Qj7qdk7Pw6ae+c^$l|loN1m2TVz7QRqd1 zyC0Y=FE6{?E-!7~t$ESVXfWLS_Cjar81MCqEQQn2HFpCQ^_epa^)(E7Ql?VU_fbHV z)0K{Nrb*8e!!8_}Vd(yK* zAM_L-#PbFVZ8X7xLq~ROR)LjP^5V1Wg(cVL>h2yHs{J+n0+U6Wx^5EVOst!GT<`N_ zbnDCR?r$-XxTaINn)f5(XA_!R+G8&l=%{}6iuhZn*TKP*qKIk9>w<$5K*M(eOU}B9 z9FOV7mObCp%nhDzJND+qy9HI9>gdxbnmo=e{DO?98hzUI<4Kfz$6HULVkYdzQ7?S{ zKzhnOa_{3G@`y?|L`R_RyPtL8%;U0h&gkR^)wKmJTD4dsYJclx_ z(acD6AHy!4?K+;Q_LR+gy!OecMUU!FZBF~{YmbN4!QretFGYxGH(&c<8&$@29@tL!6xsuH~wb-rH1iY|FK zF+7%hEBj(%Wn|%zmKK68r$0h?gt=0Bv9+MUEXR`mA5=lq^HbmRD)YcrF9>15P& ze}bNEoJGj7RH^M$dFMk)K=X-_n6i>1@r4h*YD5>0mUouYmohF24k+i{(~c!DrQL;n z9i0oL4w!4as+#QN%ryx?{<$bAna47|KFM16x>8vU#!#{Y$Jb0g;IQgbzgD0%=7XsB zwWW4avUS+4o<$++;fIrMGPTyVTWh2JcQ?qz#8#`6DXMgsFBs$;F5a#VBt;(vl+07| h(5F)u$Cnb96lO;4@UL7c0Dy!50Qvv`0D$NK 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 h(5F)u$Cnb96lO;4@UL7c0Dy!50Qvv`0D$NK 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 P0000<2SrXqu0mjfW!hFm literal 2723 zcmbVO3pkYN9v_#nC?b*MGDa7-nQtzPnPFTqVq|jJskBbzo0)IMFgG*9jLX&#$7-Xb zQj$Y?4Z~rxdAByOpRcdc^rgcy{;coPExI&+~ro`+o25{r&I%=gW;) z8*E~1V~ju`Oc){bD0oI`hoJ%dmZzqEfF~nq$VNE=G0#FfbP&ZQ)(C|Dpnw&vh-QXS zIT8^T*3`|bVm_z1R{VV0t6xkN2Yp`s6;Q+%!7u-$+$df6n*uK zFnC8p$0-z2DgY>zO01HImB{!2o7twN7iWCB(Ah{$- z#({#8VKtq!)<~&=G6+;iWGsn9I2);mS(Q-)4=f4gyg?x5N|bV!-&}y`paPE8wMk35q9DJ;=X-hURF)3USdhp9O!}1D|P?aS%-UKf?VPCYSION>By` z@L_NLX*vP0@_@DuKidKP(ZuPq-_-0Z9Bu^KUvEjbt z>&v|O1056mf%XzPecd>4ed0uq4rSfMVCVeSb&HM03rutCgFu(7+5UF8`L?Ua+pT+a zDbM()$fUI0CZ`s#X$N>Huc#fk&OJ`g_lY#UDY5#eWA2fc+>gj=C_59_{F0dXUE8vK z@nuWOAG(A)UNr4bj2V(u;Ib!Ou3JeC_@&%8Ty*s-$azuw!73M@5a+bC7kxdpCv(C~ zhb(l53=G^(yI2?ISBV(UY|BnM)9;20CwPn_jsr-G_r|_csO56YSRLn>4r}xedptX# zO471-V*h{Co~i?1-fH~&P#3F|Z#&^>J4|ld*=D96uYPs-UE-pe){9+T3|E`Q-nZlC zzxX*s=fb1>cKhSeH``cN>~fOvM$Y^MHW@XvG?S`vkJVX~bSD6Hx^BgxLvdkE_s1~t z?R^u>ed=h`rkAfvi&`z?fqx#t=>|t`IwN? z_*(4l@l&?Di$?m5>#IKc13`=PQFr4ELF)nfq-BP#_eY?u;AVBGaqbQSbp}8qi;a$+ zQ?nw^*^O;pqDK7_&o9jL?;}n19B4N3S|PMyT_L7p)%O)6l}))GOPRbwPq*&Xw%vlPoibC{Mj( zDXAS9jY)us?~Su{jV71UeQDu`YWN1dYS(6_Rqfu3Fx3947`M|OFcB!Gab`}LERIJ$ZPOY4O*3|E5>8G4C zWyKRIF7}hYeYsoveJ#68N_MeX)VADdv7gX8W7z!H z2$qfL2hNOet&fg;pqqE9iaSviHTN^%>MHR12 zo^Um6C`xO1&m4?fia&VgG{r{UU_Mw%D7>n4-qgZbW98N>NYlM_biGt48dYXi?-_2M z3LB%g~>lSSw*hof_HmYmAnqTVR3se zFpO5~to*_AZ*KQ4m*BsBvn{NlkACTfX*uIsbhq$Ji2d-zjEy!JPe5wwswF Fe*iRDIduR4 diff --git a/public/images/os/windows-8-1.png b/public/images/os/windows-8-1.png index 3ce98aaaf9152cdc6117aa39e222038d2d79f063..f6605f4c53eaa36ec6325def7a6d9bc639b4f22d 100644 GIT binary patch literal 1412 zcmZ`%drVVj6hE!BKnV=(gO?&Gh*HZ+TZq7ngSCZ@M+8{~g{;t0=%RyZU8aAK@|bgD zi*;@jfe~SK3@B}3n_HN`RzR}|W@N@_vSg^BfPKQ2JtI5!UP@e+-F)|a=brEU9{o<= z-d~U%%!*_IK(Hc5T7-KH@dj+bC^fxv4L85)%=}D%!LvbsS1^b}bxu(}z^NSohGu|2 zv1C{PI3WaBQURp50)*9d3>Q8JK#$*_S1bdI2L?P~c7fRg7B86HFyMvHoM3T*)dT%l z_d<^o`aRI=f&n+!95CPliyN%Cyab$LKoTrs-sgm&6#|PDqKFYW$%S>Ih$#H@)hS(y zXN}6&k7k~RBMCNBRD&z<#mX8F_Mn+{6=X(ro3E3olZMwC$Wy`qNlsxY=akkbP8j=z zWTrTu`U%)TEdq--gBLS&dOV_f&vPZ3hm7<~)`$udbrl2AS=GZg2V zbL2!tpAeBy5>9e7e`Mvpevk| z)w#O5de`*qB>t1k`l5mZxc~Q|MenOT-RrePWRx3B*B3^%q(}zsbBt}76^*8e2Of3P z*Ee0x%4P4!$WFnogoOCGxNWgpV>YuQ!g*ZwrqGb!AXWg=Zv!nQBULOCCME6W?~V)L zaJXC^j~9kd6x*Sjs2Q)Wwrjt0Xq{ExR%yn@-mpwQQIDpTo@&4P)2L9;IXE-wpOCF< zvA9PVaZ>GD)193%y9dqEDs!`?SNi;ey!a7Vms5HiAc#%8G-&Gx$D+SpkuURq6hOzk z+(G{VF=rLh%;G&a)GC#xQdmfY3ynrcZ%H@U0uLUZ0=^~iT^d$MK(MEb{ z+DV2iem*6}rey3KpS)oYR9@=4)??}F{gN37+&a-Q-Q$I&#i;Pefn=?uw25V4Fs-W^W(cD~9~LfiK}>-OF5M3ubAR=?MHM|EB0o?V!8 z++TX=y0U!Nc@?3Hy~9?yauGS!?$BpKlqu zFP*!r6SuL78Pi#Un&tMxg`5)6m9B4S`3aNTgcIi^e29+`PHue{F%LRe>MDJ zY?!IFPbkj!P?(Z1h+;GnQHZuw(IEvG!)$dmS%+HAUmkR{QnctJcm|X+pJ{ ziy5^~8sBW03TxsVDQ`3xHkzJ4b57QJF|+ybnWKwC^L(Y{P%!+y-M{5BH~uP4C|)ls z(Q7L7Rq5(uRk(pjC=v^Vdj!JdVv#sKB`sYn;R}W7LSb4^?#RCYM{6}Y?aBWK$P@s`7yz(Svt$YYlmGy1d3-`5 z0ICfD?DR=K1%Ck8sgv9n0NA1&sR#g#0RWjOMDC0@ZjPh;=*jPLSYvv5M~MFBAl0-BYzV}=L1a63;+Nc`O(4tI6si* z=H%h#X6J10^u?n7Yw&L(J|Xen{=AF=1OO0D&+pn_<>l4`aK{0#b-!z=TL9Wt0BGO& zT{GJWpjryhdijfaIQ&2!o}p04JRKYg3k&TfVxhe-d$gBmT#QfBlXr(c(0*Tr3re@mPttP$EsodAU-N zL?OwQ;u7h9GVvdl{RxwI4FIf$Pry#L2er#=z<%xl0*ek<(slqqe)BDi8VivC5N9+p zdG`PSlfU_oKq~Co|$RIL`{EECsF_eL_Q3KQhbwIhO9~z3rpmWi5G!I>XmZEFX8nhlgfVQHi z(M#xcbO3#dj$?q)F%D*o*1Pf{>6$SWH+$s3q(pv=X`qSAPkEgfYS=B9o|3v?Y2H`NVi)In3rTB8+ej^>Q=~r95NVuDChL%G$=>7$vVg20 zmyx%S50Foi`^m%Pw-h?Xh~i8Mq9jtJloCocWk2NvrJpiFnV_ms&8eQ$2&#xWpMP3O zZJ>5gFH?u96Et<2CC!@_L(8Nsqt(!wX=iEoXfNq>x(VHb9z~bXm(pwK2kGbOgYnv-SO=4TJ`Rq(~1^XLzFMCW= zLvyNTtY(pBo#t`P0S?Bo;P5%woJ!6i&JE6cEdwn-EwR>Wt!Ax$tvA|w+JC;G2sV%zY%w@v@XU~7=xdm1xY6*0;iwVIXu6TaXrs|dqbIl~?uTdNHFy_3 zW~^@n!VS)>mv$8&{hQ zn>w4zwy3R}t;BYlZQm5)6pty=DfLrs+A-|>>;~;Q_F?uV_J3{m&mGJh5*^k% zbUS=lFJ5+D zSzi0S9#6BJCZ5(XZGXty#9QFK%X?rtK0Rgn&gla_#y$d{dY^~BroJNIJ-#D;)_$3O z2mGGIT7>CCnWh~P(T zh`1kV8JQRPeH1AwGO8}>QM6NZadh`A)~w`N`)9q5@sFvDxjWlxwsLl7tZHmhY-8-3 zxPZ8-xPf?w_(k!T5_A(J3GIpG#Ms0=iQ{tu=WLoYoaCBRmULsT<=mpV7v|~C%bs^U zSv6UZd^m-e5`UMnKjniULQpRlPvxg>O&t^Rgqwv=MZThqqEWH8xJo>d=ABlR_Bh=; zeM9Tw|Ih34~oTE|=X_mAr*D$vzw@+p( zE0Yc6dFE}(8m z`6CO07JR*suu!$(^sg%jfZm#rNxnmV!m1I@#YM0epR(~oNm0zrItf;Q|utvD% z;#W>z)qi~Td2QO--b%O1?dwSEr0Z_1_gTNMO1)}9)zF6U4XqpTjpZ9(ZA#vBp?Yfd zj?J{q%FP2cVKwbr%(krC@}V}P_IjOvUCUPet*f`b*(Tc7zuk9x^A3X@6+7PVl%($2E^vo}is5J@NVEf|KK?WT&2;PCq@=ncR8z zOn>~DYh6)Yy=Ozuo6w@a-(u02P7aQ)#(uUl{H zW%tYNS3ItC^iAtK(eKlL`f9+{bJzISE?u8_z3;~C8@FyI-5j_jy7l;W_U#vU3hqqY zU3!mrul&B+{ptt$59)uk{;_4iZQ%G|z<*%R!&wjS4he^z{*?dIhvCvk%tzHDMk9@n zogW_?4H~`jWX_Y}r?RIL&&qyQ|9R_ktLNYS;`>X_Sp3-V3;B!Bzpi@Bz|Vn|xtSVb=-o>~3l;EJ(NMtrdq0nW#pR#>9{5K+uo#qb-%Nln>4EbaSOBfu z3m#!DA!_6(eIfD^K_U=l;3&Fl2oP^T2Z&RQ^BS2%U>g%inI}2&90d^A!4J%aq5@XH z%b5O%L>N23fkh!RwvbO~1=J}JC)hQA5+IegOj!9VVA$v?i7?b29w2cowK(pdRG9a` z>L^v5=`lW&JL?-!-9&x{yaG-4_+e+AOnbjPCwDFZH6I&#ASNO9SIGSakd>yBX`F0_%o&BqL3}vg!^X-Fy!@>DQ8JK#$*_S1bdI2L?P~c7fRg7B86HFyMvHoM3T*)dT%l z_d<^o`aRI=f&n+!95CPliyN%Cyab$LKoTrs-sgm&6#|PDqKFYW$%S>Ih$#H@)hS(y zXN}6&k7k~RBMCNBRD&z<#mX8F_Mn+{6=X(ro3E3olZMwC$Wy`qNlsxY=akkbP8j=z zWTrTu`U%)TEdq--gBLS&dOV_f&vPZ3hm7<~)`$udbrl2AS=GZg2V zbL2!tpAeBy5>9e7e`Mvpevk| z)w#O5de`*qB>t1k`l5mZxc~Q|MenOT-RrePWRx3B*B3^%q(}zsbBt}76^*8e2Of3P z*Ee0x%4P4!$WFnogoOCGxNWgpV>YuQ!g*ZwrqGb!AXWg=Zv!nQBULOCCME6W?~V)L zaJXC^j~9kd6x*Sjs2Q)Wwrjt0Xq{ExR%yn@-mpwQQIDpTo@&4P)2L9;IXE-wpOCF< zvA9PVaZ>GD)193%y9dqEDs!`?SNi;ey!a7Vms5HiAc#%8G-&Gx$D+SpkuURq6hOzk z+(G{VF=rLh%;G&a)GC#xQdmfY3ynrcZ%H@U0uLUZ0=^~iT^d$MK(MEb{ z+DV2iem*6}rey3KpS)oYR9@=4)??}F{gN37+&a-Q-Q$I&#i;Pefn=?uw25V4Fs-W^W(cD~9~LfiK}>-OF5M3ubAR=?MHM|EB0o?V!8 z++TX=y0U!Nc@?3Hy~9?yauGS!?$BpKlqu zFP*!r6SuL78Pi#Un&tMxg`5)6m9B4S`3aNTgcIi^e29+`PHue{F%LRe>MDJ zY?!IFPbkj!P?(Z1h+;GnQHZuw(IEvG!)$dmS%+HAUmkR{QnctJcm|X+pJ{ ziy5^~8sBW03TxsVDQ`3xHkzJ4b57QJF|+ybnWKwC^L(Y{P%!+y-M{5BH~uP4C|)ls z(Q7L7Rq5(uRk(pjC=v^Vdj!JdVv#sKB`sYn;R}W7LSb4^?#RCYM{6}Y?aBWK$P@s`7yz(Svt$YYlmGy1d3-`5 z0ICfD?DR=K1%Ck8sgv9n0NA1&sR#g#0RWjOMDC0@ZjPh;=*jPLSYvv5M~MFBAl0-BYzV}=L1a63;+Nc`O(4tI6si* z=H%h#X6J10^u?n7Yw&L(J|Xen{=AF=1OO0D&+pn_<>l4`aK{0#b-!z=TL9Wt0BGO& zT{GJWpjryhdijfaIQ&2!o}p04JRKYg3k&TfVxhe-d$gBmT#QfBlXr(c(0*Tr3re@mPttP$EsodAU-N zL?OwQ;u7h9GVvdl{RxwI4FIf$Pry#L2er#=z<%xl0*ek<(slqqe)BDi8VivC5N9+p zdG`PSlfU_oKq~Co|$RIL`{EECsF_eL_Q3KQhbwIhO9~z3rpmWi5G!I>XmZEFX8nhlgfVQHi z(M#xcbO3#dj$?q)F%D*o*1Pf{>6$SWH+$s3q(pv=X`qSAPkEgfYS=B9o|3v?Y2H`NVi)In3rTB8+ej^>Q=~r95NVuDChL%G$=>7$vVg20 zmyx%S50Foi`^m%Pw-h?Xh~i8Mq9jtJloCocWk2NvrJpiFnV_ms&8eQ$2&#xWpMP3O zZJ>5gFH?u96Et<2CC!@_L(8Nsqt(!wX=iEoXfNq>x(VHb9z~bXm(pwK2kGbOgYnv-SO=4TJ`Rq(~1^XLzFMCW= zLvyNTtY(pBo#t`P0S?Bo;P5%woJ!6i&JE6cEdwn-EwR>Wt!Ax$tvA|w+JC;G2sV%zY%w@v@XU~7=xdm1xY6*0;iwVIXu6TaXrs|dqbIl~?uTdNHFy_3 zW~^@n!VS)>mv$8&{hQ zn>w4zwy3R}t;BYlZQm5)6pty=DfLrs+A-|>>;~;Q_F?uV_J3{m&mGJh5*^k% zbUS=lFJ5+D zSzi0S9#6BJCZ5(XZGXty#9QFK%X?rtK0Rgn&gla_#y$d{dY^~BroJNIJ-#D;)_$3O z2mGGIT7>CCnWh~P(T zh`1kV8JQRPeH1AwGO8}>QM6NZadh`A)~w`N`)9q5@sFvDxjWlxwsLl7tZHmhY-8-3 zxPZ8-xPf?w_(k!T5_A(J3GIpG#Ms0=iQ{tu=WLoYoaCBRmULsT<=mpV7v|~C%bs^U zSv6UZd^m-e5`UMnKjniULQpRlPvxg>O&t^Rgqwv=MZThqqEWH8xJo>d=ABlR_Bh=; zeM9Tw|Ih34~oTE|=X_mAr*D$vzw@+p( zE0Yc6dFE}(8m z`6CO07JR*suu!$(^sg%jfZm#rNxnmV!m1I@#YM0epR(~oNm0zrItf;Q|utvD% z;#W>z)qi~Td2QO--b%O1?dwSEr0Z_1_gTNMO1)}9)zF6U4XqpTjpZ9(ZA#vBp?Yfd zj?J{q%FP2cVKwbr%(krC@}V}P_IjOvUCUPet*f`b*(Tc7zuk9x^A3X@6+7PVl%($2E^vo}is5J@NVEf|KK?WT&2;PCq@=ncR8z zOn>~DYh6)Yy=Ozuo6w@a-(u02P7aQ)#(uUl{H zW%tYNS3ItC^iAtK(eKlL`f9+{bJzISE?u8_z3;~C8@FyI-5j_jy7l;W_U#vU3hqqY zU3!mrul&B+{ptt$59)uk{;_4iZQ%G|z<*%R!&wjS4he^z{*?dIhvCvk%tzHDMk9@n zogW_?4H~`jWX_Y}r?RIL&&qyQ|9R_ktLNYS;`>X_Sp3-V3;B!Bzpi@Bz|Vn|xtSVb=-o>~3l;EJ(NMtrdq0nW#pR#>9{5K+uo#qb-%Nln>4EbaSOBfu z3m#!DA!_6(eIfD^K_U=l;3&Fl2oP^T2Z&RQ^BS2%U>g%inI}2&90d^A!4J%aq5@XH z%b5O%L>N23fkh!RwvbO~1=J}JC)hQA5+IegOj!9VVA$v?i7?b29w2cowK(pdRG9a` z>L^v5=`lW&JL?-!-9&x{yaG-4_+e+AOnbjPCwDFZH6I&#ASNO9SIGSakd>yBX`F0_%o&BqL3}vg!^X-Fy!@>D0Dy!50Qvv`0D$NK z0Cg|`0P0`>06Lfe02gqax=}m;000JJOGiWi{{a60|De66laV18e*+YHR9JLUVRs;K za&Km7Y-J#Hd2nSQcx`Y107!|&R!0)sFburo6?p;$kbw9c*^+LV$uZB*F6b)TsfSo& z6%t%5DkcwOs?^5Sj0t=hPe^R$r9Zvw7VUeoswrR0*%(lerSm-O2`J{#4e)cVlb3zSZQ++27 zTgJpqJek~KFm~dKdRw8T&2?=>l;rlvA?v!?q$~IoT+nL4F`sOPQ59d!OwSlsf5dOR56ka1$Qxbhm%5?|{y>EseFMPt4Y)3=@s(uVRA@%Z>)f(=Z~@ZVw0}_1 zY8_817%ceLG>$+)2g$m7M;0AgE1wqW_i>cQqAl9`%IWq+__zh&6r z8A=Wr=26dKf7k;7`CA4@0&-(OC-X+T>a#~@}eiM~!wm*%f?#CTif1bDCqu4ku+~JR>q%p&znjFq*jnp%e zI*wWL1LuH6uhS8M4FCWJ=Sf6CR9M61muXOy*B!=xclClBic!F&f}rT-%9V?|JW27y0Ui@2Kq}vLY->a%Pk9j&>;qs1 zb^~eO8GyRB`LeqCqS0L^Xi;>GtBn^x9uN=o0Xlu_0M*Y%JXC%Jx8}LHkrO^xxvsF< z{OC?=q5=R$0ylsufE|eb_C{c4mxr3wf0?^E-F&W3wo{!QhUWI(r5sn%$dJ|qm<(RP zvw(oVc|ZmOE>Bf{Ku4f0;15{89zdSsq2@YJUSwWPdQ8I2EIU5>;?FtC!>ElyGiNa&S96tjE7|dtKAasP(t9oKIIi(`#ONa*#i; z9C)vp*|@AEuKoVC?hab)^3!x@e_OfQdN&4g2Y9BwMT60V(Q2!^d>)ANHkoX*Bif>Q zeR?U^_3`aAd!MRl&L+3gx>;&c1JSIAN-c3~najTfr~&Ry>ehO*R>WUdew36SB`uHF z_#htva%OaweN4!sAU^}ZwP-iJm;QmMCHv?QpN_{o$AN^mQi8aaVRtB_f9D|0jnuty z_BX3jL#Uk-5%+m!n7`Rzm=Bx#){qqV@uFOs=x3mOuflq;{z~mM-vwL+Pf3DAp z$iF((IU~Ha*QUCj{3f|`xn6MBYPwU(jnMsRPPH-_1StKTq58>%e@V*zV65^#o1*j` z!(=oY>i{Ovck?I2t~wqaTXeK#%&Mc-erqb;h}}@prE6LN*5E{drJk1$05h7|Ou61h zLnN>Y*z038E{Wa&&_6EC(k?uu6_C)Ynl8fKNau_OwD$UcF`RUtIBa zOwqBB=z`d6Uk1H#XNh0JIE!y+!oU`tf5Q6!%YX`Cr*}Z;9@>Ws z-(X?(5g6)CX7+2T*f|lJ`B8>m(gMZIayB$z_qwIG3UJvo)-vFgn`2{NJm=`U^vLMg zmyQ!ve4>5Avg7OH)}8uMV(F!KM()%;M|PPD_&KnzX+Y?rfu)rd12%rH#Nx9WRCZnd zapMK3{ZkK@e_V?|AC3Ff(`x5vQ+Mw~!{n0=bO91ucI(T4t#=3haNE7p@r578#4kTG zG_LSu*Vw`nB(DDRuB5WcUp5WtU6=V3JZkZtlsEOCgjF9YspO&tmi|+gRuAU?@-DN^ zAFwFJZD=eRWj>k#yLe0%7!Wyvu&J9EwEbSnkS#Tee}DP>rkKJbVaa7TlEQPh*?`r+ zdq%5QfxGb7AN#MnAa}u0CBJ@0VOb>t6c>hc*(zPNN>{8ZU1(Ah!7E|00L}r$?IMN|Ip^Im-80ri7_44Jz-6PQ z$@}i2Q@XMRKn=f2iO`x$GLSCo%yP5ck=?1^bf04`9OzC%?+PteN%vzfHz;#XRtx4E}V+I)H=UadM3g1I?0SbY8jf{+) zfA28@fjtcxKh6`1bsm7}Ji#agg1`D${{Q}W{9lg1o)-Lms^S0u03~!qSaf7zbY(hY za%Ew3WdJfTGBYhOI4v+SR5CLN#&r!&dO@tOuCf=(kN-Hn|vPHZbMfNmeb0ry2XyHO*8 zs6-mld?&(8K!ON@K^_Gm2xc(3Bmo(@%u9mS;?HO#Vj02pt#{o9z}p+ay7p&Sc6bk=<&lw)s-J8KX0OmPxu5+6c?>O}u+dlpjM%1SAdR|$V~27W%fe|?G{^vhK_-t2 zVX-S%b^wdV;s&t(3-!wJUj#tDb#hvf@wd7J2d{|W@@&E&Gd?@yZ_!-0a5ji`1-YzH z4i&TsgVfX%Tf-(#c^Y?7+)gg@eSTwp{_}@Z}NXEFoK%tzWQFxSp1k$#O1R;)$A8I)Ck1Ol@dggO@oYEeP4 z_oe_qI@H11(#<38WkxuA|I1Cs&t4kbu0^;8*U)g3(30HMuNgp!d1e2sG%l7mJ zs+T*5R`#~aufLMzs_!Cop$myXrXBdX#lrHzTCJAtF*-r3XmxPH+%R(_ms`IW!qv?; z8U69jwuSau!zY}M+RiIy=Ax3cVxRt=`mrveA%0QDY$Dh+xp%|uKKaNuQ=hC_g@5z|j**8#7#r?Fs`6W+k2x0@>JI%XF)K5NG*ap{n zlkG+83|jg0q)-_sCCI2qWTBA;8FZK_E-`O64Q8DEks zVpu3Ol+Q)o_VteG5se?ME(ZF@GdgVAQsu|iXLXznV^#C>MZWu~o-NAvnxFN~>Q=mc zJ3BktQM6C5)U5CFZ0_5UXjtm@;;HKy--oKxce{o=_KSkNwg`g?g}OYledhZ^UTc2d zU2*JuphtuxLe6D;z+>e7gP74W)uea$mJRqllHN#eyHUFe6AoUuq3YPdIdW>yVUwMn znrzH*Sa=}iVODeVZ=;Tjs>CHempP^`@x-JjbqG zV=3{PeR_TKVei3>DtUQ%I|ahV;Ifvq{5k`h+m(g%vyK|Yh4gr*Kw;FkR#p=PmCI+x zDVB;pOG4ABu9}BE(Tlw@VK&3NPM$r!v;M?=2kPwbr2JxtZs`CmQ1L!BRv|{iBL2*o z!3-@vzpF+eBCpS?`1@*BPKjgAmTn;YVK29HeZ!nhJ9e=D!G;s$qH8&hDHYw$j=G6a zkAfb)!^Lh~Qz_rZjwW42RQc=wtf?uwP%(90HKtxgcCWf#h9UvS4%=2xs-E!5MWq?* zmGj3@fsF z;a2NG<>Q0nUA2~v@-OjU=6@{bPFC97Vc2NO9X0bwGxx2Xy8p|3^R#=!u@A*&>vMbA zK!29sDb)@6q=O2o56}0WfNydRJ2=5@T1Qj7qdk7Pw6ae+c^$l|loN1m2TVz7QRqd1 zyC0Y=FE6{?E-!7~t$ESVXfWLS_Cjar81MCqEQQn2HFpCQ^_epa^)(E7Ql?VU_fbHV z)0K{Nrb*8e!!8_}Vd(yK* zAM_L-#PbFVZ8X7xLq~ROR)LjP^5V1Wg(cVL>h2yHs{J+n0+U6Wx^5EVOst!GT<`N_ zbnDCR?r$-XxTaINn)f5(XA_!R+G8&l=%{}6iuhZn*TKP*qKIk9>w<$5K*M(eOU}B9 z9FOV7mObCp%nhDzJND+qy9HI9>gdxbnmo=e{DO?98hzUI<4Kfz$6HULVkYdzQ7?S{ zKzhnOa_{3G@`y?|L`R_RyPtL8%;U0h&gkR^)wKmJTD4dsYJclx_ z(acD6AHy!4?K+;Q_LR+gy!OecMUU!FZBF~{YmbN4!QretFGYxGH(&c<8&$@29@tL!6xsuH~wb-rH1iY|FK zF+7%hEBj(%Wn|%zmKK68r$0h?gt=0Bv9+MUEXR`mA5=lq^HbmRD)YcrF9>15P& ze}bNEoJGj7RH^M$dFMk)K=X-_n6i>1@r4h*YD5>0mUouYmohF24k+i{(~c!DrQL;n z9i0oL4w!4as+#QN%ryx?{<$bAna47|KFM16x>8vU#!#{Y$Jb0g;IQgbzgD0%=7XsB zwWW4avUS+4o<$++;fIrMGPTyVTWh2JcQ?qz#8#`6DXMgsFBs$;F5a#VBt;(vl+07| h(5F)u$Cnb96lO;4@UL7c0Dy!50Qvv`0D$NK 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 h(5F)u$Cnb96lO;4@UL7c0Dy!50Qvv`0D$NK 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 P0000<2SrXqu0mjfW!hFm literal 2723 zcmbVO3pkYN9v_#nC?b*MGDa7-nQtzPnPFTqVq|jJskBbzo0)IMFgG*9jLX&#$7-Xb zQj$Y?4Z~rxdAByOpRcdc^rgcy{;coPExI&+~ro`+o25{r&I%=gW;) z8*E~1V~ju`Oc){bD0oI`hoJ%dmZzqEfF~nq$VNE=G0#FfbP&ZQ)(C|Dpnw&vh-QXS zIT8^T*3`|bVm_z1R{VV0t6xkN2Yp`s6;Q+%!7u-$+$df6n*uK zFnC8p$0-z2DgY>zO01HImB{!2o7twN7iWCB(Ah{$- z#({#8VKtq!)<~&=G6+;iWGsn9I2);mS(Q-)4=f4gyg?x5N|bV!-&}y`paPE8wMk35q9DJ;=X-hURF)3USdhp9O!}1D|P?aS%-UKf?VPCYSION>By` z@L_NLX*vP0@_@DuKidKP(ZuPq-_-0Z9Bu^KUvEjbt z>&v|O1056mf%XzPecd>4ed0uq4rSfMVCVeSb&HM03rutCgFu(7+5UF8`L?Ua+pT+a zDbM()$fUI0CZ`s#X$N>Huc#fk&OJ`g_lY#UDY5#eWA2fc+>gj=C_59_{F0dXUE8vK z@nuWOAG(A)UNr4bj2V(u;Ib!Ou3JeC_@&%8Ty*s-$azuw!73M@5a+bC7kxdpCv(C~ zhb(l53=G^(yI2?ISBV(UY|BnM)9;20CwPn_jsr-G_r|_csO56YSRLn>4r}xedptX# zO471-V*h{Co~i?1-fH~&P#3F|Z#&^>J4|ld*=D96uYPs-UE-pe){9+T3|E`Q-nZlC zzxX*s=fb1>cKhSeH``cN>~fOvM$Y^MHW@XvG?S`vkJVX~bSD6Hx^BgxLvdkE_s1~t z?R^u>ed=h`rkAfvi&`z?fqx#t=>|t`IwN? z_*(4l@l&?Di$?m5>#IKc13`=PQFr4ELF)nfq-BP#_eY?u;AVBGaqbQSbp}8qi;a$+ zQ?nw^*^O;pqDK7_&o9jL?;}n19B4N3S|PMyT_L7p)%O)6l}))GOPRbwPq*&Xw%vlPoibC{Mj( zDXAS9jY)us?~Su{jV71UeQDu`YWN1dYS(6_Rqfu3Fx3947`M|OFcB!Gab`}LERIJ$ZPOY4O*3|E5>8G4C zWyKRIF7}hYeYsoveJ#68N_MeX)VADdv7gX8W7z!H z2$qfL2hNOet&fg;pqqE9iaSviHTN^%>MHR12 zo^Um6C`xO1&m4?fia&VgG{r{UU_Mw%D7>n4-qgZbW98N>NYlM_biGt48dYXi?-_2M z3LB%g~>lSSw*hof_HmYmAnqTVR3se zFpO5~to*_AZ*KQ4m*BsBvn{NlkACTfX*uIsbhq$Ji2d-zjEy!JPe5wwswF Fe*iRDIduR4 diff --git a/public/images/os/windows-server-2003.png b/public/images/os/windows-server-2003.png index cd2db79e40aa9d3434ca2bac727d692aee997142..4a899a30f8ae88d3cca51b71c3d7d82887c14882 100644 GIT binary patch delta 2397 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 P0000<2SrXqu0mjfW!hFm literal 2723 zcmbVO3pkYN9v_#nC?b*MGDa7-nQtzPnPFTqVq|jJskBbzo0)IMFgG*9jLX&#$7-Xb zQj$Y?4Z~rxdAByOpRcdc^rgcy{;coPExI&+~ro`+o25{r&I%=gW;) z8*E~1V~ju`Oc){bD0oI`hoJ%dmZzqEfF~nq$VNE=G0#FfbP&ZQ)(C|Dpnw&vh-QXS zIT8^T*3`|bVm_z1R{VV0t6xkN2Yp`s6;Q+%!7u-$+$df6n*uK zFnC8p$0-z2DgY>zO01HImB{!2o7twN7iWCB(Ah{$- z#({#8VKtq!)<~&=G6+;iWGsn9I2);mS(Q-)4=f4gyg?x5N|bV!-&}y`paPE8wMk35q9DJ;=X-hURF)3USdhp9O!}1D|P?aS%-UKf?VPCYSION>By` z@L_NLX*vP0@_@DuKidKP(ZuPq-_-0Z9Bu^KUvEjbt z>&v|O1056mf%XzPecd>4ed0uq4rSfMVCVeSb&HM03rutCgFu(7+5UF8`L?Ua+pT+a zDbM()$fUI0CZ`s#X$N>Huc#fk&OJ`g_lY#UDY5#eWA2fc+>gj=C_59_{F0dXUE8vK z@nuWOAG(A)UNr4bj2V(u;Ib!Ou3JeC_@&%8Ty*s-$azuw!73M@5a+bC7kxdpCv(C~ zhb(l53=G^(yI2?ISBV(UY|BnM)9;20CwPn_jsr-G_r|_csO56YSRLn>4r}xedptX# zO471-V*h{Co~i?1-fH~&P#3F|Z#&^>J4|ld*=D96uYPs-UE-pe){9+T3|E`Q-nZlC zzxX*s=fb1>cKhSeH``cN>~fOvM$Y^MHW@XvG?S`vkJVX~bSD6Hx^BgxLvdkE_s1~t z?R^u>ed=h`rkAfvi&`z?fqx#t=>|t`IwN? z_*(4l@l&?Di$?m5>#IKc13`=PQFr4ELF)nfq-BP#_eY?u;AVBGaqbQSbp}8qi;a$+ zQ?nw^*^O;pqDK7_&o9jL?;}n19B4N3S|PMyT_L7p)%O)6l}))GOPRbwPq*&Xw%vlPoibC{Mj( zDXAS9jY)us?~Su{jV71UeQDu`YWN1dYS(6_Rqfu3Fx3947`M|OFcB!Gab`}LERIJ$ZPOY4O*3|E5>8G4C zWyKRIF7}hYeYsoveJ#68N_MeX)VADdv7gX8W7z!H z2$qfL2hNOet&fg;pqqE9iaSviHTN^%>MHR12 zo^Um6C`xO1&m4?fia&VgG{r{UU_Mw%D7>n4-qgZbW98N>NYlM_biGt48dYXi?-_2M z3LB%g~>lSSw*hof_HmYmAnqTVR3se zFpO5~to*_AZ*KQ4m*BsBvn{NlkACTfX*uIsbhq$Ji2d-zjEy!JPe5wwswF Fe*iRDIduR4 diff --git a/public/images/os/windows-vista.png b/public/images/os/windows-vista.png index cd2db79e40aa9d3434ca2bac727d692aee997142..4a899a30f8ae88d3cca51b71c3d7d82887c14882 100644 GIT binary patch delta 2397 zcmV-j38MC+75fs9BufNmK}|sb0I`n?{9y$E001CkNK#Dz0D2|>0Dy!50Qvv`0D$NK z0Cg|`0P0`>06Lfe02gqax=}m;000JJOGiWi{{a60|De66laV18e*+qNR9JLUVRs;K za&Km7Y-J#Hd2nSQcx`Y107!|&R!NfEFburo6?p;$kbw9c*^+Ll$}!JRH|SNivmP?4 zp=1(lG*(i+fBhz3@Hc5KVrSc9j^dM>pS(rY;_A)AyKlCtZu{wU+Ep>%y>h07wYplV zYm{n`oxMw)V+m|*e-=yKT6MeuXogLyw%ew~&N15(vty&LROXWw{`RwPVO$ex0GsMN zdDt=*ZsN)04uiQ9chuVsEp4uAE21Q~M^0JS%_d#Jr{IBB3(omuGmNVE)y(vac{To< z?s#)l!3)+JbVA56nuV6bqZb1eJRsAw*6Pp3$z_oS&Qb9ce_LSC9{HzRKnk45IiPv6 z&L^2DnP6OrA@2CFK!oU%m|+++aQ&GDC?Hq}g?dk4b@&Jp*L8uT75d;>vKp>^j0Ro& z$~Tp5_aNye$e!I-m@Is-HS4Go=BDe$YlGyPsd`>%~`+byJ}kDX(+O=D`g}Yt#Ng zMXPnZsbJLIwbmlX-YSZKb%&D8y7RG`Gvx_(b*4;re~(UBdB*(!I{|V*_0OMSW%L~% zRHs+UzY8r_>@0!BJu`hJTn4)H8*R5`GH$2=@_2DIfY{cME!cjsda!wjWTxg)*&i&~ zZy9!YhLS^udDOF|FX#~Jh@X)gIs*G78w93Tj14QCo7gA857h`09i1IYt)d^T43e#3;emZ^=$Stcb;T3+Dt?iElM3==_dle9e2l~cg9zkr8(cpXj{Kt5AK_R7 z_yX)i!)ulfBT5DG^LF*iZgsh>uxG!CN;cb{f5uYx;|VO!NAOW>To>-}$6L}^;Z;oz zceO_98A%=2EcpR}x<%jftVGTL00mb`L_t(o!{wJ-Y*bYghQGDXrJYV&Mp^}m$jyK# zF;FyWf|%Y=Gz5tkUeH8iygc|of-!1h)cBy$L`7qw4;s~oXMPh)?Vvh|GI2ZRsPFm_>Y%d$$vXHMSjI> zAv@3_5%($FjxZCb0{VdyDBl^whahLYbQ$NT_OgEEF(j%{N1xg35-B4 zcvAWT!Hlg<;Yk8H)HyBlL0>hbGJqG;0H>$7M4N&!$;fpZ;v5c8mKVZ_7r*`S)-FTD zYbN4tJpM)s>Pn0)pj>#Q453mg8tP(HYvR&a(r#O4P2ZWWn&6H^mX|F2*&nhNe`Qjs z3j!6uT~^r;Mr?6yW#x!18s%HiaLCRv@JHQkXQ%d)FK2lo4Ute82E1hqZ$)ra;3R@t z%M@T9LV4bTFMQ&fbmnqp0=B|yP-8~oHyQwuKr+|bJ|m6I(TEhxmZ1c{p(rD0<_Mcy z>-(h~xKW{gC|duP#Ng%J^WBwwf4kdjbG20gw`vUBihD95>@s4MK@kPtG-6Dw^A&*{ zJG*CRNT*mKxmZEFM4jCZTyP1(VO2>{aa-`5q5*RRP!U{B3wl>oMM@d~(J!KTW&$-2TcroJmY!!1bvO)71AKV`=*yDtZn* z;#StJCp-B_#|`H*U6X=46TQSrTP+~>i@*^JxbNQbI*Q%Z00^ zHQ}=up$-rX9ay#{{_5WQf9#2`K7ZxVJ9`s-$FFOj;*ocFup(wtRlpIp8wCppdxw1) zvI=1hd2S=<2OC@-IPoA+(L<6KrycG3_>JC|`Q!eNpp)+>W@)nS*aldj%&^RbEro1o zy?E)}%}CS+`tKQ_#Uyg8cao%W{jBRr_8j+r`I~h~!&8YXcpU;+e+VP^z>8Qj*b4~r zgVxXGT6u?M~>e_IoWFxJiR~Qxo+h#6`BoPi(*vhag+lomoFAte!VV) zpom1lfJ>ElK$aGR5@R_4Up?S|0}k&K(XcpR)gp(lPr$lP%ep=VN1@8V)xfnDl9pF- zIE-P#5bqCUA*6=xf9Nz3$yhUzt;YA$RVS~yF|1sK{ch1unI1HN5CLX+{@uY#~|iL)L` zF1Pp9Pyhe`C3HntbYx+4WjbSWWnpw>05UK#Gc7PUEif@uGBY|gIXW{mD=;uRFfcg_ zOT7R903~!qSaf7zbY(hiZ)9m^c>ppnGBYhOI4v+SR5CL P0000<2SrXqu0mjfW!hFm literal 2723 zcmbVO3pkYN9v_#nC?b*MGDa7-nQtzPnPFTqVq|jJskBbzo0)IMFgG*9jLX&#$7-Xb zQj$Y?4Z~rxdAByOpRcdc^rgcy{;coPExI&+~ro`+o25{r&I%=gW;) z8*E~1V~ju`Oc){bD0oI`hoJ%dmZzqEfF~nq$VNE=G0#FfbP&ZQ)(C|Dpnw&vh-QXS zIT8^T*3`|bVm_z1R{VV0t6xkN2Yp`s6;Q+%!7u-$+$df6n*uK zFnC8p$0-z2DgY>zO01HImB{!2o7twN7iWCB(Ah{$- z#({#8VKtq!)<~&=G6+;iWGsn9I2);mS(Q-)4=f4gyg?x5N|bV!-&}y`paPE8wMk35q9DJ;=X-hURF)3USdhp9O!}1D|P?aS%-UKf?VPCYSION>By` z@L_NLX*vP0@_@DuKidKP(ZuPq-_-0Z9Bu^KUvEjbt z>&v|O1056mf%XzPecd>4ed0uq4rSfMVCVeSb&HM03rutCgFu(7+5UF8`L?Ua+pT+a zDbM()$fUI0CZ`s#X$N>Huc#fk&OJ`g_lY#UDY5#eWA2fc+>gj=C_59_{F0dXUE8vK z@nuWOAG(A)UNr4bj2V(u;Ib!Ou3JeC_@&%8Ty*s-$azuw!73M@5a+bC7kxdpCv(C~ zhb(l53=G^(yI2?ISBV(UY|BnM)9;20CwPn_jsr-G_r|_csO56YSRLn>4r}xedptX# zO471-V*h{Co~i?1-fH~&P#3F|Z#&^>J4|ld*=D96uYPs-UE-pe){9+T3|E`Q-nZlC zzxX*s=fb1>cKhSeH``cN>~fOvM$Y^MHW@XvG?S`vkJVX~bSD6Hx^BgxLvdkE_s1~t z?R^u>ed=h`rkAfvi&`z?fqx#t=>|t`IwN? z_*(4l@l&?Di$?m5>#IKc13`=PQFr4ELF)nfq-BP#_eY?u;AVBGaqbQSbp}8qi;a$+ zQ?nw^*^O;pqDK7_&o9jL?;}n19B4N3S|PMyT_L7p)%O)6l}))GOPRbwPq*&Xw%vlPoibC{Mj( zDXAS9jY)us?~Su{jV71UeQDu`YWN1dYS(6_Rqfu3Fx3947`M|OFcB!Gab`}LERIJ$ZPOY4O*3|E5>8G4C zWyKRIF7}hYeYsoveJ#68N_MeX)VADdv7gX8W7z!H z2$qfL2hNOet&fg;pqqE9iaSviHTN^%>MHR12 zo^Um6C`xO1&m4?fia&VgG{r{UU_Mw%D7>n4-qgZbW98N>NYlM_biGt48dYXi?-_2M z3LB%g~>lSSw*hof_HmYmAnqTVR3se zFpO5~to*_AZ*KQ4m*BsBvn{NlkACTfX*uIsbhq$Ji2d-zjEy!JPe5wwswF Fe*iRDIduR4 diff --git a/public/images/os/windows-xp.png b/public/images/os/windows-xp.png index cd2db79e40aa9d3434ca2bac727d692aee997142..4a899a30f8ae88d3cca51b71c3d7d82887c14882 100644 GIT binary patch delta 2397 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 P0000<2SrXqu0mjfW!hFm literal 2723 zcmbVO3pkYN9v_#nC?b*MGDa7-nQtzPnPFTqVq|jJskBbzo0)IMFgG*9jLX&#$7-Xb zQj$Y?4Z~rxdAByOpRcdc^rgcy{;coPExI&+~ro`+o25{r&I%=gW;) z8*E~1V~ju`Oc){bD0oI`hoJ%dmZzqEfF~nq$VNE=G0#FfbP&ZQ)(C|Dpnw&vh-QXS zIT8^T*3`|bVm_z1R{VV0t6xkN2Yp`s6;Q+%!7u-$+$df6n*uK zFnC8p$0-z2DgY>zO01HImB{!2o7twN7iWCB(Ah{$- z#({#8VKtq!)<~&=G6+;iWGsn9I2);mS(Q-)4=f4gyg?x5N|bV!-&}y`paPE8wMk35q9DJ;=X-hURF)3USdhp9O!}1D|P?aS%-UKf?VPCYSION>By` z@L_NLX*vP0@_@DuKidKP(ZuPq-_-0Z9Bu^KUvEjbt z>&v|O1056mf%XzPecd>4ed0uq4rSfMVCVeSb&HM03rutCgFu(7+5UF8`L?Ua+pT+a zDbM()$fUI0CZ`s#X$N>Huc#fk&OJ`g_lY#UDY5#eWA2fc+>gj=C_59_{F0dXUE8vK z@nuWOAG(A)UNr4bj2V(u;Ib!Ou3JeC_@&%8Ty*s-$azuw!73M@5a+bC7kxdpCv(C~ zhb(l53=G^(yI2?ISBV(UY|BnM)9;20CwPn_jsr-G_r|_csO56YSRLn>4r}xedptX# zO471-V*h{Co~i?1-fH~&P#3F|Z#&^>J4|ld*=D96uYPs-UE-pe){9+T3|E`Q-nZlC zzxX*s=fb1>cKhSeH``cN>~fOvM$Y^MHW@XvG?S`vkJVX~bSD6Hx^BgxLvdkE_s1~t z?R^u>ed=h`rkAfvi&`z?fqx#t=>|t`IwN? z_*(4l@l&?Di$?m5>#IKc13`=PQFr4ELF)nfq-BP#_eY?u;AVBGaqbQSbp}8qi;a$+ zQ?nw^*^O;pqDK7_&o9jL?;}n19B4N3S|PMyT_L7p)%O)6l}))GOPRbwPq*&Xw%vlPoibC{Mj( zDXAS9jY)us?~Su{jV71UeQDu`YWN1dYS(6_Rqfu3Fx3947`M|OFcB!Gab`}LERIJ$ZPOY4O*3|E5>8G4C zWyKRIF7}hYeYsoveJ#68N_MeX)VADdv7gX8W7z!H z2$qfL2hNOet&fg;pqqE9iaSviHTN^%>MHR12 zo^Um6C`xO1&m4?fia&VgG{r{UU_Mw%D7>n4-qgZbW98N>NYlM_biGt48dYXi?-_2M z3LB%g~>lSSw*hof_HmYmAnqTVR3se zFpO5~to*_AZ*KQ4m*BsBvn{NlkACTfX*uIsbhq$Ji2d-zjEy!JPe5wwswF Fe*iRDIduR4 diff --git a/public/mstile-150x150.png b/public/mstile-150x150.png index 7f73bcd84d4b9eb920a14f92d1eaea37a6480de4..ae4fd79c1c12b638413ad613966d7a7e26663198 100644 GIT binary patch literal 2602 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 diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg index 179f3e69b..2d116eb84 100644 --- a/public/safari-pinned-tab.svg +++ b/public/safari-pinned-tab.svg @@ -1,75 +1 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - +Created by potrace 1.11, written by Peter Selinger 2001-2013 \ No newline at end of file From 7b9c29e039cae638475047386d814138237cf3b4 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 2 Aug 2023 11:56:42 -0700 Subject: [PATCH 005/357] Check for DISABLE_LOGIN on api route. --- pages/api/auth/login.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts index af2069387..b9a2be000 100644 --- a/pages/api/auth/login.ts +++ b/pages/api/auth/login.ts @@ -7,6 +7,7 @@ import { checkPassword, createSecureToken, methodNotAllowed, + forbidden, } from 'next-basics'; import redis from '@umami/redis-client'; import { getUserByUsername } from 'queries'; @@ -30,6 +31,10 @@ export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { + if (process.env.DISABLE_LOGIN) { + return forbidden(res); + } + if (req.method === 'POST') { const { username, password } = req.body; From 4497951000065e9cc57ac8556bcc5a1bf04087ea Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 2 Aug 2023 14:21:13 -0700 Subject: [PATCH 006/357] Split out session query. --- components/layout/AppLayout.js | 6 +- lib/clickhouse.ts | 7 +- lib/prisma.ts | 22 ++--- pages/api/websites/[id]/metrics.ts | 4 +- pages/api/websites/[id]/pageviews.ts | 6 +- .../analytics/pageviews/getPageviewMetrics.ts | 3 +- .../analytics/pageviews/getPageviewStats.ts | 41 ++++---- .../analytics/sessions/getSessionMetrics.ts | 8 +- queries/analytics/sessions/getSessionStats.ts | 98 +++++++++++++++++++ 9 files changed, 139 insertions(+), 56 deletions(-) create mode 100644 queries/analytics/sessions/getSessionStats.ts diff --git a/components/layout/AppLayout.js b/components/layout/AppLayout.js index 989128f9e..7ab74351b 100644 --- a/components/layout/AppLayout.js +++ b/components/layout/AppLayout.js @@ -2,9 +2,7 @@ import { Container } from 'react-basics'; import Head from 'next/head'; import NavBar from 'components/layout/NavBar'; import UpdateNotice from 'components/common/UpdateNotice'; -import useRequireLogin from 'hooks/useRequireLogin'; -import useConfig from 'hooks/useConfig'; -import { CURRENT_VERSION } from 'lib/constants'; +import { useRequireLogin, useConfig } from 'hooks'; import styles from './AppLayout.module.css'; export function AppLayout({ title, children }) { @@ -16,7 +14,7 @@ export function AppLayout({ title, children }) { } return ( -

+
{title ? `${title} | umami` : 'umami'} diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index b3dc2c48b..d294110c9 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -61,14 +61,13 @@ function getDateFormat(date) { return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`; } -function getFilterQuery(filters = {}, params = {}) { +function getFilterQuery(filters = {}) { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; if (filter !== undefined) { const column = FILTER_COLUMNS[key] || key; arr.push(`and ${column} = {${key}:String}`); - params[key] = decodeURIComponent(filter); } return arr; @@ -77,9 +76,9 @@ function getFilterQuery(filters = {}, params = {}) { return query.join('\n'); } -function parseFilters(filters: WebsiteMetricFilter = {}, params: any = {}) { +function parseFilters(filters: WebsiteMetricFilter = {}) { return { - filterQuery: getFilterQuery(filters, params), + filterQuery: getFilterQuery(filters), }; } diff --git a/lib/prisma.ts b/lib/prisma.ts index 08309e311..0a52dd7e8 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -1,7 +1,7 @@ import prisma from '@umami/prisma-client'; import moment from 'moment-timezone'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; -import { FILTER_COLUMNS } from './constants'; +import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -64,14 +64,13 @@ function getTimestampIntervalQuery(field: string): string { } } -function getFilterQuery(filters = {}, params = []): string { +function getFilterQuery(filters = {}): string { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; if (filter !== undefined) { const column = FILTER_COLUMNS[key] || key; arr.push(`and ${column}={{${key}}}`); - params.push(decodeURIComponent(filter)); } return arr; @@ -80,19 +79,12 @@ function getFilterQuery(filters = {}, params = []): string { return query.join('\n'); } -function parseFilters( - filters: { [key: string]: any } = {}, - params = [], - sessionKey = 'session_id', -) { - const { os, browser, device, country, region, city } = filters; - +function parseFilters(filters: { [key: string]: any } = {}) { return { - joinSession: - os || browser || device || country || region || city - ? `inner join session on website_event.${sessionKey} = session.${sessionKey}` - : '', - filterQuery: getFilterQuery(filters, params), + joinSession: Object.keys(filters).find(key => SESSION_COLUMNS[key]) + ? `inner join session on website_event.session_id = session.session_id` + : '', + filterQuery: getFilterQuery(filters), }; } diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index 37a046916..fa0b75541 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -68,7 +68,7 @@ export default async ( filters[type] = undefined; - let data = await getSessionMetrics(websiteId, { + const data = await getSessionMetrics(websiteId, { startDate, endDate, column, @@ -88,7 +88,7 @@ export default async ( } } - data = Object.values(combined); + return ok(res, Object.values(combined)); } return ok(res, data); diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index 453c6733c..810854a75 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -6,6 +6,7 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { getPageviewStats } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import { getSessionStats } from '../../../../queries/analytics/sessions/getSessionStats'; export interface WebsitePageviewRequestQuery { id: string; @@ -62,7 +63,6 @@ export default async ( endDate, timezone, unit, - count: '*', filters: { url, referrer, @@ -75,14 +75,14 @@ export default async ( city, }, }), - getPageviewStats(websiteId, { + getSessionStats(websiteId, { startDate, endDate, timezone, unit, - count: 'distinct website_event.', filters: { url, + referrer, title, os, browser, diff --git a/queries/analytics/pageviews/getPageviewMetrics.ts b/queries/analytics/pageviews/getPageviewMetrics.ts index 1032540b8..a5da178af 100644 --- a/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/queries/analytics/pageviews/getPageviewMetrics.ts @@ -84,6 +84,7 @@ async function clickhouseQuery( const { rawQuery, parseFilters } = clickhouse; const website = await loadWebsite(websiteId); const params = { + ...filters, websiteId, startDate: maxDate(startDate, website.resetAt), endDate, @@ -98,7 +99,7 @@ async function clickhouseQuery( params.domain = website.domain; } - const { filterQuery } = parseFilters(filters, params); + const { filterQuery } = parseFilters(filters); return rawQuery( ` diff --git a/queries/analytics/pageviews/getPageviewStats.ts b/queries/analytics/pageviews/getPageviewStats.ts index f6d4158c9..6d702993b 100644 --- a/queries/analytics/pageviews/getPageviewStats.ts +++ b/queries/analytics/pageviews/getPageviewStats.ts @@ -10,9 +10,19 @@ export interface PageviewStatsCriteria { endDate: Date; timezone?: string; unit?: string; - count?: string; - filters: object; - sessionKey?: string; + filters: { + url?: string; + referrer?: string; + title?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; + region?: string; + city?: string; + }; } export async function getPageviewStats( @@ -25,15 +35,7 @@ export async function getPageviewStats( } async function relationalQuery(websiteId: string, criteria: PageviewStatsCriteria) { - const { - startDate, - endDate, - timezone = 'utc', - unit = 'day', - count = '*', - filters = {}, - sessionKey = 'session_id', - } = criteria; + const { startDate, endDate, timezone = 'utc', unit = 'day', filters = {} } = criteria; const { getDateQuery, parseFilters, rawQuery } = prisma; const website = await loadWebsite(websiteId); const { filterQuery, joinSession } = parseFilters(filters); @@ -42,7 +44,7 @@ async function relationalQuery(websiteId: string, criteria: PageviewStatsCriteri ` select ${getDateQuery('website_event.created_at', unit, timezone)} x, - count(${count !== '*' ? `${count}${sessionKey}` : count}) y + count(*) y from website_event ${joinSession} where website_event.website_id = {{websiteId::uuid}} @@ -52,24 +54,17 @@ async function relationalQuery(websiteId: string, criteria: PageviewStatsCriteri group by 1 `, { + ...filters, websiteId, startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.pageView, - ...filters, }, ); } async function clickhouseQuery(websiteId: string, criteria: PageviewStatsCriteria) { - const { - startDate, - endDate, - timezone = 'UTC', - unit = 'day', - count = '*', - filters = {}, - } = criteria; + const { startDate, endDate, timezone = 'UTC', unit = 'day', filters = {} } = criteria; const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; const website = await loadWebsite(websiteId); const { filterQuery } = parseFilters(filters); @@ -82,7 +77,7 @@ async function clickhouseQuery(websiteId: string, criteria: PageviewStatsCriteri from ( select ${getDateQuery('created_at', unit, timezone)} as t, - count(${count !== '*' ? 'distinct session_id' : count}) as y + count(*) as y from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} diff --git a/queries/analytics/sessions/getSessionMetrics.ts b/queries/analytics/sessions/getSessionMetrics.ts index aec2d8f13..2512b06ce 100644 --- a/queries/analytics/sessions/getSessionMetrics.ts +++ b/queries/analytics/sessions/getSessionMetrics.ts @@ -28,8 +28,8 @@ async function relationalQuery( return rawQuery( `select ${column} x, count(*) y - from session as x - where x.session_id in ( + from session as s + where s.session_id in ( select website_event.session_id from website_event join website @@ -38,7 +38,7 @@ async function relationalQuery( where website.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} ${filterQuery} - ) + ) as t group by 1 order by 2 desc limit 100`, @@ -64,7 +64,7 @@ async function clickhouseQuery( ` select ${column} x, count(distinct session_id) y - from website_event as x + from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} and event_type = {eventType:UInt32} diff --git a/queries/analytics/sessions/getSessionStats.ts b/queries/analytics/sessions/getSessionStats.ts new file mode 100644 index 000000000..966fd91ff --- /dev/null +++ b/queries/analytics/sessions/getSessionStats.ts @@ -0,0 +1,98 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; +import { EVENT_TYPE } from 'lib/constants'; +import { loadWebsite } from 'lib/load'; +import { maxDate } from 'lib/date'; + +export interface SessionStatsCriteria { + startDate: Date; + endDate: Date; + timezone?: string; + unit?: string; + filters: { + url?: string; + referrer?: string; + title?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; + region?: string; + city?: string; + }; +} + +export async function getSessionStats( + ...args: [websiteId: string, criteria: SessionStatsCriteria] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, criteria: SessionStatsCriteria) { + const { startDate, endDate, timezone = 'utc', unit = 'day', filters = {} } = criteria; + const { getDateQuery, parseFilters, rawQuery } = prisma; + const website = await loadWebsite(websiteId); + const { filterQuery, joinSession } = parseFilters(filters); + + return rawQuery( + ` + select + ${getDateQuery('website_event.created_at', unit, timezone)} x, + count(distinct website_event.session_id) y + from website_event + ${joinSession} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and event_type = {{eventType}} + ${filterQuery} + group by 1 + `, + { + ...filters, + websiteId, + startDate: maxDate(startDate, website.resetAt), + endDate, + eventType: EVENT_TYPE.pageView, + }, + ); +} + +async function clickhouseQuery(websiteId: string, criteria: SessionStatsCriteria) { + const { startDate, endDate, timezone = 'UTC', unit = 'day', filters = {} } = criteria; + const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; + const website = await loadWebsite(websiteId); + const { filterQuery } = parseFilters(filters); + + return rawQuery( + ` + select + ${getDateStringQuery('g.t', unit)} as x, + g.y as y + from ( + select + ${getDateQuery('created_at', unit, timezone)} as t, + count(distinct session_id) as y + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime} and {endDate:DateTime} + and event_type = {eventType:UInt32} + ${filterQuery} + group by t + ) as g + order by t + `, + { + ...filters, + websiteId, + startDate: maxDate(startDate, website.resetAt), + endDate, + eventType: EVENT_TYPE.pageView, + }, + ); +} From 2d04e46dedbaac8c968c9829a6386f69f35f4206 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 3 Aug 2023 10:44:35 -0700 Subject: [PATCH 007/357] Insights progress update. --- components/pages/reports/ReportTemplates.js | 2 - .../pages/reports/insights/InsightsTable.js | 6 +- queries/analytics/reports/getInsights.ts | 84 ++++++++++++++++++- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/components/pages/reports/ReportTemplates.js b/components/pages/reports/ReportTemplates.js index 60ae11e79..c1e0acdf3 100644 --- a/components/pages/reports/ReportTemplates.js +++ b/components/pages/reports/ReportTemplates.js @@ -33,14 +33,12 @@ export function ReportTemplates() { const { formatMessage, labels } = useMessages(); const reports = [ - /* { title: formatMessage(labels.insights), description: 'Dive deeper into your data by using segments and filters.', url: '/reports/insights', icon: , }, - */ { title: formatMessage(labels.funnel), description: 'Understand the conversion and drop-off rate of users.', diff --git a/components/pages/reports/insights/InsightsTable.js b/components/pages/reports/insights/InsightsTable.js index a767468e4..d751445b9 100644 --- a/components/pages/reports/insights/InsightsTable.js +++ b/components/pages/reports/insights/InsightsTable.js @@ -6,11 +6,13 @@ import { ReportContext } from '../Report'; export function InsightsTable() { const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); + const { fields = [] } = report?.parameters || {}; return ( - - + {fields.map(({ name }) => { + return ; + })} ); diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index 1d8970ed0..68f06e218 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -1,11 +1,14 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; +import { maxDate } from 'lib/date'; +import { EVENT_TYPE } from 'lib/constants'; +import { loadWebsite } from 'lib/load'; export interface GetInsightsCriteria { startDate: Date; endDate: Date; - fields: string[]; + fields: { name: string; type: string; value: string }[]; filters: string[]; groups: string[]; } @@ -26,7 +29,33 @@ async function relationalQuery( y: number; }[] > { - return null; + const { startDate, endDate, fields = [], filters = [], groups = [] } = criteria; + const { parseFilters, rawQuery } = prisma; + const website = await loadWebsite(websiteId); + const params = {}; + const { filterQuery, joinSession } = parseFilters(params); + + return rawQuery( + ` + select + url_path, + count(*) y + from website_event + ${joinSession} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type = {{eventType}} + ${filterQuery} + group by 1 + `, + { + ...filters, + websiteId, + startDate: maxDate(startDate, website.resetAt), + endDate, + eventType: EVENT_TYPE.pageView, + }, + ); } async function clickhouseQuery( @@ -38,5 +67,54 @@ async function clickhouseQuery( y: number; }[] > { - return null; + const { startDate, endDate, fields = [], filters = [], groups = [] } = criteria; + const { parseFilters, rawQuery } = clickhouse; + const website = await loadWebsite(websiteId); + const params = {}; + const { filterQuery } = parseFilters(params); + + const fieldsQuery = parseFields(fields); + + return rawQuery( + ` + select + ${fieldsQuery} + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime} and {endDate:DateTime} + and event_type = {eventType:UInt32} + ${filterQuery} + group by ${fields.map(({ name }) => name).join(',')} + order by total desc + limit 500 + `, + { + ...filters, + websiteId, + startDate: maxDate(startDate, website.resetAt), + endDate, + eventType: EVENT_TYPE.pageView, + }, + ); +} + +function parseFields(fields) { + let count = false; + let distinct = false; + + const query = fields.reduce((arr, field) => { + const { name, value } = field; + + if (!count && value === 'total') { + count = true; + arr = arr.concat(`count(*) as total`); + } else if (!distinct && value === 'unique') { + distinct = true; + //arr = arr.concat(`count(distinct ${name})`); + } + + return arr.concat(name); + }, []); + + return query.join(',\n'); } From 157862834d810e7036913e2c7e2710beab88e72b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 4 Aug 2023 00:51:52 -0700 Subject: [PATCH 008/357] Refactored query filters. --- components/pages/reports/FieldSelectForm.js | 2 +- .../pages/reports/funnel/FunnelParameters.js | 7 ++- components/pages/reports/funnel/UrlAddForm.js | 40 +++++++-------- .../reports/insights/InsightsParameters.js | 36 +++++++++---- lib/clickhouse.ts | 4 ++ lib/constants.ts | 41 ++------------- lib/prisma.ts | 6 +++ pages/api/reports/insights.ts | 2 +- pages/api/websites/[id]/metrics.ts | 49 ++++++++---------- queries/analytics/events/getEventMetrics.ts | 4 +- .../analytics/pageviews/getPageviewMetrics.ts | 51 ++++++------------- .../analytics/pageviews/getPageviewStats.ts | 2 + queries/analytics/reports/getInsights.ts | 4 +- .../analytics/sessions/getSessionMetrics.ts | 11 ++-- queries/analytics/sessions/getSessionStats.ts | 2 + queries/analytics/stats/getWebsiteStats.ts | 4 +- 16 files changed, 118 insertions(+), 147 deletions(-) diff --git a/components/pages/reports/FieldSelectForm.js b/components/pages/reports/FieldSelectForm.js index 0e41ea1fb..69f399bbe 100644 --- a/components/pages/reports/FieldSelectForm.js +++ b/components/pages/reports/FieldSelectForm.js @@ -13,7 +13,7 @@ export default function FieldSelectForm({ fields, onSelect }) { return (
{label || name}
-
{type}
+ {type &&
{type}
}
); })} diff --git a/components/pages/reports/funnel/FunnelParameters.js b/components/pages/reports/funnel/FunnelParameters.js index ae4981761..03898db3e 100644 --- a/components/pages/reports/funnel/FunnelParameters.js +++ b/components/pages/reports/funnel/FunnelParameters.js @@ -16,6 +16,7 @@ import UrlAddForm from './UrlAddForm'; import { ReportContext } from 'components/pages/reports/Report'; import BaseParameters from '../BaseParameters'; import ParameterList from '../ParameterList'; +import PopupForm from '../PopupForm'; export function FunnelParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); @@ -53,7 +54,11 @@ export function FunnelParameters() { {(close, element) => { - return ; + return ( + + + + ); }} diff --git a/components/pages/reports/funnel/UrlAddForm.js b/components/pages/reports/funnel/UrlAddForm.js index 0fb78b3d2..ce2021162 100644 --- a/components/pages/reports/funnel/UrlAddForm.js +++ b/components/pages/reports/funnel/UrlAddForm.js @@ -2,16 +2,14 @@ import { useState } from 'react'; import { useMessages } from 'hooks'; import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics'; import styles from './UrlAddForm.module.css'; -import PopupForm from '../PopupForm'; -export function UrlAddForm({ defaultValue = '', element, onAdd, onClose }) { +export function UrlAddForm({ defaultValue = '', onAdd }) { const [url, setUrl] = useState(defaultValue); const { formatMessage, labels } = useMessages(); const handleSave = () => { onAdd(url); setUrl(''); - onClose(); }; const handleChange = e => { @@ -26,25 +24,23 @@ export function UrlAddForm({ defaultValue = '', element, onAdd, onClose }) { }; return ( - -
- - - - - - -
-
+
+ + + + + + +
); } diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js index b87a566da..692c5eadb 100644 --- a/components/pages/reports/insights/InsightsParameters.js +++ b/components/pages/reports/insights/InsightsParameters.js @@ -2,12 +2,28 @@ import { useContext, useRef } from 'react'; import { useMessages } from 'hooks'; import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; import { ReportContext } from 'components/pages/reports/Report'; -import { REPORT_PARAMETERS, WEBSITE_EVENT_FIELDS } from 'lib/constants'; +import { REPORT_PARAMETERS } from 'lib/constants'; import Icons from 'components/icons'; import BaseParameters from '../BaseParameters'; -import FieldAddForm from '../FieldAddForm'; import ParameterList from '../ParameterList'; import styles from './InsightsParameters.module.css'; +import FieldSelectForm from '../FieldSelectForm'; +import PopupForm from '../PopupForm'; +import FieldFilterForm from '../FieldFilterForm'; + +const fieldOptions = [ + { name: 'url', type: 'string' }, + { name: 'title', type: 'string' }, + { name: 'referrer', type: 'string' }, + { name: 'query', type: 'string' }, + { name: 'browser', type: 'string' }, + { name: 'os', type: 'string' }, + { name: 'device', type: 'string' }, + { name: 'country', type: 'string' }, + { name: 'region', type: 'string' }, + { name: 'city', type: 'string' }, + { name: 'language', type: 'string' }, +]; export function InsightsParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); @@ -16,7 +32,6 @@ export function InsightsParameters() { const { parameters } = report || {}; const { websiteId, dateRange, fields, filters, groups } = parameters || {}; const queryEnabled = websiteId && dateRange && fields?.length; - const fieldOptions = Object.keys(WEBSITE_EVENT_FIELDS).map(key => WEBSITE_EVENT_FIELDS[key]); const parameterGroups = [ { label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields }, @@ -57,13 +72,14 @@ export function InsightsParameters() { {(close, element) => { return ( - + + {group === REPORT_PARAMETERS.fields && ( + + )} + {group === REPORT_PARAMETERS.filters && ( + + )} + ); }} diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index d294110c9..19d094054 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -70,6 +70,10 @@ function getFilterQuery(filters = {}) { arr.push(`and ${column} = {${key}:String}`); } + if (key === 'referrer') { + arr.push('and referrer_domain != {domain:String}'); + } + return arr; }, []); diff --git a/lib/constants.ts b/lib/constants.ts index c275ed8d4..2b3da8754 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -43,11 +43,6 @@ export const SESSION_COLUMNS = [ 'city', ]; -export const COLLECTION_TYPE = { - event: 'event', - identify: 'identify', -}; - export const FILTER_COLUMNS = { url: 'url_path', referrer: 'referrer_domain', @@ -57,6 +52,11 @@ export const FILTER_COLUMNS = { region: 'subdivision1', }; +export const COLLECTION_TYPE = { + event: 'event', + identify: 'identify', +}; + export const EVENT_TYPE = { pageView: 1, customEvent: 2, @@ -120,37 +120,6 @@ export const ROLE_PERMISSIONS = { [ROLES.teamMember]: [], } as const; -export const WEBSITE_EVENT_FIELDS = { - eventId: { name: 'event_id', type: 'uuid', label: 'Event ID' }, - websiteId: { name: 'website_id', type: 'uuid', label: 'Website ID' }, - sessionId: { name: 'session_id', type: 'uuid', label: 'Session ID' }, - createdAt: { name: 'created_at', type: 'date', label: 'Created date' }, - urlPath: { name: 'url_path', type: 'string', label: 'URL path' }, - urlQuery: { name: 'url_query', type: 'string', label: 'URL query' }, - referrerPath: { name: 'referrer_path', type: 'string', label: 'Referrer path' }, - referrerQuery: { name: 'referrer_query', type: 'string', label: 'Referrer query' }, - referrerDomain: { name: 'referrer_domain', type: 'string', label: 'Referrer domain' }, - pageTitle: { name: 'page_title', type: 'string', label: 'Page title' }, - eventType: { name: 'event_type', type: 'string', label: 'Event type' }, - eventName: { name: 'event_name', type: 'string', label: 'Event name' }, -}; - -export const SESSION_FIELDS = { - sessionId: { name: 'session_id', type: 'uuid' }, - websiteId: { name: 'website_id', type: 'uuid' }, - hostname: { name: 'hostname', type: 'string' }, - browser: { name: 'browser', type: 'string' }, - os: { name: 'os', type: 'string' }, - device: { name: 'device', type: 'string' }, - screen: { name: 'screen', type: 'string' }, - language: { name: 'language', type: 'string' }, - country: { name: 'country', type: 'string' }, - subdivision1: { name: 'subdivision1', type: 'string' }, - subdivision2: { name: 'subdivision2', type: 'string' }, - city: { name: 'city', type: 'string' }, - createdAt: { name: 'created_at', type: 'date' }, -}; - export const THEME_COLORS = { light: { primary: '#2680eb', diff --git a/lib/prisma.ts b/lib/prisma.ts index 0a52dd7e8..a6f1ff889 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -73,6 +73,12 @@ function getFilterQuery(filters = {}): string { arr.push(`and ${column}={{${key}}}`); } + if (key === 'referrer') { + arr.push( + 'and (website_event.referrer_domain != {{domain}} or website_event.referrer_domain is null)', + ); + } + return arr; }, []); diff --git a/pages/api/reports/insights.ts b/pages/api/reports/insights.ts index dba11953c..a40c21246 100644 --- a/pages/api/reports/insights.ts +++ b/pages/api/reports/insights.ts @@ -11,7 +11,7 @@ export interface InsightsRequestBody { startDate: string; endDate: string; }; - fields: string[]; + fields: { name: string; type: string; value: string }[]; filters: string[]; groups: string[]; } diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index fa0b75541..15389e3ee 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -46,6 +46,7 @@ export default async ( country, region, city, + language, } = req.query; if (req.method === 'GET') { @@ -55,19 +56,26 @@ export default async ( const { startDate, endDate } = await parseDateRangeQuery(req); + const filters = { + url, + referrer, + title, + query, + event, + os, + browser, + device, + country, + region, + city, + language, + }; + + filters[type] = undefined; + + const column = FILTER_COLUMNS[type] || type; + if (SESSION_COLUMNS.includes(type)) { - const column = FILTER_COLUMNS[type] || type; - const filters = { - os, - browser, - device, - country, - region, - city, - }; - - filters[type] = undefined; - const data = await getSessionMetrics(websiteId, { startDate, endDate, @@ -95,23 +103,6 @@ export default async ( } if (EVENT_COLUMNS.includes(type)) { - const column = FILTER_COLUMNS[type] || type; - const filters = { - url, - referrer, - title, - query, - event, - os, - browser, - device, - country, - region, - city, - }; - - filters[type] = undefined; - const data = await getPageviewMetrics(websiteId, { startDate, endDate, diff --git a/queries/analytics/events/getEventMetrics.ts b/queries/analytics/events/getEventMetrics.ts index e97540366..03b252b79 100644 --- a/queries/analytics/events/getEventMetrics.ts +++ b/queries/analytics/events/getEventMetrics.ts @@ -47,11 +47,12 @@ async function relationalQuery(websiteId: string, criteria: GetEventMetricsCrite order by 2 `, { + ...filters, websiteId, startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.customEvent, - ...filters, + domain: website.domain, }, ); } @@ -82,6 +83,7 @@ async function clickhouseQuery(websiteId: string, criteria: GetEventMetricsCrite startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.customEvent, + domain: website.domain, }, ); } diff --git a/queries/analytics/pageviews/getPageviewMetrics.ts b/queries/analytics/pageviews/getPageviewMetrics.ts index a5da178af..8e4460e61 100644 --- a/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/queries/analytics/pageviews/getPageviewMetrics.ts @@ -34,22 +34,6 @@ async function relationalQuery( const { startDate, endDate, filters = {}, column } = criteria; const { rawQuery, parseFilters } = prisma; const website = await loadWebsite(websiteId); - const params: any = { - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, - ...filters, - }; - - let excludeDomain = ''; - - if (column === 'referrer_domain') { - excludeDomain = - 'and (website_event.referrer_domain != {{domain}} or website_event.referrer_domain is null)'; - - params.domain = website.domain; - } const { filterQuery, joinSession } = parseFilters(filters); @@ -61,13 +45,19 @@ async function relationalQuery( where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} and event_type = {{eventType}} - ${excludeDomain} ${filterQuery} group by 1 order by 2 desc limit 100 `, - params, + { + ...filters, + websiteId, + startDate: maxDate(startDate, website.resetAt), + endDate, + eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, + domain: website.domain, + }, ); } @@ -83,21 +73,6 @@ async function clickhouseQuery( const { startDate, endDate, filters = {}, column } = criteria; const { rawQuery, parseFilters } = clickhouse; const website = await loadWebsite(websiteId); - const params = { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, - domain: undefined, - }; - - let excludeDomain = ''; - - if (column === 'referrer_domain') { - excludeDomain = 'and referrer_domain != {domain:String}'; - params.domain = website.domain; - } const { filterQuery } = parseFilters(filters); @@ -108,12 +83,18 @@ async function clickhouseQuery( where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} and event_type = {eventType:UInt32} - ${excludeDomain} ${filterQuery} group by x order by y desc limit 100 `, - params, + { + ...filters, + websiteId, + startDate: maxDate(startDate, website.resetAt), + endDate, + eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, + domain: website.domain, + }, ); } diff --git a/queries/analytics/pageviews/getPageviewStats.ts b/queries/analytics/pageviews/getPageviewStats.ts index 6d702993b..cdbd6442b 100644 --- a/queries/analytics/pageviews/getPageviewStats.ts +++ b/queries/analytics/pageviews/getPageviewStats.ts @@ -59,6 +59,7 @@ async function relationalQuery(websiteId: string, criteria: PageviewStatsCriteri startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.pageView, + domain: website.domain, }, ); } @@ -93,6 +94,7 @@ async function clickhouseQuery(websiteId: string, criteria: PageviewStatsCriteri startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.pageView, + domain: website.domain, }, ); } diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index 68f06e218..ff1399317 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -29,7 +29,7 @@ async function relationalQuery( y: number; }[] > { - const { startDate, endDate, fields = [], filters = [], groups = [] } = criteria; + const { startDate, endDate, filters = [] } = criteria; const { parseFilters, rawQuery } = prisma; const website = await loadWebsite(websiteId); const params = {}; @@ -107,7 +107,7 @@ function parseFields(fields) { if (!count && value === 'total') { count = true; - arr = arr.concat(`count(*) as total`); + arr = arr.concat(`count(*) as views`); } else if (!distinct && value === 'unique') { distinct = true; //arr = arr.concat(`count(distinct ${name})`); diff --git a/queries/analytics/sessions/getSessionMetrics.ts b/queries/analytics/sessions/getSessionMetrics.ts index 2512b06ce..a9b49ec81 100644 --- a/queries/analytics/sessions/getSessionMetrics.ts +++ b/queries/analytics/sessions/getSessionMetrics.ts @@ -1,7 +1,7 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { DEFAULT_RESET_DATE, EVENT_TYPE } from 'lib/constants'; +import { EVENT_TYPE } from 'lib/constants'; import { loadWebsite } from 'lib/load'; import { maxDate } from 'lib/date'; @@ -28,14 +28,9 @@ async function relationalQuery( return rawQuery( `select ${column} x, count(*) y - from session as s - where s.session_id in ( - select website_event.session_id from website_event - join website - on website_event.website_id = website.website_id - ${joinSession} - where website.website_id = {{websiteId::uuid}} + ${joinSession} + where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} ${filterQuery} ) as t diff --git a/queries/analytics/sessions/getSessionStats.ts b/queries/analytics/sessions/getSessionStats.ts index 966fd91ff..7633f242e 100644 --- a/queries/analytics/sessions/getSessionStats.ts +++ b/queries/analytics/sessions/getSessionStats.ts @@ -59,6 +59,7 @@ async function relationalQuery(websiteId: string, criteria: SessionStatsCriteria startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.pageView, + domain: website.domain, }, ); } @@ -93,6 +94,7 @@ async function clickhouseQuery(websiteId: string, criteria: SessionStatsCriteria startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.pageView, + domain: website.domain, }, ); } diff --git a/queries/analytics/stats/getWebsiteStats.ts b/queries/analytics/stats/getWebsiteStats.ts index 4d3730ee4..e048fc8f0 100644 --- a/queries/analytics/stats/getWebsiteStats.ts +++ b/queries/analytics/stats/getWebsiteStats.ts @@ -51,11 +51,12 @@ async function relationalQuery( ) as t `, { + ...filters, websiteId, startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.pageView, - ...filters, + domain: website.domain, }, ); } @@ -97,6 +98,7 @@ async function clickhouseQuery( startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.pageView, + domain: website.domain, }, ); } From 9cde107ddfa857a65431d54be44d1c79f311d24d Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 4 Aug 2023 13:10:03 -0700 Subject: [PATCH 009/357] build out retention reports --- components/messages.js | 1 + components/pages/reports/ReportDetails.js | 2 + components/pages/reports/ReportTemplates.js | 6 + .../pages/reports/funnel/FunnelChart.js | 17 +- .../pages/reports/retention/RetentionChart.js | 74 +++++++ .../retention/RetentionChart.module.css | 3 + .../reports/retention/RetentionParameters.js | 44 ++++ .../reports/retention/RetentionReport.js | 28 +++ .../retention/RetentionReport.module.css | 10 + .../pages/reports/retention/RetentionTable.js | 19 ++ pages/api/reports/retention.ts | 55 +++++ pages/reports/retention.js | 13 ++ queries/analytics/reports/getRetention.ts | 209 ++++++++++++++++++ queries/index.js | 1 + 14 files changed, 479 insertions(+), 3 deletions(-) create mode 100644 components/pages/reports/retention/RetentionChart.js create mode 100644 components/pages/reports/retention/RetentionChart.module.css create mode 100644 components/pages/reports/retention/RetentionParameters.js create mode 100644 components/pages/reports/retention/RetentionReport.js create mode 100644 components/pages/reports/retention/RetentionReport.module.css create mode 100644 components/pages/reports/retention/RetentionTable.js create mode 100644 pages/api/reports/retention.ts create mode 100644 pages/reports/retention.js create mode 100644 queries/analytics/reports/getRetention.ts diff --git a/components/messages.js b/components/messages.js index a31e28751..68e3b3d59 100644 --- a/components/messages.js +++ b/components/messages.js @@ -161,6 +161,7 @@ export const labels = defineMessages({ overview: { id: 'labels.overview', defaultMessage: 'Overview' }, totalRecords: { id: 'labels.total-records', defaultMessage: 'Total records' }, insights: { id: 'label.insights', defaultMessage: 'Insights' }, + retention: { id: 'label.retention', defaultMessage: 'Retention' }, dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' }, }); diff --git a/components/pages/reports/ReportDetails.js b/components/pages/reports/ReportDetails.js index c41d12f6a..39cd285d7 100644 --- a/components/pages/reports/ReportDetails.js +++ b/components/pages/reports/ReportDetails.js @@ -1,9 +1,11 @@ import FunnelReport from './funnel/FunnelReport'; import EventDataReport from './event-data/EventDataReport'; +import RetentionReport from './retention/RetentionReport'; const reports = { funnel: FunnelReport, 'event-data': EventDataReport, + retention: RetentionReport, }; export default function ReportDetails({ reportId, reportType }) { diff --git a/components/pages/reports/ReportTemplates.js b/components/pages/reports/ReportTemplates.js index 60ae11e79..29c193a86 100644 --- a/components/pages/reports/ReportTemplates.js +++ b/components/pages/reports/ReportTemplates.js @@ -47,6 +47,12 @@ export function ReportTemplates() { url: '/reports/funnel', icon: , }, + { + title: formatMessage(labels.retention), + description: 'Track your websites user retention', + url: '/reports/retention', + icon: , + }, ]; return ( diff --git a/components/pages/reports/funnel/FunnelChart.js b/components/pages/reports/funnel/FunnelChart.js index 7253c3fa5..c35afe4e6 100644 --- a/components/pages/reports/funnel/FunnelChart.js +++ b/components/pages/reports/funnel/FunnelChart.js @@ -1,5 +1,5 @@ import { useCallback, useContext, useMemo } from 'react'; -import { Loading } from 'react-basics'; +import { Loading, StatusLight } from 'react-basics'; import useMessages from 'hooks/useMessages'; import useTheme from 'hooks/useTheme'; import BarChart from 'components/metrics/BarChart'; @@ -22,14 +22,25 @@ export function FunnelChart({ className, loading }) { ); const renderTooltipPopup = useCallback((setTooltipPopup, model) => { - const { opacity, dataPoints } = model.tooltip; + const { opacity, labelColors, dataPoints } = model.tooltip; if (!dataPoints?.length || !opacity) { setTooltipPopup(null); return; } - setTooltipPopup(`${formatLongNumber(dataPoints[0].raw.y)} ${formatMessage(labels.visitors)}`); + setTooltipPopup( + <> +
+ {formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)} +
+
+ + {formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)} + +
+ , + ); }, []); const datasets = useMemo(() => { diff --git a/components/pages/reports/retention/RetentionChart.js b/components/pages/reports/retention/RetentionChart.js new file mode 100644 index 000000000..5f7361fdb --- /dev/null +++ b/components/pages/reports/retention/RetentionChart.js @@ -0,0 +1,74 @@ +import { useCallback, useContext, useMemo } from 'react'; +import { Loading, StatusLight } from 'react-basics'; +import useMessages from 'hooks/useMessages'; +import useTheme from 'hooks/useTheme'; +import BarChart from 'components/metrics/BarChart'; +import { formatLongNumber } from 'lib/format'; +import styles from './RetentionChart.module.css'; +import { ReportContext } from '../Report'; + +export function RetentionChart({ className, loading }) { + const { report } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + const { colors } = useTheme(); + + const { parameters, data } = report || {}; + + const renderXLabel = useCallback( + (label, index) => { + return parameters.urls[index]; + }, + [parameters], + ); + + const renderTooltipPopup = useCallback((setTooltipPopup, model) => { + const { opacity, labelColors, dataPoints } = model.tooltip; + + if (!dataPoints?.length || !opacity) { + setTooltipPopup(null); + return; + } + + setTooltipPopup( + <> +
+ {formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)} +
+
+ + {formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)} + +
+ , + ); + }, []); + + const datasets = useMemo(() => { + return [ + { + label: formatMessage(labels.uniqueVisitors), + data: data, + borderWidth: 1, + ...colors.chart.visitors, + }, + ]; + }, [data]); + + if (loading) { + return ; + } + + return ( + + ); +} + +export default RetentionChart; diff --git a/components/pages/reports/retention/RetentionChart.module.css b/components/pages/reports/retention/RetentionChart.module.css new file mode 100644 index 000000000..9e1690b31 --- /dev/null +++ b/components/pages/reports/retention/RetentionChart.module.css @@ -0,0 +1,3 @@ +.loading { + height: 300px; +} diff --git a/components/pages/reports/retention/RetentionParameters.js b/components/pages/reports/retention/RetentionParameters.js new file mode 100644 index 000000000..29c0eff2f --- /dev/null +++ b/components/pages/reports/retention/RetentionParameters.js @@ -0,0 +1,44 @@ +import { useContext, useRef } from 'react'; +import { useMessages } from 'hooks'; +import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from 'react-basics'; +import { ReportContext } from 'components/pages/reports/Report'; +import BaseParameters from '../BaseParameters'; + +export function RetentionParameters() { + const { report, runReport, isRunning } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + const ref = useRef(null); + + const { parameters } = report || {}; + const { websiteId, dateRange } = parameters || {}; + const queryDisabled = !websiteId || !dateRange; + + const handleSubmit = (data, e) => { + e.stopPropagation(); + e.preventDefault(); + if (!queryDisabled) { + runReport(data); + } + }; + + return ( +
+ + + + + + + + + {formatMessage(labels.runQuery)} + + + + ); +} + +export default RetentionParameters; diff --git a/components/pages/reports/retention/RetentionReport.js b/components/pages/reports/retention/RetentionReport.js new file mode 100644 index 000000000..31d085f7b --- /dev/null +++ b/components/pages/reports/retention/RetentionReport.js @@ -0,0 +1,28 @@ +import RetentionChart from './RetentionChart'; +import RetentionTable from './RetentionTable'; +import RetentionParameters from './RetentionParameters'; +import Report from '../Report'; +import ReportHeader from '../ReportHeader'; +import ReportMenu from '../ReportMenu'; +import ReportBody from '../ReportBody'; +import Funnel from 'assets/funnel.svg'; + +const defaultParameters = { + type: 'Retention', + parameters: { window: 60, urls: [] }, +}; + +export default function RetentionReport({ reportId }) { + return ( + + } /> + + + + + + + + + ); +} diff --git a/components/pages/reports/retention/RetentionReport.module.css b/components/pages/reports/retention/RetentionReport.module.css new file mode 100644 index 000000000..aed66b74e --- /dev/null +++ b/components/pages/reports/retention/RetentionReport.module.css @@ -0,0 +1,10 @@ +.filters { + display: flex; + flex-direction: column; + justify-content: space-between; + border: 1px solid var(--base400); + border-radius: var(--border-radius); + line-height: 32px; + padding: 10px; + overflow: hidden; +} diff --git a/components/pages/reports/retention/RetentionTable.js b/components/pages/reports/retention/RetentionTable.js new file mode 100644 index 000000000..4ef879862 --- /dev/null +++ b/components/pages/reports/retention/RetentionTable.js @@ -0,0 +1,19 @@ +import { useContext } from 'react'; +import DataTable from 'components/metrics/DataTable'; +import { useMessages } from 'hooks'; +import { ReportContext } from '../Report'; + +export function RetentionTable() { + const { report } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + return ( + + ); +} + +export default RetentionTable; diff --git a/pages/api/reports/retention.ts b/pages/api/reports/retention.ts new file mode 100644 index 000000000..6b8aebcc3 --- /dev/null +++ b/pages/api/reports/retention.ts @@ -0,0 +1,55 @@ +import { canViewWebsite } from 'lib/auth'; +import { useCors, useAuth } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { getRetention } from 'queries'; + +export interface RetentionRequestBody { + websiteId: string; + urls: string[]; + window: number; + dateRange: { + startDate: string; + endDate: string; + }; +} + +export interface RetentionResponse { + urls: string[]; + window: number; + startAt: number; + endAt: number; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + if (req.method === 'POST') { + const { + websiteId, + urls, + window, + dateRange: { startDate, endDate }, + } = req.body; + + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const data = await getRetention(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + urls, + windowMinutes: +window, + }); + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/pages/reports/retention.js b/pages/reports/retention.js new file mode 100644 index 000000000..b7f0bd0f8 --- /dev/null +++ b/pages/reports/retention.js @@ -0,0 +1,13 @@ +import AppLayout from 'components/layout/AppLayout'; +import RetentionReport from 'components/pages/reports/retention/RetentionReport'; +import useMessages from 'hooks/useMessages'; + +export default function () { + const { formatMessage, labels } = useMessages(); + + return ( + + + + ); +} diff --git a/queries/analytics/reports/getRetention.ts b/queries/analytics/reports/getRetention.ts new file mode 100644 index 000000000..b2c478827 --- /dev/null +++ b/queries/analytics/reports/getRetention.ts @@ -0,0 +1,209 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; + +export async function getRetention( + ...args: [ + websiteId: string, + criteria: { + windowMinutes: number; + startDate: Date; + endDate: Date; + urls: string[]; + }, + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + criteria: { + windowMinutes: number; + startDate: Date; + endDate: Date; + urls: string[]; + }, +): Promise< + { + x: string; + y: number; + z: number; + }[] +> { + const { windowMinutes, startDate, endDate, urls } = criteria; + const { rawQuery, getAddMinutesQuery } = prisma; + const { levelQuery, sumQuery } = getRetentionQuery(urls, windowMinutes); + + function getRetentionQuery( + urls: string[], + windowMinutes: number, + ): { + levelQuery: string; + sumQuery: string; + } { + return urls.reduce( + (pv, cv, i) => { + const levelNumber = i + 1; + const startSum = i > 0 ? 'union ' : ''; + + if (levelNumber >= 2) { + pv.levelQuery += ` + , level${levelNumber} AS ( + select distinct we.session_id, we.created_at + from level${i} l + join website_event we + on l.session_id = we.session_id + where we.created_at between l.created_at + and ${getAddMinutesQuery(`l.created_at `, windowMinutes)} + and we.referrer_path = {{${i - 1}}} + and we.url_path = {{${i}}} + and we.created_at <= {{endDate}} + and we.website_id = {{websiteId::uuid}} + )`; + } + + pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`; + + return pv; + }, + { + levelQuery: '', + sumQuery: '', + }, + ); + } + + return rawQuery( + ` + WITH level1 AS ( + select distinct session_id, created_at + from website_event + where website_id = {{websiteId::uuid}} + and created_at between {{startDate}} and {{endDate}} + and url_path = {{0}} + ) + ${levelQuery} + ${sumQuery} + ORDER BY level; + `, + { + websiteId, + startDate, + endDate, + ...urls, + }, + ).then(results => { + return urls.map((a, i) => ({ + x: a, + y: results[i]?.count || 0, + z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off + })); + }); +} + +async function clickhouseQuery( + websiteId: string, + criteria: { + windowMinutes: number; + startDate: Date; + endDate: Date; + urls: string[]; + }, +): Promise< + { + x: string; + y: number; + z: number; + }[] +> { + const { windowMinutes, startDate, endDate, urls } = criteria; + const { rawQuery } = clickhouse; + const { levelQuery, sumQuery, urlFilterQuery, urlParams } = getRetentionQuery( + urls, + windowMinutes, + ); + + function getRetentionQuery( + urls: string[], + windowMinutes: number, + ): { + levelQuery: string; + sumQuery: string; + urlFilterQuery: string; + urlParams: { [key: string]: string }; + } { + return urls.reduce( + (pv, cv, i) => { + const levelNumber = i + 1; + const startSum = i > 0 ? 'union all ' : ''; + const startFilter = i > 0 ? ', ' : ''; + + if (levelNumber >= 2) { + pv.levelQuery += `\n + , level${levelNumber} AS ( + select distinct y.session_id as session_id, + y.url_path as url_path, + y.referrer_path as referrer_path, + y.created_at as created_at + from level${i} x + join level0 y + on x.session_id = y.session_id + where y.created_at between x.created_at and x.created_at + interval ${windowMinutes} minute + and y.referrer_path = {url${i - 1}:String} + and y.url_path = {url${i}:String} + )`; + } + + pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`; + pv.urlFilterQuery += `${startFilter}{url${i}:String} `; + pv.urlParams[`url${i}`] = cv; + + return pv; + }, + { + levelQuery: '', + sumQuery: '', + urlFilterQuery: '', + urlParams: {}, + }, + ); + } + + return rawQuery<{ level: number; count: number }[]>( + ` + WITH level0 AS ( + select distinct session_id, url_path, referrer_path, created_at + from umami.website_event + where url_path in (${urlFilterQuery}) + and website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + ), + level1 AS ( + select * + from level0 + where url_path = {url0:String} + ) + ${levelQuery} + select * + from ( + ${sumQuery} + ) ORDER BY level; + `, + { + websiteId, + startDate, + endDate, + ...urlParams, + }, + ).then(results => { + return urls.map((a, i) => ({ + x: a, + y: results[i]?.count || 0, + z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off + })); + }); +} diff --git a/queries/index.js b/queries/index.js index f509e0392..0fb2bf2cf 100644 --- a/queries/index.js +++ b/queries/index.js @@ -12,6 +12,7 @@ export * from './analytics/eventData/getEventDataFields'; export * from './analytics/eventData/getEventDataUsage'; export * from './analytics/events/saveEvent'; export * from './analytics/reports/getFunnel'; +export * from './analytics/reports/getRetention'; export * from './analytics/reports/getInsights'; export * from './analytics/pageviews/getPageviewMetrics'; export * from './analytics/pageviews/getPageviewStats'; From 7148f66d1af901b7d55e93b005261484461a9ec1 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 4 Aug 2023 13:18:30 -0700 Subject: [PATCH 010/357] Refactored query parameter handling. --- lib/clickhouse.ts | 23 ++++-- lib/constants.ts | 3 + lib/prisma.ts | 19 ++++- lib/types.ts | 36 +++++---- pages/api/websites/[id]/metrics.ts | 17 ++-- pages/api/websites/[id]/pageviews.ts | 52 +++++-------- pages/api/websites/[id]/stats.ts | 46 ++++------- .../analytics/eventData/getEventDataEvents.ts | 77 ++++++++----------- .../analytics/eventData/getEventDataFields.ts | 28 +++---- queries/analytics/events/getEventMetrics.ts | 59 +++++--------- .../analytics/pageviews/getPageviewMetrics.ts | 67 ++++------------ .../analytics/pageviews/getPageviewStats.ts | 65 ++++------------ queries/analytics/reports/getInsights.ts | 54 ++++--------- .../analytics/sessions/getSessionMetrics.ts | 48 ++++-------- queries/analytics/sessions/getSessionStats.ts | 65 ++++------------ .../analytics/stats/getWebsiteDateRange.ts | 14 ++-- queries/analytics/stats/getWebsiteStats.ts | 56 ++++---------- 17 files changed, 260 insertions(+), 469 deletions(-) diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index 19d094054..6d5bcf428 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -2,8 +2,10 @@ import { ClickHouse } from 'clickhouse'; import dateFormat from 'dateformat'; import debug from 'debug'; import { CLICKHOUSE } from 'lib/db'; -import { WebsiteMetricFilter } from './types'; -import { FILTER_COLUMNS } from './constants'; +import { QueryFilters } from './types'; +import { FILTER_COLUMNS, IGNORED_FILTERS } from './constants'; +import { loadWebsite } from './load'; +import { maxDate } from './date'; export const CLICKHOUSE_DATE_FORMATS = { minute: '%Y-%m-%d %H:%M:00', @@ -65,13 +67,13 @@ function getFilterQuery(filters = {}) { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; - if (filter !== undefined) { + if (filter !== undefined && !IGNORED_FILTERS.includes(key)) { const column = FILTER_COLUMNS[key] || key; arr.push(`and ${column} = {${key}:String}`); } if (key === 'referrer') { - arr.push('and referrer_domain != {domain:String}'); + arr.push('and referrer_domain != {websiteDomain:String}'); } return arr; @@ -80,9 +82,20 @@ function getFilterQuery(filters = {}) { return query.join('\n'); } -function parseFilters(filters: WebsiteMetricFilter = {}) { +async function parseFilters( + websiteId: string, + filters: QueryFilters & { [key: string]: any } = {}, +) { + const website = await loadWebsite(websiteId); + return { filterQuery: getFilterQuery(filters), + params: { + ...filters, + websiteId, + startDate: maxDate(filters.startDate, website.resetAt), + websiteDomain: website.domain, + }, }; } diff --git a/lib/constants.ts b/lib/constants.ts index 2b3da8754..9362b4560 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -50,8 +50,11 @@ export const FILTER_COLUMNS = { query: 'url_query', event: 'event_name', region: 'subdivision1', + type: 'event_type', }; +export const IGNORED_FILTERS = ['startDate', 'endDate', 'timezone', 'unit', 'eventType']; + export const COLLECTION_TYPE = { event: 'event', identify: 'identify', diff --git a/lib/prisma.ts b/lib/prisma.ts index a6f1ff889..d1b9d0e5d 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -1,7 +1,10 @@ import prisma from '@umami/prisma-client'; import moment from 'moment-timezone'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; -import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants'; +import { FILTER_COLUMNS, IGNORED_FILTERS, SESSION_COLUMNS } from './constants'; +import { loadWebsite } from './load'; +import { maxDate } from './date'; +import { QueryFilters } from './types'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -68,14 +71,14 @@ function getFilterQuery(filters = {}): string { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; - if (filter !== undefined) { + if (filter !== undefined && !IGNORED_FILTERS.includes(key)) { const column = FILTER_COLUMNS[key] || key; arr.push(`and ${column}={{${key}}}`); } if (key === 'referrer') { arr.push( - 'and (website_event.referrer_domain != {{domain}} or website_event.referrer_domain is null)', + 'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)', ); } @@ -85,12 +88,20 @@ function getFilterQuery(filters = {}): string { return query.join('\n'); } -function parseFilters(filters: { [key: string]: any } = {}) { +async function parseFilters(websiteId, filters: QueryFilters & { [key: string]: any } = {}) { + const website = await loadWebsite(websiteId); + return { joinSession: Object.keys(filters).find(key => SESSION_COLUMNS[key]) ? `inner join session on website_event.session_id = session.session_id` : '', filterQuery: getFilterQuery(filters), + params: { + ...filters, + websiteId, + startDate: maxDate(filters.startDate, website.resetAt), + websiteDomain: website.domain, + }, }; } diff --git a/lib/types.ts b/lib/types.ts index 7c91ec4f7..131740f88 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -73,21 +73,6 @@ export interface WebsiteMetric { y: number; } -export interface WebsiteMetricFilter { - domain?: string; - url?: string; - referrer?: string; - title?: string; - query?: string; - event?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; -} - export interface WebsiteEventMetric { x: string; t: string; @@ -144,3 +129,24 @@ export interface DateRange { unit: string; value: string; } + +export interface QueryFilters { + startDate?: Date; + endDate?: Date; + timezone?: string; + unit?: string; + domain?: string; + eventType?: number; + url?: string; + referrer?: string; + title?: string; + query?: string; + event?: string; + os?: string; + browser?: string; + device?: string; + country?: string; + region?: string; + city?: string; + language?: string; +} diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index 15389e3ee..7c84583c2 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -23,6 +23,7 @@ export interface WebsiteMetricsRequestQuery { country: string; region: string; city: string; + language: string; } export default async ( @@ -57,6 +58,8 @@ export default async ( const { startDate, endDate } = await parseDateRangeQuery(req); const filters = { + startDate, + endDate, url, referrer, title, @@ -76,12 +79,7 @@ export default async ( const column = FILTER_COLUMNS[type] || type; if (SESSION_COLUMNS.includes(type)) { - const data = await getSessionMetrics(websiteId, { - startDate, - endDate, - column, - filters, - }); + const data = await getSessionMetrics(websiteId, column, filters); if (type === 'language') { const combined = {}; @@ -103,12 +101,7 @@ export default async ( } if (EVENT_COLUMNS.includes(type)) { - const data = await getPageviewMetrics(websiteId, { - startDate, - endDate, - column, - filters, - }); + const data = await getPageviewMetrics(websiteId, column, filters); return ok(res, data); } diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index 810854a75..87c60d58e 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -57,41 +57,25 @@ export default async ( return badRequest(res); } + const filters = { + startDate, + endDate, + timezone, + unit, + url, + referrer, + title, + os, + browser, + device, + country, + region, + city, + }; + const [pageviews, sessions] = await Promise.all([ - getPageviewStats(websiteId, { - startDate, - endDate, - timezone, - unit, - filters: { - url, - referrer, - title, - os, - browser, - device, - country, - region, - city, - }, - }), - getSessionStats(websiteId, { - startDate, - endDate, - timezone, - unit, - filters: { - url, - referrer, - title, - os, - browser, - device, - country, - region, - city, - }, - }), + getPageviewStats(websiteId, filters), + getSessionStats(websiteId, filters), ]); return ok(res, { pageviews, sessions }); diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index 3164913d9..a77c7eaf8 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -56,40 +56,26 @@ export default async ( const prevStartDate = subMinutes(startDate, diff); const prevEndDate = subMinutes(endDate, diff); - const metrics = await getWebsiteStats(websiteId, { - startDate, - endDate, - filters: { - url, - referrer, - title, - query, - event, - os, - browser, - device, - country, - region, - city, - }, - }); + const filters = { + url, + referrer, + title, + query, + event, + os, + browser, + device, + country, + region, + city, + }; + + const metrics = await getWebsiteStats(websiteId, { ...filters, startDate, endDate }); const prevPeriod = await getWebsiteStats(websiteId, { + ...filters, startDate: prevStartDate, endDate: prevEndDate, - filters: { - url, - referrer, - title, - query, - event, - os, - browser, - device, - country, - region, - city, - }, }); const stats = Object.keys(metrics[0]).reduce((obj, key) => { diff --git a/queries/analytics/eventData/getEventDataEvents.ts b/queries/analytics/eventData/getEventDataEvents.ts index 634a28a26..fae46db10 100644 --- a/queries/analytics/eventData/getEventDataEvents.ts +++ b/queries/analytics/eventData/getEventDataEvents.ts @@ -1,17 +1,10 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { WebsiteEventDataFields } from 'lib/types'; -import { loadWebsite } from 'lib/load'; -import { maxDate } from 'lib/date'; +import { QueryFilters, WebsiteEventDataFields } from 'lib/types'; export async function getEventDataEvents( - ...args: [ - websiteId: string, - startDate: Date, - endDate: Date, - filters: { field?: string; event?: string }, - ] + ...args: [websiteId: string, filters: QueryFilters & { field?: string; event?: string }] ): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -21,64 +14,60 @@ export async function getEventDataEvents( async function relationalQuery( websiteId: string, - startDate: Date, - endDate: Date, - filters: { field?: string; event?: string }, + filters: QueryFilters & { field?: string; event?: string }, ) { - const { rawQuery } = prisma; - const website = await loadWebsite(websiteId); - const { event } = filters; + const { rawQuery, parseFilters } = prisma; + const { params } = await parseFilters(websiteId, filters); if (event) { return rawQuery( ` select - we.event_name as event, - ed.event_key as field, - ed.data_type as type, - ed.string_value as value, + website_event.event_name as event, + event_data.event_key as field, + event_data.data_type as type, + event_data.string_value as value, count(*) as total - from event_data as ed - inner join website_event as we - on we.event_id = ed.website_event_id - where ed.website_id = {{websiteId::uuid}} - and ed.created_at between {{startDate}} and {{endDate}} - and we.event_name = {{event}} - group by we.event_name, ed.event_key, ed.data_type, ed.string_value + from event_data + inner join website_event + on website_event.event_id = event_data.website_event_id + where event_data.website_id = {{websiteId::uuid}} + and event_data.created_at between {{startDate}} and {{endDate}} + and websit_event.event_name = {{event}} + group by website_event.event_name, event_data.event_key, event_data.data_type, event_data.string_value order by 1 asc, 2 asc, 3 asc, 4 desc `, - { websiteId, startDate: maxDate(startDate, website.resetAt), endDate, ...filters }, + params, ); } + return rawQuery( ` select - we.event_name as event, - ed.event_key as field, - ed.data_type as type, + website_event.event_name as event, + event_data.event_key as field, + event_data.data_type as type, count(*) as total - from event_data as ed - inner join website_event as we - on we.event_id = ed.website_event_id - where ed.website_id = {{websiteId::uuid}} - and ed.created_at between {{startDate}} and {{endDate}} - group by we.event_name, ed.event_key, ed.data_type + from event_data + inner join website_event + on website_event.event_id = event_data.website_event_id + where event_data.website_id = {{websiteId::uuid}} + and event_data.created_at between {{startDate}} and {{endDate}} + group by website_event.event_name, event_data.event_key, event_data.data_type order by 1 asc, 2 asc limit 100 `, - { websiteId, startDate: maxDate(startDate, website.resetAt), endDate }, + params, ); } async function clickhouseQuery( websiteId: string, - startDate: Date, - endDate: Date, - filters: { field?: string; event?: string }, + filters: QueryFilters & { field?: string; event?: string }, ) { - const { rawQuery } = clickhouse; - const website = await loadWebsite(websiteId); + const { rawQuery, parseFilters } = clickhouse; const { event } = filters; + const { params } = await parseFilters(websiteId, filters); if (event) { return rawQuery( @@ -97,7 +86,7 @@ async function clickhouseQuery( order by 1 asc, 2 asc, 3 asc, 4 desc limit 100 `, - { ...filters, websiteId, startDate: maxDate(startDate, website.resetAt), endDate }, + params, ); } @@ -115,6 +104,6 @@ async function clickhouseQuery( order by 1 asc, 2 asc limit 100 `, - { websiteId, startDate: maxDate(startDate, website.resetAt), endDate }, + params, ); } diff --git a/queries/analytics/eventData/getEventDataFields.ts b/queries/analytics/eventData/getEventDataFields.ts index 516c58d0c..a27f22810 100644 --- a/queries/analytics/eventData/getEventDataFields.ts +++ b/queries/analytics/eventData/getEventDataFields.ts @@ -1,12 +1,10 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { WebsiteEventDataFields } from 'lib/types'; -import { loadWebsite } from 'lib/load'; -import { maxDate } from 'lib/date'; +import { QueryFilters, WebsiteEventDataFields } from 'lib/types'; export async function getEventDataFields( - ...args: [websiteId: string, startDate: Date, endDate: Date, field?: string] + ...args: [websiteId: string, filters: QueryFilters & { field?: string }] ): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -14,9 +12,10 @@ export async function getEventDataFields( }); } -async function relationalQuery(websiteId: string, startDate: Date, endDate: Date, field: string) { - const { rawQuery } = prisma; - const website = await loadWebsite(websiteId); +async function relationalQuery(websiteId: string, filters: QueryFilters & { field?: string }) { + const { rawQuery, parseFilters } = prisma; + const { field } = filters; + const { params } = await parseFilters(websiteId, filters); if (field) { return rawQuery( @@ -33,7 +32,7 @@ async function relationalQuery(websiteId: string, startDate: Date, endDate: Date order by 3 desc, 2 desc, 1 asc limit 100 `, - { websiteId, field, startDate: maxDate(startDate, website.resetAt), endDate }, + params, ); } @@ -50,13 +49,14 @@ async function relationalQuery(websiteId: string, startDate: Date, endDate: Date order by 3 desc, 2 asc, 1 asc limit 100 `, - { websiteId, startDate: maxDate(startDate, website.resetAt), endDate }, + params, ); } -async function clickhouseQuery(websiteId: string, startDate: Date, endDate: Date, field: string) { - const { rawQuery } = clickhouse; - const website = await loadWebsite(websiteId); +async function clickhouseQuery(websiteId: string, filters: QueryFilters & { field?: string }) { + const { rawQuery, parseFilters } = clickhouse; + const { field } = filters; + const { params } = await parseFilters(websiteId, filters); if (field) { return rawQuery( @@ -73,7 +73,7 @@ async function clickhouseQuery(websiteId: string, startDate: Date, endDate: Date order by 3 desc, 2 desc, 1 asc limit 100 `, - { websiteId, field, startDate: maxDate(startDate, website.resetAt), endDate }, + params, ); } @@ -90,6 +90,6 @@ async function clickhouseQuery(websiteId: string, startDate: Date, endDate: Date order by 3 desc, 2 asc, 1 asc limit 100 `, - { websiteId, startDate: maxDate(startDate, website.resetAt), endDate }, + params, ); } diff --git a/queries/analytics/events/getEventMetrics.ts b/queries/analytics/events/getEventMetrics.ts index 03b252b79..cf862f4a7 100644 --- a/queries/analytics/events/getEventMetrics.ts +++ b/queries/analytics/events/getEventMetrics.ts @@ -1,24 +1,11 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { WebsiteEventMetric } from 'lib/types'; +import { WebsiteEventMetric, QueryFilters } from 'lib/types'; import { EVENT_TYPE } from 'lib/constants'; -import { loadWebsite } from 'lib/load'; -import { maxDate } from 'lib/date'; - -export interface GetEventMetricsCriteria { - startDate: Date; - endDate: Date; - timezone: string; - unit: string; - filters: { - url: string; - eventName: string; - }; -} export async function getEventMetrics( - ...args: [websiteId: string, criteria: GetEventMetricsCriteria] + ...args: [websiteId: string, criteria: QueryFilters] ): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -26,11 +13,13 @@ export async function getEventMetrics( }); } -async function relationalQuery(websiteId: string, criteria: GetEventMetricsCriteria) { - const { startDate, endDate, timezone = 'utc', unit = 'day', filters } = criteria; - const { rawQuery, getDateQuery, getFilterQuery } = prisma; - const website = await loadWebsite(websiteId); - const filterQuery = getFilterQuery(filters); +async function relationalQuery(websiteId: string, filters: QueryFilters) { + const { timezone = 'utc', unit = 'day' } = filters; + const { rawQuery, getDateQuery, parseFilters } = prisma; + const { filterQuery, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.customEvent, + }); return rawQuery( ` @@ -46,22 +35,17 @@ async function relationalQuery(websiteId: string, criteria: GetEventMetricsCrite group by 1, 2 order by 2 `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.customEvent, - domain: website.domain, - }, + params, ); } -async function clickhouseQuery(websiteId: string, criteria: GetEventMetricsCriteria) { - const { startDate, endDate, timezone = 'utc', unit = 'day', filters } = criteria; - const { rawQuery, getDateQuery, getFilterQuery } = clickhouse; - const website = await loadWebsite(websiteId); - const filterQuery = getFilterQuery(filters); +async function clickhouseQuery(websiteId: string, filters: QueryFilters) { + const { timezone = 'utc', unit = 'day' } = filters; + const { rawQuery, getDateQuery, parseFilters } = clickhouse; + const { filterQuery, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.customEvent, + }); return rawQuery( ` @@ -77,13 +61,6 @@ async function clickhouseQuery(websiteId: string, criteria: GetEventMetricsCrite group by x, t order by t `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.customEvent, - domain: website.domain, - }, + params, ); } diff --git a/queries/analytics/pageviews/getPageviewMetrics.ts b/queries/analytics/pageviews/getPageviewMetrics.ts index 8e4460e61..bbbd77052 100644 --- a/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/queries/analytics/pageviews/getPageviewMetrics.ts @@ -2,19 +2,10 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import { EVENT_TYPE } from 'lib/constants'; -import { loadWebsite } from 'lib/load'; -import { maxDate } from 'lib/date'; +import { QueryFilters } from 'lib/types'; export async function getPageviewMetrics( - ...args: [ - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - column: string; - filters: object; - }, - ] + ...args: [websiteId: string, columns: string, filters: QueryFilters] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -22,20 +13,12 @@ export async function getPageviewMetrics( }); } -async function relationalQuery( - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - column: string; - filters: object; - }, -) { - const { startDate, endDate, filters = {}, column } = criteria; +async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) { const { rawQuery, parseFilters } = prisma; - const website = await loadWebsite(websiteId); - - const { filterQuery, joinSession } = parseFilters(filters); + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -50,31 +33,16 @@ async function relationalQuery( order by 2 desc limit 100 `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, - domain: website.domain, - }, + params, ); } -async function clickhouseQuery( - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - column: string; - filters: object; - }, -) { - const { startDate, endDate, filters = {}, column } = criteria; +async function clickhouseQuery(websiteId: string, column: string, filters: QueryFilters) { const { rawQuery, parseFilters } = clickhouse; - const website = await loadWebsite(websiteId); - - const { filterQuery } = parseFilters(filters); + const { filterQuery, params } = await parseFilters(websiteId, { + ...filters, + eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -88,13 +56,6 @@ async function clickhouseQuery( order by y desc limit 100 `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, - domain: website.domain, - }, + params, ); } diff --git a/queries/analytics/pageviews/getPageviewStats.ts b/queries/analytics/pageviews/getPageviewStats.ts index cdbd6442b..d6a980ef7 100644 --- a/queries/analytics/pageviews/getPageviewStats.ts +++ b/queries/analytics/pageviews/getPageviewStats.ts @@ -2,43 +2,22 @@ import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import { EVENT_TYPE } from 'lib/constants'; -import { loadWebsite } from 'lib/load'; -import { maxDate } from 'lib/date'; +import { QueryFilters } from 'lib/types'; -export interface PageviewStatsCriteria { - startDate: Date; - endDate: Date; - timezone?: string; - unit?: string; - filters: { - url?: string; - referrer?: string; - title?: string; - browser?: string; - os?: string; - device?: string; - screen?: string; - language?: string; - country?: string; - region?: string; - city?: string; - }; -} - -export async function getPageviewStats( - ...args: [websiteId: string, criteria: PageviewStatsCriteria] -) { +export async function getPageviewStats(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId: string, criteria: PageviewStatsCriteria) { - const { startDate, endDate, timezone = 'utc', unit = 'day', filters = {} } = criteria; +async function relationalQuery(websiteId: string, filters: QueryFilters) { + const { timezone = 'utc', unit = 'day' } = filters; const { getDateQuery, parseFilters, rawQuery } = prisma; - const website = await loadWebsite(websiteId); - const { filterQuery, joinSession } = parseFilters(filters); + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -53,22 +32,17 @@ async function relationalQuery(websiteId: string, criteria: PageviewStatsCriteri ${filterQuery} group by 1 `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.pageView, - domain: website.domain, - }, + params, ); } -async function clickhouseQuery(websiteId: string, criteria: PageviewStatsCriteria) { - const { startDate, endDate, timezone = 'UTC', unit = 'day', filters = {} } = criteria; +async function clickhouseQuery(websiteId: string, filters: QueryFilters) { + const { timezone = 'UTC', unit = 'day' } = filters; const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; - const website = await loadWebsite(websiteId); - const { filterQuery } = parseFilters(filters); + const { filterQuery, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -88,13 +62,6 @@ async function clickhouseQuery(websiteId: string, criteria: PageviewStatsCriteri ) as g order by t `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.pageView, - domain: website.domain, - }, + params, ); } diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index ff1399317..dfe7c3975 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -1,19 +1,10 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; -import { maxDate } from 'lib/date'; import { EVENT_TYPE } from 'lib/constants'; -import { loadWebsite } from 'lib/load'; +import { QueryFilters } from 'lib/types'; -export interface GetInsightsCriteria { - startDate: Date; - endDate: Date; - fields: { name: string; type: string; value: string }[]; - filters: string[]; - groups: string[]; -} - -export async function getInsights(...args: [websiteId: string, criteria: GetInsightsCriteria]) { +export async function getInsights(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -22,18 +13,18 @@ export async function getInsights(...args: [websiteId: string, criteria: GetInsi async function relationalQuery( websiteId: string, - criteria: GetInsightsCriteria, + filters: QueryFilters, ): Promise< { x: string; y: number; }[] > { - const { startDate, endDate, filters = [] } = criteria; const { parseFilters, rawQuery } = prisma; - const website = await loadWebsite(websiteId); - const params = {}; - const { filterQuery, joinSession } = parseFilters(params); + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -48,37 +39,30 @@ async function relationalQuery( ${filterQuery} group by 1 `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.pageView, - }, + params, ); } async function clickhouseQuery( websiteId: string, - criteria: GetInsightsCriteria, + filters: QueryFilters, ): Promise< { x: string; y: number; }[] > { - const { startDate, endDate, fields = [], filters = [], groups = [] } = criteria; const { parseFilters, rawQuery } = clickhouse; - const website = await loadWebsite(websiteId); - const params = {}; - const { filterQuery } = parseFilters(params); - - const fieldsQuery = parseFields(fields); + const { fields } = filters; + const { filterQuery, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( ` select - ${fieldsQuery} + ${parseFields(fields)} from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} @@ -88,13 +72,7 @@ async function clickhouseQuery( order by total desc limit 500 `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.pageView, - }, + params, ); } diff --git a/queries/analytics/sessions/getSessionMetrics.ts b/queries/analytics/sessions/getSessionMetrics.ts index a9b49ec81..910c9785a 100644 --- a/queries/analytics/sessions/getSessionMetrics.ts +++ b/queries/analytics/sessions/getSessionMetrics.ts @@ -2,14 +2,10 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import { EVENT_TYPE } from 'lib/constants'; -import { loadWebsite } from 'lib/load'; -import { maxDate } from 'lib/date'; +import { QueryFilters } from 'lib/types'; export async function getSessionMetrics( - ...args: [ - websiteId: string, - criteria: { startDate: Date; endDate: Date; column: string; filters: object }, - ] + ...args: [websiteId: string, column: string, filters: QueryFilters] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -17,14 +13,12 @@ export async function getSessionMetrics( }); } -async function relationalQuery( - websiteId: string, - criteria: { startDate: Date; endDate: Date; column: string; filters: object }, -) { - const website = await loadWebsite(websiteId); - const { startDate, endDate, column, filters = {} } = criteria; +async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) { const { parseFilters, rawQuery } = prisma; - const { filterQuery, joinSession } = parseFilters(filters); + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( `select ${column} x, count(*) y @@ -32,28 +26,22 @@ async function relationalQuery( ${joinSession} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type = {{eventType}} ${filterQuery} ) as t group by 1 order by 2 desc limit 100`, - { - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - ...filters, - }, + params, ); } -async function clickhouseQuery( - websiteId: string, - data: { startDate: Date; endDate: Date; column: string; filters: object }, -) { - const { startDate, endDate, column, filters = {} } = data; +async function clickhouseQuery(websiteId: string, column: string, filters: QueryFilters) { const { parseFilters, rawQuery } = clickhouse; - const website = await loadWebsite(websiteId); - const { filterQuery } = parseFilters(filters); + const { filterQuery, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -68,12 +56,6 @@ async function clickhouseQuery( order by y desc limit 100 `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.pageView, - }, + params, ); } diff --git a/queries/analytics/sessions/getSessionStats.ts b/queries/analytics/sessions/getSessionStats.ts index 7633f242e..9ed01a594 100644 --- a/queries/analytics/sessions/getSessionStats.ts +++ b/queries/analytics/sessions/getSessionStats.ts @@ -2,43 +2,22 @@ import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import { EVENT_TYPE } from 'lib/constants'; -import { loadWebsite } from 'lib/load'; -import { maxDate } from 'lib/date'; +import { QueryFilters } from 'lib/types'; -export interface SessionStatsCriteria { - startDate: Date; - endDate: Date; - timezone?: string; - unit?: string; - filters: { - url?: string; - referrer?: string; - title?: string; - browser?: string; - os?: string; - device?: string; - screen?: string; - language?: string; - country?: string; - region?: string; - city?: string; - }; -} - -export async function getSessionStats( - ...args: [websiteId: string, criteria: SessionStatsCriteria] -) { +export async function getSessionStats(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId: string, criteria: SessionStatsCriteria) { - const { startDate, endDate, timezone = 'utc', unit = 'day', filters = {} } = criteria; +async function relationalQuery(websiteId: string, filters: QueryFilters) { + const { timezone = 'utc', unit = 'day' } = filters; const { getDateQuery, parseFilters, rawQuery } = prisma; - const website = await loadWebsite(websiteId); - const { filterQuery, joinSession } = parseFilters(filters); + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -53,22 +32,17 @@ async function relationalQuery(websiteId: string, criteria: SessionStatsCriteria ${filterQuery} group by 1 `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.pageView, - domain: website.domain, - }, + params, ); } -async function clickhouseQuery(websiteId: string, criteria: SessionStatsCriteria) { - const { startDate, endDate, timezone = 'UTC', unit = 'day', filters = {} } = criteria; +async function clickhouseQuery(websiteId: string, filters: QueryFilters) { + const { timezone = 'UTC', unit = 'day' } = filters; const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; - const website = await loadWebsite(websiteId); - const { filterQuery } = parseFilters(filters); + const { filterQuery, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -88,13 +62,6 @@ async function clickhouseQuery(websiteId: string, criteria: SessionStatsCriteria ) as g order by t `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.pageView, - domain: website.domain, - }, + params, ); } diff --git a/queries/analytics/stats/getWebsiteDateRange.ts b/queries/analytics/stats/getWebsiteDateRange.ts index 45885e45c..4fb24733a 100644 --- a/queries/analytics/stats/getWebsiteDateRange.ts +++ b/queries/analytics/stats/getWebsiteDateRange.ts @@ -1,9 +1,7 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { loadWebsite } from 'lib/load'; import { DEFAULT_RESET_DATE } from 'lib/constants'; -import { maxDate } from 'lib/date'; export async function getWebsiteDateRange(...args: [websiteId: string]) { return runQuery({ @@ -13,8 +11,8 @@ export async function getWebsiteDateRange(...args: [websiteId: string]) { } async function relationalQuery(websiteId: string) { - const { rawQuery } = prisma; - const website = await loadWebsite(websiteId); + const { rawQuery, parseFilters } = prisma; + const { params } = await parseFilters(websiteId, { startDate: new Date(DEFAULT_RESET_DATE) }); const result = await rawQuery( ` @@ -25,15 +23,15 @@ async function relationalQuery(websiteId: string) { where website_id = {{websiteId::uuid}} and created_at >= {{startDate}} `, - { websiteId, startDate: maxDate(new Date(DEFAULT_RESET_DATE), new Date(website.resetAt)) }, + params, ); return result[0] ?? null; } async function clickhouseQuery(websiteId: string) { - const { rawQuery } = clickhouse; - const website = await loadWebsite(websiteId); + const { rawQuery, parseFilters } = clickhouse; + const { params } = await parseFilters(websiteId, { startDate: new Date(DEFAULT_RESET_DATE) }); const result = await rawQuery( ` @@ -44,7 +42,7 @@ async function clickhouseQuery(websiteId: string) { where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime} `, - { websiteId, startDate: maxDate(new Date(DEFAULT_RESET_DATE), new Date(website.resetAt)) }, + params, ); return result[0] ?? null; diff --git a/queries/analytics/stats/getWebsiteStats.ts b/queries/analytics/stats/getWebsiteStats.ts index e048fc8f0..165195119 100644 --- a/queries/analytics/stats/getWebsiteStats.ts +++ b/queries/analytics/stats/getWebsiteStats.ts @@ -2,29 +2,21 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import { EVENT_TYPE } from 'lib/constants'; -import { loadWebsite } from 'lib/load'; -import { maxDate } from 'lib/date'; +import { QueryFilters } from 'lib/types'; -export async function getWebsiteStats( - ...args: [ - websiteId: string, - data: { startDate: Date; endDate: Date; type?: string; filters: object }, - ] -) { +export async function getWebsiteStats(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery( - websiteId: string, - criteria: { startDate: Date; endDate: Date; filters: object }, -) { - const { startDate, endDate, filters = {} } = criteria; +async function relationalQuery(websiteId: string, filters: QueryFilters) { const { getDateQuery, getTimestampIntervalQuery, parseFilters, rawQuery } = prisma; - const website = await loadWebsite(websiteId); - const { filterQuery, joinSession } = parseFilters(filters); + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -43,32 +35,23 @@ async function relationalQuery( join website on website_event.website_id = website.website_id ${joinSession} - where event_type = {{eventType}} - and website.website_id = {{websiteId::uuid}} + where website.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and event_type = {{eventType}} ${filterQuery} group by 1, 2 ) as t `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.pageView, - domain: website.domain, - }, + params, ); } -async function clickhouseQuery( - websiteId: string, - criteria: { startDate: Date; endDate: Date; filters: object }, -) { - const { startDate, endDate, filters = {} } = criteria; +async function clickhouseQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, getDateQuery, parseFilters } = clickhouse; - const website = await loadWebsite(websiteId); - const { filterQuery } = parseFilters(filters); + const { filterQuery, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.pageView, + }); return rawQuery( ` @@ -92,13 +75,6 @@ async function clickhouseQuery( group by session_id, time_series ) as t; `, - { - ...filters, - websiteId, - startDate: maxDate(startDate, website.resetAt), - endDate, - eventType: EVENT_TYPE.pageView, - domain: website.domain, - }, + params, ); } From c0ef8dace4f622a4951b012e2a9a1422344f60e9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 4 Aug 2023 13:33:41 -0700 Subject: [PATCH 011/357] Fixed session queries. --- pages/api/websites/[id]/pageviews.ts | 3 +-- queries/analytics/sessions/getSessionMetrics.ts | 5 +++-- queries/analytics/sessions/getSessionStats.ts | 5 +++-- queries/index.js | 1 + 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index 87c60d58e..c5532e76e 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -4,9 +4,8 @@ import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; -import { getPageviewStats } from 'queries'; +import { getPageviewStats, getSessionStats } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; -import { getSessionStats } from '../../../../queries/analytics/sessions/getSessionStats'; export interface WebsitePageviewRequestQuery { id: string; diff --git a/queries/analytics/sessions/getSessionMetrics.ts b/queries/analytics/sessions/getSessionMetrics.ts index 910c9785a..5ef387ecf 100644 --- a/queries/analytics/sessions/getSessionMetrics.ts +++ b/queries/analytics/sessions/getSessionMetrics.ts @@ -15,7 +15,7 @@ export async function getSessionMetrics( async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) { const { parseFilters, rawQuery } = prisma; - const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + const { filterQuery, params } = await parseFilters(websiteId, { ...filters, eventType: EVENT_TYPE.pageView, }); @@ -23,7 +23,8 @@ async function relationalQuery(websiteId: string, column: string, filters: Query return rawQuery( `select ${column} x, count(*) y from website_event - ${joinSession} + inner join session + on session.session_id = website_event.session_id where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} and website_event.event_type = {{eventType}} diff --git a/queries/analytics/sessions/getSessionStats.ts b/queries/analytics/sessions/getSessionStats.ts index 9ed01a594..b8884a44e 100644 --- a/queries/analytics/sessions/getSessionStats.ts +++ b/queries/analytics/sessions/getSessionStats.ts @@ -14,7 +14,7 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer async function relationalQuery(websiteId: string, filters: QueryFilters) { const { timezone = 'utc', unit = 'day' } = filters; const { getDateQuery, parseFilters, rawQuery } = prisma; - const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + const { filterQuery, params } = await parseFilters(websiteId, { ...filters, eventType: EVENT_TYPE.pageView, }); @@ -25,7 +25,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { ${getDateQuery('website_event.created_at', unit, timezone)} x, count(distinct website_event.session_id) y from website_event - ${joinSession} + inner join session + on session.session_id = website_event.session_id where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} and event_type = {{eventType}} diff --git a/queries/index.js b/queries/index.js index f509e0392..474ab31ee 100644 --- a/queries/index.js +++ b/queries/index.js @@ -19,6 +19,7 @@ export * from './analytics/sessions/createSession'; export * from './analytics/sessions/getSession'; export * from './analytics/sessions/getSessionMetrics'; export * from './analytics/sessions/getSessions'; +export * from './analytics/sessions/getSessionStats'; export * from './analytics/sessions/saveSessionData'; export * from './analytics/stats/getActiveVisitors'; export * from './analytics/stats/getRealtimeData'; From 2559263e91daeef2ab6f3b06675613da28191a07 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 4 Aug 2023 14:00:37 -0700 Subject: [PATCH 012/357] Fixed session queries. --- .../analytics/sessions/getSessionMetrics.ts | 18 +++++++++--------- queries/analytics/sessions/getSessionStats.ts | 5 ++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/queries/analytics/sessions/getSessionMetrics.ts b/queries/analytics/sessions/getSessionMetrics.ts index 5ef387ecf..f9bc2e942 100644 --- a/queries/analytics/sessions/getSessionMetrics.ts +++ b/queries/analytics/sessions/getSessionMetrics.ts @@ -21,15 +21,15 @@ async function relationalQuery(websiteId: string, column: string, filters: Query }); return rawQuery( - `select ${column} x, count(*) y - from website_event - inner join session - on session.session_id = website_event.session_id - where website_event.website_id = {{websiteId::uuid}} - and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type = {{eventType}} - ${filterQuery} - ) as t + ` + select ${column} x, count(*) y + from website_event + inner join session + on session.session_id = website_event.session_id + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type = {{eventType}} + ${filterQuery} group by 1 order by 2 desc limit 100`, diff --git a/queries/analytics/sessions/getSessionStats.ts b/queries/analytics/sessions/getSessionStats.ts index b8884a44e..9ed01a594 100644 --- a/queries/analytics/sessions/getSessionStats.ts +++ b/queries/analytics/sessions/getSessionStats.ts @@ -14,7 +14,7 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer async function relationalQuery(websiteId: string, filters: QueryFilters) { const { timezone = 'utc', unit = 'day' } = filters; const { getDateQuery, parseFilters, rawQuery } = prisma; - const { filterQuery, params } = await parseFilters(websiteId, { + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { ...filters, eventType: EVENT_TYPE.pageView, }); @@ -25,8 +25,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { ${getDateQuery('website_event.created_at', unit, timezone)} x, count(distinct website_event.session_id) y from website_event - inner join session - on session.session_id = website_event.session_id + ${joinSession} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} and event_type = {{eventType}} From fab818e7c207028889bc5e8220bae30792074f2c Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 4 Aug 2023 14:09:45 -0700 Subject: [PATCH 013/357] add referrer query inside undefine / ignore check --- lib/prisma.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/prisma.ts b/lib/prisma.ts index d1b9d0e5d..e1e209d21 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -72,14 +72,14 @@ function getFilterQuery(filters = {}): string { const filter = filters[key]; if (filter !== undefined && !IGNORED_FILTERS.includes(key)) { - const column = FILTER_COLUMNS[key] || key; - arr.push(`and ${column}={{${key}}}`); - } - - if (key === 'referrer') { - arr.push( - 'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)', - ); + if (key === 'referrer') { + arr.push( + 'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)', + ); + } else { + const column = FILTER_COLUMNS[key] || key; + arr.push(`and ${column}={{${key}}}`); + } } return arr; From 688705dbb3eb960bd8a45db69690374bed1d7083 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 4 Aug 2023 14:23:03 -0700 Subject: [PATCH 014/357] Added query options. --- lib/prisma.ts | 15 ++++++++++----- lib/types.ts | 6 +++++- pages/api/websites/[id]/events.ts | 6 ++---- queries/analytics/events/getEventMetrics.ts | 5 +++-- .../analytics/pageviews/getPageviewMetrics.ts | 16 ++++++++++------ .../analytics/sessions/getSessionMetrics.ts | 19 ++++++++++++------- 6 files changed, 42 insertions(+), 25 deletions(-) diff --git a/lib/prisma.ts b/lib/prisma.ts index d1b9d0e5d..0125e7f35 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -4,7 +4,7 @@ import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; import { FILTER_COLUMNS, IGNORED_FILTERS, SESSION_COLUMNS } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; -import { QueryFilters } from './types'; +import { QueryFilters, QueryOptions } from './types'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -88,13 +88,18 @@ function getFilterQuery(filters = {}): string { return query.join('\n'); } -async function parseFilters(websiteId, filters: QueryFilters & { [key: string]: any } = {}) { +async function parseFilters( + websiteId, + filters: QueryFilters & { [key: string]: any } = {}, + options: QueryOptions = {}, +) { const website = await loadWebsite(websiteId); return { - joinSession: Object.keys(filters).find(key => SESSION_COLUMNS[key]) - ? `inner join session on website_event.session_id = session.session_id` - : '', + joinSession: + options?.joinSession || Object.keys(filters).find(key => SESSION_COLUMNS.includes(key)) + ? `inner join session on website_event.session_id = session.session_id` + : '', filterQuery: getFilterQuery(filters), params: { ...filters, diff --git a/lib/types.ts b/lib/types.ts index 131740f88..3ce852aec 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -137,11 +137,11 @@ export interface QueryFilters { unit?: string; domain?: string; eventType?: number; + eventName?: string; url?: string; referrer?: string; title?: string; query?: string; - event?: string; os?: string; browser?: string; device?: string; @@ -150,3 +150,7 @@ export interface QueryFilters { city?: string; language?: string; } + +export interface QueryOptions { + joinSession?: boolean; +} diff --git a/pages/api/websites/[id]/events.ts b/pages/api/websites/[id]/events.ts index b9e3ac71c..7d4f999f6 100644 --- a/pages/api/websites/[id]/events.ts +++ b/pages/api/websites/[id]/events.ts @@ -43,10 +43,8 @@ export default async ( endDate, timezone, unit, - filters: { - url, - eventName, - }, + url, + eventName, }); return ok(res, events); diff --git a/queries/analytics/events/getEventMetrics.ts b/queries/analytics/events/getEventMetrics.ts index cf862f4a7..09a859461 100644 --- a/queries/analytics/events/getEventMetrics.ts +++ b/queries/analytics/events/getEventMetrics.ts @@ -5,7 +5,7 @@ import { WebsiteEventMetric, QueryFilters } from 'lib/types'; import { EVENT_TYPE } from 'lib/constants'; export async function getEventMetrics( - ...args: [websiteId: string, criteria: QueryFilters] + ...args: [websiteId: string, filters: QueryFilters] ): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -16,7 +16,7 @@ export async function getEventMetrics( async function relationalQuery(websiteId: string, filters: QueryFilters) { const { timezone = 'utc', unit = 'day' } = filters; const { rawQuery, getDateQuery, parseFilters } = prisma; - const { filterQuery, params } = await parseFilters(websiteId, { + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { ...filters, eventType: EVENT_TYPE.customEvent, }); @@ -28,6 +28,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { ${getDateQuery('created_at', unit, timezone)} t, count(*) y from website_event + ${joinSession} where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} and event_type = {{eventType}} diff --git a/queries/analytics/pageviews/getPageviewMetrics.ts b/queries/analytics/pageviews/getPageviewMetrics.ts index bbbd77052..b365d3f6b 100644 --- a/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/queries/analytics/pageviews/getPageviewMetrics.ts @@ -1,7 +1,7 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { EVENT_TYPE } from 'lib/constants'; +import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getPageviewMetrics( @@ -15,16 +15,20 @@ export async function getPageviewMetrics( async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) { const { rawQuery, parseFilters } = prisma; - const { filterQuery, joinSession, params } = await parseFilters(websiteId, { - ...filters, - eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, - }); + const { filterQuery, joinSession, params } = await parseFilters( + websiteId, + { + ...filters, + eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, + }, + { joinSession: SESSION_COLUMNS.includes(column) }, + ); return rawQuery( ` select ${column} x, count(*) y from website_event - ${joinSession} + ${joinSession} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} and event_type = {{eventType}} diff --git a/queries/analytics/sessions/getSessionMetrics.ts b/queries/analytics/sessions/getSessionMetrics.ts index f9bc2e942..fb546a73a 100644 --- a/queries/analytics/sessions/getSessionMetrics.ts +++ b/queries/analytics/sessions/getSessionMetrics.ts @@ -1,7 +1,7 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { EVENT_TYPE } from 'lib/constants'; +import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getSessionMetrics( @@ -15,17 +15,22 @@ export async function getSessionMetrics( async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) { const { parseFilters, rawQuery } = prisma; - const { filterQuery, params } = await parseFilters(websiteId, { - ...filters, - eventType: EVENT_TYPE.pageView, - }); + const { filterQuery, joinSession, params } = await parseFilters( + websiteId, + { + ...filters, + eventType: EVENT_TYPE.pageView, + }, + { + joinSession: SESSION_COLUMNS.includes(column), + }, + ); return rawQuery( ` select ${column} x, count(*) y from website_event - inner join session - on session.session_id = website_event.session_id + ${joinSession} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} and website_event.event_type = {{eventType}} From dbdbc90ee7b8361c6597a444010c50691b4dce59 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 4 Aug 2023 14:30:10 -0700 Subject: [PATCH 015/357] fix if else in referrer filter --- lib/prisma.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/prisma.ts b/lib/prisma.ts index aa9efe741..50f26f75a 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -72,13 +72,13 @@ function getFilterQuery(filters = {}): string { const filter = filters[key]; if (filter !== undefined && !IGNORED_FILTERS.includes(key)) { + const column = FILTER_COLUMNS[key] || key; + arr.push(`and ${column}={{${key}}}`); + if (key === 'referrer') { arr.push( 'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)', ); - } else { - const column = FILTER_COLUMNS[key] || key; - arr.push(`and ${column}={{${key}}}`); } } From e11766ca1c6ee44f5c477c8d655b1daa66545347 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 4 Aug 2023 15:21:14 -0700 Subject: [PATCH 016/357] Fixed event data queries. --- components/pages/event-data/EventDataTable.js | 13 +++--- lib/constants.ts | 6 +-- lib/types.ts | 1 + pages/api/event-data/events.ts | 12 +++-- pages/api/event-data/stats.ts | 5 ++- .../analytics/eventData/getEventDataEvents.ts | 45 +++++++++---------- 6 files changed, 44 insertions(+), 38 deletions(-) diff --git a/components/pages/event-data/EventDataTable.js b/components/pages/event-data/EventDataTable.js index 8260ac35b..88de01094 100644 --- a/components/pages/event-data/EventDataTable.js +++ b/components/pages/event-data/EventDataTable.js @@ -13,15 +13,18 @@ export function EventDataTable({ data = [] }) { return ( - + {row => ( - - {row.event} + + {row.eventName} )} - - {row => row.field} + + {row => row.fieldName} + + + {row => row.dataType} {({ total }) => total.toLocaleString()} diff --git a/lib/constants.ts b/lib/constants.ts index 9362b4560..dcb64143f 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -48,12 +48,12 @@ export const FILTER_COLUMNS = { referrer: 'referrer_domain', title: 'page_title', query: 'url_query', - event: 'event_name', region: 'subdivision1', - type: 'event_type', + eventType: 'event_type', + eventName: 'event_name', }; -export const IGNORED_FILTERS = ['startDate', 'endDate', 'timezone', 'unit', 'eventType']; +export const IGNORED_FILTERS = ['startDate', 'endDate', 'timezone', 'unit']; export const COLLECTION_TYPE = { event: 'event', diff --git a/lib/types.ts b/lib/types.ts index 3ce852aec..7cc9a619e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -153,4 +153,5 @@ export interface QueryFilters { export interface QueryOptions { joinSession?: boolean; + ignoreFilters?: string[]; } diff --git a/pages/api/event-data/events.ts b/pages/api/event-data/events.ts index 1d74c3d24..e86931081 100644 --- a/pages/api/event-data/events.ts +++ b/pages/api/event-data/events.ts @@ -21,15 +21,19 @@ export default async ( await useAuth(req, res); if (req.method === 'GET') { - const { websiteId, startAt, endAt, field, event } = req.query; + const { websiteId, startAt, endAt, eventName } = req.query; if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } - const data = await getEventDataEvents(websiteId, new Date(+startAt), new Date(+endAt), { - field, - event, + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataEvents(websiteId, { + startDate, + endDate, + eventName, }); return ok(res, data); diff --git a/pages/api/event-data/stats.ts b/pages/api/event-data/stats.ts index 8fdf9438b..969568e28 100644 --- a/pages/api/event-data/stats.ts +++ b/pages/api/event-data/stats.ts @@ -28,7 +28,10 @@ export default async ( return unauthorized(res); } - const results = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt)); + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const results = await getEventDataFields(websiteId, { startDate, endDate }); const data = results.reduce( (obj, row) => { diff --git a/queries/analytics/eventData/getEventDataEvents.ts b/queries/analytics/eventData/getEventDataEvents.ts index fae46db10..d0d4ff46c 100644 --- a/queries/analytics/eventData/getEventDataEvents.ts +++ b/queries/analytics/eventData/getEventDataEvents.ts @@ -4,7 +4,7 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { QueryFilters, WebsiteEventDataFields } from 'lib/types'; export async function getEventDataEvents( - ...args: [websiteId: string, filters: QueryFilters & { field?: string; event?: string }] + ...args: [websiteId: string, filters: QueryFilters] ): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -12,20 +12,18 @@ export async function getEventDataEvents( }); } -async function relationalQuery( - websiteId: string, - filters: QueryFilters & { field?: string; event?: string }, -) { +async function relationalQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, parseFilters } = prisma; + const { eventName } = filters; const { params } = await parseFilters(websiteId, filters); - if (event) { + if (eventName) { return rawQuery( ` select - website_event.event_name as event, - event_data.event_key as field, - event_data.data_type as type, + website_event.event_name as eventName, + event_data.event_key as fieldName, + event_data.data_type as dataType, event_data.string_value as value, count(*) as total from event_data @@ -33,7 +31,7 @@ async function relationalQuery( on website_event.event_id = event_data.website_event_id where event_data.website_id = {{websiteId::uuid}} and event_data.created_at between {{startDate}} and {{endDate}} - and websit_event.event_name = {{event}} + and websit_event.event_name = {{eventName}} group by website_event.event_name, event_data.event_key, event_data.data_type, event_data.string_value order by 1 asc, 2 asc, 3 asc, 4 desc `, @@ -44,9 +42,9 @@ async function relationalQuery( return rawQuery( ` select - website_event.event_name as event, - event_data.event_key as field, - event_data.data_type as type, + website_event.event_name as eventName, + event_data.event_key as fieldName, + event_data.data_type as dataType, count(*) as total from event_data inner join website_event @@ -61,21 +59,18 @@ async function relationalQuery( ); } -async function clickhouseQuery( - websiteId: string, - filters: QueryFilters & { field?: string; event?: string }, -) { +async function clickhouseQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, parseFilters } = clickhouse; - const { event } = filters; + const { eventName } = filters; const { params } = await parseFilters(websiteId, filters); - if (event) { + if (eventName) { return rawQuery( ` select - event_name as event, - event_key as field, - data_type as type, + event_name as eventName, + event_key as fieldName, + data_type as dataType, string_value as value, count(*) as total from event_data @@ -93,9 +88,9 @@ async function clickhouseQuery( return rawQuery( ` select - event_name as event, - event_key as field, - data_type as type, + event_name as eventName, + event_key as fieldName, + data_type as dataType, count(*) as total from event_data where website_id = {websiteId:UUID} From 91d2b596d6c6d1b317451dd0461f6b8a7a9702d5 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 4 Aug 2023 15:31:46 -0700 Subject: [PATCH 017/357] Fixed event data display. --- components/pages/event-data/EventDataTable.js | 5 +++-- .../pages/event-data/EventDataValueTable.js | 15 +++++++++------ components/pages/websites/WebsiteEventData.js | 14 +++++++------- queries/analytics/eventData/getEventDataEvents.ts | 2 +- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/components/pages/event-data/EventDataTable.js b/components/pages/event-data/EventDataTable.js index 88de01094..55fb0f590 100644 --- a/components/pages/event-data/EventDataTable.js +++ b/components/pages/event-data/EventDataTable.js @@ -2,6 +2,7 @@ import Link from 'next/link'; import { GridTable, GridColumn } from 'react-basics'; import { useMessages, usePageQuery } from 'hooks'; import Empty from 'components/common/Empty'; +import { DATA_TYPES } from 'lib/constants'; export function EventDataTable({ data = [] }) { const { formatMessage, labels } = useMessages(); @@ -15,7 +16,7 @@ export function EventDataTable({ data = [] }) { {row => ( - + {row.eventName} )} @@ -24,7 +25,7 @@ export function EventDataTable({ data = [] }) { {row => row.fieldName} - {row => row.dataType} + {row => DATA_TYPES[row.dataType]} {({ total }) => total.toLocaleString()} diff --git a/components/pages/event-data/EventDataValueTable.js b/components/pages/event-data/EventDataValueTable.js index 2637053ef..b52c46d3d 100644 --- a/components/pages/event-data/EventDataValueTable.js +++ b/components/pages/event-data/EventDataValueTable.js @@ -1,18 +1,19 @@ -import { GridTable, GridColumn, Button, Icon, Text, Flexbox } from 'react-basics'; +import { GridTable, GridColumn, Button, Icon, Text } from 'react-basics'; import { useMessages, usePageQuery } from 'hooks'; import Link from 'next/link'; import Icons from 'components/icons'; import PageHeader from 'components/layout/PageHeader'; import Empty from 'components/common/Empty'; +import { DATA_TYPES } from 'lib/constants'; -export function EventDataValueTable({ data = [], event }) { +export function EventDataValueTable({ data = [], eventName }) { const { formatMessage, labels } = useMessages(); const { resolveUrl } = usePageQuery(); const Title = () => { return ( <> - + - {event} + {eventName} ); }; @@ -31,8 +32,10 @@ export function EventDataValueTable({ data = [], event }) { {data.length <= 0 && } {data.length > 0 && ( - - + + + {row => DATA_TYPES[row.dataType]} + {({ total }) => total.toLocaleString()} diff --git a/components/pages/websites/WebsiteEventData.js b/components/pages/websites/WebsiteEventData.js index 7f9a68294..d6cb26397 100644 --- a/components/pages/websites/WebsiteEventData.js +++ b/components/pages/websites/WebsiteEventData.js @@ -5,18 +5,18 @@ import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetric import { useDateRange, useApi, usePageQuery } from 'hooks'; import styles from './WebsiteEventData.module.css'; -function useData(websiteId, event) { +function useData(websiteId, eventName) { const [dateRange] = useDateRange(websiteId); const { startDate, endDate } = dateRange; const { get, useQuery } = useApi(); const { data, error, isLoading } = useQuery( - ['event-data:events', { websiteId, startDate, endDate, event }], + ['event-data:events', { websiteId, startDate, endDate, eventName }], () => get('/event-data/events', { websiteId, startAt: +startDate, endAt: +endDate, - event, + eventName, }), { enabled: !!(websiteId && startDate && endDate) }, ); @@ -26,15 +26,15 @@ function useData(websiteId, event) { export default function WebsiteEventData({ websiteId }) { const { - query: { event }, + query: { eventName }, } = usePageQuery(); - const { data } = useData(websiteId, event); + const { data } = useData(websiteId, eventName); return ( - {!event && } - {event && } + {!eventName && } + {eventName && } ); } diff --git a/queries/analytics/eventData/getEventDataEvents.ts b/queries/analytics/eventData/getEventDataEvents.ts index d0d4ff46c..dcb21283f 100644 --- a/queries/analytics/eventData/getEventDataEvents.ts +++ b/queries/analytics/eventData/getEventDataEvents.ts @@ -76,7 +76,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { from event_data where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} - and event_name = {event:String} + and event_name = {eventName:String} group by event_key, data_type, string_value, event_name order by 1 asc, 2 asc, 3 asc, 4 desc limit 100 From 5e1111db5d6f486dad585297c70d37f90b4cb108 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 5 Aug 2023 09:09:54 -0700 Subject: [PATCH 018/357] Update to insights query. --- components/pages/reports/FilterSelectForm.js | 13 +++++++ .../reports/insights/InsightsParameters.js | 24 +++++-------- lib/constants.ts | 11 +++++- pages/api/reports/insights.ts | 14 ++++---- queries/analytics/reports/getInsights.ts | 35 ++++++++----------- 5 files changed, 54 insertions(+), 43 deletions(-) create mode 100644 components/pages/reports/FilterSelectForm.js diff --git a/components/pages/reports/FilterSelectForm.js b/components/pages/reports/FilterSelectForm.js new file mode 100644 index 000000000..0dc107b0d --- /dev/null +++ b/components/pages/reports/FilterSelectForm.js @@ -0,0 +1,13 @@ +import { useState } from 'react'; +import FieldSelectForm from './FieldSelectForm'; +import FieldFilterForm from './FieldFilterForm'; + +export default function FilterSelectForm({ fields, onSelect }) { + const [field, setField] = useState(); + + if (!field) { + return ; + } + + return ; +} diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js index 692c5eadb..5b9b8f185 100644 --- a/components/pages/reports/insights/InsightsParameters.js +++ b/components/pages/reports/insights/InsightsParameters.js @@ -7,9 +7,9 @@ import Icons from 'components/icons'; import BaseParameters from '../BaseParameters'; import ParameterList from '../ParameterList'; import styles from './InsightsParameters.module.css'; -import FieldSelectForm from '../FieldSelectForm'; import PopupForm from '../PopupForm'; -import FieldFilterForm from '../FieldFilterForm'; +import FilterSelectForm from '../FilterSelectForm'; +import FieldSelectForm from '../FieldSelectForm'; const fieldOptions = [ { name: 'url', type: 'string' }, @@ -30,17 +30,15 @@ export function InsightsParameters() { const { formatMessage, labels } = useMessages(); const ref = useRef(null); const { parameters } = report || {}; - const { websiteId, dateRange, fields, filters, groups } = parameters || {}; - const queryEnabled = websiteId && dateRange && fields?.length; + const { websiteId, dateRange, filters, groups } = parameters || {}; + const queryEnabled = websiteId && dateRange && (filters?.length || groups?.length); const parameterGroups = [ - { label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields }, { label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters }, { label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups }, ]; const parameterData = { - fields, filters, groups, }; @@ -73,11 +71,11 @@ export function InsightsParameters() { {(close, element) => { return ( - {group === REPORT_PARAMETERS.fields && ( - - )} {group === REPORT_PARAMETERS.filters && ( - + + )} + {group === REPORT_PARAMETERS.groups && ( + )} ); @@ -100,12 +98,6 @@ export function InsightsParameters() { {({ name, value }) => { return (
- {group === REPORT_PARAMETERS.fields && ( - <> -
{name}
-
{value}
- - )} {group === REPORT_PARAMETERS.filters && ( <>
{name}
diff --git a/lib/constants.ts b/lib/constants.ts index dcb64143f..cb520c272 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -53,7 +53,16 @@ export const FILTER_COLUMNS = { eventName: 'event_name', }; -export const IGNORED_FILTERS = ['startDate', 'endDate', 'timezone', 'unit']; +export const IGNORED_FILTERS = [ + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'eventType', + 'fields', + 'filters', + 'groups', +]; export const COLLECTION_TYPE = { event: 'event', diff --git a/pages/api/reports/insights.ts b/pages/api/reports/insights.ts index a40c21246..f245153ff 100644 --- a/pages/api/reports/insights.ts +++ b/pages/api/reports/insights.ts @@ -36,13 +36,15 @@ export default async ( return unauthorized(res); } - const data = await getInsights(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - fields, - filters, + const data = await getInsights( + websiteId, + { + ...filters, + startDate: new Date(startDate), + endDate: new Date(endDate), + }, groups, - }); + ); return ok(res, data); } diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index dfe7c3975..93569fe74 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -4,7 +4,9 @@ import clickhouse from 'lib/clickhouse'; import { EVENT_TYPE } from 'lib/constants'; import { QueryFilters } from 'lib/types'; -export async function getInsights(...args: [websiteId: string, filters: QueryFilters]) { +export async function getInsights( + ...args: [websiteId: string, filters: QueryFilters, groups: { name: string; type: string }[]] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -14,6 +16,7 @@ export async function getInsights(...args: [websiteId: string, filters: QueryFil async function relationalQuery( websiteId: string, filters: QueryFilters, + groups: { name: string; type: string }[], ): Promise< { x: string; @@ -46,6 +49,7 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, filters: QueryFilters, + groups: { name: string; type: string }[], ): Promise< { x: string; @@ -53,7 +57,6 @@ async function clickhouseQuery( }[] > { const { parseFilters, rawQuery } = clickhouse; - const { fields } = filters; const { filterQuery, params } = await parseFilters(websiteId, { ...filters, eventType: EVENT_TYPE.pageView, @@ -62,14 +65,14 @@ async function clickhouseQuery( return rawQuery( ` select - ${parseFields(fields)} + ${parseFields(groups)} from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} and event_type = {eventType:UInt32} ${filterQuery} - group by ${fields.map(({ name }) => name).join(',')} - order by total desc + group by ${groups.map(({ name }) => name).join(',')} + order by 1 desc limit 500 `, params, @@ -77,22 +80,14 @@ async function clickhouseQuery( } function parseFields(fields) { - let count = false; - let distinct = false; + const query = fields.reduce( + (arr, field) => { + const { name } = field; - const query = fields.reduce((arr, field) => { - const { name, value } = field; - - if (!count && value === 'total') { - count = true; - arr = arr.concat(`count(*) as views`); - } else if (!distinct && value === 'unique') { - distinct = true; - //arr = arr.concat(`count(distinct ${name})`); - } - - return arr.concat(name); - }, []); + return arr.concat(name); + }, + ['count(*) as views', 'count(distinct session_id) as visitors'], + ); return query.join(',\n'); } From f48720c9159d4d4ba7ea86e0098f50896e7b484c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 6 Aug 2023 22:52:17 -0700 Subject: [PATCH 019/357] Breakdown feature for insights report. --- components/messages.js | 7 +++ components/pages/reports/ReportDetails.js | 2 + .../reports/insights/InsightsParameters.js | 54 +++++++++---------- .../pages/reports/insights/InsightsTable.js | 9 ++-- pages/api/reports/insights.ts | 19 +++---- queries/analytics/reports/getInsights.ts | 6 +-- 6 files changed, 51 insertions(+), 46 deletions(-) diff --git a/components/messages.js b/components/messages.js index a31e28751..286a5788f 100644 --- a/components/messages.js +++ b/components/messages.js @@ -162,6 +162,13 @@ export const labels = defineMessages({ totalRecords: { id: 'labels.total-records', defaultMessage: 'Total records' }, insights: { id: 'label.insights', defaultMessage: 'Insights' }, dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' }, + referrer: { id: 'label.referrer', defaultMessage: 'Referrer' }, + country: { id: 'label.country', defaultMessage: 'Country' }, + region: { id: 'label.region', defaultMessage: 'Region' }, + city: { id: 'label.city', defaultMessage: 'City' }, + browser: { id: 'label.browser', defaultMessage: 'Browser' }, + device: { id: 'label.device', defaultMessage: 'Device' }, + pageTitle: { id: 'label.pageTitle', defaultMessage: 'Page title' }, }); export const messages = defineMessages({ diff --git a/components/pages/reports/ReportDetails.js b/components/pages/reports/ReportDetails.js index c41d12f6a..df1307609 100644 --- a/components/pages/reports/ReportDetails.js +++ b/components/pages/reports/ReportDetails.js @@ -1,9 +1,11 @@ import FunnelReport from './funnel/FunnelReport'; import EventDataReport from './event-data/EventDataReport'; +import InsightsReport from './insights/InsightsReport'; const reports = { funnel: FunnelReport, 'event-data': EventDataReport, + insights: InsightsReport, }; export default function ReportDetails({ reportId, reportType }) { diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js index 5b9b8f185..5d7e1fca4 100644 --- a/components/pages/reports/insights/InsightsParameters.js +++ b/components/pages/reports/insights/InsightsParameters.js @@ -11,20 +11,6 @@ import PopupForm from '../PopupForm'; import FilterSelectForm from '../FilterSelectForm'; import FieldSelectForm from '../FieldSelectForm'; -const fieldOptions = [ - { name: 'url', type: 'string' }, - { name: 'title', type: 'string' }, - { name: 'referrer', type: 'string' }, - { name: 'query', type: 'string' }, - { name: 'browser', type: 'string' }, - { name: 'os', type: 'string' }, - { name: 'device', type: 'string' }, - { name: 'country', type: 'string' }, - { name: 'region', type: 'string' }, - { name: 'city', type: 'string' }, - { name: 'language', type: 'string' }, -]; - export function InsightsParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); @@ -33,9 +19,23 @@ export function InsightsParameters() { const { websiteId, dateRange, filters, groups } = parameters || {}; const queryEnabled = websiteId && dateRange && (filters?.length || groups?.length); + const fieldOptions = [ + { name: 'url_path', type: 'string', label: formatMessage(labels.url) }, + { name: 'page_title', type: 'string', label: formatMessage(labels.pageTitle) }, + { name: 'referrer_domain', type: 'string', label: formatMessage(labels.referrer) }, + { name: 'url_query', type: 'string', label: formatMessage(labels.query) }, + { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, + { name: 'os', type: 'string', label: formatMessage(labels.os) }, + { name: 'device', type: 'string', label: formatMessage(labels.device) }, + { name: 'country', type: 'string', label: formatMessage(labels.country) }, + { name: 'region', type: 'string', label: formatMessage(labels.region) }, + { name: 'city', type: 'string', label: formatMessage(labels.city) }, + { name: 'language', type: 'string', label: formatMessage(labels.language) }, + ]; + const parameterGroups = [ - { label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters }, { label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups }, + { label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters }, ]; const parameterData = { @@ -71,12 +71,12 @@ export function InsightsParameters() { {(close, element) => { return ( - {group === REPORT_PARAMETERS.filters && ( - - )} {group === REPORT_PARAMETERS.groups && ( )} + {group === REPORT_PARAMETERS.filters && ( + + )} ); }} @@ -95,19 +95,19 @@ export function InsightsParameters() { items={parameterData[group]} onRemove={index => handleRemove(group, index)} > - {({ name, value }) => { + {({ value, label }) => { return (
- {group === REPORT_PARAMETERS.filters && ( - <> -
{name}
-
{value[0]}
-
{value[1]}
- - )} {group === REPORT_PARAMETERS.groups && ( <> -
{name}
+
{label}
+ + )} + {group === REPORT_PARAMETERS.filters && ( + <> +
{label}
+
{value[0]}
+
{value[1]}
)}
diff --git a/components/pages/reports/insights/InsightsTable.js b/components/pages/reports/insights/InsightsTable.js index d751445b9..7832a899a 100644 --- a/components/pages/reports/insights/InsightsTable.js +++ b/components/pages/reports/insights/InsightsTable.js @@ -6,14 +6,15 @@ import { ReportContext } from '../Report'; export function InsightsTable() { const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); - const { fields = [] } = report?.parameters || {}; + const { groups = [] } = report?.parameters || {}; return ( - {fields.map(({ name }) => { - return ; + {groups.map(({ name, label }) => { + return ; })} - + + ); } diff --git a/pages/api/reports/insights.ts b/pages/api/reports/insights.ts index f245153ff..44f720637 100644 --- a/pages/api/reports/insights.ts +++ b/pages/api/reports/insights.ts @@ -13,7 +13,7 @@ export interface InsightsRequestBody { }; fields: { name: string; type: string; value: string }[]; filters: string[]; - groups: string[]; + groups: { name: string; type: string }[]; } export default async ( @@ -27,24 +27,19 @@ export default async ( const { websiteId, dateRange: { startDate, endDate }, - fields, - filters, groups, + filters, } = req.body; if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } - const data = await getInsights( - websiteId, - { - ...filters, - startDate: new Date(startDate), - endDate: new Date(endDate), - }, - groups, - ); + const data = await getInsights(websiteId, groups, { + ...filters, + startDate: new Date(startDate), + endDate: new Date(endDate), + }); return ok(res, data); } diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index 93569fe74..0f778555a 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -5,7 +5,7 @@ import { EVENT_TYPE } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getInsights( - ...args: [websiteId: string, filters: QueryFilters, groups: { name: string; type: string }[]] + ...args: [websiteId: string, groups: { name: string; type: string }[], filters: QueryFilters] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -15,8 +15,8 @@ export async function getInsights( async function relationalQuery( websiteId: string, - filters: QueryFilters, groups: { name: string; type: string }[], + filters: QueryFilters, ): Promise< { x: string; @@ -48,8 +48,8 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - filters: QueryFilters, groups: { name: string; type: string }[], + filters: QueryFilters, ): Promise< { x: string; From f57fbe6ba1543882ebf69bad929b73654f22519b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 6 Aug 2023 22:52:58 -0700 Subject: [PATCH 020/357] Support Cloudflare headers for city and region. --- lib/detect.ts | 15 +++++++-------- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/detect.ts b/lib/detect.ts index 9c1e1fa49..43dac6499 100644 --- a/lib/detect.ts +++ b/lib/detect.ts @@ -3,6 +3,7 @@ import { getClientIp } from 'request-ip'; import { browserName, detectOS } from 'detect-browser'; import isLocalhost from 'is-localhost-ip'; import maxmind from 'maxmind'; +import { safeDecodeURIComponent } from 'next-basics'; import { DESKTOP_OS, @@ -65,20 +66,18 @@ export async function getLocation(ip, req) { // Cloudflare headers if (req.headers['cf-ipcountry']) { return { - country: req.headers['cf-ipcountry'], + country: safeDecodeURIComponent(req.headers['cf-ipcountry']), + subdivision1: safeDecodeURIComponent(req.headers['cf-region-code']), + city: safeDecodeURIComponent(req.headers['cf-ipcity']), }; } // Vercel headers if (req.headers['x-vercel-ip-country']) { - const country = req.headers['x-vercel-ip-country']; - const region = req.headers['x-vercel-ip-country-region']; - const city = req.headers['x-vercel-ip-city']; - return { - country, - subdivision1: region, - city: city ? decodeURIComponent(city) : undefined, + country: safeDecodeURIComponent(req.headers['x-vercel-ip-country']), + subdivision1: safeDecodeURIComponent(req.headers['x-vercel-ip-country-region']), + city: safeDecodeURIComponent(req.headers['x-vercel-ip-city']), }; } diff --git a/package.json b/package.json index 868b3cdfb..647cdf418 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", "next": "13.3.1", - "next-basics": "^0.35.0", + "next-basics": "^0.36.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 275bcd631..d9224c2a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6371,10 +6371,10 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next-basics@^0.35.0: - version "0.35.0" - resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.35.0.tgz#aa68fd35a0e3fbabfdaf570cd092b6a7cf8df6f5" - integrity sha512-yqXZMLe109hSJ8sebI/f2m1XNnVuQowpELOhZSGOFOmLfvUyFBAEi0ULdqX1eb8xbttLgjcrumrZfMgmEwuCPw== +next-basics@^0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.36.0.tgz#b1675c3f2b98df2fec8df605095dab7d17f9dc7b" + integrity sha512-Nwou8pCjFuoD/ZxUw9iKC7hhZeWbo/ng0ze74yck3W89MNc/CepwCDziflAHY5XcmIVNmpXOCu9OfmzTdVRPWQ== dependencies: bcryptjs "^2.4.3" jsonwebtoken "^9.0.0" From 112005212e90ef678a3e8475f9a8c909cb49ee31 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 6 Aug 2023 23:04:19 -0700 Subject: [PATCH 021/357] Fixed events query. --- lib/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/constants.ts b/lib/constants.ts index cb520c272..67ed1c513 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -51,6 +51,7 @@ export const FILTER_COLUMNS = { region: 'subdivision1', eventType: 'event_type', eventName: 'event_name', + event: 'event_name', }; export const IGNORED_FILTERS = [ From 9d86385f5c6b48c515974dad56eee7c000b25300 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 7 Aug 2023 12:43:43 -0700 Subject: [PATCH 022/357] Updated filtering logic. --- components/pages/event-data/EventDataTable.js | 2 +- .../pages/event-data/EventDataValueTable.js | 6 +++--- components/pages/websites/WebsiteEventData.js | 8 ++++---- lib/clickhouse.ts | 6 +++--- lib/constants.ts | 19 ++++++------------- lib/prisma.ts | 6 +++--- lib/types.ts | 3 +-- pages/api/event-data/events.ts | 4 ++-- 8 files changed, 23 insertions(+), 31 deletions(-) diff --git a/components/pages/event-data/EventDataTable.js b/components/pages/event-data/EventDataTable.js index 55fb0f590..b0d11b9d4 100644 --- a/components/pages/event-data/EventDataTable.js +++ b/components/pages/event-data/EventDataTable.js @@ -16,7 +16,7 @@ export function EventDataTable({ data = [] }) { {row => ( - + {row.eventName} )} diff --git a/components/pages/event-data/EventDataValueTable.js b/components/pages/event-data/EventDataValueTable.js index b52c46d3d..3688ad094 100644 --- a/components/pages/event-data/EventDataValueTable.js +++ b/components/pages/event-data/EventDataValueTable.js @@ -6,14 +6,14 @@ import PageHeader from 'components/layout/PageHeader'; import Empty from 'components/common/Empty'; import { DATA_TYPES } from 'lib/constants'; -export function EventDataValueTable({ data = [], eventName }) { +export function EventDataValueTable({ data = [], event }) { const { formatMessage, labels } = useMessages(); const { resolveUrl } = usePageQuery(); const Title = () => { return ( <> - + - {eventName} + {event} ); }; diff --git a/components/pages/websites/WebsiteEventData.js b/components/pages/websites/WebsiteEventData.js index d6cb26397..5e2083557 100644 --- a/components/pages/websites/WebsiteEventData.js +++ b/components/pages/websites/WebsiteEventData.js @@ -26,15 +26,15 @@ function useData(websiteId, eventName) { export default function WebsiteEventData({ websiteId }) { const { - query: { eventName }, + query: { event }, } = usePageQuery(); - const { data } = useData(websiteId, eventName); + const { data } = useData(websiteId, event); return ( - {!eventName && } - {eventName && } + {!event && } + {event && } ); } diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index 6d5bcf428..a40567d3f 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -3,7 +3,7 @@ import dateFormat from 'dateformat'; import debug from 'debug'; import { CLICKHOUSE } from 'lib/db'; import { QueryFilters } from './types'; -import { FILTER_COLUMNS, IGNORED_FILTERS } from './constants'; +import { FILTER_COLUMNS } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; @@ -66,9 +66,9 @@ function getDateFormat(date) { function getFilterQuery(filters = {}) { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; + const column = FILTER_COLUMNS[key]; - if (filter !== undefined && !IGNORED_FILTERS.includes(key)) { - const column = FILTER_COLUMNS[key] || key; + if (filter !== undefined && column) { arr.push(`and ${column} = {${key}:String}`); } diff --git a/lib/constants.ts b/lib/constants.ts index 67ed1c513..887f90a96 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -48,23 +48,16 @@ export const FILTER_COLUMNS = { referrer: 'referrer_domain', title: 'page_title', query: 'url_query', + os: 'os', + browser: 'browser', + device: 'device', + country: 'country', region: 'subdivision1', - eventType: 'event_type', - eventName: 'event_name', + city: 'city', + language: 'language', event: 'event_name', }; -export const IGNORED_FILTERS = [ - 'startDate', - 'endDate', - 'timezone', - 'unit', - 'eventType', - 'fields', - 'filters', - 'groups', -]; - export const COLLECTION_TYPE = { event: 'event', identify: 'identify', diff --git a/lib/prisma.ts b/lib/prisma.ts index 50f26f75a..a9ddf1889 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -1,7 +1,7 @@ import prisma from '@umami/prisma-client'; import moment from 'moment-timezone'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; -import { FILTER_COLUMNS, IGNORED_FILTERS, SESSION_COLUMNS } from './constants'; +import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; import { QueryFilters, QueryOptions } from './types'; @@ -70,9 +70,9 @@ function getTimestampIntervalQuery(field: string): string { function getFilterQuery(filters = {}): string { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; + const column = FILTER_COLUMNS[key]; - if (filter !== undefined && !IGNORED_FILTERS.includes(key)) { - const column = FILTER_COLUMNS[key] || key; + if (filter !== undefined && column) { arr.push(`and ${column}={{${key}}}`); if (key === 'referrer') { diff --git a/lib/types.ts b/lib/types.ts index 7cc9a619e..6057c42ec 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -135,9 +135,7 @@ export interface QueryFilters { endDate?: Date; timezone?: string; unit?: string; - domain?: string; eventType?: number; - eventName?: string; url?: string; referrer?: string; title?: string; @@ -149,6 +147,7 @@ export interface QueryFilters { region?: string; city?: string; language?: string; + event?: string; } export interface QueryOptions { diff --git a/pages/api/event-data/events.ts b/pages/api/event-data/events.ts index e86931081..e83e541bf 100644 --- a/pages/api/event-data/events.ts +++ b/pages/api/event-data/events.ts @@ -21,7 +21,7 @@ export default async ( await useAuth(req, res); if (req.method === 'GET') { - const { websiteId, startAt, endAt, eventName } = req.query; + const { websiteId, startAt, endAt, event } = req.query; if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); @@ -33,7 +33,7 @@ export default async ( const data = await getEventDataEvents(websiteId, { startDate, endDate, - eventName, + event, }); return ok(res, data); From 7da7f58cbe174120021b1889efa555a83845b70d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 7 Aug 2023 13:28:32 -0700 Subject: [PATCH 023/357] Added "columns" to query options. Added events count to event data metrics. --- .../pages/event-data/EventDataMetricsBar.js | 5 ++ lib/clickhouse.ts | 9 +-- lib/prisma.ts | 6 +- lib/types.ts | 12 ++-- pages/api/event-data/fields.ts | 6 +- pages/api/event-data/stats.ts | 4 +- .../analytics/eventData/getEventDataFields.ts | 68 +++++-------------- 7 files changed, 45 insertions(+), 65 deletions(-) diff --git a/components/pages/event-data/EventDataMetricsBar.js b/components/pages/event-data/EventDataMetricsBar.js index 48843287c..90f065d5f 100644 --- a/components/pages/event-data/EventDataMetricsBar.js +++ b/components/pages/event-data/EventDataMetricsBar.js @@ -28,6 +28,11 @@ export function EventDataMetricsBar({ websiteId }) { {!error && isFetched && ( <> + { const filter = filters[key]; - const column = FILTER_COLUMNS[key]; + const column = FILTER_COLUMNS[key] ?? options?.columns?.[key]; if (filter !== undefined && column) { arr.push(`and ${column} = {${key}:String}`); @@ -85,11 +85,12 @@ function getFilterQuery(filters = {}) { async function parseFilters( websiteId: string, filters: QueryFilters & { [key: string]: any } = {}, + options?: QueryOptions, ) { const website = await loadWebsite(websiteId); return { - filterQuery: getFilterQuery(filters), + filterQuery: getFilterQuery(filters, options), params: { ...filters, websiteId, diff --git a/lib/prisma.ts b/lib/prisma.ts index a9ddf1889..753f1ae4a 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -67,10 +67,10 @@ function getTimestampIntervalQuery(field: string): string { } } -function getFilterQuery(filters = {}): string { +function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; - const column = FILTER_COLUMNS[key]; + const column = FILTER_COLUMNS[key] ?? options?.columns?.[key]; if (filter !== undefined && column) { arr.push(`and ${column}={{${key}}}`); @@ -100,7 +100,7 @@ async function parseFilters( options?.joinSession || Object.keys(filters).find(key => SESSION_COLUMNS.includes(key)) ? `inner join session on website_event.session_id = session.session_id` : '', - filterQuery: getFilterQuery(filters), + filterQuery: getFilterQuery(filters, options), params: { ...filters, websiteId, diff --git a/lib/types.ts b/lib/types.ts index 6057c42ec..dc54fd473 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -80,15 +80,15 @@ export interface WebsiteEventMetric { } export interface WebsiteEventDataStats { - field: string; - type: number; + fieldName: string; + dataType: number; total: number; } export interface WebsiteEventDataFields { - field: string; - type: number; - value?: string; + fieldName: string; + dataType: number; + fieldValue?: string; total: number; } @@ -152,5 +152,5 @@ export interface QueryFilters { export interface QueryOptions { joinSession?: boolean; - ignoreFilters?: string[]; + columns?: { [key: string]: string }; } diff --git a/pages/api/event-data/fields.ts b/pages/api/event-data/fields.ts index 18b74bc32..f21bd570f 100644 --- a/pages/api/event-data/fields.ts +++ b/pages/api/event-data/fields.ts @@ -11,6 +11,7 @@ export interface EventDataFieldsRequestBody { startDate: string; endDate: string; }; + field?: string; } export default async ( @@ -27,7 +28,10 @@ export default async ( return unauthorized(res); } - const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt), field); + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataFields(websiteId, { startDate, endDate, field }); return ok(res, data); } diff --git a/pages/api/event-data/stats.ts b/pages/api/event-data/stats.ts index 969568e28..74f420c49 100644 --- a/pages/api/event-data/stats.ts +++ b/pages/api/event-data/stats.ts @@ -32,16 +32,18 @@ export default async ( const endDate = new Date(+endAt); const results = await getEventDataFields(websiteId, { startDate, endDate }); + const events = new Set(); const data = results.reduce( (obj, row) => { + events.add(row.fieldName); obj.records += Number(row.total); return obj; }, { fields: results.length, records: 0 }, ); - return ok(res, data); + return ok(res, { ...data, events: events.size }); } return methodNotAllowed(res); diff --git a/queries/analytics/eventData/getEventDataFields.ts b/queries/analytics/eventData/getEventDataFields.ts index a27f22810..c61de517c 100644 --- a/queries/analytics/eventData/getEventDataFields.ts +++ b/queries/analytics/eventData/getEventDataFields.ts @@ -14,39 +14,23 @@ export async function getEventDataFields( async function relationalQuery(websiteId: string, filters: QueryFilters & { field?: string }) { const { rawQuery, parseFilters } = prisma; - const { field } = filters; - const { params } = await parseFilters(websiteId, filters); - - if (field) { - return rawQuery( - ` - select - event_key as field, - string_value as value, - count(*) as total - from event_data - where website_id = {{websiteId::uuid}} - and event_key = {{field}} - and created_at between {{startDate}} and {{endDate}} - group by event_key, string_value - order by 3 desc, 2 desc, 1 asc - limit 100 - `, - params, - ); - } + const { filterQuery, params } = await parseFilters(websiteId, filters, { + columns: { field: 'event_key' }, + }); return rawQuery( ` select - event_key as field, - data_type as type, + event_key as fieldName, + data_type as dataType, + string_value as fieldValue, count(*) as total from event_data where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} - group by event_key, data_type - order by 3 desc, 2 asc, 1 asc + ${filterQuery} + group by event_key, data_type, string_value + order by 3 desc, 2 desc, 1 asc limit 100 `, params, @@ -55,39 +39,23 @@ async function relationalQuery(websiteId: string, filters: QueryFilters & { fiel async function clickhouseQuery(websiteId: string, filters: QueryFilters & { field?: string }) { const { rawQuery, parseFilters } = clickhouse; - const { field } = filters; - const { params } = await parseFilters(websiteId, filters); - - if (field) { - return rawQuery( - ` - select - event_key as field, - string_value as value, - count(*) as total - from event_data - where website_id = {websiteId:UUID} - and event_key = {field:String} - and created_at between {startDate:DateTime} and {endDate:DateTime} - group by event_key, string_value - order by 3 desc, 2 desc, 1 asc - limit 100 - `, - params, - ); - } + const { filterQuery, params } = await parseFilters(websiteId, filters, { + columns: { field: 'event_key' }, + }); return rawQuery( ` select - event_key as field, - data_type as type, + event_key as fieldName, + data_type as dataType, + string_value as fieldValue, count(*) as total from event_data where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} - group by event_key, data_type - order by 3 desc, 2 asc, 1 asc + ${filterQuery} + group by event_key, data_type, string_value + order by 3 desc, 2 desc, 1 asc limit 100 `, params, From a71cf675ae1968b977a87b52f7c1f77165e54195 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 7 Aug 2023 13:43:44 -0700 Subject: [PATCH 024/357] Fixed event data query. --- queries/analytics/eventData/getEventDataEvents.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/queries/analytics/eventData/getEventDataEvents.ts b/queries/analytics/eventData/getEventDataEvents.ts index dcb21283f..ec0939b64 100644 --- a/queries/analytics/eventData/getEventDataEvents.ts +++ b/queries/analytics/eventData/getEventDataEvents.ts @@ -14,10 +14,10 @@ export async function getEventDataEvents( async function relationalQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, parseFilters } = prisma; - const { eventName } = filters; + const { event } = filters; const { params } = await parseFilters(websiteId, filters); - if (eventName) { + if (event) { return rawQuery( ` select @@ -31,7 +31,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { on website_event.event_id = event_data.website_event_id where event_data.website_id = {{websiteId::uuid}} and event_data.created_at between {{startDate}} and {{endDate}} - and websit_event.event_name = {{eventName}} + and website_event.event_name = {{event}} group by website_event.event_name, event_data.event_key, event_data.data_type, event_data.string_value order by 1 asc, 2 asc, 3 asc, 4 desc `, @@ -61,10 +61,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, parseFilters } = clickhouse; - const { eventName } = filters; + const { event } = filters; const { params } = await parseFilters(websiteId, filters); - if (eventName) { + if (event) { return rawQuery( ` select @@ -76,7 +76,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { from event_data where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} - and event_name = {eventName:String} + and event_name = {event:String} group by event_key, data_type, string_value, event_name order by 1 asc, 2 asc, 3 asc, 4 desc limit 100 From 13530c9cdcf38289c80cc42563b05cb92dcf53ea Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 7 Aug 2023 14:01:53 -0700 Subject: [PATCH 025/357] add psql query for retention --- .../reports/retention/RetentionParameters.js | 14 +- .../reports/retention/RetentionReport.js | 6 +- .../pages/reports/retention/RetentionTable.js | 25 +- pages/api/reports/retention.ts | 15 +- queries/analytics/reports/getRetention.ts | 328 +++++++++--------- 5 files changed, 193 insertions(+), 195 deletions(-) diff --git a/components/pages/reports/retention/RetentionParameters.js b/components/pages/reports/retention/RetentionParameters.js index 29c0eff2f..bf40236df 100644 --- a/components/pages/reports/retention/RetentionParameters.js +++ b/components/pages/reports/retention/RetentionParameters.js @@ -4,6 +4,11 @@ import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from ' import { ReportContext } from 'components/pages/reports/Report'; import BaseParameters from '../BaseParameters'; +const fieldOptions = [ + { name: 'daily', type: 'string' }, + { name: 'weekly', type: 'string' }, +]; + export function RetentionParameters() { const { report, runReport, isRunning } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); @@ -24,14 +29,7 @@ export function RetentionParameters() { return (
- - - - - + {formatMessage(labels.runQuery)} diff --git a/components/pages/reports/retention/RetentionReport.js b/components/pages/reports/retention/RetentionReport.js index 31d085f7b..cab3c16c2 100644 --- a/components/pages/reports/retention/RetentionReport.js +++ b/components/pages/reports/retention/RetentionReport.js @@ -8,8 +8,8 @@ import ReportBody from '../ReportBody'; import Funnel from 'assets/funnel.svg'; const defaultParameters = { - type: 'Retention', - parameters: { window: 60, urls: [] }, + type: 'retention', + parameters: {}, }; export default function RetentionReport({ reportId }) { @@ -20,7 +20,7 @@ export default function RetentionReport({ reportId }) { - + {/* */} diff --git a/components/pages/reports/retention/RetentionTable.js b/components/pages/reports/retention/RetentionTable.js index 4ef879862..53db7841d 100644 --- a/components/pages/reports/retention/RetentionTable.js +++ b/components/pages/reports/retention/RetentionTable.js @@ -1,18 +1,29 @@ import { useContext } from 'react'; -import DataTable from 'components/metrics/DataTable'; +import { GridTable, GridColumn } from 'react-basics'; import { useMessages } from 'hooks'; import { ReportContext } from '../Report'; export function RetentionTable() { const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); + const { fields = [] } = report?.parameters || {}; + + // return ( + // + // {fields.map(({ name }) => { + // return ; + // })} + // + // + // ); return ( - + + {row => row.cohortDate} + {row => row.date_number} + + {row => row.date_number} + + ); } diff --git a/pages/api/reports/retention.ts b/pages/api/reports/retention.ts index 6b8aebcc3..0e2c71b86 100644 --- a/pages/api/reports/retention.ts +++ b/pages/api/reports/retention.ts @@ -7,17 +7,12 @@ import { getRetention } from 'queries'; export interface RetentionRequestBody { websiteId: string; - urls: string[]; - window: number; - dateRange: { - startDate: string; - endDate: string; - }; + window: string; + dateRange: { window; startDate: string; endDate: string }; } export interface RetentionResponse { - urls: string[]; - window: number; + window: string; startAt: number; endAt: number; } @@ -32,7 +27,6 @@ export default async ( if (req.method === 'POST') { const { websiteId, - urls, window, dateRange: { startDate, endDate }, } = req.body; @@ -44,8 +38,7 @@ export default async ( const data = await getRetention(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), - urls, - windowMinutes: +window, + window: window, }); return ok(res, data); diff --git a/queries/analytics/reports/getRetention.ts b/queries/analytics/reports/getRetention.ts index b2c478827..68d3b4b2a 100644 --- a/queries/analytics/reports/getRetention.ts +++ b/queries/analytics/reports/getRetention.ts @@ -6,204 +6,200 @@ export async function getRetention( ...args: [ websiteId: string, criteria: { - windowMinutes: number; + window: string; startDate: Date; endDate: Date; - urls: string[]; }, ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), + // [CLICKHOUSE]: () => clickhouseQuery(...args), }); } async function relationalQuery( websiteId: string, criteria: { - windowMinutes: number; + window: string; startDate: Date; endDate: Date; - urls: string[]; }, ): Promise< { - x: string; - y: number; - z: number; + date: Date; + visitors: number; + day: number; + percentage: number; }[] > { - const { windowMinutes, startDate, endDate, urls } = criteria; - const { rawQuery, getAddMinutesQuery } = prisma; - const { levelQuery, sumQuery } = getRetentionQuery(urls, windowMinutes); - - function getRetentionQuery( - urls: string[], - windowMinutes: number, - ): { - levelQuery: string; - sumQuery: string; - } { - return urls.reduce( - (pv, cv, i) => { - const levelNumber = i + 1; - const startSum = i > 0 ? 'union ' : ''; - - if (levelNumber >= 2) { - pv.levelQuery += ` - , level${levelNumber} AS ( - select distinct we.session_id, we.created_at - from level${i} l - join website_event we - on l.session_id = we.session_id - where we.created_at between l.created_at - and ${getAddMinutesQuery(`l.created_at `, windowMinutes)} - and we.referrer_path = {{${i - 1}}} - and we.url_path = {{${i}}} - and we.created_at <= {{endDate}} - and we.website_id = {{websiteId::uuid}} - )`; - } - - pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`; - - return pv; - }, - { - levelQuery: '', - sumQuery: '', - }, - ); - } + const { window, startDate, endDate } = criteria; + const { rawQuery } = prisma; return rawQuery( ` - WITH level1 AS ( - select distinct session_id, created_at - from website_event + WITH cohort_items AS ( + select + date_trunc('week', created_at)::date as cohort_date, + session_id + from session where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} - and url_path = {{0}} - ) - ${levelQuery} - ${sumQuery} - ORDER BY level; - `, - { - websiteId, - startDate, - endDate, - ...urls, - }, - ).then(results => { - return urls.map((a, i) => ({ - x: a, - y: results[i]?.count || 0, - z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off - })); - }); -} - -async function clickhouseQuery( - websiteId: string, - criteria: { - windowMinutes: number; - startDate: Date; - endDate: Date; - urls: string[]; - }, -): Promise< - { - x: string; - y: number; - z: number; - }[] -> { - const { windowMinutes, startDate, endDate, urls } = criteria; - const { rawQuery } = clickhouse; - const { levelQuery, sumQuery, urlFilterQuery, urlParams } = getRetentionQuery( - urls, - windowMinutes, - ); - - function getRetentionQuery( - urls: string[], - windowMinutes: number, - ): { - levelQuery: string; - sumQuery: string; - urlFilterQuery: string; - urlParams: { [key: string]: string }; - } { - return urls.reduce( - (pv, cv, i) => { - const levelNumber = i + 1; - const startSum = i > 0 ? 'union all ' : ''; - const startFilter = i > 0 ? ', ' : ''; - - if (levelNumber >= 2) { - pv.levelQuery += `\n - , level${levelNumber} AS ( - select distinct y.session_id as session_id, - y.url_path as url_path, - y.referrer_path as referrer_path, - y.created_at as created_at - from level${i} x - join level0 y - on x.session_id = y.session_id - where y.created_at between x.created_at and x.created_at + interval ${windowMinutes} minute - and y.referrer_path = {url${i - 1}:String} - and y.url_path = {url${i}:String} - )`; - } - - pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`; - pv.urlFilterQuery += `${startFilter}{url${i}:String} `; - pv.urlParams[`url${i}`] = cv; - - return pv; - }, - { - levelQuery: '', - sumQuery: '', - urlFilterQuery: '', - urlParams: {}, - }, - ); - } - - return rawQuery<{ level: number; count: number }[]>( - ` - WITH level0 AS ( - select distinct session_id, url_path, referrer_path, created_at - from umami.website_event - where url_path in (${urlFilterQuery}) - and website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} + order by 1, 2 ), - level1 AS ( - select * - from level0 - where url_path = {url0:String} + user_activities AS ( + select distinct + w.session_id, + (date_trunc('week', w.created_at)::date - c.cohort_date::date) / 7 as date_number + from website_event w + left join cohort_items c + on w.session_id = c.session_id + where website_id = {{websiteId::uuid}} + and created_at between {{startDate}} and {{endDate}} + ), + cohort_size as ( + select cohort_date, + count(*) as visitors + from cohort_items + group by 1 + order by 1 + ), + cohort_date as ( + select + c.cohort_date, + a.date_number, + count(*) as visitors + from user_activities a + left join cohort_items c + on a.session_id = c.session_id + group by 1, 2 ) - ${levelQuery} - select * - from ( - ${sumQuery} - ) ORDER BY level; - `, + select + c.cohort_date, + c.date_number, + s.visitors, + c.visitors, + c.visitors::float * 100 / s.visitors as percentage + from cohort_date c + left join cohort_size s + on c.cohort_date = s.cohort_date + where c.cohort_date IS NOT NULL + order by 1, 2`, { websiteId, startDate, endDate, - ...urlParams, + window, }, ).then(results => { - return urls.map((a, i) => ({ - x: a, - y: results[i]?.count || 0, - z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off - })); + return results; + // return results.map((a, i) => ({ + // x: a, + // y: results[i]?.count || 0, + // z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off + // })); }); } + +// async function clickhouseQuery( +// websiteId: string, +// criteria: { +// windowMinutes: number; +// startDate: Date; +// endDate: Date; +// urls: string[]; +// }, +// ): Promise< +// { +// x: string; +// y: number; +// z: number; +// }[] +// > { +// const { windowMinutes, startDate, endDate, urls } = criteria; +// const { rawQuery } = clickhouse; +// const { levelQuery, sumQuery, urlFilterQuery, urlParams } = getRetentionQuery( +// urls, +// windowMinutes, +// ); + +// function getRetentionQuery( +// urls: string[], +// windowMinutes: number, +// ): { +// levelQuery: string; +// sumQuery: string; +// urlFilterQuery: string; +// urlParams: { [key: string]: string }; +// } { +// return urls.reduce( +// (pv, cv, i) => { +// const levelNumber = i + 1; +// const startSum = i > 0 ? 'union all ' : ''; +// const startFilter = i > 0 ? ', ' : ''; + +// if (levelNumber >= 2) { +// pv.levelQuery += `\n +// , level${levelNumber} AS ( +// select distinct y.session_id as session_id, +// y.url_path as url_path, +// y.referrer_path as referrer_path, +// y.created_at as created_at +// from level${i} x +// join level0 y +// on x.session_id = y.session_id +// where y.created_at between x.created_at and x.created_at + interval ${windowMinutes} minute +// and y.referrer_path = {url${i - 1}:String} +// and y.url_path = {url${i}:String} +// )`; +// } + +// pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`; +// pv.urlFilterQuery += `${startFilter}{url${i}:String} `; +// pv.urlParams[`url${i}`] = cv; + +// return pv; +// }, +// { +// levelQuery: '', +// sumQuery: '', +// urlFilterQuery: '', +// urlParams: {}, +// }, +// ); +// } + +// return rawQuery<{ level: number; count: number }[]>( +// ` +// WITH level0 AS ( +// select distinct session_id, url_path, referrer_path, created_at +// from umami.website_event +// where url_path in (${urlFilterQuery}) +// and website_id = {websiteId:UUID} +// and created_at between {startDate:DateTime64} and {endDate:DateTime64} +// ), +// level1 AS ( +// select * +// from level0 +// where url_path = {url0:String} +// ) +// ${levelQuery} +// select * +// from ( +// ${sumQuery} +// ) ORDER BY level; +// `, +// { +// websiteId, +// startDate, +// endDate, +// ...urlParams, +// }, +// ).then(results => { +// return urls.map((a, i) => ({ +// x: a, +// y: results[i]?.count || 0, +// z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off +// })); +// }); +// } From 15575d7783be1b1a8b1a3d13ddc2199fd037dde6 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 7 Aug 2023 14:41:41 -0700 Subject: [PATCH 026/357] fix column data in relational event data query --- .../analytics/eventData/getEventDataEvents.ts | 16 ++++++++-------- .../analytics/eventData/getEventDataFields.ts | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/queries/analytics/eventData/getEventDataEvents.ts b/queries/analytics/eventData/getEventDataEvents.ts index ec0939b64..a3a19bb1c 100644 --- a/queries/analytics/eventData/getEventDataEvents.ts +++ b/queries/analytics/eventData/getEventDataEvents.ts @@ -21,10 +21,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { return rawQuery( ` select - website_event.event_name as eventName, - event_data.event_key as fieldName, - event_data.data_type as dataType, - event_data.string_value as value, + website_event.event_name as "eventName", + event_data.event_key as "fieldName", + event_data.data_type as "dataType", + event_data.string_value as "value", count(*) as total from event_data inner join website_event @@ -42,10 +42,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { return rawQuery( ` select - website_event.event_name as eventName, - event_data.event_key as fieldName, - event_data.data_type as dataType, - count(*) as total + website_event.event_name as "eventName", + event_data.event_key as "fieldName", + event_data.data_type as "dataType", + count(*) as "total" from event_data inner join website_event on website_event.event_id = event_data.website_event_id diff --git a/queries/analytics/eventData/getEventDataFields.ts b/queries/analytics/eventData/getEventDataFields.ts index c61de517c..f5f426e0c 100644 --- a/queries/analytics/eventData/getEventDataFields.ts +++ b/queries/analytics/eventData/getEventDataFields.ts @@ -21,10 +21,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters & { fiel return rawQuery( ` select - event_key as fieldName, - data_type as dataType, - string_value as fieldValue, - count(*) as total + event_key as "fieldName", + data_type as "dataType", + string_value as "fieldValue", + count(*) as "total" from event_data where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} From 2eee9c23c3dee26cdf967d23d55390e55ae666bc Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 7 Aug 2023 15:02:50 -0700 Subject: [PATCH 027/357] Added useFormat hook to format special values. --- components/metrics/BrowsersTable.js | 7 ++-- components/metrics/CountriesTable.js | 6 +-- components/metrics/DevicesTable.js | 8 ++-- .../pages/reports/insights/InsightsTable.js | 17 ++++++-- hooks/index.js | 1 + hooks/useFormat.js | 39 +++++++++++++++++++ hooks/useMessages.js | 4 +- lib/data.ts | 4 +- queries/analytics/reports/getInsights.ts | 1 + 9 files changed, 68 insertions(+), 19 deletions(-) create mode 100644 hooks/useFormat.js diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index 2920280f6..bf4d0aaab 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -1,16 +1,17 @@ +import { useRouter } from 'next/router'; import FilterLink from 'components/common/FilterLink'; import MetricsTable from 'components/metrics/MetricsTable'; -import { BROWSERS } from 'lib/constants'; import useMessages from 'hooks/useMessages'; -import { useRouter } from 'next/router'; +import useFormat from 'hooks/useFormat'; export function BrowsersTable({ websiteId, ...props }) { const { formatMessage, labels } = useMessages(); const { basePath } = useRouter(); + const { formatBrowser } = useFormat(); function renderLink({ x: browser }) { return ( - + {browser} {code} diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index 0b8d57086..98690d0a7 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -2,18 +2,16 @@ import MetricsTable from './MetricsTable'; import FilterLink from 'components/common/FilterLink'; import useMessages from 'hooks/useMessages'; import { useRouter } from 'next/router'; +import { useFormat } from 'hooks'; export function DevicesTable({ websiteId, ...props }) { const { formatMessage, labels } = useMessages(); const { basePath } = useRouter(); + const { formatDevice } = useFormat(); function renderLink({ x: device }) { return ( - + {device} {groups.map(({ name, label }) => { - return ; + return ( + + {row => formatValue(row[name], name)} + + ); })} - - + + {row => row.views.toLocaleString()} + + + {row => row.views.toLocaleString()} + ); } diff --git a/hooks/index.js b/hooks/index.js index 6a9b3b353..004260b0d 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -6,6 +6,7 @@ export * from './useDocumentClick'; export * from './useEscapeKey'; export * from './useFilters'; export * from './useForceUpdate'; +export * from './useFormat'; export * from './useLanguageNames'; export * from './useLocale'; export * from './useMessages'; diff --git a/hooks/useFormat.js b/hooks/useFormat.js new file mode 100644 index 000000000..3fd10ec8e --- /dev/null +++ b/hooks/useFormat.js @@ -0,0 +1,39 @@ +import useMessages from './useMessages'; +import { BROWSERS } from 'lib/constants'; +import useLocale from './useLocale'; +import useCountryNames from './useCountryNames'; + +export function useFormat() { + const { formatMessage, labels } = useMessages(); + const { locale } = useLocale(); + const countryNames = useCountryNames(locale); + + const formatBrowser = value => { + return BROWSERS[value] || value; + }; + + const formatCountry = value => { + return countryNames[value] || value; + }; + + const formatDevice = value => { + return formatMessage(labels[value] || labels.unknown); + }; + + const formatValue = (value, type) => { + switch (type) { + case 'browser': + return formatBrowser(value); + case 'country': + return formatCountry(value); + case 'device': + return formatDevice(value); + default: + return value; + } + }; + + return { formatBrowser, formatCountry, formatDevice, formatValue }; +} + +export default useFormat; diff --git a/hooks/useMessages.js b/hooks/useMessages.js index 0719afd8d..3c13fab03 100644 --- a/hooks/useMessages.js +++ b/hooks/useMessages.js @@ -4,11 +4,11 @@ import { messages, labels } from 'components/messages'; export function useMessages() { const { formatMessage } = useIntl(); - function getMessage(id) { + const getMessage = id => { const message = Object.values(messages).find(value => value.id === id); return message ? formatMessage(message) : id; - } + }; return { formatMessage, FormattedMessage, messages, labels, getMessage }; } diff --git a/lib/data.ts b/lib/data.ts index c2c53de31..47023bb43 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -25,7 +25,7 @@ export function flattenJSON( ).keyValues; } -export function getDynamicDataType(value: any): string { +export function getDataType(value: any): string { let type: string = typeof value; if ((type === 'string' && isValid(value)) || isValid(parseISO(value))) { @@ -36,7 +36,7 @@ export function getDynamicDataType(value: any): string { } function createKey(key, value, acc: { keyValues: any[]; parentKey: string }) { - const type = getDynamicDataType(value); + const type = getDataType(value); let dynamicDataType = null; diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index 0f778555a..b7c8777d3 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -41,6 +41,7 @@ async function relationalQuery( and website_event.event_type = {{eventType}} ${filterQuery} group by 1 + limit 500 `, params, ); From c4bbcf37b776071f257188dff8020cf6639133cf Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 7 Aug 2023 15:05:53 -0700 Subject: [PATCH 028/357] Wrong column. --- components/pages/reports/insights/InsightsTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/pages/reports/insights/InsightsTable.js b/components/pages/reports/insights/InsightsTable.js index 51ad36843..249602546 100644 --- a/components/pages/reports/insights/InsightsTable.js +++ b/components/pages/reports/insights/InsightsTable.js @@ -22,7 +22,7 @@ export function InsightsTable() { {row => row.views.toLocaleString()} - {row => row.views.toLocaleString()} + {row => row.visitors.toLocaleString()} ); From 85c593416aa9c8d628d4876b32eb5512e4088e7b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 7 Aug 2023 23:02:38 -0700 Subject: [PATCH 029/357] Updated insights report rendering. --- components/pages/reports/FieldSelectForm.js | 6 +- components/pages/reports/FilterSelectForm.js | 4 +- .../reports/insights/InsightsParameters.js | 68 +++++++++---------- .../pages/reports/insights/InsightsTable.js | 25 +++++-- pages/api/reports/insights.ts | 4 +- queries/analytics/reports/getInsights.ts | 12 ++-- 6 files changed, 64 insertions(+), 55 deletions(-) diff --git a/components/pages/reports/FieldSelectForm.js b/components/pages/reports/FieldSelectForm.js index 69f399bbe..30fd193da 100644 --- a/components/pages/reports/FieldSelectForm.js +++ b/components/pages/reports/FieldSelectForm.js @@ -2,14 +2,14 @@ import { Menu, Item, Form, FormRow } from 'react-basics'; import { useMessages } from 'hooks'; import styles from './FieldSelectForm.module.css'; -export default function FieldSelectForm({ fields, onSelect }) { +export default function FieldSelectForm({ items, onSelect }) { const { formatMessage, labels } = useMessages(); return ( - onSelect(fields[key])}> - {fields.map(({ label, name, type }, index) => { + onSelect(items[key])}> + {items.map(({ name, label, type }, index) => { return (
{label || name}
diff --git a/components/pages/reports/FilterSelectForm.js b/components/pages/reports/FilterSelectForm.js index 0dc107b0d..29493b080 100644 --- a/components/pages/reports/FilterSelectForm.js +++ b/components/pages/reports/FilterSelectForm.js @@ -2,11 +2,11 @@ import { useState } from 'react'; import FieldSelectForm from './FieldSelectForm'; import FieldFilterForm from './FieldFilterForm'; -export default function FilterSelectForm({ fields, onSelect }) { +export default function FilterSelectForm({ items, onSelect }) { const [field, setField] = useState(); if (!field) { - return ; + return ; } return ; diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js index 5d7e1fca4..4ec60a9a1 100644 --- a/components/pages/reports/insights/InsightsParameters.js +++ b/components/pages/reports/insights/InsightsParameters.js @@ -2,7 +2,6 @@ import { useContext, useRef } from 'react'; import { useMessages } from 'hooks'; import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; import { ReportContext } from 'components/pages/reports/Report'; -import { REPORT_PARAMETERS } from 'lib/constants'; import Icons from 'components/icons'; import BaseParameters from '../BaseParameters'; import ParameterList from '../ParameterList'; @@ -16,52 +15,52 @@ export function InsightsParameters() { const { formatMessage, labels } = useMessages(); const ref = useRef(null); const { parameters } = report || {}; - const { websiteId, dateRange, filters, groups } = parameters || {}; - const queryEnabled = websiteId && dateRange && (filters?.length || groups?.length); + const { websiteId, dateRange, fields, filters } = parameters || {}; + const queryEnabled = websiteId && dateRange && (fields?.length || filters?.length); const fieldOptions = [ - { name: 'url_path', type: 'string', label: formatMessage(labels.url) }, - { name: 'page_title', type: 'string', label: formatMessage(labels.pageTitle) }, - { name: 'referrer_domain', type: 'string', label: formatMessage(labels.referrer) }, - { name: 'url_query', type: 'string', label: formatMessage(labels.query) }, - { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, - { name: 'os', type: 'string', label: formatMessage(labels.os) }, - { name: 'device', type: 'string', label: formatMessage(labels.device) }, - { name: 'country', type: 'string', label: formatMessage(labels.country) }, - { name: 'region', type: 'string', label: formatMessage(labels.region) }, - { name: 'city', type: 'string', label: formatMessage(labels.city) }, - { name: 'language', type: 'string', label: formatMessage(labels.language) }, + { name: 'url_path', label: formatMessage(labels.url) }, + { name: 'page_title', label: formatMessage(labels.pageTitle) }, + { name: 'referrer_domain', label: formatMessage(labels.referrer) }, + { name: 'url_query', label: formatMessage(labels.query) }, + { name: 'browser', label: formatMessage(labels.browser) }, + { name: 'os', label: formatMessage(labels.os) }, + { name: 'device', label: formatMessage(labels.device) }, + { name: 'country', label: formatMessage(labels.country) }, + { name: 'region', label: formatMessage(labels.region) }, + { name: 'city', label: formatMessage(labels.city) }, + { name: 'language', label: formatMessage(labels.language) }, ]; const parameterGroups = [ - { label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups }, - { label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters }, + { id: 'fields', label: formatMessage(labels.fields) }, + { id: 'filters', label: formatMessage(labels.filters) }, ]; const parameterData = { + fields, filters, - groups, }; const handleSubmit = values => { runReport(values); }; - const handleAdd = (group, value) => { - const data = parameterData[group]; + const handleAdd = (id, value) => { + const data = parameterData[id]; if (!data.find(({ name }) => name === value.name)) { - updateReport({ parameters: { [group]: data.concat(value) } }); + updateReport({ parameters: { [id]: data.concat(value) } }); } }; - const handleRemove = (group, index) => { - const data = [...parameterData[group]]; + const handleRemove = (id, index) => { + const data = [...parameterData[id]]; data.splice(index, 1); - updateReport({ parameters: { [group]: data } }); + updateReport({ parameters: { [id]: data } }); }; - const AddButton = ({ group }) => { + const AddButton = ({ id }) => { return ( @@ -71,11 +70,11 @@ export function InsightsParameters() { {(close, element) => { return ( - {group === REPORT_PARAMETERS.groups && ( - + {id === 'fields' && ( + )} - {group === REPORT_PARAMETERS.filters && ( - + {id === 'filters' && ( + )} ); @@ -88,22 +87,19 @@ export function InsightsParameters() { return ( - {parameterGroups.map(({ label, group }) => { + {parameterGroups.map(({ id, label }) => { return ( - }> - handleRemove(group, index)} - > + }> + handleRemove(id, index)}> {({ value, label }) => { return (
- {group === REPORT_PARAMETERS.groups && ( + {id === 'fields' && ( <>
{label}
)} - {group === REPORT_PARAMETERS.filters && ( + {id === 'filters' && ( <>
{label}
{value[0]}
diff --git a/components/pages/reports/insights/InsightsTable.js b/components/pages/reports/insights/InsightsTable.js index 249602546..0d5298e4d 100644 --- a/components/pages/reports/insights/InsightsTable.js +++ b/components/pages/reports/insights/InsightsTable.js @@ -1,29 +1,42 @@ -import { useContext } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { GridTable, GridColumn } from 'react-basics'; import { useFormat, useMessages } from 'hooks'; import { ReportContext } from '../Report'; +import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; export function InsightsTable() { + const [fields, setFields] = useState(); const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); - const { groups = [] } = report?.parameters || {}; const { formatValue } = useFormat(); + useEffect( + () => { + setFields(report?.parameters?.fields); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [report?.data], + ); + + if (!fields) { + return ; + } + return ( - {groups.map(({ name, label }) => { + {fields.map(({ name, label }) => { return ( {row => formatValue(row[name], name)} ); })} - - {row => row.views.toLocaleString()} - {row => row.visitors.toLocaleString()} + + {row => row.views.toLocaleString()} + ); } diff --git a/pages/api/reports/insights.ts b/pages/api/reports/insights.ts index 44f720637..decb1f81f 100644 --- a/pages/api/reports/insights.ts +++ b/pages/api/reports/insights.ts @@ -27,7 +27,7 @@ export default async ( const { websiteId, dateRange: { startDate, endDate }, - groups, + fields, filters, } = req.body; @@ -35,7 +35,7 @@ export default async ( return unauthorized(res); } - const data = await getInsights(websiteId, groups, { + const data = await getInsights(websiteId, fields, { ...filters, startDate: new Date(startDate), endDate: new Date(endDate), diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index b7c8777d3..4c47052b8 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -5,7 +5,7 @@ import { EVENT_TYPE } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getInsights( - ...args: [websiteId: string, groups: { name: string; type: string }[], filters: QueryFilters] + ...args: [websiteId: string, fields: { name: string; type?: string }[], filters: QueryFilters] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -15,7 +15,7 @@ export async function getInsights( async function relationalQuery( websiteId: string, - groups: { name: string; type: string }[], + fields: { name: string; type?: string }[], filters: QueryFilters, ): Promise< { @@ -49,7 +49,7 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - groups: { name: string; type: string }[], + fields: { name: string; type?: string }[], filters: QueryFilters, ): Promise< { @@ -66,14 +66,14 @@ async function clickhouseQuery( return rawQuery( ` select - ${parseFields(groups)} + ${parseFields(fields)} from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} and event_type = {eventType:UInt32} ${filterQuery} - group by ${groups.map(({ name }) => name).join(',')} - order by 1 desc + group by ${fields.map(({ name }) => name).join(',')} + order by 1 desc, 2 desc limit 500 `, params, From 77d170ea51428f78e827e4201207a5c00bc3758d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 8 Aug 2023 10:06:41 -0700 Subject: [PATCH 030/357] Fixed event data display. --- .../analytics/eventData/getEventDataEvents.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/queries/analytics/eventData/getEventDataEvents.ts b/queries/analytics/eventData/getEventDataEvents.ts index ec0939b64..250841113 100644 --- a/queries/analytics/eventData/getEventDataEvents.ts +++ b/queries/analytics/eventData/getEventDataEvents.ts @@ -21,11 +21,11 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { return rawQuery( ` select - website_event.event_name as eventName, - event_data.event_key as fieldName, - event_data.data_type as dataType, - event_data.string_value as value, - count(*) as total + website_event.event_name as "eventName", + event_data.event_key as "fieldName", + event_data.data_type as "dataType", + event_data.string_value as "value", + count(*) as "total" from event_data inner join website_event on website_event.event_id = event_data.website_event_id @@ -42,10 +42,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { return rawQuery( ` select - website_event.event_name as eventName, - event_data.event_key as fieldName, - event_data.data_type as dataType, - count(*) as total + website_event.event_name as "eventName", + event_data.event_key as "fieldName", + event_data.data_type as "dataType", + count(*) as "total" from event_data inner join website_event on website_event.event_id = event_data.website_event_id From bf507037c73e1f23dc56c7a2aa04be017ae4277d Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 8 Aug 2023 11:57:58 -0700 Subject: [PATCH 031/357] finish CH query and clean up objects --- .../reports/retention/RetentionParameters.js | 1 - .../pages/reports/retention/RetentionTable.js | 25 +- pages/api/reports/retention.ts | 4 - queries/analytics/reports/getRetention.ts | 231 ++++++++---------- 4 files changed, 109 insertions(+), 152 deletions(-) diff --git a/components/pages/reports/retention/RetentionParameters.js b/components/pages/reports/retention/RetentionParameters.js index bf40236df..f6bde0b16 100644 --- a/components/pages/reports/retention/RetentionParameters.js +++ b/components/pages/reports/retention/RetentionParameters.js @@ -29,7 +29,6 @@ export function RetentionParameters() { return ( - {formatMessage(labels.runQuery)} diff --git a/components/pages/reports/retention/RetentionTable.js b/components/pages/reports/retention/RetentionTable.js index 53db7841d..35d55a640 100644 --- a/components/pages/reports/retention/RetentionTable.js +++ b/components/pages/reports/retention/RetentionTable.js @@ -6,22 +6,23 @@ import { ReportContext } from '../Report'; export function RetentionTable() { const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); - const { fields = [] } = report?.parameters || {}; - // return ( - // - // {fields.map(({ name }) => { - // return ; - // })} - // - // - // ); return ( - {row => row.cohortDate} - {row => row.date_number} + + {row => row.date} + + + {row => row.day} + - {row => row.date_number} + {row => row.visitors} + + + {row => row.returnVisitors} + + + {row => row.percentage} ); diff --git a/pages/api/reports/retention.ts b/pages/api/reports/retention.ts index 0e2c71b86..83ed0b574 100644 --- a/pages/api/reports/retention.ts +++ b/pages/api/reports/retention.ts @@ -7,12 +7,10 @@ import { getRetention } from 'queries'; export interface RetentionRequestBody { websiteId: string; - window: string; dateRange: { window; startDate: string; endDate: string }; } export interface RetentionResponse { - window: string; startAt: number; endAt: number; } @@ -27,7 +25,6 @@ export default async ( if (req.method === 'POST') { const { websiteId, - window, dateRange: { startDate, endDate }, } = req.body; @@ -38,7 +35,6 @@ export default async ( const data = await getRetention(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), - window: window, }); return ok(res, data); diff --git a/queries/analytics/reports/getRetention.ts b/queries/analytics/reports/getRetention.ts index 68d3b4b2a..c34ba068e 100644 --- a/queries/analytics/reports/getRetention.ts +++ b/queries/analytics/reports/getRetention.ts @@ -5,8 +5,7 @@ import prisma from 'lib/prisma'; export async function getRetention( ...args: [ websiteId: string, - criteria: { - window: string; + dateRange: { startDate: Date; endDate: Date; }, @@ -14,48 +13,121 @@ export async function getRetention( ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), - // [CLICKHOUSE]: () => clickhouseQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), }); } async function relationalQuery( websiteId: string, - criteria: { - window: string; + dateRange: { startDate: Date; endDate: Date; }, ): Promise< { date: Date; - visitors: number; day: number; + visitors: number; + returnVisitors: number; percentage: number; }[] > { - const { window, startDate, endDate } = criteria; + const { startDate, endDate } = dateRange; const { rawQuery } = prisma; return rawQuery( ` WITH cohort_items AS ( - select - date_trunc('week', created_at)::date as cohort_date, - session_id + select session_id, + date_trunc('day', created_at)::date as cohort_date from session where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} - order by 1, 2 ), user_activities AS ( select distinct w.session_id, - (date_trunc('week', w.created_at)::date - c.cohort_date::date) / 7 as date_number + (date_trunc('day', w.created_at)::date - c.cohort_date::date) as day_number from website_event w - left join cohort_items c + join cohort_items c on w.session_id = c.session_id where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} + and created_at between {{startDate}} and {{endDate}} + ), + cohort_size as ( + select cohort_date, + count(*) as visitors + from cohort_items + group by 1 + order by 1 + ), + cohort_date as ( + select + c.cohort_date, + a.day_number, + count(*) as visitors + from user_activities a + join cohort_items c + on a.session_id = c.session_id + where a.day_number IN (0,1,2,3,4,5,6,7,14,21,30) + group by 1, 2 + ) + select + c.cohort_date as date, + c.day_number as day, + s.visitors, + c.visitors as "returnVisitors", + c.visitors::float * 100 / s.visitors as percentage + from cohort_date c + join cohort_size s + on c.cohort_date = s.cohort_date + order by 1, 2`, + { + websiteId, + startDate, + endDate, + }, + ); +} + +async function clickhouseQuery( + websiteId: string, + dateRange: { + startDate: Date; + endDate: Date; + }, +): Promise< + { + date: Date; + day: number; + visitors: number; + returnVisitors: number; + percentage: number; + }[] +> { + const { startDate, endDate } = dateRange; + const { rawQuery } = clickhouse; + + return rawQuery( + ` + WITH cohort_items AS ( + select + min(date_trunc('day', created_at)) as cohort_date, + session_id + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + group by session_id + ), + user_activities AS ( + select distinct + w.session_id, + (date_trunc('day', w.created_at) - c.cohort_date) / 86400 as day_number + from website_event w + join cohort_items c + on w.session_id = c.session_id + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} ), cohort_size as ( select cohort_date, @@ -67,139 +139,28 @@ async function relationalQuery( cohort_date as ( select c.cohort_date, - a.date_number, + a.day_number, count(*) as visitors from user_activities a - left join cohort_items c + join cohort_items c on a.session_id = c.session_id + where a.day_number IN (0,1,2,3,4,5,6,7,14,21,30) group by 1, 2 ) select - c.cohort_date, - c.date_number, - s.visitors, - c.visitors, - c.visitors::float * 100 / s.visitors as percentage + c.cohort_date as date, + c.day_number as day, + s.visitors as visitors, + c.visitors returnVisitors, + c.visitors * 100 / s.visitors as percentage from cohort_date c - left join cohort_size s + join cohort_size s on c.cohort_date = s.cohort_date - where c.cohort_date IS NOT NULL order by 1, 2`, { websiteId, startDate, endDate, - window, }, - ).then(results => { - return results; - // return results.map((a, i) => ({ - // x: a, - // y: results[i]?.count || 0, - // z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off - // })); - }); + ); } - -// async function clickhouseQuery( -// websiteId: string, -// criteria: { -// windowMinutes: number; -// startDate: Date; -// endDate: Date; -// urls: string[]; -// }, -// ): Promise< -// { -// x: string; -// y: number; -// z: number; -// }[] -// > { -// const { windowMinutes, startDate, endDate, urls } = criteria; -// const { rawQuery } = clickhouse; -// const { levelQuery, sumQuery, urlFilterQuery, urlParams } = getRetentionQuery( -// urls, -// windowMinutes, -// ); - -// function getRetentionQuery( -// urls: string[], -// windowMinutes: number, -// ): { -// levelQuery: string; -// sumQuery: string; -// urlFilterQuery: string; -// urlParams: { [key: string]: string }; -// } { -// return urls.reduce( -// (pv, cv, i) => { -// const levelNumber = i + 1; -// const startSum = i > 0 ? 'union all ' : ''; -// const startFilter = i > 0 ? ', ' : ''; - -// if (levelNumber >= 2) { -// pv.levelQuery += `\n -// , level${levelNumber} AS ( -// select distinct y.session_id as session_id, -// y.url_path as url_path, -// y.referrer_path as referrer_path, -// y.created_at as created_at -// from level${i} x -// join level0 y -// on x.session_id = y.session_id -// where y.created_at between x.created_at and x.created_at + interval ${windowMinutes} minute -// and y.referrer_path = {url${i - 1}:String} -// and y.url_path = {url${i}:String} -// )`; -// } - -// pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`; -// pv.urlFilterQuery += `${startFilter}{url${i}:String} `; -// pv.urlParams[`url${i}`] = cv; - -// return pv; -// }, -// { -// levelQuery: '', -// sumQuery: '', -// urlFilterQuery: '', -// urlParams: {}, -// }, -// ); -// } - -// return rawQuery<{ level: number; count: number }[]>( -// ` -// WITH level0 AS ( -// select distinct session_id, url_path, referrer_path, created_at -// from umami.website_event -// where url_path in (${urlFilterQuery}) -// and website_id = {websiteId:UUID} -// and created_at between {startDate:DateTime64} and {endDate:DateTime64} -// ), -// level1 AS ( -// select * -// from level0 -// where url_path = {url0:String} -// ) -// ${levelQuery} -// select * -// from ( -// ${sumQuery} -// ) ORDER BY level; -// `, -// { -// websiteId, -// startDate, -// endDate, -// ...urlParams, -// }, -// ).then(results => { -// return urls.map((a, i) => ({ -// x: a, -// y: results[i]?.count || 0, -// z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off -// })); -// }); -// } From 577294191d6991e2f5a7de06e00f438940a56d38 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 8 Aug 2023 12:03:03 -0700 Subject: [PATCH 032/357] remove retention chart --- .../pages/reports/retention/RetentionChart.js | 74 ------------------- .../retention/RetentionChart.module.css | 3 - .../reports/retention/RetentionReport.js | 2 - 3 files changed, 79 deletions(-) delete mode 100644 components/pages/reports/retention/RetentionChart.js delete mode 100644 components/pages/reports/retention/RetentionChart.module.css diff --git a/components/pages/reports/retention/RetentionChart.js b/components/pages/reports/retention/RetentionChart.js deleted file mode 100644 index 5f7361fdb..000000000 --- a/components/pages/reports/retention/RetentionChart.js +++ /dev/null @@ -1,74 +0,0 @@ -import { useCallback, useContext, useMemo } from 'react'; -import { Loading, StatusLight } from 'react-basics'; -import useMessages from 'hooks/useMessages'; -import useTheme from 'hooks/useTheme'; -import BarChart from 'components/metrics/BarChart'; -import { formatLongNumber } from 'lib/format'; -import styles from './RetentionChart.module.css'; -import { ReportContext } from '../Report'; - -export function RetentionChart({ className, loading }) { - const { report } = useContext(ReportContext); - const { formatMessage, labels } = useMessages(); - const { colors } = useTheme(); - - const { parameters, data } = report || {}; - - const renderXLabel = useCallback( - (label, index) => { - return parameters.urls[index]; - }, - [parameters], - ); - - const renderTooltipPopup = useCallback((setTooltipPopup, model) => { - const { opacity, labelColors, dataPoints } = model.tooltip; - - if (!dataPoints?.length || !opacity) { - setTooltipPopup(null); - return; - } - - setTooltipPopup( - <> -
- {formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)} -
-
- - {formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)} - -
- , - ); - }, []); - - const datasets = useMemo(() => { - return [ - { - label: formatMessage(labels.uniqueVisitors), - data: data, - borderWidth: 1, - ...colors.chart.visitors, - }, - ]; - }, [data]); - - if (loading) { - return ; - } - - return ( - - ); -} - -export default RetentionChart; diff --git a/components/pages/reports/retention/RetentionChart.module.css b/components/pages/reports/retention/RetentionChart.module.css deleted file mode 100644 index 9e1690b31..000000000 --- a/components/pages/reports/retention/RetentionChart.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.loading { - height: 300px; -} diff --git a/components/pages/reports/retention/RetentionReport.js b/components/pages/reports/retention/RetentionReport.js index cab3c16c2..333496d82 100644 --- a/components/pages/reports/retention/RetentionReport.js +++ b/components/pages/reports/retention/RetentionReport.js @@ -1,4 +1,3 @@ -import RetentionChart from './RetentionChart'; import RetentionTable from './RetentionTable'; import RetentionParameters from './RetentionParameters'; import Report from '../Report'; @@ -20,7 +19,6 @@ export default function RetentionReport({ reportId }) { - {/* */} From 618c643a0a16f69d8b7c1d6b856a69619b9a3bb3 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 8 Aug 2023 15:29:59 -0700 Subject: [PATCH 033/357] Insights report filtering. --- components/pages/reports/FieldFilterForm.js | 45 +++++++------------ components/pages/reports/FilterSelectForm.js | 25 ++++++++++- .../reports/insights/InsightsParameters.js | 6 ++- hooks/useFilters.js | 4 +- pages/api/websites/[id]/values.ts | 43 ++++++++++++++++++ .../{stats => }/getActiveVisitors.ts | 6 +-- .../analytics/{stats => }/getRealtimeData.ts | 2 +- queries/analytics/getValues.ts | 38 ++++++++++++++++ .../{stats => }/getWebsiteDateRange.ts | 0 .../analytics/{stats => }/getWebsiteStats.ts | 0 queries/analytics/reports/getInsights.ts | 26 ++++++----- queries/index.js | 9 ++-- 12 files changed, 152 insertions(+), 52 deletions(-) create mode 100644 pages/api/websites/[id]/values.ts rename queries/analytics/{stats => }/getActiveVisitors.ts (84%) rename queries/analytics/{stats => }/getRealtimeData.ts (93%) create mode 100644 queries/analytics/getValues.ts rename queries/analytics/{stats => }/getWebsiteDateRange.ts (100%) rename queries/analytics/{stats => }/getWebsiteStats.ts (100%) diff --git a/components/pages/reports/FieldFilterForm.js b/components/pages/reports/FieldFilterForm.js index 021ea97ef..a2b68968d 100644 --- a/components/pages/reports/FieldFilterForm.js +++ b/components/pages/reports/FieldFilterForm.js @@ -1,48 +1,37 @@ import { useState } from 'react'; -import { Form, FormRow, Menu, Item, Flexbox, Dropdown, TextField, Button } from 'react-basics'; +import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics'; import { useFilters } from 'hooks'; import styles from './FieldFilterForm.module.css'; -export default function FieldFilterForm({ name, type, onSelect }) { - const [filter, setFilter] = useState(''); - const [value, setValue] = useState(''); - const { filters, types } = useFilters(); - const items = types[type]; +export default function FieldFilterForm({ label, type, values, onSelect }) { + const [filter, setFilter] = useState('eq'); + const [value, setValue] = useState(); + const filters = useFilters(type); - const renderValue = value => { - return filters[value]; + const renderFilterValue = value => { + return filters.find(f => f.value === value)?.label; }; - if (type === 'boolean') { - return ( - - - onSelect({ name, type, value: ['eq', value] })}> - {items.map(value => { - return {filters[value]}; - })} - - - - ); - } - return (
- + - {value => { - return {filters[value]}; + {({ value, label }) => { + return {label}; + }} + + + {value => { + return {value}; }} - setValue(e.target.value)} autoFocus={true} /> diff --git a/components/pages/reports/FilterSelectForm.js b/components/pages/reports/FilterSelectForm.js index 49238e1cd..844c2a1d6 100644 --- a/components/pages/reports/FilterSelectForm.js +++ b/components/pages/reports/FilterSelectForm.js @@ -27,8 +27,16 @@ export default function FilterSelectForm({ websiteId, items, onSelect }) { } if (isLoading) { - return ; + return ; } - return ; + return ( + + ); } diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js index c6140d681..d4c0b95b8 100644 --- a/components/pages/reports/insights/InsightsParameters.js +++ b/components/pages/reports/insights/InsightsParameters.js @@ -1,6 +1,15 @@ import { useContext, useRef } from 'react'; -import { useMessages } from 'hooks'; -import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; +import { useFormat, useMessages, useFilters } from 'hooks'; +import { + Form, + FormRow, + FormButtons, + SubmitButton, + PopupTrigger, + Icon, + Popup, + TooltipPopup, +} from 'react-basics'; import { ReportContext } from 'components/pages/reports/Report'; import Icons from 'components/icons'; import BaseParameters from '../BaseParameters'; @@ -13,23 +22,26 @@ import FieldSelectForm from '../FieldSelectForm'; export function InsightsParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); + const { formatValue } = useFormat(); + const { filterLabels } = useFilters(); const ref = useRef(null); const { parameters } = report || {}; const { websiteId, dateRange, fields, filters } = parameters || {}; + const { startDate, endDate } = dateRange || {}; + const parametersSelected = websiteId && startDate && endDate; const queryEnabled = websiteId && dateRange && (fields?.length || filters?.length); const fieldOptions = [ - { name: 'url_path', label: formatMessage(labels.url) }, - { name: 'page_title', label: formatMessage(labels.pageTitle) }, - { name: 'referrer_domain', label: formatMessage(labels.referrer) }, - { name: 'url_query', label: formatMessage(labels.query) }, + { name: 'url', label: formatMessage(labels.url) }, + { name: 'title', label: formatMessage(labels.pageTitle) }, + { name: 'referrer', label: formatMessage(labels.referrer) }, + { name: 'query', label: formatMessage(labels.query) }, { name: 'browser', label: formatMessage(labels.browser) }, { name: 'os', label: formatMessage(labels.os) }, { name: 'device', label: formatMessage(labels.device) }, { name: 'country', label: formatMessage(labels.country) }, { name: 'region', label: formatMessage(labels.region) }, { name: 'city', label: formatMessage(labels.city) }, - { name: 'language', label: formatMessage(labels.language) }, ]; const parameterGroups = [ @@ -63,9 +75,11 @@ export function InsightsParameters() { const AddButton = ({ id }) => { return ( - - - + + + + + {(close, element) => { return ( @@ -91,32 +105,33 @@ export function InsightsParameters() { return (
- {parameterGroups.map(({ id, label }) => { - return ( - }> - handleRemove(id, index)}> - {({ value, label }) => { - return ( -
- {id === 'fields' && ( - <> -
{label}
- - )} - {id === 'filters' && ( - <> -
{label}
-
{value[0]}
-
{value[1]}
- - )} -
- ); - }} -
-
- ); - })} + {parametersSelected && + parameterGroups.map(({ id, label }) => { + return ( + }> + handleRemove(id, index)}> + {({ name, filter, value, label }) => { + return ( +
+ {id === 'fields' && ( + <> +
{label}
+ + )} + {id === 'filters' && ( + <> +
{fieldOptions.find(f => f.name === name)?.label}
+
{filterLabels[filter]}
+
{formatValue(value, name)}
+ + )} +
+ ); + }} +
+
+ ); + })} {formatMessage(labels.runQuery)} diff --git a/hooks/useFilters.js b/hooks/useFilters.js index 0175e72a6..5143fe5b4 100644 --- a/hooks/useFilters.js +++ b/hooks/useFilters.js @@ -1,11 +1,13 @@ import { useMessages } from 'hooks'; -export function useFilters(type) { +export function useFilters() { const { formatMessage, labels } = useMessages(); - const filters = { - eq: formatMessage(labels.equals), - neq: formatMessage(labels.doesNotEqual), + const filterLabels = { + eq: formatMessage(labels.is), + neq: formatMessage(labels.isNot), + s: formatMessage(labels.isSet), + ns: formatMessage(labels.isNotSet), c: formatMessage(labels.contains), dnc: formatMessage(labels.doesNotContain), t: formatMessage(labels.true), @@ -18,7 +20,7 @@ export function useFilters(type) { af: formatMessage(labels.after), }; - const types = { + const typeFilters = { string: ['eq', 'neq'], array: ['c', 'dnc'], boolean: ['t', 'f'], @@ -27,7 +29,11 @@ export function useFilters(type) { uuid: ['eq'], }; - return types[type]?.map(key => ({ type, value: key, label: filters[key] })) ?? []; + const getFilters = type => { + return typeFilters[type]?.map(key => ({ type, value: key, label: filterLabels[key] })) ?? []; + }; + + return { getFilters, filterLabels, typeFilters }; } export default useFilters; diff --git a/pages/api/websites/[id]/values.ts b/pages/api/websites/[id]/values.ts index b40fc2622..ad8625bd4 100644 --- a/pages/api/websites/[id]/values.ts +++ b/pages/api/websites/[id]/values.ts @@ -3,7 +3,7 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { EVENT_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; +import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; import { getValues } from 'queries'; export interface WebsiteResetRequestQuery { @@ -28,7 +28,7 @@ export default async ( return unauthorized(res); } - const values = await getValues(websiteId, type as string); + const values = await getValues(websiteId, FILTER_COLUMNS[type as string]); return ok( res, diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index 2167aa2c7..9793f258c 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -45,7 +45,7 @@ async function relationalQuery( and website_event.created_at between {{startDate}} and {{endDate}} and website_event.event_type = {{eventType}} ${filterQuery} - group by ${fields.map(({ name }) => name).join(',')} + ${parseGroupBy(fields)} order by 1 desc, 2 desc limit 500 `, @@ -78,7 +78,7 @@ async function clickhouseQuery( and created_at between {startDate:DateTime} and {endDate:DateTime} and event_type = {eventType:UInt32} ${filterQuery} - group by ${fields.map(({ name }) => name).join(',')} + ${parseGroupBy(fields)} order by 1 desc, 2 desc limit 500 `, @@ -98,3 +98,10 @@ function parseFields(fields) { return query.join(',\n'); } + +function parseGroupBy(fields) { + if (!fields.length) { + return ''; + } + return `group by ${fields.map(({ name }) => name).join(',')}`; +} From 50b3ad81e2df79bc9f10b24e1210b1f7a49ba2d8 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 9 Aug 2023 15:33:06 -0700 Subject: [PATCH 038/357] increase operation-per-run for stale issues --- .github/workflows/stale-issues.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index bf2505b14..52c0d432f 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -19,4 +19,5 @@ jobs: close-issue-message: 'This issue was closed because it has been inactive for 7 days since being marked as stale.' days-before-pr-stale: -1 days-before-pr-close: -1 + operations-per-run: 500 repo-token: ${{ secrets.GITHUB_TOKEN }} From 39fb4fdaf873f0a78bafe153a1500f6455c2653d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 10 Aug 2023 09:31:25 -0700 Subject: [PATCH 039/357] Added field types. --- components/pages/reports/FieldFilterForm.js | 1 + .../pages/reports/FieldFilterForm.module.css | 5 ++++ components/pages/reports/FieldSelectForm.js | 4 +-- components/pages/reports/FilterSelectForm.js | 4 +-- .../reports/insights/InsightsParameters.js | 26 +++++++++++-------- .../pages/reports/insights/InsightsTable.js | 2 +- 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/components/pages/reports/FieldFilterForm.js b/components/pages/reports/FieldFilterForm.js index 381bb7e39..3df8f7456 100644 --- a/components/pages/reports/FieldFilterForm.js +++ b/components/pages/reports/FieldFilterForm.js @@ -40,6 +40,7 @@ export default function FieldFilterForm({ name, label, type, values, onSelect })
{label || name}
- {type &&
{type}
} + {showType && type &&
{type}
} ); })} diff --git a/components/pages/reports/FilterSelectForm.js b/components/pages/reports/FilterSelectForm.js index 844c2a1d6..38094bcad 100644 --- a/components/pages/reports/FilterSelectForm.js +++ b/components/pages/reports/FilterSelectForm.js @@ -23,7 +23,7 @@ export default function FilterSelectForm({ websiteId, items, onSelect }) { const { data, isLoading } = useValues(websiteId, field?.name); if (!field) { - return ; + return ; } if (isLoading) { @@ -34,7 +34,7 @@ export default function FilterSelectForm({ websiteId, items, onSelect }) { diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js index d4c0b95b8..18eeffc3d 100644 --- a/components/pages/reports/insights/InsightsParameters.js +++ b/components/pages/reports/insights/InsightsParameters.js @@ -32,16 +32,16 @@ export function InsightsParameters() { const queryEnabled = websiteId && dateRange && (fields?.length || filters?.length); const fieldOptions = [ - { name: 'url', label: formatMessage(labels.url) }, - { name: 'title', label: formatMessage(labels.pageTitle) }, - { name: 'referrer', label: formatMessage(labels.referrer) }, - { name: 'query', label: formatMessage(labels.query) }, - { name: 'browser', label: formatMessage(labels.browser) }, - { name: 'os', label: formatMessage(labels.os) }, - { name: 'device', label: formatMessage(labels.device) }, - { name: 'country', label: formatMessage(labels.country) }, - { name: 'region', label: formatMessage(labels.region) }, - { name: 'city', label: formatMessage(labels.city) }, + { name: 'url', type: 'string', label: formatMessage(labels.url) }, + { name: 'title', type: 'string', label: formatMessage(labels.pageTitle) }, + { name: 'referrer', type: 'string', label: formatMessage(labels.referrer) }, + { name: 'query', type: 'string', label: formatMessage(labels.query) }, + { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, + { name: 'os', type: 'string', label: formatMessage(labels.os) }, + { name: 'device', type: 'string', label: formatMessage(labels.device) }, + { name: 'country', type: 'string', label: formatMessage(labels.country) }, + { name: 'region', type: 'string', label: formatMessage(labels.region) }, + { name: 'city', type: 'string', label: formatMessage(labels.city) }, ]; const parameterGroups = [ @@ -85,7 +85,11 @@ export function InsightsParameters() { return ( {id === 'fields' && ( - + )} {id === 'filters' && ( ; } From dcf8b2edaa5c24ce5b0075e51c8e8aaf7ba2a482 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 10 Aug 2023 13:26:33 -0700 Subject: [PATCH 040/357] Add Search Api/Components. --- .eslintrc.json | 3 +- components/common/Pager.js | 37 ++++ components/common/Pager.module.css | 7 + components/common/SettingsTable.js | 121 ++++++++--- components/input/WebsiteSelect.js | 4 +- components/pages/dashboard/Dashboard.js | 13 +- components/pages/reports/ReportsTable.js | 20 +- .../settings/teams/TeamAddWebsiteForm.js | 32 +-- .../pages/settings/teams/TeamMembers.js | 25 ++- .../pages/settings/teams/TeamMembersTable.js | 26 ++- .../pages/settings/teams/TeamWebsites.js | 25 ++- .../pages/settings/teams/TeamWebsitesTable.js | 26 ++- components/pages/settings/teams/TeamsList.js | 50 +++-- components/pages/settings/teams/TeamsTable.js | 31 ++- components/pages/settings/users/UsersList.js | 32 ++- components/pages/settings/users/UsersTable.js | 21 +- .../pages/settings/websites/WebsitesList.js | 22 +- .../pages/settings/websites/WebsitesTable.js | 19 +- .../pages/websites/WebsiteReportsPage.js | 20 +- hooks/useApiFilter.ts | 28 +++ hooks/useReports.js | 21 +- lib/constants.ts | 16 ++ lib/prisma.ts | 34 ++- lib/types.ts | 55 ++++- package.json | 2 +- pages/api/reports/index.ts | 20 +- pages/api/teams/[id]/users/index.ts | 14 +- pages/api/teams/[id]/websites/index.ts | 15 +- pages/api/teams/index.ts | 15 +- pages/api/users/[id]/websites.ts | 18 +- pages/api/users/index.ts | 9 +- pages/api/websites/index.ts | 7 +- queries/admin/report.ts | 118 +++++++++-- queries/admin/team.ts | 88 +++++++- queries/admin/user.ts | 156 ++++---------- queries/admin/website.ts | 198 +++++++++++++++++- yarn.lock | 8 +- 37 files changed, 1069 insertions(+), 287 deletions(-) create mode 100644 components/common/Pager.js create mode 100644 components/common/Pager.module.css create mode 100644 hooks/useApiFilter.ts diff --git a/.eslintrc.json b/.eslintrc.json index 7a824ff6c..25e83d5ac 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -50,7 +50,8 @@ "@next/next/no-img-element": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-var-requires": "off" + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-empty-interface": "off" }, "globals": { "React": "writable" diff --git a/components/common/Pager.js b/components/common/Pager.js new file mode 100644 index 000000000..584e06693 --- /dev/null +++ b/components/common/Pager.js @@ -0,0 +1,37 @@ +import styles from './Pager.module.css'; +import { Button, Flexbox, Icon, Icons } from 'react-basics'; + +export function Pager({ page, pageSize, count, onPageChange, onPageSizeChange }) { + const maxPage = Math.ceil(count / pageSize); + const lastPage = page === maxPage; + const firstPage = page === 1; + + if (count === 0) { + return null; + } + + const handlePageChange = value => { + const nextPage = page + value; + if (nextPage > 0 && nextPage <= maxPage) { + onPageChange(nextPage); + } + }; + + return ( + + + {`Page ${page} of ${maxPage}`} + + + ); +} + +export default Pager; diff --git a/components/common/Pager.module.css b/components/common/Pager.module.css new file mode 100644 index 000000000..b4ee9f0e5 --- /dev/null +++ b/components/common/Pager.module.css @@ -0,0 +1,7 @@ +.container { + margin-top: 20px; +} + +.text { + margin: 0 10px; +} diff --git a/components/common/SettingsTable.js b/components/common/SettingsTable.js index 8f0398584..9fb4c2a98 100644 --- a/components/common/SettingsTable.js +++ b/components/common/SettingsTable.js @@ -1,37 +1,98 @@ -import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics'; +import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import useMessages from 'hooks/useMessages'; +import { useState } from 'react'; +import { + SearchField, + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, +} from 'react-basics'; import styles from './SettingsTable.module.css'; +import Pager from 'components/common/Pager'; + +export function SettingsTable({ + columns = [], + data, + children, + cellRender, + showSearch, + showPaging, + onFilterChange, + onPageChange, + onPageSizeChange, + filterValue, +}) { + const { formatMessage, messages } = useMessages(); + const [filter, setFilter] = useState(filterValue); + const { data: value, page, count, pageSize } = data; + + const handleFilterChange = value => { + setFilter(value); + onFilterChange(value); + }; -export function SettingsTable({ columns = [], data = [], children, cellRender }) { return ( - - - {(column, index) => { - return ( - - {column.label} - - ); - }} - - - {(row, keys, rowIndex) => { - row.action = children(row, keys, rowIndex); + <> + {showSearch && ( + + )} + {value.length === 0 && filterValue && ( + + )} + {value.length > 0 && ( +
+ + {(column, index) => { + return ( + + {column.label} + + ); + }} + + + {(row, keys, rowIndex) => { + row.action = children(row, keys, rowIndex); - return ( - - {(data, key, colIndex) => { - return ( - - - {cellRender ? cellRender(row, data, key, colIndex) : data[key]} - - ); - }} - - ); - }} - -
+ return ( + + {(data, key, colIndex) => { + return ( + + + {cellRender ? cellRender(row, data, key, colIndex) : data[key]} + + ); + }} + + ); + }} + + {showPaging && ( + + )} + + )} + ); } diff --git a/components/input/WebsiteSelect.js b/components/input/WebsiteSelect.js index b77ae57c8..ae3ceb46a 100644 --- a/components/input/WebsiteSelect.js +++ b/components/input/WebsiteSelect.js @@ -8,12 +8,12 @@ export function WebsiteSelect({ websiteId, onSelect }) { const { data } = useQuery(['websites:me'], () => get('/me/websites')); const renderValue = value => { - return data?.find(({ id }) => id === value)?.name; + return data?.data?.find(({ id }) => id === value)?.name; }; return ( - get('/websites', { userId, includeTeams: 1 }), + get('/websites', { includeTeams: 1 }), ); - const hasData = data && data.length !== 0; + const hasData = data && data?.data.length !== 0; + const { dir } = useLocale(); function handleMore() { @@ -47,8 +48,10 @@ export function Dashboard({ userId }) { )} {hasData && ( <> - {editing && } - {!editing && } + {editing && } + {!editing && ( + + )} {max < data.length && ( -
- + {hasData && ( +
+ + + {({ id, name }) => {name}} + + + + + + {formatMessage(labels.addWebsite)} + + + + + )} ); } diff --git a/components/pages/settings/teams/TeamMembers.js b/components/pages/settings/teams/TeamMembers.js index 3ea8232ce..9762ef297 100644 --- a/components/pages/settings/teams/TeamMembers.js +++ b/components/pages/settings/teams/TeamMembers.js @@ -2,13 +2,22 @@ import { Loading, useToasts } from 'react-basics'; import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable'; import useApi from 'hooks/useApi'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; export function TeamMembers({ teamId, readOnly }) { const { showToast } = useToasts(); - const { get, useQuery } = useApi(); const { formatMessage, messages } = useMessages(); - const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () => - get(`/teams/${teamId}/users`), + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); + const { get, useQuery } = useApi(); + const { data, isLoading, refetch } = useQuery( + ['teams:users', teamId, filter, page, pageSize], + () => + get(`/teams/${teamId}/users`, { + filter, + page, + pageSize, + }), ); if (isLoading) { @@ -22,7 +31,15 @@ export function TeamMembers({ teamId, readOnly }) { return ( <> - + ); } diff --git a/components/pages/settings/teams/TeamMembersTable.js b/components/pages/settings/teams/TeamMembersTable.js index 8e6fad82e..daa4acc6d 100644 --- a/components/pages/settings/teams/TeamMembersTable.js +++ b/components/pages/settings/teams/TeamMembersTable.js @@ -4,7 +4,15 @@ import { ROLES } from 'lib/constants'; import TeamMemberRemoveButton from './TeamMemberRemoveButton'; import SettingsTable from 'components/common/SettingsTable'; -export function TeamMembersTable({ data = [], onSave, readOnly }) { +export function TeamMembersTable({ + data = [], + onSave, + readOnly, + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); @@ -16,7 +24,7 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) { const cellRender = (row, data, key) => { if (key === 'username') { - return row?.user?.username; + return row?.username; } if (key === 'role') { return formatMessage( @@ -27,13 +35,23 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) { }; return ( - + {row => { return ( !readOnly && ( diff --git a/components/pages/settings/teams/TeamWebsites.js b/components/pages/settings/teams/TeamWebsites.js index 9a5761e56..2ae344f57 100644 --- a/components/pages/settings/teams/TeamWebsites.js +++ b/components/pages/settings/teams/TeamWebsites.js @@ -13,13 +13,22 @@ import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable import TeamAddWebsiteForm from 'components/pages/settings/teams/TeamAddWebsiteForm'; import useApi from 'hooks/useApi'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; export function TeamWebsites({ teamId }) { const { showToast } = useToasts(); const { formatMessage, labels, messages } = useMessages(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); const { get, useQuery } = useApi(); - const { data, isLoading, refetch } = useQuery(['teams:websites', teamId], () => - get(`/teams/${teamId}/websites`), + const { data, isLoading, refetch } = useQuery( + ['teams:websites', teamId, filter, page, pageSize], + () => + get(`/teams/${teamId}/websites`, { + filter, + page, + pageSize, + }), ); const hasData = data && data.length !== 0; @@ -49,7 +58,17 @@ export function TeamWebsites({ teamId }) { return (
{addButton} - {hasData && } + {hasData && ( + + )}
); } diff --git a/components/pages/settings/teams/TeamWebsitesTable.js b/components/pages/settings/teams/TeamWebsitesTable.js index 4873c6c72..564c8a782 100644 --- a/components/pages/settings/teams/TeamWebsitesTable.js +++ b/components/pages/settings/teams/TeamWebsitesTable.js @@ -6,9 +6,17 @@ import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton'; import SettingsTable from 'components/common/SettingsTable'; import useConfig from 'hooks/useConfig'; -export function TeamWebsitesTable({ data = [], onSave }) { +export function TeamWebsitesTable({ + data = [], + onSave, + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { openExternal } = useConfig(); + const { user } = useUser(); const columns = [ { name: 'name', label: formatMessage(labels.name) }, @@ -17,11 +25,19 @@ export function TeamWebsitesTable({ data = [], onSave }) { ]; return ( - + {row => { - const { teamId } = row; - const { id: websiteId, name, domain, userId } = row.website; - const { teamUser } = row.team; + const { id: teamId, teamUser } = row.teamWebsite[0].team; + const { id: websiteId, name, domain, userId } = row; const owner = teamUser[0]; const canRemove = user.id === userId || user.id === owner.userId; diff --git a/components/pages/settings/teams/TeamsList.js b/components/pages/settings/teams/TeamsList.js index 0c82639bf..061100f64 100644 --- a/components/pages/settings/teams/TeamsList.js +++ b/components/pages/settings/teams/TeamsList.js @@ -1,24 +1,37 @@ -import { useState } from 'react'; -import { Button, Icon, Modal, ModalTrigger, useToasts, Text, Flexbox } from 'react-basics'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import TeamAddForm from 'components/pages/settings/teams/TeamAddForm'; -import PageHeader from 'components/layout/PageHeader'; -import TeamsTable from 'components/pages/settings/teams/TeamsTable'; -import Page from 'components/layout/Page'; import Icons from 'components/icons'; -import TeamJoinForm from './TeamJoinForm'; +import Page from 'components/layout/Page'; +import PageHeader from 'components/layout/PageHeader'; +import TeamAddForm from 'components/pages/settings/teams/TeamAddForm'; +import TeamsTable from 'components/pages/settings/teams/TeamsTable'; import useApi from 'hooks/useApi'; import useMessages from 'hooks/useMessages'; -import { ROLES } from 'lib/constants'; import useUser from 'hooks/useUser'; +import { ROLES } from 'lib/constants'; +import { useState } from 'react'; +import { Button, Flexbox, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; +import TeamJoinForm from './TeamJoinForm'; +import useApiFilter from 'hooks/useApiFilter'; export default function TeamsList() { const { user } = useUser(); const { formatMessage, labels, messages } = useMessages(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); const [update, setUpdate] = useState(0); + const { get, useQuery } = useApi(); - const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`)); - const hasData = data && data.length !== 0; + const { data, isLoading, error } = useQuery(['teams', update, filter, page, pageSize], () => { + return get(`/teams`, { + filter, + page, + pageSize, + }); + }); + + const hasData = data && data?.data.length !== 0; + const isFiltered = filter; + const { showToast } = useToasts(); const handleSave = () => { @@ -71,15 +84,26 @@ export default function TeamsList() { return ( - {hasData && ( + {(hasData || isFiltered) && ( {joinButton} {createButton} )} - {hasData && } - {!hasData && ( + + {(hasData || isFiltered) && ( + + )} + + {!hasData && !isFiltered && ( {joinButton} diff --git a/components/pages/settings/teams/TeamsTable.js b/components/pages/settings/teams/TeamsTable.js index a344fefce..e35fb839b 100644 --- a/components/pages/settings/teams/TeamsTable.js +++ b/components/pages/settings/teams/TeamsTable.js @@ -1,14 +1,21 @@ +import SettingsTable from 'components/common/SettingsTable'; +import useLocale from 'hooks/useLocale'; +import useMessages from 'hooks/useMessages'; +import useUser from 'hooks/useUser'; +import { ROLES } from 'lib/constants'; import Link from 'next/link'; import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; import TeamDeleteForm from './TeamDeleteForm'; import TeamLeaveForm from './TeamLeaveForm'; -import useMessages from 'hooks/useMessages'; -import useUser from 'hooks/useUser'; -import { ROLES } from 'lib/constants'; -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'hooks/useLocale'; -export function TeamsTable({ data = [], onDelete }) { +export function TeamsTable({ + data = { data: [] }, + onDelete, + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); const { dir } = useLocale(); @@ -27,7 +34,17 @@ export function TeamsTable({ data = [], onDelete }) { }; return ( - + {row => { const { id, teamUser } = row; const owner = teamUser.find(({ role }) => role === ROLES.teamOwner); diff --git a/components/pages/settings/users/UsersList.js b/components/pages/settings/users/UsersList.js index 8886203b5..614aabefb 100644 --- a/components/pages/settings/users/UsersList.js +++ b/components/pages/settings/users/UsersList.js @@ -7,14 +7,27 @@ import UserAddButton from './UserAddButton'; import useApi from 'hooks/useApi'; import useUser from 'hooks/useUser'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; export function UsersList() { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); + const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), { - enabled: !!user, - }); + const { data, isLoading, error, refetch } = useQuery( + ['user', filter, page, pageSize], + () => + get(`/users`, { + filter, + page, + pageSize, + }), + { + enabled: !!user, + }, + ); const { showToast } = useToasts(); const hasData = data && data.length !== 0; @@ -33,8 +46,17 @@ export function UsersList() { - {hasData && } - {!hasData && ( + {(hasData || filter) && ( + + )} + {!hasData && !filter && ( diff --git a/components/pages/settings/users/UsersTable.js b/components/pages/settings/users/UsersTable.js index 2023efc5d..f4c9dd77a 100644 --- a/components/pages/settings/users/UsersTable.js +++ b/components/pages/settings/users/UsersTable.js @@ -8,7 +8,14 @@ import useMessages from 'hooks/useMessages'; import SettingsTable from 'components/common/SettingsTable'; import useLocale from 'hooks/useLocale'; -export function UsersTable({ data = [], onDelete }) { +export function UsersTable({ + data = { data: [] }, + onDelete, + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); const { dateLocale } = useLocale(); @@ -36,7 +43,17 @@ export function UsersTable({ data = [], onDelete }) { }; return ( - + {(row, keys, rowIndex) => { return ( <> diff --git a/components/pages/settings/websites/WebsitesList.js b/components/pages/settings/websites/WebsitesList.js index de423d0b2..310b481f7 100644 --- a/components/pages/settings/websites/WebsitesList.js +++ b/components/pages/settings/websites/WebsitesList.js @@ -8,14 +8,22 @@ import useApi from 'hooks/useApi'; import useUser from 'hooks/useUser'; import useMessages from 'hooks/useMessages'; import { ROLES } from 'lib/constants'; +import useApiFilter from 'hooks/useApiFilter'; export function WebsitesList() { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); const { get, useQuery } = useApi(); const { data, isLoading, error, refetch } = useQuery( - ['websites', user?.id], - () => get(`/users/${user?.id}/websites`), + ['websites', user?.id, filter, page, pageSize], + () => + get(`/users/${user?.id}/websites`, { + filter, + page, + pageSize, + }), { enabled: !!user }, ); const { showToast } = useToasts(); @@ -47,7 +55,15 @@ export function WebsitesList() { return ( {addButton} - {hasData && } + {hasData && ( + + )} {!hasData && ( {addButton} diff --git a/components/pages/settings/websites/WebsitesTable.js b/components/pages/settings/websites/WebsitesTable.js index 902393e6b..aa8cbe8a9 100644 --- a/components/pages/settings/websites/WebsitesTable.js +++ b/components/pages/settings/websites/WebsitesTable.js @@ -4,7 +4,13 @@ import SettingsTable from 'components/common/SettingsTable'; import useMessages from 'hooks/useMessages'; import useConfig from 'hooks/useConfig'; -export function WebsitesTable({ data = [] }) { +export function WebsitesTable({ + data = [], + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { openExternal } = useConfig(); @@ -15,7 +21,16 @@ export function WebsitesTable({ data = [] }) { ]; return ( - + {row => { const { id } = row; diff --git a/components/pages/websites/WebsiteReportsPage.js b/components/pages/websites/WebsiteReportsPage.js index 569270284..a1d49d101 100644 --- a/components/pages/websites/WebsiteReportsPage.js +++ b/components/pages/websites/WebsiteReportsPage.js @@ -7,7 +7,16 @@ import WebsiteHeader from './WebsiteHeader'; export function WebsiteReportsPage({ websiteId }) { const { formatMessage, labels } = useMessages(); - const { reports, error, isLoading, deleteReport } = useReports(websiteId); + const { + reports, + error, + isLoading, + deleteReport, + filter, + handleFilterChange, + handlePageChange, + handlePageSizeChange, + } = useReports(websiteId); const handleDelete = async id => { await deleteReport(id); @@ -26,7 +35,14 @@ export function WebsiteReportsPage({ websiteId }) { - + ); } diff --git a/hooks/useApiFilter.ts b/hooks/useApiFilter.ts new file mode 100644 index 000000000..d411fd434 --- /dev/null +++ b/hooks/useApiFilter.ts @@ -0,0 +1,28 @@ +import { useState } from 'react'; + +export function useApiFilter() { + const [filter, setFilter] = useState(); + const [filterType, setFilterType] = useState('All'); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const handleFilterChange = value => setFilter(value); + const handlePageChange = value => setPage(value); + const handlePageSizeChange = value => setPageSize(value); + + return { + filter, + setFilter, + filterType, + setFilterType, + page, + setPage, + pageSize, + setPageSize, + handleFilterChange, + handlePageChange, + handlePageSizeChange, + }; +} + +export default useApiFilter; diff --git a/hooks/useReports.js b/hooks/useReports.js index f4369eec3..57d76492b 100644 --- a/hooks/useReports.js +++ b/hooks/useReports.js @@ -1,12 +1,16 @@ import { useState } from 'react'; import useApi from './useApi'; +import useApiFilter from 'hooks/useApiFilter'; export function useReports(websiteId) { const [modified, setModified] = useState(Date.now()); const { get, useQuery, del, useMutation } = useApi(); const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); - const { data, error, isLoading } = useQuery(['reports:website', { websiteId, modified }], () => - get(`/reports`, { websiteId }), + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); + const { data, error, isLoading } = useQuery( + ['reports:website', { websiteId, modified, filter, page, pageSize }], + () => get(`/reports`, { websiteId, filter, page, pageSize }), ); const deleteReport = id => { @@ -17,7 +21,18 @@ export function useReports(websiteId) { }); }; - return { reports: data, error, isLoading, deleteReport }; + return { + reports: data, + error, + isLoading, + deleteReport, + filter, + page, + pageSize, + handleFilterChange, + handlePageChange, + handlePageSizeChange, + }; } export default useReports; diff --git a/lib/constants.ts b/lib/constants.ts index 887f90a96..9257298c1 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -30,6 +30,22 @@ export const FILTER_RANGE = 'filter-range'; export const FILTER_REFERRERS = 'filter-referrers'; export const FILTER_PAGES = 'filter-pages'; +export const USER_FILTER_TYPES = { + all: 'All', + username: 'Username', +} as const; +export const WEBSITE_FILTER_TYPES = { all: 'All', name: 'Name', domain: 'Domain' } as const; +export const TEAM_FILTER_TYPES = { all: 'All', name: 'Name', 'user:username': 'Owner' } as const; +export const REPORT_FILTER_TYPES = { + all: 'All', + name: 'Name', + description: 'Description', + type: 'Type', + 'user:username': 'Username', + 'website:name': 'Website Name', + 'website:domain': 'Website Domain', +} as const; + export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event']; export const SESSION_COLUMNS = [ diff --git a/lib/prisma.ts b/lib/prisma.ts index 753f1ae4a..c67ce4bc9 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -4,7 +4,7 @@ import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; -import { QueryFilters, QueryOptions } from './types'; +import { QueryFilters, QueryOptions, SearchFilter } from './types'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -128,6 +128,37 @@ async function rawQuery(sql: string, data: object): Promise { return prisma.rawQuery(query, params); } +function getPageFilters(filters: SearchFilter): [ + { + orderBy: { + [x: string]: string; + }[]; + take: number; + skip: number; + }, + { + pageSize: number; + page: number; + orderBy: string; + }, +] { + const { pageSize = 10, page = 1, orderBy } = filters; + + return [ + { + ...(pageSize > 0 && { take: pageSize, skip: pageSize * (page - 1) }), + ...(orderBy && { + orderBy: [ + { + [orderBy]: 'asc', + }, + ], + }), + }, + { pageSize, page: +page, orderBy }, + ]; +} + export default { ...prisma, getAddMinutesQuery, @@ -135,5 +166,6 @@ export default { getTimestampIntervalQuery, getFilterQuery, parseFilters, + getPageFilters, rawQuery, }; diff --git a/lib/types.ts b/lib/types.ts index dc54fd473..5a25169a9 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,17 +1,62 @@ import { NextApiRequest } from 'next'; -import { COLLECTION_TYPE, DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, ROLES } from './constants'; +import { + COLLECTION_TYPE, + DATA_TYPE, + EVENT_TYPE, + KAFKA_TOPIC, + REPORT_FILTER_TYPES, + ROLES, + TEAM_FILTER_TYPES, + USER_FILTER_TYPES, + WEBSITE_FILTER_TYPES, +} from './constants'; type ObjectValues = T[keyof T]; export type CollectionType = ObjectValues; - export type Role = ObjectValues; - export type EventType = ObjectValues; - export type DynamicDataType = ObjectValues; - export type KafkaTopic = ObjectValues; +export type ReportSearchFilterType = ObjectValues; +export type UserSearchFilterType = ObjectValues; +export type WebsiteSearchFilterType = ObjectValues; +export type TeamSearchFilterType = ObjectValues; + +export interface WebsiteSearchFilter extends SearchFilter { + userId?: string; + teamId?: string; + includeTeams?: boolean; +} + +export interface UserSearchFilter extends SearchFilter { + teamId?: string; +} + +export interface TeamSearchFilter extends SearchFilter { + userId?: string; +} + +export interface ReportSearchFilter extends SearchFilter { + userId?: string; + websiteId?: string; +} + +export interface SearchFilter { + filter?: string; + filterType?: T; + pageSize?: number; + page?: number; + orderBy?: string; +} + +export interface FilterResult { + data: T; + count: number; + pageSize: number; + page: number; + orderBy?: string; +} export interface DynamicData { [key: string]: number | string | DynamicData | number[] | string[] | DynamicData[]; diff --git a/package.json b/package.json index 647cdf418..89dc5e97f 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.91.0", + "react-basics": "^0.92.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts index c856b5659..8c6825f15 100644 --- a/pages/api/reports/index.ts +++ b/pages/api/reports/index.ts @@ -1,10 +1,12 @@ -import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createReport, getWebsiteReports } from 'queries'; import { canViewWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { createReport, getReportsByWebsiteId } from 'queries'; + +export interface ReportsRequestQuery extends SearchFilter {} export interface ReportRequestBody { websiteId: string; @@ -35,7 +37,13 @@ export default async ( return unauthorized(res); } - const data = await getWebsiteReports(websiteId); + const { page, filter, pageSize } = req.query; + + const data = await getReportsByWebsiteId(websiteId, { + page, + filter, + pageSize: +pageSize || null, + }); return ok(res, data); } diff --git a/pages/api/teams/[id]/users/index.ts b/pages/api/teams/[id]/users/index.ts index c73da6836..6f8b077e0 100644 --- a/pages/api/teams/[id]/users/index.ts +++ b/pages/api/teams/[id]/users/index.ts @@ -1,11 +1,11 @@ import { canUpdateTeam, canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, getTeamUsers, getUserByUsername } from 'queries'; +import { createTeamUser, getUserByUsername, getUsersByTeamId } from 'queries'; -export interface TeamUserRequestQuery { +export interface TeamUserRequestQuery extends SearchFilter { id: string; } @@ -27,7 +27,13 @@ export default async ( return unauthorized(res); } - const users = await getTeamUsers(teamId); + const { page, filter, pageSize } = req.query; + + const users = await getUsersByTeamId(teamId, { + page, + filter, + pageSize: +pageSize || null, + }); return ok(res, users); } diff --git a/pages/api/teams/[id]/websites/index.ts b/pages/api/teams/[id]/websites/index.ts index 63be478b1..dcd08939a 100644 --- a/pages/api/teams/[id]/websites/index.ts +++ b/pages/api/teams/[id]/websites/index.ts @@ -1,11 +1,12 @@ import { canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamWebsites, getTeamWebsites } from 'queries/admin/teamWebsite'; +import { getWebsites, getWebsitesByTeamId } from 'queries'; +import { createTeamWebsites } from 'queries/admin/teamWebsite'; -export interface TeamWebsiteRequestQuery { +export interface TeamWebsiteRequestQuery extends SearchFilter { id: string; } @@ -26,7 +27,13 @@ export default async ( return unauthorized(res); } - const websites = await getTeamWebsites(teamId); + const { page, filter, pageSize } = req.query; + + const websites = await getWebsitesByTeamId(teamId, { + page, + filter, + pageSize: +pageSize || null, + }); return ok(res, websites); } diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index 453f1ef3b..997ed8854 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -1,18 +1,19 @@ import { Team } from '@prisma/client'; -import { NextApiRequestQueryBody } from 'lib/types'; import { canCreateTeam } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; +import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeam, getUserTeams } from 'queries'; +import { createTeam, getTeamsByUserId } from 'queries'; -export interface TeamsRequestBody { +export interface TeamsRequestQuery extends SearchFilter {} +export interface TeamsRequestBody extends SearchFilter { name: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); @@ -22,9 +23,11 @@ export default async ( } = req.auth; if (req.method === 'GET') { - const teams = await getUserTeams(userId); + const { page, filter, pageSize } = req.query; - return ok(res, teams); + const results = await getTeamsByUserId(userId, { page, filter, pageSize: +pageSize || null }); + + return ok(res, results); } if (req.method === 'POST') { diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts index e94094a45..e1761291d 100644 --- a/pages/api/users/[id]/websites.ts +++ b/pages/api/users/[id]/websites.ts @@ -1,9 +1,12 @@ import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserWebsites } from 'queries'; +import { getWebsitesByUserId } from 'queries'; +export interface UserWebsitesRequestQuery extends SearchFilter { + id: string; +} export interface UserWebsitesRequestBody { name: string; domain: string; @@ -17,16 +20,19 @@ export default async ( await useCors(req, res); await useAuth(req, res); const { user } = req.auth; - const { id: userId } = req.query; + const { id: userId, page, filter, pageSize, includeTeams } = req.query; if (req.method === 'GET') { if (!user.isAdmin && user.id !== userId) { return unauthorized(res); } - const { includeTeams } = req.query; - - const websites = await getUserWebsites(userId, { includeTeams }); + const websites = await getWebsitesByUserId(userId, { + page, + filter, + pageSize: +pageSize || null, + includeTeams, + }); return ok(res, websites); } diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 6f6c205f4..5e913c027 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -2,11 +2,12 @@ import { canCreateUser, canViewUsers } from 'lib/auth'; import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, User } from 'lib/types'; +import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUserByUsername, getUsers } from 'queries'; +export interface UsersRequestQuery extends SearchFilter {} export interface UsersRequestBody { username: string; password: string; @@ -15,7 +16,7 @@ export interface UsersRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); @@ -25,7 +26,9 @@ export default async ( return unauthorized(res); } - const users = await getUsers(); + const { page, filter, pageSize } = req.query; + + const users = await getUsers({ page, filter, pageSize: +pageSize || null }); return ok(res, users); } diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index c8b5aba29..f94fa0378 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,12 +1,14 @@ import { canCreateWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; import userWebsites from 'pages/api/users/[id]/websites'; +export interface WebsitesRequestQuery extends SearchFilter {} + export interface WebsitesRequestBody { name: string; domain: string; @@ -14,7 +16,7 @@ export interface WebsitesRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); @@ -26,6 +28,7 @@ export default async ( if (req.method === 'GET') { req.query.id = userId; + req.query.pageSize = 100; return userWebsites(req, res); } diff --git a/queries/admin/report.ts b/queries/admin/report.ts index ee7a05920..d2523f82f 100644 --- a/queries/admin/report.ts +++ b/queries/admin/report.ts @@ -1,5 +1,7 @@ import { Prisma, Report } from '@prisma/client'; +import { REPORT_FILTER_TYPES } from 'lib/constants'; import prisma from 'lib/prisma'; +import { FilterResult, ReportSearchFilter, ReportSearchFilterType, SearchFilter } from 'lib/types'; export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise { return prisma.client.report.create({ data }); @@ -13,22 +15,6 @@ export async function getReportById(reportId: string): Promise { }); } -export async function getUserReports(userId: string): Promise { - return prisma.client.report.findMany({ - where: { - userId, - }, - }); -} - -export async function getWebsiteReports(websiteId: string): Promise { - return prisma.client.report.findMany({ - where: { - websiteId, - }, - }); -} - export async function updateReport( reportId: string, data: Prisma.ReportUpdateInput, @@ -39,3 +25,103 @@ export async function updateReport( export async function deleteReport(reportId: string): Promise { return prisma.client.report.delete({ where: { id: reportId } }); } + +export async function getReports( + ReportSearchFilter: ReportSearchFilter, +): Promise> { + const { userId, websiteId, filter, filterType = REPORT_FILTER_TYPES.all } = ReportSearchFilter; + const where: Prisma.ReportWhereInput = { + ...(userId && { userId: userId }), + ...(websiteId && { websiteId: websiteId }), + ...(filter && { + AND: { + OR: [ + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES.name) && { + name: { + startsWith: filter, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES.description) && { + description: { + startsWith: filter, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES.type) && { + type: { + startsWith: filter, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES['user:username']) && { + user: { + username: { + startsWith: filter, + }, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES['website:name']) && { + website: { + name: { + startsWith: filter, + }, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES['website:domain']) && { + website: { + domain: { + startsWith: filter, + }, + }, + }), + }, + ], + }, + }), + }; + + const [pageFilters, getParameters] = prisma.getPageFilters(ReportSearchFilter); + + const reports = await prisma.client.report.findMany({ + where, + ...pageFilters, + }); + const count = await prisma.client.report.count({ + where, + }); + + return { + data: reports, + count, + ...getParameters, + }; +} + +export async function getReportsByUserId( + userId: string, + filter: SearchFilter, +): Promise> { + return getReports({ userId, ...filter }); +} + +export async function getReportsByWebsiteId( + websiteId: string, + filter: SearchFilter, +): Promise> { + return getReports({ websiteId, ...filter }); +} diff --git a/queries/admin/team.ts b/queries/admin/team.ts index a8b3385ca..978382276 100644 --- a/queries/admin/team.ts +++ b/queries/admin/team.ts @@ -1,7 +1,8 @@ import { Prisma, Team } from '@prisma/client'; import prisma from 'lib/prisma'; -import { ROLES } from 'lib/constants'; +import { ROLES, TEAM_FILTER_TYPES } from 'lib/constants'; import { uuid } from 'lib/crypto'; +import { FilterResult, TeamSearchFilter, TeamSearchFilterType, SearchFilter } from 'lib/types'; export interface GetTeamOptions { includeTeamUser?: boolean; @@ -26,12 +27,6 @@ export function getTeamByAccessCode(accessCode: string, options: GetTeamOptions return getTeam({ accessCode }, options); } -export async function getTeams(where: Prisma.TeamWhereInput): Promise { - return prisma.client.team.findMany({ - where, - }); -} - export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise { const { id } = data; @@ -85,3 +80,82 @@ export async function deleteTeam( }), ]); } + +export async function getTeams( + TeamSearchFilter: TeamSearchFilter, + options?: { include?: Prisma.TeamInclude }, +): Promise> { + const { userId, filter, filterType = TEAM_FILTER_TYPES.all } = TeamSearchFilter; + const where: Prisma.TeamWhereInput = { + ...(userId && { + teamUser: { + some: { userId }, + }, + }), + ...(filter && { + AND: { + OR: [ + { + ...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES.name) && { + name: { startsWith: filter }, + }), + }, + { + ...((filterType === TEAM_FILTER_TYPES.all || + filterType === TEAM_FILTER_TYPES['user:username']) && { + teamUser: { + every: { + role: ROLES.teamOwner, + user: { + username: { + startsWith: filter, + }, + }, + }, + }, + }), + }, + ], + }, + }), + }; + + const [pageFilters, getParameters] = prisma.getPageFilters({ + orderBy: 'name', + ...TeamSearchFilter, + }); + + const teams = await prisma.client.team.findMany({ + where: { + ...where, + }, + ...pageFilters, + ...(options?.include && { include: options?.include }), + }); + const count = await prisma.client.team.count({ where }); + + return { data: teams, count, ...getParameters }; +} + +export async function getTeamsByUserId( + userId: string, + filter?: SearchFilter, +): Promise> { + return getTeams( + { userId, ...filter }, + { + include: { + teamUser: { + include: { + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, + }, + }, + ); +} diff --git a/queries/admin/user.ts b/queries/admin/user.ts index f60c48010..f4be4751f 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -1,9 +1,9 @@ -import { Prisma, Team, TeamUser } from '@prisma/client'; -import { getRandomChars } from 'next-basics'; +import { Prisma } from '@prisma/client'; import cache from 'lib/cache'; -import { ROLES } from 'lib/constants'; +import { ROLES, USER_FILTER_TYPES } from 'lib/constants'; import prisma from 'lib/prisma'; -import { Website, User, Role } from 'lib/types'; +import { FilterResult, Role, User, UserSearchFilter } from 'lib/types'; +import { getRandomChars } from 'next-basics'; export interface GetUserOptions { includePassword?: boolean; @@ -36,125 +36,59 @@ export async function getUserByUsername(username: string, options: GetUserOption return getUser({ username }, options); } -export async function getUsers(): Promise { - return prisma.client.user.findMany({ - take: 100, - where: { - deletedAt: null, - }, - orderBy: [ - { - username: 'asc', - }, - ], - select: { - id: true, - username: true, - role: true, - createdAt: true, - }, - }); -} - -export async function getUserTeams(userId: string): Promise< - (Team & { - teamUser: (TeamUser & { - user: { id: string; username: string }; - })[]; - })[] -> { - return prisma.client.team.findMany({ - where: { +export async function getUsers( + UserSearchFilter: UserSearchFilter = {}, + options?: { include?: Prisma.UserInclude }, +): Promise> { + const { teamId, filter, filterType = USER_FILTER_TYPES.all } = UserSearchFilter; + const where: Prisma.UserWhereInput = { + ...(teamId && { teamUser: { some: { - userId, + teamId, }, }, - }, - include: { - teamUser: { - include: { - user: { - select: { - id: true, - username: true, - }, + }), + ...(filter && { + AND: { + OR: [ + { + ...((filterType === USER_FILTER_TYPES.all || + filterType === USER_FILTER_TYPES.username) && { + username: { + startsWith: filter, + }, + }), }, - }, + ], }, - }, + }), + }; + const [pageFilters, getParameters] = prisma.getPageFilters({ + orderBy: 'username', + ...UserSearchFilter, }); -} -export async function getUserWebsites( - userId: string, - options?: { includeTeams: boolean }, -): Promise { - const { rawQuery } = prisma; - - if (options?.includeTeams) { - const websites = await rawQuery( - ` - select - website_id as "id", - name, - domain, - share_id as "shareId", - reset_at as "resetAt", - user_id as "userId", - created_at as "createdAt", - updated_at as "updatedAt", - deleted_at as "deletedAt", - null as "teamId", - null as "teamName" - from website - where user_id = {{userId::uuid}} - and deleted_at is null - union - select - w.website_id as "id", - w.name, - w.domain, - w.share_id as "shareId", - w.reset_at as "resetAt", - w.user_id as "userId", - w.created_at as "createdAt", - w.updated_at as "updatedAt", - w.deleted_at as "deletedAt", - t.team_id as "teamId", - t.name as "teamName" - from website w - inner join team_website tw - on tw.website_id = w.website_id - inner join team t - on t.team_id = tw.team_id - inner join team_user tu - on tu.team_id = tw.team_id - where tu.user_id = {{userId::uuid}} - and w.deleted_at is null - `, - { userId }, - ); - - return websites.reduce((arr, item) => { - if (!arr.find(({ id }) => id === item.id)) { - return arr.concat(item); - } - return arr; - }, []); - } - - return prisma.client.website.findMany({ + const users = await prisma.client.user.findMany({ where: { - userId, + ...where, deletedAt: null, }, - orderBy: [ - { - name: 'asc', - }, - ], + ...pageFilters, + ...(options?.include && { include: options.include }), }); + const count = await prisma.client.user.count({ + where: { + ...where, + deletedAt: null, + }, + }); + + return { data: users as any, count, ...getParameters }; +} + +export async function getUsersByTeamId(teamId: string, filter?: UserSearchFilter) { + return getUsers({ teamId, ...filter }); } export async function createUser(data: { diff --git a/queries/admin/website.ts b/queries/admin/website.ts index 35f32bac2..68f634a6c 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -1,6 +1,8 @@ import { Prisma, Website } from '@prisma/client'; import cache from 'lib/cache'; +import { ROLES, WEBSITE_FILTER_TYPES } from 'lib/constants'; import prisma from 'lib/prisma'; +import { FilterResult, WebsiteSearchFilter } from 'lib/types'; async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise { return prisma.client.website.findUnique({ @@ -16,11 +18,199 @@ export async function getWebsiteByShareId(shareId: string) { return getWebsite({ shareId }); } -export async function getWebsites(): Promise { - return prisma.client.website.findMany({ - orderBy: { - name: 'asc', +export async function getWebsites( + WebsiteSearchFilter: WebsiteSearchFilter, + options?: { include?: Prisma.WebsiteInclude }, +): Promise> { + const { + userId, + teamId, + includeTeams, + filter, + filterType = WEBSITE_FILTER_TYPES.all, + } = WebsiteSearchFilter; + + const filterQuery = { + AND: { + OR: [ + { + ...((filterType === WEBSITE_FILTER_TYPES.all || + filterType === WEBSITE_FILTER_TYPES.name) && { + name: { startsWith: filter }, + }), + }, + { + ...((filterType === WEBSITE_FILTER_TYPES.all || + filterType === WEBSITE_FILTER_TYPES.domain) && { + domain: { startsWith: filter }, + }), + }, + ], }, + }; + + const where: Prisma.WebsiteWhereInput = { + ...(teamId && { + teamWebsite: { + some: { + teamId, + }, + }, + }), + AND: { + OR: [ + { + ...(userId && { + userId, + }), + }, + { + ...(includeTeams && { + teamWebsite: { + some: { + team: { + teamUser: { + some: { + userId, + }, + }, + }, + }, + }, + }), + }, + ], + }, + ...(filter && filterQuery), + }; + + const [pageFilters, getParameters] = prisma.getPageFilters({ + orderBy: 'name', + ...WebsiteSearchFilter, + }); + + const websites = await prisma.client.website.findMany({ + where: { + ...where, + deletedAt: null, + }, + ...pageFilters, + ...(options?.include && { include: options.include }), + }); + const count = await prisma.client.website.count({ where }); + + return { data: websites, count, ...getParameters }; +} + +export async function getWebsitesByUserId( + userId: string, + filter?: WebsiteSearchFilter, +): Promise> { + return getWebsites({ userId, ...filter }); +} + +export async function getWebsitesByTeamId( + teamId: string, + filter?: WebsiteSearchFilter, +): Promise> { + return getWebsites( + { + teamId, + ...filter, + includeTeams: true, + }, + { + include: { + teamWebsite: { + include: { + team: { + include: { + teamUser: { + where: { role: ROLES.teamOwner }, + }, + }, + }, + }, + }, + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, + ); +} + +export async function getUserWebsites( + userId: string, + options?: { includeTeams: boolean }, +): Promise { + const { rawQuery } = prisma; + + if (options?.includeTeams) { + const websites = await rawQuery( + ` + select + website_id as "id", + name, + domain, + share_id as "shareId", + reset_at as "resetAt", + user_id as "userId", + created_at as "createdAt", + updated_at as "updatedAt", + deleted_at as "deletedAt", + null as "teamId", + null as "teamName" + from website + where user_id = {{userId::uuid}} + and deleted_at is null + union + select + w.website_id as "id", + w.name, + w.domain, + w.share_id as "shareId", + w.reset_at as "resetAt", + w.user_id as "userId", + w.created_at as "createdAt", + w.updated_at as "updatedAt", + w.deleted_at as "deletedAt", + t.team_id as "teamId", + t.name as "teamName" + from website w + inner join team_website tw + on tw.website_id = w.website_id + inner join team t + on t.team_id = tw.team_id + inner join team_user tu + on tu.team_id = tw.team_id + where tu.user_id = {{userId::uuid}} + and w.deleted_at is null + `, + { userId }, + ); + + return websites.reduce((arr, item) => { + if (!arr.find(({ id }) => id === item.id)) { + return arr.concat(item); + } + return arr; + }, []); + } + + return prisma.client.website.findMany({ + where: { + userId, + deletedAt: null, + }, + orderBy: [ + { + name: 'asc', + }, + ], }); } diff --git a/yarn.lock b/yarn.lock index d9224c2a4..115e3cc9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,10 +7557,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.91.0: - version "0.91.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.91.0.tgz#2970529a22a455ec73a1be884eb93a109c9dafc0" - integrity sha512-vP8LYWiFwA+eguMEuHvHct4Jl5R/2GUjWc1tMujDG0CsAAUGhx68tAJr0K3gBrWjmpJrTPVfX8SdBNKSDAjQsw== +react-basics@^0.92.0: + version "0.92.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.92.0.tgz#02bc6e88bdaf189c30cc6cbd8bbb1c9d12cd089b" + integrity sha512-BVUWg5a7R88konA9NedYMBx1hl50d6h/MD7qlKOEO/Cnm8cOC7AYTRKAKhO6kHMWjY4ZpUuvlg0UcF+SJP/uXA== dependencies: classnames "^2.3.1" date-fns "^2.29.3" From cbe1a21e671db9ebf343600055219e2345957542 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 10 Aug 2023 22:50:41 -0700 Subject: [PATCH 041/357] Add query types. --- components/common/SettingsTable.js | 2 +- pages/api/me/teams.ts | 9 +++++++-- pages/api/me/websites.ts | 9 +++++++-- pages/api/users/[id]/teams.ts | 20 +++++++++++++++----- pages/api/users/[id]/websites.ts | 1 + 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/components/common/SettingsTable.js b/components/common/SettingsTable.js index 9fb4c2a98..a57919f15 100644 --- a/components/common/SettingsTable.js +++ b/components/common/SettingsTable.js @@ -42,7 +42,7 @@ export function SettingsTable({ delay={1000} value={filter} placeholder="Search" - style={{ maxWidth: '300px', 'margin-bottom': '10px' }} + style={{ maxWidth: '300px', marginBottom: '10px' }} /> )} {value.length === 0 && filterValue && ( diff --git a/pages/api/me/teams.ts b/pages/api/me/teams.ts index 366990163..d323043b0 100644 --- a/pages/api/me/teams.ts +++ b/pages/api/me/teams.ts @@ -1,10 +1,15 @@ import { useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userTeams from 'pages/api/users/[id]/teams'; -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { +export interface MyTeamsRequestQuery extends SearchFilter {} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); if (req.method === 'GET') { diff --git a/pages/api/me/websites.ts b/pages/api/me/websites.ts index 29f1e4311..f9ccbcabd 100644 --- a/pages/api/me/websites.ts +++ b/pages/api/me/websites.ts @@ -1,11 +1,16 @@ import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userWebsites from 'pages/api/users/[id]/websites'; -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { +export interface MyWebsitesRequestQuery extends SearchFilter {} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/users/[id]/teams.ts b/pages/api/users/[id]/teams.ts index c31b98ca7..831a992d8 100644 --- a/pages/api/users/[id]/teams.ts +++ b/pages/api/users/[id]/teams.ts @@ -1,17 +1,21 @@ import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserTeams } from 'queries'; +import { getTeamsByUserId } from 'queries'; -export interface UserWebsitesRequestBody { +export interface UserTeamsRequestQuery extends SearchFilter { + id: string; +} + +export interface UserTeamsRequestBody { name: string; domain: string; shareId: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); @@ -25,7 +29,13 @@ export default async ( return unauthorized(res); } - const teams = await getUserTeams(userId); + const { page, filter, pageSize } = req.query; + + const teams = await getTeamsByUserId(userId, { + page, + filter, + pageSize: +pageSize || null, + }); return ok(res, teams); } diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts index e1761291d..72d793d1f 100644 --- a/pages/api/users/[id]/websites.ts +++ b/pages/api/users/[id]/websites.ts @@ -19,6 +19,7 @@ export default async ( ) => { await useCors(req, res); await useAuth(req, res); + const { user } = req.auth; const { id: userId, page, filter, pageSize, includeTeams } = req.query; From 37f0e374174de85f00c4a66d0ddf479ce60b26c8 Mon Sep 17 00:00:00 2001 From: Yan Waipann Date: Fri, 11 Aug 2023 21:00:59 +0630 Subject: [PATCH 042/357] feat: add some Myanmar translation --- lang/my-MM.json | 195 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/lang.js | 1 + 2 files changed, 196 insertions(+) create mode 100644 lang/my-MM.json diff --git a/lang/my-MM.json b/lang/my-MM.json new file mode 100644 index 000000000..669934939 --- /dev/null +++ b/lang/my-MM.json @@ -0,0 +1,195 @@ +{ + "label.access-code": "ဝင်ခွင့်ကုဒ်", + "label.actions": "လုပ်ဆောင်ချက်များ", + "label.activity-log": "လုပ်ဆောင်ချက်စာရင်း", + "label.add": "ထပ်ထည့်မည်", + "label.add-description": "အကြောင်းအရာဖော်ပြချက် ထည့်မည်", + "label.add-website": "ဝက်ဘ်ဆိုဒ်ထည့်မည်", + "label.admin": "အက်ဒမင်", + "label.all": "အားလုံး", + "label.all-time": "အချိန်အစမှအခုထိ", + "label.analytics": "အန်နလစ်တစ်", + "label.average-visit-time": "ဝဘက်ဘ်ဆိုဒ်တွင် ပျမ်းမျှကုန်ဆုံးချိန်", + "label.back": "နောက်သို့", + "label.bounce-rate": "Bounce နှုန်း", + "label.browsers": "ဝက်ဘ်ဘရောင်ဇာများ", + "label.cancel": "မလုပ်တော့ပါ", + "label.change-password": "စကားဝှက် ပြောင်းမည်", + "label.cities": "မြို့များ", + "label.clear-all": "အားလုံးကိုဖျက်မည်", + "label.confirm": "အတည်ပြုသည်", + "label.confirm-password": "စကားဝှက်အတည်ပြုသည်", + "label.continue": "ဆက်သွားမည်", + "label.countries": "နိုင်ငံများ", + "label.create-team": "Team ပြုလုပ်မည်", + "label.create-user": "အသုံးပြုသူထည့်မည်", + "label.created": "ပြုလုပ်ပြီးသော", + "label.current-password": "လက်ရှိစကားဝှက်", + "label.custom-range": "အချိန်အပိုင်းအခြားရွေးရန်", + "label.dashboard": "ဒက်ရှ်ဘုတ်", + "label.data": "ဒေတာ", + "label.date-range": "ရက်အပိုင်းအခြား", + "label.default-date-range": "ပုံသေ ရက်အပိုင်းအခြား", + "label.delete": "ဖျက်မည်", + "label.delete-team": "Team ကိုဖျက်မည်", + "label.delete-user": "အသုံးပြုသူကိုဖျက်မည်", + "label.delete-website": "ဝက်ဘ်ဆိုဒ်ကိုဖျက်မည်", + "label.desktop": "စားပွဲတင်ကွန်ပျူတာ", + "label.details": "အသေးစိတ်", + "label.devices": "အသုံးပြုသည့် ကိရိယာများ", + "label.dismiss": "ပိတ်ပါ", + "label.domain": "ဒိုမိန်း", + "label.dropoff": "Dropoff", + "label.edit": "ပြုပြင်မည်", + "label.edit-dashboard": "ဒက်ရှ်ဘုတ်ကို ပြုပြင်မည်", + "label.enable-share-url": "ဝေငှခြင်းကိုလင့်ကို ဖွင့်မည်", + "label.event": "အဖြစ်အပျက်", + "label.event-data": "အဖြစ်အပျက် ဒေတာ", + "label.events": "အဖြစ်အပျက်များ", + "label.field": "Field အမည်", + "label.fields": "Field အမည်များ", + "label.filter-combined": "ပေါင်းစပ်ပြီး", + "label.filter-raw": "အရှိအတိုင်း", + "label.funnel": "ဖန်နယ်", + "label.insights": "အသေးစိတ်သိမြင်နိုင်ရန်", + "label.join": "ဝင်မည်", + "label.join-team": "Team သို့ဝင်မည်", + "label.language": "ဘာသာစကား", + "label.languages": "ဘာသာစကားများ", + "label.laptop": "လက်တော့ပ်", + "label.last-days": "လွန်ခဲ့သော {x} ရက်က", + "label.last-hours": "လွန်ခဲ့သော {x} နာရီက", + "label.leave": "ထွက်မည်", + "label.leave-team": "Team မှထွက်မည်", + "label.login": "လော့ဂ်အင်", + "label.logout": "လော့ဂ်အောက်လုပ်မည်", + "label.members": "အဖွဲ့ဝင်များ", + "label.mobile": "မိုဘိုင်း", + "label.more": "နောက်ထပ်", + "label.name": "အမည်", + "label.new-password": "စကားဝှက်အသစ်", + "label.none": "မရှိပါ", + "label.operating-systems": "ကွန်ပျူတာလည်ပတ်မှုစနစ်", + "label.owner": "ပိုင်ဆိုင်သူ", + "label.page-views": "ဝင်ရောက်ကြည့်ရှုသူ", + "label.pages": "စာမျက်နှာများ", + "label.password": "စကားဝှက်", + "label.powered-by": "{name} ထောက်ပံ့သည်", + "label.profile": "ပရိုဖိုင်း", + "label.queries": "Queries (ကွာရီများ)", + "label.query": "Query (ကွာရီ)", + "label.query-parameters": "Query parameters (ကွာရီပါရာမီတာများ)", + "label.realtime": "အချိန်နှင့်တပြေးညီ", + "label.referrers": "ရည်ညွှန်းမှုများ", + "label.refresh": "Refresh လုပ်မည်", + "label.regenerate": "ပြန်ထုတ်မည်", + "label.regions": "ဒေသများ", + "label.remove": "ဖျက်မည်", + "label.reports": "တင်ပြမှုများ", + "label.required": "လိုအပ်သည်", + "label.reset": "ပြန်စမည်", + "label.reset-website": "ဝက်ဘ်ဆိုဒ်ဒေတာကိုဖျက်မည်", + "label.role": "အခန်းကဏ္ဍ", + "label.run-query": "Query ကိုလုပ်ဆောင်မည်", + "label.save": "သိမ်းဆည်းမည်", + "label.screens": "မြင်ကွင်းများ", + "label.select-date": "ရက်ရွေးပါ", + "label.select-website": "ဝဘက်ဘ်ဆိုဒ်ရွေးပါ", + "label.sessions": "ဆက်ရှင်များ", + "label.settings": "ဆက်တင်များ", + "label.share-url": "URL ကိုရှဲမည်", + "label.single-day": "တစ်ရက်အတွင်း", + "label.tablet": "တက်ဘလက်", + "label.team": "အသင်း", + "label.team-guest": "အသင်း ဧည့်သည်", + "label.team-id": "အသင်း အိုင်ဒီ", + "label.team-member": "အသင်းဝင်", + "label.team-owner": "အသင်းကိုပိုင်ဆိုင်သူ", + "label.teams": "အသင်းများ", + "label.theme": "Theme (အပြင်အဆင်)", + "label.this-month": "ယခုလ", + "label.this-week": "ယခုအပတ်", + "label.this-year": "ယခုနှစ်", + "label.timezone": "အချိန်ဇုန်", + "label.title": "ခေါင်းစဥ်", + "label.today": "ယနေ့", + "label.toggle-charts": "ဇယားများကို အဖွင့်အပိတ်လုပ်မည်", + "label.tracking-code": "ထရက်လုပ်သည့် ကုဒ်", + "label.unique-visitors": "ဝင်ရောက်သူ (ထပ်ခြင်းမရှိ)", + "label.unknown": "မသိသော", + "label.url": "URL", + "label.urls": "URL များ", + "label.user": "အသုံးပြုသူ", + "label.username": "အသုံးပြုသူအမည်", + "label.users": "အသုံးပြုသူများ", + "label.view": "ဝင်ရောက်ကြည့်ရှုမှု", + "label.view-details": "အသေးစိတ်ကို ကြည့်ရှုမည်", + "label.view-only": "ဝင်ရောက်ကြည့်ရှုမှုများသာ", + "label.views": "ဝင်ရောက်ကြည့်ရှုမှုများ", + "label.visitors": "ဝင်ရောက်ကြည့်ရှုသူများ", + "label.website": "ဝက်ဘ်ဆိုဒ်", + "label.website-id": "ဝက်ဘ်ဆိုဒ် အိုင်ဒီ", + "label.websites": "ဝက်ဘ်ဆိုဒ်များ", + "label.window": "ဝင်းဒိုး", + "label.yesterday": "မနေ့က", + "labels.after": "ပြီးနောက်", + "labels.average": "ပျမ်းမျှ", + "labels.before": "မတိုင်မီ", + "labels.breakdown": "ခွဲခြမ်းစိတ်ဖြာမှု", + "labels.contains": "ပါဝင်သည်", + "labels.create-report": "ရီပို့လုပ်မည်", + "labels.description": "ရှင်းပြချက်", + "labels.does-not-contain": "မပါဝင်ပါ", + "labels.does-not-equal": "မတူညီပါ", + "labels.equals": "တူညီသည်", + "labels.false": "မှားသည်", + "labels.filters": "Filter များ", + "labels.greater-than": "ထက်ပို၍ကြီးသည်", + "labels.greater-than-equals": "ထက်ပို၍ကြီးသည်သို့မဟုတ်တူသည်", + "labels.less-than": "ထက်ပို၍ငယ်သည်", + "labels.less-than-equals": "ထက်ပို၍ငယ်သည်သို့မဟုတ်တူသည်", + "labels.max": "အများဆုံး", + "labels.min": "အနည်းဆုံး", + "labels.overview": "အပေါ်ယံမြင်ကွင်း", + "labels.sum": "ပေါင်းလဒ်", + "labels.total": "စုစုပေါင်း", + "labels.total-records": "စုစုပေါင်း", + "labels.true": "True", + "labels.type": "Type", + "labels.unique": "Unique", + "labels.untitled": "Untitled", + "labels.value": "Value", + "message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}", + "message.confirm-delete": "Are you sure you want to delete {target}?", + "message.confirm-leave": "Are you sure you want to leave {target}?", + "message.confirm-reset": "Are you sure you want to reset {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", + "message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", + "message.delete-website-warning": "All website data will be deleted.", + "message.error": "Something went wrong.", + "message.event-log": "{event} on {url}", + "message.go-to-settings": "Go to settings", + "message.incorrect-username-password": "Incorrect username and/or password.", + "message.invalid-domain": "Invalid domain. Do not include http/https.", + "message.min-password-length": "Minimum length of {n} characters", + "message.no-data-available": "No data available.", + "message.no-event-data": "No event data is available.", + "message.no-match-password": "Passwords do not match.", + "message.no-teams": "You have not created any teams.", + "message.no-users": "There are no users.", + "message.page-not-found": "Page not found", + "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", + "message.reset-website-warning": "All statistics for this website will be deleted, but your settings will remain intact.", + "message.saved": "Saved.", + "message.share-url": "Your website stats are publically available at the following URL:", + "message.team-already-member": "You are already a member of the team.", + "message.team-not-found": "Team not found.", + "message.tracking-code": "To track stats for this website, place the following code in the ... section of your HTML.", + "message.user-deleted": "User deleted.", + "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}", + "message.no-results-found": "No results were found.", + "message.no-team-websites": "This team does not have any websites.", + "message.no-websites-configured": "You do not have any websites configured.", + "message.team-websites-info": "Websites can be viewed by anyone on the team.", + "message.new-version-available": "A new version of Umami {version} is available!" +} diff --git a/lib/lang.js b/lib/lang.js index 0b03c4495..cf90c458c 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -93,6 +93,7 @@ export const languages = { 'vi-VN': { label: 'Tiếng Việt', dateLocale: vi }, 'zh-CN': { label: '中文', dateLocale: zhCN }, 'zh-TW': { label: '中文(繁體)', dateLocale: zhTW }, + 'my-MM': { label: 'မြန်မာဘာသာ', dateLocale: enUS }, }; export function getDateLocale(locale) { From e6ab1e8b894984844680cffc176949f99f219b80 Mon Sep 17 00:00:00 2001 From: Htet Oo Wai Yan Date: Fri, 11 Aug 2023 21:21:51 +0630 Subject: [PATCH 043/357] feat: add the rest of Myanmar translations --- lang/my-MM.json | 80 ++++++++++++++++++++++++------------------------- lib/lang.js | 2 +- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/lang/my-MM.json b/lang/my-MM.json index 669934939..de92b275d 100644 --- a/lang/my-MM.json +++ b/lang/my-MM.json @@ -53,14 +53,14 @@ "label.funnel": "ဖန်နယ်", "label.insights": "အသေးစိတ်သိမြင်နိုင်ရန်", "label.join": "ဝင်မည်", - "label.join-team": "Team သို့ဝင်မည်", + "label.join-team": "အသင်းဝင်မည်", "label.language": "ဘာသာစကား", "label.languages": "ဘာသာစကားများ", "label.laptop": "လက်တော့ပ်", "label.last-days": "လွန်ခဲ့သော {x} ရက်က", "label.last-hours": "လွန်ခဲ့သော {x} နာရီက", "label.leave": "ထွက်မည်", - "label.leave-team": "Team မှထွက်မည်", + "label.leave-team": "အသင်းမှထွက်မည်", "label.login": "လော့ဂ်အင်", "label.logout": "လော့ဂ်အောက်လုပ်မည်", "label.members": "အဖွဲ့ဝင်များ", @@ -153,43 +153,43 @@ "labels.overview": "အပေါ်ယံမြင်ကွင်း", "labels.sum": "ပေါင်းလဒ်", "labels.total": "စုစုပေါင်း", - "labels.total-records": "စုစုပေါင်း", - "labels.true": "True", - "labels.type": "Type", + "labels.total-records": "မှတ်တမ်းစုစုပေါင်း", + "labels.true": "မှန်သည်", + "labels.type": "အမျိုးအစား", "labels.unique": "Unique", - "labels.untitled": "Untitled", - "labels.value": "Value", - "message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}", - "message.confirm-delete": "Are you sure you want to delete {target}?", - "message.confirm-leave": "Are you sure you want to leave {target}?", - "message.confirm-reset": "Are you sure you want to reset {target}?", - "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", - "message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", - "message.delete-website-warning": "All website data will be deleted.", - "message.error": "Something went wrong.", - "message.event-log": "{event} on {url}", - "message.go-to-settings": "Go to settings", - "message.incorrect-username-password": "Incorrect username and/or password.", - "message.invalid-domain": "Invalid domain. Do not include http/https.", - "message.min-password-length": "Minimum length of {n} characters", - "message.no-data-available": "No data available.", - "message.no-event-data": "No event data is available.", - "message.no-match-password": "Passwords do not match.", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", - "message.page-not-found": "Page not found", - "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", - "message.reset-website-warning": "All statistics for this website will be deleted, but your settings will remain intact.", - "message.saved": "Saved.", - "message.share-url": "Your website stats are publically available at the following URL:", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", - "message.tracking-code": "To track stats for this website, place the following code in the ... section of your HTML.", - "message.user-deleted": "User deleted.", - "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}", - "message.no-results-found": "No results were found.", - "message.no-team-websites": "This team does not have any websites.", - "message.no-websites-configured": "You do not have any websites configured.", - "message.team-websites-info": "Websites can be viewed by anyone on the team.", - "message.new-version-available": "A new version of Umami {version} is available!" + "labels.untitled": "ခေါင်းစဉ်မရှိ", + "labels.value": "တန်ဖိုး", + "message.active-users": "{x} လက်ရှိအသုံးပြုနေသူ {x, plural, one {ယောက်} other {ယောက်}}", + "message.confirm-delete": "{target} ကို ဖျက်ရန် သေချာပါသလား?", + "message.confirm-leave": "{target} ကို ထွက်ရန် သေချာပါသလား?", + "message.confirm-reset": "{target} ကို ဖျက်၍ပြန်စလုပ်ရန် သေချာပါသလား?", + "message.delete-account": "ဤအကောင့်ကိုဖျက်ရန် {confirmation} ကို ရိုက်ထည့်ပေးပါ.", + "message.delete-website": "ဤ ဝက်ဘ်ဆိုဒ်ကိုဖျက်ရန် {confirmation} ကို ရိုက်ထည့်ပေးပါ", + "message.delete-website-warning": "ဝက်ဘ်ဆိုဒ် ဒေတာအကုန် ဖျက်မည်", + "message.error": "မှားယွင်းမှုတစ်ခု ရှိသွားပါသည်", + "message.event-log": "{url} တွင် {event}", + "message.go-to-settings": "ဆက်တင်သို့ သွားရန်", + "message.incorrect-username-password": "အသုံးပြုသူအမည် သို့မဟုတ် စကားဝှက် မှားနေသည်", + "message.invalid-domain": "ဒိုမိန်း မမှန်ပါ http/https. မပါရပါ", + "message.min-password-length": "အနည်းဆုံး {n} character ရှိရမည်", + "message.no-data-available": "ဒေတာ မရှိပါ", + "message.no-event-data": "အဖြစ်အပျက်ဒေတာ မရှိပါ", + "message.no-match-password": "စကားဝှက် မှားနေသည်", + "message.no-teams": "အသင်း မပြုလုပ်ရသေးပါ", + "message.no-users": "အသုံးပြုသူ မရှိသေးပါ", + "message.page-not-found": "ဤစာမျက်နှာသည် မရှိပါ", + "message.reset-website": "ဤ ဝက်ဘ်ဆိုဒ်ဒေတာကိုဖျက်၍ ပြန်စလုပ်ရန် အောက်တွင် {confirmation} ကို ရိုက်ထည့်ပေးပါ", + "message.reset-website-warning": "ဤဝက်ဘ်ဆိုဒ်က စာရင်းအချက်အလက်များကို ဖျက်မည်၊ ဆက်တင်ဒေတာများ မပါပါ", + "message.saved": "မှတ်သားပြီး", + "message.share-url": "သင့်ဝက်ဆိုဒ်ဘ်၏ စာရင်းအချက်အလက်များကို အောက်ပါ URL တွင် ဝင်ရောက်ကြည့်ရှုနိုင်သည်", + "message.team-already-member": "ဤအသင်းတွင် ဝင်ပြီးသားဖြစ်နေသည်", + "message.team-not-found": "အသင်း မရှိပါ", + "message.tracking-code": "ဤဝက်ဘ်ဆိုဒ်၏ ဒေတာကိုကောက်ခံရန် အောက်ပါ code ကို သင်၏ HTML တွင်ထည့်ပါ", + "message.user-deleted": "အသုံးပြုသူ ဖျက်ပြီးပါပြီ", + "message.visitor-log": "{country} မှ {browser} ဖြင့် {os} {device} တွင် ဝင်ရောက်ကြည့်ရှုသူ", + "message.no-results-found": "ရလဒ်မရှိပါ", + "message.no-team-websites": "ဤအသင်းတွင် ဝက်ဘ်ဆိုက်မရှိသေးပါ", + "message.no-websites-configured": "ဝက်ဘ်ဆိုဒ်တစ်ခုမှ မထည့်ရသေးပါ", + "message.team-websites-info": "ဤဝက်ဘ်ဆိုဒ်များကို အသင်းထဲမှ လူတိုင်းဝင်ကြည့်နိုင်သည်", + "message.new-version-available": "အူမာမီ {version} အသစ်ထွက်နေပါပြီ" } diff --git a/lib/lang.js b/lib/lang.js index cf90c458c..caf182644 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -74,6 +74,7 @@ export const languages = { 'lt-LT': { label: 'Lietuvių', dateLocale: lt }, 'mn-MN': { label: 'Монгол', dateLocale: mn }, 'ms-MY': { label: 'Malay', dateLocale: ms }, + 'my-MM': { label: 'မြန်မာဘာသာ', dateLocale: enUS }, 'nl-NL': { label: 'Nederlands', dateLocale: nl }, 'nb-NO': { label: 'Norsk Bokmål', dateLocale: nb }, 'pl-PL': { label: 'Polski', dateLocale: pl }, @@ -93,7 +94,6 @@ export const languages = { 'vi-VN': { label: 'Tiếng Việt', dateLocale: vi }, 'zh-CN': { label: '中文', dateLocale: zhCN }, 'zh-TW': { label: '中文(繁體)', dateLocale: zhTW }, - 'my-MM': { label: 'မြန်မာဘာသာ', dateLocale: enUS }, }; export function getDateLocale(locale) { From 9436efabc054dc84924f79deb4266edf71a52029 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 11 Aug 2023 09:05:56 -0700 Subject: [PATCH 044/357] Insights report filtering. --- .../pages/reports/funnel/FunnelReport.js | 3 +- .../reports/insights/InsightsParameters.js | 2 +- .../pages/reports/insights/InsightsReport.js | 5 +- .../pages/reports/insights/InsightsTable.js | 9 +++- hooks/useFilters.js | 48 +++++++++++-------- lib/clickhouse.ts | 36 ++++++++------ lib/constants.ts | 23 +++++++++ lib/prisma.ts | 44 ++++++++++++----- package.json | 2 +- pages/api/reports/insights.ts | 10 +++- queries/analytics/reports/getInsights.ts | 6 +-- yarn.lock | 8 ++-- 12 files changed, 134 insertions(+), 62 deletions(-) diff --git a/components/pages/reports/funnel/FunnelReport.js b/components/pages/reports/funnel/FunnelReport.js index 7b4d8eced..d2971fa30 100644 --- a/components/pages/reports/funnel/FunnelReport.js +++ b/components/pages/reports/funnel/FunnelReport.js @@ -6,9 +6,10 @@ import ReportHeader from '../ReportHeader'; import ReportMenu from '../ReportMenu'; import ReportBody from '../ReportBody'; import Funnel from 'assets/funnel.svg'; +import { REPORT_TYPES } from 'lib/constants'; const defaultParameters = { - type: 'funnel', + type: REPORT_TYPES.funnel, parameters: { window: 60, urls: [] }, }; diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js index 18eeffc3d..6de4b838e 100644 --- a/components/pages/reports/insights/InsightsParameters.js +++ b/components/pages/reports/insights/InsightsParameters.js @@ -119,7 +119,7 @@ export function InsightsParameters() {
{id === 'fields' && ( <> -
{label}
+
{fieldOptions.find(f => f.name === name)?.label}
)} {id === 'filters' && ( diff --git a/components/pages/reports/insights/InsightsReport.js b/components/pages/reports/insights/InsightsReport.js index 88f12304c..3d855d9e8 100644 --- a/components/pages/reports/insights/InsightsReport.js +++ b/components/pages/reports/insights/InsightsReport.js @@ -5,10 +5,11 @@ import ReportBody from '../ReportBody'; import InsightsParameters from './InsightsParameters'; import InsightsTable from './InsightsTable'; import Lightbulb from 'assets/lightbulb.svg'; +import { REPORT_TYPES } from 'lib/constants'; const defaultParameters = { - type: 'insights', - parameters: { fields: [], filters: [], groups: [] }, + type: REPORT_TYPES.insights, + parameters: { fields: [], filters: [] }, }; export default function InsightsReport({ reportId }) { diff --git a/components/pages/reports/insights/InsightsTable.js b/components/pages/reports/insights/InsightsTable.js index f45490018..d5422c9ef 100644 --- a/components/pages/reports/insights/InsightsTable.js +++ b/components/pages/reports/insights/InsightsTable.js @@ -31,10 +31,15 @@ export function InsightsTable() { ); })} - + {row => row.visitors.toLocaleString()} - + {row => row.views.toLocaleString()} diff --git a/hooks/useFilters.js b/hooks/useFilters.js index 5143fe5b4..089f2ee84 100644 --- a/hooks/useFilters.js +++ b/hooks/useFilters.js @@ -1,32 +1,40 @@ import { useMessages } from 'hooks'; +import { OPERATORS } from 'lib/constants'; export function useFilters() { const { formatMessage, labels } = useMessages(); const filterLabels = { - eq: formatMessage(labels.is), - neq: formatMessage(labels.isNot), - s: formatMessage(labels.isSet), - ns: formatMessage(labels.isNotSet), - c: formatMessage(labels.contains), - dnc: formatMessage(labels.doesNotContain), - t: formatMessage(labels.true), - f: formatMessage(labels.false), - gt: formatMessage(labels.greaterThan), - lt: formatMessage(labels.lessThan), - gte: formatMessage(labels.greaterThanEquals), - lte: formatMessage(labels.lessThanEquals), - be: formatMessage(labels.before), - af: formatMessage(labels.after), + [OPERATORS.equals]: formatMessage(labels.is), + [OPERATORS.notEquals]: formatMessage(labels.isNot), + [OPERATORS.set]: formatMessage(labels.isSet), + [OPERATORS.notSet]: formatMessage(labels.isNotSet), + [OPERATORS.contains]: formatMessage(labels.contains), + [OPERATORS.doesNotContain]: formatMessage(labels.doesNotContain), + [OPERATORS.true]: formatMessage(labels.true), + [OPERATORS.false]: formatMessage(labels.false), + [OPERATORS.greaterThan]: formatMessage(labels.greaterThan), + [OPERATORS.lessThan]: formatMessage(labels.lessThan), + [OPERATORS.greaterThanEquals]: formatMessage(labels.greaterThanEquals), + [OPERATORS.lessThanEquals]: formatMessage(labels.lessThanEquals), + [OPERATORS.before]: formatMessage(labels.before), + [OPERATORS.after]: formatMessage(labels.after), }; const typeFilters = { - string: ['eq', 'neq'], - array: ['c', 'dnc'], - boolean: ['t', 'f'], - number: ['eq', 'neq', 'gt', 'lt', 'gte', 'lte'], - date: ['be', 'af'], - uuid: ['eq'], + string: [OPERATORS.equals, OPERATORS.notEquals], + array: [OPERATORS.contains, OPERATORS.doesNotContain], + boolean: [OPERATORS.true, OPERATORS.false], + number: [ + OPERATORS.equals, + OPERATORS.notEquals, + OPERATORS.greaterThan, + OPERATORS.lessThan, + OPERATORS.greaterThanEquals, + OPERATORS.lessThanEquals, + ], + date: [OPERATORS.before, OPERATORS.after], + uuid: [OPERATORS.equals], }; const getFilters = type => { diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index f7abd94f2..75786850d 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -3,7 +3,7 @@ import dateFormat from 'dateformat'; import debug from 'debug'; import { CLICKHOUSE } from 'lib/db'; import { QueryFilters, QueryOptions } from './types'; -import { FILTER_COLUMNS } from './constants'; +import { FILTER_COLUMNS, OPERATORS } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; @@ -63,17 +63,29 @@ function getDateFormat(date) { return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`; } +function mapFilter(column, operator, name) { + switch (operator) { + case OPERATORS.equals: + return `${column} = {${name}:String}`; + case OPERATORS.notEquals: + return `${column} != {${name}:String}`; + default: + return ''; + } +} + function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) { - const query = Object.keys(filters).reduce((arr, key) => { - const filter = filters[key]; - const column = FILTER_COLUMNS[key] ?? options?.columns?.[key]; + const query = Object.keys(filters).reduce((arr, name) => { + const value = filters[name]; + const operator = value?.filter ?? OPERATORS.equals; + const column = FILTER_COLUMNS[name] ?? options?.columns?.[name]; - if (filter !== undefined && column) { - arr.push(`and ${column} = {${key}:String}`); - } + if (value !== undefined && column) { + arr.push(`and ${mapFilter(column, operator, name)}`); - if (key === 'referrer') { - arr.push('and referrer_domain != {websiteDomain:String}'); + if (name === 'referrer') { + arr.push('and referrer_domain != {websiteDomain:String}'); + } } return arr; @@ -82,11 +94,7 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) return query.join('\n'); } -async function parseFilters( - websiteId: string, - filters: QueryFilters & { [key: string]: any } = {}, - options?: QueryOptions, -) { +async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) { const website = await loadWebsite(websiteId); return { diff --git a/lib/constants.ts b/lib/constants.ts index 887f90a96..8972f81fc 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -76,6 +76,23 @@ export const DATA_TYPE = { array: 5, } as const; +export const OPERATORS = { + equals: 'eq', + notEquals: 'neq', + set: 's', + notSet: 'ns', + contains: 'c', + doesNotContain: 'dnc', + true: 't', + false: 'f', + greaterThan: 'gt', + lessThan: 'lt', + greaterThanEquals: 'gte', + lessThanEquals: 'lte', + before: 'bf', + after: 'af', +} as const; + export const DATA_TYPES = { [DATA_TYPE.string]: 'string', [DATA_TYPE.number]: 'number', @@ -84,6 +101,12 @@ export const DATA_TYPES = { [DATA_TYPE.array]: 'array', }; +export const REPORT_TYPES = { + funnel: 'funnel', + insights: 'insights', + retention: 'retention', +} as const; + export const REPORT_PARAMETERS = { fields: 'fields', filters: 'filters', diff --git a/lib/prisma.ts b/lib/prisma.ts index 753f1ae4a..a49932869 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -1,7 +1,7 @@ import prisma from '@umami/prisma-client'; import moment from 'moment-timezone'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; -import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants'; +import { FILTER_COLUMNS, SESSION_COLUMNS, OPERATORS } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; import { QueryFilters, QueryOptions } from './types'; @@ -67,15 +67,27 @@ function getTimestampIntervalQuery(field: string): string { } } +function mapFilter(column, operator, name) { + switch (operator) { + case OPERATORS.equals: + return `${column} = {{${name}}}`; + case OPERATORS.notEquals: + return `${column} != {{${name}}}`; + default: + return ''; + } +} + function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string { - const query = Object.keys(filters).reduce((arr, key) => { - const filter = filters[key]; - const column = FILTER_COLUMNS[key] ?? options?.columns?.[key]; + const query = Object.keys(filters).reduce((arr, name) => { + const value = filters[name]; + const operator = value?.filter ?? OPERATORS.equals; + const column = FILTER_COLUMNS[name] ?? options?.columns?.[name]; - if (filter !== undefined && column) { - arr.push(`and ${column}={{${key}}}`); + if (value !== undefined && column) { + arr.push(`and ${mapFilter(column, operator, name)}`); - if (key === 'referrer') { + if (name === 'referrer') { arr.push( 'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)', ); @@ -88,11 +100,17 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): return query.join('\n'); } -async function parseFilters( - websiteId, - filters: QueryFilters & { [key: string]: any } = {}, - options: QueryOptions = {}, -) { +function normalizeFilters(filters = {}) { + return Object.keys(filters).reduce((obj, key) => { + const value = filters[key]; + + obj[key] = value?.value ?? value; + + return obj; + }, {}); +} + +async function parseFilters(websiteId, filters: QueryFilters = {}, options: QueryOptions = {}) { const website = await loadWebsite(websiteId); return { @@ -102,7 +120,7 @@ async function parseFilters( : '', filterQuery: getFilterQuery(filters, options), params: { - ...filters, + ...normalizeFilters(filters), websiteId, startDate: maxDate(filters.startDate, website.resetAt), websiteDomain: website.domain, diff --git a/package.json b/package.json index 647cdf418..89dc5e97f 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.91.0", + "react-basics": "^0.92.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/pages/api/reports/insights.ts b/pages/api/reports/insights.ts index decb1f81f..09a07d2fb 100644 --- a/pages/api/reports/insights.ts +++ b/pages/api/reports/insights.ts @@ -16,6 +16,14 @@ export interface InsightsRequestBody { groups: { name: string; type: string }[]; } +function convertFilters(filters) { + return filters.reduce((obj, { name, ...value }) => { + obj[name] = value; + + return obj; + }, {}); +} + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -36,7 +44,7 @@ export default async ( } const data = await getInsights(websiteId, fields, { - ...filters, + ...convertFilters(filters), startDate: new Date(startDate), endDate: new Date(endDate), }); diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index 9793f258c..fa54488b6 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -1,7 +1,7 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; -import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants'; +import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getInsights( @@ -91,7 +91,7 @@ function parseFields(fields) { (arr, field) => { const { name } = field; - return arr.concat(name); + return arr.concat(`${FILTER_COLUMNS[name]} as "${name}"`); }, ['count(*) as views', 'count(distinct website_event.session_id) as visitors'], ); @@ -103,5 +103,5 @@ function parseGroupBy(fields) { if (!fields.length) { return ''; } - return `group by ${fields.map(({ name }) => name).join(',')}`; + return `group by ${fields.map(({ name }) => FILTER_COLUMNS[name]).join(',')}`; } diff --git a/yarn.lock b/yarn.lock index d9224c2a4..115e3cc9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,10 +7557,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.91.0: - version "0.91.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.91.0.tgz#2970529a22a455ec73a1be884eb93a109c9dafc0" - integrity sha512-vP8LYWiFwA+eguMEuHvHct4Jl5R/2GUjWc1tMujDG0CsAAUGhx68tAJr0K3gBrWjmpJrTPVfX8SdBNKSDAjQsw== +react-basics@^0.92.0: + version "0.92.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.92.0.tgz#02bc6e88bdaf189c30cc6cbd8bbb1c9d12cd089b" + integrity sha512-BVUWg5a7R88konA9NedYMBx1hl50d6h/MD7qlKOEO/Cnm8cOC7AYTRKAKhO6kHMWjY4ZpUuvlg0UcF+SJP/uXA== dependencies: classnames "^2.3.1" date-fns "^2.29.3" From cbed961d0126590501ee8e1ebc0e1729a9e98247 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 11 Aug 2023 11:37:01 -0700 Subject: [PATCH 045/357] Fix search results. --- pages/api/me/websites.ts | 2 +- queries/admin/team.ts | 2 +- queries/admin/website.ts | 44 +++++++++++++++++++++------------------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/pages/api/me/websites.ts b/pages/api/me/websites.ts index f9ccbcabd..238d1b6e2 100644 --- a/pages/api/me/websites.ts +++ b/pages/api/me/websites.ts @@ -1,5 +1,5 @@ import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody, WebsiteSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; diff --git a/queries/admin/team.ts b/queries/admin/team.ts index 978382276..3294c029e 100644 --- a/queries/admin/team.ts +++ b/queries/admin/team.ts @@ -104,7 +104,7 @@ export async function getTeams( ...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES['user:username']) && { teamUser: { - every: { + some: { role: ROLES.teamOwner, user: { username: { diff --git a/queries/admin/website.ts b/queries/admin/website.ts index 68f634a6c..721b06620 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -57,31 +57,33 @@ export async function getWebsites( }, }, }), - AND: { - OR: [ - { - ...(userId && { - userId, - }), - }, - { - ...(includeTeams && { - teamWebsite: { - some: { - team: { - teamUser: { - some: { - userId, + AND: [ + { + OR: [ + { + ...(userId && { + userId, + }), + }, + { + ...(includeTeams && { + teamWebsite: { + some: { + team: { + teamUser: { + some: { + userId, + }, }, }, }, }, - }, - }), - }, - ], - }, - ...(filter && filterQuery), + }), + }, + ], + }, + { ...(filter && filterQuery) }, + ], }; const [pageFilters, getParameters] = prisma.getPageFilters({ From 5e64eac396f29b17873c362255487289c2805351 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 11 Aug 2023 13:52:10 -0700 Subject: [PATCH 046/357] Add no reports message. --- components/messages.js | 4 +++ .../pages/websites/WebsiteReportsPage.js | 34 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/components/messages.js b/components/messages.js index 85a46ff5b..f47513e8c 100644 --- a/components/messages.js +++ b/components/messages.js @@ -246,6 +246,10 @@ export const messages = defineMessages({ id: 'message.no-websites-configured', defaultMessage: 'You do not have any websites configured.', }, + noReportsConfigured: { + id: 'message.no-reports-configured', + defaultMessage: 'You do not have any reports configured.', + }, noTeamWebsites: { id: 'message.no-team-websites', defaultMessage: 'This team does not have any websites.', diff --git a/components/pages/websites/WebsiteReportsPage.js b/components/pages/websites/WebsiteReportsPage.js index a1d49d101..beb9bc4f0 100644 --- a/components/pages/websites/WebsiteReportsPage.js +++ b/components/pages/websites/WebsiteReportsPage.js @@ -1,12 +1,13 @@ +import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import Page from 'components/layout/Page'; -import Link from 'next/link'; -import { Button, Icon, Icons, Text, Flexbox } from 'react-basics'; -import { useMessages, useReports } from 'hooks'; import ReportsTable from 'components/pages/reports/ReportsTable'; +import { useMessages, useReports } from 'hooks'; +import Link from 'next/link'; +import { Button, Flexbox, Icon, Icons, Text } from 'react-basics'; import WebsiteHeader from './WebsiteHeader'; export function WebsiteReportsPage({ websiteId }) { - const { formatMessage, labels } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); const { reports, error, @@ -18,6 +19,8 @@ export function WebsiteReportsPage({ websiteId }) { handlePageSizeChange, } = useReports(websiteId); + const hasData = reports && reports.data.length !== 0; + const handleDelete = async id => { await deleteReport(id); }; @@ -35,14 +38,21 @@ export function WebsiteReportsPage({ websiteId }) { - + {hasData && ( + + )} + {!hasData && ( + + {/* {addButton} */} + + )} ); } From b37a1fce634c261fd634183434ab9f3ef4a90dda Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 11 Aug 2023 16:15:11 -0700 Subject: [PATCH 047/357] fix day filter --- queries/analytics/reports/getRetention.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queries/analytics/reports/getRetention.ts b/queries/analytics/reports/getRetention.ts index c34ba068e..9b18df49a 100644 --- a/queries/analytics/reports/getRetention.ts +++ b/queries/analytics/reports/getRetention.ts @@ -69,7 +69,6 @@ async function relationalQuery( from user_activities a join cohort_items c on a.session_id = c.session_id - where a.day_number IN (0,1,2,3,4,5,6,7,14,21,30) group by 1, 2 ) select @@ -81,6 +80,7 @@ async function relationalQuery( from cohort_date c join cohort_size s on c.cohort_date = s.cohort_date + where c.day_number IN (0,1,2,3,4,5,6,7,14,21,30) order by 1, 2`, { websiteId, @@ -144,7 +144,6 @@ async function clickhouseQuery( from user_activities a join cohort_items c on a.session_id = c.session_id - where a.day_number IN (0,1,2,3,4,5,6,7,14,21,30) group by 1, 2 ) select @@ -156,6 +155,7 @@ async function clickhouseQuery( from cohort_date c join cohort_size s on c.cohort_date = s.cohort_date + where c.day_number IN (0,1,2,3,4,5,6,7,14,21,30) order by 1, 2`, { websiteId, From 820ad69d608729a2f17684214014b0261e18bd36 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 12 Aug 2023 20:13:11 -0700 Subject: [PATCH 048/357] Retention report updates. --- assets/magnet.svg | 1 + components/icons.ts | 2 + components/pages/reports/ReportTemplates.js | 5 +- .../pages/reports/ReportTemplates.module.css | 1 - .../reports/retention/RetentionReport.js | 7 +-- .../pages/reports/retention/RetentionTable.js | 52 +++++++++++++++++-- .../retention/RetentionTable.module.css | 28 ++++++++++ 7 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 assets/magnet.svg create mode 100644 components/pages/reports/retention/RetentionTable.module.css diff --git a/assets/magnet.svg b/assets/magnet.svg new file mode 100644 index 000000000..3c64c3ee5 --- /dev/null +++ b/assets/magnet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/icons.ts b/components/icons.ts index e42b15fee..01d7caf5a 100644 --- a/components/icons.ts +++ b/components/icons.ts @@ -11,6 +11,7 @@ import Gear from 'assets/gear.svg'; import Globe from 'assets/globe.svg'; import Lock from 'assets/lock.svg'; import Logo from 'assets/logo.svg'; +import Magnet from 'assets/magnet.svg'; import Moon from 'assets/moon.svg'; import Nodes from 'assets/nodes.svg'; import Overview from 'assets/overview.svg'; @@ -35,6 +36,7 @@ const icons = { Globe, Lock, Logo, + Magnet, Moon, Nodes, Overview, diff --git a/components/pages/reports/ReportTemplates.js b/components/pages/reports/ReportTemplates.js index 1de7de9c7..0f5e710d5 100644 --- a/components/pages/reports/ReportTemplates.js +++ b/components/pages/reports/ReportTemplates.js @@ -4,6 +4,7 @@ import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import Funnel from 'assets/funnel.svg'; import Lightbulb from 'assets/lightbulb.svg'; +import Magnet from 'assets/magnet.svg'; import styles from './ReportTemplates.module.css'; import { useMessages } from 'hooks'; @@ -47,9 +48,9 @@ export function ReportTemplates() { }, { title: formatMessage(labels.retention), - description: 'Track your websites user retention', + description: 'Measure you website stickiness by tracking how often users return.', url: '/reports/retention', - icon: , + icon: , }, ]; diff --git a/components/pages/reports/ReportTemplates.module.css b/components/pages/reports/ReportTemplates.module.css index 331835051..0cdcb8351 100644 --- a/components/pages/reports/ReportTemplates.module.css +++ b/components/pages/reports/ReportTemplates.module.css @@ -2,7 +2,6 @@ display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 20px; - width: 360px; } .report { diff --git a/components/pages/reports/retention/RetentionReport.js b/components/pages/reports/retention/RetentionReport.js index 333496d82..63eea44c7 100644 --- a/components/pages/reports/retention/RetentionReport.js +++ b/components/pages/reports/retention/RetentionReport.js @@ -4,17 +4,18 @@ import Report from '../Report'; import ReportHeader from '../ReportHeader'; import ReportMenu from '../ReportMenu'; import ReportBody from '../ReportBody'; -import Funnel from 'assets/funnel.svg'; +import Magnet from 'assets/magnet.svg'; +import { REPORT_TYPES } from 'lib/constants'; const defaultParameters = { - type: 'retention', + type: REPORT_TYPES.retention, parameters: {}, }; export default function RetentionReport({ reportId }) { return ( - } /> + } /> diff --git a/components/pages/reports/retention/RetentionTable.js b/components/pages/reports/retention/RetentionTable.js index 35d55a640..7d0f25220 100644 --- a/components/pages/reports/retention/RetentionTable.js +++ b/components/pages/reports/retention/RetentionTable.js @@ -1,21 +1,65 @@ import { useContext } from 'react'; import { GridTable, GridColumn } from 'react-basics'; -import { useMessages } from 'hooks'; import { ReportContext } from '../Report'; +import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import styles from './RetentionTable.module.css'; export function RetentionTable() { const { report } = useContext(ReportContext); - const { formatMessage, labels } = useMessages(); + const { data } = report || {}; + + if (!data) { + return ; + } + + const dates = data.reduce((arr, { date }) => { + if (!arr.includes(date)) { + return arr.concat(date); + } + return arr; + }, []); + + const days = Array(14).fill(null); return ( - + <> +
+
+ {days.map((n, i) => ( +
+ Day {i} +
+ ))} +
+ {dates.map((date, i) => { + return ( +
+ {days.map((n, day) => { + return ( +
+ {data.find(row => row.date === date && row.day === day)?.percentage.toFixed(2)} +
+ ); + })} +
+ ); + })} +
+ + + ); +} + +function DataTable({ data }) { + return ( + {row => row.date} {row => row.day} - + {row => row.visitors} diff --git a/components/pages/reports/retention/RetentionTable.module.css b/components/pages/reports/retention/RetentionTable.module.css new file mode 100644 index 000000000..785582a09 --- /dev/null +++ b/components/pages/reports/retention/RetentionTable.module.css @@ -0,0 +1,28 @@ +.table { + display: flex; + flex-direction: column; +} + +.header { + width: 60px; + height: 40px; + text-align: center; + font-size: var(--font-size-sm); +} + +.row { + display: flex; + flex-direction: row; + gap: 1px; + margin-bottom: 1px; +} + +.cell { + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + background: var(--blue100); + border-radius: var(--border-radius); +} From 6d43cb23dd3203a9d39db8172bc432e1f8ea1359 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sun, 13 Aug 2023 16:42:11 -0700 Subject: [PATCH 049/357] Remove case sensitivity on search. --- queries/admin/report.ts | 6 ++++++ queries/admin/team.ts | 3 ++- queries/admin/user.ts | 1 + queries/admin/website.ts | 4 ++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/queries/admin/report.ts b/queries/admin/report.ts index d2523f82f..7ca2f2b29 100644 --- a/queries/admin/report.ts +++ b/queries/admin/report.ts @@ -41,6 +41,7 @@ export async function getReports( filterType === REPORT_FILTER_TYPES.name) && { name: { startsWith: filter, + mode: 'insensitive', }, }), }, @@ -49,6 +50,7 @@ export async function getReports( filterType === REPORT_FILTER_TYPES.description) && { description: { startsWith: filter, + mode: 'insensitive', }, }), }, @@ -57,6 +59,7 @@ export async function getReports( filterType === REPORT_FILTER_TYPES.type) && { type: { startsWith: filter, + mode: 'insensitive', }, }), }, @@ -66,6 +69,7 @@ export async function getReports( user: { username: { startsWith: filter, + mode: 'insensitive', }, }, }), @@ -76,6 +80,7 @@ export async function getReports( website: { name: { startsWith: filter, + mode: 'insensitive', }, }, }), @@ -86,6 +91,7 @@ export async function getReports( website: { domain: { startsWith: filter, + mode: 'insensitive', }, }, }), diff --git a/queries/admin/team.ts b/queries/admin/team.ts index 3294c029e..71ea634a1 100644 --- a/queries/admin/team.ts +++ b/queries/admin/team.ts @@ -97,7 +97,7 @@ export async function getTeams( OR: [ { ...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES.name) && { - name: { startsWith: filter }, + name: { startsWith: filter, mode: 'insensitive' }, }), }, { @@ -109,6 +109,7 @@ export async function getTeams( user: { username: { startsWith: filter, + mode: 'insensitive', }, }, }, diff --git a/queries/admin/user.ts b/queries/admin/user.ts index f4be4751f..3aece6d12 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -57,6 +57,7 @@ export async function getUsers( filterType === USER_FILTER_TYPES.username) && { username: { startsWith: filter, + mode: 'insensitive', }, }), }, diff --git a/queries/admin/website.ts b/queries/admin/website.ts index 721b06620..a55db8140 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -36,13 +36,13 @@ export async function getWebsites( { ...((filterType === WEBSITE_FILTER_TYPES.all || filterType === WEBSITE_FILTER_TYPES.name) && { - name: { startsWith: filter }, + name: { startsWith: filter, mode: 'insensitive' }, }), }, { ...((filterType === WEBSITE_FILTER_TYPES.all || filterType === WEBSITE_FILTER_TYPES.domain) && { - domain: { startsWith: filter }, + domain: { startsWith: filter, mode: 'insensitive' }, }), }, ], From f7eeaa622b39c7a75f18465cd12bf5fbb0d8cd62 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sun, 13 Aug 2023 22:21:49 -0700 Subject: [PATCH 050/357] Add website/reports to top nav. --- components/layout/NavBar.js | 2 + components/messages.js | 3 + components/pages/reports/ReportsPage.js | 36 +++++++- components/pages/reports/ReportsTable.js | 12 +++ .../pages/settings/websites/WebsitesList.js | 9 +- .../pages/settings/websites/WebsitesTable.js | 17 +++- .../pages/websites/WebsiteReportsPage.js | 12 +-- components/pages/websites/WebsitesPage.js | 67 ++++++++++++++ hooks/index.js | 1 + hooks/useReports.js | 6 +- hooks/useWebsiteReports.js | 38 ++++++++ lib/types.ts | 2 + pages/api/reports/index.ts | 10 +- pages/api/users/[id]/websites.ts | 3 +- pages/api/websites/[id]/reports.ts | 38 ++++++++ pages/reports/index.js | 13 +++ pages/websites/index.js | 13 +++ queries/admin/report.ts | 54 ++++++++++- queries/admin/website.ts | 91 ++++++++++++------- 19 files changed, 361 insertions(+), 66 deletions(-) create mode 100644 components/pages/websites/WebsitesPage.js create mode 100644 hooks/useWebsiteReports.js create mode 100644 pages/api/websites/[id]/reports.ts create mode 100644 pages/reports/index.js create mode 100644 pages/websites/index.js diff --git a/components/layout/NavBar.js b/components/layout/NavBar.js index 97eaa46c3..e896b4046 100644 --- a/components/layout/NavBar.js +++ b/components/layout/NavBar.js @@ -18,6 +18,8 @@ export function NavBar() { const links = [ { label: formatMessage(labels.dashboard), url: '/dashboard' }, + { label: formatMessage(labels.websites), url: '/websites' }, + { label: formatMessage(labels.reports), url: '/reports' }, !cloudMode && { label: formatMessage(labels.settings), url: '/settings' }, ].filter(n => n); diff --git a/components/messages.js b/components/messages.js index f47513e8c..c00248107 100644 --- a/components/messages.js +++ b/components/messages.js @@ -21,6 +21,8 @@ export const labels = defineMessages({ details: { id: 'label.details', defaultMessage: 'Details' }, website: { id: 'label.website', defaultMessage: 'Website' }, websites: { id: 'label.websites', defaultMessage: 'Websites' }, + myWebsites: { id: 'label.my-websites', defaultMessage: 'My Websites' }, + teamWebsites: { id: 'label.team-websites', defaultMessage: 'Team Websites' }, created: { id: 'label.created', defaultMessage: 'Created' }, edit: { id: 'label.edit', defaultMessage: 'Edit' }, name: { id: 'label.name', defaultMessage: 'Name' }, @@ -28,6 +30,7 @@ export const labels = defineMessages({ accessCode: { id: 'label.access-code', defaultMessage: 'Access code' }, teamId: { id: 'label.team-id', defaultMessage: 'Team ID' }, team: { id: 'label.team', defaultMessage: 'Team' }, + teamName: { id: 'label.team-name', defaultMessage: 'Team Name' }, regenerate: { id: 'label.regenerate', defaultMessage: 'Regenerate' }, remove: { id: 'label.remove', defaultMessage: 'Remove' }, join: { id: 'label.join', defaultMessage: 'Join' }, diff --git a/components/pages/reports/ReportsPage.js b/components/pages/reports/ReportsPage.js index 470e1b08d..d63fc77fa 100644 --- a/components/pages/reports/ReportsPage.js +++ b/components/pages/reports/ReportsPage.js @@ -1,13 +1,24 @@ +import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; -import Link from 'next/link'; -import { Button, Icon, Icons, Text } from 'react-basics'; import { useMessages, useReports } from 'hooks'; +import Link from 'next/link'; +import { Button, Flexbox, Icon, Icons, Text } from 'react-basics'; import ReportsTable from './ReportsTable'; export function ReportsPage() { - const { formatMessage, labels } = useMessages(); - const { reports, error, isLoading } = useReports(); + const { formatMessage, labels, messages } = useMessages(); + const { + reports, + error, + isLoading, + filter, + handleFilterChange, + handlePageChange, + handlePageSizeChange, + } = useReports(); + + const hasData = (reports && reports?.data.length !== 0) || filter; return ( @@ -21,7 +32,22 @@ export function ReportsPage() { - + + {hasData && ( + + )} + {!hasData && ( + + )} ); } diff --git a/components/pages/reports/ReportsTable.js b/components/pages/reports/ReportsTable.js index 529f53599..e59e4069a 100644 --- a/components/pages/reports/ReportsTable.js +++ b/components/pages/reports/ReportsTable.js @@ -12,14 +12,23 @@ export function ReportsTable({ onFilterChange, onPageChange, onPageSizeChange, + showDomain, }) { const [report, setReport] = useState(null); const { formatMessage, labels } = useMessages(); + const domainColumn = [ + { + name: 'domain', + label: formatMessage(labels.domain), + }, + ]; + const columns = [ { name: 'name', label: formatMessage(labels.name) }, { name: 'description', label: formatMessage(labels.description) }, { name: 'type', label: formatMessage(labels.type) }, + ...(showDomain ? domainColumn : []), { name: 'action', label: ' ' }, ]; @@ -41,6 +50,9 @@ export function ReportsTable({ > {row => { const { id } = row; + if (showDomain) { + row.domain = row.website.domain; + } return ( diff --git a/components/pages/settings/websites/WebsitesList.js b/components/pages/settings/websites/WebsitesList.js index 310b481f7..f99b2d6ec 100644 --- a/components/pages/settings/websites/WebsitesList.js +++ b/components/pages/settings/websites/WebsitesList.js @@ -10,19 +10,21 @@ import useMessages from 'hooks/useMessages'; import { ROLES } from 'lib/constants'; import useApiFilter from 'hooks/useApiFilter'; -export function WebsitesList() { +export function WebsitesList({ showTeam, showHeader = true, includeTeams, onlyTeams, fetch }) { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = useApiFilter(); const { get, useQuery } = useApi(); const { data, isLoading, error, refetch } = useQuery( - ['websites', user?.id, filter, page, pageSize], + ['websites', fetch, user?.id, filter, page, pageSize, includeTeams, onlyTeams], () => get(`/users/${user?.id}/websites`, { filter, page, pageSize, + includeTeams, + onlyTeams, }), { enabled: !!user }, ); @@ -54,10 +56,11 @@ export function WebsitesList() { return ( - {addButton} + {showHeader && {addButton}} {hasData && ( {row => { - const { id } = row; + const { + id, + teamWebsite, + user: { username }, + } = row; + if (showTeam) { + row.teamName = teamWebsite[0]?.team.name; + row.owner = username; + } return ( <> diff --git a/components/pages/websites/WebsiteReportsPage.js b/components/pages/websites/WebsiteReportsPage.js index beb9bc4f0..85a002e69 100644 --- a/components/pages/websites/WebsiteReportsPage.js +++ b/components/pages/websites/WebsiteReportsPage.js @@ -1,7 +1,7 @@ import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import Page from 'components/layout/Page'; import ReportsTable from 'components/pages/reports/ReportsTable'; -import { useMessages, useReports } from 'hooks'; +import { useMessages, useWebsiteReports } from 'hooks'; import Link from 'next/link'; import { Button, Flexbox, Icon, Icons, Text } from 'react-basics'; import WebsiteHeader from './WebsiteHeader'; @@ -17,9 +17,9 @@ export function WebsiteReportsPage({ websiteId }) { handleFilterChange, handlePageChange, handlePageSizeChange, - } = useReports(websiteId); + } = useWebsiteReports(websiteId); - const hasData = reports && reports.data.length !== 0; + const hasData = (reports && reports.data.length !== 0) || filter; const handleDelete = async id => { await deleteReport(id); @@ -48,11 +48,7 @@ export function WebsiteReportsPage({ websiteId }) { filterValue={filter} /> )} - {!hasData && ( - - {/* {addButton} */} - - )} + {!hasData && } ); } diff --git a/components/pages/websites/WebsitesPage.js b/components/pages/websites/WebsitesPage.js new file mode 100644 index 000000000..4fdd025de --- /dev/null +++ b/components/pages/websites/WebsitesPage.js @@ -0,0 +1,67 @@ +import Page from 'components/layout/Page'; +import PageHeader from 'components/layout/PageHeader'; +import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; +import WebsiteList from 'components/pages/settings/websites/WebsitesList'; +import { useMessages } from 'hooks'; +import useUser from 'hooks/useUser'; +import { ROLES } from 'lib/constants'; +import { useState } from 'react'; +import { + Button, + Icon, + Icons, + Item, + Modal, + ModalTrigger, + Tabs, + Text, + useToasts, +} from 'react-basics'; + +export function WebsitesPage() { + const { formatMessage, labels, messages } = useMessages(); + const [tab, setTab] = useState('my-websites'); + const [fetch, setFetch] = useState(1); + const { user } = useUser(); + const { showToast } = useToasts(); + + const handleSave = async () => { + setFetch(fetch + 1); + showToast({ message: formatMessage(messages.saved), variant: 'success' }); + }; + + const addButton = ( + <> + {user.role !== ROLES.viewOnly && ( + + + + {close => } + + + )} + + ); + + return ( + + {addButton} + + {formatMessage(labels.myWebsites)} + {formatMessage(labels.teamWebsites)} + + + {tab === 'my-websites' && } + {tab === 'team-webaites' && ( + + )} + + ); +} + +export default WebsitesPage; diff --git a/hooks/index.js b/hooks/index.js index 004260b0d..2596ba57e 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -20,3 +20,4 @@ export * from './useTheme'; export * from './useTimezone'; export * from './useUser'; export * from './useWebsite'; +export * from './useWebsiteReports'; diff --git a/hooks/useReports.js b/hooks/useReports.js index 57d76492b..932fa6dc4 100644 --- a/hooks/useReports.js +++ b/hooks/useReports.js @@ -2,15 +2,15 @@ import { useState } from 'react'; import useApi from './useApi'; import useApiFilter from 'hooks/useApiFilter'; -export function useReports(websiteId) { +export function useReports() { const [modified, setModified] = useState(Date.now()); const { get, useQuery, del, useMutation } = useApi(); const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = useApiFilter(); const { data, error, isLoading } = useQuery( - ['reports:website', { websiteId, modified, filter, page, pageSize }], - () => get(`/reports`, { websiteId, filter, page, pageSize }), + ['reports', { modified, filter, page, pageSize }], + () => get(`/reports`, { filter, page, pageSize }), ); const deleteReport = id => { diff --git a/hooks/useWebsiteReports.js b/hooks/useWebsiteReports.js new file mode 100644 index 000000000..3b7ec4155 --- /dev/null +++ b/hooks/useWebsiteReports.js @@ -0,0 +1,38 @@ +import { useState } from 'react'; +import useApi from './useApi'; +import useApiFilter from 'hooks/useApiFilter'; + +export function useWebsiteReports(websiteId) { + const [modified, setModified] = useState(Date.now()); + const { get, useQuery, del, useMutation } = useApi(); + const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); + const { data, error, isLoading } = useQuery( + ['reports:website', { websiteId, modified, filter, page, pageSize }], + () => get(`/websites/${websiteId}/reports`, { websiteId, filter, page, pageSize }), + ); + + const deleteReport = id => { + mutate(id, { + onSuccess: () => { + setModified(Date.now()); + }, + }); + }; + + return { + reports: data, + error, + isLoading, + deleteReport, + filter, + page, + pageSize, + handleFilterChange, + handlePageChange, + handlePageSizeChange, + }; +} + +export default useWebsiteReports; diff --git a/lib/types.ts b/lib/types.ts index 5a25169a9..65bef8fb9 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -27,6 +27,7 @@ export interface WebsiteSearchFilter extends SearchFilter { @@ -40,6 +41,7 @@ export interface TeamSearchFilter extends SearchFilter { export interface ReportSearchFilter extends SearchFilter { userId?: string; websiteId?: string; + includeTeams?: boolean; } export interface SearchFilter { diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts index 8c6825f15..db83e6edc 100644 --- a/pages/api/reports/index.ts +++ b/pages/api/reports/index.ts @@ -4,7 +4,7 @@ import { useAuth, useCors } from 'lib/middleware'; import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createReport, getReportsByWebsiteId } from 'queries'; +import { createReport, getReportsByUserId, getReportsByWebsiteId } from 'queries'; export interface ReportsRequestQuery extends SearchFilter {} @@ -26,20 +26,14 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { websiteId } = req.query; - const { user: { id: userId }, } = req.auth; if (req.method === 'GET') { - if (!(websiteId && (await canViewWebsite(req.auth, websiteId)))) { - return unauthorized(res); - } - const { page, filter, pageSize } = req.query; - const data = await getReportsByWebsiteId(websiteId, { + const data = await getReportsByUserId(userId, { page, filter, pageSize: +pageSize || null, diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts index 72d793d1f..0e9231f73 100644 --- a/pages/api/users/[id]/websites.ts +++ b/pages/api/users/[id]/websites.ts @@ -21,7 +21,7 @@ export default async ( await useAuth(req, res); const { user } = req.auth; - const { id: userId, page, filter, pageSize, includeTeams } = req.query; + const { id: userId, page, filter, pageSize, includeTeams, onlyTeams } = req.query; if (req.method === 'GET') { if (!user.isAdmin && user.id !== userId) { @@ -33,6 +33,7 @@ export default async ( filter, pageSize: +pageSize || null, includeTeams, + onlyTeams, }); return ok(res, websites); diff --git a/pages/api/websites/[id]/reports.ts b/pages/api/websites/[id]/reports.ts new file mode 100644 index 000000000..60c6f7140 --- /dev/null +++ b/pages/api/websites/[id]/reports.ts @@ -0,0 +1,38 @@ +import { canViewWebsite } from 'lib/auth'; +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getReportsByWebsiteId } from 'queries'; + +export interface ReportsRequestQuery extends SearchFilter { + id: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + const { id: websiteId } = req.query; + + if (req.method === 'GET') { + if (!(websiteId && (await canViewWebsite(req.auth, websiteId)))) { + return unauthorized(res); + } + + const { page, filter, pageSize } = req.query; + + const data = await getReportsByWebsiteId(websiteId, { + page, + filter, + pageSize: +pageSize || null, + }); + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/pages/reports/index.js b/pages/reports/index.js new file mode 100644 index 000000000..ff3b4e862 --- /dev/null +++ b/pages/reports/index.js @@ -0,0 +1,13 @@ +import AppLayout from 'components/layout/AppLayout'; +import ReportsPage from 'components/pages/reports/ReportsPage'; +import { useMessages } from 'hooks'; + +export default function () { + const { formatMessage, labels } = useMessages(); + + return ( + + + + ); +} diff --git a/pages/websites/index.js b/pages/websites/index.js new file mode 100644 index 000000000..42a327bcc --- /dev/null +++ b/pages/websites/index.js @@ -0,0 +1,13 @@ +import AppLayout from 'components/layout/AppLayout'; +import WebsitesPage from 'components/pages/websites/WebsitesPage'; +import useMessages from 'hooks/useMessages'; + +export default function () { + const { formatMessage, labels } = useMessages(); + + return ( + + + + ); +} diff --git a/queries/admin/report.ts b/queries/admin/report.ts index 7ca2f2b29..3c50c2cb3 100644 --- a/queries/admin/report.ts +++ b/queries/admin/report.ts @@ -28,13 +28,45 @@ export async function deleteReport(reportId: string): Promise { export async function getReports( ReportSearchFilter: ReportSearchFilter, + options?: { include?: Prisma.ReportInclude }, ): Promise> { - const { userId, websiteId, filter, filterType = REPORT_FILTER_TYPES.all } = ReportSearchFilter; + const { + userId, + websiteId, + includeTeams, + filter, + filterType = REPORT_FILTER_TYPES.all, + } = ReportSearchFilter; + const where: Prisma.ReportWhereInput = { ...(userId && { userId: userId }), ...(websiteId && { websiteId: websiteId }), - ...(filter && { - AND: { + AND: [ + { + OR: [ + { + ...(userId && { userId: userId }), + }, + { + ...(includeTeams && { + website: { + teamWebsite: { + some: { + team: { + teamUser: { + some: { + userId, + }, + }, + }, + }, + }, + }, + }), + }, + ], + }, + { OR: [ { ...((filterType === REPORT_FILTER_TYPES.all || @@ -98,7 +130,7 @@ export async function getReports( }, ], }, - }), + ], }; const [pageFilters, getParameters] = prisma.getPageFilters(ReportSearchFilter); @@ -106,6 +138,7 @@ export async function getReports( const reports = await prisma.client.report.findMany({ where, ...pageFilters, + ...(options?.include && { include: options.include }), }); const count = await prisma.client.report.count({ where, @@ -122,7 +155,18 @@ export async function getReportsByUserId( userId: string, filter: SearchFilter, ): Promise> { - return getReports({ userId, ...filter }); + return getReports( + { userId, ...filter }, + { + include: { + website: { + select: { + domain: true, + }, + }, + }, + }, + ); } export async function getReportsByWebsiteId( diff --git a/queries/admin/website.ts b/queries/admin/website.ts index a55db8140..d7b98b45a 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -26,29 +26,11 @@ export async function getWebsites( userId, teamId, includeTeams, + onlyTeams, filter, filterType = WEBSITE_FILTER_TYPES.all, } = WebsiteSearchFilter; - const filterQuery = { - AND: { - OR: [ - { - ...((filterType === WEBSITE_FILTER_TYPES.all || - filterType === WEBSITE_FILTER_TYPES.name) && { - name: { startsWith: filter, mode: 'insensitive' }, - }), - }, - { - ...((filterType === WEBSITE_FILTER_TYPES.all || - filterType === WEBSITE_FILTER_TYPES.domain) && { - domain: { startsWith: filter, mode: 'insensitive' }, - }), - }, - ], - }, - }; - const where: Prisma.WebsiteWhereInput = { ...(teamId && { teamWebsite: { @@ -61,28 +43,53 @@ export async function getWebsites( { OR: [ { - ...(userId && { - userId, - }), + ...(userId && + !onlyTeams && { + userId, + }), }, { - ...(includeTeams && { - teamWebsite: { - some: { - team: { - teamUser: { - some: { - userId, + ...((includeTeams || onlyTeams) && { + AND: [ + { + teamWebsite: { + some: { + team: { + teamUser: { + some: { + userId, + }, + }, }, }, }, }, - }, + { + userId: { + not: userId, + }, + }, + ], + }), + }, + ], + }, + { + OR: [ + { + ...((filterType === WEBSITE_FILTER_TYPES.all || + filterType === WEBSITE_FILTER_TYPES.name) && { + name: { startsWith: filter, mode: 'insensitive' }, + }), + }, + { + ...((filterType === WEBSITE_FILTER_TYPES.all || + filterType === WEBSITE_FILTER_TYPES.domain) && { + domain: { startsWith: filter, mode: 'insensitive' }, }), }, ], }, - { ...(filter && filterQuery) }, ], }; @@ -108,7 +115,27 @@ export async function getWebsitesByUserId( userId: string, filter?: WebsiteSearchFilter, ): Promise> { - return getWebsites({ userId, ...filter }); + return getWebsites( + { userId, ...filter }, + { + include: { + teamWebsite: { + include: { + team: { + select: { + name: true, + }, + }, + }, + }, + user: { + select: { + username: true, + }, + }, + }, + }, + ); } export async function getWebsitesByTeamId( From 96d74783e06ed5bedb79588f404421409392c5d2 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sun, 13 Aug 2023 22:32:25 -0700 Subject: [PATCH 051/357] Edit button states. --- components/pages/reports/ReportsTable.js | 17 ++++++++++++----- queries/admin/report.ts | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/components/pages/reports/ReportsTable.js b/components/pages/reports/ReportsTable.js index e59e4069a..39a35c960 100644 --- a/components/pages/reports/ReportsTable.js +++ b/components/pages/reports/ReportsTable.js @@ -1,9 +1,10 @@ -import { useState } from 'react'; -import { Flexbox, Icon, Icons, Text, Button, Modal } from 'react-basics'; +import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm'; import LinkButton from 'components/common/LinkButton'; import SettingsTable from 'components/common/SettingsTable'; -import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm'; import { useMessages } from 'hooks'; +import useUser from 'hooks/useUser'; +import { useState } from 'react'; +import { Button, Flexbox, Icon, Icons, Modal, Text } from 'react-basics'; export function ReportsTable({ data = [], @@ -16,6 +17,7 @@ export function ReportsTable({ }) { const [report, setReport] = useState(null); const { formatMessage, labels } = useMessages(); + const { user } = useUser(); const domainColumn = [ { @@ -49,14 +51,19 @@ export function ReportsTable({ filterValue={filterValue} > {row => { - const { id } = row; + const { + id, + userId: reportOwnerId, + website: { domain, userId: websiteOwnerId }, + } = row; if (showDomain) { - row.domain = row.website.domain; + row.domain = domain; } return ( {formatMessage(labels.view)} + {!showDomain || user.id === reportOwnerId || user.id === websiteOwnerId} - + {(!showTeam || ownerId === user.id) && ( + + + + )} - - )} - - - - - ); - }} - + return ( + <> + {(!showTeam || ownerId === user.id) && ( + + + + )} + + + + + ); + }} + + )} + {!showTable && } + ); } From 38445fce7a22edbce476985d4beb3ab163bc7a81 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 15 Aug 2023 13:08:18 -0700 Subject: [PATCH 062/357] Fix test console. --- components/pages/console/TestConsole.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/pages/console/TestConsole.js b/components/pages/console/TestConsole.js index 4f167b9a7..060314fd2 100644 --- a/components/pages/console/TestConsole.js +++ b/components/pages/console/TestConsole.js @@ -72,7 +72,7 @@ export function TestConsole() { } const [websiteId] = id || []; - const website = data.find(({ id }) => websiteId === id); + const website = data?.data.find(({ id }) => websiteId === id); return ( From c3e261fc50a78f0d50119aff9d583c8cea766516 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 16 Aug 2023 08:49:22 -0700 Subject: [PATCH 063/357] Resolve issues in event data --- components/pages/event-data/EventDataValueTable.js | 1 + components/pages/websites/WebsiteEventData.js | 6 +++--- lib/types.ts | 9 ++------- pages/api/event-data/events.ts | 5 +++-- pages/api/event-data/fields.ts | 4 ++-- pages/api/event-data/stats.ts | 12 ++++++------ queries/analytics/eventData/getEventDataEvents.ts | 8 ++++---- queries/analytics/eventData/getEventDataFields.ts | 4 ++-- 8 files changed, 23 insertions(+), 26 deletions(-) diff --git a/components/pages/event-data/EventDataValueTable.js b/components/pages/event-data/EventDataValueTable.js index 3688ad094..69ed10a7a 100644 --- a/components/pages/event-data/EventDataValueTable.js +++ b/components/pages/event-data/EventDataValueTable.js @@ -36,6 +36,7 @@ export function EventDataValueTable({ data = [], event }) { {row => DATA_TYPES[row.dataType]} + {({ total }) => total.toLocaleString()} diff --git a/components/pages/websites/WebsiteEventData.js b/components/pages/websites/WebsiteEventData.js index 5e2083557..7f9a68294 100644 --- a/components/pages/websites/WebsiteEventData.js +++ b/components/pages/websites/WebsiteEventData.js @@ -5,18 +5,18 @@ import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetric import { useDateRange, useApi, usePageQuery } from 'hooks'; import styles from './WebsiteEventData.module.css'; -function useData(websiteId, eventName) { +function useData(websiteId, event) { const [dateRange] = useDateRange(websiteId); const { startDate, endDate } = dateRange; const { get, useQuery } = useApi(); const { data, error, isLoading } = useQuery( - ['event-data:events', { websiteId, startDate, endDate, eventName }], + ['event-data:events', { websiteId, startDate, endDate, event }], () => get('/event-data/events', { websiteId, startAt: +startDate, endAt: +endDate, - eventName, + event, }), { enabled: !!(websiteId && startDate && endDate) }, ); diff --git a/lib/types.ts b/lib/types.ts index 65bef8fb9..3f3ac5337 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -126,13 +126,8 @@ export interface WebsiteEventMetric { y: number; } -export interface WebsiteEventDataStats { - fieldName: string; - dataType: number; - total: number; -} - -export interface WebsiteEventDataFields { +export interface WebsiteEventData { + eventName?: string; fieldName: string; dataType: number; fieldValue?: string; diff --git a/pages/api/event-data/events.ts b/pages/api/event-data/events.ts index e83e541bf..9f8f964b3 100644 --- a/pages/api/event-data/events.ts +++ b/pages/api/event-data/events.ts @@ -5,16 +5,17 @@ import { NextApiResponse } from 'next'; import { ok, methodNotAllowed, unauthorized } from 'next-basics'; import { getEventDataEvents } from 'queries'; -export interface EventDataFieldsRequestBody { +export interface EventDataEventsRequestQuery { websiteId: string; dateRange: { startDate: string; endDate: string; }; + event?: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/event-data/fields.ts b/pages/api/event-data/fields.ts index f21bd570f..b6a731336 100644 --- a/pages/api/event-data/fields.ts +++ b/pages/api/event-data/fields.ts @@ -5,7 +5,7 @@ import { NextApiResponse } from 'next'; import { ok, methodNotAllowed, unauthorized } from 'next-basics'; import { getEventDataFields } from 'queries'; -export interface EventDataFieldsRequestBody { +export interface EventDataFieldsRequestQuery { websiteId: string; dateRange: { startDate: string; @@ -15,7 +15,7 @@ export interface EventDataFieldsRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/event-data/stats.ts b/pages/api/event-data/stats.ts index 74f420c49..d1ee396bc 100644 --- a/pages/api/event-data/stats.ts +++ b/pages/api/event-data/stats.ts @@ -5,7 +5,7 @@ import { NextApiResponse } from 'next'; import { ok, methodNotAllowed, unauthorized } from 'next-basics'; import { getEventDataFields } from 'queries'; -export interface EventDataRequestBody { +export interface EventDataStatsRequestQuery { websiteId: string; dateRange: { startDate: string; @@ -15,7 +15,7 @@ export interface EventDataRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); @@ -32,18 +32,18 @@ export default async ( const endDate = new Date(+endAt); const results = await getEventDataFields(websiteId, { startDate, endDate }); - const events = new Set(); + const fields = new Set(); const data = results.reduce( (obj, row) => { - events.add(row.fieldName); + fields.add(row.fieldName); obj.records += Number(row.total); return obj; }, - { fields: results.length, records: 0 }, + { events: results.length, records: 0 }, ); - return ok(res, { ...data, events: events.size }); + return ok(res, { ...data, fields: fields.size }); } return methodNotAllowed(res); diff --git a/queries/analytics/eventData/getEventDataEvents.ts b/queries/analytics/eventData/getEventDataEvents.ts index 250841113..2c8cb0e0d 100644 --- a/queries/analytics/eventData/getEventDataEvents.ts +++ b/queries/analytics/eventData/getEventDataEvents.ts @@ -1,11 +1,11 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventDataFields } from 'lib/types'; +import { QueryFilters, WebsiteEventData } from 'lib/types'; export async function getEventDataEvents( ...args: [websiteId: string, filters: QueryFilters] -): Promise { +): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -24,7 +24,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { website_event.event_name as "eventName", event_data.event_key as "fieldName", event_data.data_type as "dataType", - event_data.string_value as "value", + event_data.string_value as "fieldValue", count(*) as "total" from event_data inner join website_event @@ -71,7 +71,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { event_name as eventName, event_key as fieldName, data_type as dataType, - string_value as value, + string_value as fieldValue, count(*) as total from event_data where website_id = {websiteId:UUID} diff --git a/queries/analytics/eventData/getEventDataFields.ts b/queries/analytics/eventData/getEventDataFields.ts index f5f426e0c..ac32b188f 100644 --- a/queries/analytics/eventData/getEventDataFields.ts +++ b/queries/analytics/eventData/getEventDataFields.ts @@ -1,11 +1,11 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventDataFields } from 'lib/types'; +import { QueryFilters, WebsiteEventData } from 'lib/types'; export async function getEventDataFields( ...args: [websiteId: string, filters: QueryFilters & { field?: string }] -): Promise { +): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), From c548267d91409db46d42894953dd57b4cfbecd48 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 16 Aug 2023 10:50:28 -0700 Subject: [PATCH 064/357] Added month select component. --- components/input/DateFilter.js | 6 +-- components/input/MonthSelect.js | 51 +++++++++++++++++++ components/input/MonthSelect.module.css | 12 +++++ components/layout/SettingsLayout.module.css | 1 + components/pages/realtime/RealtimeLog.js | 4 +- components/pages/reports/BaseParameters.js | 37 +++++++++----- .../reports/retention/RetentionParameters.js | 21 +++++--- .../pages/reports/retention/RetentionTable.js | 29 +++++------ .../retention/RetentionTable.module.css | 26 ++++++++-- lib/charts.js | 14 ++--- lib/clickhouse.ts | 6 +-- lib/date.js | 2 +- lib/prisma.ts | 6 +-- package.json | 2 +- queries/analytics/reports/getRetention.ts | 15 +++--- yarn.lock | 8 +-- 16 files changed, 169 insertions(+), 71 deletions(-) create mode 100644 components/input/MonthSelect.js create mode 100644 components/input/MonthSelect.module.css diff --git a/components/input/DateFilter.js b/components/input/DateFilter.js index 7fc4319de..af4b69dde 100644 --- a/components/input/DateFilter.js +++ b/components/input/DateFilter.js @@ -3,7 +3,7 @@ import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; import { endOfYear, isSameDay } from 'date-fns'; import DatePickerForm from 'components/metrics/DatePickerForm'; import useLocale from 'hooks/useLocale'; -import { dateFormat } from 'lib/date'; +import { formatDate } from 'lib/date'; import Icons from 'components/icons'; import useMessages from 'hooks/useMessages'; @@ -135,8 +135,8 @@ const CustomRange = ({ startDate, endDate, onClick }) => { - {dateFormat(startDate, 'd LLL y', locale)} - {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} + {formatDate(startDate, 'd LLL y', locale)} + {!isSameDay(startDate, endDate) && ` — ${formatDate(endDate, 'd LLL y', locale)}`} ); diff --git a/components/input/MonthSelect.js b/components/input/MonthSelect.js new file mode 100644 index 000000000..bb0544468 --- /dev/null +++ b/components/input/MonthSelect.js @@ -0,0 +1,51 @@ +import { useRef, useState } from 'react'; +import { Text, Icon, CalendarMonthSelect, CalendarYearSelect, Button } from 'react-basics'; +import { startOfMonth, endOfMonth } from 'date-fns'; +import Icons from 'components/icons'; +import { useLocale } from 'hooks'; +import { formatDate } from 'lib/date'; +import { getDateLocale } from 'lib/lang'; +import styles from './MonthSelect.module.css'; + +const MONTH = 'month'; +const YEAR = 'year'; + +export function MonthSelect({ date = new Date(), onChange }) { + const { locale } = useLocale(); + const [select, setSelect] = useState(null); + const month = formatDate(date, 'MMMM', locale); + const year = date.getFullYear(); + const ref = useRef(); + + const handleSelect = value => { + setSelect(state => (state !== value ? value : null)); + }; + + const handleChange = date => { + onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`); + setSelect(null); + }; + + return ( + <> +
+ + +
+ {select === MONTH && ( + + )} + {select === YEAR && ( + + )} + + ); +} + +export default MonthSelect; diff --git a/components/input/MonthSelect.module.css b/components/input/MonthSelect.module.css new file mode 100644 index 000000000..04cf575c3 --- /dev/null +++ b/components/input/MonthSelect.module.css @@ -0,0 +1,12 @@ +.container { + display: flex; + align-items: center; + justify-content: center; +} + +.input { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; +} diff --git a/components/layout/SettingsLayout.module.css b/components/layout/SettingsLayout.module.css index 569b903ba..36d029f02 100644 --- a/components/layout/SettingsLayout.module.css +++ b/components/layout/SettingsLayout.module.css @@ -2,6 +2,7 @@ display: flex; flex-direction: column; padding-top: 40px; + padding-right: 20px; } .content { diff --git a/components/pages/realtime/RealtimeLog.js b/components/pages/realtime/RealtimeLog.js index 744bff00b..6486f707f 100644 --- a/components/pages/realtime/RealtimeLog.js +++ b/components/pages/realtime/RealtimeLog.js @@ -8,7 +8,7 @@ import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; import { BROWSERS } from 'lib/constants'; import { stringToColor } from 'lib/format'; -import { dateFormat } from 'lib/date'; +import { formatDate } from 'lib/date'; import { safeDecodeURI } from 'next-basics'; import Icons from 'components/icons'; import styles from './RealtimeLog.module.css'; @@ -50,7 +50,7 @@ export function RealtimeLog({ data, websiteDomain }) { }, ]; - const getTime = ({ createdAt }) => dateFormat(new Date(createdAt), 'pp', locale); + const getTime = ({ createdAt }) => formatDate(new Date(createdAt), 'pp', locale); const getColor = ({ id, sessionId }) => stringToColor(sessionId || id); diff --git a/components/pages/reports/BaseParameters.js b/components/pages/reports/BaseParameters.js index 394432cfa..76c35a581 100644 --- a/components/pages/reports/BaseParameters.js +++ b/components/pages/reports/BaseParameters.js @@ -6,7 +6,12 @@ import { useContext } from 'react'; import { ReportContext } from './Report'; import { useMessages } from 'hooks'; -export function BaseParameters() { +export function BaseParameters({ + showWebsiteSelect = true, + allowWebsiteSelect = true, + showDateSelect = true, + allowDateSelect = true, +}) { const { report, updateReport } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); @@ -24,17 +29,25 @@ export function BaseParameters() { return ( <> - - - - - - + {showWebsiteSelect && ( + + {allowWebsiteSelect && ( + + )} + + )} + {showDateSelect && ( + + {allowDateSelect && ( + + )} + + )} ); } diff --git a/components/pages/reports/retention/RetentionParameters.js b/components/pages/reports/retention/RetentionParameters.js index f6bde0b16..1eee6bf27 100644 --- a/components/pages/reports/retention/RetentionParameters.js +++ b/components/pages/reports/retention/RetentionParameters.js @@ -1,21 +1,19 @@ import { useContext, useRef } from 'react'; import { useMessages } from 'hooks'; -import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from 'react-basics'; +import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics'; import { ReportContext } from 'components/pages/reports/Report'; +import { MonthSelect } from 'components/input/MonthSelect'; import BaseParameters from '../BaseParameters'; - -const fieldOptions = [ - { name: 'daily', type: 'string' }, - { name: 'weekly', type: 'string' }, -]; +import { parseDateRange } from 'lib/date'; export function RetentionParameters() { - const { report, runReport, isRunning } = useContext(ReportContext); + const { report, runReport, isRunning, updateReport } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); const ref = useRef(null); const { parameters } = report || {}; const { websiteId, dateRange } = parameters || {}; + const { startDate } = dateRange || {}; const queryDisabled = !websiteId || !dateRange; const handleSubmit = (data, e) => { @@ -26,9 +24,16 @@ export function RetentionParameters() { } }; + const handleDateChange = value => { + updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } }); + }; + return (
- + + + + {formatMessage(labels.runQuery)} diff --git a/components/pages/reports/retention/RetentionTable.js b/components/pages/reports/retention/RetentionTable.js index 01a84a012..f7d8c4bb9 100644 --- a/components/pages/reports/retention/RetentionTable.js +++ b/components/pages/reports/retention/RetentionTable.js @@ -1,9 +1,10 @@ import { useContext } from 'react'; import { GridTable, GridColumn } from 'react-basics'; +import classNames from 'classnames'; import { ReportContext } from '../Report'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import { useMessages } from 'hooks'; -import { dateFormat } from 'lib/date'; +import { formatDate } from 'lib/date'; import styles from './RetentionTable.module.css'; export function RetentionTable() { @@ -15,34 +16,32 @@ export function RetentionTable() { return ; } - const dates = data.reduce((arr, { date }) => { - if (!arr.includes(date)) { - return arr.concat(date); + const rows = data.reduce((arr, { date, visitors }) => { + if (!arr.find(a => a.date === date)) { + return arr.concat({ date, visitors }); } return arr; }, []); - const days = Array(32).fill(null); + const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 30]; return ( <>
-
+
{formatMessage(labels.date)}
- {days.map((n, i) => ( -
- {formatMessage(labels.day)} {i} +
{formatMessage(labels.visitors)}
+ {days.map(n => ( +
+ {formatMessage(labels.day)} {n}
))}
- {dates.map((date, i) => { + {rows.map(({ date, visitors }, i) => { return (
-
- {dateFormat(date, 'P')} -
- {date} -
+
{formatDate(`${date} 00:00:00`, 'PP')}
+
{visitors}
{days.map((n, day) => { return (
diff --git a/components/pages/reports/retention/RetentionTable.module.css b/components/pages/reports/retention/RetentionTable.module.css index 0943ffc07..79cbbc5fa 100644 --- a/components/pages/reports/retention/RetentionTable.module.css +++ b/components/pages/reports/retention/RetentionTable.module.css @@ -4,10 +4,7 @@ } .header { - width: 60px; - height: 40px; - text-align: center; - font-size: var(--font-size-sm); + font-weight: 700; } .row { @@ -28,5 +25,24 @@ } .date { - min-width: 200px; + display: flex; + align-items: center; + min-width: 160px; +} + +.visitors { + display: flex; + align-items: center; + min-width: 80px; +} + +.day { + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + text-align: center; + font-size: var(--font-size-sm); + font-weight: 400; } diff --git a/lib/charts.js b/lib/charts.js index 0571a9a9e..ff746cb50 100644 --- a/lib/charts.js +++ b/lib/charts.js @@ -1,5 +1,5 @@ import { StatusLight } from 'react-basics'; -import { dateFormat } from 'lib/date'; +import { formatDate } from 'lib/date'; import { formatLongNumber } from 'lib/format'; export function renderNumberLabels(label) { @@ -12,15 +12,15 @@ export function renderDateLabels(unit, locale) { switch (unit) { case 'minute': - return dateFormat(d, 'h:mm', locale); + return formatDate(d, 'h:mm', locale); case 'hour': - return dateFormat(d, 'p', locale); + return formatDate(d, 'p', locale); case 'day': - return dateFormat(d, 'MMM d', locale); + return formatDate(d, 'MMM d', locale); case 'month': - return dateFormat(d, 'MMM', locale); + return formatDate(d, 'MMM', locale); case 'year': - return dateFormat(d, 'YYY', locale); + return formatDate(d, 'YYY', locale); default: return label; } @@ -50,7 +50,7 @@ export function renderStatusTooltipPopup(unit, locale) { setTooltipPopup( <> -
{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}
+
{formatDate(new Date(dataPoints[0].raw.x), formats[unit], locale)}
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label} diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index 75786850d..aa2f21ed3 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -63,12 +63,12 @@ function getDateFormat(date) { return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`; } -function mapFilter(column, operator, name) { +function mapFilter(column, operator, name, type = 'String') { switch (operator) { case OPERATORS.equals: - return `${column} = {${name}:String}`; + return `${column} = {${name}:${type}`; case OPERATORS.notEquals: - return `${column} != {${name}:String}`; + return `${column} != {${name}:${type}}`; default: return ''; } diff --git a/lib/date.js b/lib/date.js index 8a023822e..49bff8970 100644 --- a/lib/date.js +++ b/lib/date.js @@ -249,7 +249,7 @@ export const customFormats = { }, }; -export function dateFormat(date, str, locale = 'en-US') { +export function formatDate(date, str, locale = 'en-US') { return format( typeof date === 'string' ? new Date(date) : date, customFormats?.[locale]?.[str] || str, diff --git a/lib/prisma.ts b/lib/prisma.ts index efce3f4eb..6a6c17909 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -67,12 +67,12 @@ function getTimestampIntervalQuery(field: string): string { } } -function mapFilter(column, operator, name) { +function mapFilter(column, operator, name, type = 'String') { switch (operator) { case OPERATORS.equals: - return `${column} = {{${name}}}`; + return `${column} = {${name}:${type}`; case OPERATORS.notEquals: - return `${column} != {{${name}}}`; + return `${column} != {${name}:${type}}`; default: return ''; } diff --git a/package.json b/package.json index 89dc5e97f..46ad4d2d4 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.92.0", + "react-basics": "^0.94.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/queries/analytics/reports/getRetention.ts b/queries/analytics/reports/getRetention.ts index ee7e4619b..7473e0428 100644 --- a/queries/analytics/reports/getRetention.ts +++ b/queries/analytics/reports/getRetention.ts @@ -5,9 +5,10 @@ import prisma from 'lib/prisma'; export async function getRetention( ...args: [ websiteId: string, - dateRange: { + filters: { startDate: Date; endDate: Date; + timezone: string; }, ] ) { @@ -19,9 +20,10 @@ export async function getRetention( async function relationalQuery( websiteId: string, - dateRange: { + filters: { startDate: Date; endDate: Date; + timezone: string; }, ): Promise< { @@ -32,9 +34,8 @@ async function relationalQuery( percentage: number; }[] > { - const { startDate, endDate } = dateRange; + const { startDate, endDate, timezone = 'UTC' } = filters; const { getDateQuery, rawQuery } = prisma; - const timezone = 'utc'; const unit = 'day'; return rawQuery( @@ -94,9 +95,10 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - dateRange: { + filters: { startDate: Date; endDate: Date; + timezone: string; }, ): Promise< { @@ -107,9 +109,8 @@ async function clickhouseQuery( percentage: number; }[] > { - const { startDate, endDate } = dateRange; + const { startDate, endDate, timezone = 'UTC' } = filters; const { getDateQuery, getDateStringQuery, rawQuery } = clickhouse; - const timezone = 'UTC'; const unit = 'day'; return rawQuery( diff --git a/yarn.lock b/yarn.lock index 115e3cc9c..e67cc4130 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,10 +7557,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.92.0: - version "0.92.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.92.0.tgz#02bc6e88bdaf189c30cc6cbd8bbb1c9d12cd089b" - integrity sha512-BVUWg5a7R88konA9NedYMBx1hl50d6h/MD7qlKOEO/Cnm8cOC7AYTRKAKhO6kHMWjY4ZpUuvlg0UcF+SJP/uXA== +react-basics@^0.94.0: + version "0.94.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.94.0.tgz#c15698148b959f40c6b451088f36f5735eb82815" + integrity sha512-OlUHWrGRctRGEm+yL9iWSC9HRnxZhlm3enP2iCKytVmt7LvaPtsK4RtZ27qp4irNvuzg79aqF+h5IFnG+Vi7WA== dependencies: classnames "^2.3.1" date-fns "^2.29.3" From b69a6bb6136d9d3b011662d020c75f6102274688 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 16 Aug 2023 10:52:01 -0700 Subject: [PATCH 065/357] stale-issues add ordering to look at old issues first --- .github/workflows/stale-issues.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 52c0d432f..24711fba7 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -19,5 +19,6 @@ jobs: close-issue-message: 'This issue was closed because it has been inactive for 7 days since being marked as stale.' days-before-pr-stale: -1 days-before-pr-close: -1 - operations-per-run: 500 + operations-per-run: 200 + ascending: true repo-token: ${{ secrets.GITHUB_TOKEN }} From 3601cb63a550b90b5ec377dcea9dacf321951672 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 16 Aug 2023 10:58:07 -0700 Subject: [PATCH 066/357] Change day range for retention. --- queries/analytics/reports/getRetention.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queries/analytics/reports/getRetention.ts b/queries/analytics/reports/getRetention.ts index 6ab49470e..3c384b6e5 100644 --- a/queries/analytics/reports/getRetention.ts +++ b/queries/analytics/reports/getRetention.ts @@ -86,7 +86,7 @@ async function relationalQuery( from cohort_date c join cohort_size s on c.cohort_date = s.cohort_date - where c.day_number IN (0,1,2,3,4,5,6,7,14,21,30) + where c.day_number <= 31 order by 1, 2`, { websiteId, @@ -165,7 +165,7 @@ async function clickhouseQuery( from cohort_date c join cohort_size s on c.cohort_date = s.cohort_date - where c.day_number IN (0,1,2,3,4,5,6,7,14,21,30) + where c.day_number <= 31 order by 1, 2`, { websiteId, From 7df142b02b31afee1576ce55ae887ea6aa9b8695 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 16 Aug 2023 12:12:45 -0700 Subject: [PATCH 067/357] reorder getfunnel where to match index --- queries/analytics/reports/getFunnel.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/queries/analytics/reports/getFunnel.ts b/queries/analytics/reports/getFunnel.ts index 3c5c65e09..1bbbc8782 100644 --- a/queries/analytics/reports/getFunnel.ts +++ b/queries/analytics/reports/getFunnel.ts @@ -57,12 +57,14 @@ async function relationalQuery( from level${i} l join website_event we on l.session_id = we.session_id - where we.created_at between l.created_at - and ${getAddMinutesQuery(`l.created_at `, windowMinutes)} + where we.website_id = {{websiteId::uuid}} + and we.created_at between l.created_at and ${getAddMinutesQuery( + `l.created_at `, + windowMinutes, + )} and we.referrer_path = {{${i - 1}}} and we.url_path = {{${i}}} and we.created_at <= {{endDate}} - and we.website_id = {{websiteId::uuid}} )`; } From d6c8f3aa18bdcfe6a46c2397819ddb45424cb03c Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 16 Aug 2023 13:47:43 -0700 Subject: [PATCH 068/357] fix mapfilter / rawquery for relational --- lib/clickhouse.ts | 2 +- lib/prisma.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index aa2f21ed3..8ce7bc980 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -66,7 +66,7 @@ function getDateFormat(date) { function mapFilter(column, operator, name, type = 'String') { switch (operator) { case OPERATORS.equals: - return `${column} = {${name}:${type}`; + return `${column} = {${name}:${type}}`; case OPERATORS.notEquals: return `${column} != {${name}:${type}}`; default: diff --git a/lib/prisma.ts b/lib/prisma.ts index cebd71932..12bafa513 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -92,12 +92,12 @@ function getTimestampIntervalQuery(field: string): string { } } -function mapFilter(column, operator, name, type = 'String') { +function mapFilter(column, operator, name, type = 'varchar') { switch (operator) { case OPERATORS.equals: - return `${column} = {${name}:${type}`; + return `${column} = {{${name}::${type}}}`; case OPERATORS.notEquals: - return `${column} != {${name}:${type}}`; + return `${column} != {{${name}::${type}}}`; default: return ''; } @@ -161,7 +161,7 @@ async function rawQuery(sql: string, data: object): Promise { return Promise.reject(new Error('Unknown database.')); } - const query = sql?.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*}}/g, (...args) => { + const query = sql?.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*\}\}/g, (...args) => { const [, name, type] = args; params.push(data[name]); From 0dfa6c120cade8669c87637f5975426a003c809b Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 16 Aug 2023 13:56:12 -0700 Subject: [PATCH 069/357] Remove add buttons for cloud mode. --- components/pages/reports/ReportsPage.js | 24 +++++++++---------- .../pages/settings/websites/WebsitesList.js | 10 ++++---- components/pages/websites/WebsitesPage.js | 4 +++- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/components/pages/reports/ReportsPage.js b/components/pages/reports/ReportsPage.js index 8fc569174..29c779758 100644 --- a/components/pages/reports/ReportsPage.js +++ b/components/pages/reports/ReportsPage.js @@ -3,11 +3,13 @@ import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import { useMessages, useReports } from 'hooks'; import Link from 'next/link'; +import useConfig from 'hooks/useConfig'; import { Button, Icon, Icons, Text } from 'react-basics'; import ReportsTable from './ReportsTable'; export function ReportsPage() { const { formatMessage, labels, messages } = useMessages(); + const { cloudMode } = useConfig(); const { reports, error, @@ -21,21 +23,19 @@ export function ReportsPage() { const hasData = (reports && reports?.data.length !== 0) || filter; - const handleDelete = async id => { - await deleteReport(id); - }; - return ( - - - + {!cloudMode && ( + + + + )} {hasData && ( diff --git a/components/pages/settings/websites/WebsitesList.js b/components/pages/settings/websites/WebsitesList.js index 799b032bf..538fc61ad 100644 --- a/components/pages/settings/websites/WebsitesList.js +++ b/components/pages/settings/websites/WebsitesList.js @@ -1,18 +1,18 @@ -import { Button, Icon, Text, Modal, ModalTrigger, useToasts, Icons } from 'react-basics'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; import useApi from 'hooks/useApi'; -import useUser from 'hooks/useUser'; -import useMessages from 'hooks/useMessages'; -import { ROLES } from 'lib/constants'; import useApiFilter from 'hooks/useApiFilter'; +import useMessages from 'hooks/useMessages'; +import useUser from 'hooks/useUser'; +import { ROLES } from 'lib/constants'; +import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; export function WebsitesList({ showTeam, showHeader = true, includeTeams, onlyTeams, fetch }) { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = useApiFilter(); const { get, useQuery } = useApi(); diff --git a/components/pages/websites/WebsitesPage.js b/components/pages/websites/WebsitesPage.js index 4fdd025de..4a2207dac 100644 --- a/components/pages/websites/WebsitesPage.js +++ b/components/pages/websites/WebsitesPage.js @@ -4,6 +4,7 @@ import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; import WebsiteList from 'components/pages/settings/websites/WebsitesList'; import { useMessages } from 'hooks'; import useUser from 'hooks/useUser'; +import useConfig from 'hooks/useConfig'; import { ROLES } from 'lib/constants'; import { useState } from 'react'; import { @@ -23,6 +24,7 @@ export function WebsitesPage() { const [tab, setTab] = useState('my-websites'); const [fetch, setFetch] = useState(1); const { user } = useUser(); + const { cloudMode } = useConfig(); const { showToast } = useToasts(); const handleSave = async () => { @@ -50,7 +52,7 @@ export function WebsitesPage() { return ( - {addButton} + {!cloudMode && addButton} {formatMessage(labels.myWebsites)} {formatMessage(labels.teamWebsites)} From 146650586b3cbd4d484e321c15d45c7c4baa0109 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 16 Aug 2023 13:56:41 -0700 Subject: [PATCH 070/357] add normalize filters to clickhouse --- lib/clickhouse.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index 8ce7bc980..bc10a6d4e 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -94,13 +94,23 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) return query.join('\n'); } +function normalizeFilters(filters = {}) { + return Object.keys(filters).reduce((obj, key) => { + const value = filters[key]; + + obj[key] = value?.value ?? value; + + return obj; + }, {}); +} + async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) { const website = await loadWebsite(websiteId); return { filterQuery: getFilterQuery(filters, options), params: { - ...filters, + ...normalizeFilters(filters), websiteId, startDate: maxDate(filters.startDate, website.resetAt), websiteDomain: website.domain, From 6dba68c823df80302fc261b3a5d854a660d7cade Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 16 Aug 2023 14:33:55 -0700 Subject: [PATCH 071/357] add metric performance indexes --- .../03_metric_performance_index/migration.sql | 50 +++++++++++++++++++ db/mysql/schema.prisma | 17 +++++++ .../03_metric_performance_index/migration.sql | 50 +++++++++++++++++++ db/postgresql/schema.prisma | 17 +++++++ 4 files changed, 134 insertions(+) create mode 100644 db/mysql/migrations/03_metric_performance_index/migration.sql create mode 100644 db/postgresql/migrations/03_metric_performance_index/migration.sql diff --git a/db/mysql/migrations/03_metric_performance_index/migration.sql b/db/mysql/migrations/03_metric_performance_index/migration.sql new file mode 100644 index 000000000..646813647 --- /dev/null +++ b/db/mysql/migrations/03_metric_performance_index/migration.sql @@ -0,0 +1,50 @@ +-- CreateIndex +CREATE INDEX `event_data_website_id_created_at_idx` ON `event_data`(`website_id`, `created_at`); + +-- CreateIndex +CREATE INDEX `event_data_website_id_created_at_event_key_idx` ON `event_data`(`website_id`, `created_at`, `event_key`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_idx` ON `session`(`website_id`, `created_at`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_hostname_idx` ON `session`(`website_id`, `created_at`, `hostname`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_browser_idx` ON `session`(`website_id`, `created_at`, `browser`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_os_idx` ON `session`(`website_id`, `created_at`, `os`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_device_idx` ON `session`(`website_id`, `created_at`, `device`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_screen_idx` ON `session`(`website_id`, `created_at`, `screen`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_language_idx` ON `session`(`website_id`, `created_at`, `language`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_country_idx` ON `session`(`website_id`, `created_at`, `country`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_subdivision1_idx` ON `session`(`website_id`, `created_at`, `subdivision1`); + +-- CreateIndex +CREATE INDEX `session_website_id_created_at_city_idx` ON `session`(`website_id`, `created_at`, `city`); + +-- CreateIndex +CREATE INDEX `website_event_website_id_created_at_url_path_idx` ON `website_event`(`website_id`, `created_at`, `url_path`); + +-- CreateIndex +CREATE INDEX `website_event_website_id_created_at_url_query_idx` ON `website_event`(`website_id`, `created_at`, `url_query`); + +-- CreateIndex +CREATE INDEX `website_event_website_id_created_at_referrer_domain_idx` ON `website_event`(`website_id`, `created_at`, `referrer_domain`); + +-- CreateIndex +CREATE INDEX `website_event_website_id_created_at_page_title_idx` ON `website_event`(`website_id`, `created_at`, `page_title`); + +-- CreateIndex +CREATE INDEX `website_event_website_id_created_at_event_name_idx` ON `website_event`(`website_id`, `created_at`, `event_name`); diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index a25405dff..38bb91f4b 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -44,6 +44,16 @@ model Session { @@index([createdAt]) @@index([websiteId]) + @@index([websiteId, createdAt]) + @@index([websiteId, createdAt, hostname]) + @@index([websiteId, createdAt, browser]) + @@index([websiteId, createdAt, os]) + @@index([websiteId, createdAt, device]) + @@index([websiteId, createdAt, screen]) + @@index([websiteId, createdAt, language]) + @@index([websiteId, createdAt, country]) + @@index([websiteId, createdAt, subdivision1]) + @@index([websiteId, createdAt, city]) @@map("session") } @@ -91,6 +101,11 @@ model WebsiteEvent { @@index([sessionId]) @@index([websiteId]) @@index([websiteId, createdAt]) + @@index([websiteId, createdAt, urlPath]) + @@index([websiteId, createdAt, urlQuery]) + @@index([websiteId, createdAt, referrerDomain]) + @@index([websiteId, createdAt, pageTitle]) + @@index([websiteId, createdAt, eventName]) @@index([websiteId, sessionId, createdAt]) @@map("website_event") } @@ -113,6 +128,8 @@ model EventData { @@index([websiteId]) @@index([websiteEventId]) @@index([websiteId, websiteEventId, createdAt]) + @@index([websiteId, createdAt]) + @@index([websiteId, createdAt, eventKey]) @@map("event_data") } diff --git a/db/postgresql/migrations/03_metric_performance_index/migration.sql b/db/postgresql/migrations/03_metric_performance_index/migration.sql new file mode 100644 index 000000000..5db7aa50c --- /dev/null +++ b/db/postgresql/migrations/03_metric_performance_index/migration.sql @@ -0,0 +1,50 @@ +-- CreateIndex +CREATE INDEX "event_data_website_id_created_at_idx" ON "event_data"("website_id", "created_at"); + +-- CreateIndex +CREATE INDEX "event_data_website_id_created_at_event_key_idx" ON "event_data"("website_id", "created_at", "event_key"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_idx" ON "session"("website_id", "created_at"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_hostname_idx" ON "session"("website_id", "created_at", "hostname"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_browser_idx" ON "session"("website_id", "created_at", "browser"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_os_idx" ON "session"("website_id", "created_at", "os"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_device_idx" ON "session"("website_id", "created_at", "device"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_screen_idx" ON "session"("website_id", "created_at", "screen"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_language_idx" ON "session"("website_id", "created_at", "language"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_country_idx" ON "session"("website_id", "created_at", "country"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_subdivision1_idx" ON "session"("website_id", "created_at", "subdivision1"); + +-- CreateIndex +CREATE INDEX "session_website_id_created_at_city_idx" ON "session"("website_id", "created_at", "city"); + +-- CreateIndex +CREATE INDEX "website_event_website_id_created_at_url_path_idx" ON "website_event"("website_id", "created_at", "url_path"); + +-- CreateIndex +CREATE INDEX "website_event_website_id_created_at_url_query_idx" ON "website_event"("website_id", "created_at", "url_query"); + +-- CreateIndex +CREATE INDEX "website_event_website_id_created_at_referrer_domain_idx" ON "website_event"("website_id", "created_at", "referrer_domain"); + +-- CreateIndex +CREATE INDEX "website_event_website_id_created_at_page_title_idx" ON "website_event"("website_id", "created_at", "page_title"); + +-- CreateIndex +CREATE INDEX "website_event_website_id_created_at_event_name_idx" ON "website_event"("website_id", "created_at", "event_name"); diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 5753c6ef7..d7a70ab05 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -44,6 +44,16 @@ model Session { @@index([createdAt]) @@index([websiteId]) + @@index([websiteId, createdAt]) + @@index([websiteId, createdAt, hostname]) + @@index([websiteId, createdAt, browser]) + @@index([websiteId, createdAt, os]) + @@index([websiteId, createdAt, device]) + @@index([websiteId, createdAt, screen]) + @@index([websiteId, createdAt, language]) + @@index([websiteId, createdAt, country]) + @@index([websiteId, createdAt, subdivision1]) + @@index([websiteId, createdAt, city]) @@map("session") } @@ -91,6 +101,11 @@ model WebsiteEvent { @@index([sessionId]) @@index([websiteId]) @@index([websiteId, createdAt]) + @@index([websiteId, createdAt, urlPath]) + @@index([websiteId, createdAt, urlQuery]) + @@index([websiteId, createdAt, referrerDomain]) + @@index([websiteId, createdAt, pageTitle]) + @@index([websiteId, createdAt, eventName]) @@index([websiteId, sessionId, createdAt]) @@map("website_event") } @@ -112,6 +127,8 @@ model EventData { @@index([createdAt]) @@index([websiteId]) @@index([websiteEventId]) + @@index([websiteId, createdAt]) + @@index([websiteId, createdAt, eventKey]) @@map("event_data") } From 9b8fa08d8235729cb23c901d1cf0f4a301f1d75d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 17 Aug 2023 01:33:10 -0700 Subject: [PATCH 072/357] Fixed ugly navbar. --- components/layout/AppLayout.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/components/layout/AppLayout.module.css b/components/layout/AppLayout.module.css index 0afd11f9f..be51f83cf 100644 --- a/components/layout/AppLayout.module.css +++ b/components/layout/AppLayout.module.css @@ -10,6 +10,7 @@ width: 100vw; grid-column: 1; grid-row: 1 / 2; + z-index: 1; } .body { From 2c8996b68f8c7ea42e4c5d006249337957e15427 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 17 Aug 2023 03:21:20 -0700 Subject: [PATCH 073/357] Retention report UI updates. --- components/input/MonthSelect.js | 65 ++++++++++++------- components/input/MonthSelect.module.css | 10 +++ components/pages/reports/PopupForm.js | 26 ++------ components/pages/reports/PopupForm.module.css | 2 +- .../reports/insights/InsightsParameters.js | 8 +-- .../insights/InsightsParameters.module.css | 5 ++ .../reports/retention/RetentionParameters.js | 2 +- .../reports/retention/RetentionReport.js | 8 ++- .../pages/reports/retention/RetentionTable.js | 61 ++++++++--------- .../retention/RetentionTable.module.css | 6 +- package.json | 2 +- yarn.lock | 8 +-- 12 files changed, 110 insertions(+), 93 deletions(-) diff --git a/components/input/MonthSelect.js b/components/input/MonthSelect.js index bb0544468..a20890ad9 100644 --- a/components/input/MonthSelect.js +++ b/components/input/MonthSelect.js @@ -1,5 +1,13 @@ -import { useRef, useState } from 'react'; -import { Text, Icon, CalendarMonthSelect, CalendarYearSelect, Button } from 'react-basics'; +import { useRef } from 'react'; +import { + Text, + Icon, + CalendarMonthSelect, + CalendarYearSelect, + Button, + PopupTrigger, + Popup, +} from 'react-basics'; import { startOfMonth, endOfMonth } from 'date-fns'; import Icons from 'components/icons'; import { useLocale } from 'hooks'; @@ -7,43 +15,50 @@ import { formatDate } from 'lib/date'; import { getDateLocale } from 'lib/lang'; import styles from './MonthSelect.module.css'; -const MONTH = 'month'; -const YEAR = 'year'; - export function MonthSelect({ date = new Date(), onChange }) { const { locale } = useLocale(); - const [select, setSelect] = useState(null); const month = formatDate(date, 'MMMM', locale); const year = date.getFullYear(); const ref = useRef(); - const handleSelect = value => { - setSelect(state => (state !== value ? value : null)); - }; - const handleChange = date => { onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`); - setSelect(null); }; return ( <>
- - + + + + + + + + + + + +
- {select === MONTH && ( - - )} - {select === YEAR && ( - - )} ); } diff --git a/components/input/MonthSelect.module.css b/components/input/MonthSelect.module.css index 04cf575c3..3b13bcc18 100644 --- a/components/input/MonthSelect.module.css +++ b/components/input/MonthSelect.module.css @@ -2,6 +2,8 @@ display: flex; align-items: center; justify-content: center; + border: 1px solid var(--base400); + border-radius: var(--border-radius); } .input { @@ -10,3 +12,11 @@ gap: 10px; cursor: pointer; } + +.popup { + border: 1px solid var(--base400); + background: var(--base50); + border-radius: var(--border-radius); + padding: 20px; + margin-top: 5px; +} diff --git a/components/pages/reports/PopupForm.js b/components/pages/reports/PopupForm.js index 0f0ead369..0e825a267 100644 --- a/components/pages/reports/PopupForm.js +++ b/components/pages/reports/PopupForm.js @@ -1,29 +1,11 @@ -import { createPortal } from 'react-dom'; -import { useDocumentClick, useKeyDown } from 'react-basics'; import classNames from 'classnames'; import styles from './PopupForm.module.css'; -export function PopupForm({ element, className, children, onClose }) { - const { right, top } = element.getBoundingClientRect(); - const style = { position: 'absolute', left: right, top }; - - useKeyDown('Escape', onClose); - - useDocumentClick(e => { - if (e.target !== element && !element?.parentElement?.contains(e.target)) { - onClose(); - } - }); - - const handleClick = e => { - e.stopPropagation(); - }; - - return createPortal( -
+export function PopupForm({ className, style, children }) { + return ( +
{children} -
, - document.body, +
); } diff --git a/components/pages/reports/PopupForm.module.css b/components/pages/reports/PopupForm.module.css index 4daf199ac..94d98b38c 100644 --- a/components/pages/reports/PopupForm.module.css +++ b/components/pages/reports/PopupForm.module.css @@ -3,8 +3,8 @@ background: var(--base50); min-width: 300px; padding: 20px; - margin-left: 30px; border: 1px solid var(--base400); border-radius: var(--border-radius); box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1); + z-index: 1000; } diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js index 6de4b838e..9f3571e6d 100644 --- a/components/pages/reports/insights/InsightsParameters.js +++ b/components/pages/reports/insights/InsightsParameters.js @@ -80,10 +80,10 @@ export function InsightsParameters() { - - {(close, element) => { + + {close => { return ( - + {id === 'fields' && ( }> handleRemove(id, index)}> - {({ name, filter, value, label }) => { + {({ name, filter, value }) => { return (
{id === 'fields' && ( diff --git a/components/pages/reports/insights/InsightsParameters.module.css b/components/pages/reports/insights/InsightsParameters.module.css index 06b624149..c84f8a9e9 100644 --- a/components/pages/reports/insights/InsightsParameters.module.css +++ b/components/pages/reports/insights/InsightsParameters.module.css @@ -10,3 +10,8 @@ .op { font-weight: bold; } + +.popup { + margin-top: -10px; + margin-left: 30px; +} diff --git a/components/pages/reports/retention/RetentionParameters.js b/components/pages/reports/retention/RetentionParameters.js index 1eee6bf27..d98608ae7 100644 --- a/components/pages/reports/retention/RetentionParameters.js +++ b/components/pages/reports/retention/RetentionParameters.js @@ -31,7 +31,7 @@ export function RetentionParameters() { return ( - + diff --git a/components/pages/reports/retention/RetentionReport.js b/components/pages/reports/retention/RetentionReport.js index 63eea44c7..a9aaeb3e7 100644 --- a/components/pages/reports/retention/RetentionReport.js +++ b/components/pages/reports/retention/RetentionReport.js @@ -6,10 +6,16 @@ import ReportMenu from '../ReportMenu'; import ReportBody from '../ReportBody'; import Magnet from 'assets/magnet.svg'; import { REPORT_TYPES } from 'lib/constants'; +import { parseDateRange } from 'lib/date'; +import { endOfMonth, startOfMonth } from 'date-fns'; const defaultParameters = { type: REPORT_TYPES.retention, - parameters: {}, + parameters: { + dateRange: parseDateRange( + `range:${startOfMonth(new Date()).getTime()}:${endOfMonth(new Date()).getTime()}`, + ), + }, }; export default function RetentionReport({ reportId }) { diff --git a/components/pages/reports/retention/RetentionTable.js b/components/pages/reports/retention/RetentionTable.js index f7d8c4bb9..df0b0f99f 100644 --- a/components/pages/reports/retention/RetentionTable.js +++ b/components/pages/reports/retention/RetentionTable.js @@ -1,5 +1,4 @@ import { useContext } from 'react'; -import { GridTable, GridColumn } from 'react-basics'; import classNames from 'classnames'; import { ReportContext } from '../Report'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; @@ -16,14 +15,26 @@ export function RetentionTable() { return ; } - const rows = data.reduce((arr, { date, visitors }) => { - if (!arr.find(a => a.date === date)) { - return arr.concat({ date, visitors }); + const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28]; + + const rows = data.reduce((arr, row) => { + const { date, visitors, day } = row; + if (day === 0) { + return arr.concat({ + date, + visitors, + records: days + .reduce((arr, day) => { + arr[day] = data.find(x => x.date === date && x.day === day); + return arr; + }, []) + .filter(n => n), + }); } return arr; }, []); - const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 30]; + const totalDays = rows.length; return ( <> @@ -37,15 +48,22 @@ export function RetentionTable() {
))}
- {rows.map(({ date, visitors }, i) => { + {rows.map(({ date, visitors, records }, rowIndex) => { return ( -
+
{formatDate(`${date} 00:00:00`, 'PP')}
{visitors}
- {days.map((n, day) => { + {days.map(day => { + if (totalDays - rowIndex < day) { + return null; + } + const percentage = records[day]?.percentage; return ( -
- {data.find(row => row.date === date && row.day === day)?.percentage.toFixed(2)} +
+ {percentage ? `${percentage.toFixed(2)}%` : ''}
); })} @@ -53,31 +71,8 @@ export function RetentionTable() { ); })}
- ); } -function DataTable({ data }) { - return ( - - - {row => row.date} - - - {row => row.day} - - - {row => row.visitors} - - - {row => row.returnVisitors} - - - {row => row.percentage} - - - ); -} - export default RetentionTable; diff --git a/components/pages/reports/retention/RetentionTable.module.css b/components/pages/reports/retention/RetentionTable.module.css index 79cbbc5fa..bfe3ac1c4 100644 --- a/components/pages/reports/retention/RetentionTable.module.css +++ b/components/pages/reports/retention/RetentionTable.module.css @@ -20,7 +20,7 @@ justify-content: center; width: 60px; height: 60px; - background: var(--blue100); + background: var(--blue200); border-radius: var(--border-radius); } @@ -46,3 +46,7 @@ font-size: var(--font-size-sm); font-weight: 400; } + +.empty { + background: var(--blue100); +} diff --git a/package.json b/package.json index 46ad4d2d4..9d2de0936 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.94.0", + "react-basics": "^0.96.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/yarn.lock b/yarn.lock index e67cc4130..350e483fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,10 +7557,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.94.0: - version "0.94.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.94.0.tgz#c15698148b959f40c6b451088f36f5735eb82815" - integrity sha512-OlUHWrGRctRGEm+yL9iWSC9HRnxZhlm3enP2iCKytVmt7LvaPtsK4RtZ27qp4irNvuzg79aqF+h5IFnG+Vi7WA== +react-basics@^0.96.0: + version "0.96.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.96.0.tgz#e5e72201abdccdda94b952ef605163ca11772d8f" + integrity sha512-WNAxP+0xBtUNgEXrL8aW6UQMmD6WoX9My0VW6uq+Q262DOPTU3zPtWl+9vvES4pF3tPJCFvmFAlK/Alw9+XKVQ== dependencies: classnames "^2.3.1" date-fns "^2.29.3" From 58527c3c51aa10050f469604fb72ca3f2b1073bc Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 17 Aug 2023 11:57:29 -0700 Subject: [PATCH 074/357] Set onfocus. --- components/common/SettingsTable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/components/common/SettingsTable.js b/components/common/SettingsTable.js index a57919f15..e9491331e 100644 --- a/components/common/SettingsTable.js +++ b/components/common/SettingsTable.js @@ -41,6 +41,7 @@ export function SettingsTable({ onChange={handleFilterChange} delay={1000} value={filter} + autoFocus={true} placeholder="Search" style={{ maxWidth: '300px', marginBottom: '10px' }} /> From f35a9f0950f540f1edfabd3c261e05813d7ca425 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 17 Aug 2023 12:39:58 -0700 Subject: [PATCH 075/357] Add page of intl. --- components/common/Pager.js | 8 ++++++-- lib/constants.ts | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/components/common/Pager.js b/components/common/Pager.js index 584e06693..4a00590d0 100644 --- a/components/common/Pager.js +++ b/components/common/Pager.js @@ -1,7 +1,9 @@ import styles from './Pager.module.css'; import { Button, Flexbox, Icon, Icons } from 'react-basics'; +import useMessages from 'hooks/useMessages'; -export function Pager({ page, pageSize, count, onPageChange, onPageSizeChange }) { +export function Pager({ page, pageSize, count, onPageChange }) { + const { formatMessage, labels } = useMessages(); const maxPage = Math.ceil(count / pageSize); const lastPage = page === maxPage; const firstPage = page === 1; @@ -24,7 +26,9 @@ export function Pager({ page, pageSize, count, onPageChange, onPageSizeChange }) - {`Page ${page} of ${maxPage}`} + + {formatMessage(labels.pageOf, { x: page, y: maxPage })} + - {formatMessage(labels.pageOf, { x: page, y: maxPage })} + {formatMessage(labels.pageOf, { current: page, total: maxPage })} diff --git a/components/common/Pager.module.css b/components/common/Pager.module.css index b4ee9f0e5..99eb70ce0 100644 --- a/components/common/Pager.module.css +++ b/components/common/Pager.module.css @@ -3,5 +3,5 @@ } .text { - margin: 0 10px; + margin: 0 16px; } diff --git a/components/input/MonthSelect.js b/components/input/MonthSelect.js index a20890ad9..88373fddf 100644 --- a/components/input/MonthSelect.js +++ b/components/input/MonthSelect.js @@ -21,8 +21,9 @@ export function MonthSelect({ date = new Date(), onChange }) { const year = date.getFullYear(); const ref = useRef(); - const handleChange = date => { + const handleChange = (close, date) => { onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`); + close(); }; return ( @@ -36,11 +37,13 @@ export function MonthSelect({ date = new Date(), onChange }) { - + {close => ( + + )} @@ -51,11 +54,13 @@ export function MonthSelect({ date = new Date(), onChange }) { - + {close => ( + + )}
diff --git a/components/messages.js b/components/messages.js index afd8d848e..ff6199450 100644 --- a/components/messages.js +++ b/components/messages.js @@ -21,8 +21,8 @@ export const labels = defineMessages({ details: { id: 'label.details', defaultMessage: 'Details' }, website: { id: 'label.website', defaultMessage: 'Website' }, websites: { id: 'label.websites', defaultMessage: 'Websites' }, - myWebsites: { id: 'label.my-websites', defaultMessage: 'My Websites' }, - teamWebsites: { id: 'label.team-websites', defaultMessage: 'Team Websites' }, + myWebsites: { id: 'label.my-websites', defaultMessage: 'My websites' }, + teamWebsites: { id: 'label.team-websites', defaultMessage: 'Team websites' }, created: { id: 'label.created', defaultMessage: 'Created' }, edit: { id: 'label.edit', defaultMessage: 'Edit' }, name: { id: 'label.name', defaultMessage: 'Name' }, @@ -30,7 +30,7 @@ export const labels = defineMessages({ accessCode: { id: 'label.access-code', defaultMessage: 'Access code' }, teamId: { id: 'label.team-id', defaultMessage: 'Team ID' }, team: { id: 'label.team', defaultMessage: 'Team' }, - teamName: { id: 'label.team-name', defaultMessage: 'Team Name' }, + teamName: { id: 'label.team-name', defaultMessage: 'Team name' }, regenerate: { id: 'label.regenerate', defaultMessage: 'Regenerate' }, remove: { id: 'label.remove', defaultMessage: 'Remove' }, join: { id: 'label.join', defaultMessage: 'Join' }, @@ -177,6 +177,7 @@ export const labels = defineMessages({ pageTitle: { id: 'label.pageTitle', defaultMessage: 'Page title' }, day: { id: 'label.day', defaultMessage: 'Day' }, date: { id: 'label.date', defaultMessage: 'Date' }, + pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' }, }); export const messages = defineMessages({ diff --git a/hooks/useMessages.js b/hooks/useMessages.js index 3c13fab03..e3a6c20be 100644 --- a/hooks/useMessages.js +++ b/hooks/useMessages.js @@ -2,7 +2,7 @@ import { useIntl, FormattedMessage } from 'react-intl'; import { messages, labels } from 'components/messages'; export function useMessages() { - const { formatMessage } = useIntl(); + const intl = useIntl(); const getMessage = id => { const message = Object.values(messages).find(value => value.id === id); @@ -10,6 +10,10 @@ export function useMessages() { return message ? formatMessage(message) : id; }; + const formatMessage = (descriptor, values, opts) => { + return descriptor ? intl.formatMessage(descriptor, values, opts) : null; + }; + return { formatMessage, FormattedMessage, messages, labels, getMessage }; } diff --git a/lang/am-ET.json b/lang/am-ET.json index f91e5edab..7bed14236 100644 --- a/lang/am-ET.json +++ b/lang/am-ET.json @@ -37,7 +37,9 @@ "label.custom-range": "Custom range", "label.dashboard": "Dashboard", "label.data": "Data", + "label.date": "Date", "label.date-range": "Date range", + "label.day": "Day", "label.default-date-range": "Default date range", "label.delete": "Delete", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobile", "label.more": "More", + "label.my-websites": "My websites", "label.name": "Name", "label.new-password": "New password", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Page views", "label.pageTitle": "Page title", "label.pages": "Pages", @@ -117,6 +121,7 @@ "label.required": "Required", "label.reset": "Reset", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Save", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "This month", diff --git a/lang/ar-SA.json b/lang/ar-SA.json index f10ba91d7..0efdfee73 100644 --- a/lang/ar-SA.json +++ b/lang/ar-SA.json @@ -37,7 +37,9 @@ "label.custom-range": "فترة مخصصة", "label.dashboard": "الشاشة الرئيسية", "label.data": "البيانات", + "label.date": "Date", "label.date-range": "فترة مخصصة", + "label.day": "Day", "label.default-date-range": "الفترة المخصصة الافتراضية", "label.delete": "حذف", "label.delete-team": "حذف مجموعة", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "جوال", "label.more": "المزيد", + "label.my-websites": "My websites", "label.name": "الإسم", "label.new-password": "كلمة مرور جديدة", "label.none": "غير معرف", "label.os": "OS", "label.overview": "Overview", "label.owner": "المالك", + "label.page-of": "Page {current} of {total}", "label.page-views": "مشاهدات الصفحة", "label.pageTitle": "Page title", "label.pages": "الصفحات", @@ -117,6 +121,7 @@ "label.required": "اجباري", "label.reset": "اعادة تعيين", "label.reset-website": "اعادة تعيين الإحصائيات", + "label.retention": "Retention", "label.role": "الصلاحية", "label.run-query": "Run query", "label.save": "حفظ", @@ -133,7 +138,9 @@ "label.team-guest": "زائر للمجموعة", "label.team-id": "معرف المجموعة", "label.team-member": "عضو المجموعة", + "label.team-name": "Team name", "label.team-owner": "مدير المجموعة", + "label.team-websites": "Team websites", "label.teams": "المجموعات", "label.theme": "المظهر", "label.this-month": "الشهر الحالي", diff --git a/lang/be-BY.json b/lang/be-BY.json index 72e96b409..32693ebd4 100644 --- a/lang/be-BY.json +++ b/lang/be-BY.json @@ -37,7 +37,9 @@ "label.custom-range": "Карыстацкі дыяпазон", "label.dashboard": "Інфармацыйная панэль", "label.data": "Data", + "label.date": "Date", "label.date-range": "Дыяпазон дат", + "label.day": "Day", "label.default-date-range": "Дыяпазон дат па змаўчанню", "label.delete": "Выдаліць", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Мабільны", "label.more": "Болей", + "label.my-websites": "My websites", "label.name": "Імя", "label.new-password": "Новы пароль", "label.none": "Няма", "label.os": "OS", "label.overview": "Overview", "label.owner": "Уласнік", + "label.page-of": "Page {current} of {total}", "label.page-views": "Прагляды старонкі", "label.pageTitle": "Page title", "label.pages": "Старонкі", @@ -117,6 +121,7 @@ "label.required": "Абавязкова", "label.reset": "Скінуць", "label.reset-website": "Скінуць статыстыку", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Захаваць", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Тэма", "label.this-month": "Гэты месяц", diff --git a/lang/bn-BD.json b/lang/bn-BD.json index ba2bcbb55..483d10080 100644 --- a/lang/bn-BD.json +++ b/lang/bn-BD.json @@ -37,7 +37,9 @@ "label.custom-range": "কাস্টম রেঞ্জ", "label.dashboard": "ড্যাশবোর্ড", "label.data": "Data", + "label.date": "Date", "label.date-range": "তারিখের পরিসীমা", + "label.day": "Day", "label.default-date-range": "ডিফল্ট তারিখের পরিসীমা", "label.delete": "মুছে ফেলুন", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "মুঠোফোন", "label.more": "আরও", + "label.my-websites": "My websites", "label.name": "নাম", "label.new-password": "নতুন পাসওয়ার্ড", "label.none": "কিছুই না", "label.os": "OS", "label.overview": "Overview", "label.owner": "মালিক", + "label.page-of": "Page {current} of {total}", "label.page-views": "পৃষ্ঠা পরিদর্শন গুলো", "label.pageTitle": "Page title", "label.pages": "পৃষ্ঠাগুলি", @@ -117,6 +121,7 @@ "label.required": "প্রয়োজনীয়", "label.reset": "রিসেট", "label.reset-website": "ওয়েবসাইট রিসেট করুন", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "সংরক্ষণ", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "থিম", "label.this-month": "এই মাস", diff --git a/lang/ca-ES.json b/lang/ca-ES.json index ba3e7bd67..51aee79dd 100644 --- a/lang/ca-ES.json +++ b/lang/ca-ES.json @@ -37,7 +37,9 @@ "label.custom-range": "Rang personalitzat", "label.dashboard": "Panell", "label.data": "Data", + "label.date": "Date", "label.date-range": "Interval de dates", + "label.day": "Day", "label.default-date-range": "Interval de dates per defecte", "label.delete": "Esborra", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mòbil", "label.more": "Més", + "label.my-websites": "My websites", "label.name": "Nom", "label.new-password": "Contrasenya nova", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Propietari", + "label.page-of": "Page {current} of {total}", "label.page-views": "Pàgines vistes", "label.pageTitle": "Page title", "label.pages": "Pàgines", @@ -117,6 +121,7 @@ "label.required": "Obligatori", "label.reset": "Restableix", "label.reset-website": "Restableix estadístiques", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Desa", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Aquest mes", diff --git a/lang/cs-CZ.json b/lang/cs-CZ.json index 0017d4185..548ee817d 100644 --- a/lang/cs-CZ.json +++ b/lang/cs-CZ.json @@ -37,7 +37,9 @@ "label.custom-range": "Vlastní rozsah", "label.dashboard": "Přehled", "label.data": "Data", + "label.date": "Date", "label.date-range": "Období", + "label.day": "Day", "label.default-date-range": "Výchozí období", "label.delete": "Smazat", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobilní telefon", "label.more": "Více", + "label.my-websites": "My websites", "label.name": "Jméno", "label.new-password": "Nové heslo", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Zobrazení stránek", "label.pageTitle": "Page title", "label.pages": "Stránky", @@ -117,6 +121,7 @@ "label.required": "Vyžadováno", "label.reset": "Reset", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Uložit", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Tento měsíc", diff --git a/lang/da-DK.json b/lang/da-DK.json index 7c221beb7..9d4fe50ee 100644 --- a/lang/da-DK.json +++ b/lang/da-DK.json @@ -37,7 +37,9 @@ "label.custom-range": "Tilpasset interval", "label.dashboard": "Betjeningspanel", "label.data": "Data", + "label.date": "Date", "label.date-range": "Datointerval", + "label.day": "Day", "label.default-date-range": "Standard datointerval", "label.delete": "Slet", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobil", "label.more": "Mere", + "label.my-websites": "My websites", "label.name": "Navn", "label.new-password": "Ny adgangskode", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Ejer", + "label.page-of": "Page {current} of {total}", "label.page-views": "Sidevisninger", "label.pageTitle": "Page title", "label.pages": "Sider", @@ -117,6 +121,7 @@ "label.required": "Påkrævet", "label.reset": "Nulstil", "label.reset-website": "Nulstil statistikker", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Gem", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Tema", "label.this-month": "Denne måned", diff --git a/lang/de-CH.json b/lang/de-CH.json index 9064d63f6..5c6c45d14 100644 --- a/lang/de-CH.json +++ b/lang/de-CH.json @@ -37,7 +37,9 @@ "label.custom-range": "Benutzerdefinierte Bereich", "label.dashboard": "Übersicht", "label.data": "Datä", + "label.date": "Date", "label.date-range": "Datumsbereich", + "label.day": "Day", "label.default-date-range": "Vorigstellte Datumsbereich", "label.delete": "Lösche", "label.delete-team": "Team lösche", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Handy", "label.more": "Meh", + "label.my-websites": "My websites", "label.name": "Name", "label.new-password": "Neus Passwort", "label.none": "Keis", "label.os": "OS", "label.overview": "Overview", "label.owner": "Bsitzer", + "label.page-of": "Page {current} of {total}", "label.page-views": "Siitenufrüef", "label.pageTitle": "Page title", "label.pages": "Siite", @@ -117,6 +121,7 @@ "label.required": "Erforderlich", "label.reset": "Zruggsetze", "label.reset-website": "Statistik zruggsetze", + "label.retention": "Retention", "label.role": "Rollä", "label.run-query": "Run query", "label.save": "Speichere", @@ -133,7 +138,9 @@ "label.team-guest": "Team Gast", "label.team-id": "Team ID", "label.team-member": "Team Mitglied", + "label.team-name": "Team name", "label.team-owner": "Team Bsitzer", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Thema", "label.this-month": "De Monet", diff --git a/lang/de-DE.json b/lang/de-DE.json index 6f6934ec8..3307dfa40 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -37,7 +37,9 @@ "label.custom-range": "Benutzerdefinierter Bereich", "label.dashboard": "Übersicht", "label.data": "Daten", + "label.date": "Date", "label.date-range": "Datumsbereich", + "label.day": "Day", "label.default-date-range": "Voreingestellter Datumsbereich", "label.delete": "Löschen", "label.delete-team": "Team löschen", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Handy", "label.more": "Mehr", + "label.my-websites": "My websites", "label.name": "Name", "label.new-password": "Neues Passwort", "label.none": "Keine", "label.os": "OS", "label.overview": "Übersicht", "label.owner": "Besitzer", + "label.page-of": "Page {current} of {total}", "label.page-views": "Seitenaufrufe", "label.pageTitle": "Page title", "label.pages": "Seiten", @@ -117,6 +121,7 @@ "label.required": "Erforderlich", "label.reset": "Zurücksetzen", "label.reset-website": "Statistik zurücksetzen", + "label.retention": "Retention", "label.role": "Rolle", "label.run-query": "Abfrage starten", "label.save": "Speichern", @@ -133,7 +138,9 @@ "label.team-guest": "Team Gast", "label.team-id": "Team ID", "label.team-member": "Team Mitglied", + "label.team-name": "Team name", "label.team-owner": "Team Eigentümer", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Thema", "label.this-month": "Diesen Monat", diff --git a/lang/el-GR.json b/lang/el-GR.json index 2280f5454..dd95c7772 100644 --- a/lang/el-GR.json +++ b/lang/el-GR.json @@ -37,7 +37,9 @@ "label.custom-range": "Προσαρμοσμένο εύρος", "label.dashboard": "Πίνακας", "label.data": "Data", + "label.date": "Date", "label.date-range": "Εύρος ημερομηνιών", + "label.day": "Day", "label.default-date-range": "Προεπιλεγμένο εύρος ημερομηνιών", "label.delete": "Διαγραφή", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Κινητό", "label.more": "Περισσότερα", + "label.my-websites": "My websites", "label.name": "Όνομα", "label.new-password": "Νέος κωδικός", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Προβολές σελίδας", "label.pageTitle": "Page title", "label.pages": "Σελίδες", @@ -117,6 +121,7 @@ "label.required": "Απαιτείται", "label.reset": "Επαναφορά", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Αποθήκευση", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Αυτο το μήνα", diff --git a/lang/en-GB.json b/lang/en-GB.json index fa67bbb83..4efaec5df 100644 --- a/lang/en-GB.json +++ b/lang/en-GB.json @@ -37,7 +37,9 @@ "label.custom-range": "Custom range", "label.dashboard": "Dashboard", "label.data": "Data", + "label.date": "Date", "label.date-range": "Date range", + "label.day": "Day", "label.default-date-range": "Default date range", "label.delete": "Delete", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobile", "label.more": "More", + "label.my-websites": "My websites", "label.name": "Name", "label.new-password": "New password", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Page views", "label.pageTitle": "Page title", "label.pages": "Pages", @@ -117,6 +121,7 @@ "label.required": "Required", "label.reset": "Reset", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Save", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "This month", diff --git a/lang/en-US.json b/lang/en-US.json index d83cffb5b..b7c77a694 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -37,7 +37,9 @@ "label.custom-range": "Custom range", "label.dashboard": "Dashboard", "label.data": "Data", + "label.date": "Date", "label.date-range": "Date range", + "label.day": "Day", "label.default-date-range": "Default date range", "label.delete": "Delete", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobile", "label.more": "More", + "label.my-websites": "My websites", "label.name": "Name", "label.new-password": "New password", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Page views", "label.pageTitle": "Page title", "label.pages": "Pages", @@ -117,6 +121,7 @@ "label.required": "Required", "label.reset": "Reset", "label.reset-website": "Reset website", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Save", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "This month", @@ -185,7 +192,7 @@ "message.no-data-available": "No data available.", "message.no-event-data": "No event data is available.", "message.no-match-password": "Passwords do not match.", - "message.no-results-found": "No results were found.", + "message.no-results-found": "No results found.", "message.no-team-websites": "This team does not have any websites.", "message.no-teams": "You have not created any teams.", "message.no-users": "There are no users.", diff --git a/lang/es-ES.json b/lang/es-ES.json index cdaf194cf..7a401e51d 100644 --- a/lang/es-ES.json +++ b/lang/es-ES.json @@ -37,7 +37,9 @@ "label.custom-range": "Intervalo personalizado", "label.dashboard": "Panel de control", "label.data": "Datos", + "label.date": "Date", "label.date-range": "Intervalo de fechas", + "label.day": "Day", "label.default-date-range": "Intervalo por defecto", "label.delete": "Eliminar", "label.delete-team": "Eliminar equipo", @@ -90,12 +92,14 @@ "label.min": "Mín", "label.mobile": "Móvil", "label.more": "Más", + "label.my-websites": "My websites", "label.name": "Nombre", "label.new-password": "Nueva contraseña", "label.none": "Ninguno", "label.os": "OS", "label.overview": "Resumen", "label.owner": "Propietario", + "label.page-of": "Page {current} of {total}", "label.page-views": "Vistas", "label.pageTitle": "Page title", "label.pages": "Páginas", @@ -117,6 +121,7 @@ "label.required": "Obligatorio", "label.reset": "Reiniciar", "label.reset-website": "Reiniciar estadísticas", + "label.retention": "Retention", "label.role": "Rol", "label.run-query": "Ejecutar consulta", "label.save": "Guardar", @@ -133,7 +138,9 @@ "label.team-guest": "Invitado al equipo", "label.team-id": "ID de equipo", "label.team-member": "Miembro del equipo", + "label.team-name": "Team name", "label.team-owner": "Admin. del equipo", + "label.team-websites": "Team websites", "label.teams": "Equipos", "label.theme": "Tema", "label.this-month": "Este mes", diff --git a/lang/es-MX.json b/lang/es-MX.json index f42c9c553..499b2533f 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -37,7 +37,9 @@ "label.custom-range": "Intervalo personalizado", "label.dashboard": "Panel de control", "label.data": "Datos", + "label.date": "Date", "label.date-range": "Intervalo de fechas", + "label.day": "Day", "label.default-date-range": "Intervalo por defecto", "label.delete": "Eliminar", "label.delete-team": "Eliminar team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Móvil", "label.more": "Más", + "label.my-websites": "My websites", "label.name": "Nombre", "label.new-password": "Nueva contraseña", "label.none": "Ninguno", "label.os": "OS", "label.overview": "Overview", "label.owner": "Propietario", + "label.page-of": "Page {current} of {total}", "label.page-views": "Vistas", "label.pageTitle": "Page title", "label.pages": "Páginas", @@ -117,6 +121,7 @@ "label.required": "Obligatorio", "label.reset": "Reiniciar", "label.reset-website": "Reiniciar estadísticas", + "label.retention": "Retention", "label.role": "Rol", "label.run-query": "Run query", "label.save": "Guardar", @@ -133,7 +138,9 @@ "label.team-guest": "Invitado de equipo", "label.team-id": "ID de equipo", "label.team-member": "Miembro de equipo", + "label.team-name": "Team name", "label.team-owner": "Admin. del equipo", + "label.team-websites": "Team websites", "label.teams": "Equipos", "label.theme": "Tema", "label.this-month": "Este mes", diff --git a/lang/fa-IR.json b/lang/fa-IR.json index 1839cb61a..b263a7d1a 100644 --- a/lang/fa-IR.json +++ b/lang/fa-IR.json @@ -37,7 +37,9 @@ "label.custom-range": "محدوده‌ی دلخواه", "label.dashboard": "داشبورد", "label.data": "Data", + "label.date": "Date", "label.date-range": "محدوده‌ی تاریخ", + "label.day": "Day", "label.default-date-range": "محدوده‌ی پیشفرض تاریخ", "label.delete": "حذف", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "موبایل", "label.more": "بیشتر", + "label.my-websites": "My websites", "label.name": "نام", "label.new-password": "رمز جدید", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "ایجاد شده توسط", + "label.page-of": "Page {current} of {total}", "label.page-views": "بازدید صفحه", "label.pageTitle": "Page title", "label.pages": "صفحه‌ها", @@ -117,6 +121,7 @@ "label.required": "ضروری", "label.reset": "بازنشانی", "label.reset-website": "بازنشانی آمار", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "ذخیره", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "تم", "label.this-month": "این ماه", diff --git a/lang/fi-FI.json b/lang/fi-FI.json index 31b2f1eed..9e9c1de03 100644 --- a/lang/fi-FI.json +++ b/lang/fi-FI.json @@ -37,7 +37,9 @@ "label.custom-range": "Mukautettu ajanjakso", "label.dashboard": "Ohjauspaneeli", "label.data": "Data", + "label.date": "Date", "label.date-range": "Ajanjakso", + "label.day": "Day", "label.default-date-range": "Oletusajanjakso", "label.delete": "Poista", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Puhelin", "label.more": "Lisää", + "label.my-websites": "My websites", "label.name": "Nimi", "label.new-password": "Uusi salasana", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Omistaja", + "label.page-of": "Page {current} of {total}", "label.page-views": "Sivun näyttökerrat", "label.pageTitle": "Page title", "label.pages": "Sivut", @@ -117,6 +121,7 @@ "label.required": "Vaaditaan", "label.reset": "Nollaa", "label.reset-website": "Nollaa tilastot", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Tallenna", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Teema", "label.this-month": "Tämä kuukausi", diff --git a/lang/fo-FO.json b/lang/fo-FO.json index 030b815f6..6259a5557 100644 --- a/lang/fo-FO.json +++ b/lang/fo-FO.json @@ -37,7 +37,9 @@ "label.custom-range": "Tillaga spenni", "label.dashboard": "Yvirlitsskíggi", "label.data": "Data", + "label.date": "Date", "label.date-range": "Vel dato", + "label.day": "Day", "label.default-date-range": "Forsett dato", "label.delete": "Sletta", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Telefon", "label.more": "Meira", + "label.my-websites": "My websites", "label.name": "Navn", "label.new-password": "Nýtt loyniorð", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Opnaðar síðir", "label.pageTitle": "Page title", "label.pages": "Síðir", @@ -117,6 +121,7 @@ "label.required": "Kravt", "label.reset": "Nulstilla", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Goym", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Hendan mánan", diff --git a/lang/fr-FR.json b/lang/fr-FR.json index 29ac8728f..558c1cd26 100644 --- a/lang/fr-FR.json +++ b/lang/fr-FR.json @@ -37,7 +37,9 @@ "label.custom-range": "Période personnalisée", "label.dashboard": "Tableau de bord", "label.data": "Données", + "label.date": "Date", "label.date-range": "Période", + "label.day": "Day", "label.default-date-range": "Période par défaut", "label.delete": "Supprimer", "label.delete-team": "Supprimer l'équipe", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Téléphone", "label.more": "Plus", + "label.my-websites": "My websites", "label.name": "Nom", "label.new-password": "Nouveau mot de passe", "label.none": "Aucun·e", "label.os": "OS", "label.overview": "Vue d'ensemble", "label.owner": "Propriétaire", + "label.page-of": "Page {current} of {total}", "label.page-views": "Pages vues", "label.pageTitle": "Page title", "label.pages": "Pages", @@ -117,6 +121,7 @@ "label.required": "Requis", "label.reset": "Réinitialiser", "label.reset-website": "Réinitialiser les statistiques", + "label.retention": "Retention", "label.role": "Rôle", "label.run-query": "Éxécuter la requête", "label.save": "Enregistrer", @@ -133,7 +138,9 @@ "label.team-guest": "Invité dans l'équipe", "label.team-id": "ID d'équipe", "label.team-member": "Membre de l'équipe", + "label.team-name": "Team name", "label.team-owner": "Propriétaire de l'équipe", + "label.team-websites": "Team websites", "label.teams": "Équipes", "label.theme": "Thème", "label.this-month": "Ce mois", diff --git a/lang/ga-ES.json b/lang/ga-ES.json index 402f76818..e6ceda8a9 100644 --- a/lang/ga-ES.json +++ b/lang/ga-ES.json @@ -37,7 +37,9 @@ "label.custom-range": "Rango personalizado", "label.dashboard": "Taboleiro", "label.data": "Data", + "label.date": "Date", "label.date-range": "Rango temporal", + "label.day": "Day", "label.default-date-range": "Rango temporal por defecto", "label.delete": "Eliminar", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Móbil", "label.more": "Máis", + "label.my-websites": "My websites", "label.name": "Nome", "label.new-password": "Novo contrasinal", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Dona", + "label.page-of": "Page {current} of {total}", "label.page-views": "Vistas de páxinas", "label.pageTitle": "Page title", "label.pages": "Páxinas", @@ -117,6 +121,7 @@ "label.required": "Requerido", "label.reset": "Restablecer", "label.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Gardar", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Decorado", "label.this-month": "Este mes", diff --git a/lang/he-IL.json b/lang/he-IL.json index 5ba91136f..fd3e0b8b8 100644 --- a/lang/he-IL.json +++ b/lang/he-IL.json @@ -37,7 +37,9 @@ "label.custom-range": "טווח מותאם", "label.dashboard": "דשבורד", "label.data": "Data", + "label.date": "Date", "label.date-range": "טווח תאריכים", + "label.day": "Day", "label.default-date-range": "טווח תאריכים בברירת מחדל", "label.delete": "הסרה", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "מובייל", "label.more": "עוד", + "label.my-websites": "My websites", "label.name": "שם", "label.new-password": "סיסמה חדשה", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "צפיות בדפים", "label.pageTitle": "Page title", "label.pages": "דפים", @@ -117,6 +121,7 @@ "label.required": "נדרש", "label.reset": "איפוס", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "שמירה", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "החודש", diff --git a/lang/hi-IN.json b/lang/hi-IN.json index e57b6865c..6e268aa6e 100644 --- a/lang/hi-IN.json +++ b/lang/hi-IN.json @@ -37,7 +37,9 @@ "label.custom-range": "कस्टम रेंज", "label.dashboard": "नियंत्रण-पट्ट", "label.data": "Data", + "label.date": "Date", "label.date-range": "तिथि सीमा", + "label.day": "Day", "label.default-date-range": "डिफ़ॉल्ट तिथि सीमा", "label.delete": "खाता हटाएं", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "मोबाइल फोन", "label.more": "और", + "label.my-websites": "My websites", "label.name": "नाम", "label.new-password": "नया पासवर्ड", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "पृष्ठ दृश्य", "label.pageTitle": "Page title", "label.pages": "पृष्ठों", @@ -117,6 +121,7 @@ "label.required": "अपेक्षित", "label.reset": "रीसेट", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "सहेजें", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "इस महीने", diff --git a/lang/hr-HR.json b/lang/hr-HR.json index 805d820ee..ecde7100e 100644 --- a/lang/hr-HR.json +++ b/lang/hr-HR.json @@ -37,7 +37,9 @@ "label.custom-range": "Prilagođeni raspon", "label.dashboard": "Nadzorna ploča", "label.data": "Data", + "label.date": "Date", "label.date-range": "Raspon datuma", + "label.day": "Day", "label.default-date-range": "Zadani datumski raspon", "label.delete": "Obriši", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobile", "label.more": "Više", + "label.my-websites": "My websites", "label.name": "Ime", "label.new-password": "Nova lozinka", "label.none": "Ništa", "label.os": "OS", "label.overview": "Overview", "label.owner": "Vlasnik", + "label.page-of": "Page {current} of {total}", "label.page-views": "Page views", "label.pageTitle": "Page title", "label.pages": "Pages", @@ -117,6 +121,7 @@ "label.required": "Potrebna", "label.reset": "Resetirati", "label.reset-website": "Resetirati web stranicu", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Spremi", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Tema", "label.this-month": "Ovaj mjesec", diff --git a/lang/hu-HU.json b/lang/hu-HU.json index 96512e15d..0401afff7 100644 --- a/lang/hu-HU.json +++ b/lang/hu-HU.json @@ -37,7 +37,9 @@ "label.custom-range": "Egyedi tartomány", "label.dashboard": "Áttekintés", "label.data": "Data", + "label.date": "Date", "label.date-range": "Időintervallum", + "label.day": "Day", "label.default-date-range": "Alapértelmezett időintervallum", "label.delete": "Eltávolítás", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Telefon", "label.more": "Bővebben", + "label.my-websites": "My websites", "label.name": "Név", "label.new-password": "Új jelszó", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Oldalmegtekintések", "label.pageTitle": "Page title", "label.pages": "Oldalak", @@ -117,6 +121,7 @@ "label.required": "Kötelező", "label.reset": "Visszaállítás", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Mentés", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Ezen hónap", diff --git a/lang/id-ID.json b/lang/id-ID.json index 0d81b55c7..d0b8a0643 100644 --- a/lang/id-ID.json +++ b/lang/id-ID.json @@ -37,7 +37,9 @@ "label.custom-range": "Rentang khusus", "label.dashboard": "Dasbor", "label.data": "Data", + "label.date": "Date", "label.date-range": "Rentang tanggal", + "label.day": "Day", "label.default-date-range": "Rentang tanggal bawaan", "label.delete": "Hapus", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Ponsel", "label.more": "Lebih banyak", + "label.my-websites": "My websites", "label.name": "Nama", "label.new-password": "Kata sandi baru", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Pemilik", + "label.page-of": "Page {current} of {total}", "label.page-views": "Tampilan halaman", "label.pageTitle": "Page title", "label.pages": "Halaman", @@ -117,6 +121,7 @@ "label.required": "Wajib", "label.reset": "Atur ulang", "label.reset-website": "Atur ulang statistik", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Simpan", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Tema", "label.this-month": "Bulan ini", diff --git a/lang/it-IT.json b/lang/it-IT.json index 0d6647671..57d6d5ba3 100644 --- a/lang/it-IT.json +++ b/lang/it-IT.json @@ -37,7 +37,9 @@ "label.custom-range": "Personalizzato", "label.dashboard": "Pannello di Controllo", "label.data": "Data", + "label.date": "Date", "label.date-range": "Periodo", + "label.day": "Day", "label.default-date-range": "Periodo standard", "label.delete": "Elimina", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Cellulare", "label.more": "Dettagli", + "label.my-websites": "My websites", "label.name": "Nome", "label.new-password": "Nuova password", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Proprietario", + "label.page-of": "Page {current} of {total}", "label.page-views": "Visualizzazioni di pagina", "label.pageTitle": "Page title", "label.pages": "Pagine", @@ -117,6 +121,7 @@ "label.required": "Obbligatorio", "label.reset": "Reset", "label.reset-website": "Resetta le statistiche", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Salva", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Tema", "label.this-month": "Questo mese", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index 1e1dd7f3d..0f4d54504 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -37,7 +37,9 @@ "label.custom-range": "期間を指定する", "label.dashboard": "ダッシュボード", "label.data": "Data", + "label.date": "Date", "label.date-range": "範囲指定", + "label.day": "Day", "label.default-date-range": "最初に表示する期間", "label.delete": "削除", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "携帯電話", "label.more": "さらに表示", + "label.my-websites": "My websites", "label.name": "名前", "label.new-password": "新しいパスワード", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "閲覧数", "label.pageTitle": "Page title", "label.pages": "ページ", @@ -117,6 +121,7 @@ "label.required": "必須", "label.reset": "リセット", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "保存", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "今月", diff --git a/lang/km-KH.json b/lang/km-KH.json index d5c8af6b7..58f7926f3 100644 --- a/lang/km-KH.json +++ b/lang/km-KH.json @@ -37,7 +37,9 @@ "label.custom-range": "កំណត់ដោយខ្លួនឯង", "label.dashboard": "ផ្ទាំងគ្រប់គ្រង", "label.data": "Data", + "label.date": "Date", "label.date-range": "ចន្លោះកាលបរិច្ឆេទ", + "label.day": "Day", "label.default-date-range": "ចន្លោះកាលបរិច្ឆេទស្រាប់", "label.delete": "លុប", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "ទូរស័ព្ទចល័ត", "label.more": "បន្ថែម", + "label.my-websites": "My websites", "label.name": "ឈ្មោះ", "label.new-password": "ពាក្យសម្ងាត់​ថ្មី", "label.none": "មិនមាន", "label.os": "OS", "label.overview": "Overview", "label.owner": "ម្ចាស់", + "label.page-of": "Page {current} of {total}", "label.page-views": "អ្នកមើលទំព័រ", "label.pageTitle": "Page title", "label.pages": "ទំព័រ", @@ -117,6 +121,7 @@ "label.required": "ទាមទារ", "label.reset": "កំណត់ឡើងវិញ", "label.reset-website": "កំណត់ស្ថិតិឡើងវិញ", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "រក្សាទុក", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "រូបរាង", "label.this-month": "ខែនេះ", diff --git a/lang/ko-KR.json b/lang/ko-KR.json index 66e036320..767e8e220 100644 --- a/lang/ko-KR.json +++ b/lang/ko-KR.json @@ -37,7 +37,9 @@ "label.custom-range": "범위 지정", "label.dashboard": "대시보드", "label.data": "Data", + "label.date": "Date", "label.date-range": "날짜 범위", + "label.day": "Day", "label.default-date-range": "기본 날짜 범위", "label.delete": "삭제", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "모바일", "label.more": "더 보기", + "label.my-websites": "My websites", "label.name": "이름", "label.new-password": "새 비밀번호", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "페이지 뷰(PV)", "label.pageTitle": "Page title", "label.pages": "페이지", @@ -117,6 +121,7 @@ "label.required": "필수", "label.reset": "리셋", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "저장", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "이번 달", diff --git a/lang/lt-LT.json b/lang/lt-LT.json index a90dbb68d..c8161f1db 100644 --- a/lang/lt-LT.json +++ b/lang/lt-LT.json @@ -37,7 +37,9 @@ "label.custom-range": "Pasirinktinis intervalas", "label.dashboard": "Švieslentė", "label.data": "Data", + "label.date": "Date", "label.date-range": "Laikotarpis", + "label.day": "Day", "label.default-date-range": "Numatytasis laikotarpis", "label.delete": "Ištrinti", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobilusis", "label.more": "Daugiau", + "label.my-websites": "My websites", "label.name": "Pavadinimas", "label.new-password": "Naujas slaptažodis", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Savininkas", + "label.page-of": "Page {current} of {total}", "label.page-views": "Puslapių peržiūros", "label.pageTitle": "Page title", "label.pages": "Puslapiai", @@ -117,6 +121,7 @@ "label.required": "Reikalinga", "label.reset": "Atstatyti", "label.reset-website": "Atstatyti statistikos duomenis", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Išsaugoti", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Šis mėnuo", diff --git a/lang/mn-MN.json b/lang/mn-MN.json index 99efcd3ef..1478c0792 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -37,7 +37,9 @@ "label.custom-range": "Дурын хугацаа", "label.dashboard": "Хянах самбар", "label.data": "Өгөгдөл", + "label.date": "Date", "label.date-range": "Хугацааны муж", + "label.day": "Day", "label.default-date-range": "Өгөгдмөл хугацааны муж", "label.delete": "Устгах", "label.delete-team": "Баг устгах", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Утас", "label.more": "Цааш", + "label.my-websites": "My websites", "label.name": "Нэр", "label.new-password": "Шинэ нууц үг", "label.none": "Байхгүй", "label.os": "OS", "label.overview": "Overview", "label.owner": "Эзэмшигч", + "label.page-of": "Page {current} of {total}", "label.page-views": "Хуудас үзсэн", "label.pageTitle": "Page title", "label.pages": "Хуудас", @@ -117,6 +121,7 @@ "label.required": "Шаардлагатай", "label.reset": "Дахин эхлүүлэх", "label.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх", + "label.retention": "Retention", "label.role": "Эрх", "label.run-query": "Run query", "label.save": "Хадгалах", @@ -133,7 +138,9 @@ "label.team-guest": "Багийн зочин", "label.team-id": "Багийн ID", "label.team-member": "Багийн гишүүн", + "label.team-name": "Team name", "label.team-owner": "Багийн эзэмшигч", + "label.team-websites": "Team websites", "label.teams": "Багууд", "label.theme": "Загвар", "label.this-month": "Энэ сар", diff --git a/lang/ms-MY.json b/lang/ms-MY.json index 53de54770..5b8769c58 100644 --- a/lang/ms-MY.json +++ b/lang/ms-MY.json @@ -37,7 +37,9 @@ "label.custom-range": "Julat khas", "label.dashboard": "Papan pemuka", "label.data": "Data", + "label.date": "Date", "label.date-range": "Julat tarikh", + "label.day": "Day", "label.default-date-range": "Julat tarikh lalai", "label.delete": "Padam", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Telefon bimbit", "label.more": "Lebih banyak lagi", + "label.my-websites": "My websites", "label.name": "Nama", "label.new-password": "Kata laluan baru", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Paparan halaman", "label.pageTitle": "Page title", "label.pages": "Halaman", @@ -117,6 +121,7 @@ "label.required": "Diperlukan", "label.reset": "Tetapkan semula", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Simpan", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Bulan ini", diff --git a/lang/nb-NO.json b/lang/nb-NO.json index 59210425b..654c3c791 100644 --- a/lang/nb-NO.json +++ b/lang/nb-NO.json @@ -37,7 +37,9 @@ "label.custom-range": "Egendefinert utvalg", "label.dashboard": "Dashbord", "label.data": "Data", + "label.date": "Date", "label.date-range": "Datointervall", + "label.day": "Day", "label.default-date-range": "Standard datoperiode", "label.delete": "Slett", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobiltelefon", "label.more": "Mer", + "label.my-websites": "My websites", "label.name": "Navn", "label.new-password": "Nytt passord", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Eier", + "label.page-of": "Page {current} of {total}", "label.page-views": "Sidevisninger", "label.pageTitle": "Page title", "label.pages": "Sider", @@ -117,6 +121,7 @@ "label.required": "Påkrevd", "label.reset": "Nullstill", "label.reset-website": "Nullstill statistikk", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Lagre", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Denne måneden", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index eb01cebf8..ad30cf36b 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -37,7 +37,9 @@ "label.custom-range": "Aangepast bereik", "label.dashboard": "Overzicht", "label.data": "Gegevens", + "label.date": "Date", "label.date-range": "Datumbereik", + "label.day": "Day", "label.default-date-range": "Standaard bereik", "label.delete": "Verwijderen", "label.delete-team": "Team verwijderen", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobiel", "label.more": "Toon meer", + "label.my-websites": "My websites", "label.name": "Naam", "label.new-password": "Nieuw wachtwoord", "label.none": "Geen", "label.os": "OS", "label.overview": "Overview", "label.owner": "Eigenaar", + "label.page-of": "Page {current} of {total}", "label.page-views": "Paginaweergaven", "label.pageTitle": "Page title", "label.pages": "Pagina's", @@ -117,6 +121,7 @@ "label.required": "Verplicht", "label.reset": "Opnieuw instellen", "label.reset-website": "Statistieken opnieuw instellen", + "label.retention": "Retention", "label.role": "Gebruikersrol", "label.run-query": "Run query", "label.save": "Opslaan", @@ -133,7 +138,9 @@ "label.team-guest": "Team gast", "label.team-id": "Team ID", "label.team-member": "Teamlid", + "label.team-name": "Team name", "label.team-owner": "Teameigenaar", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Thema", "label.this-month": "Deze maand", diff --git a/lang/pl-PL.json b/lang/pl-PL.json index c56cccc48..eb9406139 100644 --- a/lang/pl-PL.json +++ b/lang/pl-PL.json @@ -37,7 +37,9 @@ "label.custom-range": "Zakres niestandardowy", "label.dashboard": "Panel", "label.data": "Data", + "label.date": "Date", "label.date-range": "Zakres dat", + "label.day": "Day", "label.default-date-range": "Domyślny zakres dat", "label.delete": "Usuń", "label.delete-team": "Usuń zespół", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Smartfon", "label.more": "Więcej", + "label.my-websites": "My websites", "label.name": "Nazwa", "label.new-password": "Nowe hasło", "label.none": "Brak", "label.os": "OS", "label.overview": "Przegląd", "label.owner": "Właściciel", + "label.page-of": "Page {current} of {total}", "label.page-views": "Wyświetlenia strony", "label.pageTitle": "Page title", "label.pages": "Strony", @@ -117,6 +121,7 @@ "label.required": "Wymagany", "label.reset": "Zresetuj", "label.reset-website": "Zresetuj statystyki", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Uruchom zapytanie", "label.save": "Zapisz", @@ -133,7 +138,9 @@ "label.team-guest": "Gość zespołu", "label.team-id": "ID zespołu", "label.team-member": "Członek zespołu", + "label.team-name": "Team name", "label.team-owner": "Właściciel zespołu", + "label.team-websites": "Team websites", "label.teams": "Zespoły", "label.theme": "Motyw", "label.this-month": "W tym miesiącu", diff --git a/lang/pt-BR.json b/lang/pt-BR.json index dbaa97b1f..b68d96154 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -37,7 +37,9 @@ "label.custom-range": "Intervalo personalizado", "label.dashboard": "Painel", "label.data": "Data", + "label.date": "Date", "label.date-range": "Intervalo de datas", + "label.day": "Day", "label.default-date-range": "Intervalo de datas predefinido", "label.delete": "Remover", "label.delete-team": "Remover time", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Celular", "label.more": "Mais", + "label.my-websites": "My websites", "label.name": "Nome", "label.new-password": "Nova senha", "label.none": "Nenhum", "label.os": "OS", "label.overview": "Overview", "label.owner": "Proprietário", + "label.page-of": "Page {current} of {total}", "label.page-views": "Visualizações de página", "label.pageTitle": "Page title", "label.pages": "Páginas", @@ -117,6 +121,7 @@ "label.required": "Obrigatório", "label.reset": "Redefinir", "label.reset-website": "Redefinir estatísticas", + "label.retention": "Retention", "label.role": "Papel", "label.run-query": "Executar query", "label.save": "Salvar", @@ -133,7 +138,9 @@ "label.team-guest": "Convidado", "label.team-id": "ID do Time", "label.team-member": "Membro", + "label.team-name": "Team name", "label.team-owner": "Proprietário", + "label.team-websites": "Team websites", "label.teams": "Times", "label.theme": "Tema", "label.this-month": "Este mês", diff --git a/lang/pt-PT.json b/lang/pt-PT.json index faf9185c2..fcf7ff03f 100644 --- a/lang/pt-PT.json +++ b/lang/pt-PT.json @@ -37,7 +37,9 @@ "label.custom-range": "Intervalo personalizado", "label.dashboard": "Painel", "label.data": "Data", + "label.date": "Date", "label.date-range": "Intervalo de datas", + "label.day": "Day", "label.default-date-range": "Intervalo de datas predefinido", "label.delete": "Eliminar", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Telemóvel", "label.more": "Mais", + "label.my-websites": "My websites", "label.name": "Nome", "label.new-password": "Nova senha", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Proprietário", + "label.page-of": "Page {current} of {total}", "label.page-views": "Visualizações da página", "label.pageTitle": "Page title", "label.pages": "Páginas", @@ -117,6 +121,7 @@ "label.required": "Obrigatório", "label.reset": "Repor", "label.reset-website": "Repor estatísticas", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Guardar", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Tema", "label.this-month": "Este mês", diff --git a/lang/ro-RO.json b/lang/ro-RO.json index 45fc73d73..43a78ecd2 100644 --- a/lang/ro-RO.json +++ b/lang/ro-RO.json @@ -37,7 +37,9 @@ "label.custom-range": "Interval personalizat", "label.dashboard": "Tablou de bord", "label.data": "Data", + "label.date": "Date", "label.date-range": "Interval de date", + "label.day": "Day", "label.default-date-range": "Interval de date implicit", "label.delete": "Șterge", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobil", "label.more": "Mai mult", + "label.my-websites": "My websites", "label.name": "Nume", "label.new-password": "Parola nouă", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Vizualizări de pagină", "label.pageTitle": "Page title", "label.pages": "Pagini", @@ -117,6 +121,7 @@ "label.required": "Obligatoriu", "label.reset": "Resetează", "label.reset-website": "Resetează statisticile pentru site", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Salvează", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Această lună", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index 80cf0da8f..b9129beb4 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -37,7 +37,9 @@ "label.custom-range": "Другой период", "label.dashboard": "Информационная панель", "label.data": "Данные", + "label.date": "Date", "label.date-range": "Диапазон дат", + "label.day": "Day", "label.default-date-range": "Диапазон дат по-умолчанию", "label.delete": "Удалить", "label.delete-team": "Удалить команду", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Смартфон", "label.more": "Больше", + "label.my-websites": "My websites", "label.name": "Имя", "label.new-password": "Новый пароль", "label.none": "Не указано", "label.os": "OS", "label.overview": "Overview", "label.owner": "Владелец", + "label.page-of": "Page {current} of {total}", "label.page-views": "Просмотры страниц", "label.pageTitle": "Page title", "label.pages": "Страницы", @@ -117,6 +121,7 @@ "label.required": "Обязательное", "label.reset": "Сбросить", "label.reset-website": "Сбросить статистику", + "label.retention": "Retention", "label.role": "Роль", "label.run-query": "Run query", "label.save": "Сохранить", @@ -133,7 +138,9 @@ "label.team-guest": "Гость команды", "label.team-id": "ID команды", "label.team-member": "Член команды", + "label.team-name": "Team name", "label.team-owner": "Владелец команды", + "label.team-websites": "Team websites", "label.teams": "Команды", "label.theme": "Тема", "label.this-month": "Этот месяц", diff --git a/lang/si-LK.json b/lang/si-LK.json index f656cc004..6f6dda6d1 100644 --- a/lang/si-LK.json +++ b/lang/si-LK.json @@ -37,7 +37,9 @@ "label.custom-range": "අභිරුචි පරාසය", "label.dashboard": "උපකරණ පුවරුව", "label.data": "Data", + "label.date": "Date", "label.date-range": "දින පරාසය", + "label.day": "Day", "label.default-date-range": "පෙරනිමි දින පරාසය", "label.delete": "මකන්න", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobile", "label.more": "තවත්", + "label.my-websites": "My websites", "label.name": "නම", "label.new-password": "අලුත් මුරපදය", "label.none": "කිසිවක් නැත", "label.os": "OS", "label.overview": "Overview", "label.owner": "හිමිකරු", + "label.page-of": "Page {current} of {total}", "label.page-views": "Page views", "label.pageTitle": "Page title", "label.pages": "Pages", @@ -117,6 +121,7 @@ "label.required": "අවශ්‍යයි", "label.reset": "යළි පිහිටුවන්න", "label.reset-website": "සංඛ්යා ලේඛන නැවත සකසන්න", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "සුරකින්න", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "තේමාව", "label.this-month": "මෙ මාසය", diff --git a/lang/sk-SK.json b/lang/sk-SK.json index 49301b799..3f0339236 100644 --- a/lang/sk-SK.json +++ b/lang/sk-SK.json @@ -37,7 +37,9 @@ "label.custom-range": "Vlastný rozsah", "label.dashboard": "Prehlad", "label.data": "Data", + "label.date": "Date", "label.date-range": "Obdobie", + "label.day": "Day", "label.default-date-range": "Predvolené obdobie", "label.delete": "Zmazať", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobilný telefon", "label.more": "Viac", + "label.my-websites": "My websites", "label.name": "Meno", "label.new-password": "Nové heslo", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Zobrazenie stánok", "label.pageTitle": "Page title", "label.pages": "Stránky", @@ -117,6 +121,7 @@ "label.required": "Povinné", "label.reset": "Reset", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Uložiť", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Tento mesiac", diff --git a/lang/sl-SI.json b/lang/sl-SI.json index faffc4c1e..aae7888d8 100644 --- a/lang/sl-SI.json +++ b/lang/sl-SI.json @@ -37,7 +37,9 @@ "label.custom-range": "Razpon po meri", "label.dashboard": "Nadzorna plošča", "label.data": "Data", + "label.date": "Date", "label.date-range": "Časovni razpon", + "label.day": "Day", "label.default-date-range": "Privzeti časovni razpon", "label.delete": "Izbriši", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobilni telefon", "label.more": "Več", + "label.my-websites": "My websites", "label.name": "Ime", "label.new-password": "Novo geslo", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Ogledi strani", "label.pageTitle": "Page title", "label.pages": "Strani", @@ -117,6 +121,7 @@ "label.required": "Zahtevano", "label.reset": "Ponastavi", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Shrani", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Ta mesec", diff --git a/lang/sv-SE.json b/lang/sv-SE.json index b07bffc65..e6abb5bf7 100644 --- a/lang/sv-SE.json +++ b/lang/sv-SE.json @@ -37,7 +37,9 @@ "label.custom-range": "Anpassat urval", "label.dashboard": "Översikt", "label.data": "Data", + "label.date": "Date", "label.date-range": "Datumomfång", + "label.day": "Day", "label.default-date-range": "Standard datum-urval", "label.delete": "Radera", "label.delete-team": "Radera team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobil", "label.more": "Mer", + "label.my-websites": "My websites", "label.name": "Namn", "label.new-password": "Nytt lösenord", "label.none": "Inga", "label.os": "OS", "label.overview": "Overview", "label.owner": "Ägare", + "label.page-of": "Page {current} of {total}", "label.page-views": "Sidvisningar", "label.pageTitle": "Page title", "label.pages": "Sidor", @@ -117,6 +121,7 @@ "label.required": "Krävs", "label.reset": "Återställ", "label.reset-website": "Återställ statistik", + "label.retention": "Retention", "label.role": "Roll", "label.run-query": "Run query", "label.save": "Spara", @@ -133,7 +138,9 @@ "label.team-guest": "Team-gäst", "label.team-id": "Team ID", "label.team-member": "Team-medlem", + "label.team-name": "Team name", "label.team-owner": "Team-ägare", + "label.team-websites": "Team websites", "label.teams": "Team", "label.theme": "Tema", "label.this-month": "Denna månad", diff --git a/lang/ta-IN.json b/lang/ta-IN.json index 5a2d9458f..be3d5e815 100644 --- a/lang/ta-IN.json +++ b/lang/ta-IN.json @@ -37,7 +37,9 @@ "label.custom-range": "தனிப்பயன் வேறுபாட்டெல்லை", "label.dashboard": "முகப்பு", "label.data": "Data", + "label.date": "Date", "label.date-range": "தேதி வரம்பு", + "label.day": "Day", "label.default-date-range": "இயல்புநிலை தேதி வரம்பு", "label.delete": "அழி", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "கைபேசி", "label.more": "மேலும்", + "label.my-websites": "My websites", "label.name": "பெயர்", "label.new-password": "புதிய கடவுச்சொல்", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "பக்க காட்சிகள்", "label.pageTitle": "Page title", "label.pages": "பக்கங்கள்", @@ -117,6 +121,7 @@ "label.required": "தேவையானவை", "label.reset": "மீட்டமை", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "சேமி", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "இந்த மாதம்", diff --git a/lang/th-TH.json b/lang/th-TH.json index 949b79d3e..43f2f7587 100644 --- a/lang/th-TH.json +++ b/lang/th-TH.json @@ -37,7 +37,9 @@ "label.custom-range": "กำหนดช่วงเวลา", "label.dashboard": "แดชบอร์ด", "label.data": "Data", + "label.date": "Date", "label.date-range": "ตั้งแต่วันที่", + "label.day": "Day", "label.default-date-range": "ช่วงเวลา", "label.delete": "ลบ", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "โทรศัพท์มือถือ", "label.more": "เพิ่มเติม", + "label.my-websites": "My websites", "label.name": "ชื่อ", "label.new-password": "รหัสผ่านใหม่", "label.none": "ไม่ได้กำหนด", "label.os": "OS", "label.overview": "Overview", "label.owner": "เจ้าของ", + "label.page-of": "Page {current} of {total}", "label.page-views": "การเข้าชม", "label.pageTitle": "Page title", "label.pages": "หน้าเพจ", @@ -117,6 +121,7 @@ "label.required": "ต้องการ", "label.reset": "รีเซต", "label.reset-website": "รีเซตข้อมูลสถิติ", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "บันทึก", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "ธีม", "label.this-month": "เดือนปัจจุบัน", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index 3ec447220..0ec10e0b9 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -37,7 +37,9 @@ "label.custom-range": "Özelleştirilmiş aralık", "label.dashboard": "Kontrol Paneli", "label.data": "Data", + "label.date": "Date", "label.date-range": "Tarih aralığı", + "label.day": "Day", "label.default-date-range": "Varsayılan tarih aralığı", "label.delete": "Sil", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Mobil Cihaz", "label.more": "Detaylı göster", + "label.my-websites": "My websites", "label.name": "İsim", "label.new-password": "Yeni parola", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Owner", + "label.page-of": "Page {current} of {total}", "label.page-views": "Sayfa görünümü", "label.pageTitle": "Page title", "label.pages": "Sayfalar", @@ -117,6 +121,7 @@ "label.required": "Zorunlu alan", "label.reset": "Sıfırla", "label.reset-website": "Reset statistics", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Kaydet", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Bu ay", diff --git a/lang/uk-UA.json b/lang/uk-UA.json index 29a89c0ae..89079eff2 100644 --- a/lang/uk-UA.json +++ b/lang/uk-UA.json @@ -37,7 +37,9 @@ "label.custom-range": "Довільний період", "label.dashboard": "Інформаційна панель", "label.data": "Data", + "label.date": "Date", "label.date-range": "Діапазон дат", + "label.day": "Day", "label.default-date-range": "Діапазон дат за замовчуванням", "label.delete": "Видалити", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Мобільний", "label.more": "Більше", + "label.my-websites": "My websites", "label.name": "Ім'я", "label.new-password": "Новий пароль", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Власник", + "label.page-of": "Page {current} of {total}", "label.page-views": "Перегляди сторінок", "label.pageTitle": "Page title", "label.pages": "Сторінки", @@ -117,6 +121,7 @@ "label.required": "Обов'язкове", "label.reset": "Скинути", "label.reset-website": "Скинути статистику сайту", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Зберегти", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "Цього місяця", diff --git a/lang/ur-PK.json b/lang/ur-PK.json index 582ba2486..4d585dcbc 100644 --- a/lang/ur-PK.json +++ b/lang/ur-PK.json @@ -37,7 +37,9 @@ "label.custom-range": "اپنی مرضی کی حد", "label.dashboard": "ڈیش بورڈ", "label.data": "Data", + "label.date": "Date", "label.date-range": "تاریخ کی حد", + "label.day": "Day", "label.default-date-range": "پہلے سے طے شدہ تاریخ کی حد", "label.delete": "حذف کریں", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "موبائل", "label.more": "مزید", + "label.my-websites": "My websites", "label.name": "نام", "label.new-password": "نیا پاس ورڈ", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "مالک", + "label.page-of": "Page {current} of {total}", "label.page-views": "صفحہ کے نظارے", "label.pageTitle": "Page title", "label.pages": "صفحات", @@ -117,6 +121,7 @@ "label.required": "درکار ہے", "label.reset": "دوبارہ ترتیب دیں", "label.reset-website": "اعدادوشمار کو دوبارہ ترتیب دیں", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "محفوظ کریں", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "اس مہینے", diff --git a/lang/vi-VN.json b/lang/vi-VN.json index 46791971b..e9bce2d3c 100644 --- a/lang/vi-VN.json +++ b/lang/vi-VN.json @@ -37,7 +37,9 @@ "label.custom-range": "Phạm vi ngày tuỳ chọn", "label.dashboard": "Bảng điều khiển", "label.data": "Data", + "label.date": "Date", "label.date-range": "Phạm vi ngày", + "label.day": "Day", "label.default-date-range": "Khoảng thời gian mặc định", "label.delete": "Xoá", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "Di động", "label.more": "Thêm", + "label.my-websites": "My websites", "label.name": "Tên", "label.new-password": "Mật khẩu mới", "label.none": "None", "label.os": "OS", "label.overview": "Overview", "label.owner": "Chủ sở hữu", + "label.page-of": "Page {current} of {total}", "label.page-views": "Lượt xem", "label.pageTitle": "Page title", "label.pages": "Trang", @@ -117,6 +121,7 @@ "label.required": "Yêu cầu", "label.reset": "Tái thiết lập", "label.reset-website": "Tái thiết lập thống kê", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "Lưu", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "Giao diện", "label.this-month": "Tháng này", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index 274e1f693..f26b30518 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -37,7 +37,9 @@ "label.custom-range": "自定义时间段", "label.dashboard": "仪表板", "label.data": "统计数据", + "label.date": "Date", "label.date-range": "时间段", + "label.day": "Day", "label.default-date-range": "默认时间段", "label.delete": "删除", "label.delete-team": "删除团队", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "手机", "label.more": "更多", + "label.my-websites": "My websites", "label.name": "名字", "label.new-password": "新密码", "label.none": "无", "label.os": "OS", "label.overview": "Overview", "label.owner": "所有者", + "label.page-of": "Page {current} of {total}", "label.page-views": "页面浏览量", "label.pageTitle": "Page title", "label.pages": "网页", @@ -117,6 +121,7 @@ "label.required": "必填", "label.reset": "重置", "label.reset-website": "重置统计数据", + "label.retention": "Retention", "label.role": "角色", "label.run-query": "Run query", "label.save": "保存", @@ -133,7 +138,9 @@ "label.team-guest": "团队访客", "label.team-id": "团队 ID", "label.team-member": "团队成员", + "label.team-name": "Team name", "label.team-owner": "团队所有者", + "label.team-websites": "Team websites", "label.teams": "团队", "label.theme": "主题", "label.this-month": "本月", diff --git a/lang/zh-TW.json b/lang/zh-TW.json index 03fcd00b7..c5761150c 100644 --- a/lang/zh-TW.json +++ b/lang/zh-TW.json @@ -37,7 +37,9 @@ "label.custom-range": "自定義時段", "label.dashboard": "管理面板", "label.data": "Data", + "label.date": "Date", "label.date-range": "多日", + "label.day": "Day", "label.default-date-range": "默認日期範圍", "label.delete": "刪除", "label.delete-team": "Delete team", @@ -90,12 +92,14 @@ "label.min": "Min", "label.mobile": "手機", "label.more": "更多", + "label.my-websites": "My websites", "label.name": "名字", "label.new-password": "新密碼", "label.none": "無", "label.os": "OS", "label.overview": "Overview", "label.owner": "擁有者", + "label.page-of": "Page {current} of {total}", "label.page-views": "網頁流量", "label.pageTitle": "Page title", "label.pages": "網頁", @@ -117,6 +121,7 @@ "label.required": "必填", "label.reset": "重置", "label.reset-website": "重置統計數據", + "label.retention": "Retention", "label.role": "Role", "label.run-query": "Run query", "label.save": "保存", @@ -133,7 +138,9 @@ "label.team-guest": "Team guest", "label.team-id": "Team ID", "label.team-member": "Team member", + "label.team-name": "Team name", "label.team-owner": "Team owner", + "label.team-websites": "Team websites", "label.teams": "Teams", "label.theme": "主題", "label.this-month": "本月", diff --git a/public/intl/messages/am-ET.json b/public/intl/messages/am-ET.json index eec8866bb..f48fe83c4 100644 --- a/public/intl/messages/am-ET.json +++ b/public/intl/messages/am-ET.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Date range" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "More" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ar-SA.json b/public/intl/messages/ar-SA.json index 9b5119148..a9a12404e 100644 --- a/public/intl/messages/ar-SA.json +++ b/public/intl/messages/ar-SA.json @@ -227,12 +227,24 @@ "value": "البيانات" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "فترة مخصصة" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "المزيد" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "المالك" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "اعادة تعيين الإحصائيات" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "عضو المجموعة" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "مدير المجموعة" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/be-BY.json b/public/intl/messages/be-BY.json index a267a7224..4978aa45c 100644 --- a/public/intl/messages/be-BY.json +++ b/public/intl/messages/be-BY.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Дыяпазон дат" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Болей" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Уласнік" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Скінуць статыстыку" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/bn-BD.json b/public/intl/messages/bn-BD.json index c2b9dca83..938f6f98f 100644 --- a/public/intl/messages/bn-BD.json +++ b/public/intl/messages/bn-BD.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "তারিখের পরিসীমা" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "আরও" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "মালিক" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "ওয়েবসাইট রিসেট করুন" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ca-ES.json b/public/intl/messages/ca-ES.json index 91996324d..694b49c22 100644 --- a/public/intl/messages/ca-ES.json +++ b/public/intl/messages/ca-ES.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Interval de dates" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Més" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Propietari" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Restableix estadístiques" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/cs-CZ.json b/public/intl/messages/cs-CZ.json index a8469a04b..3fd34c31d 100644 --- a/public/intl/messages/cs-CZ.json +++ b/public/intl/messages/cs-CZ.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Období" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Více" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/da-DK.json b/public/intl/messages/da-DK.json index 325d1a65c..d8da1c3eb 100644 --- a/public/intl/messages/da-DK.json +++ b/public/intl/messages/da-DK.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Datointerval" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Mere" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Ejer" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Nulstil statistikker" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/de-CH.json b/public/intl/messages/de-CH.json index fc6fad599..aa0b2d942 100644 --- a/public/intl/messages/de-CH.json +++ b/public/intl/messages/de-CH.json @@ -227,12 +227,24 @@ "value": "Datä" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Datumsbereich" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Meh" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Bsitzer" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Statistik zruggsetze" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team Mitglied" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team Bsitzer" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/de-DE.json b/public/intl/messages/de-DE.json index 035f61d10..136cd31dd 100644 --- a/public/intl/messages/de-DE.json +++ b/public/intl/messages/de-DE.json @@ -227,12 +227,24 @@ "value": "Daten" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Datumsbereich" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Mehr" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Besitzer" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Statistik zurücksetzen" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team Mitglied" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team Eigentümer" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/el-GR.json b/public/intl/messages/el-GR.json index b1caa8178..d3ff5e424 100644 --- a/public/intl/messages/el-GR.json +++ b/public/intl/messages/el-GR.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Εύρος ημερομηνιών" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Περισσότερα" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/en-GB.json b/public/intl/messages/en-GB.json index 0894c6ec6..0e6ac6149 100644 --- a/public/intl/messages/en-GB.json +++ b/public/intl/messages/en-GB.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Date range" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "More" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index ca2a683fd..64a99ae1b 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Date range" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "More" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset website" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, @@ -1230,7 +1284,7 @@ "message.no-results-found": [ { "type": 0, - "value": "No results were found." + "value": "No results found." } ], "message.no-team-websites": [ diff --git a/public/intl/messages/es-ES.json b/public/intl/messages/es-ES.json index b01782de5..43e101709 100644 --- a/public/intl/messages/es-ES.json +++ b/public/intl/messages/es-ES.json @@ -227,12 +227,24 @@ "value": "Datos" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Intervalo de fechas" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Más" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Propietario" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reiniciar estadísticas" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Miembro del equipo" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Admin. del equipo" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/es-MX.json b/public/intl/messages/es-MX.json index 0b42ba04b..c238951fe 100644 --- a/public/intl/messages/es-MX.json +++ b/public/intl/messages/es-MX.json @@ -227,12 +227,24 @@ "value": "Datos" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Intervalo de fechas" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Más" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Propietario" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reiniciar estadísticas" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Miembro de equipo" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Admin. del equipo" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/fa-IR.json b/public/intl/messages/fa-IR.json index 4c52ec604..757b5ae8e 100644 --- a/public/intl/messages/fa-IR.json +++ b/public/intl/messages/fa-IR.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "محدوده‌ی تاریخ" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "بیشتر" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "ایجاد شده توسط" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "بازنشانی آمار" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/fi-FI.json b/public/intl/messages/fi-FI.json index 6cb32963e..5fdf5b193 100644 --- a/public/intl/messages/fi-FI.json +++ b/public/intl/messages/fi-FI.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Ajanjakso" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Lisää" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Omistaja" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Nollaa tilastot" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/fo-FO.json b/public/intl/messages/fo-FO.json index 4473b9990..3eb3f452c 100644 --- a/public/intl/messages/fo-FO.json +++ b/public/intl/messages/fo-FO.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Vel dato" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Meira" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/fr-FR.json b/public/intl/messages/fr-FR.json index a72e35edc..326c99a46 100644 --- a/public/intl/messages/fr-FR.json +++ b/public/intl/messages/fr-FR.json @@ -227,12 +227,24 @@ "value": "Données" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Période" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -553,6 +565,12 @@ "value": "Plus" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -589,6 +607,24 @@ "value": "Propriétaire" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -719,6 +755,12 @@ "value": "Réinitialiser les statistiques" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -815,12 +857,24 @@ "value": "Membre de l'équipe" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Propriétaire de l'équipe" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ga-ES.json b/public/intl/messages/ga-ES.json index 26dcf380b..d086b57f8 100644 --- a/public/intl/messages/ga-ES.json +++ b/public/intl/messages/ga-ES.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Rango temporal" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Máis" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Dona" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -735,6 +771,12 @@ "value": " in the box below to confirm." } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -831,12 +873,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/he-IL.json b/public/intl/messages/he-IL.json index e9d8425c7..dc206268c 100644 --- a/public/intl/messages/he-IL.json +++ b/public/intl/messages/he-IL.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "טווח תאריכים" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -553,6 +565,12 @@ "value": "עוד" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -589,6 +607,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -719,6 +755,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -815,12 +857,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/hi-IN.json b/public/intl/messages/hi-IN.json index afb246e38..91f1f0267 100644 --- a/public/intl/messages/hi-IN.json +++ b/public/intl/messages/hi-IN.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "तिथि सीमा" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "और" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/hr-HR.json b/public/intl/messages/hr-HR.json index 3e29f21ef..cd8d4d38b 100644 --- a/public/intl/messages/hr-HR.json +++ b/public/intl/messages/hr-HR.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Raspon datuma" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Više" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Vlasnik" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Resetirati web stranicu" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/hu-HU.json b/public/intl/messages/hu-HU.json index 2e6f72091..e39182b18 100644 --- a/public/intl/messages/hu-HU.json +++ b/public/intl/messages/hu-HU.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Időintervallum" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Bővebben" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/id-ID.json b/public/intl/messages/id-ID.json index aa76cb8b8..97526840b 100644 --- a/public/intl/messages/id-ID.json +++ b/public/intl/messages/id-ID.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Rentang tanggal" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -553,6 +565,12 @@ "value": "Lebih banyak" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -589,6 +607,24 @@ "value": "Pemilik" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -719,6 +755,12 @@ "value": "Atur ulang statistik" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -815,12 +857,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/it-IT.json b/public/intl/messages/it-IT.json index 6d67165e3..a93715d3d 100644 --- a/public/intl/messages/it-IT.json +++ b/public/intl/messages/it-IT.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Periodo" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Dettagli" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Proprietario" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Resetta le statistiche" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ja-JP.json b/public/intl/messages/ja-JP.json index dccd27f25..bde2f3a93 100644 --- a/public/intl/messages/ja-JP.json +++ b/public/intl/messages/ja-JP.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "範囲指定" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "さらに表示" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -731,6 +767,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -827,12 +869,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/km-KH.json b/public/intl/messages/km-KH.json index a7a57c0a3..1f7b82ca4 100644 --- a/public/intl/messages/km-KH.json +++ b/public/intl/messages/km-KH.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "ចន្លោះកាលបរិច្ឆេទ" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -553,6 +565,12 @@ "value": "បន្ថែម" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -589,6 +607,24 @@ "value": "ម្ចាស់" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -719,6 +755,12 @@ "value": "កំណត់ស្ថិតិឡើងវិញ" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -815,12 +857,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ko-KR.json b/public/intl/messages/ko-KR.json index 29d170c75..26413708b 100644 --- a/public/intl/messages/ko-KR.json +++ b/public/intl/messages/ko-KR.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "날짜 범위" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "더 보기" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -731,6 +767,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -827,12 +869,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/lt-LT.json b/public/intl/messages/lt-LT.json index af7aa81f6..21610b7bd 100644 --- a/public/intl/messages/lt-LT.json +++ b/public/intl/messages/lt-LT.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Laikotarpis" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -666,6 +678,12 @@ "value": "Daugiau" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -702,6 +720,24 @@ "value": "Savininkas" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -832,6 +868,12 @@ "value": "Atstatyti statistikos duomenis" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -928,12 +970,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json index 1ffc8ad15..013e5c88c 100644 --- a/public/intl/messages/mn-MN.json +++ b/public/intl/messages/mn-MN.json @@ -227,12 +227,24 @@ "value": "Өгөгдөл" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Хугацааны муж" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Цааш" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Эзэмшигч" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Тоон үзүүлэлтийг дахин эхлүүлэх" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Багийн гишүүн" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Багийн эзэмшигч" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ms-MY.json b/public/intl/messages/ms-MY.json index 4fb07f17e..e022e1227 100644 --- a/public/intl/messages/ms-MY.json +++ b/public/intl/messages/ms-MY.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Julat tarikh" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -553,6 +565,12 @@ "value": "Lebih banyak lagi" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -589,6 +607,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -719,6 +755,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -815,12 +857,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/nb-NO.json b/public/intl/messages/nb-NO.json index ed27041ac..82576ff89 100644 --- a/public/intl/messages/nb-NO.json +++ b/public/intl/messages/nb-NO.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Datointervall" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Mer" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Eier" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Nullstill statistikk" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/nl-NL.json b/public/intl/messages/nl-NL.json index cdee1071d..5ee25206b 100644 --- a/public/intl/messages/nl-NL.json +++ b/public/intl/messages/nl-NL.json @@ -227,12 +227,24 @@ "value": "Gegevens" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Datumbereik" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Toon meer" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Eigenaar" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Statistieken opnieuw instellen" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Teamlid" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Teameigenaar" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/pl-PL.json b/public/intl/messages/pl-PL.json index 8772081f9..6da1ff7ad 100644 --- a/public/intl/messages/pl-PL.json +++ b/public/intl/messages/pl-PL.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Zakres dat" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Więcej" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Właściciel" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Zresetuj statystyki" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Członek zespołu" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Właściciel zespołu" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/pt-BR.json b/public/intl/messages/pt-BR.json index 80f014cd7..ba508a504 100644 --- a/public/intl/messages/pt-BR.json +++ b/public/intl/messages/pt-BR.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Intervalo de datas" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Mais" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Proprietário" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Redefinir estatísticas" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Membro" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Proprietário" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/pt-PT.json b/public/intl/messages/pt-PT.json index 80a269295..a6431fb3b 100644 --- a/public/intl/messages/pt-PT.json +++ b/public/intl/messages/pt-PT.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Intervalo de datas" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Mais" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Proprietário" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Repor estatísticas" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ro-RO.json b/public/intl/messages/ro-RO.json index d3055f7da..1438ab413 100644 --- a/public/intl/messages/ro-RO.json +++ b/public/intl/messages/ro-RO.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Interval de date" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Mai mult" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Resetează statisticile pentru site" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ru-RU.json b/public/intl/messages/ru-RU.json index fdaeef7f1..b3213e679 100644 --- a/public/intl/messages/ru-RU.json +++ b/public/intl/messages/ru-RU.json @@ -227,12 +227,24 @@ "value": "Данные" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Диапазон дат" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Больше" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Владелец" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Сбросить статистику" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Член команды" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Владелец команды" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/si-LK.json b/public/intl/messages/si-LK.json index c43f8bf36..f4e5bca2f 100644 --- a/public/intl/messages/si-LK.json +++ b/public/intl/messages/si-LK.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "දින පරාසය" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "තවත්" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "හිමිකරු" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "සංඛ්යා ලේඛන නැවත සකසන්න" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/sk-SK.json b/public/intl/messages/sk-SK.json index d838c058f..b7e2914a9 100644 --- a/public/intl/messages/sk-SK.json +++ b/public/intl/messages/sk-SK.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Obdobie" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Viac" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/sl-SI.json b/public/intl/messages/sl-SI.json index 17a848819..a3af95cb8 100644 --- a/public/intl/messages/sl-SI.json +++ b/public/intl/messages/sl-SI.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Časovni razpon" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Več" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/sv-SE.json b/public/intl/messages/sv-SE.json index 8013bc70c..4a7f4130c 100644 --- a/public/intl/messages/sv-SE.json +++ b/public/intl/messages/sv-SE.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Datumomfång" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Mer" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Ägare" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Återställ statistik" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team-medlem" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team-ägare" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ta-IN.json b/public/intl/messages/ta-IN.json index feaa5b16a..90fb9ebf1 100644 --- a/public/intl/messages/ta-IN.json +++ b/public/intl/messages/ta-IN.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "தேதி வரம்பு" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "மேலும்" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/th-TH.json b/public/intl/messages/th-TH.json index 407ddfd38..c30a9d61f 100644 --- a/public/intl/messages/th-TH.json +++ b/public/intl/messages/th-TH.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "ตั้งแต่วันที่" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -553,6 +565,12 @@ "value": "เพิ่มเติม" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -589,6 +607,24 @@ "value": "เจ้าของ" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -719,6 +755,12 @@ "value": "รีเซตข้อมูลสถิติ" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -815,12 +857,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/tr-TR.json b/public/intl/messages/tr-TR.json index 52f68030a..138681adb 100644 --- a/public/intl/messages/tr-TR.json +++ b/public/intl/messages/tr-TR.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Tarih aralığı" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Detaylı göster" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Owner" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Reset statistics" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/uk-UA.json b/public/intl/messages/uk-UA.json index 6698e61cd..bdc2d345b 100644 --- a/public/intl/messages/uk-UA.json +++ b/public/intl/messages/uk-UA.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Діапазон дат" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "Більше" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "Власник" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "Скинути статистику сайту" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/ur-PK.json b/public/intl/messages/ur-PK.json index 317c7dc12..2005bc719 100644 --- a/public/intl/messages/ur-PK.json +++ b/public/intl/messages/ur-PK.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "تاریخ کی حد" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "مزید" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "مالک" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "اعدادوشمار کو دوبارہ ترتیب دیں" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/vi-VN.json b/public/intl/messages/vi-VN.json index 868f214a4..9fe0dd4ed 100644 --- a/public/intl/messages/vi-VN.json +++ b/public/intl/messages/vi-VN.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "Phạm vi ngày" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -553,6 +565,12 @@ "value": "Thêm" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -589,6 +607,24 @@ "value": "Chủ sở hữu" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -719,6 +755,12 @@ "value": "Tái thiết lập thống kê" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -815,12 +857,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index 43f24574d..666918f55 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -227,12 +227,24 @@ "value": "统计数据" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "时间段" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "更多" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "所有者" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -731,6 +767,12 @@ "value": "重置统计数据" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -827,12 +869,24 @@ "value": "团队成员" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "团队所有者" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, diff --git a/public/intl/messages/zh-TW.json b/public/intl/messages/zh-TW.json index e1b60217b..43a1996df 100644 --- a/public/intl/messages/zh-TW.json +++ b/public/intl/messages/zh-TW.json @@ -227,12 +227,24 @@ "value": "Data" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "多日" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -561,6 +573,12 @@ "value": "更多" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -597,6 +615,24 @@ "value": "擁有者" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, @@ -727,6 +763,12 @@ "value": "重置統計數據" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], "label.role": [ { "type": 0, @@ -823,12 +865,24 @@ "value": "Team member" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "Team owner" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, From 69389ebcd5e73d12ca2f395be4d2d30fc8c42fba Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 17 Aug 2023 16:39:59 -0700 Subject: [PATCH 079/357] Add team reports. --- pages/api/reports/index.ts | 1 + queries/admin/report.ts | 6 +++--- queries/admin/team.ts | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts index db83e6edc..762f297c7 100644 --- a/pages/api/reports/index.ts +++ b/pages/api/reports/index.ts @@ -37,6 +37,7 @@ export default async ( page, filter, pageSize: +pageSize || null, + includeTeams: true, }); return ok(res, data); diff --git a/queries/admin/report.ts b/queries/admin/report.ts index 22f3c62bc..a053ba92d 100644 --- a/queries/admin/report.ts +++ b/queries/admin/report.ts @@ -1,7 +1,7 @@ import { Prisma, Report } from '@prisma/client'; import { REPORT_FILTER_TYPES } from 'lib/constants'; import prisma from 'lib/prisma'; -import { FilterResult, ReportSearchFilter, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { FilterResult, ReportSearchFilter } from 'lib/types'; export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise { return prisma.client.report.create({ data }); @@ -155,7 +155,7 @@ export async function getReports( export async function getReportsByUserId( userId: string, - filter: SearchFilter, + filter: ReportSearchFilter, ): Promise> { return getReports( { userId, ...filter }, @@ -174,7 +174,7 @@ export async function getReportsByUserId( export async function getReportsByWebsiteId( websiteId: string, - filter: SearchFilter, + filter: ReportSearchFilter, ): Promise> { return getReports({ websiteId, ...filter }); } diff --git a/queries/admin/team.ts b/queries/admin/team.ts index 79735fc77..284b218e2 100644 --- a/queries/admin/team.ts +++ b/queries/admin/team.ts @@ -1,8 +1,8 @@ import { Prisma, Team } from '@prisma/client'; -import prisma from 'lib/prisma'; import { ROLES, TEAM_FILTER_TYPES } from 'lib/constants'; import { uuid } from 'lib/crypto'; -import { FilterResult, TeamSearchFilter, TeamSearchFilterType, SearchFilter } from 'lib/types'; +import prisma from 'lib/prisma'; +import { FilterResult, TeamSearchFilter } from 'lib/types'; export interface GetTeamOptions { includeTeamUser?: boolean; @@ -142,7 +142,7 @@ export async function getTeams( export async function getTeamsByUserId( userId: string, - filter?: SearchFilter, + filter?: TeamSearchFilter, ): Promise> { return getTeams( { userId, ...filter }, From 478235dde0f899559a7c93b775540be9368be800 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 17 Aug 2023 17:16:13 -0700 Subject: [PATCH 080/357] Updated messages. --- lang/zh-CN.json | 6 +-- public/intl/messages/zh-CN.json | 72 ++++++++++++++++----------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/lang/zh-CN.json b/lang/zh-CN.json index 2eb59de5b..91043dacc 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -66,7 +66,7 @@ "label.filter-combined": "合并", "label.filter-raw": "原始", "label.filters": "筛选", - "label.funnel": ""分析", + "label.funnel": "分析", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "见解", @@ -188,7 +188,7 @@ "message.incorrect-username-password": "用户名或密码不正确。", "message.invalid-domain": "无效域名", "message.min-password-length": "密码最短长度为 {n} 个字符", - "message.new-version-available": "Umami的新版本{version}已推出!" + "message.new-version-available": "Umami的新版本{version}已推出!", "message.no-data-available": "无可用数据。", "message.no-event-data": "无可用事件。", "message.no-match-password": "密码不一致", @@ -207,5 +207,5 @@ "message.team-websites-info": "团队中的任何人都可查看网站。", "message.tracking-code": "跟踪代码", "message.user-deleted": "用户已删除。", - "message.visitor-log": "来自{country}的访客在搭载 {os} 的{device}上使用 {browser} 浏览器进行访问。", + "message.visitor-log": "来自{country}的访客在搭载 {os} 的{device}上使用 {browser} 浏览器进行访问。" } diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index 666918f55..71234564d 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -44,7 +44,7 @@ "label.after": [ { "type": 0, - "value": "After" + "value": "之后" } ], "label.all": [ @@ -68,7 +68,7 @@ "label.average": [ { "type": 0, - "value": "Average" + "value": "平均" } ], "label.average-visit-time": [ @@ -86,7 +86,7 @@ "label.before": [ { "type": 0, - "value": "Before" + "value": "之前" } ], "label.bounce-rate": [ @@ -98,7 +98,7 @@ "label.breakdown": [ { "type": 0, - "value": "Breakdown" + "value": "故障" } ], "label.browser": [ @@ -158,7 +158,7 @@ "label.contains": [ { "type": 0, - "value": "Contains" + "value": "包含" } ], "label.continue": [ @@ -314,7 +314,7 @@ "label.does-not-contain": [ { "type": 0, - "value": "Does not contain" + "value": "不包含" } ], "label.domain": [ @@ -326,7 +326,7 @@ "label.dropoff": [ { "type": 0, - "value": "Dropoff" + "value": "丢弃" } ], "label.edit": [ @@ -350,13 +350,13 @@ "label.event": [ { "type": 0, - "value": "Event" + "value": "事件" } ], "label.event-data": [ { "type": 0, - "value": "Event data" + "value": "事件数据" } ], "label.events": [ @@ -368,7 +368,7 @@ "label.false": [ { "type": 0, - "value": "False" + "value": "否" } ], "label.field": [ @@ -398,13 +398,13 @@ "label.filters": [ { "type": 0, - "value": "Filters" + "value": "筛选" } ], "label.funnel": [ { "type": 0, - "value": "Funnel" + "value": "分析" } ], "label.greater-than": [ @@ -422,19 +422,19 @@ "label.insights": [ { "type": 0, - "value": "Insights" + "value": "见解" } ], "label.is": [ { "type": 0, - "value": "Is" + "value": "等于" } ], "label.is-not": [ { "type": 0, - "value": "Is not" + "value": "不等于" } ], "label.is-not-set": [ @@ -522,13 +522,13 @@ "label.less-than": [ { "type": 0, - "value": "Less than" + "value": "少于" } ], "label.less-than-equals": [ { "type": 0, - "value": "Less than or equals" + "value": "少于等于" } ], "label.login": [ @@ -546,7 +546,7 @@ "label.max": [ { "type": 0, - "value": "Max" + "value": "最大" } ], "label.members": [ @@ -558,7 +558,7 @@ "label.min": [ { "type": 0, - "value": "Min" + "value": "最小" } ], "label.mobile": [ @@ -606,7 +606,7 @@ "label.overview": [ { "type": 0, - "value": "Overview" + "value": "概览" } ], "label.owner": [ @@ -686,7 +686,7 @@ "label.query": [ { "type": 0, - "value": "Query" + "value": "查询" } ], "label.query-parameters": [ @@ -746,7 +746,7 @@ "label.reports": [ { "type": 0, - "value": "Reports" + "value": "报告" } ], "label.required": [ @@ -782,7 +782,7 @@ "label.run-query": [ { "type": 0, - "value": "Run query" + "value": "查询" } ], "label.save": [ @@ -800,7 +800,7 @@ "label.select-date": [ { "type": 0, - "value": "Select date" + "value": "选择数据" } ], "label.select-website": [ @@ -836,7 +836,7 @@ "label.sum": [ { "type": 0, - "value": "Sum" + "value": "总和" } ], "label.tablet": [ @@ -944,13 +944,13 @@ "label.total": [ { "type": 0, - "value": "Total" + "value": "总数" } ], "label.total-records": [ { "type": 0, - "value": "Total records" + "value": "总记录数" } ], "label.tracking-code": [ @@ -962,13 +962,13 @@ "label.true": [ { "type": 0, - "value": "True" + "value": "是" } ], "label.type": [ { "type": 0, - "value": "Type" + "value": "类型" } ], "label.unique": [ @@ -1028,7 +1028,7 @@ "label.value": [ { "type": 0, - "value": "Value" + "value": "值" } ], "label.view": [ @@ -1064,7 +1064,7 @@ "label.website": [ { "type": 0, - "value": "Website" + "value": "网站" } ], "label.website-id": [ @@ -1236,7 +1236,7 @@ "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "Umami的新版本" }, { "type": 1, @@ -1244,7 +1244,7 @@ }, { "type": 0, - "value": " is available!" + "value": "已推出!" } ], "message.no-data-available": [ @@ -1256,7 +1256,7 @@ "message.no-event-data": [ { "type": 0, - "value": "No event data is available." + "value": "无可用事件。" } ], "message.no-match-password": [ @@ -1304,7 +1304,7 @@ "message.reset-website": [ { "type": 0, - "value": "确定重置该网站, 请在下面的输入框中输入 " + "value": "如果确定重置该网站, 请在下面的输入框中输入 " }, { "type": 1, @@ -1368,7 +1368,7 @@ "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "用户已删除。" } ], "message.visitor-log": [ From 1e7681fb156749a6a36b798264acc33c30237da6 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 17 Aug 2023 17:28:05 -0700 Subject: [PATCH 081/357] Bump version v2.5.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d2de0936..d529ba771 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.4.1", + "version": "2.5.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", From e4828604fa2f189cd6354d052243658c69b4181f Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 17 Aug 2023 18:20:38 -0700 Subject: [PATCH 082/357] Updated my-MY lang files. --- public/intl/country/my-MM.json | 251 ++++++ public/intl/language/my-MM.json | 611 +++++++++++++++ public/intl/messages/my-MM.json | 1296 +++++++++++++++++++++++++++++++ 3 files changed, 2158 insertions(+) create mode 100644 public/intl/country/my-MM.json create mode 100644 public/intl/language/my-MM.json create mode 100644 public/intl/messages/my-MM.json diff --git a/public/intl/country/my-MM.json b/public/intl/country/my-MM.json new file mode 100644 index 000000000..e233349e2 --- /dev/null +++ b/public/intl/country/my-MM.json @@ -0,0 +1,251 @@ +{ + "CA": "\u1000\u1014\u1031\u1012\u102b", + "KG": "\u1000\u102c\u1002\u103b\u1005\u1039\u1005\u1010\u1014\u103a", + "KZ": "\u1000\u102c\u1007\u1000\u103a\u1005\u1010\u1014\u103a", + "QA": "\u1000\u102c\u1010\u102c", + "BQ": "\u1000\u102c\u101b\u1005\u103a\u1018\u102e\u101a\u1036 \u1014\u101a\u103a\u101e\u102c\u101c\u1014\u103a", + "KW": "\u1000\u1030\u101d\u102d\u1010\u103a", + "KY": "\u1000\u1031\u1019\u1014\u103a \u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "CC": "\u1000\u102d\u102f\u1000\u102d\u102f\u1038\u1000\u103b\u103d\u1014\u103a\u1038", + "KM": "\u1000\u102d\u102f\u1019\u102d\u102f\u101b\u102d\u102f\u1005\u103a", + "CO": "\u1000\u102d\u102f\u101c\u1036\u1018\u102e\u101a\u102c", + "CR": "\u1000\u102d\u102f\u1037\u1005\u103a\u1010\u102c\u101b\u102e\u1000\u102c", + "CI": "\u1000\u102d\u102f\u1037\u1010\u103a \u1012\u102e\u1017\u103d\u102c", + "KE": "\u1000\u1004\u103a\u100a\u102c", + "CM": "\u1000\u1004\u103a\u1019\u101b\u103d\u1014\u103a\u1038", + "CV": "\u1000\u102d\u1010\u103a\u1017\u102c\u1012\u102e", + "KH": "\u1000\u1019\u1039\u1018\u1031\u102c\u1012\u102e\u1038\u101a\u102c\u1038", + "CW": "\u1000\u103b\u1030\u101b\u1031\u1038\u1000\u102d\u102f\u1038\u1005\u103a", + "CU": "\u1000\u103b\u1030\u1038\u1018\u102c\u1038", + "CK": "\u1000\u103d\u1010\u103a \u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "CD": "\u1000\u103d\u1014\u103a\u1002\u102d\u102f", + "CG": "\u1000\u103d\u1014\u103a\u1002\u102d\u102f-\u1018\u101b\u102c\u1007\u102c\u1017\u102e\u1038\u101c\u103a", + "HR": "\u1001\u101b\u102d\u102f\u1021\u1031\u1038\u101b\u103e\u102c\u1038", + "CX": "\u1001\u101b\u1005\u103a\u1005\u1019\u1010\u103a \u1000\u103b\u103d\u1014\u103a\u1038", + "KI": "\u1001\u102e\u101b\u102e\u1018\u102c\u1010\u102e", + "CL": "\u1001\u103b\u102e\u101c\u102e", + "CZ": "\u1001\u103b\u1000\u103a\u1000\u102e\u101a\u102c\u1038", + "TD": "\u1001\u103b\u1012\u103a", + "GR": "\u1002\u101b\u102d", + "GD": "\u1002\u101b\u102e\u1014\u1031\u1012\u102b", + "GL": "\u1002\u101b\u1004\u103a\u1038\u101c\u1014\u103a\u1038", + "GH": "\u1002\u102b\u1014\u102c", + "GA": "\u1002\u102b\u1018\u103d\u1014\u103a", + "GN": "\u1002\u102e\u1014\u102e", + "GW": "\u1002\u102e\u1014\u102e-\u1018\u102e\u1005\u1031\u102c", + "GU": "\u1002\u1030\u1021\u1019\u103a", + "GY": "\u1002\u102d\u102f\u1004\u103a\u101a\u102c\u1014\u102c", + "GM": "\u1002\u1019\u103a\u1018\u102e\u101b\u102c", + "JP": "\u1002\u103b\u1015\u1014\u103a", + "JM": "\u1002\u103b\u1019\u1031\u1000\u102c", + "JE": "\u1002\u103b\u102c\u1005\u102e", + "DE": "\u1002\u103b\u102c\u1019\u1014\u102e", + "GI": "\u1002\u103b\u102e\u1018\u101b\u1031\u102c\u103a\u101c\u103a\u1010\u102c", + "DJ": "\u1002\u103b\u102e\u1018\u1030\u1010\u102e", + "GE": "\u1002\u103b\u1031\u102c\u103a\u1002\u103b\u102e\u101a\u102c", + "JO": "\u1002\u103b\u1031\u102c\u103a\u1012\u1014\u103a", + "GT": "\u1002\u103d\u102b\u1010\u102e\u1019\u102c\u101c\u102c", + "GP": "\u1002\u103d\u102b\u1012\u102e\u101c\u102f", + "GG": "\u1002\u103d\u1014\u103a\u1038\u1007\u102e", + "ES": "\u1005\u1015\u102d\u1014\u103a", + "SJ": "\u1005\u1017\u102d\u102f\u101c\u103a\u1018\u1010\u103a\u1014\u103e\u1004\u1037\u103a\u1002\u103b\u1014\u103a\u1019\u1031\u101b\u1014\u103a", + "KN": "\u1005\u102d\u1014\u1037\u103a\u1000\u1005\u103a\u1014\u103e\u1004\u1037\u103a\u1014\u102e\u1017\u102e\u1005\u103a", + "PM": "\u1005\u102d\u1014\u1037\u103a\u1015\u102e\u1021\u1032\u101b\u103a\u1014\u103e\u1004\u1037\u103a \u1019\u102e\u1000\u103d\u102e\u101c\u103d\u1014\u103a", + "VC": "\u1005\u102d\u1014\u1037\u103a\u1017\u1004\u103a\u1038\u1006\u1004\u1037\u103a\u1014\u103e\u1004\u1037\u103a \u1002\u101b\u102d\u1014\u1031\u1012\u102d\u102f\u1004\u103a", + "BL": "\u1005\u102d\u1014\u1037\u103a\u1018\u102c\u101e\u101a\u103a\u101c\u103a\u1019\u102e", + "MF": "\u1005\u102d\u1014\u1037\u103a\u1019\u102c\u1010\u1004\u103a", + "LC": "\u1005\u102d\u1014\u1037\u103a\u101c\u1030\u1005\u102e\u101a\u102c", + "SH": "\u1005\u102d\u1014\u1037\u103a\u101f\u101a\u103a\u101c\u101a\u103a\u1014\u102c", + "SG": "\u1005\u1004\u103a\u1039\u1000\u102c\u1015\u1030", + "SX": "\u1005\u1004\u1037\u103a\u1019\u102c\u1010\u1004\u103a", + "WS": "\u1006\u1019\u102d\u102f\u1038\u1021\u102c\u1038", + "SI": "\u1006\u101c\u102d\u102f\u1017\u1031\u1038\u1014\u102e\u1038\u101a\u102c\u1038", + "SK": "\u1006\u101c\u102d\u102f\u1017\u1000\u103a\u1000\u102e\u1038\u101a\u102c\u1038", + "RS": "\u1006\u102c\u1038\u1018\u102e\u1038\u101a\u102c\u1038", + "SN": "\u1006\u102e\u1014\u102e\u1002\u1031\u102b", + "SL": "\u1006\u102e\u101a\u102c\u101b\u102c \u101c\u102e\u101a\u103d\u1014\u103a\u1038", + "SY": "\u1006\u102e\u1038\u101b\u102e\u1038\u101a\u102c\u1038", + "SD": "\u1006\u1030\u1012\u1014\u103a", + "SR": "\u1006\u1030\u101b\u102c\u1014\u1019\u103a", + "SC": "\u1006\u1031\u1038\u101b\u103e\u1032", + "SA": "\u1006\u1031\u102c\u103a\u1012\u102e\u1021\u102c\u101b\u1031\u1038\u1018\u102e\u1038\u101a\u102c\u1038", + "SB": "\u1006\u1031\u102c\u103a\u101c\u1019\u103d\u1014\u103a\u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "SO": "\u1006\u102d\u102f\u1019\u102c\u101c\u102e\u101a\u102c", + "CY": "\u1006\u102d\u102f\u1000\u103a\u1015\u101b\u1015\u103a\u1005\u103a", + "ST": "\u1006\u1031\u102c\u1004\u103a\u1010\u1030\u1019\u1031\u1038\u1014\u103e\u1004\u1037\u103a \u1015\u101b\u1004\u103a\u1005\u102e\u1015\u102e", + "SM": "\u1006\u1014\u103a\u1019\u102c\u101b\u102e\u1014\u102d\u102f", + "SZ": "\u1006\u103d\u102c\u1007\u102e\u101c\u1014\u103a", + "SE": "\u1006\u103d\u102e\u1012\u1004\u103a", + "CH": "\u1006\u103d\u1005\u103a\u1007\u102c\u101c\u1014\u103a", + "ZW": "\u1007\u1004\u103a\u1018\u102c\u1018\u103d\u1031", + "ZM": "\u1007\u1019\u103a\u1018\u102e\u101a\u102c", + "CN": "\u1010\u101b\u102f\u1010\u103a", + "TJ": "\u1010\u102c\u1002\u103b\u102e\u1000\u1005\u1039\u1005\u1010\u1014\u103a", + "TM": "\u1010\u102c\u1037\u1001\u103a\u1019\u1004\u103a\u1014\u1005\u1039\u1005\u1010\u1014\u103a", + "TN": "\u1010\u1030\u1014\u102e\u1038\u101b\u103e\u102c\u1038", + "TV": "\u1010\u1030\u1017\u102c\u1038\u101c\u1030", + "TR": "\u1010\u1030\u101b\u1000\u102e", + "TK": "\u1010\u102d\u102f\u1000\u101c\u1031\u102c\u1004\u103a", + "TG": "\u1010\u102d\u102f\u1002\u102d\u102f", + "TC": "\u1010\u1001\u103a\u1005\u103a\u1014\u103e\u1004\u1037\u103a\u1000\u102c\u1021\u102e\u1000\u102d\u102f\u1005\u103a\u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "GS": "\u1010\u1031\u102c\u1004\u103a \u1002\u103b\u1031\u102c\u103a\u1002\u103b\u102e\u101a\u102c \u1014\u103e\u1004\u1037\u103a \u1010\u1031\u102c\u1004\u103a \u1006\u1004\u103a\u1038\u1012\u101d\u1005\u103a\u1002\u103b\u103a \u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f\u1019\u103b\u102c\u1038", + "SS": "\u1010\u1031\u102c\u1004\u103a \u1006\u1030\u1012\u1014\u103a", + "KR": "\u1010\u1031\u102c\u1004\u103a\u1000\u102d\u102f\u101b\u102e\u1038\u101a\u102c\u1038", + "ZA": "\u1010\u1031\u102c\u1004\u103a\u1021\u102c\u1016\u101b\u102d\u1000", + "TZ": "\u1010\u1014\u103a\u1007\u1014\u103a\u1038\u1014\u102e\u1038\u101a\u102c\u1038", + "TO": "\u1010\u103d\u1014\u103a\u1002\u102b", + "TT": "\u1011\u101b\u102e\u1014\u102e\u1012\u1010\u103a\u1014\u103e\u1004\u1037\u103a \u1010\u102d\u102f\u1018\u1000\u103a\u1002\u102d\u102f", + "TW": "\u1011\u102d\u102f\u1004\u103a\u101d\u1019\u103a", + "TH": "\u1011\u102d\u102f\u1004\u103a\u1038", + "DM": "\u1012\u102d\u102f\u1019\u102e\u1014\u102e\u1000\u102c", + "DO": "\u1012\u102d\u102f\u1019\u102e\u1014\u102e\u1000\u1014\u103a", + "DK": "\u1012\u102d\u1014\u103a\u1038\u1019\u1010\u103a", + "NA": "\u1014\u1019\u102e\u1038\u1018\u102e\u1038\u101a\u102c\u1038", + "NC": "\u1014\u101a\u1030\u1038 \u1000\u101a\u103a\u101c\u102e\u1012\u102d\u102f\u1014\u102e\u1038\u101a\u102c\u1038", + "NZ": "\u1014\u101a\u1030\u1038\u1007\u102e\u101c\u1014\u103a", + "NI": "\u1014\u102e\u1000\u102c\u101b\u102c\u1002\u103d\u102b", + "NP": "\u1014\u102e\u1015\u1031\u102b", + "NU": "\u1014\u102e\u1025\u1030\u1021\u1031", + "NF": "\u1014\u1031\u102c\u1016\u102f\u1010\u103a\u1000\u103b\u103d\u1014\u103a\u1038", + "NR": "\u1014\u1031\u102c\u103a\u101b\u1030\u1038", + "NO": "\u1014\u1031\u102c\u103a\u101d\u1031", + "NE": "\u1014\u102d\u102f\u1004\u103a\u1002\u103b\u102c", + "NG": "\u1014\u102d\u102f\u1004\u103a\u1002\u103b\u102e\u1038\u101b\u102e\u1038\u101a\u102c\u1038", + "NL": "\u1014\u101a\u103a\u101e\u102c\u101c\u1014\u103a", + "PA": "\u1015\u1014\u102c\u1038\u1019\u102c\u1038", + "PW": "\u1015\u101c\u102c\u1021\u102d\u102f", + "PK": "\u1015\u102b\u1000\u1005\u1039\u1005\u1010\u1014\u103a", + "PG": "\u1015\u102b\u1015\u1030\u1021\u102c \u1014\u101a\u1030\u1038\u1002\u102e\u1014\u102e", + "PY": "\u1015\u102b\u101b\u102c\u1002\u103d\u1031\u1038", + "PS": "\u1015\u102b\u101c\u1000\u103a\u1005\u1010\u102d\u102f\u1004\u103a\u1038 \u1015\u102d\u102f\u1004\u103a\u1014\u1000\u103a", + "PE": "\u1015\u102e\u101b\u1030\u1038", + "PT": "\u1015\u1031\u102b\u103a\u1010\u1030\u1002\u102e", + "PR": "\u1015\u1031\u102b\u103a\u1010\u102d\u102f\u101b\u102e\u1000\u102d\u102f", + "PL": "\u1015\u102d\u102f\u101c\u1014\u103a", + "PN": "\u1015\u1005\u103a\u1010\u103a\u1000\u102d\u1014\u103a\u1038\u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "FR": "\u1015\u103c\u1004\u103a\u101e\u1005\u103a", + "GF": "\u1015\u103c\u1004\u103a\u101e\u1005\u103a \u1002\u102e\u101a\u102c\u1014\u102c", + "TF": "\u1015\u103c\u1004\u103a\u101e\u1005\u103a \u1010\u1031\u102c\u1004\u103a\u1015\u102d\u102f\u1004\u103a\u1038 \u1015\u102d\u102f\u1004\u103a\u1014\u1000\u103a\u1019\u103b\u102c\u1038", + "PF": "\u1015\u103c\u1004\u103a\u101e\u1005\u103a \u1015\u1031\u102b\u103a\u101c\u102e\u1014\u102e\u1038\u101b\u103e\u102c\u1038", + "FO": "\u1016\u102c\u101b\u102d\u102f\u1038 \u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f\u1019\u103b\u102c\u1038", + "PH": "\u1016\u102d\u101c\u1005\u103a\u1015\u102d\u102f\u1004\u103a", + "FJ": "\u1016\u102e\u1002\u103b\u102e", + "FK": "\u1016\u1031\u102c\u1037\u1000\u101c\u1014\u103a \u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "FI": "\u1016\u1004\u103a\u101c\u1014\u103a", + "VU": "\u1017\u1014\u103d\u102c\u1038\u1010\u1030", + "CF": "\u1017\u101f\u102d\u102f \u1021\u102c\u1016\u101b\u102d\u1000 \u1015\u103c\u100a\u103a\u1011\u1031\u102c\u1004\u103a\u1005\u102f", + "VA": "\u1017\u102c\u1010\u102e\u1000\u1014\u103a\u1005\u102e\u1038\u1010\u102e\u1038", + "VN": "\u1017\u102e\u101a\u1000\u103a\u1014\u1019\u103a", + "VE": "\u1017\u1004\u103a\u1014\u102e\u1007\u103d\u1032\u101c\u102c\u1038", + "VG": "\u1017\u103c\u102d\u1010\u102d\u101e\u103b\u103e \u1017\u102c\u1002\u103b\u1004\u103a\u1038 \u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "IO": "\u1017\u103c\u102d\u1010\u102d\u101e\u103b\u103e\u1015\u102d\u102f\u1004\u103a \u1021\u102d\u1014\u1039\u1012\u102d\u101a\u101e\u1019\u102f\u1012\u1039\u1012\u101b\u102c\u1000\u103b\u103d\u1014\u103a\u1038\u1019\u103b\u102c\u1038", + "BR": "\u1018\u101b\u102c\u1007\u102e\u1038", + "BN": "\u1018\u101b\u1030\u1014\u102d\u102f\u1004\u103a\u1038", + "BZ": "\u1018\u101c\u102d\u1007\u103a", + "BS": "\u1018\u101f\u102c\u1038\u1019\u102c\u1038", + "BF": "\u1018\u102c\u1000\u102e\u1038\u1014\u102c\u1038 \u1016\u102c\u1038\u1006\u102d\u102f", + "BB": "\u1018\u102c\u1018\u1031\u1038\u1012\u102d\u102f\u1038\u1005\u103a", + "BM": "\u1018\u102c\u1019\u103c\u1030\u1012\u102b", + "BH": "\u1018\u102c\u101b\u102d\u1014\u103a\u1038", + "BJ": "\u1018\u102e\u1014\u1004\u103a", + "BY": "\u1018\u102e\u101c\u102c\u101b\u102f\u1005\u103a", + "BT": "\u1018\u1030\u1010\u1014\u103a", + "BV": "\u1018\u1030\u1017\u1000\u103a\u1000\u103b\u103d\u1014\u103a\u1038", + "BI": "\u1018\u1030\u101b\u103d\u1014\u103a\u1012\u102e", + "BG": "\u1018\u1030\u101c\u103a\u1002\u1031\u1038\u101b\u102e\u1038\u101a\u102c\u1038", + "BA": "\u1018\u1031\u102c\u1037\u1005\u1014\u102e\u1038\u101a\u102c\u1038\u1014\u103e\u1004\u1037\u103a \u101f\u102c\u1007\u102e\u1002\u102d\u102f\u1017\u102e\u1014\u102c\u1038", + "BW": "\u1018\u1031\u102c\u1037\u1006\u103d\u102c\u1014\u102c", + "BO": "\u1018\u102d\u102f\u101c\u102e\u1038\u1017\u102e\u1038\u101a\u102c\u1038", + "BD": "\u1018\u1004\u103a\u1039\u1002\u101c\u102c\u1038\u1012\u1031\u1037\u101b\u103e\u103a", + "BE": "\u1018\u101a\u103a\u101c\u103a\u1002\u103b\u102e\u101a\u1019\u103a", + "MO": "\u1019\u1000\u102c\u1021\u102d\u102f (\u1010\u101b\u102f\u1010\u103a\u1015\u103c\u100a\u103a)", + "MG": "\u1019\u1012\u102b\u1002\u1010\u103a\u1005\u1000\u102c\u1038", + "MY": "\u1019\u101c\u1031\u1038\u101b\u103e\u102c\u1038", + "MQ": "\u1019\u102c\u1010\u102e\u1014\u102d\u1001\u103a", + "MH": "\u1019\u102c\u101b\u103e\u101a\u103a \u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "MW": "\u1019\u102c\u101c\u102c\u101d\u102e", + "ML": "\u1019\u102c\u101c\u102e", + "YT": "\u1019\u1031\u101a\u1031\u102c\u1037", + "MU": "\u1019\u1031\u102c\u101b\u1005\u103a\u101b\u103e", + "MS": "\u1019\u1031\u102c\u1004\u1037\u103a\u1005\u1032\u101b\u1000\u103a", + "MR": "\u1019\u1031\u102c\u103a\u101b\u102e\u1010\u1031\u1038\u1014\u102e\u1038\u101a\u102c\u1038", + "MA": "\u1019\u1031\u102c\u103a\u101b\u102d\u102f\u1000\u102d\u102f", + "MV": "\u1019\u1031\u102c\u103a\u101c\u103a\u1012\u102d\u102f\u1000\u103a", + "MZ": "\u1019\u102d\u102f\u1007\u1019\u103a\u1018\u1005\u103a", + "MC": "\u1019\u102d\u102f\u1014\u102c\u1000\u102d\u102f", + "MX": "\u1019\u1000\u103a\u1000\u1006\u102e\u1000\u102d\u102f", + "FM": "\u1019\u102d\u102f\u1004\u103a\u1001\u101b\u102d\u102f\u1014\u102e\u101b\u103e\u102c\u1038", + "IM": "\u1019\u1014\u103a\u1000\u103b\u103d\u1014\u103a\u1038", + "MT": "\u1019\u1031\u102c\u101c\u103a\u1010\u102c", + "MD": "\u1019\u1031\u102c\u101c\u103a\u1012\u102d\u102f\u1017\u102c", + "MK": "\u1019\u103c\u1031\u102c\u1000\u103a \u1019\u1000\u103a\u1006\u102e\u1012\u102d\u102f\u1038\u1014\u102e\u1038\u101a\u102c\u1038", + "KP": "\u1019\u103c\u1031\u102c\u1000\u103a\u1000\u102d\u102f\u101b\u102e\u1038\u101a\u102c\u1038", + "MP": "\u1019\u103c\u1031\u102c\u1000\u103a\u1015\u102d\u102f\u1004\u103a\u1038\u1019\u102c\u101b\u102e\u1021\u102c\u1014\u102c\u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "MM": "\u1019\u103c\u1014\u103a\u1019\u102c", + "MN": "\u1019\u103d\u1014\u103a\u1002\u102d\u102f\u1038\u101c\u102e\u1038\u101a\u102c\u1038", + "ME": "\u1019\u103d\u1014\u103a\u1010\u102e\u1014\u102d\u1002\u101b\u102d\u102f\u1038", + "YE": "\u101a\u102e\u1019\u1004\u103a", + "UA": "\u101a\u1030\u1000\u101b\u102d\u1014\u103a\u1038", + "UG": "\u101a\u1030\u1002\u1014\u103a\u1038\u1012\u102b\u1038", + "GB": "\u101a\u1030\u1014\u102d\u102f\u1000\u103a\u1010\u1000\u103a\u1000\u1004\u103a\u1038\u1012\u1019\u103a\u1038", + "UM": "\u101a\u1030\u1014\u102d\u102f\u1000\u103a\u1010\u1000\u103a\u1005\u1010\u102d\u1010\u103a \u1000\u103b\u103d\u1014\u103a\u1038\u1014\u102d\u102f\u1004\u103a\u1004\u1036\u1019\u103b\u102c\u1038", + "AE": "\u101a\u1030\u1021\u1031\u1021\u102e\u1038", + "VI": "\u101a\u1030\u1021\u1000\u103a\u1005\u103a \u1017\u102c\u1002\u103b\u1004\u103a\u1038 \u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "RW": "\u101b\u101d\u1014\u103a\u1012\u102b", + "RE": "\u101b\u102e\u101a\u1030\u1014\u102e\u101a\u1014\u103a", + "RU": "\u101b\u102f\u101b\u103e\u102c\u1038", + "RO": "\u101b\u102d\u102f\u1019\u1031\u1038\u1014\u102e\u1038\u101a\u102c\u1038", + "LA": "\u101c\u102c\u1021\u102d\u102f", + "LS": "\u101c\u102e\u1006\u102d\u102f\u101e\u102d\u102f", + "LU": "\u101c\u1030\u1007\u1004\u103a\u1018\u1010\u103a", + "LB": "\u101c\u1000\u103a\u1018\u1014\u103d\u1014\u103a", + "LR": "\u101c\u102d\u102f\u1000\u103a\u1018\u1031\u1038\u101b\u102e\u1038\u101a\u102c\u1038", + "LI": "\u101c\u1005\u103a\u1010\u1014\u103a\u1005\u1010\u102d\u1014\u103a\u1038", + "LY": "\u101c\u1005\u103a\u1017\u103b\u102c\u1038", + "LT": "\u101c\u1005\u103a\u101e\u1030\u101a\u1031\u1038\u1014\u102e\u1038\u101a\u102c\u1038", + "LV": "\u101c\u1010\u103a\u1017\u102e\u1038\u101a\u102c\u1038", + "WF": "\u101d\u1031\u102b\u101c\u1005\u103a\u1014\u103e\u1004\u1037\u103a \u1016\u1030\u1000\u103b\u1030\u1038\u1014\u102c\u1038", + "LK": "\u101e\u102e\u101b\u102d\u101c\u1004\u103a\u1039\u1000\u102c", + "HT": "\u101f\u1031\u1010\u102e", + "HK": "\u101f\u1031\u102c\u1004\u103a\u1000\u1031\u102c\u1004\u103a (\u1010\u101b\u102f\u1010\u103a\u1015\u103c\u100a\u103a)", + "HM": "\u101f\u1010\u103a\u1000\u103b\u103d\u1014\u103a\u1038\u1014\u103e\u1004\u1037\u103a\u1019\u1000\u103a\u1012\u1031\u102b\u1014\u101a\u103a\u1000\u103b\u103d\u1014\u103a\u1038\u1005\u102f", + "HU": "\u101f\u1014\u103a\u1002\u1031\u101b\u102e", + "HN": "\u101f\u103d\u1014\u103a\u1012\u1030\u1038\u101b\u1015\u103a\u1005\u103a", + "AZ": "\u1021\u1007\u102c\u1018\u102d\u102f\u1004\u103a\u1002\u103b\u1014\u103a", + "EH": "\u1021\u1014\u1031\u102c\u1000\u103a \u1006\u102c\u101f\u102c\u101b", + "AS": "\u1021\u1019\u1031\u101b\u102d\u1000\u1014\u103a \u1006\u1019\u102d\u102f\u1038\u1021\u102c\u1038", + "US": "\u1021\u1019\u1031\u101b\u102d\u1000\u1014\u103a \u1015\u103c\u100a\u103a\u1011\u1031\u102c\u1004\u103a\u1005\u102f", + "TL": "\u1021\u101b\u103e\u1031\u1037\u1010\u102e\u1019\u1031\u102c", + "AR": "\u1021\u102c\u1002\u103b\u1004\u103a\u1010\u102e\u1038\u1014\u102c\u1038", + "AF": "\u1021\u102c\u1016\u1002\u1014\u103a\u1014\u1005\u1039\u1005\u1010\u1014\u103a", + "AM": "\u1021\u102c\u1019\u1031\u1038\u1014\u102e\u1038\u101a\u102c\u1038", + "AW": "\u1021\u102c\u101b\u1030\u1038\u1017\u102c\u1038", + "AX": "\u1021\u102c\u101c\u1014\u103a\u1000\u103b\u103d\u1014\u103a\u1038", + "GQ": "\u1021\u102e\u1000\u103d\u1031\u1010\u102c \u1002\u102e\u1014\u102e", + "EC": "\u1021\u102e\u1000\u103d\u1031\u1012\u1031\u102b", + "EG": "\u1021\u102e\u1002\u103b\u1005\u103a", + "IT": "\u1021\u102e\u1010\u101c\u102e", + "ER": "\u1021\u102e\u101b\u102e\u1011\u101b\u102e\u1038\u101a\u102c\u1038", + "IQ": "\u1021\u102e\u101b\u1010\u103a", + "IR": "\u1021\u102e\u101b\u1014\u103a", + "ET": "\u1021\u102e\u101e\u102e\u101a\u102d\u102f\u1038\u1015\u102e\u1038\u101a\u102c\u1038", + "UZ": "\u1025\u1007\u1018\u1000\u103a\u1000\u1005\u1039\u1005\u1010\u1014\u103a", + "UY": "\u1025\u101b\u102f\u1002\u103d\u1031\u1038", + "AT": "\u1029\u1005\u1010\u103c\u102e\u1038\u101a\u102c\u1038", + "AU": "\u1029\u1005\u1010\u103c\u1031\u1038\u101c\u103b", + "OM": "\u1021\u102d\u102f\u1019\u1014\u103a", + "EE": "\u1021\u1000\u103a\u1005\u1010\u102d\u102f\u1038\u1014\u102e\u1038\u101a\u102c\u1038", + "IS": "\u1021\u102d\u102f\u1000\u103a\u1005\u101c\u1014\u103a", + "ID": "\u1021\u1004\u103a\u1012\u102d\u102f\u1014\u102e\u1038\u101b\u103e\u102c\u1038", + "IE": "\u1021\u102d\u102f\u1004\u103a\u101a\u102c\u101c\u1014\u103a", + "IL": "\u1021\u1005\u1039\u1005\u101b\u1031\u1038", + "AI": "\u1021\u1014\u103a\u1002\u102e\u101c\u102c", + "AO": "\u1021\u1014\u103a\u1002\u102d\u102f\u101c\u102c", + "AQ": "\u1021\u1014\u103a\u1010\u102c\u1010\u102d\u1000", + "AG": "\u1021\u1014\u103a\u1010\u102e\u1002\u103d\u102b\u1014\u103e\u1004\u1037\u103a \u1018\u102c\u1018\u1030\u1012\u102b", + "AD": "\u1021\u1014\u103a\u1012\u102d\u102f\u101b\u102c", + "IN": "\u1021\u102d\u1014\u1039\u1012\u102d\u101a", + "DZ": "\u1021\u101a\u103a\u101c\u103a\u1002\u103b\u102e\u1038\u101b\u102e\u1038\u101a\u102c\u1038", + "SV": "\u1021\u101a\u103a\u101c\u103a\u1006\u102c\u1017\u1031\u1038\u1012\u102d\u102f\u1038", + "AL": "\u1021\u101a\u103a\u101c\u103a\u1018\u1031\u1038\u1014\u102e\u1038\u101a\u102c\u1038" +} diff --git a/public/intl/language/my-MM.json b/public/intl/language/my-MM.json new file mode 100644 index 000000000..e853cde4a --- /dev/null +++ b/public/intl/language/my-MM.json @@ -0,0 +1,611 @@ +{ + "kac": "\u1000\u1001\u103b\u1004\u103a", + "fr_CA": "\u1000\u1014\u1031\u1012\u102b \u1015\u103c\u1004\u103a\u101e\u1005\u103a", + "en_CA": "\u1000\u1014\u1031\u1012\u102b \u1021\u1004\u103a\u1039\u1002\u101c\u102d\u1015\u103a", + "ca": "\u1000\u102c\u1010\u102c\u101c\u1014\u103a", + "koi": "\u1000\u102d\u102f\u1019\u102e-\u1015\u102b\u1019\u103c\u1000\u103a", + "ko": "\u1000\u102d\u102f\u1038\u101b\u102e\u1038\u101a\u102c\u1038", + "ks": "\u1000\u1000\u103a\u101b\u103e\u103a\u1019\u102e\u101b\u102e", + "rw": "\u1000\u1004\u103a\u101b\u102c\u101d\u1014\u103a\u1012\u102b", + "ku": "\u1000\u1012\u103a", + "kn": "\u1000\u1014\u1039\u1014\u102c\u1012\u102b", + "kok": "\u1000\u103d\u1014\u103a\u1000\u1014\u102e", + "kg": "\u1000\u103d\u1014\u103a\u1002\u102d\u102f", + "kea": "\u1001\u1018\u1030\u1017\u102c\u1012\u102e\u1021\u102c\u1014\u1030", + "kab": "\u1001\u1018\u102d\u102f\u1004\u103a\u101c\u103a", + "km": "\u1001\u1019\u102c", + "cr": "\u1001\u101b\u102e\u1038", + "ky": "\u1001\u101b\u1030\u1002\u1005\u103a", + "hr": "\u1001\u101b\u102d\u102f\u1021\u1031\u101b\u103e\u1014\u103a", + "kl": "\u1001\u101c\u102c\u1021\u103a\u101c\u102e\u1006\u1015\u103a", + "kha": "\u1001\u102b\u1005\u102e", + "kk": "\u1001\u102b\u1007\u102b\u1001\u103a", + "kln": "\u1001\u102b\u101c\u102d\u1019\u103a\u1002\u103b\u1004\u103a", + "ki": "\u1001\u102e\u1001\u1030\u101a\u1030", + "quc": "\u1001\u102e\u1001\u103a\u1021\u102e\u1001\u103b\u102e", + "co": "\u1001\u102d\u102f\u1005\u102e\u1000\u1014\u103a", + "kw": "\u1001\u102d\u102f\u1014\u102e\u101b\u103e\u103a", + "khq": "\u1001\u102d\u102f\u101b\u102c \u1001\u103b\u102e\u1021\u102e\u1014\u102e", + "ses": "\u1001\u102d\u102f\u101b\u102c\u1018\u102d\u102f\u101b\u102d\u102f \u1006\u1019\u103a\u1014\u102e", + "qu": "\u1001\u1000\u103a\u1001\u103b\u103a\u101d\u102b", + "kam": "\u1001\u1019\u103a\u1018\u102c", + "sms": "\u1001\u102d\u102f\u101c\u103a \u1006\u102c\u1019\u102d", + "chr": "\u1001\u103b\u102c\u101b\u102d\u102f\u1000\u102e", + "cgg": "\u1001\u103b\u102e\u1002\u102b", + "ce": "\u1001\u103b\u1031\u1001\u103b\u1004\u103a\u1038", + "cs": "\u1001\u103b\u1000\u103a", + "swc": "\u1001\u103d\u1014\u103a\u1002\u102d\u102f \u1005\u103d\u102c\u101f\u102e\u101c\u102e", + "el": "\u1002\u101b\u102d", + "gag": "\u1002\u102b\u1002\u102b\u1007\u103a", + "gl": "\u1002\u102b\u101c\u102c\u1005\u102e\u101a\u1036", + "gu": "\u1002\u1030\u1002\u103b\u102c\u101b\u101e\u102e", + "guz": "\u1002\u1030\u1005\u102e\u1038", + "gn": "\u1002\u1030\u1021\u102c\u101b\u102c\u1014\u102e", + "lg": "\u1002\u1014\u103a\u1012\u102e", + "ja": "\u1002\u103b\u1015\u1014\u103a", + "jv": "\u1002\u103b\u102c\u1017\u102c\u1038\u1014\u102e\u1038\u1005\u103a", + "de": "\u1002\u103b\u102c\u1019\u1014\u103a", + "jpr": "\u1002\u103b\u1030\u1012\u102e\u101a\u102d\u102f-\u1015\u102b\u101b\u103e\u1014\u103a", + "jrb": "\u1002\u103b\u1030\u1012\u102e\u101a\u102d\u102f-\u1021\u102c\u101b\u1031\u1017\u103b", + "ka": "\u1002\u103b\u1031\u102c\u103a\u1002\u103b\u102e\u101a\u1014\u103a", + "dyo": "\u1002\u103b\u102d\u102f\u101c\u102c-\u1016\u103d\u1014\u103a\u101b\u102e", + "jgo": "\u1002\u103d\u1019\u103a\u1018\u102c", + "sco": "\u1005\u1000\u1031\u102c\u1037", + "es": "\u1005\u1015\u102d\u1014\u103a", + "es_ES": "\u1005\u1015\u102d\u1014\u103a(\u1025\u101b\u1031\u102c\u1015)", + "sl": "\u1005\u101c\u102d\u102f\u1017\u1031\u1038\u1014\u102e\u1038\u101a\u1019\u103a\u1038", + "sk": "\u1005\u101c\u102d\u102f\u1017\u1000\u103a", + "ii": "\u1005\u102e\u1001\u103b\u103d\u1019\u103a \u101b\u102e", + "seh": "\u1005\u102e\u1014\u102c", + "sd": "\u1005\u1004\u103a\u1012\u102e", + "sr": "\u1006\u102c\u1038\u1017\u102e\u1038\u101a\u1014\u103a\u1038", + "su": "\u1006\u1030\u1012\u1014\u103a", + "xog": "\u1006\u102d\u102f\u1002\u102b", + "so": "\u1006\u102d\u102f\u1019\u102c\u101c\u102e", + "ckb": "\u1006\u102d\u102f\u101b\u102c\u1014\u102e \u1000\u1030\u1012\u1005\u103a\u101b\u103e\u103a", + "si": "\u1006\u1004\u103a\u101f\u102c\u101c", + "sbp": "\u1006\u1014\u103a\u1002\u102f", + "saq": "\u1006\u1014\u103a\u1018\u1030\u101b\u102f", + "sg": "\u1006\u1019\u103a\u1002\u102d\u102f", + "sw": "\u1006\u103c\u102c\u101f\u102e\u101c\u102e", + "sv": "\u1006\u103d\u102e\u1012\u1004\u103a", + "gsw": "\u1006\u103d\u1005\u103a \u1002\u103b\u102c\u1019\u1014\u103a", + "fr_CH": "\u1006\u103d\u1005\u103a \u1015\u103c\u1004\u103a\u101e\u1005\u103a", + "de_CH": "\u1006\u103d\u1005\u103a \u1021\u1019\u103c\u1004\u1037\u103a \u1002\u103b\u102c\u1019\u1014\u103a", + "dje": "\u1007\u102c\u1019\u102c", + "zu": "\u1007\u1030\u101c\u1030", + "xh": "\u1007\u102d\u102f\u1005\u102c", + "tt": "\u1010\u1010\u102c", + "ta": "\u1010\u1019\u102e\u1038\u101c\u103a", + "zh": "\u1010\u101b\u102f\u1010\u103a", + "tr": "\u1010\u102c\u1000\u1005\u103a", + "shi": "\u1010\u102c\u1001\u103b\u101a\u103a\u101c\u103a\u101f\u1005\u103a", + "tg": "\u1010\u102c\u1002\u103d\u102e\u1001\u103a", + "twq": "\u1010\u102c\u1006\u102c\u101d\u102b\u1001\u103a", + "bo": "\u1010\u102d\u1018\u1000\u103a", + "teo": "\u1010\u102e\u1006\u102d\u102f", + "te": "\u1010\u102e\u101c\u102e\u1002\u102f", + "ti": "\u1010\u102e\u1002\u103a\u101b\u1004\u103a\u101b\u102c", + "tk": "\u1010\u1001\u1039\u1019\u1004\u103a", + "se": "\u1010\u1031\u102c\u1004\u103a\u1006\u102c\u1019\u102d", + "nd": "\u1010\u1031\u102c\u1004\u103a\u1012\u102e\u1018\u102e\u101c\u102e", + "sma": "\u1010\u1031\u102c\u1004\u103a\u1015\u102d\u102f\u1004\u103a\u1038 \u1006\u102c\u1019\u102d", + "dav": "\u1010\u102d\u102f\u1004\u103a\u1010\u102c", + "to": "\u1010\u103d\u1014\u103a\u1002\u102b", + "th": "\u1011\u102d\u102f\u1004\u103a\u1038", + "dak": "\u1012\u102b\u1000\u102d\u102f\u1010\u102c", + "del": "\u1012\u102e\u101c\u102c\u101d\u1032", + "dua": "\u1012\u1030\u1021\u101c\u102c", + "nl": "\u1012\u1010\u103a\u1001\u103b\u103a", + "da": "\u1012\u102d\u1014\u103a\u1038\u1019\u1010\u103a", + "dz": "\u1012\u103d\u1014\u103a\u1000\u102c", + "nqo": "\u1014\u1000\u102d\u102f", + "naq": "\u1014\u102c\u1019\u102c", + "ne": "\u1014\u102e\u1015\u102b\u101c\u102e", + "nus": "\u1014\u1030\u1021\u102c", + "no": "\u1014\u1031\u102c\u103a\u101d\u1031\u1038", + "nn": "\u1014\u1031\u102c\u103a\u101d\u1031\u1038 \u1014\u102e\u1038\u1014\u1031\u102c\u103a\u1005\u103a\u1001\u103a", + "fa": "\u1015\u102b\u101b\u103e\u1014\u103a", + "peo": "\u1015\u102b\u101b\u103e\u1014\u103a \u1021\u101f\u1031\u102c\u1004\u103a\u1038", + "ps": "\u1015\u102b\u101b\u103e\u103a\u1010\u102d\u102f", + "pi": "\u1015\u102b\u1020\u102d", + "pt": "\u1015\u1031\u102b\u103a\u1010\u1030\u1002\u102e", + "pl": "\u1015\u102d\u102f\u101c\u1014\u103a", + "pa": "\u1015\u1014\u103a\u1001\u103b\u102c\u1015\u102e", + "fr": "\u1015\u103c\u1004\u103a\u101e\u1005\u103a", + "fro": "\u1015\u103c\u1004\u103a\u101e\u1005\u103a\u101f\u1031\u102c\u1004\u103a\u1038", + "nl_BE": "\u1016\u101c\u102e\u1019\u1005\u103a\u101b\u103e\u103a", + "fo": "\u1016\u102c\u101b\u102d\u102f\u1021\u102d\u1005\u103a", + "fil": "\u1016\u102d\u101c\u1005\u103a\u1015\u102e\u1014\u102d\u102f", + "fj": "\u1016\u102e\u1002\u103b\u102e", + "fi": "\u1016\u1004\u103a\u1014\u1005\u103a\u101b\u103e\u103a", + "my": "\u1017\u1019\u102c", + "vi": "\u1017\u102e\u101a\u1000\u103a\u1014\u1019\u103a", + "brx": "\u1017\u102d\u102f\u1012\u102d\u102f", + "vai": "\u1017\u102d\u102f\u1004\u103a", + "vun": "\u1017\u1014\u103a\u1002\u103b\u102d\u102f", + "en_GB": "\u1017\u103c\u102d\u1010\u102d\u101e\u103b\u103e \u1021\u1004\u103a\u1039\u1002\u101c\u102d\u1015\u103a", + "pt_BR": "\u1018\u101b\u102c\u1007\u102e\u1038 \u1015\u1031\u102b\u103a\u1010\u1030\u1002\u102e", + "br": "\u1018\u101b\u102e\u1010\u103d\u1014\u103a", + "bas": "\u1018\u102c\u1006\u102c", + "ksf": "\u1018\u102c\u1016\u102e\u1021\u102c", + "ban": "\u1018\u102c\u101c\u102e", + "zxx": "\u1018\u102c\u101e\u102c\u1005\u1000\u102c\u1038 \u1014\u103e\u1004\u1037\u103a \u1015\u1010\u103a\u101e\u1000\u103a \u101e\u1031\u102c\u1021\u101b\u102c\u1019\u101b\u103e\u102d", + "bez": "\u1018\u102e\u1014\u102c", + "be": "\u1018\u102e\u101c\u102c\u101b\u102f", + "bg": "\u1018\u1030\u1002\u1031\u1038\u101b\u102e\u1038\u101a\u102c\u1038", + "bs": "\u1018\u1031\u102c\u1037\u1005\u103a\u1014\u102e\u1038\u101a\u102c\u1038", + "ba": "\u1018\u1000\u103a\u101b\u103e\u103a\u1000\u102e\u1021\u102c", + "bn": "\u1018\u1004\u103a\u1039\u1002\u102b\u101c\u102e", + "eu": "\u1018\u1005\u103a\u1000\u1039\u1000\u102e", + "bm": "\u1018\u1014\u103a\u1018\u102c\u101b\u102c", + "bem": "\u1018\u102d\u1014\u103a\u1018\u102c", + "nb": "\u1018\u103d\u1010\u103a\u1019\u1031\u102c\u103a\u101c\u103a", + "jmc": "\u1019\u1001\u103b\u102c\u1019\u102e", + "ms": "\u1019\u101c\u1031\u1038", + "ml": "\u1019\u101c\u1031\u1038\u101b\u102c\u101c\u1019\u103a", + "und": "\u1019\u101e\u102d \u101e\u102d\u102f\u1037\u1019\u101f\u102f\u1010\u103a \u1019\u101b\u103e\u102d \u101e\u1031\u102c \u1018\u102c\u101e\u102c\u1005\u1000\u102c\u1038", + "mgh": "\u1019\u102c\u1001\u1030\u101d\u102b-\u1019\u102e\u1021\u102e\u1010\u102d\u102f", + "kde": "\u1019\u102c\u1001\u103d\u1014\u103a\u1012\u102e", + "mk": "\u1019\u102c\u1005\u102e\u1012\u102d\u102f\u1014\u102e\u101a\u1036", + "mas": "\u1019\u102c\u1006\u102d\u102f\u1004\u103a", + "arn": "\u1019\u102c\u1015\u102f\u1001\u103b\u102e", + "mr": "\u1019\u102c\u101b\u101e\u102e", + "mgo": "\u1019\u102e\u1010\u102c", + "mer": "\u1019\u102e\u101b\u102f", + "root": "\u1019\u1030\u101c\u101b\u1004\u103a\u1038\u1019\u103c\u1005\u103a", + "zgh": "\u1019\u102d\u102f\u101b\u102d\u102f\u1000\u1014\u103a \u1010\u103d\u1019\u103a\u1019\u1007\u102d\u102f\u1010\u103a \u1005\u1036", + "mfe": "\u1019\u102d\u102f\u101b\u103e\u102e\u1005\u103a\u101a\u1014\u103a\u1038", + "moh": "\u1019\u102d\u102f\u101f\u1031\u102c\u1037\u1001\u103a", + "mi": "\u1019\u1031\u102c\u1004\u103a\u1038\u101b\u102e (\u1014\u101a\u1030\u1038\u1007\u102e\u101c\u1014\u103a\u1000\u103b\u103d\u1014\u103a\u1038\u101b\u103e\u102d \u1015\u1004\u103a\u101b\u1004\u103a\u1038\u1010\u102d\u102f\u1004\u103a\u1038\u101b\u1004\u103a\u1038\u101e\u102c\u1038\u101c\u1030\u1019\u103b\u102d\u102f\u1038)", + "mnc": "\u1019\u1014\u103a\u1001\u103b\u1030\u1038", + "mua": "\u1019\u1014\u103a\u1012\u1014\u103a\u1038", + "gv": "\u1019\u1014\u103a\u1038\u1007\u103a", + "mt": "\u1019\u1031\u102c\u101c\u1039\u1010\u102e\u1005\u103a", + "frr": "\u1019\u103c\u1031\u102c\u1000\u103a\u1015\u102d\u102f\u1004\u103a\u1038 \u1016\u101b\u102e\u1005\u102e\u101b\u1014\u103a", + "mn": "\u1019\u103d\u1014\u103a\u1002\u102d\u102f\u101c\u102e\u1038\u101a\u1014\u103a\u1038", + "uk": "\u101a\u1030\u1000\u101b\u102d\u1014\u103a\u1038", + "nyn": "\u101a\u1014\u103a\u1000\u102d\u102f\u101c\u102e", + "ru": "\u101b\u102f\u101b\u103e", + "rm": "\u101b\u1031\u102c\u1019", + "ro": "\u101b\u102d\u102f\u1019\u1031\u1014\u102e\u101a\u102c\u1038", + "yo": "\u101b\u102d\u102f\u101b\u102f\u1018\u102c", + "zh_Hans": "\u101b\u102d\u102f\u1038\u101b\u103e\u1004\u103a\u1038\u101e\u1031\u102c \u1010\u101b\u102f\u1010\u103a", + "rn": "\u101b\u103d\u1014\u103a\u1012\u102e", + "rof": "\u101b\u103d\u1019\u103a\u1018\u102d\u102f", + "sn": "\u101b\u103e\u102d\u1014\u102c", + "zh_Hant": "\u101b\u103e\u1031\u1038\u101b\u102d\u102f\u1038\u1005\u1009\u103a\u101c\u102c \u1010\u101b\u102f\u1010\u103a", + "grc": "\u101b\u103e\u1031\u1038\u101f\u1031\u102c\u1004\u103a\u1038 \u1002\u101b\u102d", + "egy": "\u101b\u103e\u1031\u1038\u101f\u1031\u102c\u1004\u103a\u1038 \u1021\u102e\u1002\u103b\u1005\u103a", + "ksb": "\u101b\u103e\u1014\u103a\u1018\u102c\u101c\u102c", + "shn": "\u101b\u103e\u1019\u103a\u1038", + "lkt": "\u101c\u102c\u1000\u102d\u102f\u1010\u102c", + "lo": "\u101c\u102c\u1021\u102d\u102f", + "lb": "\u101c\u1030\u1007\u1004\u103a\u1018\u1010\u103a\u1000\u103a", + "lu": "\u101c\u1030\u1018\u102c-\u1001\u102b\u1010\u1014\u103a\u1002\u102b", + "luy": "\u101c\u1030\u101b\u102e\u1021\u102c", + "smj": "\u101c\u1030\u101c\u102e \u1006\u102c\u1019\u102d", + "luo": "\u101c\u1030\u1021\u102d\u102f", + "la": "\u101c\u1000\u103a\u1010\u1004\u103a", + "es_419": "\u101c\u1000\u103a\u1010\u1004\u103a\u1021\u1019\u1031\u101b\u102d\u1000 \u1005\u1015\u102d\u1014\u103a", + "lv": "\u101c\u1000\u1039\u1018\u102e\u1021\u1036", + "ln": "\u101c\u1004\u103a\u1002\u102b\u101c\u102c", + "lt": "\u101c\u1005\u1039\u101e\u1030\u1021\u102c\u1014\u102e\u101a\u1036", + "lag": "\u101c\u1014\u103a\u1002\u102e", + "rwk": "\u101d\u102b", + "nmg": "\u101d\u102b\u1006\u102e\u1021\u102d\u102f", + "ee": "\u101d\u102e", + "ug": "\u101d\u102e\u1002\u102b", + "wo": "\u101d\u1030\u101c\u102d\u102f\u1016\u103a", + "cy": "\u101d\u1031\u101c", + "sa": "\u101e\u1004\u103a\u1039\u101e\u1000\u101b\u102d\u102f\u1000\u103a", + "haw": "\u101f\u102c\u101d\u1031\u101a\u1036", + "ht": "\u101f\u102c\u1021\u102e\u1010\u102e\u1021\u1014\u103a", + "ha": "\u101f\u102c\u1025\u1005\u102c", + "he": "\u101f\u102e\u1038\u1018\u101b\u1030\u1038", + "hu": "\u101f\u1014\u103a\u1002\u1031\u101b\u102e", + "hi": "\u101f\u102d\u1014\u1039\u1012\u102e", + "mul": "\u1021\u1000\u103c\u102d\u1019\u103a\u1019\u103b\u102c\u1038\u1005\u103d\u102c \u1018\u102c\u101e\u102c\u1005\u1000\u102c\u1038\u1019\u103b\u102c\u1038", + "fy": "\u1021\u1014\u1031\u102c\u1000\u103a\u1015\u102d\u102f\u1004\u103a\u1038 \u1016\u101b\u102e\u1005\u102e\u101b\u1014\u103a", + "nds": "\u1021\u1014\u102d\u1019\u1037\u103a \u1002\u103b\u102c\u1019\u1014\u103a", + "hsb": "\u1021\u1015\u1031\u102b\u103a\u1006\u102d\u102f\u1018\u102e\u1021\u1019\u103a", + "en_US": "\u1021\u1019\u1031\u101b\u102d\u1000\u1014\u103a \u1021\u1004\u103a\u1039\u1002\u101c\u102d\u1015\u103a", + "ar_001": "\u1021\u101b\u1031\u1017\u102e(\u1015\u102f\u1036\u1019\u103e\u1014\u103a)", + "frs": "\u1021\u101b\u103e\u1031\u1037\u1015\u102d\u102f\u1004\u103a\u1038 \u1016\u101b\u102e\u1005\u102e\u101b\u1014\u103a", + "dum": "\u1021\u101c\u101a\u103a\u1015\u102d\u102f\u1004\u103a\u1038 \u1012\u1010\u103a\u1001\u103b\u103a", + "enm": "\u1021\u101c\u101a\u103a\u1015\u102d\u102f\u1004\u103a\u1038 \u1021\u1004\u103a\u1039\u1002\u101c\u102d\u1015\u103a", + "mga": "\u1021\u101c\u101a\u103a\u1015\u102d\u102f\u1004\u103a\u1038 \u1021\u102d\u102f\u1004\u103a\u1038\u101b\u1005\u103a", + "frm": "\u1021\u101c\u101a\u103a\u1015\u102d\u102f\u1004\u103a\u1038\u1015\u103c\u1004\u103a\u101e\u1005\u103a", + "gmh": "\u1021\u101c\u101a\u103a\u1015\u102d\u102f\u1004\u103a\u1038\u1021\u1019\u103c\u1004\u1037\u103a\u1002\u103b\u102c\u1019\u1014\u103a", + "tzm": "\u1021\u101c\u101a\u103a\u1021\u1000\u103a\u1010\u103a\u101c\u1000\u103a\u1005\u103a \u1010\u102c\u1019\u102c\u1007\u102d\u102f\u1000\u103a", + "ak": "\u1021\u102c\u1000\u102d\u1014\u103a", + "asa": "\u1021\u102c\u1005\u102f", + "af": "\u1021\u102c\u1016\u101b\u102d\u1000\u1014\u103a\u1038\u1005\u103a", + "hy": "\u1021\u102c\u1019\u1031\u1014\u102e\u1021\u1014\u103a", + "ar": "\u1021\u102c\u101b\u1031\u1017\u102e", + "mg": "\u1021\u102c\u101c\u102c\u1002\u102b\u1005\u102e", + "it": "\u1021\u102e\u1010\u101c\u102e", + "smn": "\u1021\u102e\u1014\u102c\u101b\u102e \u1006\u102c\u1019\u102d", + "iu": "\u1021\u102e\u1014\u102f\u1001\u103a\u1010\u102e\u1010\u102f", + "pt_PT": "\u1025\u101b\u1031\u102c\u1015 \u1015\u1031\u102b\u103a\u1010\u1030\u1002\u102e", + "uz": "\u1026\u1038\u1007\u103a\u1018\u1000\u103a", + "de_AT": "\u1029\u1005\u1010\u103c\u102e\u1038\u101a \u1002\u103b\u102c\u1019\u1014\u103a", + "en_AU": "\u1029\u1005\u1010\u103c\u1031\u1038\u101c\u103b\u103e \u1021\u1004\u103a\u1039\u1002\u101c\u102d\u1015\u103a", + "az": "\u1021\u1031\u102c\u103a\u1007\u1031\u102c\u1018\u102d\u102f\u1004\u103a\u1002\u103b\u1031\u102c\u1014\u102e", + "ur": "\u1021\u1031\u102c\u103a\u1012\u1030", + "or": "\u1021\u102d\u102f\u101b\u102e\u101b\u102c", + "om": "\u1021\u102d\u102f\u101b\u102d\u102f\u1019\u102d\u102f", + "ab": "\u1021\u1000\u103a\u1001\u102b\u1007\u1030\u1021\u1014\u103b", + "eo": "\u1021\u1000\u1039\u1005\u1015\u101b\u1014\u1039\u1010\u102d\u102f", + "as": "\u1021\u1000\u1039\u1005\u1019\u102e\u1005\u103a", + "et": "\u1021\u1000\u103a\u1005\u103a\u1010\u102d\u102f\u1038\u1014\u102e\u1038\u101b\u1014\u103a\u1038", + "dsb": "\u1021\u1031\u102c\u1000\u103a\u1006\u102d\u102f\u1018\u102e\u1021\u1019\u103a", + "agq": "\u1021\u102c\u1002\u103a\u101f\u102d\u1014\u103a\u1038", + "en": "\u1021\u1004\u103a\u1039\u1002\u101c\u102d\u1015\u103a", + "ang": "\u1021\u1004\u103a\u1039\u1002\u101c\u102d\u1015\u103a\u1005\u102c\u101f\u1031\u102c\u1004\u103a\u1038", + "id": "\u1021\u1004\u103a\u1012\u102d\u102f\u1014\u102e\u1038\u101b\u103e\u102c\u1038", + "is": "\u1021\u102d\u102f\u1004\u103a\u1005\u103a\u101c\u1014\u1039\u1012\u102e", + "ga": "\u1021\u102d\u102f\u1004\u103a\u1038\u101b\u1005\u103a", + "sga": "\u1021\u102d\u102f\u1004\u103a\u1038\u101b\u1005\u103a \u101f\u1031\u102c\u1004\u103a\u1038", + "ig": "\u1021\u1005\u1039\u1002\u1018\u102d\u102f", + "am": "\u1021\u1014\u103a\u101f\u102c\u101b\u1005\u103b\u1001\u103b", + "ebu": "\u1021\u1019\u103a\u1018\u1030", + "sq": "\u1021\u101a\u103a\u101c\u103a\u1018\u1031\u1038\u1014\u102e\u1038\u101a\u1014\u103a\u1038", + "ace": "Achinese", + "ach": "Acoli", + "ada": "Adangme", + "ady": "Adyghe", + "aa": "Afar", + "afh": "Afrihili", + "ain": "Ainu", + "akk": "Akkadian", + "bss": "Akoose", + "akz": "Alabama", + "ale": "Aleut", + "arq": "Algerian Arabic", + "ase": "American Sign Language", + "anp": "Angika", + "njo": "Ao Naga", + "an": "Aragonese", + "arc": "Aramaic", + "aro": "Araona", + "arp": "Arapaho", + "arw": "Arawak", + "rup": "Aromanian", + "frp": "Arpitan", + "ast": "Asturian", + "cch": "Atsam", + "av": "Avaric", + "ae": "Avestan", + "awa": "Awadhi", + "ay": "Aymara", + "bfq": "Badaga", + "bfd": "Bafut", + "bqi": "Bakhtiari", + "bal": "Baluchi", + "bax": "Bamun", + "bjn": "Banjar", + "bbc": "Batak Toba", + "bar": "Bavarian", + "bej": "Beja", + "bew": "Betawi", + "bho": "Bhojpuri", + "bik": "Bikol", + "bin": "Bini", + "bpy": "Bishnupriya", + "bi": "Bislama", + "byn": "Blin", + "zbl": "Blissymbols", + "brh": "Brahui", + "bra": "Braj", + "bug": "Buginese", + "bum": "Bulu", + "bua": "Buriat", + "cad": "Caddo", + "frc": "Cajun French", + "yue": "Cantonese", + "cps": "Capiznon", + "car": "Carib", + "cay": "Cayuga", + "ceb": "Cebuano", + "dtp": "Central Dusun", + "esu": "Central Yupik", + "shu": "Chadian Arabic", + "chg": "Chagatai", + "ch": "Chamorro", + "chy": "Cheyenne", + "chb": "Chibcha", + "qug": "Chimborazo Highland Quichua", + "chn": "Chinook Jargon", + "chp": "Chipewyan", + "cho": "Choctaw", + "cu": "Church Slavic", + "chk": "Chuukese", + "cv": "Chuvash", + "nwc": "Classical Newari", + "syc": "Classical Syriac", + "ksh": "Colognian", + "swb": "Comorian", + "cop": "Coptic", + "mus": "Creek", + "crh": "Crimean Turkish", + "dar": "Dargwa", + "dzg": "Dazaga", + "din": "Dinka", + "dv": "Divehi", + "doi": "Dogri", + "dgr": "Dogrib", + "dyu": "Dyula", + "efi": "Efik", + "arz": "Egyptian Arabic", + "eka": "Ekajuk", + "elx": "Elamite", + "egl": "Emilian", + "myv": "Erzya", + "ewo": "Ewondo", + "ext": "Extremaduran", + "fan": "Fang", + "fat": "Fanti", + "hif": "Fiji Hindi", + "fon": "Fon", + "gur": "Frafra", + "fur": "Friulian", + "ff": "Fulah", + "gaa": "Ga", + "gan": "Gan Chinese", + "gay": "Gayo", + "gba": "Gbaya", + "gez": "Geez", + "aln": "Gheg Albanian", + "bbj": "Ghomala", + "glk": "Gilaki", + "gil": "Gilbertese", + "gom": "Goan Konkani", + "gon": "Gondi", + "gor": "Gorontalo", + "got": "Gothic", + "grb": "Grebo", + "gwi": "Gwich\u02bcin", + "hai": "Haida", + "hak": "Hakka Chinese", + "hz": "Herero", + "hil": "Hiligaynon", + "ho": "Hiri Motu", + "hit": "Hittite", + "hmn": "Hmong", + "hup": "Hupa", + "iba": "Iban", + "ibb": "Ibibio", + "io": "Ido", + "ilo": "Iloko", + "izh": "Ingrian", + "inh": "Ingush", + "ia": "Interlingua", + "ie": "Interlingue", + "ik": "Inupiaq", + "jam": "Jamaican Creole English", + "kaj": "Jju", + "jut": "Jutish", + "kbd": "Kabardian", + "kgp": "Kaingang", + "kkj": "Kako", + "xal": "Kalmyk", + "kbl": "Kanembu", + "kr": "Kanuri", + "kaa": "Kara-Kalpak", + "krc": "Karachay-Balkar", + "krl": "Karelian", + "csb": "Kashubian", + "kaw": "Kawi", + "ken": "Kenyang", + "kho": "Khotanese", + "khw": "Khowar", + "kmb": "Kimbundu", + "krj": "Kinaray-a", + "kiu": "Kirmanjki", + "tlh": "Klingon", + "bkm": "Kom", + "kv": "Komi", + "kfo": "Koro", + "kos": "Kosraean", + "avk": "Kotava", + "kpe": "Kpelle", + "kri": "Krio", + "kj": "Kuanyama", + "kum": "Kumyk", + "kru": "Kurukh", + "kut": "Kutenai", + "lad": "Ladino", + "lah": "Lahnda", + "lam": "Lamba", + "ltg": "Latgalian", + "lzz": "Laz", + "lez": "Lezghian", + "lij": "Ligurian", + "li": "Limburgish", + "lfn": "Lingua Franca Nova", + "lzh": "Literary Chinese", + "liv": "Livonian", + "jbo": "Lojban", + "lmo": "Lombard", + "sli": "Lower Silesian", + "loz": "Lozi", + "lua": "Luba-Lulua", + "lui": "Luiseno", + "lun": "Lunda", + "mde": "Maba", + "mad": "Madurese", + "maf": "Mafa", + "mag": "Magahi", + "vmf": "Main-Franconian", + "mai": "Maithili", + "mak": "Makasar", + "mdr": "Mandar", + "man": "Mandingo", + "mni": "Manipuri", + "chm": "Mari", + "mh": "Marshallese", + "mwr": "Marwari", + "mzn": "Mazanderani", + "byv": "Medumba", + "men": "Mende", + "mwv": "Mentawai", + "es_MX": "Mexican Spanish", + "mic": "Micmac", + "nan": "Min Nan Chinese", + "min": "Minangkabau", + "xmf": "Mingrelian", + "mwl": "Mirandese", + "lus": "Mizo", + "mdf": "Moksha", + "ro_MD": "Moldavian", + "lol": "Mongo", + "ary": "Moroccan Arabic", + "mos": "Mossi", + "ttt": "Muslim Tat", + "mye": "Myene", + "na": "Nauru", + "nv": "Navajo", + "ng": "Ndonga", + "nap": "Neapolitan", + "new": "Newari", + "sba": "Ngambay", + "nnh": "Ngiemboon", + "yrl": "Nheengatu", + "nia": "Nias", + "niu": "Niuean", + "nog": "Nogai", + "nso": "Northern Sotho", + "nov": "Novial", + "nym": "Nyamwezi", + "ny": "Nyanja", + "tog": "Nyasa Tonga", + "nyo": "Nyoro", + "nzi": "Nzima", + "oc": "Occitan", + "oj": "Ojibwa", + "goh": "Old High German", + "non": "Old Norse", + "pro": "Old Proven\u00e7al", + "osa": "Osage", + "os": "Ossetic", + "ota": "Ottoman Turkish", + "pal": "Pahlavi", + "pfl": "Palatine German", + "pau": "Palauan", + "pam": "Pampanga", + "pag": "Pangasinan", + "pap": "Papiamento", + "pdc": "Pennsylvania German", + "phn": "Phoenician", + "pcd": "Picard", + "pms": "Piedmontese", + "pdt": "Plautdietsch", + "pon": "Pohnpeian", + "pnt": "Pontic", + "prg": "Prussian", + "raj": "Rajasthani", + "rap": "Rapanui", + "rar": "Rarotongan", + "rif": "Riffian", + "rgn": "Romagnol", + "rom": "Romany", + "rtm": "Rotuman", + "rug": "Roviana", + "rue": "Rusyn", + "ssy": "Saho", + "sah": "Sakha", + "sam": "Samaritan Aramaic", + "sm": "Samoan", + "sgs": "Samogitian", + "sad": "Sandawe", + "sat": "Santali", + "sc": "Sardinian", + "sas": "Sasak", + "sdc": "Sassarese Sardinian", + "stq": "Saterland Frisian", + "saz": "Saurashtra", + "gd": "Scottish Gaelic", + "sly": "Selayar", + "sel": "Selkup", + "see": "Seneca", + "sh": "Serbo-Croatian", + "srr": "Serer", + "sei": "Seri", + "scn": "Sicilian", + "sid": "Sidamo", + "bla": "Siksika", + "szl": "Silesian", + "den": "Slave", + "sog": "Sogdien", + "snk": "Soninke", + "azb": "South Azerbaijani", + "nr": "South Ndebele", + "alt": "Southern Altai", + "st": "Southern Sotho", + "srn": "Sranan Tongo", + "suk": "Sukuma", + "sux": "Sumerian", + "sus": "Susu", + "ss": "Swati", + "syr": "Syriac", + "tl": "Tagalog", + "ty": "Tahitian", + "tly": "Talysh", + "tmh": "Tamashek", + "trv": "Taroko", + "ter": "Tereno", + "tet": "Tetum", + "tig": "Tigre", + "tem": "Timne", + "tiv": "Tiv", + "tli": "Tlingit", + "tpi": "Tok Pisin", + "tkl": "Tokelau", + "fit": "Tornedalen Finnish", + "tkr": "Tsakhur", + "tsd": "Tsakonian", + "tsi": "Tsimshian", + "ts": "Tsonga", + "tn": "Tswana", + "tcy": "Tulu", + "tum": "Tumbuka", + "aeb": "Tunisian Arabic", + "tru": "Turoyo", + "tvl": "Tuvalu", + "tyv": "Tuvinian", + "tw": "Twi", + "kcg": "Tyap", + "udm": "Udmurt", + "uga": "Ugaritic", + "umb": "Umbundu", + "ve": "Venda", + "vec": "Venetian", + "vep": "Veps", + "vo": "Volap\u00fck", + "vro": "V\u00f5ro", + "vot": "Votic", + "wa": "Walloon", + "wae": "Walser", + "war": "Waray", + "wbp": "Warlpiri", + "was": "Washo", + "guc": "Wayuu", + "vls": "West Flemish", + "mrj": "Western Mari", + "wal": "Wolaytta", + "wuu": "Wu Chinese", + "hsn": "Xiang Chinese", + "yav": "Yangben", + "yao": "Yao", + "yap": "Yapese", + "ybb": "Yemba", + "yi": "Yiddish", + "zap": "Zapotec", + "zza": "Zaza", + "zea": "Zeelandic", + "zen": "Zenaga", + "za": "Zhuang", + "gbz": "Zoroastrian Dari", + "zun": "Zuni" +} diff --git a/public/intl/messages/my-MM.json b/public/intl/messages/my-MM.json new file mode 100644 index 000000000..3995a22b0 --- /dev/null +++ b/public/intl/messages/my-MM.json @@ -0,0 +1,1296 @@ +{ + "label.access-code": [ + { + "type": 0, + "value": "ဝင်ခွင့်ကုဒ်" + } + ], + "label.actions": [ + { + "type": 0, + "value": "လုပ်ဆောင်ချက်များ" + } + ], + "label.activity-log": [ + { + "type": 0, + "value": "လုပ်ဆောင်ချက်စာရင်း" + } + ], + "label.add": [ + { + "type": 0, + "value": "ထပ်ထည့်မည်" + } + ], + "label.add-description": [ + { + "type": 0, + "value": "အကြောင်းအရာဖော်ပြချက် ထည့်မည်" + } + ], + "label.add-website": [ + { + "type": 0, + "value": "ဝက်ဘ်ဆိုဒ်ထည့်မည်" + } + ], + "label.admin": [ + { + "type": 0, + "value": "အက်ဒမင်" + } + ], + "label.all": [ + { + "type": 0, + "value": "အားလုံး" + } + ], + "label.all-time": [ + { + "type": 0, + "value": "အချိန်အစမှအခုထိ" + } + ], + "label.analytics": [ + { + "type": 0, + "value": "အန်နလစ်တစ်" + } + ], + "label.average-visit-time": [ + { + "type": 0, + "value": "ဝဘက်ဘ်ဆိုဒ်တွင် ပျမ်းမျှကုန်ဆုံးချိန်" + } + ], + "label.back": [ + { + "type": 0, + "value": "နောက်သို့" + } + ], + "label.bounce-rate": [ + { + "type": 0, + "value": "Bounce နှုန်း" + } + ], + "label.browsers": [ + { + "type": 0, + "value": "ဝက်ဘ်ဘရောင်ဇာများ" + } + ], + "label.cancel": [ + { + "type": 0, + "value": "မလုပ်တော့ပါ" + } + ], + "label.change-password": [ + { + "type": 0, + "value": "စကားဝှက် ပြောင်းမည်" + } + ], + "label.cities": [ + { + "type": 0, + "value": "မြို့များ" + } + ], + "label.clear-all": [ + { + "type": 0, + "value": "အားလုံးကိုဖျက်မည်" + } + ], + "label.confirm": [ + { + "type": 0, + "value": "အတည်ပြုသည်" + } + ], + "label.confirm-password": [ + { + "type": 0, + "value": "စကားဝှက်အတည်ပြုသည်" + } + ], + "label.continue": [ + { + "type": 0, + "value": "ဆက်သွားမည်" + } + ], + "label.countries": [ + { + "type": 0, + "value": "နိုင်ငံများ" + } + ], + "label.create-team": [ + { + "type": 0, + "value": "Team ပြုလုပ်မည်" + } + ], + "label.create-user": [ + { + "type": 0, + "value": "အသုံးပြုသူထည့်မည်" + } + ], + "label.created": [ + { + "type": 0, + "value": "ပြုလုပ်ပြီးသော" + } + ], + "label.current-password": [ + { + "type": 0, + "value": "လက်ရှိစကားဝှက်" + } + ], + "label.custom-range": [ + { + "type": 0, + "value": "အချိန်အပိုင်းအခြားရွေးရန်" + } + ], + "label.dashboard": [ + { + "type": 0, + "value": "ဒက်ရှ်ဘုတ်" + } + ], + "label.data": [ + { + "type": 0, + "value": "ဒေတာ" + } + ], + "label.date-range": [ + { + "type": 0, + "value": "ရက်အပိုင်းအခြား" + } + ], + "label.default-date-range": [ + { + "type": 0, + "value": "ပုံသေ ရက်အပိုင်းအခြား" + } + ], + "label.delete": [ + { + "type": 0, + "value": "ဖျက်မည်" + } + ], + "label.delete-team": [ + { + "type": 0, + "value": "Team ကိုဖျက်မည်" + } + ], + "label.delete-user": [ + { + "type": 0, + "value": "အသုံးပြုသူကိုဖျက်မည်" + } + ], + "label.delete-website": [ + { + "type": 0, + "value": "ဝက်ဘ်ဆိုဒ်ကိုဖျက်မည်" + } + ], + "label.desktop": [ + { + "type": 0, + "value": "စားပွဲတင်ကွန်ပျူတာ" + } + ], + "label.details": [ + { + "type": 0, + "value": "အသေးစိတ်" + } + ], + "label.devices": [ + { + "type": 0, + "value": "အသုံးပြုသည့် ကိရိယာများ" + } + ], + "label.dismiss": [ + { + "type": 0, + "value": "ပိတ်ပါ" + } + ], + "label.domain": [ + { + "type": 0, + "value": "ဒိုမိန်း" + } + ], + "label.dropoff": [ + { + "type": 0, + "value": "Dropoff" + } + ], + "label.edit": [ + { + "type": 0, + "value": "ပြုပြင်မည်" + } + ], + "label.edit-dashboard": [ + { + "type": 0, + "value": "ဒက်ရှ်ဘုတ်ကို ပြုပြင်မည်" + } + ], + "label.enable-share-url": [ + { + "type": 0, + "value": "ဝေငှခြင်းကိုလင့်ကို ဖွင့်မည်" + } + ], + "label.event": [ + { + "type": 0, + "value": "အဖြစ်အပျက်" + } + ], + "label.event-data": [ + { + "type": 0, + "value": "အဖြစ်အပျက် ဒေတာ" + } + ], + "label.events": [ + { + "type": 0, + "value": "အဖြစ်အပျက်များ" + } + ], + "label.field": [ + { + "type": 0, + "value": "Field အမည်" + } + ], + "label.fields": [ + { + "type": 0, + "value": "Field အမည်များ" + } + ], + "label.filter-combined": [ + { + "type": 0, + "value": "ပေါင်းစပ်ပြီး" + } + ], + "label.filter-raw": [ + { + "type": 0, + "value": "အရှိအတိုင်း" + } + ], + "label.funnel": [ + { + "type": 0, + "value": "ဖန်နယ်" + } + ], + "label.insights": [ + { + "type": 0, + "value": "အသေးစိတ်သိမြင်နိုင်ရန်" + } + ], + "label.join": [ + { + "type": 0, + "value": "ဝင်မည်" + } + ], + "label.join-team": [ + { + "type": 0, + "value": "အသင်းဝင်မည်" + } + ], + "label.language": [ + { + "type": 0, + "value": "ဘာသာစကား" + } + ], + "label.languages": [ + { + "type": 0, + "value": "ဘာသာစကားများ" + } + ], + "label.laptop": [ + { + "type": 0, + "value": "လက်တော့ပ်" + } + ], + "label.last-days": [ + { + "type": 0, + "value": "လွန်ခဲ့သော " + }, + { + "type": 1, + "value": "x" + }, + { + "type": 0, + "value": " ရက်က" + } + ], + "label.last-hours": [ + { + "type": 0, + "value": "လွန်ခဲ့သော " + }, + { + "type": 1, + "value": "x" + }, + { + "type": 0, + "value": " နာရီက" + } + ], + "label.leave": [ + { + "type": 0, + "value": "ထွက်မည်" + } + ], + "label.leave-team": [ + { + "type": 0, + "value": "အသင်းမှထွက်မည်" + } + ], + "label.login": [ + { + "type": 0, + "value": "လော့ဂ်အင်" + } + ], + "label.logout": [ + { + "type": 0, + "value": "လော့ဂ်အောက်လုပ်မည်" + } + ], + "label.members": [ + { + "type": 0, + "value": "အဖွဲ့ဝင်များ" + } + ], + "label.mobile": [ + { + "type": 0, + "value": "မိုဘိုင်း" + } + ], + "label.more": [ + { + "type": 0, + "value": "နောက်ထပ်" + } + ], + "label.name": [ + { + "type": 0, + "value": "အမည်" + } + ], + "label.new-password": [ + { + "type": 0, + "value": "စကားဝှက်အသစ်" + } + ], + "label.none": [ + { + "type": 0, + "value": "မရှိပါ" + } + ], + "label.operating-systems": [ + { + "type": 0, + "value": "ကွန်ပျူတာလည်ပတ်မှုစနစ်" + } + ], + "label.owner": [ + { + "type": 0, + "value": "ပိုင်ဆိုင်သူ" + } + ], + "label.page-views": [ + { + "type": 0, + "value": "ဝင်ရောက်ကြည့်ရှုသူ" + } + ], + "label.pages": [ + { + "type": 0, + "value": "စာမျက်နှာများ" + } + ], + "label.password": [ + { + "type": 0, + "value": "စကားဝှက်" + } + ], + "label.powered-by": [ + { + "type": 1, + "value": "name" + }, + { + "type": 0, + "value": " ထောက်ပံ့သည်" + } + ], + "label.profile": [ + { + "type": 0, + "value": "ပရိုဖိုင်း" + } + ], + "label.queries": [ + { + "type": 0, + "value": "Queries (ကွာရီများ)" + } + ], + "label.query": [ + { + "type": 0, + "value": "Query (ကွာရီ)" + } + ], + "label.query-parameters": [ + { + "type": 0, + "value": "Query parameters (ကွာရီပါရာမီတာများ)" + } + ], + "label.realtime": [ + { + "type": 0, + "value": "အချိန်နှင့်တပြေးညီ" + } + ], + "label.referrers": [ + { + "type": 0, + "value": "ရည်ညွှန်းမှုများ" + } + ], + "label.refresh": [ + { + "type": 0, + "value": "Refresh လုပ်မည်" + } + ], + "label.regenerate": [ + { + "type": 0, + "value": "ပြန်ထုတ်မည်" + } + ], + "label.regions": [ + { + "type": 0, + "value": "ဒေသများ" + } + ], + "label.remove": [ + { + "type": 0, + "value": "ဖျက်မည်" + } + ], + "label.reports": [ + { + "type": 0, + "value": "တင်ပြမှုများ" + } + ], + "label.required": [ + { + "type": 0, + "value": "လိုအပ်သည်" + } + ], + "label.reset": [ + { + "type": 0, + "value": "ပြန်စမည်" + } + ], + "label.reset-website": [ + { + "type": 0, + "value": "ဝက်ဘ်ဆိုဒ်ဒေတာကိုဖျက်မည်" + } + ], + "label.role": [ + { + "type": 0, + "value": "အခန်းကဏ္ဍ" + } + ], + "label.run-query": [ + { + "type": 0, + "value": "Query ကိုလုပ်ဆောင်မည်" + } + ], + "label.save": [ + { + "type": 0, + "value": "သိမ်းဆည်းမည်" + } + ], + "label.screens": [ + { + "type": 0, + "value": "မြင်ကွင်းများ" + } + ], + "label.select-date": [ + { + "type": 0, + "value": "ရက်ရွေးပါ" + } + ], + "label.select-website": [ + { + "type": 0, + "value": "ဝဘက်ဘ်ဆိုဒ်ရွေးပါ" + } + ], + "label.sessions": [ + { + "type": 0, + "value": "ဆက်ရှင်များ" + } + ], + "label.settings": [ + { + "type": 0, + "value": "ဆက်တင်များ" + } + ], + "label.share-url": [ + { + "type": 0, + "value": "URL ကိုရှဲမည်" + } + ], + "label.single-day": [ + { + "type": 0, + "value": "တစ်ရက်အတွင်း" + } + ], + "label.tablet": [ + { + "type": 0, + "value": "တက်ဘလက်" + } + ], + "label.team": [ + { + "type": 0, + "value": "အသင်း" + } + ], + "label.team-guest": [ + { + "type": 0, + "value": "အသင်း ဧည့်သည်" + } + ], + "label.team-id": [ + { + "type": 0, + "value": "အသင်း အိုင်ဒီ" + } + ], + "label.team-member": [ + { + "type": 0, + "value": "အသင်းဝင်" + } + ], + "label.team-owner": [ + { + "type": 0, + "value": "အသင်းကိုပိုင်ဆိုင်သူ" + } + ], + "label.teams": [ + { + "type": 0, + "value": "အသင်းများ" + } + ], + "label.theme": [ + { + "type": 0, + "value": "Theme (အပြင်အဆင်)" + } + ], + "label.this-month": [ + { + "type": 0, + "value": "ယခုလ" + } + ], + "label.this-week": [ + { + "type": 0, + "value": "ယခုအပတ်" + } + ], + "label.this-year": [ + { + "type": 0, + "value": "ယခုနှစ်" + } + ], + "label.timezone": [ + { + "type": 0, + "value": "အချိန်ဇုန်" + } + ], + "label.title": [ + { + "type": 0, + "value": "ခေါင်းစဥ်" + } + ], + "label.today": [ + { + "type": 0, + "value": "ယနေ့" + } + ], + "label.toggle-charts": [ + { + "type": 0, + "value": "ဇယားများကို အဖွင့်အပိတ်လုပ်မည်" + } + ], + "label.tracking-code": [ + { + "type": 0, + "value": "ထရက်လုပ်သည့် ကုဒ်" + } + ], + "label.unique-visitors": [ + { + "type": 0, + "value": "ဝင်ရောက်သူ (ထပ်ခြင်းမရှိ)" + } + ], + "label.unknown": [ + { + "type": 0, + "value": "မသိသော" + } + ], + "label.url": [ + { + "type": 0, + "value": "URL" + } + ], + "label.urls": [ + { + "type": 0, + "value": "URL များ" + } + ], + "label.user": [ + { + "type": 0, + "value": "အသုံးပြုသူ" + } + ], + "label.username": [ + { + "type": 0, + "value": "အသုံးပြုသူအမည်" + } + ], + "label.users": [ + { + "type": 0, + "value": "အသုံးပြုသူများ" + } + ], + "label.view": [ + { + "type": 0, + "value": "ဝင်ရောက်ကြည့်ရှုမှု" + } + ], + "label.view-details": [ + { + "type": 0, + "value": "အသေးစိတ်ကို ကြည့်ရှုမည်" + } + ], + "label.view-only": [ + { + "type": 0, + "value": "ဝင်ရောက်ကြည့်ရှုမှုများသာ" + } + ], + "label.views": [ + { + "type": 0, + "value": "ဝင်ရောက်ကြည့်ရှုမှုများ" + } + ], + "label.visitors": [ + { + "type": 0, + "value": "ဝင်ရောက်ကြည့်ရှုသူများ" + } + ], + "label.website": [ + { + "type": 0, + "value": "ဝက်ဘ်ဆိုဒ်" + } + ], + "label.website-id": [ + { + "type": 0, + "value": "ဝက်ဘ်ဆိုဒ် အိုင်ဒီ" + } + ], + "label.websites": [ + { + "type": 0, + "value": "ဝက်ဘ်ဆိုဒ်များ" + } + ], + "label.window": [ + { + "type": 0, + "value": "ဝင်းဒိုး" + } + ], + "label.yesterday": [ + { + "type": 0, + "value": "မနေ့က" + } + ], + "labels.after": [ + { + "type": 0, + "value": "ပြီးနောက်" + } + ], + "labels.average": [ + { + "type": 0, + "value": "ပျမ်းမျှ" + } + ], + "labels.before": [ + { + "type": 0, + "value": "မတိုင်မီ" + } + ], + "labels.breakdown": [ + { + "type": 0, + "value": "ခွဲခြမ်းစိတ်ဖြာမှု" + } + ], + "labels.contains": [ + { + "type": 0, + "value": "ပါဝင်သည်" + } + ], + "labels.create-report": [ + { + "type": 0, + "value": "ရီပို့လုပ်မည်" + } + ], + "labels.description": [ + { + "type": 0, + "value": "ရှင်းပြချက်" + } + ], + "labels.does-not-contain": [ + { + "type": 0, + "value": "မပါဝင်ပါ" + } + ], + "labels.does-not-equal": [ + { + "type": 0, + "value": "မတူညီပါ" + } + ], + "labels.equals": [ + { + "type": 0, + "value": "တူညီသည်" + } + ], + "labels.false": [ + { + "type": 0, + "value": "မှားသည်" + } + ], + "labels.filters": [ + { + "type": 0, + "value": "Filter များ" + } + ], + "labels.greater-than": [ + { + "type": 0, + "value": "ထက်ပို၍ကြီးသည်" + } + ], + "labels.greater-than-equals": [ + { + "type": 0, + "value": "ထက်ပို၍ကြီးသည်သို့မဟုတ်တူသည်" + } + ], + "labels.less-than": [ + { + "type": 0, + "value": "ထက်ပို၍ငယ်သည်" + } + ], + "labels.less-than-equals": [ + { + "type": 0, + "value": "ထက်ပို၍ငယ်သည်သို့မဟုတ်တူသည်" + } + ], + "labels.max": [ + { + "type": 0, + "value": "အများဆုံး" + } + ], + "labels.min": [ + { + "type": 0, + "value": "အနည်းဆုံး" + } + ], + "labels.overview": [ + { + "type": 0, + "value": "အပေါ်ယံမြင်ကွင်း" + } + ], + "labels.sum": [ + { + "type": 0, + "value": "ပေါင်းလဒ်" + } + ], + "labels.total": [ + { + "type": 0, + "value": "စုစုပေါင်း" + } + ], + "labels.total-records": [ + { + "type": 0, + "value": "မှတ်တမ်းစုစုပေါင်း" + } + ], + "labels.true": [ + { + "type": 0, + "value": "မှန်သည်" + } + ], + "labels.type": [ + { + "type": 0, + "value": "အမျိုးအစား" + } + ], + "labels.unique": [ + { + "type": 0, + "value": "Unique" + } + ], + "labels.untitled": [ + { + "type": 0, + "value": "ခေါင်းစဉ်မရှိ" + } + ], + "labels.value": [ + { + "type": 0, + "value": "တန်ဖိုး" + } + ], + "message.active-users": [ + { + "type": 1, + "value": "x" + }, + { + "type": 0, + "value": " လက်ရှိအသုံးပြုနေသူ " + }, + { + "offset": 0, + "options": { + "one": { + "value": [ + { + "type": 0, + "value": "ယောက်" + } + ] + }, + "other": { + "value": [ + { + "type": 0, + "value": "ယောက်" + } + ] + } + }, + "pluralType": "cardinal", + "type": 6, + "value": "x" + } + ], + "message.confirm-delete": [ + { + "type": 1, + "value": "target" + }, + { + "type": 0, + "value": " ကို ဖျက်ရန် သေချာပါသလား?" + } + ], + "message.confirm-leave": [ + { + "type": 1, + "value": "target" + }, + { + "type": 0, + "value": " ကို ထွက်ရန် သေချာပါသလား?" + } + ], + "message.confirm-reset": [ + { + "type": 1, + "value": "target" + }, + { + "type": 0, + "value": " ကို ဖျက်၍ပြန်စလုပ်ရန် သေချာပါသလား?" + } + ], + "message.delete-account": [ + { + "type": 0, + "value": "ဤအကောင့်ကိုဖျက်ရန် " + }, + { + "type": 1, + "value": "confirmation" + }, + { + "type": 0, + "value": " ကို ရိုက်ထည့်ပေးပါ." + } + ], + "message.delete-website": [ + { + "type": 0, + "value": "ဤ ဝက်ဘ်ဆိုဒ်ကိုဖျက်ရန် " + }, + { + "type": 1, + "value": "confirmation" + }, + { + "type": 0, + "value": " ကို ရိုက်ထည့်ပေးပါ" + } + ], + "message.delete-website-warning": [ + { + "type": 0, + "value": "ဝက်ဘ်ဆိုဒ် ဒေတာအကုန် ဖျက်မည်" + } + ], + "message.error": [ + { + "type": 0, + "value": "မှားယွင်းမှုတစ်ခု ရှိသွားပါသည်" + } + ], + "message.event-log": [ + { + "type": 1, + "value": "url" + }, + { + "type": 0, + "value": " တွင် " + }, + { + "type": 1, + "value": "event" + } + ], + "message.go-to-settings": [ + { + "type": 0, + "value": "ဆက်တင်သို့ သွားရန်" + } + ], + "message.incorrect-username-password": [ + { + "type": 0, + "value": "အသုံးပြုသူအမည် သို့မဟုတ် စကားဝှက် မှားနေသည်" + } + ], + "message.invalid-domain": [ + { + "type": 0, + "value": "ဒိုမိန်း မမှန်ပါ http/https. မပါရပါ" + } + ], + "message.min-password-length": [ + { + "type": 0, + "value": "အနည်းဆုံး " + }, + { + "type": 1, + "value": "n" + }, + { + "type": 0, + "value": " character ရှိရမည်" + } + ], + "message.new-version-available": [ + { + "type": 0, + "value": "အူမာမီ " + }, + { + "type": 1, + "value": "version" + }, + { + "type": 0, + "value": " အသစ်ထွက်နေပါပြီ" + } + ], + "message.no-data-available": [ + { + "type": 0, + "value": "ဒေတာ မရှိပါ" + } + ], + "message.no-event-data": [ + { + "type": 0, + "value": "အဖြစ်အပျက်ဒေတာ မရှိပါ" + } + ], + "message.no-match-password": [ + { + "type": 0, + "value": "စကားဝှက် မှားနေသည်" + } + ], + "message.no-results-found": [ + { + "type": 0, + "value": "ရလဒ်မရှိပါ" + } + ], + "message.no-team-websites": [ + { + "type": 0, + "value": "ဤအသင်းတွင် ဝက်ဘ်ဆိုက်မရှိသေးပါ" + } + ], + "message.no-teams": [ + { + "type": 0, + "value": "အသင်း မပြုလုပ်ရသေးပါ" + } + ], + "message.no-users": [ + { + "type": 0, + "value": "အသုံးပြုသူ မရှိသေးပါ" + } + ], + "message.no-websites-configured": [ + { + "type": 0, + "value": "ဝက်ဘ်ဆိုဒ်တစ်ခုမှ မထည့်ရသေးပါ" + } + ], + "message.page-not-found": [ + { + "type": 0, + "value": "ဤစာမျက်နှာသည် မရှိပါ" + } + ], + "message.reset-website": [ + { + "type": 0, + "value": "ဤ ဝက်ဘ်ဆိုဒ်ဒေတာကိုဖျက်၍ ပြန်စလုပ်ရန် အောက်တွင် " + }, + { + "type": 1, + "value": "confirmation" + }, + { + "type": 0, + "value": " ကို ရိုက်ထည့်ပေးပါ" + } + ], + "message.reset-website-warning": [ + { + "type": 0, + "value": "ဤဝက်ဘ်ဆိုဒ်က စာရင်းအချက်အလက်များကို ဖျက်မည်၊ ဆက်တင်ဒေတာများ မပါပါ" + } + ], + "message.saved": [ + { + "type": 0, + "value": "မှတ်သားပြီး" + } + ], + "message.share-url": [ + { + "type": 0, + "value": "သင့်ဝက်ဆိုဒ်ဘ်၏ စာရင်းအချက်အလက်များကို အောက်ပါ URL တွင် ဝင်ရောက်ကြည့်ရှုနိုင်သည်" + } + ], + "message.team-already-member": [ + { + "type": 0, + "value": "ဤအသင်းတွင် ဝင်ပြီးသားဖြစ်နေသည်" + } + ], + "message.team-not-found": [ + { + "type": 0, + "value": "အသင်း မရှိပါ" + } + ], + "message.team-websites-info": [ + { + "type": 0, + "value": "ဤဝက်ဘ်ဆိုဒ်များကို အသင်းထဲမှ လူတိုင်းဝင်ကြည့်နိုင်သည်" + } + ], + "message.tracking-code": [ + { + "type": 0, + "value": "ဤဝက်ဘ်ဆိုဒ်၏ ဒေတာကိုကောက်ခံရန် အောက်ပါ code ကို သင်၏ HTML တွင်ထည့်ပါ" + } + ], + "message.user-deleted": [ + { + "type": 0, + "value": "အသုံးပြုသူ ဖျက်ပြီးပါပြီ" + } + ], + "message.visitor-log": [ + { + "type": 1, + "value": "country" + }, + { + "type": 0, + "value": " မှ " + }, + { + "type": 1, + "value": "browser" + }, + { + "type": 0, + "value": " ဖြင့် " + }, + { + "type": 1, + "value": "os" + }, + { + "type": 0, + "value": " " + }, + { + "type": 1, + "value": "device" + }, + { + "type": 0, + "value": " တွင် ဝင်ရောက်ကြည့်ရှုသူ" + } + ] +} From d3e175a31d74414716f4d044c27aa0cee287a189 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 17 Aug 2023 21:25:12 -0700 Subject: [PATCH 083/357] Hide edit button cloudMode. --- components/pages/settings/websites/WebsitesList.js | 10 +++++++++- components/pages/settings/websites/WebsitesTable.js | 3 ++- components/pages/websites/WebsitesPage.js | 12 ++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/components/pages/settings/websites/WebsitesList.js b/components/pages/settings/websites/WebsitesList.js index 538fc61ad..f1a2eb0f8 100644 --- a/components/pages/settings/websites/WebsitesList.js +++ b/components/pages/settings/websites/WebsitesList.js @@ -9,7 +9,14 @@ import useUser from 'hooks/useUser'; import { ROLES } from 'lib/constants'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -export function WebsitesList({ showTeam, showHeader = true, includeTeams, onlyTeams, fetch }) { +export function WebsitesList({ + showTeam, + showEditButton = true, + showHeader = true, + includeTeams, + onlyTeams, + fetch, +}) { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); @@ -59,6 +66,7 @@ export function WebsitesList({ showTeam, showHeader = true, includeTeams, onlyTe - {(!showTeam || ownerId === user.id) && ( + {showEditButton && (!showTeam || ownerId === user.id) && ( - - )} + + + {hasData && ( From fefea8898b9d1b84eeffc0355a7f7c49df5b7ca4 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 17 Aug 2023 21:34:52 -0700 Subject: [PATCH 085/357] Update react-basics. --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d529ba771..e1361d208 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.96.0", + "react-basics": "^0.98.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/yarn.lock b/yarn.lock index 350e483fe..e18f833ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,10 +7557,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.96.0: - version "0.96.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.96.0.tgz#e5e72201abdccdda94b952ef605163ca11772d8f" - integrity sha512-WNAxP+0xBtUNgEXrL8aW6UQMmD6WoX9My0VW6uq+Q262DOPTU3zPtWl+9vvES4pF3tPJCFvmFAlK/Alw9+XKVQ== +react-basics@^0.98.0: + version "0.98.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.98.0.tgz#b207bedbd9dac749d28ea6de2197a0efe648b78c" + integrity sha512-ebUigu+s6Iusq14EZTFTTUzdDPYFQEZjeD4feeq3o7dE+ndOVnajEdQ2va/x6CsRBUsWgjLJipfQi0XIrxYupA== dependencies: classnames "^2.3.1" date-fns "^2.29.3" From 87cf5d52985e1dbeb0a2b07c86ef443bb4bb2c6d Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Aug 2023 11:10:44 -0700 Subject: [PATCH 086/357] Fix UserWebsites. --- .../pages/settings/users/UserWebsites.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/components/pages/settings/users/UserWebsites.js b/components/pages/settings/users/UserWebsites.js index 144fae444..d61df4d8a 100644 --- a/components/pages/settings/users/UserWebsites.js +++ b/components/pages/settings/users/UserWebsites.js @@ -2,12 +2,19 @@ import { Loading } from 'react-basics'; import useApi from 'hooks/useApi'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; export function UserWebsites({ userId }) { const { formatMessage, messages } = useMessages(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); const { get, useQuery } = useApi(); - const { data, isLoading } = useQuery(['user:websites', userId], () => - get(`/users/${userId}/websites`), + const { data, isLoading } = useQuery(['user:websites', userId, filter, page, pageSize], () => + get(`/users/${userId}/websites`, { + filter, + page, + pageSize, + }), ); const hasData = data && data.length !== 0; @@ -17,7 +24,15 @@ export function UserWebsites({ userId }) { return (
- {hasData && } + {hasData && ( + + )} {!hasData && formatMessage(messages.noDataAvailable)}
); From 1aa407027ee234711bb60dfbe163ddd3f3fc4699 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Aug 2023 11:16:03 -0700 Subject: [PATCH 087/357] Remove password. --- queries/admin/user.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/queries/admin/user.ts b/queries/admin/user.ts index dfb923f31..ca459b9fd 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -37,7 +37,7 @@ export async function getUserByUsername(username: string, options: GetUserOption } export async function getUsers( - UserSearchFilter: UserSearchFilter = {}, + UserSearchFilter: UserSearchFilter, options?: { include?: Prisma.UserInclude }, ): Promise> { const { teamId, filter, filterType = USER_FILTER_TYPES.all } = UserSearchFilter; @@ -72,14 +72,22 @@ export async function getUsers( ...UserSearchFilter, }); - const users = await prisma.client.user.findMany({ - where: { - ...where, - deletedAt: null, - }, - ...pageFilters, - ...(options?.include && { include: options.include }), - }); + const users = await prisma.client.user + .findMany({ + where: { + ...where, + deletedAt: null, + }, + ...pageFilters, + ...(options?.include && { include: options.include }), + }) + .then(a => { + return a.map(a => { + const { password, ...rest } = a; + + return rest; + }); + }); const count = await prisma.client.user.count({ where: { ...where, From a296ecb96b321b4c3821d600d30b38632a735de3 Mon Sep 17 00:00:00 2001 From: Yash Khandelwal Date: Sat, 19 Aug 2023 00:29:06 +0530 Subject: [PATCH 088/357] Fixed error when opening user websites in the settings. --- .../pages/settings/users/UserWebsites.js | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/components/pages/settings/users/UserWebsites.js b/components/pages/settings/users/UserWebsites.js index 144fae444..8189ad8dd 100644 --- a/components/pages/settings/users/UserWebsites.js +++ b/components/pages/settings/users/UserWebsites.js @@ -1,25 +1,38 @@ -import { Loading } from 'react-basics'; import useApi from 'hooks/useApi'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; +import Page from 'components/layout/Page'; +import useConfig from 'hooks/useConfig'; export function UserWebsites({ userId }) { + const { cloudMode } = useConfig(); const { formatMessage, messages } = useMessages(); + const { filter, page, pageSize, handlePageSizeChange, handleFilterChange, handlePageChange } = useApiFilter(); const { get, useQuery } = useApi(); - const { data, isLoading } = useQuery(['user:websites', userId], () => - get(`/users/${userId}/websites`), + const { data, isLoading, error } = useQuery(['user:websites', userId, filter, page, pageSize], () => + get(`/users/${userId}/websites`, { + filter, + page, + pageSize, + }), ); const hasData = data && data.length !== 0; - if (isLoading) { - return ; - } - return ( -
- {hasData && } + + {hasData && ( + ) + } {!hasData && formatMessage(messages.noDataAvailable)} -
+ ); } From c213b6414f0943dac8050fbc1db33f2d2c7203c6 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Aug 2023 12:39:31 -0700 Subject: [PATCH 089/357] Default list size. --- lib/prisma.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/prisma.ts b/lib/prisma.ts index 12bafa513..8fa7e8ae4 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -185,7 +185,9 @@ function getPageFilters(filters: SearchFilter): [ orderBy: string; }, ] { - const { pageSize = 10, page = 1, orderBy } = filters; + const pageSize = filters?.pageSize || 10; + const page = filters?.page || 1; + const orderBy = filters?.orderBy; return [ { From 7d5a24044a475717b36931c8f4dd724be3563c5b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 18 Aug 2023 21:52:59 -0700 Subject: [PATCH 090/357] Code cleanup. --- .eslintrc.json | 3 ++- components/common/SettingsTable.js | 6 ++--- components/pages/reports/ReportTemplates.js | 4 ++-- components/pages/reports/ReportsPage.js | 6 ++--- .../pages/settings/teams/TeamWebsitesTable.js | 6 +---- .../pages/settings/users/UserWebsites.js | 22 +++++++++---------- .../pages/settings/websites/WebsitesTable.js | 6 ++--- .../pages/websites/WebsiteReportsPage.js | 6 ++--- lib/prisma.ts | 4 +--- queries/admin/user.ts | 14 +++++------- 10 files changed, 34 insertions(+), 43 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 25e83d5ac..f6d90ccab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -51,7 +51,8 @@ "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-empty-interface": "off" + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] }, "globals": { "React": "writable" diff --git a/components/common/SettingsTable.js b/components/common/SettingsTable.js index e9491331e..eb7a64112 100644 --- a/components/common/SettingsTable.js +++ b/components/common/SettingsTable.js @@ -1,4 +1,4 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import Empty from 'components/common/Empty'; import useMessages from 'hooks/useMessages'; import { useState } from 'react'; import { @@ -36,7 +36,7 @@ export function SettingsTable({ return ( <> - {showSearch && ( + {showSearch && !!value.length && ( )} {value.length === 0 && filterValue && ( - + )} {value.length > 0 && ( diff --git a/components/pages/reports/ReportTemplates.js b/components/pages/reports/ReportTemplates.js index 0f5e710d5..57cb113ee 100644 --- a/components/pages/reports/ReportTemplates.js +++ b/components/pages/reports/ReportTemplates.js @@ -30,7 +30,7 @@ function ReportItem({ title, description, url, icon }) { ); } -export function ReportTemplates() { +export function ReportTemplates({ showHeader = true }) { const { formatMessage, labels } = useMessages(); const reports = [ @@ -56,7 +56,7 @@ export function ReportTemplates() { return ( - + {showHeader && }
{reports.map(({ title, description, url, icon }) => { return ( diff --git a/components/pages/reports/ReportsPage.js b/components/pages/reports/ReportsPage.js index 95959832e..7ae102b02 100644 --- a/components/pages/reports/ReportsPage.js +++ b/components/pages/reports/ReportsPage.js @@ -7,7 +7,7 @@ import { Button, Icon, Icons, Text } from 'react-basics'; import ReportsTable from './ReportsTable'; export function ReportsPage() { - const { formatMessage, labels, messages } = useMessages(); + const { formatMessage, labels } = useMessages(); const { reports, error, @@ -47,9 +47,7 @@ export function ReportsPage() { showDomain={true} /> )} - {!hasData && ( - - )} + {!hasData && } ); } diff --git a/components/pages/settings/teams/TeamWebsitesTable.js b/components/pages/settings/teams/TeamWebsitesTable.js index 564c8a782..f89a91661 100644 --- a/components/pages/settings/teams/TeamWebsitesTable.js +++ b/components/pages/settings/teams/TeamWebsitesTable.js @@ -55,11 +55,7 @@ export function TeamWebsitesTable({ {canRemove && ( - + )} ); diff --git a/components/pages/settings/users/UserWebsites.js b/components/pages/settings/users/UserWebsites.js index 709f6e4d2..df8c9f572 100644 --- a/components/pages/settings/users/UserWebsites.js +++ b/components/pages/settings/users/UserWebsites.js @@ -1,24 +1,25 @@ +import Page from 'components/layout/Page'; import useApi from 'hooks/useApi'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; -import useMessages from 'hooks/useMessages'; import useApiFilter from 'hooks/useApiFilter'; export function UserWebsites({ userId }) { - const { formatMessage, messages } = useMessages(); const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = useApiFilter(); const { get, useQuery } = useApi(); - const { data, isLoading } = useQuery(['user:websites', userId, filter, page, pageSize], () => - get(`/users/${userId}/websites`, { - filter, - page, - pageSize, - }), + const { data, isLoading, error } = useQuery( + ['user:websites', userId, filter, page, pageSize], + () => + get(`/users/${userId}/websites`, { + filter, + page, + pageSize, + }), ); const hasData = data && data.length !== 0; return ( -
+ {hasData && ( )} - {!hasData && formatMessage(messages.noDataAvailable)} -
+ ); } diff --git a/components/pages/settings/websites/WebsitesTable.js b/components/pages/settings/websites/WebsitesTable.js index 08c906aa0..89898c811 100644 --- a/components/pages/settings/websites/WebsitesTable.js +++ b/components/pages/settings/websites/WebsitesTable.js @@ -1,7 +1,7 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import Link from 'next/link'; import { Button, Text, Icon, Icons } from 'react-basics'; import SettingsTable from 'components/common/SettingsTable'; +import Empty from 'components/common/Empty'; import useMessages from 'hooks/useMessages'; import useConfig from 'hooks/useConfig'; import useUser from 'hooks/useUser'; @@ -15,7 +15,7 @@ export function WebsitesTable({ showTeam, showEditButton, }) { - const { formatMessage, labels, messages } = useMessages(); + const { formatMessage, labels } = useMessages(); const { openExternal } = useConfig(); const { user } = useUser(); @@ -82,7 +82,7 @@ export function WebsitesTable({ }} )} - {!showTable && } + {!showTable && } ); } diff --git a/components/pages/websites/WebsiteReportsPage.js b/components/pages/websites/WebsiteReportsPage.js index b04c50d1d..be4ee8002 100644 --- a/components/pages/websites/WebsiteReportsPage.js +++ b/components/pages/websites/WebsiteReportsPage.js @@ -1,5 +1,5 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import Page from 'components/layout/Page'; +import Empty from 'components/common/Empty'; import ReportsTable from 'components/pages/reports/ReportsTable'; import { useMessages, useWebsiteReports } from 'hooks'; import Link from 'next/link'; @@ -7,7 +7,7 @@ import { Button, Flexbox, Icon, Icons, Text } from 'react-basics'; import WebsiteHeader from './WebsiteHeader'; export function WebsiteReportsPage({ websiteId }) { - const { formatMessage, labels, messages } = useMessages(); + const { formatMessage, labels } = useMessages(); const { reports, error, @@ -48,7 +48,7 @@ export function WebsiteReportsPage({ websiteId }) { filterValue={filter} /> )} - {!hasData && } + {!hasData && } ); } diff --git a/lib/prisma.ts b/lib/prisma.ts index 8fa7e8ae4..a9832c28c 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -185,9 +185,7 @@ function getPageFilters(filters: SearchFilter): [ orderBy: string; }, ] { - const pageSize = filters?.pageSize || 10; - const page = filters?.page || 1; - const orderBy = filters?.orderBy; + const { pageSize = 10, page = 1, orderBy } = filters || {}; return [ { diff --git a/queries/admin/user.ts b/queries/admin/user.ts index ca459b9fd..dfe8ea283 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -37,10 +37,10 @@ export async function getUserByUsername(username: string, options: GetUserOption } export async function getUsers( - UserSearchFilter: UserSearchFilter, + searchFilter: UserSearchFilter, options?: { include?: Prisma.UserInclude }, ): Promise> { - const { teamId, filter, filterType = USER_FILTER_TYPES.all } = UserSearchFilter; + const { teamId, filter, filterType = USER_FILTER_TYPES.all } = searchFilter; const mode = prisma.getSearchMode(); const where: Prisma.UserWhereInput = { @@ -67,9 +67,10 @@ export async function getUsers( }, }), }; + const [pageFilters, getParameters] = prisma.getPageFilters({ orderBy: 'username', - ...UserSearchFilter, + ...searchFilter, }); const users = await prisma.client.user @@ -82,12 +83,9 @@ export async function getUsers( ...(options?.include && { include: options.include }), }) .then(a => { - return a.map(a => { - const { password, ...rest } = a; - - return rest; - }); + return a.map(({ password, ...rest }) => rest); }); + const count = await prisma.client.user.count({ where: { ...where, From 7a7233ead4630d12aea291ed44a4caeffaa7b00f Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sat, 19 Aug 2023 22:23:15 -0700 Subject: [PATCH 091/357] Add api validations. --- lib/middleware.ts | 33 +++++++--- lib/types.ts | 17 ++++- lib/yup.ts | 19 ++++++ pages/api/auth/login.ts | 31 +++++---- pages/api/event-data/events.ts | 29 ++++++--- pages/api/event-data/fields.ts | 23 +++++-- pages/api/event-data/stats.ts | 23 +++++-- pages/api/me/password.ts | 16 ++++- pages/api/me/teams.ts | 19 +++++- pages/api/me/websites.ts | 18 +++++- pages/api/realtime/[id].ts | 16 ++++- pages/api/reports/[id].ts | 36 +++++++++-- pages/api/reports/funnel.ts | 23 ++++++- pages/api/reports/index.ts | 33 ++++++++-- pages/api/reports/insights.ts | 39 ++++++++++- pages/api/reports/retention.ts | 31 +++++---- pages/api/send.ts | 68 +++++++++++--------- pages/api/share/[id].ts | 13 +++- pages/api/teams/[id]/index.ts | 22 ++++++- pages/api/teams/[id]/users/[userId].ts | 14 +++- pages/api/teams/[id]/users/index.ts | 25 +------ pages/api/teams/[id]/websites/[websiteId].ts | 13 +++- pages/api/teams/[id]/websites/index.ts | 21 +++++- pages/api/teams/index.ts | 20 +++++- pages/api/teams/join.ts | 21 ++++-- pages/api/users/[id]/index.ts | 20 +++++- pages/api/users/[id]/teams.ts | 15 ++++- pages/api/users/[id]/usage.ts | 14 +++- pages/api/users/[id]/websites.ts | 25 +++++-- pages/api/users/index.ts | 24 ++++++- pages/api/websites/[id]/active.ts | 12 +++- pages/api/websites/[id]/daterange.ts | 12 +++- pages/api/websites/[id]/events.ts | 22 +++++-- pages/api/websites/[id]/index.ts | 12 +++- pages/api/websites/[id]/metrics.ts | 12 +++- pages/api/websites/[id]/pageviews.ts | 12 +++- pages/api/websites/[id]/reports.ts | 12 +++- pages/api/websites/[id]/reset.ts | 12 +++- pages/api/websites/[id]/stats.ts | 12 +++- pages/api/websites/[id]/values.ts | 12 +++- pages/api/websites/index.ts | 19 +++++- 41 files changed, 690 insertions(+), 180 deletions(-) create mode 100644 lib/yup.ts diff --git a/lib/middleware.ts b/lib/middleware.ts index 414cab239..0cb0cb880 100644 --- a/lib/middleware.ts +++ b/lib/middleware.ts @@ -1,19 +1,20 @@ +import redis from '@umami/redis-client'; +import cors from 'cors'; +import debug from 'debug'; +import { getAuthToken, parseShareToken } from 'lib/auth'; +import { ROLES } from 'lib/constants'; +import { isUuid, secret } from 'lib/crypto'; +import { findSession } from 'lib/session'; import { - createMiddleware, - unauthorized, badRequest, + createMiddleware, parseSecureToken, tooManyRequest, + unauthorized, } from 'next-basics'; -import debug from 'debug'; -import cors from 'cors'; -import redis from '@umami/redis-client'; -import { findSession } from 'lib/session'; -import { getAuthToken, parseShareToken } from 'lib/auth'; -import { secret, isUuid } from 'lib/crypto'; -import { ROLES } from 'lib/constants'; -import { getUserById } from '../queries'; import { NextApiRequestCollect } from 'pages/api/send'; +import { getUserById } from '../queries'; +import { NextApiRequestQueryBody } from './types'; const log = debug('umami:middleware'); @@ -75,3 +76,15 @@ export const useAuth = createMiddleware(async (req, res, next) => { next(); }); + +export const useValidate = createMiddleware(async (req: any, res, next) => { + try { + const { yup } = req as NextApiRequestQueryBody; + + yup[req.method].validateSync({ ...req.query, ...req.body }); + } catch (e: any) { + return badRequest(res, e.message); + } + + next(); +}); diff --git a/lib/types.ts b/lib/types.ts index 3f3ac5337..3f3839a4d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -5,11 +5,13 @@ import { EVENT_TYPE, KAFKA_TOPIC, REPORT_FILTER_TYPES, + REPORT_TYPES, ROLES, TEAM_FILTER_TYPES, USER_FILTER_TYPES, WEBSITE_FILTER_TYPES, } from './constants'; +import * as yup from 'yup'; type ObjectValues = T[keyof T]; @@ -18,6 +20,8 @@ export type Role = ObjectValues; export type EventType = ObjectValues; export type DynamicDataType = ObjectValues; export type KafkaTopic = ObjectValues; +export type ReportType = ObjectValues; + export type ReportSearchFilterType = ObjectValues; export type UserSearchFilterType = ObjectValues; export type WebsiteSearchFilterType = ObjectValues; @@ -47,8 +51,8 @@ export interface ReportSearchFilter extends SearchFilter export interface SearchFilter { filter?: string; filterType?: T; - pageSize?: number; - page?: number; + pageSize: number; + page: number; orderBy?: string; } @@ -76,11 +80,19 @@ export interface Auth { }; } +export interface YupRequest { + GET?: yup.ObjectSchema; + POST?: yup.ObjectSchema; + PUT?: yup.ObjectSchema; + DELETE?: yup.ObjectSchema; +} + export interface NextApiRequestQueryBody extends NextApiRequest { auth?: Auth; query: TQuery & { [key: string]: string | string[] }; body: TBody; headers: any; + yup: YupRequest; } export interface NextApiRequestAuth extends NextApiRequest { @@ -168,7 +180,6 @@ export interface RealtimeUpdate { export interface DateRange { startDate: Date; endDate: Date; - unit: string; value: string; } diff --git a/lib/yup.ts b/lib/yup.ts new file mode 100644 index 000000000..a9d210285 --- /dev/null +++ b/lib/yup.ts @@ -0,0 +1,19 @@ +import * as yup from 'yup'; + +export function getDateRangeValidation() { + return { + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + }; +} + +// ex: /funnel|insights|retention/i +export function getFilterValidation(matchRegex) { + return { + filter: yup.string(), + filterType: yup.string().matches(matchRegex), + pageSize: yup.number().integer().positive().max(200), + page: yup.number().integer().positive(), + orderBy: yup.string(), + }; +} diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts index b9a2be000..47521084b 100644 --- a/pages/api/auth/login.ts +++ b/pages/api/auth/login.ts @@ -1,19 +1,20 @@ +import redis from '@umami/redis-client'; import debug from 'debug'; +import { setAuthKey } from 'lib/auth'; +import { secret } from 'lib/crypto'; +import { useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody, User } from 'lib/types'; import { NextApiResponse } from 'next'; import { - ok, - unauthorized, - badRequest, checkPassword, createSecureToken, - methodNotAllowed, forbidden, + methodNotAllowed, + ok, + unauthorized, } from 'next-basics'; -import redis from '@umami/redis-client'; import { getUserByUsername } from 'queries'; -import { secret } from 'lib/crypto'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { setAuthKey } from 'lib/auth'; +import * as yup from 'yup'; const log = debug('umami:auth'); @@ -27,6 +28,13 @@ export interface LoginResponse { user: User; } +const schema = { + POST: yup.object().shape({ + username: yup.string().required(), + password: yup.string().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -35,13 +43,12 @@ export default async ( return forbidden(res); } + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { username, password } = req.body; - if (!username || !password) { - return badRequest(res); - } - const user = await getUserByUsername(username, { includePassword: true }); if (user && checkPassword(password, user.password)) { diff --git a/pages/api/event-data/events.ts b/pages/api/event-data/events.ts index 9f8f964b3..da0afc65d 100644 --- a/pages/api/event-data/events.ts +++ b/pages/api/event-data/events.ts @@ -1,26 +1,37 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getEventDataEvents } from 'queries'; +import * as yup from 'yup'; -export interface EventDataEventsRequestQuery { +export interface EventDataFieldsRequestQuery { websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; - event?: string; + startAt: string; + endAt: string; + event: string; } +const schema = { + GET: yup.object().shape({ + websiteId: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + event: yup.string().required(), + }), +}; + export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { const { websiteId, startAt, endAt, event } = req.query; diff --git a/pages/api/event-data/fields.ts b/pages/api/event-data/fields.ts index b6a731336..1cd24fe65 100644 --- a/pages/api/event-data/fields.ts +++ b/pages/api/event-data/fields.ts @@ -1,19 +1,27 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getEventDataFields } from 'queries'; +import * as yup from 'yup'; export interface EventDataFieldsRequestQuery { websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; + startAt: string; + endAt: string; field?: string; } +const schema = { + GET: yup.object().shape({ + websiteId: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + field: yup.string(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -21,6 +29,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { const { websiteId, startAt, endAt, field } = req.query; diff --git a/pages/api/event-data/stats.ts b/pages/api/event-data/stats.ts index 4ba843bef..b7b70dbfe 100644 --- a/pages/api/event-data/stats.ts +++ b/pages/api/event-data/stats.ts @@ -1,18 +1,24 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; -import { getEventDataStats } from 'queries'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import * as yup from 'yup'; export interface EventDataStatsRequestQuery { websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; + startAt: string; + endAt: string; } +const schema = { + GET: yup.object().shape({ + websiteId: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -20,6 +26,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { const { websiteId, startAt, endAt } = req.query; diff --git a/pages/api/me/password.ts b/pages/api/me/password.ts index f9f60fc53..6f49a182e 100644 --- a/pages/api/me/password.ts +++ b/pages/api/me/password.ts @@ -1,15 +1,16 @@ +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, User } from 'lib/types'; -import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, checkPassword, + forbidden, hashPassword, methodNotAllowed, - forbidden, ok, } from 'next-basics'; import { getUserById, updateUser } from 'queries'; +import * as yup from 'yup'; export interface UserPasswordRequestQuery { id: string; @@ -20,6 +21,14 @@ export interface UserPasswordRequestBody { newPassword: string; } +const schema = { + POST: yup.object().shape({ + id: yup.string().uuid().required(), + currentPassword: yup.string().required(), + newPassword: yup.string().min(8).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -30,6 +39,9 @@ export default async ( await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { currentPassword, newPassword } = req.body; const { id } = req.auth.user; diff --git a/pages/api/me/teams.ts b/pages/api/me/teams.ts index d323043b0..d394ef07d 100644 --- a/pages/api/me/teams.ts +++ b/pages/api/me/teams.ts @@ -1,10 +1,20 @@ -import { useCors } from 'lib/middleware'; +import { useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userTeams from 'pages/api/users/[id]/teams'; +import * as yup from 'yup'; -export interface MyTeamsRequestQuery extends SearchFilter {} +export interface MyTeamsRequestQuery extends SearchFilter { + id: string; +} + +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Owner/i), + }), +}; export default async ( req: NextApiRequestQueryBody, @@ -12,7 +22,12 @@ export default async ( ) => { await useCors(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { + req.query.id = req.auth.user.id; + return userTeams(req, res); } diff --git a/pages/api/me/websites.ts b/pages/api/me/websites.ts index 238d1b6e2..d4a803a0d 100644 --- a/pages/api/me/websites.ts +++ b/pages/api/me/websites.ts @@ -1,11 +1,20 @@ -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; - import userWebsites from 'pages/api/users/[id]/websites'; +import * as yup from 'yup'; -export interface MyWebsitesRequestQuery extends SearchFilter {} +export interface MyWebsitesRequestQuery extends SearchFilter { + id: string; +} + +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Domain/i), + }), +}; export default async ( req: NextApiRequestQueryBody, @@ -14,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { req.query.id = req.auth.user.id; diff --git a/pages/api/realtime/[id].ts b/pages/api/realtime/[id].ts index e78599c6d..ab7bb4060 100644 --- a/pages/api/realtime/[id].ts +++ b/pages/api/realtime/[id].ts @@ -1,22 +1,34 @@ import { subMinutes } from 'date-fns'; import { canViewWebsite } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, RealtimeInit } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRealtimeData } from 'queries'; - +import * as yup from 'yup'; export interface RealtimeRequestQuery { id: string; startAt: number; } +const currentDate = new Date().getTime(); + +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + startAt: yup.number().integer().max(currentDate).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { const { id: websiteId, startAt } = req.query; diff --git a/pages/api/reports/[id].ts b/pages/api/reports/[id].ts index 85bc302c8..eb4199bcd 100644 --- a/pages/api/reports/[id].ts +++ b/pages/api/reports/[id].ts @@ -1,9 +1,10 @@ -import { canUpdateReport, canViewReport, canDeleteReport } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { canDeleteReport, canUpdateReport, canViewReport } from 'lib/auth'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody, ReportType, YupRequest } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getReportById, updateReport, deleteReport } from 'queries'; +import { deleteReport, getReportById, updateReport } from 'queries'; +import * as yup from 'yup'; export interface ReportRequestQuery { id: string; @@ -11,12 +12,34 @@ export interface ReportRequestQuery { export interface ReportRequestBody { websiteId: string; - type: string; + type: ReportType; name: string; description: string; parameters: string; } +const schema: YupRequest = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + websiteId: yup.string().uuid().required(), + type: yup + .string() + .matches(/funnel|insights|retention/i) + .required(), + name: yup.string().max(200).required(), + description: yup.string().max(500), + parameters: yup + .object() + .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), + }), + DELETE: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -24,6 +47,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: reportId } = req.query; const { user: { id: userId }, diff --git a/pages/api/reports/funnel.ts b/pages/api/reports/funnel.ts index 33882e03f..a51817bf4 100644 --- a/pages/api/reports/funnel.ts +++ b/pages/api/reports/funnel.ts @@ -1,9 +1,10 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getFunnel } from 'queries'; +import * as yup from 'yup'; export interface FunnelRequestBody { websiteId: string; @@ -22,6 +23,21 @@ export interface FunnelResponse { endAt: number; } +const schema = { + POST: yup.object().shape({ + websiteId: yup.string().uuid().required(), + urls: yup.array().min(2).of(yup.string()).required(), + window: yup.number().positive().required(), + dateRange: yup + .object() + .shape({ + startDate: yup.date().required(), + endDate: yup.date().required(), + }) + .required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -29,6 +45,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { websiteId, diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts index 762f297c7..e62a1cc57 100644 --- a/pages/api/reports/index.ts +++ b/pages/api/reports/index.ts @@ -1,10 +1,11 @@ -import { canViewWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createReport, getReportsByUserId, getReportsByWebsiteId } from 'queries'; +import { methodNotAllowed, ok } from 'next-basics'; +import { createReport, getReportsByUserId } from 'queries'; +import * as yup from 'yup'; export interface ReportsRequestQuery extends SearchFilter {} @@ -14,11 +15,28 @@ export interface ReportRequestBody { type: string; description: string; parameters: { - window: string; - urls: string[]; + [key: string]: any; }; } +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Description|Type|Username|Website Name|Website Domain/i), + }), + POST: yup.object().shape({ + websiteId: yup.string().uuid().required(), + name: yup.string().max(200).required(), + type: yup + .string() + .matches(/funnel|insights|retention/i) + .required(), + description: yup.string().max(500), + parameters: yup + .object() + .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -26,6 +44,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user: { id: userId }, } = req.auth; diff --git a/pages/api/reports/insights.ts b/pages/api/reports/insights.ts index 09a07d2fb..04e51d4ca 100644 --- a/pages/api/reports/insights.ts +++ b/pages/api/reports/insights.ts @@ -1,9 +1,10 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getInsights } from 'queries'; +import * as yup from 'yup'; export interface InsightsRequestBody { websiteId: string; @@ -16,6 +17,37 @@ export interface InsightsRequestBody { groups: { name: string; type: string }[]; } +const schema = { + POST: yup.object().shape({ + websiteId: yup.string().uuid().required(), + dateRange: yup + .object() + .shape({ + startDate: yup.date().required(), + endDate: yup.date().required(), + }) + .required(), + fields: yup + .array() + .of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + value: yup.string().required(), + }), + ) + .min(1) + .required(), + filters: yup.array().of(yup.string()).min(1).required(), + groups: yup.array().of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + }), + ), + }), +}; + function convertFilters(filters) { return filters.reduce((obj, { name, ...value }) => { obj[name] = value; @@ -31,6 +63,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { websiteId, diff --git a/pages/api/reports/retention.ts b/pages/api/reports/retention.ts index 40b3266bd..4006ab128 100644 --- a/pages/api/reports/retention.ts +++ b/pages/api/reports/retention.ts @@ -1,33 +1,43 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRetention } from 'queries'; +import * as yup from 'yup'; export interface RetentionRequestBody { websiteId: string; - dateRange: { window; startDate: string; endDate: string }; - timezone: string; + dateRange: { startDate: string; endDate: string }; } -export interface RetentionResponse { - startAt: number; - endAt: number; -} +const schema = { + POST: yup.object().shape({ + websiteId: yup.string().uuid().required(), + dateRange: yup + .object() + .shape({ + startDate: yup.date().required(), + endDate: yup.date().required(), + }) + .required(), + }), +}; export default async ( req: NextApiRequestQueryBody, - res: NextApiResponse, + res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { websiteId, dateRange: { startDate, endDate }, - timezone, } = req.body; if (!(await canViewWebsite(req.auth, websiteId))) { @@ -37,7 +47,6 @@ export default async ( const data = await getRetention(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), - timezone, }); return ok(res, data); diff --git a/pages/api/send.ts b/pages/api/send.ts index f90ded772..a379f261f 100644 --- a/pages/api/send.ts +++ b/pages/api/send.ts @@ -1,14 +1,15 @@ -import isbot from 'isbot'; -import ipaddr from 'ipaddr.js'; -import { createToken, ok, send, badRequest, forbidden } from 'next-basics'; -import { saveEvent, saveSessionData } from 'queries'; -import { useCors, useSession } from 'lib/middleware'; -import { getJsonBody, getIpAddress } from 'lib/detect'; -import { secret } from 'lib/crypto'; -import { NextApiRequest, NextApiResponse } from 'next'; import { Resolver } from 'dns/promises'; -import { CollectionType } from 'lib/types'; -import { COLLECTION_TYPE } from 'lib/constants'; +import ipaddr from 'ipaddr.js'; +import isbot from 'isbot'; +import { COLLECTION_TYPE, HOSTNAME_REGEX } from 'lib/constants'; +import { secret } from 'lib/crypto'; +import { getIpAddress, getJsonBody } from 'lib/detect'; +import { useCors, useSession, useValidate } from 'lib/middleware'; +import { CollectionType, YupRequest } from 'lib/types'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { badRequest, createToken, forbidden, ok, send } from 'next-basics'; +import { saveEvent, saveSessionData } from 'queries'; +import * as yup from 'yup'; export interface CollectRequestBody { payload: { @@ -43,8 +44,32 @@ export interface NextApiRequestCollect extends NextApiRequest { city: string; }; headers: { [key: string]: any }; + yup: YupRequest; } +const schema = { + POST: yup.object().shape({ + payload: yup + .object() + .shape({ + data: yup.object(), + hostname: yup.string().matches(HOSTNAME_REGEX).max(100), + language: yup.string().max(35), + referrer: yup.string().max(500), + screen: yup.string().max(11), + title: yup.string().max(500), + url: yup.string().max(500), + website: yup.string().uuid().required(), + name: yup.string().max(50), + }) + .required(), + type: yup + .string() + .matches(/event|identify/i) + .required(), + }), +}; + export default async (req: NextApiRequestCollect, res: NextApiResponse) => { await useCors(req, res); @@ -54,11 +79,8 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { const { type, payload } = getJsonBody(req); - const error = validateBody({ type, payload }); - - if (error) { - return badRequest(res, error); - } + req.yup = schema; + await useValidate(req, res); if (await hasBlockedIp(req)) { return forbidden(res); @@ -118,22 +140,6 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { return send(res, token); }; -function validateBody({ type, payload }: CollectRequestBody) { - if (!type || !payload) { - return 'Invalid payload.'; - } - - if (type !== COLLECTION_TYPE.event && type !== COLLECTION_TYPE.identify) { - return 'Wrong payload type.'; - } - - const { data } = payload; - - if (data && !(typeof data === 'object' && !Array.isArray(data))) { - return 'Invalid event data.'; - } -} - async function hasBlockedIp(req: NextApiRequestCollect) { const ignoreIps = process.env.IGNORE_IP; const ignoreHostnames = process.env.IGNORE_HOSTNAME; diff --git a/pages/api/share/[id].ts b/pages/api/share/[id].ts index 0592d2167..ad6422835 100644 --- a/pages/api/share/[id].ts +++ b/pages/api/share/[id].ts @@ -1,8 +1,10 @@ -import { NextApiRequestQueryBody } from 'lib/types'; import { secret } from 'lib/crypto'; +import { useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { createToken, methodNotAllowed, notFound, ok } from 'next-basics'; import { getWebsiteByShareId } from 'queries'; +import * as yup from 'yup'; export interface ShareRequestQuery { id: string; @@ -13,10 +15,19 @@ export interface ShareResponse { token: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { + req.yup = schema; + await useValidate(req, res); + const { id: shareId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index 7fb664a01..31c47b2f1 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -1,10 +1,11 @@ import { Team } from '@prisma/client'; -import { NextApiRequestQueryBody } from 'lib/types'; import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteTeam, getTeamById, updateTeam } from 'queries'; +import * as yup from 'yup'; export interface TeamRequestQuery { id: string; @@ -15,12 +16,29 @@ export interface TeamRequestBody { accessCode: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + name: yup.string().max(50).required(), + accessCode: yup.string().max(50).required(), + }), + DELETE: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: teamId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/teams/[id]/users/[userId].ts b/pages/api/teams/[id]/users/[userId].ts index 1e4ca6239..adb635d52 100644 --- a/pages/api/teams/[id]/users/[userId].ts +++ b/pages/api/teams/[id]/users/[userId].ts @@ -1,18 +1,28 @@ import { canDeleteTeamUser } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteTeamUser } from 'queries'; - +import * as yup from 'yup'; export interface TeamUserRequestQuery { id: string; userId: string; } +const schema = { + DELETE: yup.object().shape({ + id: yup.string().uuid().required(), + userId: yup.string().uuid().required(), + }), +}; + export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'DELETE') { const { id: teamId, userId } = req.query; diff --git a/pages/api/teams/[id]/users/index.ts b/pages/api/teams/[id]/users/index.ts index 6f8b077e0..52b25da68 100644 --- a/pages/api/teams/[id]/users/index.ts +++ b/pages/api/teams/[id]/users/index.ts @@ -1,9 +1,9 @@ -import { canUpdateTeam, canViewTeam } from 'lib/auth'; +import { canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, getUserByUsername, getUsersByTeamId } from 'queries'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getUsersByTeamId } from 'queries'; export interface TeamUserRequestQuery extends SearchFilter { id: string; @@ -38,24 +38,5 @@ export default async ( return ok(res, users); } - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const { email, roleId: roleId } = req.body; - - // Check for User - const user = await getUserByUsername(email); - - if (!user) { - return badRequest(res, 'The User does not exists.'); - } - - const updated = await createTeamUser(user.id, teamId, roleId); - - return ok(res, updated); - } - return methodNotAllowed(res); }; diff --git a/pages/api/teams/[id]/websites/[websiteId].ts b/pages/api/teams/[id]/websites/[websiteId].ts index 795295d36..ada1efdc0 100644 --- a/pages/api/teams/[id]/websites/[websiteId].ts +++ b/pages/api/teams/[id]/websites/[websiteId].ts @@ -1,21 +1,32 @@ import { canDeleteTeamWebsite } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteTeamWebsite } from 'queries/admin/teamWebsite'; +import * as yup from 'yup'; export interface TeamWebsitesRequestQuery { id: string; websiteId: string; } +const schema = { + DELETE: yup.object().shape({ + id: yup.string().uuid().required(), + websiteId: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: teamId, websiteId } = req.query; if (req.method === 'DELETE') { diff --git a/pages/api/teams/[id]/websites/index.ts b/pages/api/teams/[id]/websites/index.ts index dcd08939a..4de327092 100644 --- a/pages/api/teams/[id]/websites/index.ts +++ b/pages/api/teams/[id]/websites/index.ts @@ -1,9 +1,10 @@ import { canViewTeam } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsites, getWebsitesByTeamId } from 'queries'; +import { getWebsitesByTeamId } from 'queries'; import { createTeamWebsites } from 'queries/admin/teamWebsite'; export interface TeamWebsiteRequestQuery extends SearchFilter { @@ -14,12 +15,28 @@ export interface TeamWebsiteRequestBody { websiteIds?: string[]; } +import * as yup from 'yup'; + +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + ...getFilterValidation(/All|Name|Domain/i), + }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + websiteIds: yup.array().of(yup.string()).min(1).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: teamId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index 997ed8854..dd742b9e2 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -1,23 +1,39 @@ import { Team } from '@prisma/client'; import { canCreateTeam } from 'lib/auth'; import { uuid } from 'lib/crypto'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createTeam, getTeamsByUserId } from 'queries'; +import * as yup from 'yup'; export interface TeamsRequestQuery extends SearchFilter {} -export interface TeamsRequestBody extends SearchFilter { +export interface TeamsRequestBody { name: string; } +export interface MyTeamsRequestQuery extends SearchFilter {} + +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Owner/i), + }), + POST: yup.object().shape({ + name: yup.string().max(50).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user: { id: userId }, } = req.auth; diff --git a/pages/api/teams/join.ts b/pages/api/teams/join.ts index ce7367a0c..06feda8a6 100644 --- a/pages/api/teams/join.ts +++ b/pages/api/teams/join.ts @@ -1,21 +1,30 @@ import { Team } from '@prisma/client'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, notFound } from 'next-basics'; -import { createTeamUser, getTeamByAccessCode, getTeamUser } from 'queries'; import { ROLES } from 'lib/constants'; - +import { useAuth, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, notFound, ok } from 'next-basics'; +import { createTeamUser, getTeamByAccessCode, getTeamUser } from 'queries'; +import * as yup from 'yup'; export interface TeamsJoinRequestBody { accessCode: string; } +const schema = { + POST: yup.object().shape({ + accessCode: yup.string().max(50).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { accessCode } = req.body; diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index e09b1b5f8..3ac560ede 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -1,9 +1,10 @@ -import { NextApiRequestQueryBody, Role, User } from 'lib/types'; import { canDeleteUser, canUpdateUser, canViewUser } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody, Role, User } from 'lib/types'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteUser, getUserById, getUserByUsername, updateUser } from 'queries'; +import * as yup from 'yup'; export interface UserRequestQuery { id: string; @@ -15,12 +16,27 @@ export interface UserRequestBody { role: Role; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + username: yup.string().max(255), + password: yup.string(), + role: yup.string().matches(/admin|user|view-only/i), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user: { id: userId, isAdmin }, } = req.auth; diff --git a/pages/api/users/[id]/teams.ts b/pages/api/users/[id]/teams.ts index 831a992d8..eb34410cb 100644 --- a/pages/api/users/[id]/teams.ts +++ b/pages/api/users/[id]/teams.ts @@ -1,9 +1,10 @@ -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getTeamsByUserId } from 'queries'; - +import * as yup from 'yup'; export interface UserTeamsRequestQuery extends SearchFilter { id: string; } @@ -14,6 +15,13 @@ export interface UserTeamsRequestBody { shareId: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + ...getFilterValidation('/All|Name|Owner/i'), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -21,6 +29,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user } = req.auth; const { id: userId } = req.query; diff --git a/pages/api/users/[id]/usage.ts b/pages/api/users/[id]/usage.ts index 0118df92b..b0fc2055b 100644 --- a/pages/api/users/[id]/usage.ts +++ b/pages/api/users/[id]/usage.ts @@ -1,8 +1,9 @@ -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getEventDataUsage, getEventUsage, getUserWebsites } from 'queries'; +import * as yup from 'yup'; export interface UserUsageRequestQuery { id: string; @@ -21,6 +22,14 @@ export interface UserUsageRequestResponse { }[]; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -28,6 +37,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user } = req.auth; if (req.method === 'GET') { diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts index 0e9231f73..65e9a0e8a 100644 --- a/pages/api/users/[id]/websites.ts +++ b/pages/api/users/[id]/websites.ts @@ -1,25 +1,36 @@ -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsitesByUserId } from 'queries'; +import * as yup from 'yup'; export interface UserWebsitesRequestQuery extends SearchFilter { id: string; -} -export interface UserWebsitesRequestBody { - name: string; - domain: string; - shareId: string; + includeTeams?: boolean; + onlyTeams?: boolean; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + includeTeams: yup.boolean(), + onlyTeams: yup.boolean(), + ...getFilterValidation(/All|Name|Domain/i), + }), +}; + export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user } = req.auth; const { id: userId, page, filter, pageSize, includeTeams, onlyTeams } = req.query; diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 5e913c027..0b523c70c 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -1,8 +1,9 @@ import { canCreateUser, canViewUsers } from 'lib/auth'; import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUserByUsername, getUsers } from 'queries'; @@ -15,12 +16,31 @@ export interface UsersRequestBody { role?: Role; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Username/i), + }), + POST: yup.object().shape({ + username: yup.string().max(255).required(), + password: yup.string().required(), + id: yup.string().uuid(), + role: yup + .string() + .matches(/admin|user|view-only/i) + .required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { if (!(await canViewUsers(req.auth))) { return unauthorized(res); @@ -28,7 +48,7 @@ export default async ( const { page, filter, pageSize } = req.query; - const users = await getUsers({ page, filter, pageSize: +pageSize || null }); + const users = await getUsers({ page, filter, pageSize: pageSize ? +pageSize : null }); return ok(res, users); } diff --git a/pages/api/websites/[id]/active.ts b/pages/api/websites/[id]/active.ts index 99c8d9990..abc23dd78 100644 --- a/pages/api/websites/[id]/active.ts +++ b/pages/api/websites/[id]/active.ts @@ -1,14 +1,21 @@ import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getActiveVisitors } from 'queries'; +import * as yup from 'yup'; export interface WebsiteActiveRequestQuery { id: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -16,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/[id]/daterange.ts b/pages/api/websites/[id]/daterange.ts index dc0435600..bfa5338e4 100644 --- a/pages/api/websites/[id]/daterange.ts +++ b/pages/api/websites/[id]/daterange.ts @@ -1,14 +1,21 @@ import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsiteDateRange } from 'queries'; +import * as yup from 'yup'; export interface WebsiteDateRangeRequestQuery { id: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -16,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/[id]/events.ts b/pages/api/websites/[id]/events.ts index 7d4f999f6..427cb40ea 100644 --- a/pages/api/websites/[id]/events.ts +++ b/pages/api/websites/[id]/events.ts @@ -1,6 +1,6 @@ import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import moment from 'moment-timezone'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -16,9 +16,21 @@ export interface WebsiteEventsRequestQuery { unit: string; timezone: string; url: string; - eventName: string; } +import * as yup from 'yup'; + +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + unit: yup.string().required(), + timezone: yup.string().required(), + url: yup.string(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -26,7 +38,10 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { id: websiteId, timezone, url, eventName } = req.query; + req.yup = schema; + await useValidate(req, res); + + const { id: websiteId, timezone, url } = req.query; const { startDate, endDate, unit } = await parseDateRangeQuery(req); if (req.method === 'GET') { @@ -44,7 +59,6 @@ export default async ( timezone, unit, url, - eventName, }); return ok(res, events); diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 3d053d0eb..597568de5 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -2,7 +2,7 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; import { Website, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { deleteWebsite, getWebsiteById, updateWebsite } from 'queries'; import { SHARE_ID_REGEX } from 'lib/constants'; @@ -16,6 +16,13 @@ export interface WebsiteRequestBody { shareId: string; } +import * as yup from 'yup'; + +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -23,6 +30,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index 7c84583c2..67c15ecae 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -2,10 +2,11 @@ import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS } from 'lib/constants'; import { getPageviewMetrics, getSessionMetrics } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import * as yup from 'yup'; export interface WebsiteMetricsRequestQuery { id: string; @@ -26,6 +27,12 @@ export interface WebsiteMetricsRequestQuery { language: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -33,6 +40,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId, type, diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index c5532e76e..9985ca892 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -3,7 +3,7 @@ import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { getPageviewStats, getSessionStats } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; @@ -24,6 +24,13 @@ export interface WebsitePageviewRequestQuery { city?: string; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -31,6 +38,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId, timezone, diff --git a/pages/api/websites/[id]/reports.ts b/pages/api/websites/[id]/reports.ts index 60c6f7140..738f6b372 100644 --- a/pages/api/websites/[id]/reports.ts +++ b/pages/api/websites/[id]/reports.ts @@ -1,5 +1,5 @@ import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -9,6 +9,13 @@ export interface ReportsRequestQuery extends SearchFilter, res: NextApiResponse, @@ -16,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index 23b5305db..cfd5e7679 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -1,6 +1,6 @@ import { NextApiRequestQueryBody } from 'lib/types'; import { canUpdateWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { resetWebsite } from 'queries'; @@ -9,6 +9,13 @@ export interface WebsiteResetRequestQuery { id: string; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -16,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'POST') { diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index a77c7eaf8..caf549103 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -2,7 +2,7 @@ import { subMinutes, differenceInMinutes } from 'date-fns'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; import { parseDateRangeQuery } from 'lib/query'; import { getWebsiteStats } from 'queries'; @@ -24,6 +24,13 @@ export interface WebsiteStatsRequestQuery { city: string; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -31,6 +38,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId, url, diff --git a/pages/api/websites/[id]/values.ts b/pages/api/websites/[id]/values.ts index ad8625bd4..d90a16822 100644 --- a/pages/api/websites/[id]/values.ts +++ b/pages/api/websites/[id]/values.ts @@ -1,6 +1,6 @@ import { NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; @@ -10,6 +10,13 @@ export interface WebsiteResetRequestQuery { id: string; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -17,6 +24,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId, type } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index f94fa0378..d724f12f4 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,11 +1,13 @@ import { canCreateWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; import userWebsites from 'pages/api/users/[id]/websites'; +import * as yup from 'yup'; +import { getFilterValidation } from 'lib/yup'; export interface WebsitesRequestQuery extends SearchFilter {} @@ -15,12 +17,25 @@ export interface WebsitesRequestBody { shareId: string; } +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Domain/i), + }), + POST: yup.object().shape({ + name: yup.string().max(100).required(), + domain: yup.string().max(500).required(), + shareId: yup.string().max(50), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); const { user: { id: userId }, @@ -30,7 +45,7 @@ export default async ( req.query.id = userId; req.query.pageSize = 100; - return userWebsites(req, res); + return userWebsites(req as any, res); } if (req.method === 'POST') { From ede658771e952ae03a3479d3bb0aed4916cae64c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 21 Aug 2023 02:06:09 -0700 Subject: [PATCH 092/357] Moved code into src folder. Added build for component library. --- .eslintrc.json | 17 +- jsconfig.json | 4 +- package.components.json | 10 + package.json | 25 +- rollup.components.config.mjs | 99 +++++ ...ker.config.js => rollup.tracker.config.mjs | 2 +- scripts/check-lang.js | 2 +- {assets => src/assets}/add-user.svg | 0 {assets => src/assets}/bar-chart.svg | 0 {assets => src/assets}/bars.svg | 0 {assets => src/assets}/bolt.svg | 0 {assets => src/assets}/calendar.svg | 0 {assets => src/assets}/clock.svg | 0 {assets => src/assets}/dashboard.svg | 0 {assets => src/assets}/expand.svg | 0 {assets => src/assets}/eye.svg | 0 {assets => src/assets}/funnel.svg | 0 {assets => src/assets}/gear.svg | 0 {assets => src/assets}/globe.svg | 0 {assets => src/assets}/lightbulb.svg | 0 {assets => src/assets}/link.svg | 0 {assets => src/assets}/lock.svg | 0 {assets => src/assets}/logo.svg | 0 {assets => src/assets}/magnet.svg | 0 {assets => src/assets}/moon.svg | 0 {assets => src/assets}/nodes.svg | 0 {assets => src/assets}/overview.svg | 0 {assets => src/assets}/profile.svg | 0 {assets => src/assets}/redo.svg | 0 {assets => src/assets}/reports.svg | 0 {assets => src/assets}/sun.svg | 0 {assets => src/assets}/user.svg | 0 {assets => src/assets}/users.svg | 0 {assets => src/assets}/visitor.svg | 0 {assets => src/assets}/website.svg | 0 .../components}/common/ConfirmDeleteForm.js | 2 +- .../components}/common/Empty.js | 2 +- .../components}/common/Empty.module.css | 0 .../components}/common/EmptyPlaceholder.js | 0 .../components}/common/ErrorBoundary.js | 2 +- .../common/ErrorBoundry.module.css | 0 .../components}/common/ErrorMessage.js | 2 +- .../common/ErrorMessage.module.css | 0 .../components}/common/Favicon.js | 0 .../components}/common/Favicon.module.css | 0 .../components}/common/FilterButtons.js | 0 .../components}/common/FilterLink.js | 4 +- .../components}/common/FilterLink.module.css | 0 .../components}/common/HamburgerButton.js | 4 +- .../common/HamburgerButton.module.css | 0 .../components}/common/HoverTooltip.js | 0 .../common/HoverTooltip.module.css | 0 .../components}/common/LinkButton.js | 4 +- .../components}/common/LinkButton.module.css | 0 .../components}/common/MobileMenu.js | 0 .../components}/common/MobileMenu.module.css | 0 .../components}/common/Pager.js | 2 +- .../components}/common/Pager.module.css | 0 .../components}/common/SettingsTable.js | 2 +- .../common/SettingsTable.module.css | 0 .../components}/common/UpdateNotice.js | 2 +- .../common/UpdateNotice.module.css | 0 .../components}/common/WorldMap.js | 6 +- .../components}/common/WorldMap.module.css | 0 .../components}/declarations.d.ts | 0 {hooks => src/components/hooks}/index.js | 0 {hooks => src/components/hooks}/useApi.ts | 0 .../components/hooks}/useApiFilter.ts | 0 {hooks => src/components/hooks}/useConfig.js | 2 +- .../components/hooks}/useCountryNames.js | 0 .../components/hooks}/useDateRange.js | 0 .../components/hooks}/useDocumentClick.js | 0 .../components/hooks}/useEscapeKey.js | 0 {hooks => src/components/hooks}/useFilters.js | 2 +- .../components/hooks}/useForceUpdate.js | 0 {hooks => src/components/hooks}/useFormat.js | 0 .../components/hooks}/useLanguageNames.js | 0 {hooks => src/components/hooks}/useLocale.js | 2 +- .../components/hooks}/useMessages.js | 0 .../components/hooks}/usePageQuery.js | 0 {hooks => src/components/hooks}/useReport.js | 0 {hooks => src/components/hooks}/useReports.js | 2 +- .../components/hooks}/useRequireLogin.js | 4 +- .../components/hooks}/useShareToken.js | 0 {hooks => src/components/hooks}/useSticky.js | 0 {hooks => src/components/hooks}/useTheme.js | 0 .../components/hooks}/useTimezone.js | 0 {hooks => src/components/hooks}/useUser.js | 0 {hooks => src/components/hooks}/useWebsite.js | 0 .../components/hooks}/useWebsiteReports.js | 2 +- {components => src/components}/icons.ts | 2 +- .../components}/input/DateFilter.js | 4 +- .../components}/input/LanguageButton.js | 2 +- .../input/LanguageButton.module.css | 0 .../components}/input/LogoutButton.js | 4 +- .../components}/input/MonthSelect.js | 2 +- .../components}/input/MonthSelect.module.css | 0 .../components}/input/ProfileButton.js | 8 +- .../input/ProfileButton.module.css | 0 .../components}/input/RefreshButton.js | 4 +- .../components}/input/SettingsButton.js | 2 +- .../input/SettingsButton.module.css | 0 .../components}/input/ThemeButton.js | 2 +- .../components}/input/ThemeButton.module.css | 0 .../components}/input/WebsiteDateFilter.js | 6 +- .../input/WebsiteDateFilter.module.css | 0 .../components}/input/WebsiteSelect.js | 4 +- .../components}/layout/AppLayout.js | 2 +- .../components}/layout/AppLayout.module.css | 0 .../components}/layout/Footer.js | 0 .../components}/layout/Footer.module.css | 0 {components => src/components}/layout/Grid.js | 0 .../components}/layout/Grid.module.css | 0 .../components}/layout/Header.js | 0 .../components}/layout/Header.module.css | 0 .../components}/layout/NavBar.js | 4 +- .../components}/layout/NavBar.module.css | 0 .../components}/layout/NavGroup.js | 0 .../components}/layout/NavGroup.module.css | 0 {components => src/components}/layout/Page.js | 2 +- .../components}/layout/Page.module.css | 0 .../components}/layout/PageHeader.js | 0 .../components}/layout/PageHeader.module.css | 0 .../components}/layout/ReportsLayout.js | 4 +- .../layout/ReportsLayout.module.css | 0 .../components}/layout/SettingsLayout.js | 6 +- .../layout/SettingsLayout.module.css | 0 .../components}/layout/ShareLayout.js | 0 .../components}/layout/SideNav.js | 0 .../components}/layout/SideNav.module.css | 0 {components => src/components}/messages.js | 0 .../components}/metrics/ActiveUsers.js | 4 +- .../metrics/ActiveUsers.module.css | 0 .../components}/metrics/BarChart.js | 4 +- .../components}/metrics/BarChart.module.css | 0 .../components}/metrics/BrowsersTable.js | 4 +- .../components}/metrics/CitiesTable.js | 4 +- .../components}/metrics/CountriesTable.js | 4 +- .../components}/metrics/DataTable.js | 2 +- .../components}/metrics/DataTable.module.css | 0 .../components}/metrics/DatePickerForm.js | 4 +- .../metrics/DatePickerForm.module.css | 0 .../components}/metrics/DevicesTable.js | 4 +- .../components}/metrics/EventsChart.js | 2 +- .../metrics/EventsChart.module.css | 0 .../components}/metrics/EventsTable.js | 2 +- .../components}/metrics/FilterTags.js | 4 +- .../components}/metrics/FilterTags.module.css | 0 .../components}/metrics/LanguagesTable.js | 6 +- .../components}/metrics/Legend.js | 6 +- .../components}/metrics/Legend.module.css | 0 .../components}/metrics/MetricCard.js | 0 .../components}/metrics/MetricCard.module.css | 0 .../components}/metrics/MetricsBar.js | 0 .../components}/metrics/MetricsBar.module.css | 0 .../components}/metrics/MetricsTable.js | 14 +- .../metrics/MetricsTable.module.css | 0 .../components}/metrics/OSTable.js | 2 +- .../components}/metrics/PagesTable.js | 4 +- .../components}/metrics/PageviewsChart.js | 4 +- .../metrics/QueryParametersTable.js | 2 +- .../metrics/QueryParametersTable.module.css | 0 .../components}/metrics/RealtimeChart.js | 2 +- .../components}/metrics/ReferrersTable.js | 2 +- .../components}/metrics/RegionsTable.js | 6 +- .../components}/metrics/ScreenTable.js | 2 +- .../components}/pages/console/TestConsole.js | 2 +- .../pages/console/TestConsole.module.css | 0 .../components}/pages/dashboard/Dashboard.js | 6 +- .../pages/dashboard/DashboardEdit.js | 2 +- .../pages/dashboard/DashboardEdit.module.css | 0 .../dashboard/DashboardSettingsButton.js | 4 +- .../DashboardSettingsButton.module.css | 0 .../pages/event-data/EventDataMetricsBar.js | 4 +- .../event-data/EventDataMetricsBar.module.css | 0 .../pages/event-data/EventDataTable.js | 2 +- .../pages/event-data/EventDataValueTable.js | 2 +- .../components}/pages/login/LoginForm.js | 4 +- .../pages/login/LoginForm.module.css | 0 .../components}/pages/login/LoginLayout.js | 2 +- .../pages/login/LoginLayout.module.css | 0 .../pages/realtime/RealtimeCountries.js | 6 +- .../realtime/RealtimeCountries.module.css | 0 .../pages/realtime/RealtimeHeader.js | 2 +- .../pages/realtime/RealtimeHeader.module.css | 0 .../pages/realtime/RealtimeHome.js | 4 +- .../components}/pages/realtime/RealtimeLog.js | 6 +- .../pages/realtime/RealtimeLog.module.css | 0 .../pages/realtime/RealtimePage.js | 4 +- .../pages/realtime/RealtimePage.module.css | 0 .../pages/realtime/RealtimeUrls.js | 2 +- .../pages/reports/BaseParameters.js | 2 +- .../components}/pages/reports/FieldAddForm.js | 0 .../pages/reports/FieldAddForm.module.css | 0 .../pages/reports/FieldAggregateForm.js | 2 +- .../pages/reports/FieldFilterForm.js | 2 +- .../pages/reports/FieldFilterForm.module.css | 0 .../pages/reports/FieldSelectForm.js | 2 +- .../pages/reports/FieldSelectForm.module.css | 0 .../pages/reports/FilterSelectForm.js | 2 +- .../pages/reports/ParameterList.js | 2 +- .../pages/reports/ParameterList.module.css | 0 .../components}/pages/reports/PopupForm.js | 0 .../pages/reports/PopupForm.module.css | 0 .../components}/pages/reports/Report.js | 2 +- .../components}/pages/reports/ReportBody.js | 0 .../pages/reports/ReportDetails.js | 0 .../components}/pages/reports/ReportHeader.js | 2 +- .../pages/reports/ReportHeader.module.css | 0 .../components}/pages/reports/ReportMenu.js | 0 .../pages/reports/ReportTemplates.js | 2 +- .../pages/reports/ReportTemplates.module.css | 0 .../components}/pages/reports/ReportsPage.js | 2 +- .../components}/pages/reports/ReportsTable.js | 4 +- .../reports/event-data/EventDataParameters.js | 2 +- .../event-data/EventDataParameters.module.css | 0 .../reports/event-data/EventDataReport.js | 0 .../reports/event-data/EventDataTable.js | 2 +- .../pages/reports/funnel/FunnelChart.js | 6 +- .../reports/funnel/FunnelChart.module.css | 0 .../pages/reports/funnel/FunnelParameters.js | 2 +- .../pages/reports/funnel/FunnelReport.js | 0 .../reports/funnel/FunnelReport.module.css | 0 .../pages/reports/funnel/FunnelTable.js | 2 +- .../pages/reports/funnel/UrlAddForm.js | 2 +- .../reports/funnel/UrlAddForm.module.css | 0 .../reports/insights/InsightsParameters.js | 2 +- .../insights/InsightsParameters.module.css | 0 .../pages/reports/insights/InsightsReport.js | 0 .../pages/reports/insights/InsightsTable.js | 2 +- .../pages/reports/reports.module.css | 0 .../reports/retention/RetentionParameters.js | 2 +- .../reports/retention/RetentionReport.js | 0 .../retention/RetentionReport.module.css | 0 .../pages/reports/retention/RetentionTable.js | 2 +- .../retention/RetentionTable.module.css | 0 .../settings/profile/DateRangeSetting.js | 4 +- .../pages/settings/profile/LanguageSetting.js | 4 +- .../settings/profile/PasswordChangeButton.js | 2 +- .../settings/profile/PasswordEditForm.js | 4 +- .../pages/settings/profile/ProfileDetails.js | 6 +- .../pages/settings/profile/ProfileSettings.js | 2 +- .../pages/settings/profile/ThemeSetting.js | 2 +- .../settings/profile/ThemeSetting.module.css | 0 .../pages/settings/profile/TimezoneSetting.js | 4 +- .../pages/settings/teams/TeamAddForm.js | 4 +- .../settings/teams/TeamAddWebsiteForm.js | 4 +- .../pages/settings/teams/TeamDeleteForm.js | 4 +- .../pages/settings/teams/TeamEditForm.js | 4 +- .../pages/settings/teams/TeamJoinForm.js | 4 +- .../pages/settings/teams/TeamLeaveForm.js | 4 +- .../settings/teams/TeamMemberRemoveButton.js | 4 +- .../pages/settings/teams/TeamMembers.js | 6 +- .../pages/settings/teams/TeamMembersTable.js | 4 +- .../pages/settings/teams/TeamSettings.js | 6 +- .../settings/teams/TeamWebsiteRemoveButton.js | 4 +- .../pages/settings/teams/TeamWebsites.js | 6 +- .../pages/settings/teams/TeamWebsitesTable.js | 6 +- .../pages/settings/teams/TeamsList.js | 8 +- .../pages/settings/teams/TeamsTable.js | 6 +- .../pages/settings/teams/WebsiteTags.js | 0 .../settings/teams/WebsiteTags.module.css | 0 .../pages/settings/users/UserAddButton.js | 2 +- .../pages/settings/users/UserAddForm.js | 4 +- .../pages/settings/users/UserDeleteForm.js | 4 +- .../pages/settings/users/UserEditForm.js | 4 +- .../pages/settings/users/UserSettings.js | 6 +- .../pages/settings/users/UserWebsites.js | 4 +- .../pages/settings/users/UsersList.js | 8 +- .../pages/settings/users/UsersTable.js | 8 +- .../pages/settings/websites/ShareUrl.js | 6 +- .../pages/settings/websites/TrackingCode.js | 4 +- .../pages/settings/websites/WebsiteAddForm.js | 4 +- .../pages/settings/websites/WebsiteData.js | 2 +- .../settings/websites/WebsiteDeleteForm.js | 4 +- .../settings/websites/WebsiteEditForm.js | 4 +- .../settings/websites/WebsiteResetForm.js | 4 +- .../settings/websites/WebsiteSettings.js | 6 +- .../pages/settings/websites/WebsitesList.js | 8 +- .../pages/settings/websites/WebsitesTable.js | 6 +- .../websites/WebsitesTable.module.css | 0 .../pages/websites/WebsiteChart.js | 2 +- .../pages/websites/WebsiteChart.module.css | 0 .../pages/websites/WebsiteChartList.js | 2 +- .../pages/websites/WebsiteDetailsPage.js | 4 +- .../pages/websites/WebsiteEventData.js | 2 +- .../websites/WebsiteEventData.module.css | 0 .../pages/websites/WebsiteEventDataPage.js | 0 .../pages/websites/WebsiteHeader.js | 4 +- .../pages/websites/WebsiteHeader.module.css | 0 .../pages/websites/WebsiteList.module.css | 0 .../pages/websites/WebsiteMenuView.js | 6 +- .../pages/websites/WebsiteMenuView.module.css | 0 .../pages/websites/WebsiteMetricsBar.js | 2 +- .../websites/WebsiteMetricsBar.module.css | 0 .../pages/websites/WebsiteReportsPage.js | 2 +- .../pages/websites/WebsiteTableView.js | 0 .../websites/WebsiteTableView.module.css | 0 .../pages/websites/WebsitesPage.js | 6 +- src/index.ts | 61 +++ {lang => src/lang}/am-ET.json | 0 {lang => src/lang}/ar-SA.json | 0 {lang => src/lang}/be-BY.json | 0 {lang => src/lang}/bn-BD.json | 0 {lang => src/lang}/ca-ES.json | 0 {lang => src/lang}/cs-CZ.json | 0 {lang => src/lang}/da-DK.json | 0 {lang => src/lang}/de-CH.json | 0 {lang => src/lang}/de-DE.json | 0 {lang => src/lang}/el-GR.json | 0 {lang => src/lang}/en-GB.json | 0 {lang => src/lang}/en-US.json | 0 {lang => src/lang}/es-ES.json | 0 {lang => src/lang}/es-MX.json | 0 {lang => src/lang}/fa-IR.json | 0 {lang => src/lang}/fi-FI.json | 0 {lang => src/lang}/fo-FO.json | 0 {lang => src/lang}/fr-FR.json | 0 {lang => src/lang}/ga-ES.json | 0 {lang => src/lang}/he-IL.json | 0 {lang => src/lang}/hi-IN.json | 0 {lang => src/lang}/hr-HR.json | 0 {lang => src/lang}/hu-HU.json | 0 {lang => src/lang}/id-ID.json | 0 {lang => src/lang}/it-IT.json | 0 {lang => src/lang}/ja-JP.json | 0 {lang => src/lang}/km-KH.json | 0 {lang => src/lang}/ko-KR.json | 0 {lang => src/lang}/lt-LT.json | 0 {lang => src/lang}/mn-MN.json | 0 {lang => src/lang}/ms-MY.json | 0 {lang => src/lang}/my-MM.json | 0 {lang => src/lang}/nb-NO.json | 0 {lang => src/lang}/nl-NL.json | 0 {lang => src/lang}/pl-PL.json | 0 {lang => src/lang}/pt-BR.json | 0 {lang => src/lang}/pt-PT.json | 0 {lang => src/lang}/ro-RO.json | 0 {lang => src/lang}/ru-RU.json | 0 {lang => src/lang}/si-LK.json | 0 {lang => src/lang}/sk-SK.json | 0 {lang => src/lang}/sl-SI.json | 0 {lang => src/lang}/sv-SE.json | 0 {lang => src/lang}/ta-IN.json | 0 {lang => src/lang}/th-TH.json | 0 {lang => src/lang}/tr-TR.json | 0 {lang => src/lang}/uk-UA.json | 0 {lang => src/lang}/ur-PK.json | 0 {lang => src/lang}/vi-VN.json | 0 {lang => src/lang}/zh-CN.json | 0 {lang => src/lang}/zh-TW.json | 0 {lib => src/lib}/auth.ts | 0 {lib => src/lib}/cache.ts | 0 {lib => src/lib}/charts.js | 0 {lib => src/lib}/clickhouse.ts | 0 {lib => src/lib}/client.ts | 0 {lib => src/lib}/constants.ts | 0 {lib => src/lib}/crypto.js | 0 {lib => src/lib}/data.ts | 0 {lib => src/lib}/date.js | 0 {lib => src/lib}/db.js | 0 {lib => src/lib}/detect.ts | 0 {lib => src/lib}/filters.js | 0 {lib => src/lib}/format.js | 0 {lib => src/lib}/kafka.ts | 0 {lib => src/lib}/lang.js | 0 {lib => src/lib}/load.ts | 0 {lib => src/lib}/middleware.ts | 0 {lib => src/lib}/prisma.ts | 0 {lib => src/lib}/query.ts | 0 {lib => src/lib}/session.ts | 0 {lib => src/lib}/sql.ts | 0 {lib => src/lib}/types.ts | 0 {lib => src/lib}/yup.ts | 0 {pages => src/pages}/404.js | 2 +- {pages => src/pages}/_app.js | 4 +- {pages => src/pages}/api/auth/login.ts | 0 {pages => src/pages}/api/auth/logout.ts | 0 {pages => src/pages}/api/auth/sso.ts | 0 {pages => src/pages}/api/auth/verify.ts | 0 {pages => src/pages}/api/config.ts | 0 {pages => src/pages}/api/event-data/events.ts | 0 {pages => src/pages}/api/event-data/fields.ts | 0 {pages => src/pages}/api/event-data/stats.ts | 0 {pages => src/pages}/api/heartbeat.ts | 0 {pages => src/pages}/api/me/index.ts | 0 {pages => src/pages}/api/me/password.ts | 0 {pages => src/pages}/api/me/teams.ts | 0 {pages => src/pages}/api/me/websites.ts | 0 {pages => src/pages}/api/realtime/[id].ts | 0 {pages => src/pages}/api/reports/[id].ts | 0 {pages => src/pages}/api/reports/funnel.ts | 0 {pages => src/pages}/api/reports/index.ts | 0 {pages => src/pages}/api/reports/insights.ts | 0 {pages => src/pages}/api/reports/retention.ts | 0 {pages => src/pages}/api/scripts/telemetry.js | 0 {pages => src/pages}/api/send.ts | 0 {pages => src/pages}/api/share/[id].ts | 0 {pages => src/pages}/api/teams/[id]/index.ts | 0 .../pages}/api/teams/[id]/users/[userId].ts | 0 .../pages}/api/teams/[id]/users/index.ts | 0 .../api/teams/[id]/websites/[websiteId].ts | 0 .../pages}/api/teams/[id]/websites/index.ts | 0 {pages => src/pages}/api/teams/index.ts | 0 {pages => src/pages}/api/teams/join.ts | 0 {pages => src/pages}/api/users/[id]/index.ts | 0 {pages => src/pages}/api/users/[id]/teams.ts | 0 {pages => src/pages}/api/users/[id]/usage.ts | 0 .../pages}/api/users/[id]/websites.ts | 0 {pages => src/pages}/api/users/index.ts | 0 .../pages}/api/websites/[id]/active.ts | 0 .../pages}/api/websites/[id]/daterange.ts | 0 .../pages}/api/websites/[id]/events.ts | 0 .../pages}/api/websites/[id]/index.ts | 0 .../pages}/api/websites/[id]/metrics.ts | 0 .../pages}/api/websites/[id]/pageviews.ts | 0 .../pages}/api/websites/[id]/reports.ts | 0 .../pages}/api/websites/[id]/reset.ts | 0 .../pages}/api/websites/[id]/stats.ts | 0 .../pages}/api/websites/[id]/values.ts | 0 {pages => src/pages}/api/websites/index.ts | 0 {pages => src/pages}/console/[[...id]].js | 0 {pages => src/pages}/dashboard/index.js | 2 +- {pages => src/pages}/index.js | 0 {pages => src/pages}/login.js | 0 {pages => src/pages}/logout.js | 2 +- {pages => src/pages}/reports/[id].js | 2 +- {pages => src/pages}/reports/create.js | 2 +- {pages => src/pages}/reports/funnel.js | 2 +- {pages => src/pages}/reports/index.js | 2 +- {pages => src/pages}/reports/insights.js | 2 +- {pages => src/pages}/reports/retention.js | 2 +- .../pages}/settings/profile/index.js | 2 +- {pages => src/pages}/settings/teams/[id].js | 2 +- {pages => src/pages}/settings/teams/index.js | 2 +- {pages => src/pages}/settings/users/[id].js | 2 +- {pages => src/pages}/settings/users/index.js | 2 +- .../pages}/settings/websites/[id].js | 2 +- .../pages}/settings/websites/index.js | 2 +- {pages => src/pages}/share/[...id].js | 2 +- {pages => src/pages}/sso.js | 0 .../pages}/websites/[id]/event-data.js | 2 +- {pages => src/pages}/websites/[id]/index.js | 2 +- .../pages}/websites/[id]/realtime.js | 0 {pages => src/pages}/websites/[id]/reports.js | 0 {pages => src/pages}/websites/index.js | 2 +- {queries => src/queries}/admin/report.ts | 0 {queries => src/queries}/admin/team.ts | 0 {queries => src/queries}/admin/teamUser.ts | 0 {queries => src/queries}/admin/teamWebsite.ts | 0 {queries => src/queries}/admin/user.ts | 0 {queries => src/queries}/admin/website.ts | 0 .../analytics/eventData/getEventDataEvents.ts | 0 .../analytics/eventData/getEventDataFields.ts | 0 .../analytics/eventData/getEventDataStats.ts | 0 .../analytics/eventData/getEventDataUsage.ts | 0 .../analytics/eventData/saveEventData.ts | 0 .../analytics/events/getEventMetrics.ts | 0 .../analytics/events/getEventUsage.ts | 0 .../queries}/analytics/events/getEvents.ts | 0 .../queries}/analytics/events/saveEvent.ts | 0 .../queries}/analytics/getActiveVisitors.ts | 0 .../queries}/analytics/getRealtimeData.ts | 0 .../queries}/analytics/getValues.ts | 0 .../queries}/analytics/getWebsiteDateRange.ts | 0 .../queries}/analytics/getWebsiteStats.ts | 0 .../analytics/pageviews/getPageviewMetrics.ts | 0 .../analytics/pageviews/getPageviewStats.ts | 0 .../queries}/analytics/reports/getFunnel.ts | 0 .../queries}/analytics/reports/getInsights.ts | 0 .../analytics/reports/getRetention.ts | 0 .../analytics/sessions/createSession.ts | 0 .../queries}/analytics/sessions/getSession.ts | 0 .../analytics/sessions/getSessionMetrics.ts | 0 .../analytics/sessions/getSessionStats.ts | 0 .../analytics/sessions/getSessions.ts | 0 .../analytics/sessions/saveSessionData.ts | 0 {queries => src/queries}/index.js | 0 {store => src/store}/app.js | 0 {store => src/store}/dashboard.js | 0 {store => src/store}/queries.js | 0 {store => src/store}/version.js | 0 {store => src/store}/websites.ts | 0 {styles => src/styles}/index.css | 0 {styles => src/styles}/locale.css | 0 {styles => src/styles}/variables.css | 0 {tracker => src/tracker}/index.d.ts | 0 {tracker => src/tracker}/index.js | 0 tsconfig.json | 6 +- yarn.lock | 409 ++++++++++++------ 490 files changed, 749 insertions(+), 442 deletions(-) create mode 100644 package.components.json create mode 100644 rollup.components.config.mjs rename rollup.tracker.config.js => rollup.tracker.config.mjs (93%) rename {assets => src/assets}/add-user.svg (100%) rename {assets => src/assets}/bar-chart.svg (100%) rename {assets => src/assets}/bars.svg (100%) rename {assets => src/assets}/bolt.svg (100%) rename {assets => src/assets}/calendar.svg (100%) rename {assets => src/assets}/clock.svg (100%) rename {assets => src/assets}/dashboard.svg (100%) rename {assets => src/assets}/expand.svg (100%) rename {assets => src/assets}/eye.svg (100%) rename {assets => src/assets}/funnel.svg (100%) rename {assets => src/assets}/gear.svg (100%) rename {assets => src/assets}/globe.svg (100%) rename {assets => src/assets}/lightbulb.svg (100%) rename {assets => src/assets}/link.svg (100%) rename {assets => src/assets}/lock.svg (100%) rename {assets => src/assets}/logo.svg (100%) rename {assets => src/assets}/magnet.svg (100%) rename {assets => src/assets}/moon.svg (100%) rename {assets => src/assets}/nodes.svg (100%) rename {assets => src/assets}/overview.svg (100%) rename {assets => src/assets}/profile.svg (100%) rename {assets => src/assets}/redo.svg (100%) rename {assets => src/assets}/reports.svg (100%) rename {assets => src/assets}/sun.svg (100%) rename {assets => src/assets}/user.svg (100%) rename {assets => src/assets}/users.svg (100%) rename {assets => src/assets}/visitor.svg (100%) rename {assets => src/assets}/website.svg (100%) rename {components => src/components}/common/ConfirmDeleteForm.js (93%) rename {components => src/components}/common/Empty.js (86%) rename {components => src/components}/common/Empty.module.css (100%) rename {components => src/components}/common/EmptyPlaceholder.js (100%) rename {components => src/components}/common/ErrorBoundary.js (93%) rename {components => src/components}/common/ErrorBoundry.module.css (100%) rename {components => src/components}/common/ErrorMessage.js (88%) rename {components => src/components}/common/ErrorMessage.module.css (100%) rename {components => src/components}/common/Favicon.js (100%) rename {components => src/components}/common/Favicon.module.css (100%) rename {components => src/components}/common/FilterButtons.js (100%) rename {components => src/components}/common/FilterLink.js (91%) rename {components => src/components}/common/FilterLink.module.css (100%) rename {components => src/components}/common/HamburgerButton.js (93%) rename {components => src/components}/common/HamburgerButton.module.css (100%) rename {components => src/components}/common/HoverTooltip.js (100%) rename {components => src/components}/common/HoverTooltip.module.css (100%) rename {components => src/components}/common/LinkButton.js (77%) rename {components => src/components}/common/LinkButton.module.css (100%) rename {components => src/components}/common/MobileMenu.js (100%) rename {components => src/components}/common/MobileMenu.module.css (100%) rename {components => src/components}/common/Pager.js (95%) rename {components => src/components}/common/Pager.module.css (100%) rename {components => src/components}/common/SettingsTable.js (98%) rename {components => src/components}/common/SettingsTable.module.css (100%) rename {components => src/components}/common/UpdateNotice.js (96%) rename {components => src/components}/common/UpdateNotice.module.css (100%) rename {components => src/components}/common/WorldMap.js (94%) rename {components => src/components}/common/WorldMap.module.css (100%) rename {components => src/components}/declarations.d.ts (100%) rename {hooks => src/components/hooks}/index.js (100%) rename {hooks => src/components/hooks}/useApi.ts (100%) rename {hooks => src/components/hooks}/useApiFilter.ts (100%) rename {hooks => src/components/hooks}/useConfig.js (91%) rename {hooks => src/components/hooks}/useCountryNames.js (100%) rename {hooks => src/components/hooks}/useDateRange.js (100%) rename {hooks => src/components/hooks}/useDocumentClick.js (100%) rename {hooks => src/components/hooks}/useEscapeKey.js (100%) rename {hooks => src/components/hooks}/useFilters.js (97%) rename {hooks => src/components/hooks}/useForceUpdate.js (100%) rename {hooks => src/components/hooks}/useFormat.js (100%) rename {hooks => src/components/hooks}/useLanguageNames.js (100%) rename {hooks => src/components/hooks}/useLocale.js (96%) rename {hooks => src/components/hooks}/useMessages.js (100%) rename {hooks => src/components/hooks}/usePageQuery.js (100%) rename {hooks => src/components/hooks}/useReport.js (100%) rename {hooks => src/components/hooks}/useReports.js (94%) rename {hooks => src/components/hooks}/useRequireLogin.js (84%) rename {hooks => src/components/hooks}/useShareToken.js (100%) rename {hooks => src/components/hooks}/useSticky.js (100%) rename {hooks => src/components/hooks}/useTheme.js (100%) rename {hooks => src/components/hooks}/useTimezone.js (100%) rename {hooks => src/components/hooks}/useUser.js (100%) rename {hooks => src/components/hooks}/useWebsite.js (100%) rename {hooks => src/components/hooks}/useWebsiteReports.js (94%) rename {components => src/components}/icons.ts (98%) rename {components => src/components}/input/DateFilter.js (96%) rename {components => src/components}/input/LanguageButton.js (96%) rename {components => src/components}/input/LanguageButton.module.css (100%) rename {components => src/components}/input/LogoutButton.js (84%) rename {components => src/components}/input/MonthSelect.js (97%) rename {components => src/components}/input/MonthSelect.module.css (100%) rename {components => src/components}/input/ProfileButton.js (88%) rename {components => src/components}/input/ProfileButton.module.css (100%) rename {components => src/components}/input/RefreshButton.js (86%) rename {components => src/components}/input/SettingsButton.js (94%) rename {components => src/components}/input/SettingsButton.module.css (100%) rename {components => src/components}/input/ThemeButton.js (95%) rename {components => src/components}/input/ThemeButton.module.css (100%) rename {components => src/components}/input/WebsiteDateFilter.js (76%) rename {components => src/components}/input/WebsiteDateFilter.module.css (100%) rename {components => src/components}/input/WebsiteSelect.js (87%) rename {components => src/components}/layout/AppLayout.js (92%) rename {components => src/components}/layout/AppLayout.module.css (100%) rename {components => src/components}/layout/Footer.js (100%) rename {components => src/components}/layout/Footer.module.css (100%) rename {components => src/components}/layout/Grid.js (100%) rename {components => src/components}/layout/Grid.module.css (100%) rename {components => src/components}/layout/Header.js (100%) rename {components => src/components}/layout/Header.module.css (100%) rename {components => src/components}/layout/NavBar.js (94%) rename {components => src/components}/layout/NavBar.module.css (100%) rename {components => src/components}/layout/NavGroup.js (100%) rename {components => src/components}/layout/NavGroup.module.css (100%) rename {components => src/components}/layout/Page.js (90%) rename {components => src/components}/layout/Page.module.css (100%) rename {components => src/components}/layout/PageHeader.js (100%) rename {components => src/components}/layout/PageHeader.module.css (100%) rename {components => src/components}/layout/ReportsLayout.js (83%) rename {components => src/components}/layout/ReportsLayout.module.css (100%) rename {components => src/components}/layout/SettingsLayout.js (88%) rename {components => src/components}/layout/SettingsLayout.module.css (100%) rename {components => src/components}/layout/ShareLayout.js (100%) rename {components => src/components}/layout/SideNav.js (100%) rename {components => src/components}/layout/SideNav.module.css (100%) rename {components => src/components}/messages.js (100%) rename {components => src/components}/metrics/ActiveUsers.js (89%) rename {components => src/components}/metrics/ActiveUsers.module.css (100%) rename {components => src/components}/metrics/BarChart.js (97%) rename {components => src/components}/metrics/BarChart.module.css (100%) rename {components => src/components}/metrics/BrowsersTable.js (89%) rename {components => src/components}/metrics/CitiesTable.js (86%) rename {components => src/components}/metrics/CountriesTable.js (88%) rename {components => src/components}/metrics/DataTable.js (98%) rename {components => src/components}/metrics/DataTable.module.css (100%) rename {components => src/components}/metrics/DatePickerForm.js (96%) rename {components => src/components}/metrics/DatePickerForm.module.css (100%) rename {components => src/components}/metrics/DevicesTable.js (90%) rename {components => src/components}/metrics/EventsChart.js (98%) rename {components => src/components}/metrics/EventsChart.module.css (100%) rename {components => src/components}/metrics/EventsTable.js (89%) rename {components => src/components}/metrics/FilterTags.js (92%) rename {components => src/components}/metrics/FilterTags.module.css (100%) rename {components => src/components}/metrics/LanguagesTable.js (81%) rename {components => src/components}/metrics/Legend.js (89%) rename {components => src/components}/metrics/Legend.module.css (100%) rename {components => src/components}/metrics/MetricCard.js (100%) rename {components => src/components}/metrics/MetricCard.module.css (100%) rename {components => src/components}/metrics/MetricsBar.js (100%) rename {components => src/components}/metrics/MetricsBar.module.css (100%) rename {components => src/components}/metrics/MetricsTable.js (90%) rename {components => src/components}/metrics/MetricsTable.module.css (100%) rename {components => src/components}/metrics/OSTable.js (93%) rename {components => src/components}/metrics/PagesTable.js (91%) rename {components => src/components}/metrics/PageviewsChart.js (90%) rename {components => src/components}/metrics/QueryParametersTable.js (96%) rename {components => src/components}/metrics/QueryParametersTable.module.css (100%) rename {components => src/components}/metrics/RealtimeChart.js (98%) rename {components => src/components}/metrics/ReferrersTable.js (93%) rename {components => src/components}/metrics/RegionsTable.js (87%) rename {components => src/components}/metrics/ScreenTable.js (87%) rename {components => src/components}/pages/console/TestConsole.js (99%) rename {components => src/components}/pages/console/TestConsole.module.css (100%) rename {components => src/components}/pages/dashboard/Dashboard.js (93%) rename {components => src/components}/pages/dashboard/DashboardEdit.js (98%) rename {components => src/components}/pages/dashboard/DashboardEdit.module.css (100%) rename {components => src/components}/pages/dashboard/DashboardSettingsButton.js (86%) rename {components => src/components}/pages/dashboard/DashboardSettingsButton.module.css (100%) rename {components => src/components}/pages/event-data/EventDataMetricsBar.js (94%) rename {components => src/components}/pages/event-data/EventDataMetricsBar.module.css (100%) rename {components => src/components}/pages/event-data/EventDataTable.js (94%) rename {components => src/components}/pages/event-data/EventDataValueTable.js (96%) rename {components => src/components}/pages/login/LoginForm.js (94%) rename {components => src/components}/pages/login/LoginForm.module.css (100%) rename {components => src/components}/pages/login/LoginLayout.js (86%) rename {components => src/components}/pages/login/LoginLayout.module.css (100%) rename {components => src/components}/pages/realtime/RealtimeCountries.js (84%) rename {components => src/components}/pages/realtime/RealtimeCountries.module.css (100%) rename {components => src/components}/pages/realtime/RealtimeHeader.js (95%) rename {components => src/components}/pages/realtime/RealtimeHeader.module.css (100%) rename {components => src/components}/pages/realtime/RealtimeHome.js (90%) rename {components => src/components}/pages/realtime/RealtimeLog.js (96%) rename {components => src/components}/pages/realtime/RealtimeLog.module.css (100%) rename {components => src/components}/pages/realtime/RealtimePage.js (97%) rename {components => src/components}/pages/realtime/RealtimePage.module.css (100%) rename {components => src/components}/pages/realtime/RealtimeUrls.js (97%) rename {components => src/components}/pages/reports/BaseParameters.js (97%) rename {components => src/components}/pages/reports/FieldAddForm.js (100%) rename {components => src/components}/pages/reports/FieldAddForm.module.css (100%) rename {components => src/components}/pages/reports/FieldAggregateForm.js (96%) rename {components => src/components}/pages/reports/FieldFilterForm.js (96%) rename {components => src/components}/pages/reports/FieldFilterForm.module.css (100%) rename {components => src/components}/pages/reports/FieldSelectForm.js (94%) rename {components => src/components}/pages/reports/FieldSelectForm.module.css (100%) rename {components => src/components}/pages/reports/FilterSelectForm.js (96%) rename {components => src/components}/pages/reports/ParameterList.js (95%) rename {components => src/components}/pages/reports/ParameterList.module.css (100%) rename {components => src/components}/pages/reports/PopupForm.js (100%) rename {components => src/components}/pages/reports/PopupForm.module.css (100%) rename {components => src/components}/pages/reports/Report.js (92%) rename {components => src/components}/pages/reports/ReportBody.js (100%) rename {components => src/components}/pages/reports/ReportDetails.js (100%) rename {components => src/components}/pages/reports/ReportHeader.js (97%) rename {components => src/components}/pages/reports/ReportHeader.module.css (100%) rename {components => src/components}/pages/reports/ReportMenu.js (100%) rename {components => src/components}/pages/reports/ReportTemplates.js (97%) rename {components => src/components}/pages/reports/ReportTemplates.module.css (100%) rename {components => src/components}/pages/reports/ReportsPage.js (96%) rename {components => src/components}/pages/reports/ReportsTable.js (95%) rename {components => src/components}/pages/reports/event-data/EventDataParameters.js (98%) rename {components => src/components}/pages/reports/event-data/EventDataParameters.module.css (100%) rename {components => src/components}/pages/reports/event-data/EventDataReport.js (100%) rename {components => src/components}/pages/reports/event-data/EventDataTable.js (92%) rename {components => src/components}/pages/reports/funnel/FunnelChart.js (92%) rename {components => src/components}/pages/reports/funnel/FunnelChart.module.css (100%) rename {components => src/components}/pages/reports/funnel/FunnelParameters.js (98%) rename {components => src/components}/pages/reports/funnel/FunnelReport.js (100%) rename {components => src/components}/pages/reports/funnel/FunnelReport.module.css (100%) rename {components => src/components}/pages/reports/funnel/FunnelTable.js (90%) rename {components => src/components}/pages/reports/funnel/UrlAddForm.js (95%) rename {components => src/components}/pages/reports/funnel/UrlAddForm.module.css (100%) rename {components => src/components}/pages/reports/insights/InsightsParameters.js (98%) rename {components => src/components}/pages/reports/insights/InsightsParameters.module.css (100%) rename {components => src/components}/pages/reports/insights/InsightsReport.js (100%) rename {components => src/components}/pages/reports/insights/InsightsTable.js (95%) rename {components => src/components}/pages/reports/reports.module.css (100%) rename {components => src/components}/pages/reports/retention/RetentionParameters.js (96%) rename {components => src/components}/pages/reports/retention/RetentionReport.js (100%) rename {components => src/components}/pages/reports/retention/RetentionReport.module.css (100%) rename {components => src/components}/pages/reports/retention/RetentionTable.js (97%) rename {components => src/components}/pages/reports/retention/RetentionTable.module.css (100%) rename {components => src/components}/pages/settings/profile/DateRangeSetting.js (87%) rename {components => src/components}/pages/settings/profile/LanguageSetting.js (89%) rename {components => src/components}/pages/settings/profile/PasswordChangeButton.js (94%) rename {components => src/components}/pages/settings/profile/PasswordEditForm.js (95%) rename {components => src/components}/pages/settings/profile/ProfileDetails.js (91%) rename {components => src/components}/pages/settings/profile/ProfileSettings.js (87%) rename {components => src/components}/pages/settings/profile/ThemeSetting.js (93%) rename {components => src/components}/pages/settings/profile/ThemeSetting.module.css (100%) rename {components => src/components}/pages/settings/profile/TimezoneSetting.js (87%) rename {components => src/components}/pages/settings/teams/TeamAddForm.js (92%) rename {components => src/components}/pages/settings/teams/TeamAddWebsiteForm.js (95%) rename {components => src/components}/pages/settings/teams/TeamDeleteForm.js (90%) rename {components => src/components}/pages/settings/teams/TeamEditForm.js (95%) rename {components => src/components}/pages/settings/teams/TeamJoinForm.js (91%) rename {components => src/components}/pages/settings/teams/TeamLeaveForm.js (90%) rename {components => src/components}/pages/settings/teams/TeamMemberRemoveButton.js (88%) rename {components => src/components}/pages/settings/teams/TeamMembers.js (88%) rename {components => src/components}/pages/settings/teams/TeamMembersTable.js (93%) rename {components => src/components}/pages/settings/teams/TeamSettings.js (93%) rename {components => src/components}/pages/settings/teams/TeamWebsiteRemoveButton.js (87%) rename {components => src/components}/pages/settings/teams/TeamWebsites.js (92%) rename {components => src/components}/pages/settings/teams/TeamWebsitesTable.js (92%) rename {components => src/components}/pages/settings/teams/TeamsList.js (94%) rename {components => src/components}/pages/settings/teams/TeamsTable.js (95%) rename {components => src/components}/pages/settings/teams/WebsiteTags.js (100%) rename {components => src/components}/pages/settings/teams/WebsiteTags.module.css (100%) rename {components => src/components}/pages/settings/users/UserAddButton.js (92%) rename {components => src/components}/pages/settings/users/UserAddForm.js (95%) rename {components => src/components}/pages/settings/users/UserDeleteForm.js (91%) rename {components => src/components}/pages/settings/users/UserEditForm.js (95%) rename {components => src/components}/pages/settings/users/UserSettings.js (91%) rename {components => src/components}/pages/settings/users/UserWebsites.js (90%) rename {components => src/components}/pages/settings/users/UsersList.js (90%) rename {components => src/components}/pages/settings/users/UsersTable.js (93%) rename {components => src/components}/pages/settings/websites/ShareUrl.js (94%) rename {components => src/components}/pages/settings/websites/TrackingCode.js (85%) rename {components => src/components}/pages/settings/websites/WebsiteAddForm.js (93%) rename {components => src/components}/pages/settings/websites/WebsiteData.js (96%) rename {components => src/components}/pages/settings/websites/WebsiteDeleteForm.js (92%) rename {components => src/components}/pages/settings/websites/WebsiteEditForm.js (94%) rename {components => src/components}/pages/settings/websites/WebsiteResetForm.js (92%) rename {components => src/components}/pages/settings/websites/WebsiteSettings.js (95%) rename {components => src/components}/pages/settings/websites/WebsitesList.js (91%) rename {components => src/components}/pages/settings/websites/WebsitesTable.js (94%) rename {components => src/components}/pages/settings/websites/WebsitesTable.module.css (100%) rename {components => src/components}/pages/websites/WebsiteChart.js (98%) rename {components => src/components}/pages/websites/WebsiteChart.module.css (100%) rename {components => src/components}/pages/websites/WebsiteChartList.js (96%) rename {components => src/components}/pages/websites/WebsiteDetailsPage.js (93%) rename {components => src/components}/pages/websites/WebsiteEventData.js (94%) rename {components => src/components}/pages/websites/WebsiteEventData.module.css (100%) rename {components => src/components}/pages/websites/WebsiteEventDataPage.js (100%) rename {components => src/components}/pages/websites/WebsiteHeader.js (94%) rename {components => src/components}/pages/websites/WebsiteHeader.module.css (100%) rename {components => src/components}/pages/websites/WebsiteList.module.css (100%) rename {components => src/components}/pages/websites/WebsiteMenuView.js (96%) rename {components => src/components}/pages/websites/WebsiteMenuView.module.css (100%) rename {components => src/components}/pages/websites/WebsiteMetricsBar.js (99%) rename {components => src/components}/pages/websites/WebsiteMetricsBar.module.css (100%) rename {components => src/components}/pages/websites/WebsiteReportsPage.js (95%) rename {components => src/components}/pages/websites/WebsiteTableView.js (100%) rename {components => src/components}/pages/websites/WebsiteTableView.module.css (100%) rename {components => src/components}/pages/websites/WebsitesPage.js (93%) create mode 100644 src/index.ts rename {lang => src/lang}/am-ET.json (100%) rename {lang => src/lang}/ar-SA.json (100%) rename {lang => src/lang}/be-BY.json (100%) rename {lang => src/lang}/bn-BD.json (100%) rename {lang => src/lang}/ca-ES.json (100%) rename {lang => src/lang}/cs-CZ.json (100%) rename {lang => src/lang}/da-DK.json (100%) rename {lang => src/lang}/de-CH.json (100%) rename {lang => src/lang}/de-DE.json (100%) rename {lang => src/lang}/el-GR.json (100%) rename {lang => src/lang}/en-GB.json (100%) rename {lang => src/lang}/en-US.json (100%) rename {lang => src/lang}/es-ES.json (100%) rename {lang => src/lang}/es-MX.json (100%) rename {lang => src/lang}/fa-IR.json (100%) rename {lang => src/lang}/fi-FI.json (100%) rename {lang => src/lang}/fo-FO.json (100%) rename {lang => src/lang}/fr-FR.json (100%) rename {lang => src/lang}/ga-ES.json (100%) rename {lang => src/lang}/he-IL.json (100%) rename {lang => src/lang}/hi-IN.json (100%) rename {lang => src/lang}/hr-HR.json (100%) rename {lang => src/lang}/hu-HU.json (100%) rename {lang => src/lang}/id-ID.json (100%) rename {lang => src/lang}/it-IT.json (100%) rename {lang => src/lang}/ja-JP.json (100%) rename {lang => src/lang}/km-KH.json (100%) rename {lang => src/lang}/ko-KR.json (100%) rename {lang => src/lang}/lt-LT.json (100%) rename {lang => src/lang}/mn-MN.json (100%) rename {lang => src/lang}/ms-MY.json (100%) rename {lang => src/lang}/my-MM.json (100%) rename {lang => src/lang}/nb-NO.json (100%) rename {lang => src/lang}/nl-NL.json (100%) rename {lang => src/lang}/pl-PL.json (100%) rename {lang => src/lang}/pt-BR.json (100%) rename {lang => src/lang}/pt-PT.json (100%) rename {lang => src/lang}/ro-RO.json (100%) rename {lang => src/lang}/ru-RU.json (100%) rename {lang => src/lang}/si-LK.json (100%) rename {lang => src/lang}/sk-SK.json (100%) rename {lang => src/lang}/sl-SI.json (100%) rename {lang => src/lang}/sv-SE.json (100%) rename {lang => src/lang}/ta-IN.json (100%) rename {lang => src/lang}/th-TH.json (100%) rename {lang => src/lang}/tr-TR.json (100%) rename {lang => src/lang}/uk-UA.json (100%) rename {lang => src/lang}/ur-PK.json (100%) rename {lang => src/lang}/vi-VN.json (100%) rename {lang => src/lang}/zh-CN.json (100%) rename {lang => src/lang}/zh-TW.json (100%) rename {lib => src/lib}/auth.ts (100%) rename {lib => src/lib}/cache.ts (100%) rename {lib => src/lib}/charts.js (100%) rename {lib => src/lib}/clickhouse.ts (100%) rename {lib => src/lib}/client.ts (100%) rename {lib => src/lib}/constants.ts (100%) rename {lib => src/lib}/crypto.js (100%) rename {lib => src/lib}/data.ts (100%) rename {lib => src/lib}/date.js (100%) rename {lib => src/lib}/db.js (100%) rename {lib => src/lib}/detect.ts (100%) rename {lib => src/lib}/filters.js (100%) rename {lib => src/lib}/format.js (100%) rename {lib => src/lib}/kafka.ts (100%) rename {lib => src/lib}/lang.js (100%) rename {lib => src/lib}/load.ts (100%) rename {lib => src/lib}/middleware.ts (100%) rename {lib => src/lib}/prisma.ts (100%) rename {lib => src/lib}/query.ts (100%) rename {lib => src/lib}/session.ts (100%) rename {lib => src/lib}/sql.ts (100%) rename {lib => src/lib}/types.ts (100%) rename {lib => src/lib}/yup.ts (100%) rename {pages => src/pages}/404.js (90%) rename {pages => src/pages}/_app.js (96%) rename {pages => src/pages}/api/auth/login.ts (100%) rename {pages => src/pages}/api/auth/logout.ts (100%) rename {pages => src/pages}/api/auth/sso.ts (100%) rename {pages => src/pages}/api/auth/verify.ts (100%) rename {pages => src/pages}/api/config.ts (100%) rename {pages => src/pages}/api/event-data/events.ts (100%) rename {pages => src/pages}/api/event-data/fields.ts (100%) rename {pages => src/pages}/api/event-data/stats.ts (100%) rename {pages => src/pages}/api/heartbeat.ts (100%) rename {pages => src/pages}/api/me/index.ts (100%) rename {pages => src/pages}/api/me/password.ts (100%) rename {pages => src/pages}/api/me/teams.ts (100%) rename {pages => src/pages}/api/me/websites.ts (100%) rename {pages => src/pages}/api/realtime/[id].ts (100%) rename {pages => src/pages}/api/reports/[id].ts (100%) rename {pages => src/pages}/api/reports/funnel.ts (100%) rename {pages => src/pages}/api/reports/index.ts (100%) rename {pages => src/pages}/api/reports/insights.ts (100%) rename {pages => src/pages}/api/reports/retention.ts (100%) rename {pages => src/pages}/api/scripts/telemetry.js (100%) rename {pages => src/pages}/api/send.ts (100%) rename {pages => src/pages}/api/share/[id].ts (100%) rename {pages => src/pages}/api/teams/[id]/index.ts (100%) rename {pages => src/pages}/api/teams/[id]/users/[userId].ts (100%) rename {pages => src/pages}/api/teams/[id]/users/index.ts (100%) rename {pages => src/pages}/api/teams/[id]/websites/[websiteId].ts (100%) rename {pages => src/pages}/api/teams/[id]/websites/index.ts (100%) rename {pages => src/pages}/api/teams/index.ts (100%) rename {pages => src/pages}/api/teams/join.ts (100%) rename {pages => src/pages}/api/users/[id]/index.ts (100%) rename {pages => src/pages}/api/users/[id]/teams.ts (100%) rename {pages => src/pages}/api/users/[id]/usage.ts (100%) rename {pages => src/pages}/api/users/[id]/websites.ts (100%) rename {pages => src/pages}/api/users/index.ts (100%) rename {pages => src/pages}/api/websites/[id]/active.ts (100%) rename {pages => src/pages}/api/websites/[id]/daterange.ts (100%) rename {pages => src/pages}/api/websites/[id]/events.ts (100%) rename {pages => src/pages}/api/websites/[id]/index.ts (100%) rename {pages => src/pages}/api/websites/[id]/metrics.ts (100%) rename {pages => src/pages}/api/websites/[id]/pageviews.ts (100%) rename {pages => src/pages}/api/websites/[id]/reports.ts (100%) rename {pages => src/pages}/api/websites/[id]/reset.ts (100%) rename {pages => src/pages}/api/websites/[id]/stats.ts (100%) rename {pages => src/pages}/api/websites/[id]/values.ts (100%) rename {pages => src/pages}/api/websites/index.ts (100%) rename {pages => src/pages}/console/[[...id]].js (100%) rename {pages => src/pages}/dashboard/index.js (85%) rename {pages => src/pages}/index.js (100%) rename {pages => src/pages}/login.js (100%) rename {pages => src/pages}/logout.js (93%) rename {pages => src/pages}/reports/[id].js (92%) rename {pages => src/pages}/reports/create.js (87%) rename {pages => src/pages}/reports/funnel.js (86%) rename {pages => src/pages}/reports/index.js (86%) rename {pages => src/pages}/reports/insights.js (88%) rename {pages => src/pages}/reports/retention.js (86%) rename {pages => src/pages}/settings/profile/index.js (89%) rename {pages => src/pages}/settings/teams/[id].js (93%) rename {pages => src/pages}/settings/teams/index.js (91%) rename {pages => src/pages}/settings/users/[id].js (93%) rename {pages => src/pages}/settings/users/index.js (91%) rename {pages => src/pages}/settings/websites/[id].js (93%) rename {pages => src/pages}/settings/websites/index.js (92%) rename {pages => src/pages}/share/[...id].js (89%) rename {pages => src/pages}/sso.js (100%) rename {pages => src/pages}/websites/[id]/event-data.js (89%) rename {pages => src/pages}/websites/[id]/index.js (89%) rename {pages => src/pages}/websites/[id]/realtime.js (100%) rename {pages => src/pages}/websites/[id]/reports.js (100%) rename {pages => src/pages}/websites/index.js (84%) rename {queries => src/queries}/admin/report.ts (100%) rename {queries => src/queries}/admin/team.ts (100%) rename {queries => src/queries}/admin/teamUser.ts (100%) rename {queries => src/queries}/admin/teamWebsite.ts (100%) rename {queries => src/queries}/admin/user.ts (100%) rename {queries => src/queries}/admin/website.ts (100%) rename {queries => src/queries}/analytics/eventData/getEventDataEvents.ts (100%) rename {queries => src/queries}/analytics/eventData/getEventDataFields.ts (100%) rename {queries => src/queries}/analytics/eventData/getEventDataStats.ts (100%) rename {queries => src/queries}/analytics/eventData/getEventDataUsage.ts (100%) rename {queries => src/queries}/analytics/eventData/saveEventData.ts (100%) rename {queries => src/queries}/analytics/events/getEventMetrics.ts (100%) rename {queries => src/queries}/analytics/events/getEventUsage.ts (100%) rename {queries => src/queries}/analytics/events/getEvents.ts (100%) rename {queries => src/queries}/analytics/events/saveEvent.ts (100%) rename {queries => src/queries}/analytics/getActiveVisitors.ts (100%) rename {queries => src/queries}/analytics/getRealtimeData.ts (100%) rename {queries => src/queries}/analytics/getValues.ts (100%) rename {queries => src/queries}/analytics/getWebsiteDateRange.ts (100%) rename {queries => src/queries}/analytics/getWebsiteStats.ts (100%) rename {queries => src/queries}/analytics/pageviews/getPageviewMetrics.ts (100%) rename {queries => src/queries}/analytics/pageviews/getPageviewStats.ts (100%) rename {queries => src/queries}/analytics/reports/getFunnel.ts (100%) rename {queries => src/queries}/analytics/reports/getInsights.ts (100%) rename {queries => src/queries}/analytics/reports/getRetention.ts (100%) rename {queries => src/queries}/analytics/sessions/createSession.ts (100%) rename {queries => src/queries}/analytics/sessions/getSession.ts (100%) rename {queries => src/queries}/analytics/sessions/getSessionMetrics.ts (100%) rename {queries => src/queries}/analytics/sessions/getSessionStats.ts (100%) rename {queries => src/queries}/analytics/sessions/getSessions.ts (100%) rename {queries => src/queries}/analytics/sessions/saveSessionData.ts (100%) rename {queries => src/queries}/index.js (100%) rename {store => src/store}/app.js (100%) rename {store => src/store}/dashboard.js (100%) rename {store => src/store}/queries.js (100%) rename {store => src/store}/version.js (100%) rename {store => src/store}/websites.ts (100%) rename {styles => src/styles}/index.css (100%) rename {styles => src/styles}/locale.css (100%) rename {styles => src/styles}/variables.css (100%) rename {tracker => src/tracker}/index.d.ts (100%) rename {tracker => src/tracker}/index.js (100%) diff --git a/.eslintrc.json b/.eslintrc.json index f6d90ccab..a77ed5bd8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,22 +19,21 @@ "plugin:@typescript-eslint/recommended", "next" ], - "plugins": ["@typescript-eslint", "prettier"], "settings": { "import/resolver": { "alias": { "map": [ - ["assets", "./assets"], - ["components", "./components"], + ["assets", "./src/assets"], + ["components", "./src/components"], ["db", "./db"], - ["hooks", "./hooks"], - ["lang", "./lang"], - ["lib", "./lib"], + ["hooks", "./src/components/hooks"], + ["lang", "./src/lang"], + ["lib", "./src/lib"], ["public", "./public"], - ["queries", "./queries"], - ["store", "./store"], - ["styles", "./styles"] + ["queries", "./src/queries"], + ["store", "./src/store"], + ["styles", "./src/styles"] ], "extensions": [".ts", ".tsx", ".js", ".jsx", ".json"] } diff --git a/jsconfig.json b/jsconfig.json index b639b0f8f..738e8a465 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,5 +1,5 @@ { "compilerOptions": { - "baseUrl": "." + "baseUrl": "./src" } -} \ No newline at end of file +} diff --git a/package.components.json b/package.components.json new file mode 100644 index 000000000..4596caa23 --- /dev/null +++ b/package.components.json @@ -0,0 +1,10 @@ +{ + "name": "@umami/components", + "version": "0.1.0", + "description": "Umami React components.", + "author": "Mike Cao ", + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts" +} diff --git a/package.json b/package.json index e1361d208..4f1eaa795 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "start-env": "node scripts/start-env.js", "start-server": "node server.js", "build-app": "next build", - "build-tracker": "rollup -c rollup.tracker.config.js", + "build-components": "rollup -c rollup.components.config.mjs", + "build-tracker": "rollup -c rollup.tracker.config.mjs", "build-db": "npm-run-all copy-db-files build-db-client", "build-lang": "npm-run-all format-lang compile-lang download-country-names download-language-names", "build-geo": "node scripts/build-geo.js", @@ -115,13 +116,16 @@ "@formatjs/cli": "^4.2.29", "@netlify/plugin-nextjs": "^4.27.3", "@rollup/plugin-alias": "^5.0.0", - "@rollup/plugin-buble": "^0.21.3", - "@rollup/plugin-commonjs": "^24.1.0", + "@rollup/plugin-buble": "^1.0.2", + "@rollup/plugin-commonjs": "^25.0.4", "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.0.2", - "@rollup/plugin-replace": "^4.0.0", - "@svgr/rollup": "^7.0.0", + "@rollup/plugin-node-resolve": "^15.2.0", + "@rollup/plugin-replace": "^5.0.2", + "@svgr/rollup": "^8.1.0", "@svgr/webpack": "^6.2.1", + "@types/node": "^18.11.9", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", "@typescript-eslint/eslint-plugin": "^5.50.0", "@typescript-eslint/parser": "^5.50.0", "cross-env": "^7.0.3", @@ -143,11 +147,12 @@ "prettier": "^2.6.2", "prisma": "5.0.0", "prompts": "2.4.2", - "rollup": "^2.70.1", + "rollup": "^3.28.0", + "rollup-plugin-copy": "^3.4.0", "rollup-plugin-delete": "^2.0.0", - "rollup-plugin-dts": "^5.3.0", + "rollup-plugin-dts": "^6.0.0", "rollup-plugin-esbuild": "^5.0.0", - "rollup-plugin-node-externals": "^5.1.2", + "rollup-plugin-node-externals": "^6.1.1", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-terser": "^7.0.2", "stylelint": "^15.10.1", @@ -156,6 +161,6 @@ "stylelint-config-recommended": "^9.0.0", "tar": "^6.1.2", "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "typescript": "^5.1.6" } } diff --git a/rollup.components.config.mjs b/rollup.components.config.mjs new file mode 100644 index 000000000..5c8722b4c --- /dev/null +++ b/rollup.components.config.mjs @@ -0,0 +1,99 @@ +import path from 'path'; +import crypto from 'crypto'; +import resolve from '@rollup/plugin-node-resolve'; +import alias from '@rollup/plugin-alias'; +import json from '@rollup/plugin-json'; +import postcss from 'rollup-plugin-postcss'; +import copy from 'rollup-plugin-copy'; +import del from 'rollup-plugin-delete'; +import nodeExternals from 'rollup-plugin-node-externals'; +import esbuild from 'rollup-plugin-esbuild'; +import dts from 'rollup-plugin-dts'; +import svgr from '@svgr/rollup'; + +const md5 = str => crypto.createHash('md5').update(str).digest('hex'); + +const customResolver = resolve({ + extensions: ['.js', '.jsx', '.ts', '.tsx'], +}); + +const aliasConfig = { + entries: [ + { find: /^components/, replacement: path.resolve('./src/components') }, + { find: /^hooks/, replacement: path.resolve('./src/hooks') }, + { find: /^lib/, replacement: path.resolve('./src/lib') }, + { find: /^store/, replacement: path.resolve('./src/store') }, + { find: /^public/, replacement: path.resolve('./public') }, + { find: /^assets/, replacement: path.resolve('./src/assets') }, + ], + customResolver, +}; + +const external = [ + 'react', + 'react-dom', + 'react/jsx-runtime', + 'react-intl', + 'react-basics', + 'classnames', + 'next', +]; + +const jsBundle = { + input: 'src/index.ts', + output: [ + { + file: 'dist/index.js', + format: 'cjs', + sourcemap: true, + }, + { + file: 'dist/index.mjs', + format: 'es', + sourcemap: true, + }, + ], + plugins: [ + del({ targets: 'dist/*', runOnce: true }), + copy({ targets: [{ src: './package.components.json', dest: 'dist', rename: 'package.json' }] }), + postcss({ + config: false, + extract: 'styles.css', + sourceMap: true, + minimize: true, + modules: { + generateScopedName: function (name, filename, css) { + const file = path.basename(filename, '.css').replace('.module', ''); + const hash = Buffer.from(md5(`${name}:${filename}:${css}`)) + .toString('base64') + .substring(0, 5); + + return `${file}-${name}--${hash}`; + }, + }, + }), + svgr({ icon: true }), + nodeExternals(), + json(), + alias(aliasConfig), + esbuild({ + target: 'es6', + jsx: 'transform', + loaders: { + '.js': 'jsx', + }, + }), + ], +}; + +const dtsBundle = { + input: 'src/index.ts', + output: { + file: 'dist/index.d.ts', + format: 'es', + }, + plugins: [alias(aliasConfig), nodeExternals(), json(), dts()], + external: [/\.css/], +}; + +export default [jsBundle, dtsBundle]; diff --git a/rollup.tracker.config.js b/rollup.tracker.config.mjs similarity index 93% rename from rollup.tracker.config.js rename to rollup.tracker.config.mjs index f4e7223c6..465e1af36 100644 --- a/rollup.tracker.config.js +++ b/rollup.tracker.config.mjs @@ -4,7 +4,7 @@ import replace from '@rollup/plugin-replace'; import { terser } from 'rollup-plugin-terser'; export default { - input: 'tracker/index.js', + input: 'src/tracker/index.js', output: { file: 'public/script.js', format: 'iife', diff --git a/scripts/check-lang.js b/scripts/check-lang.js index e5a0bf095..a1b604310 100644 --- a/scripts/check-lang.js +++ b/scripts/check-lang.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const chalk = require('chalk'); -const messages = require('../lang/en-US.json'); +const messages = require('../src/lang/en-US.json'); const ignore = require('../lang-ignore.json'); const dir = path.resolve(__dirname, '../lang'); diff --git a/assets/add-user.svg b/src/assets/add-user.svg similarity index 100% rename from assets/add-user.svg rename to src/assets/add-user.svg diff --git a/assets/bar-chart.svg b/src/assets/bar-chart.svg similarity index 100% rename from assets/bar-chart.svg rename to src/assets/bar-chart.svg diff --git a/assets/bars.svg b/src/assets/bars.svg similarity index 100% rename from assets/bars.svg rename to src/assets/bars.svg diff --git a/assets/bolt.svg b/src/assets/bolt.svg similarity index 100% rename from assets/bolt.svg rename to src/assets/bolt.svg diff --git a/assets/calendar.svg b/src/assets/calendar.svg similarity index 100% rename from assets/calendar.svg rename to src/assets/calendar.svg diff --git a/assets/clock.svg b/src/assets/clock.svg similarity index 100% rename from assets/clock.svg rename to src/assets/clock.svg diff --git a/assets/dashboard.svg b/src/assets/dashboard.svg similarity index 100% rename from assets/dashboard.svg rename to src/assets/dashboard.svg diff --git a/assets/expand.svg b/src/assets/expand.svg similarity index 100% rename from assets/expand.svg rename to src/assets/expand.svg diff --git a/assets/eye.svg b/src/assets/eye.svg similarity index 100% rename from assets/eye.svg rename to src/assets/eye.svg diff --git a/assets/funnel.svg b/src/assets/funnel.svg similarity index 100% rename from assets/funnel.svg rename to src/assets/funnel.svg diff --git a/assets/gear.svg b/src/assets/gear.svg similarity index 100% rename from assets/gear.svg rename to src/assets/gear.svg diff --git a/assets/globe.svg b/src/assets/globe.svg similarity index 100% rename from assets/globe.svg rename to src/assets/globe.svg diff --git a/assets/lightbulb.svg b/src/assets/lightbulb.svg similarity index 100% rename from assets/lightbulb.svg rename to src/assets/lightbulb.svg diff --git a/assets/link.svg b/src/assets/link.svg similarity index 100% rename from assets/link.svg rename to src/assets/link.svg diff --git a/assets/lock.svg b/src/assets/lock.svg similarity index 100% rename from assets/lock.svg rename to src/assets/lock.svg diff --git a/assets/logo.svg b/src/assets/logo.svg similarity index 100% rename from assets/logo.svg rename to src/assets/logo.svg diff --git a/assets/magnet.svg b/src/assets/magnet.svg similarity index 100% rename from assets/magnet.svg rename to src/assets/magnet.svg diff --git a/assets/moon.svg b/src/assets/moon.svg similarity index 100% rename from assets/moon.svg rename to src/assets/moon.svg diff --git a/assets/nodes.svg b/src/assets/nodes.svg similarity index 100% rename from assets/nodes.svg rename to src/assets/nodes.svg diff --git a/assets/overview.svg b/src/assets/overview.svg similarity index 100% rename from assets/overview.svg rename to src/assets/overview.svg diff --git a/assets/profile.svg b/src/assets/profile.svg similarity index 100% rename from assets/profile.svg rename to src/assets/profile.svg diff --git a/assets/redo.svg b/src/assets/redo.svg similarity index 100% rename from assets/redo.svg rename to src/assets/redo.svg diff --git a/assets/reports.svg b/src/assets/reports.svg similarity index 100% rename from assets/reports.svg rename to src/assets/reports.svg diff --git a/assets/sun.svg b/src/assets/sun.svg similarity index 100% rename from assets/sun.svg rename to src/assets/sun.svg diff --git a/assets/user.svg b/src/assets/user.svg similarity index 100% rename from assets/user.svg rename to src/assets/user.svg diff --git a/assets/users.svg b/src/assets/users.svg similarity index 100% rename from assets/users.svg rename to src/assets/users.svg diff --git a/assets/visitor.svg b/src/assets/visitor.svg similarity index 100% rename from assets/visitor.svg rename to src/assets/visitor.svg diff --git a/assets/website.svg b/src/assets/website.svg similarity index 100% rename from assets/website.svg rename to src/assets/website.svg diff --git a/components/common/ConfirmDeleteForm.js b/src/components/common/ConfirmDeleteForm.js similarity index 93% rename from components/common/ConfirmDeleteForm.js rename to src/components/common/ConfirmDeleteForm.js index 3496a3053..fed618da9 100644 --- a/components/common/ConfirmDeleteForm.js +++ b/src/components/common/ConfirmDeleteForm.js @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Button, LoadingButton, Form, FormButtons } from 'react-basics'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function ConfirmDeleteForm({ name, onConfirm, onClose }) { const [loading, setLoading] = useState(false); diff --git a/components/common/Empty.js b/src/components/common/Empty.js similarity index 86% rename from components/common/Empty.js rename to src/components/common/Empty.js index 95681b167..c0be761a0 100644 --- a/components/common/Empty.js +++ b/src/components/common/Empty.js @@ -1,6 +1,6 @@ import classNames from 'classnames'; import styles from './Empty.module.css'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function Empty({ message, className }) { const { formatMessage, messages } = useMessages(); diff --git a/components/common/Empty.module.css b/src/components/common/Empty.module.css similarity index 100% rename from components/common/Empty.module.css rename to src/components/common/Empty.module.css diff --git a/components/common/EmptyPlaceholder.js b/src/components/common/EmptyPlaceholder.js similarity index 100% rename from components/common/EmptyPlaceholder.js rename to src/components/common/EmptyPlaceholder.js diff --git a/components/common/ErrorBoundary.js b/src/components/common/ErrorBoundary.js similarity index 93% rename from components/common/ErrorBoundary.js rename to src/components/common/ErrorBoundary.js index f97fd92c2..32cedb398 100644 --- a/components/common/ErrorBoundary.js +++ b/src/components/common/ErrorBoundary.js @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { ErrorBoundary as Boundary } from 'react-error-boundary'; import { Button } from 'react-basics'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; import styles from './ErrorBoundry.module.css'; const logError = (error, info) => { diff --git a/components/common/ErrorBoundry.module.css b/src/components/common/ErrorBoundry.module.css similarity index 100% rename from components/common/ErrorBoundry.module.css rename to src/components/common/ErrorBoundry.module.css diff --git a/components/common/ErrorMessage.js b/src/components/common/ErrorMessage.js similarity index 88% rename from components/common/ErrorMessage.js rename to src/components/common/ErrorMessage.js index e2b227478..f8129c6b0 100644 --- a/components/common/ErrorMessage.js +++ b/src/components/common/ErrorMessage.js @@ -1,6 +1,6 @@ import { Icon, Icons, Text } from 'react-basics'; import styles from './ErrorMessage.module.css'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function ErrorMessage() { const { formatMessage, messages } = useMessages(); diff --git a/components/common/ErrorMessage.module.css b/src/components/common/ErrorMessage.module.css similarity index 100% rename from components/common/ErrorMessage.module.css rename to src/components/common/ErrorMessage.module.css diff --git a/components/common/Favicon.js b/src/components/common/Favicon.js similarity index 100% rename from components/common/Favicon.js rename to src/components/common/Favicon.js diff --git a/components/common/Favicon.module.css b/src/components/common/Favicon.module.css similarity index 100% rename from components/common/Favicon.module.css rename to src/components/common/Favicon.module.css diff --git a/components/common/FilterButtons.js b/src/components/common/FilterButtons.js similarity index 100% rename from components/common/FilterButtons.js rename to src/components/common/FilterButtons.js diff --git a/components/common/FilterLink.js b/src/components/common/FilterLink.js similarity index 91% rename from components/common/FilterLink.js rename to src/components/common/FilterLink.js index 30cdc025b..2a95e011d 100644 --- a/components/common/FilterLink.js +++ b/src/components/common/FilterLink.js @@ -2,8 +2,8 @@ import { Icon, Icons } from 'react-basics'; import classNames from 'classnames'; import Link from 'next/link'; import { safeDecodeURI } from 'next-basics'; -import usePageQuery from 'hooks/usePageQuery'; -import useMessages from 'hooks/useMessages'; +import usePageQuery from 'components/hooks/usePageQuery'; +import useMessages from 'components/hooks/useMessages'; import styles from './FilterLink.module.css'; export function FilterLink({ id, value, label, externalUrl, children, className }) { diff --git a/components/common/FilterLink.module.css b/src/components/common/FilterLink.module.css similarity index 100% rename from components/common/FilterLink.module.css rename to src/components/common/FilterLink.module.css diff --git a/components/common/HamburgerButton.js b/src/components/common/HamburgerButton.js similarity index 93% rename from components/common/HamburgerButton.js rename to src/components/common/HamburgerButton.js index 48c807708..9feee67b7 100644 --- a/components/common/HamburgerButton.js +++ b/src/components/common/HamburgerButton.js @@ -2,8 +2,8 @@ import { Button, Icon } from 'react-basics'; import { useState } from 'react'; import MobileMenu from './MobileMenu'; import Icons from 'components/icons'; -import useMessages from 'hooks/useMessages'; -import useConfig from 'hooks/useConfig'; +import useMessages from 'components/hooks/useMessages'; +import useConfig from 'components/hooks/useConfig'; export function HamburgerButton() { const { formatMessage, labels } = useMessages(); diff --git a/components/common/HamburgerButton.module.css b/src/components/common/HamburgerButton.module.css similarity index 100% rename from components/common/HamburgerButton.module.css rename to src/components/common/HamburgerButton.module.css diff --git a/components/common/HoverTooltip.js b/src/components/common/HoverTooltip.js similarity index 100% rename from components/common/HoverTooltip.js rename to src/components/common/HoverTooltip.js diff --git a/components/common/HoverTooltip.module.css b/src/components/common/HoverTooltip.module.css similarity index 100% rename from components/common/HoverTooltip.module.css rename to src/components/common/HoverTooltip.module.css diff --git a/components/common/LinkButton.js b/src/components/common/LinkButton.js similarity index 77% rename from components/common/LinkButton.js rename to src/components/common/LinkButton.js index 8c0501475..54c7fa631 100644 --- a/components/common/LinkButton.js +++ b/src/components/common/LinkButton.js @@ -2,7 +2,7 @@ import Link from 'next/link'; import { Icon, Icons, Text } from 'react-basics'; import styles from './LinkButton.module.css'; -export default function LinkButton({ href, icon, children }) { +export function LinkButton({ href, icon, children }) { return ( {icon || } @@ -10,3 +10,5 @@ export default function LinkButton({ href, icon, children }) { ); } + +export default LinkButton; diff --git a/components/common/LinkButton.module.css b/src/components/common/LinkButton.module.css similarity index 100% rename from components/common/LinkButton.module.css rename to src/components/common/LinkButton.module.css diff --git a/components/common/MobileMenu.js b/src/components/common/MobileMenu.js similarity index 100% rename from components/common/MobileMenu.js rename to src/components/common/MobileMenu.js diff --git a/components/common/MobileMenu.module.css b/src/components/common/MobileMenu.module.css similarity index 100% rename from components/common/MobileMenu.module.css rename to src/components/common/MobileMenu.module.css diff --git a/components/common/Pager.js b/src/components/common/Pager.js similarity index 95% rename from components/common/Pager.js rename to src/components/common/Pager.js index aaeffbaec..7a5e7ed5f 100644 --- a/components/common/Pager.js +++ b/src/components/common/Pager.js @@ -1,6 +1,6 @@ import styles from './Pager.module.css'; import { Button, Flexbox, Icon, Icons } from 'react-basics'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function Pager({ page, pageSize, count, onPageChange }) { const { formatMessage, labels } = useMessages(); diff --git a/components/common/Pager.module.css b/src/components/common/Pager.module.css similarity index 100% rename from components/common/Pager.module.css rename to src/components/common/Pager.module.css diff --git a/components/common/SettingsTable.js b/src/components/common/SettingsTable.js similarity index 98% rename from components/common/SettingsTable.js rename to src/components/common/SettingsTable.js index eb7a64112..2df3b391b 100644 --- a/components/common/SettingsTable.js +++ b/src/components/common/SettingsTable.js @@ -1,5 +1,5 @@ import Empty from 'components/common/Empty'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; import { useState } from 'react'; import { SearchField, diff --git a/components/common/SettingsTable.module.css b/src/components/common/SettingsTable.module.css similarity index 100% rename from components/common/SettingsTable.module.css rename to src/components/common/SettingsTable.module.css diff --git a/components/common/UpdateNotice.js b/src/components/common/UpdateNotice.js similarity index 96% rename from components/common/UpdateNotice.js rename to src/components/common/UpdateNotice.js index bef6be982..e3edc70c1 100644 --- a/components/common/UpdateNotice.js +++ b/src/components/common/UpdateNotice.js @@ -4,7 +4,7 @@ import { setItem } from 'next-basics'; import useStore, { checkVersion } from 'store/version'; import { REPO_URL, VERSION_CHECK } from 'lib/constants'; import styles from './UpdateNotice.module.css'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; import { useRouter } from 'next/router'; export function UpdateNotice({ user, config }) { diff --git a/components/common/UpdateNotice.module.css b/src/components/common/UpdateNotice.module.css similarity index 100% rename from components/common/UpdateNotice.module.css rename to src/components/common/UpdateNotice.module.css diff --git a/components/common/WorldMap.js b/src/components/common/WorldMap.js similarity index 94% rename from components/common/WorldMap.js rename to src/components/common/WorldMap.js index 9c91e4a47..b593099b7 100644 --- a/components/common/WorldMap.js +++ b/src/components/common/WorldMap.js @@ -5,9 +5,9 @@ import classNames from 'classnames'; import { colord } from 'colord'; import HoverTooltip from 'components/common/HoverTooltip'; import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants'; -import useTheme from 'hooks/useTheme'; -import useCountryNames from 'hooks/useCountryNames'; -import useLocale from 'hooks/useLocale'; +import useTheme from 'components/hooks/useTheme'; +import useCountryNames from 'components/hooks/useCountryNames'; +import useLocale from 'components/hooks/useLocale'; import { formatLongNumber } from 'lib/format'; import { percentFilter } from 'lib/filters'; import styles from './WorldMap.module.css'; diff --git a/components/common/WorldMap.module.css b/src/components/common/WorldMap.module.css similarity index 100% rename from components/common/WorldMap.module.css rename to src/components/common/WorldMap.module.css diff --git a/components/declarations.d.ts b/src/components/declarations.d.ts similarity index 100% rename from components/declarations.d.ts rename to src/components/declarations.d.ts diff --git a/hooks/index.js b/src/components/hooks/index.js similarity index 100% rename from hooks/index.js rename to src/components/hooks/index.js diff --git a/hooks/useApi.ts b/src/components/hooks/useApi.ts similarity index 100% rename from hooks/useApi.ts rename to src/components/hooks/useApi.ts diff --git a/hooks/useApiFilter.ts b/src/components/hooks/useApiFilter.ts similarity index 100% rename from hooks/useApiFilter.ts rename to src/components/hooks/useApiFilter.ts diff --git a/hooks/useConfig.js b/src/components/hooks/useConfig.js similarity index 91% rename from hooks/useConfig.js rename to src/components/hooks/useConfig.js index 2dead15a8..6b37c87b0 100644 --- a/hooks/useConfig.js +++ b/src/components/hooks/useConfig.js @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import useStore, { setConfig } from 'store/app'; -import useApi from 'hooks/useApi'; +import useApi from 'components/hooks/useApi'; let loading = false; diff --git a/hooks/useCountryNames.js b/src/components/hooks/useCountryNames.js similarity index 100% rename from hooks/useCountryNames.js rename to src/components/hooks/useCountryNames.js diff --git a/hooks/useDateRange.js b/src/components/hooks/useDateRange.js similarity index 100% rename from hooks/useDateRange.js rename to src/components/hooks/useDateRange.js diff --git a/hooks/useDocumentClick.js b/src/components/hooks/useDocumentClick.js similarity index 100% rename from hooks/useDocumentClick.js rename to src/components/hooks/useDocumentClick.js diff --git a/hooks/useEscapeKey.js b/src/components/hooks/useEscapeKey.js similarity index 100% rename from hooks/useEscapeKey.js rename to src/components/hooks/useEscapeKey.js diff --git a/hooks/useFilters.js b/src/components/hooks/useFilters.js similarity index 97% rename from hooks/useFilters.js rename to src/components/hooks/useFilters.js index 089f2ee84..e1a9a8858 100644 --- a/hooks/useFilters.js +++ b/src/components/hooks/useFilters.js @@ -1,4 +1,4 @@ -import { useMessages } from 'hooks'; +import { useMessages } from './useMessages'; import { OPERATORS } from 'lib/constants'; export function useFilters() { diff --git a/hooks/useForceUpdate.js b/src/components/hooks/useForceUpdate.js similarity index 100% rename from hooks/useForceUpdate.js rename to src/components/hooks/useForceUpdate.js diff --git a/hooks/useFormat.js b/src/components/hooks/useFormat.js similarity index 100% rename from hooks/useFormat.js rename to src/components/hooks/useFormat.js diff --git a/hooks/useLanguageNames.js b/src/components/hooks/useLanguageNames.js similarity index 100% rename from hooks/useLanguageNames.js rename to src/components/hooks/useLanguageNames.js diff --git a/hooks/useLocale.js b/src/components/hooks/useLocale.js similarity index 96% rename from hooks/useLocale.js rename to src/components/hooks/useLocale.js index 86ca99042..6353b0333 100644 --- a/hooks/useLocale.js +++ b/src/components/hooks/useLocale.js @@ -4,7 +4,7 @@ import { httpGet, setItem } from 'next-basics'; import { LOCALE_CONFIG } from 'lib/constants'; import { getDateLocale, getTextDirection } from 'lib/lang'; import useStore, { setLocale } from 'store/app'; -import useForceUpdate from 'hooks/useForceUpdate'; +import useForceUpdate from 'components/hooks/useForceUpdate'; import enUS from 'public/intl/messages/en-US.json'; const messages = { diff --git a/hooks/useMessages.js b/src/components/hooks/useMessages.js similarity index 100% rename from hooks/useMessages.js rename to src/components/hooks/useMessages.js diff --git a/hooks/usePageQuery.js b/src/components/hooks/usePageQuery.js similarity index 100% rename from hooks/usePageQuery.js rename to src/components/hooks/usePageQuery.js diff --git a/hooks/useReport.js b/src/components/hooks/useReport.js similarity index 100% rename from hooks/useReport.js rename to src/components/hooks/useReport.js diff --git a/hooks/useReports.js b/src/components/hooks/useReports.js similarity index 94% rename from hooks/useReports.js rename to src/components/hooks/useReports.js index 932fa6dc4..d9292aeb4 100644 --- a/hooks/useReports.js +++ b/src/components/hooks/useReports.js @@ -1,6 +1,6 @@ import { useState } from 'react'; import useApi from './useApi'; -import useApiFilter from 'hooks/useApiFilter'; +import useApiFilter from 'components/hooks/useApiFilter'; export function useReports() { const [modified, setModified] = useState(Date.now()); diff --git a/hooks/useRequireLogin.js b/src/components/hooks/useRequireLogin.js similarity index 84% rename from hooks/useRequireLogin.js rename to src/components/hooks/useRequireLogin.js index 3a95c9888..82a6d220a 100644 --- a/hooks/useRequireLogin.js +++ b/src/components/hooks/useRequireLogin.js @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useRouter } from 'next/router'; -import useApi from 'hooks/useApi'; -import useUser from 'hooks/useUser'; +import useApi from 'components/hooks/useApi'; +import useUser from 'components/hooks/useUser'; export function useRequireLogin() { const router = useRouter(); diff --git a/hooks/useShareToken.js b/src/components/hooks/useShareToken.js similarity index 100% rename from hooks/useShareToken.js rename to src/components/hooks/useShareToken.js diff --git a/hooks/useSticky.js b/src/components/hooks/useSticky.js similarity index 100% rename from hooks/useSticky.js rename to src/components/hooks/useSticky.js diff --git a/hooks/useTheme.js b/src/components/hooks/useTheme.js similarity index 100% rename from hooks/useTheme.js rename to src/components/hooks/useTheme.js diff --git a/hooks/useTimezone.js b/src/components/hooks/useTimezone.js similarity index 100% rename from hooks/useTimezone.js rename to src/components/hooks/useTimezone.js diff --git a/hooks/useUser.js b/src/components/hooks/useUser.js similarity index 100% rename from hooks/useUser.js rename to src/components/hooks/useUser.js diff --git a/hooks/useWebsite.js b/src/components/hooks/useWebsite.js similarity index 100% rename from hooks/useWebsite.js rename to src/components/hooks/useWebsite.js diff --git a/hooks/useWebsiteReports.js b/src/components/hooks/useWebsiteReports.js similarity index 94% rename from hooks/useWebsiteReports.js rename to src/components/hooks/useWebsiteReports.js index 3b7ec4155..c637bc76a 100644 --- a/hooks/useWebsiteReports.js +++ b/src/components/hooks/useWebsiteReports.js @@ -1,6 +1,6 @@ import { useState } from 'react'; import useApi from './useApi'; -import useApiFilter from 'hooks/useApiFilter'; +import useApiFilter from 'components/hooks/useApiFilter'; export function useWebsiteReports(websiteId) { const [modified, setModified] = useState(Date.now()); diff --git a/components/icons.ts b/src/components/icons.ts similarity index 98% rename from components/icons.ts rename to src/components/icons.ts index 01d7caf5a..8eb1f8b05 100644 --- a/components/icons.ts +++ b/src/components/icons.ts @@ -22,7 +22,7 @@ import User from 'assets/user.svg'; import Users from 'assets/users.svg'; import Visitor from 'assets/visitor.svg'; -const icons = { +const icons: any = { ...Icons, AddUser, Bars, diff --git a/components/input/DateFilter.js b/src/components/input/DateFilter.js similarity index 96% rename from components/input/DateFilter.js rename to src/components/input/DateFilter.js index af4b69dde..ffbcff690 100644 --- a/components/input/DateFilter.js +++ b/src/components/input/DateFilter.js @@ -2,10 +2,10 @@ import { useState } from 'react'; import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; import { endOfYear, isSameDay } from 'date-fns'; import DatePickerForm from 'components/metrics/DatePickerForm'; -import useLocale from 'hooks/useLocale'; +import useLocale from 'components/hooks/useLocale'; import { formatDate } from 'lib/date'; import Icons from 'components/icons'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function DateFilter({ value, diff --git a/components/input/LanguageButton.js b/src/components/input/LanguageButton.js similarity index 96% rename from components/input/LanguageButton.js rename to src/components/input/LanguageButton.js index d4c1cbc39..3c0d0cd6f 100644 --- a/components/input/LanguageButton.js +++ b/src/components/input/LanguageButton.js @@ -1,7 +1,7 @@ import { Icon, Button, PopupTrigger, Popup, Text } from 'react-basics'; import classNames from 'classnames'; import { languages } from 'lib/lang'; -import useLocale from 'hooks/useLocale'; +import useLocale from 'components/hooks/useLocale'; import Icons from 'components/icons'; import styles from './LanguageButton.module.css'; diff --git a/components/input/LanguageButton.module.css b/src/components/input/LanguageButton.module.css similarity index 100% rename from components/input/LanguageButton.module.css rename to src/components/input/LanguageButton.module.css diff --git a/components/input/LogoutButton.js b/src/components/input/LogoutButton.js similarity index 84% rename from components/input/LogoutButton.js rename to src/components/input/LogoutButton.js index 4a15cd68b..2b04a78a2 100644 --- a/components/input/LogoutButton.js +++ b/src/components/input/LogoutButton.js @@ -1,11 +1,11 @@ import { Button, Icon, Icons, TooltipPopup } from 'react-basics'; import Link from 'next/link'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function LogoutButton({ tooltipPosition = 'top' }) { const { formatMessage, labels } = useMessages(); return ( - + + + + + ); } diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/components/pages/websites/WebsiteMetricsBar.js index ad68a9fa3..c625e239a 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/components/pages/websites/WebsiteMetricsBar.js @@ -110,8 +110,8 @@ export function WebsiteMetricsBar({ websiteId, sticky }) {
- +
diff --git a/src/lib/date.js b/src/lib/date.js index 49bff8970..02f6053dc 100644 --- a/src/lib/date.js +++ b/src/lib/date.js @@ -29,9 +29,19 @@ import { max, min, isDate, + subWeeks, } from 'date-fns'; import { getDateLocale } from 'lib/lang'; +export const TIME_UNIT = { + minute: 'minute', + hour: 'hour', + day: 'day', + week: 'week', + month: 'month', + year: 'year', +}; + const dateFuncs = { minute: [differenceInMinutes, addMinutes, startOfMinute], hour: [differenceInHours, addHours, startOfHour], @@ -81,6 +91,7 @@ export function parseDateRange(value, locale = 'en-US') { if (!match) return null; const { num, unit } = match.groups; + const selectedUnit = { num, unit }; if (+num === 1) { switch (unit) { @@ -90,6 +101,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfDay(now), unit: 'hour', value, + selectedUnit, }; case 'week': return { @@ -97,6 +109,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfWeek(now, { locale: dateLocale }), unit: 'day', value, + selectedUnit, }; case 'month': return { @@ -104,6 +117,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfMonth(now), unit: 'day', value, + selectedUnit, }; case 'year': return { @@ -111,6 +125,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfYear(now), unit: 'month', value, + selectedUnit, }; } } @@ -123,6 +138,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: subDays(endOfDay(now), 1), unit: 'hour', value, + selectedUnit, }; case 'week': return { @@ -130,6 +146,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: subDays(endOfWeek(now, { locale: dateLocale }), 1), unit: 'day', value, + selectedUnit, }; case 'month': return { @@ -137,6 +154,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: subMonths(endOfMonth(now), 1), unit: 'day', value, + selectedUnit, }; case 'year': return { @@ -144,6 +162,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: subYears(endOfYear(now), 1), unit: 'month', value, + selectedUnit, }; } } @@ -155,6 +174,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfDay(now), unit, value, + selectedUnit, }; case 'hour': return { @@ -162,6 +182,46 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfHour(now), unit, value, + selectedUnit, + }; + } +} + +export function incrementDateRange(value, increment) { + const { startDate, endDate, selectedUnit } = value; + + const { num, unit } = selectedUnit; + + const sub = num * increment; + + switch (unit) { + case 'day': + return { + ...value, + startDate: subDays(startDate, sub), + endDate: subDays(endDate, sub), + value: 'range', + }; + case 'week': + return { + ...value, + startDate: subWeeks(startDate, sub), + endDate: subWeeks(endDate, sub), + value: 'range', + }; + case 'month': + return { + ...value, + startDate: subMonths(startDate, sub), + endDate: subMonths(endDate, sub), + value: 'range', + }; + case 'year': + return { + ...value, + startDate: subYears(startDate, sub), + endDate: subYears(endDate, sub), + value: 'range', }; } } @@ -237,7 +297,7 @@ export function getDateLength(startDate, endDate, unit) { return diff(endDate, startDate) + 1; } -export const customFormats = { +export const CUSTOM_FORMATS = { 'en-US': { p: 'ha', pp: 'h:mm:ss', @@ -252,7 +312,7 @@ export const customFormats = { export function formatDate(date, str, locale = 'en-US') { return format( typeof date === 'string' ? new Date(date) : date, - customFormats?.[locale]?.[str] || str, + CUSTOM_FORMATS?.[locale]?.[str] || str, { locale: getDateLocale(locale), }, diff --git a/src/lib/types.ts b/src/lib/types.ts index 3f3839a4d..e1e9da29f 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -12,9 +12,12 @@ import { WEBSITE_FILTER_TYPES, } from './constants'; import * as yup from 'yup'; +import { TIME_UNIT } from './date'; type ObjectValues = T[keyof T]; +export type TimeUnit = ObjectValues; + export type CollectionType = ObjectValues; export type Role = ObjectValues; export type EventType = ObjectValues; @@ -181,6 +184,8 @@ export interface DateRange { startDate: Date; endDate: Date; value: string; + unit?: TimeUnit; + selectedUnit?: TimeUnit; } export interface QueryFilters { diff --git a/tsconfig.json b/tsconfig.json index 37457b401..78b225f49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "baseUrl": "./src", "strictNullChecks": false, "noEmit": true, - "jsx": "preserve" + "jsx": "preserve", + "incremental": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] From 51014f6ce62e07fb8da45e8f1b862821894069f2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 21 Aug 2023 19:32:17 -0700 Subject: [PATCH 097/357] Fixed path alias. --- jsconfig.json | 5 ++++- src/components/hooks/useCountryNames.js | 2 +- src/components/hooks/useLanguageNames.js | 2 +- src/components/hooks/useLocale.js | 2 +- src/components/metrics/RegionsTable.js | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/jsconfig.json b/jsconfig.json index 738e8a465..f8124a205 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { - "baseUrl": "./src" + "baseUrl": "./src", + "paths": { + "public/*": ["./public/*"] + } } } diff --git a/src/components/hooks/useCountryNames.js b/src/components/hooks/useCountryNames.js index cd46ee442..51cabf34c 100644 --- a/src/components/hooks/useCountryNames.js +++ b/src/components/hooks/useCountryNames.js @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { httpGet } from 'next-basics'; -import enUS from '../../../public/intl/country/en-US.json'; +import enUS from 'public/intl/country/en-US.json'; const countryNames = { 'en-US': enUS, diff --git a/src/components/hooks/useLanguageNames.js b/src/components/hooks/useLanguageNames.js index b684667a8..afcb0ba65 100644 --- a/src/components/hooks/useLanguageNames.js +++ b/src/components/hooks/useLanguageNames.js @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { httpGet } from 'next-basics'; -import enUS from '../../../public/intl/country/en-US.json'; +import enUS from 'public/intl/country/en-US.json'; const languageNames = { 'en-US': enUS, diff --git a/src/components/hooks/useLocale.js b/src/components/hooks/useLocale.js index d84e1b4bb..1374af81f 100644 --- a/src/components/hooks/useLocale.js +++ b/src/components/hooks/useLocale.js @@ -5,7 +5,7 @@ import { LOCALE_CONFIG } from 'lib/constants'; import { getDateLocale, getTextDirection } from 'lib/lang'; import useStore, { setLocale } from 'store/app'; import useForceUpdate from 'components/hooks/useForceUpdate'; -import enUS from '../../../public/intl/country/en-US.json'; +import enUS from 'public/intl/country/en-US.json'; const messages = { 'en-US': enUS, diff --git a/src/components/metrics/RegionsTable.js b/src/components/metrics/RegionsTable.js index eee57a147..2e260e41d 100644 --- a/src/components/metrics/RegionsTable.js +++ b/src/components/metrics/RegionsTable.js @@ -5,7 +5,7 @@ import useLocale from 'components/hooks/useLocale'; import useMessages from 'components/hooks/useMessages'; import useCountryNames from 'components/hooks/useCountryNames'; import MetricsTable from './MetricsTable'; -import regions from '../../../public/iso-3166-2.json'; +import regions from 'public/iso-3166-2.json'; export function RegionsTable({ websiteId, ...props }) { const { locale } = useLocale(); From f62d23c9bc888da7a1f029efa62f2603dbe4418f Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 22 Aug 2023 03:36:49 -0700 Subject: [PATCH 098/357] Added alias for public folder. --- jsconfig.json | 5 +---- next.config.js | 3 +++ tsconfig.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jsconfig.json b/jsconfig.json index f8124a205..738e8a465 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,8 +1,5 @@ { "compilerOptions": { - "baseUrl": "./src", - "paths": { - "public/*": ["./public/*"] - } + "baseUrl": "./src" } } diff --git a/next.config.js b/next.config.js index 2165a6e01..4ab775107 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ require('dotenv').config(); +const path = require('path'); const pkg = require('./package.json'); const contentSecurityPolicy = ` @@ -92,6 +93,8 @@ const config = { use: ['@svgr/webpack'], }); + config.resolve.alias['public'] = path.resolve('./public'); + return config; }, async headers() { diff --git a/tsconfig.json b/tsconfig.json index 78b225f49..0faf5fbc2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "strictNullChecks": false, "noEmit": true, "jsx": "preserve", - "incremental": true + "incremental": false }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] From 84236c0cd9ef3599aab00bd3e8a6647e26c12827 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 22 Aug 2023 12:32:36 -0700 Subject: [PATCH 099/357] Fix date filter. --- src/components/input/WebsiteDateFilter.js | 29 +++++++++++++---------- src/lib/date.js | 7 ++++++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/components/input/WebsiteDateFilter.js b/src/components/input/WebsiteDateFilter.js index e56eba4f9..db8d141ab 100644 --- a/src/components/input/WebsiteDateFilter.js +++ b/src/components/input/WebsiteDateFilter.js @@ -9,7 +9,8 @@ export function WebsiteDateFilter({ websiteId }) { const [dateRange, setDateRange] = useDateRange(websiteId); const { value, startDate, endDate, selectedUnit } = dateRange; - const isFutureDate = isAfter(incrementDateRange(dateRange, -1).startDate, new Date()); + const isFutureDate = + value !== 'all' && isAfter(incrementDateRange(dateRange, -1).startDate, new Date()); const handleChange = async value => { setDateRange(value); @@ -32,19 +33,21 @@ export function WebsiteDateFilter({ websiteId }) { onChange={handleChange} showAllTime={true} /> - - + {value !== 'all' && ( + + - - + + + )} ); } diff --git a/src/lib/date.js b/src/lib/date.js index 02f6053dc..14f0e13ce 100644 --- a/src/lib/date.js +++ b/src/lib/date.js @@ -195,6 +195,13 @@ export function incrementDateRange(value, increment) { const sub = num * increment; switch (unit) { + case 'hour': + return { + ...value, + startDate: subHours(startDate, sub), + endDate: subHours(endDate, sub), + value: 'range', + }; case 'day': return { ...value, From 280f6a9113877d189e89c3ff45e0050b029808df Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 22 Aug 2023 15:37:22 -0700 Subject: [PATCH 100/357] Add grant to create website/team. --- src/lib/auth.ts | 23 ++++++++++++++++++++--- src/lib/middleware.ts | 10 ++++++++-- src/lib/types.ts | 3 +++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 10f7fbcac..a93f89c76 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -4,11 +4,12 @@ import debug from 'debug'; import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; import { createSecureToken, ensureArray, getRandomChars, parseToken } from 'next-basics'; -import { getTeamUser, getTeamWebsite, findTeamWebsiteByUserId } from 'queries'; +import { findTeamWebsiteByUserId, getTeamUser, getTeamWebsite, getWebsitesByUserId } from 'queries'; import { loadWebsite } from './load'; import { Auth } from './types'; const log = debug('umami:auth'); +const cloudMode = process.env.CLOUD_MODE; export async function setAuthKey(user, expire = 0) { const authKey = `auth:${getRandomChars(32)}`; @@ -57,7 +58,15 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri return !!(await findTeamWebsiteByUserId(websiteId, user.id)); } -export async function canCreateWebsite({ user }: Auth) { +export async function canCreateWebsite({ user, grant }: Auth) { + if (cloudMode) { + if (grant.find(a => a === PERMISSIONS.websiteCreate)) { + return true; + } + + return (await getWebsitesByUserId(user.id)).count < Number(process.env.WEBSITE_LIMIT); + } + if (user.isAdmin) { return true; } @@ -109,7 +118,15 @@ export async function canDeleteReport(auth: Auth, report: Report) { return canUpdateReport(auth, report); } -export async function canCreateTeam({ user }: Auth) { +export async function canCreateTeam({ user, grant }: Auth) { + if (cloudMode) { + if (grant.find(a => a === PERMISSIONS.teamCreate)) { + return true; + } + + return false; + } + if (user.isAdmin) { return true; } diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 0cb0cb880..18f6cc467 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -51,7 +51,7 @@ export const useAuth = createMiddleware(async (req, res, next) => { const shareToken = await parseShareToken(req); let user = null; - const { userId, authKey } = payload || {}; + const { userId, authKey, grant } = payload || {}; if (isUuid(userId)) { user = await getUserById(userId); @@ -72,7 +72,13 @@ export const useAuth = createMiddleware(async (req, res, next) => { user.isAdmin = user.role === ROLES.admin; } - (req as any).auth = { user, token, shareToken, authKey }; + (req as any).auth = { + user, + grant, + token, + shareToken, + authKey, + }; next(); }); diff --git a/src/lib/types.ts b/src/lib/types.ts index e1e9da29f..3685753e9 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -4,6 +4,7 @@ import { DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, + PERMISSIONS, REPORT_FILTER_TYPES, REPORT_TYPES, ROLES, @@ -17,6 +18,7 @@ import { TIME_UNIT } from './date'; type ObjectValues = T[keyof T]; export type TimeUnit = ObjectValues; +export type Permission = ObjectValues; export type CollectionType = ObjectValues; export type Role = ObjectValues; @@ -78,6 +80,7 @@ export interface Auth { role: string; isAdmin: boolean; }; + grant?: Permission[]; shareToken?: { websiteId: string; }; From c5345b01bb756c6055d529f0bc5e61db47a887a2 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 10:25:57 -0700 Subject: [PATCH 101/357] Update tsconfig. --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 0faf5fbc2..9d860a224 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,6 @@ "jsx": "preserve", "incremental": false }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], "exclude": ["node_modules"] } From f794b5674b3b7d6124515fb4f5f090add29f30a2 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 11:26:41 -0700 Subject: [PATCH 102/357] Update team/website permission check. --- src/lib/auth.ts | 8 ++++---- src/lib/{date.js => date.ts} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename src/lib/{date.js => date.ts} (100%) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index a93f89c76..4a42d85de 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -4,7 +4,7 @@ import debug from 'debug'; import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; import { createSecureToken, ensureArray, getRandomChars, parseToken } from 'next-basics'; -import { findTeamWebsiteByUserId, getTeamUser, getTeamWebsite, getWebsitesByUserId } from 'queries'; +import { findTeamWebsiteByUserId, getTeamUser, getTeamWebsite } from 'queries'; import { loadWebsite } from './load'; import { Auth } from './types'; @@ -60,11 +60,11 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri export async function canCreateWebsite({ user, grant }: Auth) { if (cloudMode) { - if (grant.find(a => a === PERMISSIONS.websiteCreate)) { + if (grant?.find(a => a === PERMISSIONS.websiteCreate)) { return true; } - return (await getWebsitesByUserId(user.id)).count < Number(process.env.WEBSITE_LIMIT); + return false; } if (user.isAdmin) { @@ -120,7 +120,7 @@ export async function canDeleteReport(auth: Auth, report: Report) { export async function canCreateTeam({ user, grant }: Auth) { if (cloudMode) { - if (grant.find(a => a === PERMISSIONS.teamCreate)) { + if (grant?.find(a => a === PERMISSIONS.teamCreate)) { return true; } diff --git a/src/lib/date.js b/src/lib/date.ts similarity index 100% rename from src/lib/date.js rename to src/lib/date.ts From d43ab3e5593572895c2d6c443291fd4073f1ec9e Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 11:55:45 -0700 Subject: [PATCH 103/357] Roll back session insert. --- src/lib/session.ts | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/lib/session.ts b/src/lib/session.ts index 5eb7398a9..85c173c52 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,12 +1,27 @@ -import { secret, uuid, isUuid } from 'lib/crypto'; +import { isUuid, secret, uuid } from 'lib/crypto'; import { getClientInfo, getJsonBody } from 'lib/detect'; import { parseToken } from 'next-basics'; import { CollectRequestBody, NextApiRequestCollect } from 'pages/api/send'; import { createSession } from 'queries'; import cache from './cache'; +import clickhouse from './clickhouse'; import { loadSession, loadWebsite } from './load'; -export async function findSession(req: NextApiRequestCollect) { +export async function findSession(req: NextApiRequestCollect): Promise<{ + id: any; + websiteId: string; + hostname: string; + browser: string; + os: any; + device: string; + screen: string; + language: string; + country: any; + subdivision1: any; + subdivision2: any; + city: any; + ownerId: string; +}> { const { payload } = getJsonBody(req); if (!payload) { @@ -53,6 +68,25 @@ export async function findSession(req: NextApiRequestCollect) { const sessionId = uuid(websiteId, hostname, ip, userAgent); + // Clickhouse does not require session lookup + if (clickhouse.enabled) { + return { + id: sessionId, + websiteId, + hostname, + browser, + os: os as any, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + ownerId: website.userId, + }; + } + // Find session let session = await loadSession(sessionId); From 06de67ec55821051480f5ca1ad3e612cb0cb7809 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 12:17:32 -0700 Subject: [PATCH 104/357] Add 1 day cache limit to user/website/session --- src/lib/cache.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/lib/cache.ts b/src/lib/cache.ts index bc46c23d1..c54eda2ee 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -2,17 +2,20 @@ import { User, Website } from '@prisma/client'; import redis from '@umami/redis-client'; import { getSession, getUserById, getWebsiteById } from '../queries'; -const { fetchObject, storeObject, deleteObject } = redis; +const { fetchObject, storeObject, deleteObject, expire } = redis; async function fetchWebsite(id): Promise { - return fetchObject(`website:${id}`, () => getWebsiteById(id)); + return fetchObject(`website:${id}`, () => getWebsiteById(id), 86400); } async function storeWebsite(data) { const { id } = data; const key = `website:${id}`; - return storeObject(key, data); + const obj = await storeObject(key, data); + await expire(key, 86400); + + return obj; } async function deleteWebsite(id) { @@ -20,14 +23,17 @@ async function deleteWebsite(id) { } async function fetchUser(id): Promise { - return fetchObject(`user:${id}`, () => getUserById(id, { includePassword: true })); + return fetchObject(`user:${id}`, () => getUserById(id, { includePassword: true }), 86400); } async function storeUser(data) { const { id } = data; const key = `user:${id}`; - return storeObject(key, data); + const obj = await storeObject(key, data); + await expire(key, 86400); + + return obj; } async function deleteUser(id) { @@ -35,14 +41,17 @@ async function deleteUser(id) { } async function fetchSession(id) { - return fetchObject(`session:${id}`, () => getSession(id)); + return fetchObject(`session:${id}`, () => getSession(id), 86400); } async function storeSession(data) { const { id } = data; const key = `session:${id}`; - return storeObject(key, data); + const obj = await storeObject(key, data); + await expire(key, 86400); + + return obj; } async function deleteSession(id) { From 89db57a38091f44c8e077c1f43b89deee3cb21b9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 23 Aug 2023 12:50:18 -0700 Subject: [PATCH 105/357] Updated component library build. --- package.components.json | 21 ++- package.json | 4 +- rollup.components.config.mjs | 7 +- ...{useRequireLogin.js => useRequireLogin.ts} | 6 +- .../hooks/{useUser.js => useUser.ts} | 0 src/components/input/WebsiteDateFilter.js | 8 +- .../pages/settings/teams/TeamWebsitesTable.js | 3 +- .../settings/websites/WebsiteSettings.js | 4 +- .../pages/settings/websites/WebsitesTable.js | 5 +- src/index.ts | 25 +++ src/lib/middleware.ts | 2 +- src/store/version.js | 2 +- src/store/websites.ts | 2 +- yarn.lock | 156 ++++++++++-------- 14 files changed, 146 insertions(+), 99 deletions(-) rename src/components/hooks/{useRequireLogin.js => useRequireLogin.ts} (71%) rename src/components/hooks/{useUser.js => useUser.ts} (100%) diff --git a/package.components.json b/package.components.json index 4596caa23..feb3fc2e6 100644 --- a/package.components.json +++ b/package.components.json @@ -1,10 +1,23 @@ { "name": "@umami/components", - "version": "0.1.0", + "version": "0.11.0", "description": "Umami React components.", "author": "Mike Cao ", "license": "MIT", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts" + "type": "module", + "main": "./index.js", + "types": "./index.d.ts", + "peerDependencies": { + "@tanstack/react-query": "^4.33.0", + "classnames": "^2.3.1", + "colord": "^2.9.2", + "immer": "^9.0.12", + "moment-timezone": "^0.5.35", + "next": "^13.4.0", + "next-basics": "^0.36.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-intl": "^5.24.7", + "zustand": "^4.3.8" + } } diff --git a/package.json b/package.json index 118c92697..1ff1730d1 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "dependencies": { "@fontsource/inter": "^4.5.15", "@prisma/client": "5.0.0", - "@tanstack/react-query": "^4.16.1", + "@tanstack/react-query": "^4.33.0", "@umami/prisma-client": "^0.2.0", "@umami/redis-client": "^0.5.0", "chalk": "^4.1.1", @@ -90,7 +90,7 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.3.1", + "next": "13.4.19", "next-basics": "^0.36.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", diff --git a/rollup.components.config.mjs b/rollup.components.config.mjs index 5c8722b4c..a0b8efbd6 100644 --- a/rollup.components.config.mjs +++ b/rollup.components.config.mjs @@ -44,11 +44,6 @@ const jsBundle = { output: [ { file: 'dist/index.js', - format: 'cjs', - sourcemap: true, - }, - { - file: 'dist/index.mjs', format: 'es', sourcemap: true, }, @@ -78,7 +73,7 @@ const jsBundle = { alias(aliasConfig), esbuild({ target: 'es6', - jsx: 'transform', + jsx: 'automatic', loaders: { '.js': 'jsx', }, diff --git a/src/components/hooks/useRequireLogin.js b/src/components/hooks/useRequireLogin.ts similarity index 71% rename from src/components/hooks/useRequireLogin.js rename to src/components/hooks/useRequireLogin.ts index 82a6d220a..950bb60ac 100644 --- a/src/components/hooks/useRequireLogin.js +++ b/src/components/hooks/useRequireLogin.ts @@ -3,7 +3,7 @@ import { useRouter } from 'next/router'; import useApi from 'components/hooks/useApi'; import useUser from 'components/hooks/useUser'; -export function useRequireLogin() { +export function useRequireLogin(handler: (data?: object) => void) { const router = useRouter(); const { get } = useApi(); const { user, setUser } = useUser(); @@ -11,9 +11,9 @@ export function useRequireLogin() { useEffect(() => { async function loadUser() { try { - const { user } = await get('/auth/verify'); + const data = await get('/auth/verify'); - setUser(user); + setUser(typeof handler === 'function' ? handler(data) : (data as any)?.user); } catch { await router.push('/login'); } diff --git a/src/components/hooks/useUser.js b/src/components/hooks/useUser.ts similarity index 100% rename from src/components/hooks/useUser.js rename to src/components/hooks/useUser.ts diff --git a/src/components/input/WebsiteDateFilter.js b/src/components/input/WebsiteDateFilter.js index db8d141ab..5ab19e605 100644 --- a/src/components/input/WebsiteDateFilter.js +++ b/src/components/input/WebsiteDateFilter.js @@ -12,14 +12,12 @@ export function WebsiteDateFilter({ websiteId }) { const isFutureDate = value !== 'all' && isAfter(incrementDateRange(dateRange, -1).startDate, new Date()); - const handleChange = async value => { + const handleChange = value => { setDateRange(value); }; - const handleIncrement = async value => { - const newValue = incrementDateRange(dateRange, value); - - setDateRange(newValue); + const handleIncrement = value => { + setDateRange(incrementDateRange(dateRange, value)); }; return ( diff --git a/src/components/pages/settings/teams/TeamWebsitesTable.js b/src/components/pages/settings/teams/TeamWebsitesTable.js index 848f82078..5ce08f355 100644 --- a/src/components/pages/settings/teams/TeamWebsitesTable.js +++ b/src/components/pages/settings/teams/TeamWebsitesTable.js @@ -4,7 +4,6 @@ import Link from 'next/link'; import { Button, Icon, Icons, Text } from 'react-basics'; import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton'; import SettingsTable from 'components/common/SettingsTable'; -import useConfig from 'components/hooks/useConfig'; export function TeamWebsitesTable({ data = [], @@ -13,9 +12,9 @@ export function TeamWebsitesTable({ onFilterChange, onPageChange, onPageSizeChange, + openExternal = false, }) { const { formatMessage, labels } = useMessages(); - const { openExternal } = useConfig(); const { user } = useUser(); const columns = [ diff --git a/src/components/pages/settings/websites/WebsiteSettings.js b/src/components/pages/settings/websites/WebsiteSettings.js index cdd0fe041..ac8cd87cc 100644 --- a/src/components/pages/settings/websites/WebsiteSettings.js +++ b/src/components/pages/settings/websites/WebsiteSettings.js @@ -10,12 +10,10 @@ import TrackingCode from 'components/pages/settings/websites/TrackingCode'; import ShareUrl from 'components/pages/settings/websites/ShareUrl'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; -import useConfig from 'components/hooks/useConfig'; -export function WebsiteSettings({ websiteId }) { +export function WebsiteSettings({ websiteId, openExternal = false }) { const router = useRouter(); const { formatMessage, labels, messages } = useMessages(); - const { openExternal } = useConfig(); const { get, useQuery } = useApi(); const { showToast } = useToasts(); const { data, isLoading } = useQuery( diff --git a/src/components/pages/settings/websites/WebsitesTable.js b/src/components/pages/settings/websites/WebsitesTable.js index d35da7575..7fa507168 100644 --- a/src/components/pages/settings/websites/WebsitesTable.js +++ b/src/components/pages/settings/websites/WebsitesTable.js @@ -3,7 +3,6 @@ import { Button, Text, Icon, Icons } from 'react-basics'; import SettingsTable from 'components/common/SettingsTable'; import Empty from 'components/common/Empty'; import useMessages from 'components/hooks/useMessages'; -import useConfig from 'components/hooks/useConfig'; import useUser from 'components/hooks/useUser'; export function WebsitesTable({ @@ -14,12 +13,12 @@ export function WebsitesTable({ onPageSizeChange, showTeam, showEditButton, + openExternal = false, }) { const { formatMessage, labels } = useMessages(); - const { openExternal } = useConfig(); const { user } = useUser(); - const showTable = data && (filterValue || data?.data.length !== 0); + const showTable = data && (filterValue || data?.data?.length !== 0); const teamColumns = [ { name: 'teamName', label: formatMessage(labels.teamName) }, diff --git a/src/index.ts b/src/index.ts index 6ca70afad..f2ef13cab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,6 +64,31 @@ export * from 'components/layout/SettingsLayout'; export * from 'components/layout/ShareLayout'; export * from 'components/layout/SideNav'; */ + +export * from 'components/hooks/useApi'; +export * from 'components/hooks/useConfig'; +export * from 'components/hooks/useCountryNames'; +export * from 'components/hooks/useDateRange'; +export * from 'components/hooks/useDocumentClick'; +export * from 'components/hooks/useEscapeKey'; +export * from 'components/hooks/useFilters'; +export * from 'components/hooks/useForceUpdate'; +export * from 'components/hooks/useFormat'; +export * from 'components/hooks/useLanguageNames'; +export * from 'components/hooks/useLocale'; +export * from 'components/hooks/useMessages'; +export * from 'components/hooks/usePageQuery'; +export * from 'components/hooks/useReport'; +export * from 'components/hooks/useReports'; +export * from 'components/hooks/useRequireLogin'; +export * from 'components/hooks/useShareToken'; +export * from 'components/hooks/useSticky'; +export * from 'components/hooks/useTheme'; +export * from 'components/hooks/useTimezone'; +export * from 'components/hooks/useUser'; +export * from 'components/hooks/useWebsite'; +export * from 'components/hooks/useWebsiteReports'; + export * from 'components/pages/settings/teams/TeamAddForm'; export * from 'components/pages/settings/teams/TeamAddWebsiteForm'; export * from 'components/pages/settings/teams/TeamDeleteForm'; diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 18f6cc467..0efb9762e 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -60,7 +60,7 @@ export const useAuth = createMiddleware(async (req, res, next) => { } if (process.env.NODE_ENV === 'development') { - log({ token, shareToken, payload, user }); + log({ token, shareToken, payload, user, grant }); } if (!user?.id && !shareToken) { diff --git a/src/store/version.js b/src/store/version.js index c232c7fa5..3b5afaac5 100644 --- a/src/store/version.js +++ b/src/store/version.js @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import produce from 'immer'; +import { produce } from 'immer'; import semver from 'semver'; import { CURRENT_VERSION, VERSION_CHECK, UPDATES_URL } from 'lib/constants'; import { getItem } from 'next-basics'; diff --git a/src/store/websites.ts b/src/store/websites.ts index 0d210af6d..5d0eeccd3 100644 --- a/src/store/websites.ts +++ b/src/store/websites.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import produce from 'immer'; +import { produce } from 'immer'; import { DateRange } from 'lib/types'; const store = create(() => ({})); diff --git a/yarn.lock b/yarn.lock index 5bec68b24..c20730f3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1754,10 +1754,10 @@ slash "^3.0.0" tiny-glob "^0.2.9" -"@next/env@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1.tgz#589707043065f6b71d411ed9b8f1ffd057c0fd4a" - integrity sha512-EDtCoedIZC7JlUQ3uaQpSc4aVmyhbLHmQVALg7pFfQgOTjgSnn7mKtA0DiCMkYvvsx6aFb5octGMtWrOtGXW9A== +"@next/env@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.19.tgz#46905b4e6f62da825b040343cbc233144e9578d3" + integrity sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ== "@next/eslint-plugin-next@12.3.4": version "12.3.4" @@ -1766,50 +1766,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.1.tgz#2c9719dd10a9cdf63bf50a7576b05dcf78999fe8" - integrity sha512-UXPtriEc/pBP8luSLSCZBcbzPeVv+SSjs9cH/KygTbhmACye8/OOXRZO13Z2Wq1G0gLmEAIHQAOuF+vafPd2lw== +"@next/swc-darwin-arm64@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz#77ad462b5ced4efdc26cb5a0053968d2c7dac1b6" + integrity sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ== -"@next/swc-darwin-x64@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.1.tgz#0be90342c89e53a390ccd9bece15f7f5cd480049" - integrity sha512-lT36yYxosCfLtplFzJWgo0hrPu6/do8+msgM7oQkPeohDNdhjtjFUgOOwdSnPublLR6Mo2Ym4P/wl5OANuD2bw== +"@next/swc-darwin-x64@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz#aebe38713a4ce536ee5f2a291673e14b715e633a" + integrity sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw== -"@next/swc-linux-arm64-gnu@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.1.tgz#a7353265839f8b8569a346a444dc3ab3770d297e" - integrity sha512-wRb76nLWJhonH8s3kxC/1tFguEkeOPayIwe9mkaz1G/yeS3OrjeyKMJsb4+Kdg0zbTo53bNCOl59NNtDM7yyyw== +"@next/swc-linux-arm64-gnu@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz#ec54db65b587939c7b94f9a84800f003a380f5a6" + integrity sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg== -"@next/swc-linux-arm64-musl@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.1.tgz#24552e6102c350e372f83f505a1d93c880551a50" - integrity sha512-qz3BzjJRZ16Iq/jrp+pjiYOc0jTjHlfmxQmZk9x/+5uhRP6/eWQSTAPVJ33BMo6oK5O5N4644OgTAbzXzorecg== +"@next/swc-linux-arm64-musl@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz#1f5e2c1ea6941e7d530d9f185d5d64be04279d86" + integrity sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA== -"@next/swc-linux-x64-gnu@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.1.tgz#5f335a683b6eafa52307b12af97782993b6c45ff" - integrity sha512-6mgkLmwlyWlomQmpl21I3hxgqE5INoW4owTlcLpNsd1V4wP+J46BlI/5zV5KWWbzjfncIqzXoeGs5Eg+1GHODA== +"@next/swc-linux-x64-gnu@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz#96b0882492a2f7ffcce747846d3680730f69f4d1" + integrity sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g== -"@next/swc-linux-x64-musl@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.1.tgz#58e5aad6f97203a0788783f66324456c8f9cdb50" - integrity sha512-uqm5sielhQmKJM+qayIhgZv1KlS5pqTdQ99b+Z7hMWryXS96qE0DftTmMZowBcUL6x7s2vSXyH5wPtO1ON7LBg== +"@next/swc-linux-x64-musl@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz#f276b618afa321d2f7b17c81fc83f429fb0fd9d8" + integrity sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q== -"@next/swc-win32-arm64-msvc@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.1.tgz#f8ed1badab57ed4503969758754e6fb0cf326753" - integrity sha512-WomIiTj/v3LevltlibNQKmvrOymNRYL+a0dp5R73IwPWN5FvXWwSELN/kiNALig/+T3luc4qHNTyvMCp9L6U5Q== +"@next/swc-win32-arm64-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz#1599ae0d401da5ffca0947823dac577697cce577" + integrity sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw== -"@next/swc-win32-ia32-msvc@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.1.tgz#7f599c8975b09ee5527cc49b9e5a4d13be50635a" - integrity sha512-M+PoH+0+q658wRUbs285RIaSTYnGBSTdweH/0CdzDgA6Q4rBM0sQs4DHmO3BPP0ltCO/vViIoyG7ks66XmCA5g== +"@next/swc-win32-ia32-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz#55cdd7da90818f03e4da16d976f0cb22045d16fd" + integrity sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA== -"@next/swc-win32-x64-msvc@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.1.tgz#192d43ab44ebb98bd4f5865d0e1d7ce62703182f" - integrity sha512-Sl1F4Vp5Z1rNXWZYqJwMuWRRol4bqOB6+/d7KqkgQ4AcafKPN1PZmpkCoxv4UFHtFNIB7EotnuIhtXu3zScicQ== +"@next/swc-win32-x64-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz#648f79c4e09279212ac90d871646ae12d80cdfce" + integrity sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -2244,24 +2244,24 @@ "@svgr/plugin-jsx" "^6.5.1" "@svgr/plugin-svgo" "^6.5.1" -"@swc/helpers@0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.0.tgz#bf1d807b60f7290d0ec763feea7ccdeda06e85f1" - integrity sha512-SjY/p4MmECVVEWspzSRpQEM3sjR17sP8PbGxELWrT+YZMBfiUyt1MRUNjMV23zohwlG2HYtCQOsCwsTHguXkyg== +"@swc/helpers@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" + integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== dependencies: tslib "^2.4.0" -"@tanstack/query-core@4.32.0": - version "4.32.0" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.32.0.tgz#e0f4a830283612430450c13badd353766423f523" - integrity sha512-ei4IYwL2kmlKSlCw9WgvV7PpXi0MiswVwfQRxawhJA690zWO3dU49igaQ/UMTl+Jy9jj9dK5IKAYvbX7kUvviQ== +"@tanstack/query-core@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.33.0.tgz#7756da9a75a424e521622b1d84eb55b7a2b33715" + integrity sha512-qYu73ptvnzRh6se2nyBIDHGBQvPY1XXl3yR769B7B6mIDD7s+EZhdlWHQ67JI6UOTFRaI7wupnTnwJ3gE0Mr/g== -"@tanstack/react-query@^4.16.1": - version "4.32.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.32.0.tgz#701b45b149cfd4b54a68705f9100973db3ba5d5d" - integrity sha512-B8WUMcByYAH9500ENejDCATOmEZhqjtS9wsfiQ3BNa+s+yAynY8SESI8WWHhSqUmjd0pmCSFRP6BOUGSda3QXA== +"@tanstack/react-query@^4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.33.0.tgz#e927b0343a6ecaa948fee59e9ca98fe561062638" + integrity sha512-97nGbmDK0/m0B86BdiXzx3EW9RcDYKpnyL2+WwyuLHEgpfThYAnXFaMMmnTDuAO4bQJXEhflumIEUfKmP7ESGA== dependencies: - "@tanstack/query-core" "4.32.0" + "@tanstack/query-core" "4.33.0" use-sync-external-store "^1.2.0" "@trysound/sax@0.2.0": @@ -4982,6 +4982,11 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@7.1.7: version "7.1.7" resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" @@ -6450,27 +6455,29 @@ next-basics@^0.36.0: jsonwebtoken "^9.0.0" pure-rand "^6.0.2" -next@13.3.1: - version "13.3.1" - resolved "https://registry.yarnpkg.com/next/-/next-13.3.1.tgz#17625f7423db2e059d71b41bd9031756cf2b33bc" - integrity sha512-eByWRxPzKHs2oQz1yE41LX35umhz86ZSZ+mYyXBqn2IBi2hyUqxBA88avywdr4uyH+hCJczegGsDGWbzQA5Rqw== +next@13.4.19: + version "13.4.19" + resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f" + integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw== dependencies: - "@next/env" "13.3.1" - "@swc/helpers" "0.5.0" + "@next/env" "13.4.19" + "@swc/helpers" "0.5.1" busboy "1.6.0" caniuse-lite "^1.0.30001406" postcss "8.4.14" styled-jsx "5.1.1" + watchpack "2.4.0" + zod "3.21.4" optionalDependencies: - "@next/swc-darwin-arm64" "13.3.1" - "@next/swc-darwin-x64" "13.3.1" - "@next/swc-linux-arm64-gnu" "13.3.1" - "@next/swc-linux-arm64-musl" "13.3.1" - "@next/swc-linux-x64-gnu" "13.3.1" - "@next/swc-linux-x64-musl" "13.3.1" - "@next/swc-win32-arm64-msvc" "13.3.1" - "@next/swc-win32-ia32-msvc" "13.3.1" - "@next/swc-win32-x64-msvc" "13.3.1" + "@next/swc-darwin-arm64" "13.4.19" + "@next/swc-darwin-x64" "13.4.19" + "@next/swc-linux-arm64-gnu" "13.4.19" + "@next/swc-linux-arm64-musl" "13.4.19" + "@next/swc-linux-x64-gnu" "13.4.19" + "@next/swc-linux-x64-musl" "13.4.19" + "@next/swc-win32-arm64-msvc" "13.4.19" + "@next/swc-win32-ia32-msvc" "13.4.19" + "@next/swc-win32-x64-msvc" "13.4.19" nice-try@^1.0.4: version "1.0.5" @@ -9340,6 +9347,14 @@ vue@^3.2.23: "@vue/server-renderer" "3.2.36" "@vue/shared" "3.2.36" +watchpack@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + web-streams-polyfill@^3.0.3: version "3.2.1" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" @@ -9506,6 +9521,11 @@ yup@^0.32.11: property-expr "^2.0.4" toposort "^2.0.2" +zod@3.21.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + zustand@^4.3.8: version "4.3.9" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.9.tgz#a7d4332bbd75dfd25c6848180b3df1407217f2ad" From 10f92d0178111ae13c3022bd38bec427e74551e0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 23 Aug 2023 13:10:12 -0700 Subject: [PATCH 106/357] Fixed tsconfig. --- tsconfig.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 9d860a224..08e376bd2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,11 +17,20 @@ "forceConsistentCasingInFileNames": true, "allowJs": true, "strict": true, - "baseUrl": "./src", "strictNullChecks": false, "noEmit": true, "jsx": "preserve", - "incremental": false + "incremental": false, + "baseUrl": ".", + "paths": { + "assets/*": ["./src/assets/*"], + "components/*": ["./src/components/*"], + "lib/*": ["./src/lib/*"], + "pages/*": ["./src/pages/*"], + "queries/*": ["./src/queries/*"], + "store/*": ["./src/store/*"], + "styles/*": ["./src/styles/*"] + } }, "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], "exclude": ["node_modules"] From d4be41a1217f61723d2e5fdff28d56c0a43b2536 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 14:39:38 -0700 Subject: [PATCH 107/357] Fixed tsconfig. --- tsconfig.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 08e376bd2..71094dd79 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,15 +21,15 @@ "noEmit": true, "jsx": "preserve", "incremental": false, - "baseUrl": ".", + "baseUrl": "./src", "paths": { - "assets/*": ["./src/assets/*"], - "components/*": ["./src/components/*"], - "lib/*": ["./src/lib/*"], - "pages/*": ["./src/pages/*"], - "queries/*": ["./src/queries/*"], - "store/*": ["./src/store/*"], - "styles/*": ["./src/styles/*"] + "assets/*": ["./assets/*"], + "components/*": ["./components/*"], + "lib/*": ["./lib/*"], + "pages/*": ["./pages/*"], + "queries/*": ["./queries/*"], + "store/*": ["./store/*"], + "styles/*": ["./styles/*"] } }, "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], From 9180a7008b039bddff899da1acb721b8704f02c8 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 16:24:14 -0700 Subject: [PATCH 108/357] Fix insights validation. --- src/pages/api/reports/insights.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pages/api/reports/insights.ts b/src/pages/api/reports/insights.ts index 04e51d4ca..d10eba3fb 100644 --- a/src/pages/api/reports/insights.ts +++ b/src/pages/api/reports/insights.ts @@ -12,8 +12,8 @@ export interface InsightsRequestBody { startDate: string; endDate: string; }; - fields: { name: string; type: string; value: string }[]; - filters: string[]; + fields: { name: string; type: string; label: string }[]; + filters: { name: string; type: string; filter: string; value: string }[]; groups: { name: string; type: string }[]; } @@ -33,12 +33,23 @@ const schema = { yup.object().shape({ name: yup.string().required(), type: yup.string().required(), + label: yup.string().required(), + }), + ) + .min(1) + .required(), + filters: yup + .array() + .of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + filter: yup.string().required(), value: yup.string().required(), }), ) .min(1) .required(), - filters: yup.array().of(yup.string()).min(1).required(), groups: yup.array().of( yup.object().shape({ name: yup.string().required(), From bf8c891c414095d68cd41bccf5c527493e812c57 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 23 Aug 2023 21:29:16 -0700 Subject: [PATCH 109/357] Fixed mobile menu being hidden. --- src/components/common/MobileMenu.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/common/MobileMenu.js b/src/components/common/MobileMenu.js index 2228a3e95..de1e9ffa8 100644 --- a/src/components/common/MobileMenu.js +++ b/src/components/common/MobileMenu.js @@ -1,3 +1,4 @@ +import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { useRouter } from 'next/router'; import Link from 'next/link'; @@ -28,10 +29,11 @@ export function MobileMenu({ items = [], onClose }) {
); - return ( + return createPortal(
-
+ , + document.body, ); } From e0c00dece8f86e83defe683c5ef87760a5335150 Mon Sep 17 00:00:00 2001 From: Maekawa Minoru <126235344+err931@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:00:00 +0900 Subject: [PATCH 110/357] Update ja-JP.json --- src/lang/ja-JP.json | 316 ++++++++++++++++++++++---------------------- 1 file changed, 158 insertions(+), 158 deletions(-) diff --git a/src/lang/ja-JP.json b/src/lang/ja-JP.json index 0f4d54504..770f6f076 100644 --- a/src/lang/ja-JP.json +++ b/src/lang/ja-JP.json @@ -1,211 +1,211 @@ { - "label.access-code": "Access code", + "label.access-code": "アクセスコード", "label.actions": "アクション", - "label.activity-log": "Activity log", - "label.add": "Add", - "label.add-description": "Add description", + "label.activity-log": "アクティビティログ", + "label.add": "追加", + "label.add-description": "説明を追加", "label.add-website": "Webサイトの追加", "label.admin": "管理者", - "label.after": "After", - "label.all": "すべて表示", - "label.all-time": "All time", - "label.analytics": "Analytics", - "label.average": "Average", + "label.after": "直後", + "label.all": "すべて", + "label.all-time": "すべての時間帯", + "label.analytics": "アナリティクス", + "label.average": "平均", "label.average-visit-time": "平均滞在時間", "label.back": "戻る", - "label.before": "Before", + "label.before": "直前", "label.bounce-rate": "直帰率", - "label.breakdown": "Breakdown", - "label.browser": "Browser", + "label.breakdown": "故障", + "label.browser": "ブラウザ", "label.browsers": "ブラウザ", "label.cancel": "キャンセル", - "label.change-password": "パスワード変更", - "label.cities": "Cities", - "label.city": "City", - "label.clear-all": "Clear all", - "label.confirm": "Confirm", - "label.confirm-password": "パスワード(確認)", - "label.contains": "Contains", - "label.continue": "Continue", - "label.countries": "国", - "label.country": "Country", - "label.create-report": "Create report", - "label.create-team": "Create team", - "label.create-user": "Create user", - "label.created": "Created", + "label.change-password": "パスワードの変更", + "label.cities": "都市", + "label.city": "都市", + "label.clear-all": "すべてクリア", + "label.confirm": "確認", + "label.confirm-password": "パスワード(確認)", + "label.contains": "コンテンツ", + "label.continue": "続ける", + "label.countries": "国名", + "label.country": "国", + "label.create-report": "レポートの作成", + "label.create-team": "チームの作成", + "label.create-user": "ユーザーの作成", + "label.created": "作成されました", "label.current-password": "現在のパスワード", - "label.custom-range": "期間を指定する", + "label.custom-range": "範囲指定", "label.dashboard": "ダッシュボード", - "label.data": "Data", - "label.date": "Date", - "label.date-range": "範囲指定", - "label.day": "Day", - "label.default-date-range": "最初に表示する期間", + "label.data": "データ", + "label.date": "日付", + "label.date-range": "期間", + "label.day": "日", + "label.default-date-range": "デフォルトの期間", "label.delete": "削除", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-team": "チームの削除", + "label.delete-user": "ユーザーの削除", "label.delete-website": "Webサイトの削除", - "label.description": "Description", + "label.description": "説明", "label.desktop": "デスクトップ", - "label.details": "Details", - "label.device": "Device", + "label.details": "詳細情報", + "label.device": "デバイス", "label.devices": "デバイス", - "label.dismiss": "無視する", - "label.does-not-contain": "Does not contain", + "label.dismiss": "却下", + "label.does-not-contain": "を含まない", "label.domain": "ドメイン", - "label.dropoff": "Dropoff", + "label.dropoff": "切り捨て", "label.edit": "編集", - "label.edit-dashboard": "Edit dashboard", - "label.enable-share-url": "共有リンクを有効にする", - "label.event": "Event", - "label.event-data": "Event data", + "label.edit-dashboard": "ダッシュボードの編集", + "label.enable-share-url": "共有URLを有効にする", + "label.event": "イベント", + "label.event-data": "イベントデータ", "label.events": "イベント", - "label.false": "False", - "label.field": "Field", - "label.fields": "Fields", - "label.filter-combined": "パスまで", - "label.filter-raw": "すべて表示", - "label.filters": "Filters", - "label.funnel": "Funnel", - "label.greater-than": "Greater than", - "label.greater-than-equals": "Greater than or equals", - "label.insights": "Insights", - "label.is": "Is", - "label.is-not": "Is not", - "label.is-not-set": "Is not set", - "label.is-set": "Is set", - "label.join": "Join", - "label.join-team": "Join team", - "label.language": "Language", - "label.languages": "Languages", + "label.false": "偽", + "label.field": "フィールド", + "label.fields": "フィールド", + "label.filter-combined": "統合", + "label.filter-raw": "RAW", + "label.filters": "フィルター", + "label.funnel": "分析", + "label.greater-than": "超過", + "label.greater-than-equals": "以上", + "label.insights": "見通し", + "label.is": "に等しい", + "label.is-not": "に等しくない", + "label.is-not-set": "未設定", + "label.is-set": "設定済み", + "label.join": "参加", + "label.join-team": "チームに参加", + "label.language": "言語", + "label.languages": "言語", "label.laptop": "ノートPC", "label.last-days": "過去{x}日間", "label.last-hours": "過去{x}時間", - "label.leave": "Leave", - "label.leave-team": "Leave team", - "label.less-than": "Less than", - "label.less-than-equals": "Less than or equals", + "label.leave": "離脱", + "label.leave-team": "チームを離脱", + "label.less-than": "未満", + "label.less-than-equals": "以下", "label.login": "ログイン", "label.logout": "ログアウト", - "label.max": "Max", - "label.members": "Members", - "label.min": "Min", + "label.max": "最大", + "label.members": "メンバー", + "label.min": "最小", "label.mobile": "携帯電話", - "label.more": "さらに表示", - "label.my-websites": "My websites", + "label.more": "もっと見る", + "label.my-websites": "マイWebサイト", "label.name": "名前", "label.new-password": "新しいパスワード", - "label.none": "None", + "label.none": "なし", "label.os": "OS", - "label.overview": "Overview", - "label.owner": "Owner", - "label.page-of": "Page {current} of {total}", + "label.overview": "概要", + "label.owner": "所有者", + "label.page-of": "ページ {current}/{total}", "label.page-views": "閲覧数", - "label.pageTitle": "Page title", + "label.pageTitle": "ページタイトル", "label.pages": "ページ", "label.password": "パスワード", - "label.powered-by": "このシステムは {name} で実行されています。", - "label.profile": "プロファイル", - "label.queries": "Queries", - "label.query": "Query", - "label.query-parameters": "Query parameters", + "label.powered-by": "Powered by {name}", + "label.profile": "プロフィール", + "label.queries": "クエリ", + "label.query": "クエリ", + "label.query-parameters": "クエリパラメーター", "label.realtime": "リアルタイム", - "label.referrer": "Referrer", + "label.referrer": "リファラー", "label.referrers": "リファラー", "label.refresh": "更新", - "label.regenerate": "Regenerate", - "label.region": "Region", - "label.regions": "Regions", - "label.remove": "Remove", - "label.reports": "Reports", + "label.regenerate": "再生成", + "label.region": "地域", + "label.regions": "地域", + "label.remove": "削除", + "label.reports": "レポート", "label.required": "必須", "label.reset": "リセット", - "label.reset-website": "Reset statistics", - "label.retention": "Retention", - "label.role": "Role", - "label.run-query": "Run query", + "label.reset-website": "Webサイトをリセットする", + "label.retention": "保持", + "label.role": "ロール", + "label.run-query": "クエリ実行", "label.save": "保存", - "label.screens": "Screens", - "label.select-date": "Select date", - "label.select-website": "Select website", - "label.sessions": "Sessions", + "label.screens": "画面サイズ", + "label.select-date": "日付を選択", + "label.select-website": "Webサイトを選択", + "label.sessions": "セッション", "label.settings": "設定", - "label.share-url": "共有リンク", - "label.single-day": "一日のみ", - "label.sum": "Sum", + "label.share-url": "共有URL", + "label.single-day": "一日", + "label.sum": "合計", "label.tablet": "タブレット", - "label.team": "Team", - "label.team-guest": "Team guest", - "label.team-id": "Team ID", - "label.team-member": "Team member", - "label.team-name": "Team name", - "label.team-owner": "Team owner", - "label.team-websites": "Team websites", - "label.teams": "Teams", - "label.theme": "Theme", + "label.team": "チーム", + "label.team-guest": "チームゲスト", + "label.team-id": "チームID", + "label.team-member": "チームメンバー", + "label.team-name": "チーム名", + "label.team-owner": "チーム所有者", + "label.team-websites": "チームのWebサイト", + "label.teams": "チーム", + "label.theme": "テーマ", "label.this-month": "今月", "label.this-week": "今週", "label.this-year": "今年", "label.timezone": "タイムゾーン", - "label.title": "Title", + "label.title": "タイトル", "label.today": "今日", - "label.toggle-charts": "Toggle charts", - "label.total": "Total", - "label.total-records": "Total records", + "label.toggle-charts": "グラフを切り替える", + "label.total": "累計", + "label.total-records": "総記録数", "label.tracking-code": "トラッキングコード", - "label.true": "True", - "label.type": "Type", - "label.unique": "Unique", + "label.true": "真", + "label.type": "種別", + "label.unique": "ユニーク", "label.unique-visitors": "ユニーク訪問者数", "label.unknown": "不明", - "label.untitled": "Untitled", + "label.untitled": "無題", "label.url": "URL", - "label.urls": "URLs", - "label.user": "User", + "label.urls": "URL", + "label.user": "ユーザー", "label.username": "ユーザー名", - "label.users": "Users", - "label.value": "Value", - "label.view": "View", - "label.view-details": "詳細を見る", - "label.view-only": "View only", - "label.views": "閲覧数", - "label.visitors": "訪問者数", - "label.website": "Website", - "label.website-id": "Website ID", + "label.users": "ユーザー", + "label.value": "値", + "label.view": "表示", + "label.view-details": "詳細を表示", + "label.view-only": "表示のみ", + "label.views": "表示", + "label.visitors": "訪問者", + "label.website": "Webサイト", + "label.website-id": "WebサイトID", "label.websites": "Webサイト", - "label.window": "Window", - "label.yesterday": "Yesterday", - "message.active-users": "{x}人が閲覧中です。", + "label.window": "ウィンドウ", + "label.yesterday": "昨日", + "message.active-users": "{x} {x, plural, one {アクティブな訪問者} other {アクティブな訪問者}}", "message.confirm-delete": "{target}を削除してもよろしいですか?", - "message.confirm-leave": "Are you sure you want to leave {target}?", - "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", - "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", - "message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", - "message.delete-website-warning": "関連するすべてのデータも削除されます。", - "message.error": "問題が発生しました。", - "message.event-log": "{event} on {url}", - "message.go-to-settings": "設定する", - "message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。", - "message.invalid-domain": "無効なドメイン", - "message.min-password-length": "Minimum length of {n} characters", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.confirm-leave": "{target}から離脱してもよろしいですか?", + "message.confirm-reset": "{target}をリセットしてもよろしいですか?", + "message.delete-account": "このアカウントを削除するには、下のフォームに「{confirmation}」と入力してください。", + "message.delete-website": "このWebサイトを削除するには、下のフォームに「{confirmation}」と入力してください。", + "message.delete-website-warning": "Webサイトのデータがすべて削除されます。", + "message.error": "未知のエラーが発生しました。", + "message.event-log": "{url}の{event}", + "message.go-to-settings": "設定に移動する", + "message.incorrect-username-password": "ユーザー名またはパスワードが間違っています。", + "message.invalid-domain": "無効なドメインです。http/httpsを含めないでください。", + "message.min-password-length": "最小文字数は{n}文字です", + "message.new-version-available": "Umamiの新しいバージョン{version}が利用可能です!", "message.no-data-available": "データがありません。", - "message.no-event-data": "No event data is available.", - "message.no-match-password": "パスワードが一致しません", - "message.no-results-found": "No results were found.", - "message.no-team-websites": "This team does not have any websites.", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", + "message.no-event-data": "イベントデータがありません。", + "message.no-match-password": "パスワードが一致しません。", + "message.no-results-found": "結果が見つかりません。", + "message.no-team-websites": "このチームにはWebサイトがありません。", + "message.no-teams": "チームを作成していません。", + "message.no-users": "ユーザーが存在しません。", "message.no-websites-configured": "Webサイトが設定されていません。", - "message.page-not-found": "ページが見つかりません。", - "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", - "message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", - "message.saved": "正常に保存されました。", - "message.share-url": "これは{target}の共有リンクです。", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", - "message.team-websites-info": "Websites can be viewed by anyone on the team.", - "message.tracking-code": "トラッキングコード", - "message.user-deleted": "User deleted.", - "message.visitor-log": "{os}({device})で{browser}を使用している{country}からの訪問者" + "message.page-not-found": "ページが見つかりません", + "message.reset-website": "このWebサイトをリセットするには、下のフォームに「{confirmation}」と入力してください。", + "message.reset-website-warning": "このWebサイトの統計情報はすべて削除されますが、設定はそのまま残ります。", + "message.saved": "保存されました。", + "message.share-url": "あなたのWebサイトの統計情報は次のURLで公開されています:", + "message.team-already-member": "あなたはすでにチームのメンバーです。", + "message.team-not-found": "チームが見つかりません。", + "message.team-websites-info": "Webサイトはチーム内の誰でも見ることができます。", + "message.tracking-code": "このWebサイトの統計情報を追跡するには、HTMLの...セクションに以下のコードを記述します。", + "message.user-deleted": "ユーザーが削除されました。", + "message.visitor-log": "{os}({device})で{browser}を使用している{country}からの訪問者" } From 2683ff278a59813cff7f4ff6f022c48b044699fe Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 24 Aug 2023 12:55:15 -0700 Subject: [PATCH 111/357] Fix event-data calls. --- src/pages/api/event-data/events.ts | 4 ++-- src/pages/api/event-data/stats.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/api/event-data/events.ts b/src/pages/api/event-data/events.ts index da0afc65d..1d1d37877 100644 --- a/src/pages/api/event-data/events.ts +++ b/src/pages/api/event-data/events.ts @@ -10,7 +10,7 @@ export interface EventDataFieldsRequestQuery { websiteId: string; startAt: string; endAt: string; - event: string; + event?: string; } const schema = { @@ -18,7 +18,7 @@ const schema = { websiteId: yup.string().uuid().required(), startAt: yup.number().integer().required(), endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - event: yup.string().required(), + event: yup.string(), }), }; diff --git a/src/pages/api/event-data/stats.ts b/src/pages/api/event-data/stats.ts index b7b70dbfe..7f694bc62 100644 --- a/src/pages/api/event-data/stats.ts +++ b/src/pages/api/event-data/stats.ts @@ -3,6 +3,7 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getEventDataStats } from 'queries/index'; import * as yup from 'yup'; export interface EventDataStatsRequestQuery { From 7107336b491d3634376dcbd26ae77e4a6825268a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 24 Aug 2023 16:17:46 -0700 Subject: [PATCH 112/357] Added loading to event data. --- src/components/pages/websites/WebsiteEventData.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/pages/websites/WebsiteEventData.js b/src/components/pages/websites/WebsiteEventData.js index ea8e473b0..d38ca1adb 100644 --- a/src/components/pages/websites/WebsiteEventData.js +++ b/src/components/pages/websites/WebsiteEventData.js @@ -1,4 +1,4 @@ -import { Flexbox } from 'react-basics'; +import { Flexbox, Loading } from 'react-basics'; import EventDataTable from 'components/pages/event-data/EventDataTable'; import EventDataValueTable from 'components/pages/event-data/EventDataValueTable'; import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetricsBar'; @@ -28,13 +28,14 @@ export default function WebsiteEventData({ websiteId }) { const { query: { event }, } = usePageQuery(); - const { data } = useData(websiteId, event); + const { data, isLoading } = useData(websiteId, event); return ( {!event && } - {event && } + {isLoading && } + {event && data && } ); } From fb78202139fe3d62ead5388d32128c7ca5e496a3 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 10:45:59 -0700 Subject: [PATCH 113/357] Remove mandatory validation. --- src/lib/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 0efb9762e..edf3e9292 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -87,7 +87,7 @@ export const useValidate = createMiddleware(async (req: any, res, next) => { try { const { yup } = req as NextApiRequestQueryBody; - yup[req.method].validateSync({ ...req.query, ...req.body }); + yup[req.method] && yup[req.method].validateSync({ ...req.query, ...req.body }); } catch (e: any) { return badRequest(res, e.message); } From 1a47f594c2934ba28f6888d088f0b205f9904bf7 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 11:03:58 -0700 Subject: [PATCH 114/357] Add dashboard filter. --- src/components/messages.js | 1 + .../pages/reports/FieldFilterForm.js | 36 +++++++---- .../pages/reports/FilterSelectForm.js | 3 +- .../pages/websites/WebsiteMetricsBar.js | 61 +++++++++++++++++-- src/pages/api/websites/[id]/index.ts | 1 + 5 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/components/messages.js b/src/components/messages.js index ff6199450..f52ed5c5e 100644 --- a/src/components/messages.js +++ b/src/components/messages.js @@ -140,6 +140,7 @@ export const labels = defineMessages({ description: { id: 'label.description', defaultMessage: 'Description' }, untitled: { id: 'label.untitled', defaultMessage: 'Untitled' }, type: { id: 'label.type', defaultMessage: 'Type' }, + filter: { id: 'label.filter', defaultMessage: 'Filter' }, filters: { id: 'label.filters', defaultMessage: 'Filters' }, breakdown: { id: 'label.breakdown', defaultMessage: 'Breakdown' }, true: { id: 'label.true', defaultMessage: 'True' }, diff --git a/src/components/pages/reports/FieldFilterForm.js b/src/components/pages/reports/FieldFilterForm.js index 96e96697e..01efed3f4 100644 --- a/src/components/pages/reports/FieldFilterForm.js +++ b/src/components/pages/reports/FieldFilterForm.js @@ -3,7 +3,14 @@ import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics'; import { useMessages, useFilters, useFormat } from 'components/hooks'; import styles from './FieldFilterForm.module.css'; -export default function FieldFilterForm({ name, label, type, values, onSelect }) { +export default function FieldFilterForm({ + name, + label, + type, + values, + onSelect, + includeOnlyEquals, +}) { const { formatMessage, labels } = useMessages(); const [filter, setFilter] = useState('eq'); const [value, setValue] = useState(); @@ -27,17 +34,19 @@ export default function FieldFilterForm({ name, label, type, values, onSelect }) - - {({ value, label }) => { - return {label}; - }} - + {!includeOnlyEquals && ( + + {({ value, label }) => { + return {label}; + }} + + )} {value => { return {formatValue(value, name)}; diff --git a/src/components/pages/reports/FilterSelectForm.js b/src/components/pages/reports/FilterSelectForm.js index 274a00eae..8e02930ed 100644 --- a/src/components/pages/reports/FilterSelectForm.js +++ b/src/components/pages/reports/FilterSelectForm.js @@ -18,7 +18,7 @@ function useValues(websiteId, type) { return { data, error, isLoading }; } -export default function FilterSelectForm({ websiteId, items, onSelect }) { +export default function FilterSelectForm({ websiteId, items, onSelect, includeOnlyEquals }) { const [field, setField] = useState(); const { data, isLoading } = useValues(websiteId, field?.name); @@ -37,6 +37,7 @@ export default function FilterSelectForm({ websiteId, items, onSelect }) { type={field?.type} values={data} onSelect={onSelect} + includeOnlyEquals={includeOnlyEquals} /> ); } diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/components/pages/websites/WebsiteMetricsBar.js index c625e239a..5297dfcd4 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/components/pages/websites/WebsiteMetricsBar.js @@ -1,20 +1,25 @@ import classNames from 'classnames'; -import { Row, Column } from 'react-basics'; -import { formatShortTime } from 'lib/format'; -import MetricCard from 'components/metrics/MetricCard'; +import { useApi, useDateRange, useMessages, usePageQuery, useSticky } from 'components/hooks'; import RefreshButton from 'components/input/RefreshButton'; import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; +import MetricCard from 'components/metrics/MetricCard'; import MetricsBar from 'components/metrics/MetricsBar'; -import { useApi, useDateRange, usePageQuery, useMessages, useSticky } from 'components/hooks'; +import FilterSelectForm from 'components/pages/reports/FilterSelectForm'; +import PopupForm from 'components/pages/reports/PopupForm'; +import { formatShortTime } from 'lib/format'; +import { Button, Column, Icon, Icons, Popup, PopupTrigger, Row, TooltipPopup } from 'react-basics'; import styles from './WebsiteMetricsBar.module.css'; export function WebsiteMetricsBar({ websiteId, sticky }) { const { formatMessage, labels } = useMessages(); + const { get, useQuery } = useApi(); const [dateRange] = useDateRange(websiteId); const { startDate, endDate, modified } = dateRange; const { ref, isSticky } = useSticky({ enabled: sticky }); const { + resolveUrl, + router, query: { url, referrer, title, os, browser, device, country, region, city }, } = usePageQuery(); @@ -39,6 +44,17 @@ export function WebsiteMetricsBar({ websiteId, sticky }) { }), ); + const fieldOptions = [ + { name: 'url', type: 'string', label: formatMessage(labels.url) }, + { name: 'referrer', type: 'string', label: formatMessage(labels.referrer) }, + { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, + { name: 'os', type: 'string', label: formatMessage(labels.os) }, + { name: 'device', type: 'string', label: formatMessage(labels.device) }, + { name: 'country', type: 'string', label: formatMessage(labels.country) }, + { name: 'region', type: 'string', label: formatMessage(labels.region) }, + { name: 'city', type: 'string', label: formatMessage(labels.city) }, + ]; + const { pageviews, uniques, bounces, totaltime } = data || {}; const num = Math.min(data && uniques.value, data && bounces.value); const diffs = data && { @@ -48,6 +64,42 @@ export function WebsiteMetricsBar({ websiteId, sticky }) { totaltime: totaltime.value - totaltime.change, }; + const handleAddFilter = ({ name, value }) => { + router.push(resolveUrl({ [name]: value })); + }; + + const WebsiteFilterButton = () => { + return ( + + + + + + {close => { + return ( + + { + handleAddFilter(value); + close(); + }} + includeOnlyEquals={true} + /> + + ); + }} + + + ); + }; + return (
+
diff --git a/src/pages/api/websites/[id]/index.ts b/src/pages/api/websites/[id]/index.ts index 597568de5..0e5aacceb 100644 --- a/src/pages/api/websites/[id]/index.ts +++ b/src/pages/api/websites/[id]/index.ts @@ -23,6 +23,7 @@ const schema = { id: yup.string().uuid().required(), }), }; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, From fe56ed35d5cd736776d8e6e2644fd09ae3afffc3 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 11:21:16 -0700 Subject: [PATCH 115/357] Hide filter. --- src/components/pages/websites/WebsiteChartList.js | 2 +- src/components/pages/websites/WebsiteMetricsBar.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/websites/WebsiteChartList.js b/src/components/pages/websites/WebsiteChartList.js index f6f3f7656..56cbe157b 100644 --- a/src/components/pages/websites/WebsiteChartList.js +++ b/src/components/pages/websites/WebsiteChartList.js @@ -40,7 +40,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) { - + {showCharts && } ) : null; diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/components/pages/websites/WebsiteMetricsBar.js index 5297dfcd4..356058040 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/components/pages/websites/WebsiteMetricsBar.js @@ -10,7 +10,7 @@ import { formatShortTime } from 'lib/format'; import { Button, Column, Icon, Icons, Popup, PopupTrigger, Row, TooltipPopup } from 'react-basics'; import styles from './WebsiteMetricsBar.module.css'; -export function WebsiteMetricsBar({ websiteId, sticky }) { +export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); @@ -162,7 +162,7 @@ export function WebsiteMetricsBar({ websiteId, sticky }) {
- + {showFilter && }
From c67deb68e6e44904b838c1ccc4d17a400e18898f Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 11:33:30 -0700 Subject: [PATCH 116/357] Remove enddate check. --- src/pages/api/realtime/[id].ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/api/realtime/[id].ts b/src/pages/api/realtime/[id].ts index ab7bb4060..5b1e1e057 100644 --- a/src/pages/api/realtime/[id].ts +++ b/src/pages/api/realtime/[id].ts @@ -11,12 +11,10 @@ export interface RealtimeRequestQuery { startAt: number; } -const currentDate = new Date().getTime(); - const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), - startAt: yup.number().integer().max(currentDate).required(), + startAt: yup.number().integer().required(), }), }; From 30c32dca7d9a35ac48f31bd127a20631a82d0da7 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 25 Aug 2023 11:34:59 -0700 Subject: [PATCH 117/357] remove required yup filters on insights report --- src/pages/api/reports/insights.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pages/api/reports/insights.ts b/src/pages/api/reports/insights.ts index d10eba3fb..4d17c9220 100644 --- a/src/pages/api/reports/insights.ts +++ b/src/pages/api/reports/insights.ts @@ -38,18 +38,14 @@ const schema = { ) .min(1) .required(), - filters: yup - .array() - .of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - filter: yup.string().required(), - value: yup.string().required(), - }), - ) - .min(1) - .required(), + filters: yup.array().of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + filter: yup.string().required(), + value: yup.string().required(), + }), + ), groups: yup.array().of( yup.object().shape({ name: yup.string().required(), From d6a27b8e99db9bc4566bfe4f1af58cb519f23c2e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Aug 2023 11:54:44 -0700 Subject: [PATCH 118/357] Initial dev on DataTable component. --- package.json | 2 +- src/components/common/DataTable.js | 68 ++++++++++++++++++ src/components/common/DataTable.module.css | 17 +++++ src/components/common/Pager.js | 11 +-- src/components/common/Pager.module.css | 5 +- src/components/hooks/useDataTable.js | 13 ++++ src/components/hooks/usePaging.js | 9 +++ src/components/messages.js | 1 + src/components/metrics/EventsChart.js | 6 +- .../metrics/{DataTable.js => ListTable.js} | 6 +- ...aTable.module.css => ListTable.module.css} | 0 src/components/metrics/MetricsTable.js | 4 +- .../pages/realtime/RealtimeCountries.js | 4 +- src/components/pages/realtime/RealtimeUrls.js | 6 +- .../pages/reports/funnel/FunnelTable.js | 4 +- .../pages/settings/websites/WebsitesList.js | 22 +++--- .../pages/settings/websites/WebsitesTable.js | 69 ++++++++++++++++++- src/components/pages/websites/WebsitesPage.js | 21 +++--- yarn.lock | 8 +-- 19 files changed, 223 insertions(+), 53 deletions(-) create mode 100644 src/components/common/DataTable.js create mode 100644 src/components/common/DataTable.module.css create mode 100644 src/components/hooks/useDataTable.js create mode 100644 src/components/hooks/usePaging.js rename src/components/metrics/{DataTable.js => ListTable.js} (96%) rename src/components/metrics/{DataTable.module.css => ListTable.module.css} (100%) diff --git a/package.json b/package.json index 1ff1730d1..c23814dce 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.98.0", + "react-basics": "^0.100.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/src/components/common/DataTable.js b/src/components/common/DataTable.js new file mode 100644 index 000000000..cb7393445 --- /dev/null +++ b/src/components/common/DataTable.js @@ -0,0 +1,68 @@ +import { createContext } from 'react'; +import { SearchField } from 'react-basics'; +import { useDataTable } from 'components/hooks/useDataTable'; +import { useMessages } from 'components/hooks'; +import Empty from 'components/common/Empty'; +import Pager from 'components/common/Pager'; +import styles from './DataTable.module.css'; + +const DEFAULT_SEARCH_DELAY = 1000; + +export const DataTableStyles = styles; + +export const DataTableContext = createContext(null); + +export function DataTable({ + searchDelay, + showSearch = true, + showPaging = true, + children, + onChange, +}) { + const { formatMessage, labels, messages } = useMessages(); + const dataTable = useDataTable(); + const { query, setQuery, data, pageInfo, setPageInfo } = dataTable; + const { page, pageSize, count } = pageInfo || {}; + const noResults = Boolean(query && data?.length === 0); + + const handleChange = () => { + onChange?.({ query, page }); + }; + + const handleSearch = value => { + setQuery(value); + handleChange(); + }; + + const handlePageChange = page => { + setPageInfo(state => ({ ...state, page })); + }; + + return ( + + {showSearch && ( + + )} + {noResults && } +
{children}
+ {showPaging && ( + + )} +
+ ); +} + +export default DataTable; diff --git a/src/components/common/DataTable.module.css b/src/components/common/DataTable.module.css new file mode 100644 index 000000000..883110dad --- /dev/null +++ b/src/components/common/DataTable.module.css @@ -0,0 +1,17 @@ +.search { + max-width: 300px; + margin: 20px 0; +} + +.action { + justify-content: flex-end; + gap: 5px; +} + +.body td { + align-items: center; +} + +.pager { + margin-top: 20px; +} diff --git a/src/components/common/Pager.js b/src/components/common/Pager.js index 7a5e7ed5f..3f94edb01 100644 --- a/src/components/common/Pager.js +++ b/src/components/common/Pager.js @@ -1,14 +1,15 @@ -import styles from './Pager.module.css'; +import classNames from 'classnames'; import { Button, Flexbox, Icon, Icons } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; +import styles from './Pager.module.css'; -export function Pager({ page, pageSize, count, onPageChange }) { +export function Pager({ page, pageSize, count, onPageChange, className }) { const { formatMessage, labels } = useMessages(); - const maxPage = Math.ceil(count / pageSize); + const maxPage = pageSize && count ? Math.ceil(count / pageSize) : 0; const lastPage = page === maxPage; const firstPage = page === 1; - if (count === 0) { + if (count === 0 || !maxPage) { return null; } @@ -24,7 +25,7 @@ export function Pager({ page, pageSize, count, onPageChange }) { } return ( - + + + )} + + + + + ); + }} + + + )} + + ); +} + +export function WebsitesTable2({ data = [], filterValue, onFilterChange, diff --git a/src/components/pages/websites/WebsitesPage.js b/src/components/pages/websites/WebsitesPage.js index 2eb060d31..a83d13d51 100644 --- a/src/components/pages/websites/WebsitesPage.js +++ b/src/components/pages/websites/WebsitesPage.js @@ -19,16 +19,19 @@ import { useToasts, } from 'react-basics'; +const TABS = { + myWebsites: 'my-websites', + teamWebsites: 'team-websites', +}; + export function WebsitesPage() { const { formatMessage, labels, messages } = useMessages(); - const [tab, setTab] = useState('my-websites'); - const [fetch, setFetch] = useState(1); + const [tab, setTab] = useState(TABS.myWebsites); const { user } = useUser(); const { cloudMode } = useConfig(); const { showToast } = useToasts(); - const handleSave = async () => { - setFetch(fetch + 1); + const handleSave = () => { showToast({ message: formatMessage(messages.saved), variant: 'success' }); }; @@ -54,18 +57,16 @@ export function WebsitesPage() { {!cloudMode && addButton} - {formatMessage(labels.myWebsites)} - {formatMessage(labels.teamWebsites)} + {formatMessage(labels.myWebsites)} + {formatMessage(labels.teamWebsites)} - - {tab === 'my-websites' && ( + {tab === TABS.myWebsites && ( )} - {tab === 'team-webaites' && ( + {tab === TABS.teamWebsites && ( diff --git a/yarn.lock b/yarn.lock index c20730f3f..e824ca905 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7642,10 +7642,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.98.0: - version "0.98.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.98.0.tgz#b207bedbd9dac749d28ea6de2197a0efe648b78c" - integrity sha512-ebUigu+s6Iusq14EZTFTTUzdDPYFQEZjeD4feeq3o7dE+ndOVnajEdQ2va/x6CsRBUsWgjLJipfQi0XIrxYupA== +react-basics@^0.100.0: + version "0.100.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.100.0.tgz#14a36769af89f3e01641997f897e4073f16f5035" + integrity sha512-ET6DX/FYAcjGRauBE4jwqwVpd/hKmA2Nu/fi1dakwsv17hkyV5FEAhdWhQAxJX3VnaCH//QysN8+ae12KuNA9g== dependencies: classnames "^2.3.1" date-fns "^2.29.3" From 75861bda61836a31e07bd3b25f3e73b7f2be58eb Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 13:01:48 -0700 Subject: [PATCH 119/357] Fix insight. --- src/pages/api/reports/insights.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pages/api/reports/insights.ts b/src/pages/api/reports/insights.ts index d10eba3fb..4d17c9220 100644 --- a/src/pages/api/reports/insights.ts +++ b/src/pages/api/reports/insights.ts @@ -38,18 +38,14 @@ const schema = { ) .min(1) .required(), - filters: yup - .array() - .of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - filter: yup.string().required(), - value: yup.string().required(), - }), - ) - .min(1) - .required(), + filters: yup.array().of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + filter: yup.string().required(), + value: yup.string().required(), + }), + ), groups: yup.array().of( yup.object().shape({ name: yup.string().required(), From 6d0b3934ebcf313d1b8784cddfd4dfc31d994c85 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 25 Aug 2023 13:32:24 -0700 Subject: [PATCH 120/357] Fix LoadingButton isLoading property --- src/components/common/ConfirmDeleteForm.js | 2 +- src/components/input/RefreshButton.js | 2 +- src/components/pages/reports/ReportHeader.js | 2 +- .../pages/settings/teams/TeamMemberRemoveButton.js | 6 +++++- .../pages/settings/teams/TeamWebsiteRemoveButton.js | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/common/ConfirmDeleteForm.js b/src/components/common/ConfirmDeleteForm.js index fed618da9..3d2c383d9 100644 --- a/src/components/common/ConfirmDeleteForm.js +++ b/src/components/common/ConfirmDeleteForm.js @@ -17,7 +17,7 @@ export function ConfirmDeleteForm({ name, onConfirm, onClose }) { {name} }} />

- + {formatMessage(labels.delete)} diff --git a/src/components/input/RefreshButton.js b/src/components/input/RefreshButton.js index 81bca45f7..8b40cafa1 100644 --- a/src/components/input/RefreshButton.js +++ b/src/components/input/RefreshButton.js @@ -16,7 +16,7 @@ export function RefreshButton({ websiteId, isLoading }) { return ( - + diff --git a/src/components/pages/reports/ReportHeader.js b/src/components/pages/reports/ReportHeader.js index e81f66fe8..e81d6ece6 100644 --- a/src/components/pages/reports/ReportHeader.js +++ b/src/components/pages/reports/ReportHeader.js @@ -66,7 +66,7 @@ export function ReportHeader({ icon }) { }> diff --git a/src/components/pages/settings/teams/TeamMemberRemoveButton.js b/src/components/pages/settings/teams/TeamMemberRemoveButton.js index 44fd30abe..3ec0f8b36 100644 --- a/src/components/pages/settings/teams/TeamMemberRemoveButton.js +++ b/src/components/pages/settings/teams/TeamMemberRemoveButton.js @@ -19,7 +19,11 @@ export function TeamMemberRemoveButton({ teamId, userId, disabled, onSave }) { }; return ( - handleRemoveTeamMember()} disabled={disabled} loading={isLoading}> + handleRemoveTeamMember()} + disabled={disabled} + isLoading={isLoading} + > diff --git a/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js b/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js index 2ea773fcd..c0ddf95ce 100644 --- a/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js +++ b/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js @@ -19,7 +19,7 @@ export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) { }; return ( - handleRemoveTeamMember()} loading={isLoading}> + handleRemoveTeamMember()} isLoading={isLoading}> From 236a26136da65c4a3f199115d9fa18aeb21a520f Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 13:38:52 -0700 Subject: [PATCH 121/357] Add teamId. --- src/components/pages/settings/teams/TeamMembers.js | 1 + src/components/pages/settings/teams/TeamMembersTable.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/settings/teams/TeamMembers.js b/src/components/pages/settings/teams/TeamMembers.js index 1420f6def..207ad72d4 100644 --- a/src/components/pages/settings/teams/TeamMembers.js +++ b/src/components/pages/settings/teams/TeamMembers.js @@ -33,6 +33,7 @@ export function TeamMembers({ teamId, readOnly }) { <> Date: Fri, 25 Aug 2023 15:03:49 -0700 Subject: [PATCH 122/357] update tooltip --- src/components/pages/websites/WebsiteMetricsBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/components/pages/websites/WebsiteMetricsBar.js index 356058040..75fdab6a0 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/components/pages/websites/WebsiteMetricsBar.js @@ -71,7 +71,7 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { const WebsiteFilterButton = () => { return ( - + - + {close => { return ( From 35f80d9b0055f29a907137d4eebe3a8a6de7d354 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Aug 2023 15:06:47 -0700 Subject: [PATCH 124/357] Fixed scripts. --- rollup.components.config.mjs | 10 ---------- scripts/download-country-names.js | 2 +- scripts/download-language-names.js | 2 +- scripts/format-lang.js | 4 ++-- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/rollup.components.config.mjs b/rollup.components.config.mjs index a0b8efbd6..c4481d0ee 100644 --- a/rollup.components.config.mjs +++ b/rollup.components.config.mjs @@ -29,16 +29,6 @@ const aliasConfig = { customResolver, }; -const external = [ - 'react', - 'react-dom', - 'react/jsx-runtime', - 'react-intl', - 'react-basics', - 'classnames', - 'next', -]; - const jsBundle = { input: 'src/index.ts', output: [ diff --git a/scripts/download-country-names.js b/scripts/download-country-names.js index f56d91f9c..6dbbbbdcc 100644 --- a/scripts/download-country-names.js +++ b/scripts/download-country-names.js @@ -4,7 +4,7 @@ const path = require('path'); const https = require('https'); const chalk = require('chalk'); -const src = path.resolve(__dirname, '../lang'); +const src = path.resolve(__dirname, '../src/lang'); const dest = path.resolve(__dirname, '../public/intl/country'); const files = fs.readdirSync(src); diff --git a/scripts/download-language-names.js b/scripts/download-language-names.js index 5cea88cf8..5478fb8d0 100644 --- a/scripts/download-language-names.js +++ b/scripts/download-language-names.js @@ -4,7 +4,7 @@ const path = require('path'); const https = require('https'); const chalk = require('chalk'); -const src = path.resolve(__dirname, '../lang'); +const src = path.resolve(__dirname, '../src/lang'); const dest = path.resolve(__dirname, '../public/intl/language'); const files = fs.readdirSync(src); diff --git a/scripts/format-lang.js b/scripts/format-lang.js index 941fd6722..593e296d2 100644 --- a/scripts/format-lang.js +++ b/scripts/format-lang.js @@ -3,7 +3,7 @@ const path = require('path'); const del = require('del'); const prettier = require('prettier'); -const src = path.resolve(__dirname, '../lang'); +const src = path.resolve(__dirname, '../src/lang'); const dest = path.resolve(__dirname, '../build/messages'); const files = fs.readdirSync(src); @@ -17,7 +17,7 @@ async function run() { await fs.ensureDir(dest); files.forEach(file => { - const lang = require(`../lang/${file}`); + const lang = require(`../src/lang/${file}`); const keys = Object.keys(lang).sort(); const formatted = keys.reduce((obj, key) => { From 080eb34d579ccd8ab3cdd15899b22acb5561fb52 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Aug 2023 16:05:02 -0700 Subject: [PATCH 125/357] Update date filter buttons. --- package.json | 2 +- src/components/input/WebsiteDateFilter.js | 34 +++++++++---------- .../input/WebsiteDateFilter.module.css | 11 ++++++ src/components/layout/AppLayout.module.css | 2 +- src/components/metrics/FilterTags.js | 3 +- src/components/metrics/FilterTags.module.css | 12 ++++--- .../pages/websites/WebsiteDetailsPage.js | 4 +-- .../pages/websites/WebsiteMetricsBar.js | 4 +-- 8 files changed, 43 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 1ff1730d1..64e09463b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.5.0", + "version": "2.6.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", diff --git a/src/components/input/WebsiteDateFilter.js b/src/components/input/WebsiteDateFilter.js index 5ab19e605..a8403db6c 100644 --- a/src/components/input/WebsiteDateFilter.js +++ b/src/components/input/WebsiteDateFilter.js @@ -8,7 +8,6 @@ import styles from './WebsiteDateFilter.module.css'; export function WebsiteDateFilter({ websiteId }) { const [dateRange, setDateRange] = useDateRange(websiteId); const { value, startDate, endDate, selectedUnit } = dateRange; - const isFutureDate = value !== 'all' && isAfter(incrementDateRange(dateRange, -1).startDate, new Date()); @@ -21,7 +20,21 @@ export function WebsiteDateFilter({ websiteId }) { }; return ( - <> + + {value !== 'all' && ( + + + + + )} - {value !== 'all' && ( - - - - - - )} - + ); } diff --git a/src/components/input/WebsiteDateFilter.module.css b/src/components/input/WebsiteDateFilter.module.css index 13234c55c..986f5c178 100644 --- a/src/components/input/WebsiteDateFilter.module.css +++ b/src/components/input/WebsiteDateFilter.module.css @@ -1,3 +1,14 @@ .dropdown { min-width: 200px; } + +.buttons button:first-child { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.buttons button:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: 1px solid var(--base400) !important; +} diff --git a/src/components/layout/AppLayout.module.css b/src/components/layout/AppLayout.module.css index be51f83cf..bcce963f0 100644 --- a/src/components/layout/AppLayout.module.css +++ b/src/components/layout/AppLayout.module.css @@ -10,7 +10,7 @@ width: 100vw; grid-column: 1; grid-row: 1 / 2; - z-index: 1; + z-index: var(--z-index-popup); } .body { diff --git a/src/components/metrics/FilterTags.js b/src/components/metrics/FilterTags.js index 30857a6b9..cb88a7dba 100644 --- a/src/components/metrics/FilterTags.js +++ b/src/components/metrics/FilterTags.js @@ -1,8 +1,8 @@ import { safeDecodeURI } from 'next-basics'; import { Button, Icon, Icons, Text } from 'react-basics'; import usePageQuery from 'components/hooks/usePageQuery'; -import styles from './FilterTags.module.css'; import useMessages from 'components/hooks/useMessages'; +import styles from './FilterTags.module.css'; export function FilterTags({ params }) { const { formatMessage, labels } = useMessages(); @@ -26,6 +26,7 @@ export function FilterTags({ params }) { return (
+
{formatMessage(labels.filters)}
{Object.keys(params).map(key => { if (!params[key]) { return null; diff --git a/src/components/metrics/FilterTags.module.css b/src/components/metrics/FilterTags.module.css index 1c8458acd..c228dc4e4 100644 --- a/src/components/metrics/FilterTags.module.css +++ b/src/components/metrics/FilterTags.module.css @@ -4,19 +4,23 @@ gap: 10px; } +.label { + font-weight: 700; +} + .tag { display: flex; flex-direction: row; align-items: center; gap: 10px; font-size: var(--font-size-sm); - border: 1px solid var(--base600); + border: 1px solid var(--blue400); border-radius: var(--border-radius); - line-height: 30px; - padding: 0 8px; + padding: 8px 16px; cursor: pointer; + background: var(--blue100); } .tag:hover { - background: var(--base75); + background: var(--blue200); } diff --git a/src/components/pages/websites/WebsiteDetailsPage.js b/src/components/pages/websites/WebsiteDetailsPage.js index c5ac47758..222d94d9e 100644 --- a/src/components/pages/websites/WebsiteDetailsPage.js +++ b/src/components/pages/websites/WebsiteDetailsPage.js @@ -22,12 +22,12 @@ export default function WebsiteDetailsPage({ websiteId }) { return ( - - + + {!website && } {website && ( <> diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/components/pages/websites/WebsiteMetricsBar.js index 41ae859f5..0d1a20f0b 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/components/pages/websites/WebsiteMetricsBar.js @@ -10,7 +10,7 @@ import { formatShortTime } from 'lib/format'; import { Button, Column, Icon, Icons, Popup, PopupTrigger, Row } from 'react-basics'; import styles from './WebsiteMetricsBar.module.css'; -export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { +export function WebsiteMetricsBar({ websiteId, showFilter = true, showRefresh = true, sticky }) { const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); @@ -161,8 +161,8 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) {
{showFilter && } + {showRefresh && } -
From cf1dd422fee79c08711be6c05030f3f36093012d Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 16:23:10 -0700 Subject: [PATCH 126/357] Disable filter on custom range. --- src/components/input/WebsiteDateFilter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/input/WebsiteDateFilter.js b/src/components/input/WebsiteDateFilter.js index a8403db6c..6903a7085 100644 --- a/src/components/input/WebsiteDateFilter.js +++ b/src/components/input/WebsiteDateFilter.js @@ -9,7 +9,9 @@ export function WebsiteDateFilter({ websiteId }) { const [dateRange, setDateRange] = useDateRange(websiteId); const { value, startDate, endDate, selectedUnit } = dateRange; const isFutureDate = - value !== 'all' && isAfter(incrementDateRange(dateRange, -1).startDate, new Date()); + value !== 'all' && + selectedUnit && + isAfter(incrementDateRange(dateRange, -1).startDate, new Date()); const handleChange = value => { setDateRange(value); @@ -21,7 +23,7 @@ export function WebsiteDateFilter({ websiteId }) { return ( - {value !== 'all' && ( + {value !== 'all' && selectedUnit && (
)} @@ -102,4 +102,4 @@ const AnimatedRow = ({ ); }; -export default DataTable; +export default ListTable; diff --git a/src/components/metrics/MetricsTable.js b/src/components/metrics/MetricsTable.js index 893427a5f..39578381d 100644 --- a/src/components/metrics/MetricsTable.js +++ b/src/components/metrics/MetricsTable.js @@ -8,7 +8,7 @@ import { percentFilter } from 'lib/filters'; import useDateRange from 'components/hooks/useDateRange'; import usePageQuery from 'components/hooks/usePageQuery'; import ErrorMessage from 'components/common/ErrorMessage'; -import DataTable from './DataTable'; +import ListTable from './ListTable'; import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; import Icons from 'components/icons'; import useMessages from 'components/hooks/useMessages'; @@ -104,7 +104,7 @@ export function MetricsTable({
{!data && isLoading && !isFetched && } {error && } - {data && !error && } + {data && !error && }
{data && !error && limit && ( diff --git a/src/components/pages/realtime/RealtimeCountries.js b/src/components/pages/realtime/RealtimeCountries.js index 3aecad5ff..7a61651a2 100644 --- a/src/components/pages/realtime/RealtimeCountries.js +++ b/src/components/pages/realtime/RealtimeCountries.js @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { useRouter } from 'next/router'; -import DataTable from 'components/metrics/DataTable'; +import ListTable from 'components/metrics/ListTable'; import useLocale from 'components/hooks/useLocale'; import useCountryNames from 'components/hooks/useCountryNames'; import useMessages from 'components/hooks/useMessages'; @@ -24,7 +24,7 @@ export function RealtimeCountries({ data }) { ); return ( - {filter === FILTER_REFERRERS && ( - )} {filter === FILTER_PAGES && ( - Date: Sat, 26 Aug 2023 16:27:29 -0700 Subject: [PATCH 133/357] Remove required. --- src/pages/api/users/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts index 0b523c70c..fd1585d44 100644 --- a/src/pages/api/users/index.ts +++ b/src/pages/api/users/index.ts @@ -25,10 +25,7 @@ const schema = { username: yup.string().max(255).required(), password: yup.string().required(), id: yup.string().uuid(), - role: yup - .string() - .matches(/admin|user|view-only/i) - .required(), + role: yup.string().matches(/admin|user|view-only/i), }), }; From fcd7fac2ce635a5e757bcb9e419531663ca599b5 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sat, 26 Aug 2023 16:33:22 -0700 Subject: [PATCH 134/357] Re-add required. --- src/pages/api/users/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts index fd1585d44..0b523c70c 100644 --- a/src/pages/api/users/index.ts +++ b/src/pages/api/users/index.ts @@ -25,7 +25,10 @@ const schema = { username: yup.string().max(255).required(), password: yup.string().required(), id: yup.string().uuid(), - role: yup.string().matches(/admin|user|view-only/i), + role: yup + .string() + .matches(/admin|user|view-only/i) + .required(), }), }; From f2f55170513d9a9735a058a2a800621eb3eab6b8 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sat, 26 Aug 2023 16:35:43 -0700 Subject: [PATCH 135/357] fix type. --- src/pages/api/users/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts index 0b523c70c..991986e87 100644 --- a/src/pages/api/users/index.ts +++ b/src/pages/api/users/index.ts @@ -13,7 +13,7 @@ export interface UsersRequestBody { username: string; password: string; id: string; - role?: Role; + role: Role; } import * as yup from 'yup'; From cc574e6da4800b5fe6f7eedccb7c180bc49abe08 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 27 Aug 2023 14:15:02 -0700 Subject: [PATCH 136/357] Added settings redirect for cloud. --- next.config.js | 4 +++- src/components/layout/NavBar.js | 10 ++++------ src/components/pages/reports/FieldFilterForm.js | 4 ++-- src/components/pages/reports/FilterSelectForm.js | 4 ++-- src/components/pages/settings/websites/TrackingCode.js | 4 ++-- src/components/pages/websites/WebsiteMetricsBar.js | 6 ++---- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/next.config.js b/next.config.js index 4ab775107..b7a93ace6 100644 --- a/next.config.js +++ b/next.config.js @@ -59,7 +59,9 @@ if (process.env.TRACKER_SCRIPT_NAME) { const redirects = [ { source: '/settings', - destination: process.env.CLOUD_MODE ? '/settings/profile' : '/settings/websites', + destination: process.env.CLOUD_MODE + ? `${process.env.CLOUD_URL}/settings/websites` + : '/settings/websites', permanent: true, }, ]; diff --git a/src/components/layout/NavBar.js b/src/components/layout/NavBar.js index ec0881755..07627e2a2 100644 --- a/src/components/layout/NavBar.js +++ b/src/components/layout/NavBar.js @@ -1,26 +1,24 @@ import { Icon, Text, Row, Column } from 'react-basics'; import Link from 'next/link'; +import { useRouter } from 'next/router'; import classNames from 'classnames'; import Icons from 'components/icons'; import ThemeButton from 'components/input/ThemeButton'; import LanguageButton from 'components/input/LanguageButton'; import ProfileButton from 'components/input/ProfileButton'; -import styles from './NavBar.module.css'; -import useConfig from 'components/hooks/useConfig'; import useMessages from 'components/hooks/useMessages'; -import { useRouter } from 'next/router'; -import HamburgerButton from '../common/HamburgerButton'; +import HamburgerButton from 'components/common/HamburgerButton'; +import styles from './NavBar.module.css'; export function NavBar() { const { pathname } = useRouter(); - const { cloudMode } = useConfig(); const { formatMessage, labels } = useMessages(); const links = [ { label: formatMessage(labels.dashboard), url: '/dashboard' }, { label: formatMessage(labels.websites), url: '/websites' }, { label: formatMessage(labels.reports), url: '/reports' }, - !cloudMode && { label: formatMessage(labels.settings), url: '/settings' }, + { label: formatMessage(labels.settings), url: '/settings' }, ].filter(n => n); return ( diff --git a/src/components/pages/reports/FieldFilterForm.js b/src/components/pages/reports/FieldFilterForm.js index 01efed3f4..96ac06b01 100644 --- a/src/components/pages/reports/FieldFilterForm.js +++ b/src/components/pages/reports/FieldFilterForm.js @@ -9,7 +9,7 @@ export default function FieldFilterForm({ type, values, onSelect, - includeOnlyEquals, + allowFilterSelect = true, }) { const { formatMessage, labels } = useMessages(); const [filter, setFilter] = useState('eq'); @@ -34,7 +34,7 @@ export default function FieldFilterForm({ - {!includeOnlyEquals && ( + {allowFilterSelect && ( ); } diff --git a/src/components/pages/settings/websites/TrackingCode.js b/src/components/pages/settings/websites/TrackingCode.js index 5159ff914..fb4eb9a9e 100644 --- a/src/components/pages/settings/websites/TrackingCode.js +++ b/src/components/pages/settings/websites/TrackingCode.js @@ -4,10 +4,10 @@ import useConfig from 'components/hooks/useConfig'; export function TrackingCode({ websiteId }) { const { formatMessage, messages } = useMessages(); - const { basePath, trackerScriptName } = useConfig(); + const { basePath, trackerScriptName, trackerScriptOrigin } = useConfig(); const url = trackerScriptName?.startsWith('http') ? trackerScriptName - : `${location.origin}${basePath}/${ + : `${trackerScriptOrigin || location.origin}${basePath}/${ trackerScriptName?.split(',')?.map(n => n.trim())?.[0] || 'script.js' }`; diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/components/pages/websites/WebsiteMetricsBar.js index 0d1a20f0b..7ba4a8012 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/components/pages/websites/WebsiteMetricsBar.js @@ -1,6 +1,5 @@ import classNames from 'classnames'; import { useApi, useDateRange, useMessages, usePageQuery, useSticky } from 'components/hooks'; -import RefreshButton from 'components/input/RefreshButton'; import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; import MetricCard from 'components/metrics/MetricCard'; import MetricsBar from 'components/metrics/MetricsBar'; @@ -10,7 +9,7 @@ import { formatShortTime } from 'lib/format'; import { Button, Column, Icon, Icons, Popup, PopupTrigger, Row } from 'react-basics'; import styles from './WebsiteMetricsBar.module.css'; -export function WebsiteMetricsBar({ websiteId, showFilter = true, showRefresh = true, sticky }) { +export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); @@ -88,7 +87,7 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, showRefresh = handleAddFilter(value); close(); }} - includeOnlyEquals={true} + allowFilterSelect={false} /> ); @@ -161,7 +160,6 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, showRefresh =
{showFilter && } - {showRefresh && }
From 183dab3ddc73dc0c7899f621c94f227f6464542d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 27 Aug 2023 19:56:44 -0700 Subject: [PATCH 137/357] Refactored useConfig. --- next.config.js | 3 +++ src/components/common/HamburgerButton.js | 3 +-- src/components/hooks/useConfig.js | 5 +++-- src/components/input/ProfileButton.js | 3 +-- src/components/layout/AppLayout.js | 2 +- src/components/layout/SettingsLayout.js | 3 +-- .../pages/settings/profile/ProfileDetails.js | 3 +-- .../pages/settings/websites/TrackingCode.js | 12 ++++++++---- src/components/pages/websites/WebsitesPage.js | 3 +-- src/pages/_app.js | 15 +-------------- src/pages/api/config.ts | 14 ++++++-------- 11 files changed, 27 insertions(+), 39 deletions(-) diff --git a/next.config.js b/next.config.js index b7a93ace6..cc3cde7c6 100644 --- a/next.config.js +++ b/next.config.js @@ -76,6 +76,9 @@ if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN const config = { env: { + cloudMode: process.env.CLOUD_MODE, + cloudUrl: process.env.CLOUD_URL, + configUrl: '/config', currentVersion: pkg.version, defaultLocale: process.env.DEFAULT_LOCALE, isProduction: process.env.NODE_ENV === 'production', diff --git a/src/components/common/HamburgerButton.js b/src/components/common/HamburgerButton.js index 9feee67b7..f97006efd 100644 --- a/src/components/common/HamburgerButton.js +++ b/src/components/common/HamburgerButton.js @@ -3,12 +3,11 @@ import { useState } from 'react'; import MobileMenu from './MobileMenu'; import Icons from 'components/icons'; import useMessages from 'components/hooks/useMessages'; -import useConfig from 'components/hooks/useConfig'; export function HamburgerButton() { const { formatMessage, labels } = useMessages(); const [active, setActive] = useState(false); - const { cloudMode } = useConfig(); + const cloudMode = Boolean(process.env.cloudMode); const menuItems = [ { diff --git a/src/components/hooks/useConfig.js b/src/components/hooks/useConfig.js index 6b37c87b0..5a516abc7 100644 --- a/src/components/hooks/useConfig.js +++ b/src/components/hooks/useConfig.js @@ -7,15 +7,16 @@ let loading = false; export function useConfig() { const { config } = useStore(); const { get } = useApi(); + const configUrl = process.env.configUrl; async function loadConfig() { - const data = await get('/config'); + const data = await get(configUrl); loading = false; setConfig(data); } useEffect(() => { - if (!config && !loading) { + if (!config && !loading && configUrl) { loading = true; loadConfig(); } diff --git a/src/components/input/ProfileButton.js b/src/components/input/ProfileButton.js index cfb89cf80..015c0ad8d 100644 --- a/src/components/input/ProfileButton.js +++ b/src/components/input/ProfileButton.js @@ -3,16 +3,15 @@ import { useRouter } from 'next/router'; import Icons from 'components/icons'; import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; -import useConfig from 'components/hooks/useConfig'; import styles from './ProfileButton.module.css'; import useLocale from 'components/hooks/useLocale'; export function ProfileButton() { const { formatMessage, labels } = useMessages(); const { user } = useUser(); - const { cloudMode } = useConfig(); const router = useRouter(); const { dir } = useLocale(); + const cloudMode = Boolean(process.env.cloudMode); const handleSelect = key => { if (key === 'profile') { diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js index 8fd637f36..41e2ec0d7 100644 --- a/src/components/layout/AppLayout.js +++ b/src/components/layout/AppLayout.js @@ -9,7 +9,7 @@ export function AppLayout({ title, children }) { const { user } = useRequireLogin(); const config = useConfig(); - if (!user || !config) { + if (!user || !config || config?.uiDisabled) { return null; } diff --git a/src/components/layout/SettingsLayout.js b/src/components/layout/SettingsLayout.js index 851c366a4..0f4aa5d96 100644 --- a/src/components/layout/SettingsLayout.js +++ b/src/components/layout/SettingsLayout.js @@ -3,14 +3,13 @@ import { useRouter } from 'next/router'; import SideNav from './SideNav'; import useUser from 'components/hooks/useUser'; import useMessages from 'components/hooks/useMessages'; -import useConfig from 'components/hooks/useConfig'; import styles from './SettingsLayout.module.css'; export function SettingsLayout({ children }) { const { user } = useUser(); const { pathname } = useRouter(); const { formatMessage, labels } = useMessages(); - const { cloudMode } = useConfig(); + const cloudMode = Boolean(process.env.cloudMode); const items = [ { key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' }, diff --git a/src/components/pages/settings/profile/ProfileDetails.js b/src/components/pages/settings/profile/ProfileDetails.js index f9dc652b9..d4a3a7d58 100644 --- a/src/components/pages/settings/profile/ProfileDetails.js +++ b/src/components/pages/settings/profile/ProfileDetails.js @@ -6,13 +6,12 @@ import ThemeSetting from 'components/pages/settings/profile/ThemeSetting'; import PasswordChangeButton from './PasswordChangeButton'; import useUser from 'components/hooks/useUser'; import useMessages from 'components/hooks/useMessages'; -import useConfig from 'components/hooks/useConfig'; import { ROLES } from 'lib/constants'; export function ProfileDetails() { const { user } = useUser(); const { formatMessage, labels } = useMessages(); - const { cloudMode } = useConfig(); + const cloudMode = Boolean(process.env.cloudMode); if (!user) { return null; diff --git a/src/components/pages/settings/websites/TrackingCode.js b/src/components/pages/settings/websites/TrackingCode.js index fb4eb9a9e..d22f0d59e 100644 --- a/src/components/pages/settings/websites/TrackingCode.js +++ b/src/components/pages/settings/websites/TrackingCode.js @@ -1,15 +1,19 @@ import { TextArea } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; import useConfig from 'components/hooks/useConfig'; +import { useRouter } from 'next/router'; export function TrackingCode({ websiteId }) { const { formatMessage, messages } = useMessages(); - const { basePath, trackerScriptName, trackerScriptOrigin } = useConfig(); + const { basePath } = useRouter(); + const config = useConfig(); + + const trackerScriptName = + config?.trackerScriptName?.split(',')?.map(n => n.trim())?.[0] || 'script.js'; + const url = trackerScriptName?.startsWith('http') ? trackerScriptName - : `${trackerScriptOrigin || location.origin}${basePath}/${ - trackerScriptName?.split(',')?.map(n => n.trim())?.[0] || 'script.js' - }`; + : `${process.env.analyticsUrl || location.origin}${basePath}/${trackerScriptName}`; const code = ``; diff --git a/src/components/pages/websites/WebsitesPage.js b/src/components/pages/websites/WebsitesPage.js index 2eb060d31..61c29448a 100644 --- a/src/components/pages/websites/WebsitesPage.js +++ b/src/components/pages/websites/WebsitesPage.js @@ -4,7 +4,6 @@ import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; import WebsiteList from 'components/pages/settings/websites/WebsitesList'; import { useMessages } from 'components/hooks'; import useUser from 'components/hooks/useUser'; -import useConfig from 'components/hooks/useConfig'; import { ROLES } from 'lib/constants'; import { useState } from 'react'; import { @@ -24,8 +23,8 @@ export function WebsitesPage() { const [tab, setTab] = useState('my-websites'); const [fetch, setFetch] = useState(1); const { user } = useUser(); - const { cloudMode } = useConfig(); const { showToast } = useToasts(); + const cloudMode = Boolean(process.env.cloudMode); const handleSave = async () => { setFetch(fetch + 1); diff --git a/src/pages/_app.js b/src/pages/_app.js index e88b2d863..7022772c7 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -6,7 +6,6 @@ import Script from 'next/script'; import { useRouter } from 'next/router'; import ErrorBoundary from 'components/common/ErrorBoundary'; import useLocale from 'components/hooks/useLocale'; -import useConfig from 'components/hooks/useConfig'; import '@fontsource/inter/400.css'; import '@fontsource/inter/700.css'; import 'react-basics/dist/styles.css'; @@ -27,22 +26,10 @@ const client = new QueryClient({ export default function App({ Component, pageProps }) { const { locale, messages } = useLocale(); const { basePath, pathname } = useRouter(); - const config = useConfig(); - - const Wrapper = ({ children }) => {children}; - - if (config?.uiDisabled) { - return null; - } return ( - null} - > + null}> diff --git a/src/pages/api/config.ts b/src/pages/api/config.ts index bccfd0487..adba894a4 100644 --- a/src/pages/api/config.ts +++ b/src/pages/api/config.ts @@ -2,21 +2,19 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { ok, methodNotAllowed } from 'next-basics'; export interface ConfigResponse { - basePath: string; - trackerScriptName: string; - updatesDisabled: boolean; telemetryDisabled: boolean; - cloudMode: boolean; + trackerScriptName: string; + uiDisabled: boolean; + updatesDisabled: boolean; } export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { return ok(res, { - basePath: process.env.BASE_PATH || '', - trackerScriptName: process.env.TRACKER_SCRIPT_NAME, - updatesDisabled: !!process.env.DISABLE_UPDATES, telemetryDisabled: !!process.env.DISABLE_TELEMETRY, - cloudMode: !!process.env.CLOUD_MODE, + trackerScriptName: process.env.TRACKER_SCRIPT_NAME, + uiDisabled: !!process.env.DISABLE_UI, + updatesDisabled: !!process.env.DISABLE_UPDATES, }); } From aaaa44ee16f4ead73c6587787447a236e3348536 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 27 Aug 2023 21:59:39 -0700 Subject: [PATCH 138/357] Removed breadcrumbs. --- .../pages/settings/teams/TeamSettings.js | 14 ++------------ .../pages/settings/users/UserSettings.js | 14 ++------------ .../pages/settings/websites/WebsiteSettings.js | 13 ++----------- 3 files changed, 6 insertions(+), 35 deletions(-) diff --git a/src/components/pages/settings/teams/TeamSettings.js b/src/components/pages/settings/teams/TeamSettings.js index 245e64b1a..8c4fe8f49 100644 --- a/src/components/pages/settings/teams/TeamSettings.js +++ b/src/components/pages/settings/teams/TeamSettings.js @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; -import { Breadcrumbs, Item, Tabs, useToasts } from 'react-basics'; -import Link from 'next/link'; +import { Item, Tabs, useToasts } from 'react-basics'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import { ROLES } from 'lib/constants'; @@ -44,16 +43,7 @@ export function TeamSettings({ teamId }) { return ( - - - {formatMessage(labels.teams)} - - {values?.name} - - } - /> + {formatMessage(labels.details)} {formatMessage(labels.members)} diff --git a/src/components/pages/settings/users/UserSettings.js b/src/components/pages/settings/users/UserSettings.js index 596e09f06..5fadf1a17 100644 --- a/src/components/pages/settings/users/UserSettings.js +++ b/src/components/pages/settings/users/UserSettings.js @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; -import { Breadcrumbs, Item, Tabs, useToasts } from 'react-basics'; -import Link from 'next/link'; +import { Item, Tabs, useToasts } from 'react-basics'; import UserEditForm from 'components/pages/settings/users/UserEditForm'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; @@ -44,16 +43,7 @@ export function UserSettings({ userId }) { return ( - - - {formatMessage(labels.users)} - - {values?.username} - - } - /> + {formatMessage(labels.details)} {formatMessage(labels.websites)} diff --git a/src/components/pages/settings/websites/WebsiteSettings.js b/src/components/pages/settings/websites/WebsiteSettings.js index ac8cd87cc..f73e0a878 100644 --- a/src/components/pages/settings/websites/WebsiteSettings.js +++ b/src/components/pages/settings/websites/WebsiteSettings.js @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Breadcrumbs, Item, Tabs, useToasts, Button, Text, Icon, Icons } from 'react-basics'; +import { Item, Tabs, useToasts, Button, Text, Icon, Icons } from 'react-basics'; import { useRouter } from 'next/router'; import Link from 'next/link'; import Page from 'components/layout/Page'; @@ -49,16 +49,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { return ( - - - {formatMessage(labels.websites)} - - {values?.name} - - } - > + -
+ <> + + + )} )} diff --git a/src/components/pages/dashboard/DashboardEdit.js b/src/components/pages/dashboard/DashboardEdit.js index 4eb259d61..f628599fe 100644 --- a/src/components/pages/dashboard/DashboardEdit.js +++ b/src/components/pages/dashboard/DashboardEdit.js @@ -5,23 +5,33 @@ import { Button } from 'react-basics'; import { firstBy } from 'thenby'; import useDashboard, { saveDashboard } from 'store/dashboard'; import useMessages from 'components/hooks/useMessages'; +import useApi from 'components/hooks/useApi'; import styles from './DashboardEdit.module.css'; +import Page from 'components/layout/Page'; const dragId = 'dashboard-website-ordering'; -export function DashboardEdit({ websites }) { +export function DashboardEdit() { const settings = useDashboard(); const { websiteOrder } = settings; const { formatMessage, labels } = useMessages(); const [order, setOrder] = useState(websiteOrder || []); + const { get, useQuery } = useApi(); + const { + data: result, + isLoading, + error, + } = useQuery(['websites'], () => get('/websites', { includeTeams: 1 })); + const { data: websites } = result || {}; - const ordered = useMemo( - () => - websites + const ordered = useMemo(() => { + if (websites) { + return websites .map(website => ({ ...website, order: order.indexOf(website.id) })) - .sort(firstBy('order')), - [websites, order], - ); + .sort(firstBy('order')); + } + return []; + }, [websites, order]); function handleWebsiteDrag({ destination, source }) { if (!destination || destination.index === source.index) return; @@ -49,7 +59,7 @@ export function DashboardEdit({ websites }) { } return ( - <> +
- +
); } diff --git a/src/pages/api/websites/index.ts b/src/pages/api/websites/index.ts index d724f12f4..d6009caf6 100644 --- a/src/pages/api/websites/index.ts +++ b/src/pages/api/websites/index.ts @@ -42,8 +42,13 @@ export default async ( } = req.auth; if (req.method === 'GET') { - req.query.id = userId; - req.query.pageSize = 100; + if (!req.query.id) { + req.query.id = userId; + } + + if (!req.query.pageSize) { + req.query.pageSize = 100; + } return userWebsites(req as any, res); } diff --git a/src/queries/admin/report.ts b/src/queries/admin/report.ts index a053ba92d..59eb70356 100644 --- a/src/queries/admin/report.ts +++ b/src/queries/admin/report.ts @@ -142,6 +142,7 @@ export async function getReports( ...pageFilters, ...(options?.include && { include: options.include }), }); + const count = await prisma.client.report.count({ where, }); diff --git a/src/queries/admin/team.ts b/src/queries/admin/team.ts index 284b218e2..cf731ad42 100644 --- a/src/queries/admin/team.ts +++ b/src/queries/admin/team.ts @@ -135,6 +135,7 @@ export async function getTeams( ...pageFilters, ...(options?.include && { include: options?.include }), }); + const count = await prisma.client.team.count({ where }); return { data: teams, count, ...getParameters }; diff --git a/src/queries/admin/website.ts b/src/queries/admin/website.ts index 3d0c773b6..cf4570cfc 100644 --- a/src/queries/admin/website.ts +++ b/src/queries/admin/website.ts @@ -107,7 +107,8 @@ export async function getWebsites( ...pageFilters, ...(options?.include && { include: options.include }), }); - const count = await prisma.client.website.count({ where }); + + const count = await prisma.client.website.count({ where: { ...where, deletedAt: null } }); return { data: websites, count, ...getParameters }; } From 61df80112a1396e8826a55b13319e33a925cc277 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 30 Aug 2023 15:43:39 -0700 Subject: [PATCH 153/357] Updated subdivision check. --- src/lib/detect.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib/detect.ts b/src/lib/detect.ts index d60435062..b16bab7ab 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -57,6 +57,14 @@ export function getDevice(screen, os) { } } +function getRegionCode(country, region) { + if (!country || !region) { + return undefined; + } + + return region.includes('-') ? region : `${country}-${region}`; +} + export async function getLocation(ip, req) { // Ignore local ips if (await isLocalhost(ip)) { @@ -71,7 +79,7 @@ export async function getLocation(ip, req) { return { country, - subdivision1: subdivision1?.includes('-') ? subdivision1 : `${country}-${subdivision1}`, + subdivision1: getRegionCode(country, subdivision1), city, }; } @@ -84,7 +92,7 @@ export async function getLocation(ip, req) { return { country, - subdivision1: subdivision1?.includes('-') ? subdivision1 : `${country}-${subdivision1}`, + subdivision1: getRegionCode(country, subdivision1), city, }; } From 62434a3e0ceb5eb8fd9f32701bb28c0a16a38fb6 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 30 Aug 2023 15:49:26 -0700 Subject: [PATCH 154/357] Bump version v2.6.1. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4fda0a22f..9745b780e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.6.0", + "version": "2.6.1", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", From fac306328dd75f6a5676addae2c968a0be5a40ee Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 30 Aug 2023 16:13:05 -0700 Subject: [PATCH 155/357] Updated language. --- public/intl/messages/zh-CN.json | 60 ++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index 71234564d..ec5c441b2 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -104,7 +104,7 @@ "label.browser": [ { "type": 0, - "value": "Browser" + "value": "浏览器" } ], "label.browsers": [ @@ -134,7 +134,7 @@ "label.city": [ { "type": 0, - "value": "City" + "value": "市/县" } ], "label.clear-all": [ @@ -176,7 +176,7 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "国家/地区" } ], "label.create-report": [ @@ -230,7 +230,7 @@ "label.date": [ { "type": 0, - "value": "Date" + "value": "日期" } ], "label.date-range": [ @@ -242,7 +242,7 @@ "label.day": [ { "type": 0, - "value": "Day" + "value": "日" } ], "label.default-date-range": [ @@ -296,7 +296,7 @@ "label.device": [ { "type": 0, - "value": "Device" + "value": "设备" } ], "label.devices": [ @@ -440,13 +440,13 @@ "label.is-not-set": [ { "type": 0, - "value": "Is not set" + "value": "未设置" } ], "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "已设置" } ], "label.join": [ @@ -576,7 +576,7 @@ "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "我的网站" } ], "label.name": [ @@ -618,7 +618,15 @@ "label.page-of": [ { "type": 0, - "value": "Page " + "value": "总" + }, + { + "type": 1, + "value": "total" + }, + { + "type": 0, + "value": "中的第" }, { "type": 1, @@ -626,11 +634,7 @@ }, { "type": 0, - "value": " of " - }, - { - "type": 1, - "value": "total" + "value": "页" } ], "label.page-views": [ @@ -642,7 +646,7 @@ "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "标题" } ], "label.pages": [ @@ -704,7 +708,7 @@ "label.referrer": [ { "type": 0, - "value": "Referrer" + "value": "来源" } ], "label.referrers": [ @@ -728,7 +732,7 @@ "label.region": [ { "type": 0, - "value": "Region" + "value": "州/省" } ], "label.regions": [ @@ -770,7 +774,7 @@ "label.retention": [ { "type": 0, - "value": "Retention" + "value": "保留" } ], "label.role": [ @@ -872,7 +876,7 @@ "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "团队名称" } ], "label.team-owner": [ @@ -884,7 +888,7 @@ "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "团队网站" } ], "label.teams": [ @@ -974,7 +978,7 @@ "label.unique": [ { "type": 0, - "value": "Unique" + "value": "独立" } ], "label.unique-visitors": [ @@ -998,13 +1002,13 @@ "label.url": [ { "type": 0, - "value": "URL" + "value": "网址" } ], "label.urls": [ { "type": 0, - "value": "URLs" + "value": "网址" } ], "label.user": [ @@ -1046,7 +1050,7 @@ "label.view-only": [ { "type": 0, - "value": "View only" + "value": "仅浏览量" } ], "label.views": [ @@ -1190,15 +1194,15 @@ "message.event-log": [ { "type": 1, - "value": "event" + "value": "url" }, { "type": 0, - "value": " on " + "value": "上的" }, { "type": 1, - "value": "url" + "value": "event" } ], "message.go-to-settings": [ From a7ea2027853be35254039df07422d9e89fa0607c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 30 Aug 2023 16:33:06 -0700 Subject: [PATCH 156/357] Check for payload. --- src/pages/api/send.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/api/send.ts b/src/pages/api/send.ts index a379f261f..56a748e13 100644 --- a/src/pages/api/send.ts +++ b/src/pages/api/send.ts @@ -79,6 +79,10 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { const { type, payload } = getJsonBody(req); + if (!type || !payload) { + return badRequest(res); + } + req.yup = schema; await useValidate(req, res); From 620011a837937226b7b25a528a411161c7f10705 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 30 Aug 2023 16:40:32 -0700 Subject: [PATCH 157/357] Removed getJsonBody. --- src/lib/detect.ts | 8 -------- src/lib/session.ts | 6 +++--- src/pages/api/send.ts | 4 ++-- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/lib/detect.ts b/src/lib/detect.ts index b16bab7ab..3b2f9021a 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -130,11 +130,3 @@ export async function getClientInfo(req: NextApiRequestCollect, { screen }) { return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device }; } - -export function getJsonBody(req): T { - if ((req.headers['content-type'] || '').indexOf('text/plain') !== -1) { - return JSON.parse(req.body); - } - - return req.body; -} diff --git a/src/lib/session.ts b/src/lib/session.ts index 85c173c52..0f388db9a 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,7 +1,7 @@ import { isUuid, secret, uuid } from 'lib/crypto'; -import { getClientInfo, getJsonBody } from 'lib/detect'; +import { getClientInfo } from 'lib/detect'; import { parseToken } from 'next-basics'; -import { CollectRequestBody, NextApiRequestCollect } from 'pages/api/send'; +import { NextApiRequestCollect } from 'pages/api/send'; import { createSession } from 'queries'; import cache from './cache'; import clickhouse from './clickhouse'; @@ -22,7 +22,7 @@ export async function findSession(req: NextApiRequestCollect): Promise<{ city: any; ownerId: string; }> { - const { payload } = getJsonBody(req); + const { payload } = req.body; if (!payload) { throw new Error('Invalid payload.'); diff --git a/src/pages/api/send.ts b/src/pages/api/send.ts index 56a748e13..e220711c8 100644 --- a/src/pages/api/send.ts +++ b/src/pages/api/send.ts @@ -3,7 +3,7 @@ import ipaddr from 'ipaddr.js'; import isbot from 'isbot'; import { COLLECTION_TYPE, HOSTNAME_REGEX } from 'lib/constants'; import { secret } from 'lib/crypto'; -import { getIpAddress, getJsonBody } from 'lib/detect'; +import { getIpAddress } from 'lib/detect'; import { useCors, useSession, useValidate } from 'lib/middleware'; import { CollectionType, YupRequest } from 'lib/types'; import { NextApiRequest, NextApiResponse } from 'next'; @@ -77,7 +77,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { return ok(res); } - const { type, payload } = getJsonBody(req); + const { type, payload } = req.body; if (!type || !payload) { return badRequest(res); From 68ffa823f8a27490b6005a84e44be0d95e2e767c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 30 Aug 2023 23:53:05 -0700 Subject: [PATCH 158/357] Fixed missing docker middleware. --- Dockerfile | 2 +- src/pages/api/send.ts | 124 +++++++++++++++++++++--------------------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/Dockerfile b/Dockerfile index bdc678dac..e0c7e8c37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,8 @@ RUN yarn install --frozen-lockfile FROM node:18-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules -COPY docker/middleware.js . COPY . . +COPY docker/middleware.js ./src ARG DATABASE_TYPE ARG BASE_PATH diff --git a/src/pages/api/send.ts b/src/pages/api/send.ts index e220711c8..00d721046 100644 --- a/src/pages/api/send.ts +++ b/src/pages/api/send.ts @@ -7,7 +7,7 @@ import { getIpAddress } from 'lib/detect'; import { useCors, useSession, useValidate } from 'lib/middleware'; import { CollectionType, YupRequest } from 'lib/types'; import { NextApiRequest, NextApiResponse } from 'next'; -import { badRequest, createToken, forbidden, ok, send } from 'next-basics'; +import { badRequest, createToken, forbidden, methodNotAllowed, ok, send } from 'next-basics'; import { saveEvent, saveSessionData } from 'queries'; import * as yup from 'yup'; @@ -73,75 +73,75 @@ const schema = { export default async (req: NextApiRequestCollect, res: NextApiResponse) => { await useCors(req, res); - if (isbot(req.headers['user-agent']) && !process.env.DISABLE_BOT_CHECK) { - return ok(res); - } - - const { type, payload } = req.body; - - if (!type || !payload) { - return badRequest(res); - } - - req.yup = schema; - await useValidate(req, res); - - if (await hasBlockedIp(req)) { - return forbidden(res); - } - - const { url, referrer, name: eventName, data: eventData, title: pageTitle } = payload; - - await useSession(req, res); - - const session = req.session; - - if (type === COLLECTION_TYPE.event) { - // eslint-disable-next-line prefer-const - let [urlPath, urlQuery] = url?.split('?') || []; - let [referrerPath, referrerQuery] = referrer?.split('?') || []; - let referrerDomain; - - if (!urlPath) { - urlPath = '/'; + if (req.method === 'POST') { + if (isbot(req.headers['user-agent']) && !process.env.DISABLE_BOT_CHECK) { + return ok(res); } - if (referrerPath?.startsWith('http')) { - const refUrl = new URL(referrer); - referrerPath = refUrl.pathname; - referrerQuery = refUrl.search.substring(1); - referrerDomain = refUrl.hostname.replace(/www\./, ''); + const { type, payload } = req.body; + + req.yup = schema; + await useValidate(req, res); + + if (await hasBlockedIp(req)) { + return forbidden(res); } - if (process.env.REMOVE_TRAILING_SLASH) { - urlPath = urlPath.replace(/.+\/$/, ''); + const { url, referrer, name: eventName, data: eventData, title: pageTitle } = payload; + + await useSession(req, res); + + const session = req.session; + + if (type === COLLECTION_TYPE.event) { + // eslint-disable-next-line prefer-const + let [urlPath, urlQuery] = url?.split('?') || []; + let [referrerPath, referrerQuery] = referrer?.split('?') || []; + let referrerDomain; + + if (!urlPath) { + urlPath = '/'; + } + + if (referrerPath?.startsWith('http')) { + const refUrl = new URL(referrer); + referrerPath = refUrl.pathname; + referrerQuery = refUrl.search.substring(1); + referrerDomain = refUrl.hostname.replace(/www\./, ''); + } + + if (process.env.REMOVE_TRAILING_SLASH) { + urlPath = urlPath.replace(/.+\/$/, ''); + } + + await saveEvent({ + urlPath, + urlQuery, + referrerPath, + referrerQuery, + referrerDomain, + pageTitle, + eventName, + eventData, + ...session, + sessionId: session.id, + }); } - await saveEvent({ - urlPath, - urlQuery, - referrerPath, - referrerQuery, - referrerDomain, - pageTitle, - eventName, - eventData, - ...session, - sessionId: session.id, - }); + if (type === COLLECTION_TYPE.identify) { + if (!eventData) { + return badRequest(res, 'Data required.'); + } + + await saveSessionData({ ...session, sessionData: eventData, sessionId: session.id }); + } + + const token = createToken(session, secret()); + + return send(res, token); } - if (type === COLLECTION_TYPE.identify) { - if (!eventData) { - return badRequest(res, 'Data required.'); - } - - await saveSessionData({ ...session, sessionData: eventData, sessionId: session.id }); - } - - const token = createToken(session, secret()); - - return send(res, token); + return methodNotAllowed(res); }; async function hasBlockedIp(req: NextApiRequestCollect) { From 5b7cfe83b483b1f6bb83c094a02322867f36c3dd Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 31 Aug 2023 00:08:24 -0700 Subject: [PATCH 159/357] Fixed update notice. --- src/components/common/UpdateNotice.js | 6 ++++-- src/components/common/UpdateNotice.module.css | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/common/UpdateNotice.js b/src/components/common/UpdateNotice.js index e3edc70c1..23907948c 100644 --- a/src/components/common/UpdateNotice.js +++ b/src/components/common/UpdateNotice.js @@ -1,4 +1,5 @@ import { useEffect, useCallback, useState } from 'react'; +import { createPortal } from 'react-dom'; import { Button, Row, Column } from 'react-basics'; import { setItem } from 'next-basics'; import useStore, { checkVersion } from 'store/version'; @@ -44,7 +45,7 @@ export function UpdateNotice({ user, config }) { return null; } - return ( + return createPortal( {formatMessage(messages.newVersionAvailable, { version: `v${latest}` })} @@ -55,7 +56,8 @@ export function UpdateNotice({ user, config }) { - + , + document.body, ); } diff --git a/src/components/common/UpdateNotice.module.css b/src/components/common/UpdateNotice.module.css index db7a0abdc..261a31698 100644 --- a/src/components/common/UpdateNotice.module.css +++ b/src/components/common/UpdateNotice.module.css @@ -2,13 +2,14 @@ position: absolute; max-width: 800px; gap: 20px; - margin: 20px auto; - justify-self: center; + margin: 80px auto; + align-self: center; background: var(--base50); padding: 20px; border: 1px solid var(--base300); border-radius: var(--border-radius); z-index: var(--z-index-popup); + box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1); } .message { From aa5c84a6efea4a94af4052e8bf915940575b6f86 Mon Sep 17 00:00:00 2001 From: anzepintar Date: Thu, 31 Aug 2023 11:24:28 +0200 Subject: [PATCH 160/357] Update sl-SI.json --- src/lang/sl-SI.json | 306 ++++++++++++++++++++++---------------------- 1 file changed, 153 insertions(+), 153 deletions(-) diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index aae7888d8..4e7e4ad95 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -1,211 +1,211 @@ { - "label.access-code": "Access code", + "label.access-code": "Koda za dostop", "label.actions": "Dejanja", - "label.activity-log": "Activity log", - "label.add": "Add", - "label.add-description": "Add description", + "label.activity-log": "Dnevnik dejavnosti", + "label.add": "Dodaj", + "label.add-description": "Dodaj opis", "label.add-website": "Dodaj spletno mesto", "label.admin": "Administrator", - "label.after": "After", - "label.all": "Vse", - "label.all-time": "All time", - "label.analytics": "Analytics", - "label.average": "Average", + "label.after": "Po", + "label.all": "Vsi", + "label.all-time": "Ves čas", + "label.analytics": "Analitika", + "label.average": "Povprečno", "label.average-visit-time": "Povprečni čas obiska", "label.back": "Nazaj", - "label.before": "Before", - "label.bounce-rate": "Zapustna stopnja", - "label.breakdown": "Breakdown", - "label.browser": "Browser", + "label.before": "Pred", + "label.bounce-rate": "Odbojna stopnja", + "label.breakdown": "Razčlenitev", + "label.browser": "Brskalnik", "label.browsers": "Brskalniki", "label.cancel": "Prekliči", "label.change-password": "Zamenjaj geslo", - "label.cities": "Cities", - "label.city": "City", - "label.clear-all": "Clear all", - "label.confirm": "Confirm", - "label.confirm-password": "Potrditev gesla", - "label.contains": "Contains", - "label.continue": "Continue", + "label.cities": "Mesta", + "label.city": "Mesto", + "label.clear-all": "Počisti vse", + "label.confirm": "Potrdi", + "label.confirm-password": "Potrdi geslo", + "label.contains": "Vsebuje", + "label.continue": "Nadaljuj", "label.countries": "Države", - "label.country": "Country", - "label.create-report": "Create report", - "label.create-team": "Create team", - "label.create-user": "Create user", - "label.created": "Created", + "label.country": "Država", + "label.create-report": "Ustvari poročilo", + "label.create-team": "Ustvari ekipo", + "label.create-user": "Ustvari uporabnika", + "label.created": "Ustvarjeno", "label.current-password": "Trenutno geslo", - "label.custom-range": "Razpon po meri", + "label.custom-range": "Obdobje po meri", "label.dashboard": "Nadzorna plošča", - "label.data": "Data", - "label.date": "Date", - "label.date-range": "Časovni razpon", - "label.day": "Day", - "label.default-date-range": "Privzeti časovni razpon", + "label.data": "Podatki", + "label.date": "Datum", + "label.date-range": "Časovno obdobje", + "label.day": "Dan", + "label.default-date-range": "Privzeto časovno obdobje", "label.delete": "Izbriši", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-team": "Izbriši ekipo", + "label.delete-user": "Izbriši uporabnika", "label.delete-website": "Izbriši spletno mesto", - "label.description": "Description", + "label.description": "Opis", "label.desktop": "Namizni računalnik", - "label.details": "Details", - "label.device": "Device", + "label.details": "Podrobnosti", + "label.device": "Naprava", "label.devices": "Naprave", - "label.dismiss": "Opusti", - "label.does-not-contain": "Does not contain", + "label.dismiss": "Prezri", + "label.does-not-contain": "Ne vsebuje", "label.domain": "Domena", - "label.dropoff": "Dropoff", + "label.dropoff": "Zapustitev", "label.edit": "Uredi", - "label.edit-dashboard": "Edit dashboard", - "label.enable-share-url": "Omogoči URL za skupno rabo", - "label.event": "Event", - "label.event-data": "Event data", + "label.edit-dashboard": "Uredi nadzorno ploščo", + "label.enable-share-url": "Uredi povezavo za deljenje", + "label.event": "Dogodek", + "label.event-data": "Podatki dogodka", "label.events": "Dogodki", - "label.false": "False", - "label.field": "Field", - "label.fields": "Fields", - "label.filter-combined": "Skupno", - "label.filter-raw": "Neobdelane meritve", - "label.filters": "Filters", - "label.funnel": "Funnel", - "label.greater-than": "Greater than", - "label.greater-than-equals": "Greater than or equals", - "label.insights": "Insights", - "label.is": "Is", - "label.is-not": "Is not", - "label.is-not-set": "Is not set", - "label.is-set": "Is set", - "label.join": "Join", - "label.join-team": "Join team", - "label.language": "Language", - "label.languages": "Languages", + "label.false": "Napačno", + "label.field": "Polje", + "label.fields": "Polja", + "label.filter-combined": "Skupaj", + "label.filter-raw": "Neobdelano", + "label.filters": "Filtri", + "label.funnel": "Prodajni lijak", + "label.greater-than": "Večje od", + "label.greater-than-equals": "Večje ali enako kot", + "label.insights": "Vpogled", + "label.is": "Je", + "label.is-not": "Ni", + "label.is-not-set": "Ni nastavljeno", + "label.is-set": "Je nastavljeno", + "label.join": "Pridruži se", + "label.join-team": "Pridruži se ekipi", + "label.language": "Jezik", + "label.languages": "Jeziki", "label.laptop": "Prenosni računalnik", "label.last-days": "Zadnjih {x} dni", "label.last-hours": "Zadnjih {x} ur", - "label.leave": "Leave", - "label.leave-team": "Leave team", - "label.less-than": "Less than", - "label.less-than-equals": "Less than or equals", + "label.leave": "Zapusti", + "label.leave-team": "Zapusti ekipo", + "label.less-than": "Manjše kot", + "label.less-than-equals": "Manjše ali enako kot", "label.login": "Prijava", "label.logout": "Odjava", - "label.max": "Max", - "label.members": "Members", - "label.min": "Min", - "label.mobile": "Mobilni telefon", + "label.max": "Največ", + "label.members": "Člani", + "label.min": "Najmanj", + "label.mobile": "Mobilne naprave", "label.more": "Več", - "label.my-websites": "My websites", + "label.my-websites": "Moja spletna mesta", "label.name": "Ime", "label.new-password": "Novo geslo", - "label.none": "None", + "label.none": "Brez", "label.os": "OS", - "label.overview": "Overview", - "label.owner": "Owner", - "label.page-of": "Page {current} of {total}", - "label.page-views": "Ogledi strani", - "label.pageTitle": "Page title", + "label.overview": "Pregled", + "label.owner": "Lastnik", + "label.page-of": "Stran {current} od {total}", + "label.page-views": "Obiski strani", + "label.pageTitle": "Naslov strani", "label.pages": "Strani", "label.password": "Geslo", - "label.powered-by": "Zagotavlja {name}", + "label.powered-by": "Poganja {name}", "label.profile": "Profil", - "label.queries": "Queries", - "label.query": "Query", - "label.query-parameters": "Query parameters", - "label.realtime": "V realnem času", - "label.referrer": "Referrer", + "label.queries": "Poizvedbe", + "label.query": "Poizvedba", + "label.query-parameters": "Parametri poizvedbe", + "label.realtime": "V živo", + "label.referrer": "Vir", "label.referrers": "Viri", "label.refresh": "Osveži", - "label.regenerate": "Regenerate", - "label.region": "Region", - "label.regions": "Regions", - "label.remove": "Remove", - "label.reports": "Reports", + "label.regenerate": "Ponovno generiraj", + "label.region": "Regija", + "label.regions": "Regije", + "label.remove": "Odstrani", + "label.reports": "Poročila", "label.required": "Zahtevano", "label.reset": "Ponastavi", - "label.reset-website": "Reset statistics", - "label.retention": "Retention", - "label.role": "Role", - "label.run-query": "Run query", + "label.reset-website": "Ponastavi statistiko", + "label.retention": "Ohranjanje uporabnikov", + "label.role": "Vloga", + "label.run-query": "Izvedi poizvedbo", "label.save": "Shrani", - "label.screens": "Screens", - "label.select-date": "Select date", - "label.select-website": "Select website", - "label.sessions": "Sessions", + "label.screens": "Zasloni", + "label.select-date": "Izberi datum", + "label.select-website": "Izberi spletno mesto", + "label.sessions": "Seje", "label.settings": "Nastavitve", - "label.share-url": "Deli URL", + "label.share-url": "Deli povezavo", "label.single-day": "En dan", - "label.sum": "Sum", + "label.sum": "Seštevek", "label.tablet": "Tablični računalnik", - "label.team": "Team", - "label.team-guest": "Team guest", - "label.team-id": "Team ID", - "label.team-member": "Team member", - "label.team-name": "Team name", - "label.team-owner": "Team owner", - "label.team-websites": "Team websites", - "label.teams": "Teams", - "label.theme": "Theme", + "label.team": "Ekipa", + "label.team-guest": "Gost ekipe", + "label.team-id": "ID ekipe", + "label.team-member": "Član ekipe", + "label.team-name": "Ime ekipe", + "label.team-owner": "Lastnik ekipe", + "label.team-websites": "Spletna mesta ekipe", + "label.teams": "Ekipe", + "label.theme": "Tema", "label.this-month": "Ta mesec", "label.this-week": "Ta teden", - "label.this-year": "Letos", + "label.this-year": "To leto", "label.timezone": "Časovni pas", - "label.title": "Title", + "label.title": "Naslov", "label.today": "Danes", - "label.toggle-charts": "Toggle charts", - "label.total": "Total", - "label.total-records": "Total records", + "label.toggle-charts": "Preklopi grafe", + "label.total": "Skupaj", + "label.total-records": "Skupni zapisi", "label.tracking-code": "Koda za sledenje", - "label.true": "True", - "label.type": "Type", - "label.unique": "Unique", + "label.true": "Pravilno", + "label.type": "Vrsta", + "label.unique": "Unikatni", "label.unique-visitors": "Unikatni obiskovalci", "label.unknown": "Neznano", - "label.untitled": "Untitled", - "label.url": "URL", - "label.urls": "URLs", - "label.user": "User", + "label.untitled": "Brez naslova", + "label.url": "Povezava", + "label.urls": "Povezave", + "label.user": "Uporabnik", "label.username": "Uporabniško ime", - "label.users": "Users", - "label.value": "Value", - "label.view": "View", - "label.view-details": "Prikaži podrobnosti", - "label.view-only": "View only", - "label.views": "Ogledi", + "label.users": "Uporabniki", + "label.value": "Vrednost", + "label.view": "Poglej", + "label.view-details": "Poglej podrobnosti", + "label.view-only": "Samo ogledovanje", + "label.views": "Obiski", "label.visitors": "Obiskovalci", - "label.website": "Website", - "label.website-id": "Website ID", - "label.websites": "Spletna mesta", - "label.window": "Window", - "label.yesterday": "Yesterday", - "message.active-users": "{x} trenutni {x, plural, one {obiskovalec} other {obiskovalcev}}", + "label.website": "Spletno mesto", + "label.website-id": "ID spletnega mesta", + "label.websites": "Spletnih mest", + "label.window": "Okno", + "label.yesterday": "Včeraj", + "message.active-users": "{x} trenutni {x, plural, one {visitor} ostali {visitors}}", "message.confirm-delete": "Ste prepričani, da želite izbrisati {target}?", - "message.confirm-leave": "Are you sure you want to leave {target}?", - "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", - "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", - "message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", - "message.delete-website-warning": "Izbrisani bodo tudi vsi povezani podatki.", - "message.error": "Prišlo je do napake.", - "message.event-log": "{event} on {url}", + "message.confirm-leave": "Ste prepričani, da želite zapustiti {target}?", + "message.confirm-reset": "Ste prepričani, da želite ponastaviti statistiko {target}?", + "message.delete-account": "Za potrditev izbrisa tega računa vnesite {confirmation} v spodnje polje.", + "message.delete-website": "Za potrditev izbrisa tega spletnega mesta vnesite {confirmation} v spodnje polje.", + "message.delete-website-warning": "Izbrisani bodo tudi vsi pripadajoči podatki.", + "message.error": "Nekaj je šlo narobe.", + "message.event-log": "{event} na {url}", "message.go-to-settings": "Pojdi v nastavitve", - "message.incorrect-username-password": "Nepravilno uporabniško ime/geslo", + "message.incorrect-username-password": "Nepravilno uporabniško ime/geslo.", "message.invalid-domain": "Neveljavna domena", - "message.min-password-length": "Minimum length of {n} characters", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.min-password-length": "Najmanjša dolžina je {n} znakov", + "message.new-version-available": "Na voljo je nova verzija programa Umami {version}!", "message.no-data-available": "Podatki niso na voljo.", - "message.no-event-data": "No event data is available.", + "message.no-event-data": "Podatki o dogodku niso na voljo.", "message.no-match-password": "Gesli se ne ujemata", - "message.no-results-found": "No results were found.", - "message.no-team-websites": "This team does not have any websites.", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", - "message.no-websites-configured": "Ni nastavljenih spletnih mest.", + "message.no-results-found": "Rezultatov ni bilo mogoče najti.", + "message.no-team-websites": "Ta ekipa nima spletnih mest.", + "message.no-teams": "Niste še ustvarili nobene ekipe.", + "message.no-users": "Ni uporabnikov.", + "message.no-websites-configured": "Nimate nastavljenih nobenih spletnih mest.", "message.page-not-found": "Stran ni bila najdena.", - "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", - "message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", + "message.reset-website": "Za ponastavitev izbrisa tega spletnega mesta vnesite {confirmation} v spodnje polje.", + "message.reset-website-warning": "Vse statistike za to spletno mesto bodo izbrisane, koda za sledenje pa bo ostala nespremenjena.", "message.saved": "Uspešno shranjeno.", - "message.share-url": "To je javno dostopen naslov URL za {target}.", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", - "message.team-websites-info": "Websites can be viewed by anyone on the team.", + "message.share-url": "To je javno dostopna povezava za {target}.", + "message.team-already-member": "Ste že član ekipe.", + "message.team-not-found": "Ekipa ni bila najdena.", + "message.team-websites-info": "Spletne strani si lahko ogleda vsak član ekipe.", "message.tracking-code": "Koda za sledenje", - "message.user-deleted": "User deleted.", + "message.user-deleted": "Uporabnik je izbrisan.", "message.visitor-log": "Obiskovalec iz {country} uporablja {browser} na {os} {device}" } From 338d1c62ea7c978beeb6570793d9be70a214487b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 31 Aug 2023 14:32:59 -0700 Subject: [PATCH 161/357] Updated language files. --- public/intl/messages/sl-SI.json | 318 ++++++++++++++++---------------- src/lang/sl-SI.json | 2 +- 2 files changed, 160 insertions(+), 160 deletions(-) diff --git a/public/intl/messages/sl-SI.json b/public/intl/messages/sl-SI.json index a3af95cb8..ee62a3153 100644 --- a/public/intl/messages/sl-SI.json +++ b/public/intl/messages/sl-SI.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "Koda za dostop" } ], "label.actions": [ @@ -14,19 +14,19 @@ "label.activity-log": [ { "type": 0, - "value": "Activity log" + "value": "Dnevnik dejavnosti" } ], "label.add": [ { "type": 0, - "value": "Add" + "value": "Dodaj" } ], "label.add-description": [ { "type": 0, - "value": "Add description" + "value": "Dodaj opis" } ], "label.add-website": [ @@ -44,31 +44,31 @@ "label.after": [ { "type": 0, - "value": "After" + "value": "Po" } ], "label.all": [ { "type": 0, - "value": "Vse" + "value": "Vsi" } ], "label.all-time": [ { "type": 0, - "value": "All time" + "value": "Ves čas" } ], "label.analytics": [ { "type": 0, - "value": "Analytics" + "value": "Analitika" } ], "label.average": [ { "type": 0, - "value": "Average" + "value": "Povprečno" } ], "label.average-visit-time": [ @@ -86,25 +86,25 @@ "label.before": [ { "type": 0, - "value": "Before" + "value": "Pred" } ], "label.bounce-rate": [ { "type": 0, - "value": "Zapustna stopnja" + "value": "Odbojna stopnja" } ], "label.breakdown": [ { "type": 0, - "value": "Breakdown" + "value": "Razčlenitev" } ], "label.browser": [ { "type": 0, - "value": "Browser" + "value": "Brskalnik" } ], "label.browsers": [ @@ -128,43 +128,43 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "Mesta" } ], "label.city": [ { "type": 0, - "value": "City" + "value": "Mesto" } ], "label.clear-all": [ { "type": 0, - "value": "Clear all" + "value": "Počisti vse" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "Potrdi" } ], "label.confirm-password": [ { "type": 0, - "value": "Potrditev gesla" + "value": "Potrdi geslo" } ], "label.contains": [ { "type": 0, - "value": "Contains" + "value": "Vsebuje" } ], "label.continue": [ { "type": 0, - "value": "Continue" + "value": "Nadaljuj" } ], "label.countries": [ @@ -176,31 +176,31 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "Država" } ], "label.create-report": [ { "type": 0, - "value": "Create report" + "value": "Ustvari poročilo" } ], "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "Ustvari ekipo" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "Ustvari uporabnika" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "Ustvarjeno" } ], "label.current-password": [ @@ -212,7 +212,7 @@ "label.custom-range": [ { "type": 0, - "value": "Razpon po meri" + "value": "Obdobje po meri" } ], "label.dashboard": [ @@ -224,31 +224,31 @@ "label.data": [ { "type": 0, - "value": "Data" + "value": "Podatki" } ], "label.date": [ { "type": 0, - "value": "Date" + "value": "Datum" } ], "label.date-range": [ { "type": 0, - "value": "Časovni razpon" + "value": "Časovno obdobje" } ], "label.day": [ { "type": 0, - "value": "Day" + "value": "Dan" } ], "label.default-date-range": [ { "type": 0, - "value": "Privzeti časovni razpon" + "value": "Privzeto časovno obdobje" } ], "label.delete": [ @@ -260,13 +260,13 @@ "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "Izbriši ekipo" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "Izbriši uporabnika" } ], "label.delete-website": [ @@ -278,7 +278,7 @@ "label.description": [ { "type": 0, - "value": "Description" + "value": "Opis" } ], "label.desktop": [ @@ -290,13 +290,13 @@ "label.details": [ { "type": 0, - "value": "Details" + "value": "Podrobnosti" } ], "label.device": [ { "type": 0, - "value": "Device" + "value": "Naprava" } ], "label.devices": [ @@ -308,13 +308,13 @@ "label.dismiss": [ { "type": 0, - "value": "Opusti" + "value": "Prezri" } ], "label.does-not-contain": [ { "type": 0, - "value": "Does not contain" + "value": "Ne vsebuje" } ], "label.domain": [ @@ -326,7 +326,7 @@ "label.dropoff": [ { "type": 0, - "value": "Dropoff" + "value": "Zapustitev" } ], "label.edit": [ @@ -338,25 +338,25 @@ "label.edit-dashboard": [ { "type": 0, - "value": "Edit dashboard" + "value": "Uredi nadzorno ploščo" } ], "label.enable-share-url": [ { "type": 0, - "value": "Omogoči URL za skupno rabo" + "value": "Uredi povezavo za deljenje" } ], "label.event": [ { "type": 0, - "value": "Event" + "value": "Dogodek" } ], "label.event-data": [ { "type": 0, - "value": "Event data" + "value": "Podatki dogodka" } ], "label.events": [ @@ -368,109 +368,109 @@ "label.false": [ { "type": 0, - "value": "False" + "value": "Napačno" } ], "label.field": [ { "type": 0, - "value": "Field" + "value": "Polje" } ], "label.fields": [ { "type": 0, - "value": "Fields" + "value": "Polja" } ], "label.filter-combined": [ { "type": 0, - "value": "Skupno" + "value": "Skupaj" } ], "label.filter-raw": [ { "type": 0, - "value": "Neobdelane meritve" + "value": "Neobdelano" } ], "label.filters": [ { "type": 0, - "value": "Filters" + "value": "Filtri" } ], "label.funnel": [ { "type": 0, - "value": "Funnel" + "value": "Prodajni lijak" } ], "label.greater-than": [ { "type": 0, - "value": "Greater than" + "value": "Večje od" } ], "label.greater-than-equals": [ { "type": 0, - "value": "Greater than or equals" + "value": "Večje ali enako kot" } ], "label.insights": [ { "type": 0, - "value": "Insights" + "value": "Vpogled" } ], "label.is": [ { "type": 0, - "value": "Is" + "value": "Je" } ], "label.is-not": [ { "type": 0, - "value": "Is not" + "value": "Ni" } ], "label.is-not-set": [ { "type": 0, - "value": "Is not set" + "value": "Ni nastavljeno" } ], "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "Je nastavljeno" } ], "label.join": [ { "type": 0, - "value": "Join" + "value": "Pridruži se" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "Pridruži se ekipi" } ], "label.language": [ { "type": 0, - "value": "Language" + "value": "Jezik" } ], "label.languages": [ { "type": 0, - "value": "Languages" + "value": "Jeziki" } ], "label.laptop": [ @@ -510,25 +510,25 @@ "label.leave": [ { "type": 0, - "value": "Leave" + "value": "Zapusti" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "Zapusti ekipo" } ], "label.less-than": [ { "type": 0, - "value": "Less than" + "value": "Manjše kot" } ], "label.less-than-equals": [ { "type": 0, - "value": "Less than or equals" + "value": "Manjše ali enako kot" } ], "label.login": [ @@ -546,25 +546,25 @@ "label.max": [ { "type": 0, - "value": "Max" + "value": "Največ" } ], "label.members": [ { "type": 0, - "value": "Members" + "value": "Člani" } ], "label.min": [ { "type": 0, - "value": "Min" + "value": "Najmanj" } ], "label.mobile": [ { "type": 0, - "value": "Mobilni telefon" + "value": "Mobilne naprave" } ], "label.more": [ @@ -576,7 +576,7 @@ "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "Moja spletna mesta" } ], "label.name": [ @@ -594,7 +594,7 @@ "label.none": [ { "type": 0, - "value": "None" + "value": "Brez" } ], "label.os": [ @@ -606,19 +606,19 @@ "label.overview": [ { "type": 0, - "value": "Overview" + "value": "Pregled" } ], "label.owner": [ { "type": 0, - "value": "Owner" + "value": "Lastnik" } ], "label.page-of": [ { "type": 0, - "value": "Page " + "value": "Stran " }, { "type": 1, @@ -626,7 +626,7 @@ }, { "type": 0, - "value": " of " + "value": " od " }, { "type": 1, @@ -636,13 +636,13 @@ "label.page-views": [ { "type": 0, - "value": "Ogledi strani" + "value": "Obiski strani" } ], "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "Naslov strani" } ], "label.pages": [ @@ -660,7 +660,7 @@ "label.powered-by": [ { "type": 0, - "value": "Zagotavlja " + "value": "Poganja " }, { "type": 1, @@ -676,31 +676,31 @@ "label.queries": [ { "type": 0, - "value": "Queries" + "value": "Poizvedbe" } ], "label.query": [ { "type": 0, - "value": "Query" + "value": "Poizvedba" } ], "label.query-parameters": [ { "type": 0, - "value": "Query parameters" + "value": "Parametri poizvedbe" } ], "label.realtime": [ { "type": 0, - "value": "V realnem času" + "value": "V živo" } ], "label.referrer": [ { "type": 0, - "value": "Referrer" + "value": "Vir" } ], "label.referrers": [ @@ -718,31 +718,31 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "Ponovno generiraj" } ], "label.region": [ { "type": 0, - "value": "Region" + "value": "Regija" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Regije" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "Odstrani" } ], "label.reports": [ { "type": 0, - "value": "Reports" + "value": "Poročila" } ], "label.required": [ @@ -760,25 +760,25 @@ "label.reset-website": [ { "type": 0, - "value": "Reset statistics" + "value": "Ponastavi statistiko" } ], "label.retention": [ { "type": 0, - "value": "Retention" + "value": "Ohranjanje uporabnikov" } ], "label.role": [ { "type": 0, - "value": "Role" + "value": "Vloga" } ], "label.run-query": [ { "type": 0, - "value": "Run query" + "value": "Izvedi poizvedbo" } ], "label.save": [ @@ -790,25 +790,25 @@ "label.screens": [ { "type": 0, - "value": "Screens" + "value": "Zasloni" } ], "label.select-date": [ { "type": 0, - "value": "Select date" + "value": "Izberi datum" } ], "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "Izberi spletno mesto" } ], "label.sessions": [ { "type": 0, - "value": "Sessions" + "value": "Seje" } ], "label.settings": [ @@ -820,7 +820,7 @@ "label.share-url": [ { "type": 0, - "value": "Deli URL" + "value": "Deli povezavo" } ], "label.single-day": [ @@ -832,7 +832,7 @@ "label.sum": [ { "type": 0, - "value": "Sum" + "value": "Seštevek" } ], "label.tablet": [ @@ -844,55 +844,55 @@ "label.team": [ { "type": 0, - "value": "Team" + "value": "Ekipa" } ], "label.team-guest": [ { "type": 0, - "value": "Team guest" + "value": "Gost ekipe" } ], "label.team-id": [ { "type": 0, - "value": "Team ID" + "value": "ID ekipe" } ], "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "Član ekipe" } ], "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "Ime ekipe" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "Lastnik ekipe" } ], "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "Spletna mesta ekipe" } ], "label.teams": [ { "type": 0, - "value": "Teams" + "value": "Ekipe" } ], "label.theme": [ { "type": 0, - "value": "Theme" + "value": "Tema" } ], "label.this-month": [ @@ -910,7 +910,7 @@ "label.this-year": [ { "type": 0, - "value": "Letos" + "value": "To leto" } ], "label.timezone": [ @@ -922,7 +922,7 @@ "label.title": [ { "type": 0, - "value": "Title" + "value": "Naslov" } ], "label.today": [ @@ -934,19 +934,19 @@ "label.toggle-charts": [ { "type": 0, - "value": "Toggle charts" + "value": "Preklopi grafe" } ], "label.total": [ { "type": 0, - "value": "Total" + "value": "Skupaj" } ], "label.total-records": [ { "type": 0, - "value": "Total records" + "value": "Skupni zapisi" } ], "label.tracking-code": [ @@ -958,19 +958,19 @@ "label.true": [ { "type": 0, - "value": "True" + "value": "Pravilno" } ], "label.type": [ { "type": 0, - "value": "Type" + "value": "Vrsta" } ], "label.unique": [ { "type": 0, - "value": "Unique" + "value": "Unikatni" } ], "label.unique-visitors": [ @@ -988,25 +988,25 @@ "label.untitled": [ { "type": 0, - "value": "Untitled" + "value": "Brez naslova" } ], "label.url": [ { "type": 0, - "value": "URL" + "value": "Povezava" } ], "label.urls": [ { "type": 0, - "value": "URLs" + "value": "Povezave" } ], "label.user": [ { "type": 0, - "value": "User" + "value": "Uporabnik" } ], "label.username": [ @@ -1018,37 +1018,37 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "Uporabniki" } ], "label.value": [ { "type": 0, - "value": "Value" + "value": "Vrednost" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "Poglej" } ], "label.view-details": [ { "type": 0, - "value": "Prikaži podrobnosti" + "value": "Poglej podrobnosti" } ], "label.view-only": [ { "type": 0, - "value": "View only" + "value": "Samo ogledovanje" } ], "label.views": [ { "type": 0, - "value": "Ogledi" + "value": "Obiski" } ], "label.visitors": [ @@ -1060,31 +1060,31 @@ "label.website": [ { "type": 0, - "value": "Website" + "value": "Spletno mesto" } ], "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "ID spletnega mesta" } ], "label.websites": [ { "type": 0, - "value": "Spletna mesta" + "value": "Spletnih mest" } ], "label.window": [ { "type": 0, - "value": "Window" + "value": "Okno" } ], "label.yesterday": [ { "type": 0, - "value": "Yesterday" + "value": "Včeraj" } ], "message.active-users": [ @@ -1138,7 +1138,7 @@ "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "Ste prepričani, da želite zapustiti " }, { "type": 1, @@ -1152,7 +1152,7 @@ "message.confirm-reset": [ { "type": 0, - "value": "Are your sure you want to reset " + "value": "Ste prepričani, da želite ponastaviti statistiko " }, { "type": 1, @@ -1160,13 +1160,13 @@ }, { "type": 0, - "value": "'s statistics?" + "value": "?" } ], "message.delete-account": [ { "type": 0, - "value": "To delete this account, type " + "value": "Za potrditev izbrisa tega računa vnesite " }, { "type": 1, @@ -1174,13 +1174,13 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " v spodnje polje." } ], "message.delete-website": [ { "type": 0, - "value": "To delete this website, type " + "value": "Za potrditev izbrisa tega spletnega mesta vnesite " }, { "type": 1, @@ -1188,19 +1188,19 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " v spodnje polje." } ], "message.delete-website-warning": [ { "type": 0, - "value": "Izbrisani bodo tudi vsi povezani podatki." + "value": "Izbrisani bodo tudi vsi pripadajoči podatki." } ], "message.error": [ { "type": 0, - "value": "Prišlo je do napake." + "value": "Nekaj je šlo narobe." } ], "message.event-log": [ @@ -1210,7 +1210,7 @@ }, { "type": 0, - "value": " on " + "value": " na " }, { "type": 1, @@ -1226,7 +1226,7 @@ "message.incorrect-username-password": [ { "type": 0, - "value": "Nepravilno uporabniško ime/geslo" + "value": "Nepravilno uporabniško ime/geslo." } ], "message.invalid-domain": [ @@ -1238,7 +1238,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "Najmanjša dolžina je " }, { "type": 1, @@ -1246,13 +1246,13 @@ }, { "type": 0, - "value": " characters" + "value": " znakov" } ], "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "Na voljo je nova verzija programa Umami " }, { "type": 1, @@ -1260,7 +1260,7 @@ }, { "type": 0, - "value": " is available!" + "value": "!" } ], "message.no-data-available": [ @@ -1272,7 +1272,7 @@ "message.no-event-data": [ { "type": 0, - "value": "No event data is available." + "value": "Podatki o dogodku niso na voljo." } ], "message.no-match-password": [ @@ -1284,31 +1284,31 @@ "message.no-results-found": [ { "type": 0, - "value": "No results were found." + "value": "Rezultatov ni bilo mogoče najti." } ], "message.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "Ta ekipa nima spletnih mest." } ], "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "Niste še ustvarili nobene ekipe." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "Ni uporabnikov." } ], "message.no-websites-configured": [ { "type": 0, - "value": "Ni nastavljenih spletnih mest." + "value": "Nimate nastavljenih nobenih spletnih mest." } ], "message.page-not-found": [ @@ -1320,7 +1320,7 @@ "message.reset-website": [ { "type": 0, - "value": "To reset this website, type " + "value": "Za ponastavitev izbrisa tega spletnega mesta vnesite " }, { "type": 1, @@ -1328,13 +1328,13 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " v spodnje polje." } ], "message.reset-website-warning": [ { "type": 0, - "value": "All statistics for this website will be deleted, but your tracking code will remain intact." + "value": "Vse statistike za to spletno mesto bodo izbrisane, koda za sledenje pa bo ostala nespremenjena." } ], "message.saved": [ @@ -1346,7 +1346,7 @@ "message.share-url": [ { "type": 0, - "value": "To je javno dostopen naslov URL za " + "value": "To je javno dostopna povezava za " }, { "type": 1, @@ -1360,19 +1360,19 @@ "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "Ste že član ekipe." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "Ekipa ni bila najdena." } ], "message.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "Spletne strani si lahko ogleda vsak član ekipe." } ], "message.tracking-code": [ @@ -1384,7 +1384,7 @@ "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "Uporabnik je izbrisan." } ], "message.visitor-log": [ diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index 4e7e4ad95..440b9fcad 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -175,7 +175,7 @@ "label.websites": "Spletnih mest", "label.window": "Okno", "label.yesterday": "Včeraj", - "message.active-users": "{x} trenutni {x, plural, one {visitor} ostali {visitors}}", + "message.active-users": "{x} trenutni {x, plural, one {obiskovalec} other {obiskovalcev}}", "message.confirm-delete": "Ste prepričani, da želite izbrisati {target}?", "message.confirm-leave": "Ste prepričani, da želite zapustiti {target}?", "message.confirm-reset": "Ste prepričani, da želite ponastaviti statistiko {target}?", From b652ef3f7391ebdb5c1c7d947da5fa3d92401a2d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 31 Aug 2023 14:33:39 -0700 Subject: [PATCH 162/357] Bump version v2.6.2. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9745b780e..9759b2b88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.6.1", + "version": "2.6.2", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", From 251a5e5a076ac5b601957aeb6813f8b45f22565f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 1 Sep 2023 09:26:10 +0200 Subject: [PATCH 163/357] i18n: update de-DE translations --- src/lang/de-DE.json | 110 ++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/src/lang/de-DE.json b/src/lang/de-DE.json index 3307dfa40..ba797bbb4 100644 --- a/src/lang/de-DE.json +++ b/src/lang/de-DE.json @@ -2,18 +2,18 @@ "label.access-code": "Zugangscode", "label.actions": "Aktionen", "label.activity-log": "Aktivitätsverlauf", - "label.add": "Add", + "label.add": "Hinzufügen", "label.add-description": "Beschreibung hinzufügen", - "label.add-website": "Webseite hinzufügen", + "label.add-website": "Website hinzufügen", "label.admin": "Administrator", - "label.after": "After", + "label.after": "Nach", "label.all": "Alle", "label.all-time": "Gesamter Zeitraum", "label.analytics": "Analytics", "label.average": "Durchschnitt", "label.average-visit-time": "Durchschn. Besuchszeit", "label.back": "Zurück", - "label.before": "Before", + "label.before": "Vor", "label.bounce-rate": "Absprungrate", "label.breakdown": "Breakdown", "label.browser": "Browser", @@ -21,15 +21,15 @@ "label.cancel": "Abbrechen", "label.change-password": "Passwort ändern", "label.cities": "Städte", - "label.city": "City", + "label.city": "Stadt", "label.clear-all": "Alles löschen", "label.confirm": "Bestätigen", "label.confirm-password": "Passwort wiederholen", - "label.contains": "Contains", + "label.contains": "Enthält", "label.continue": "Weiter", "label.countries": "Länder", - "label.country": "Country", - "label.create-report": "Report erstellen", + "label.country": "Land", + "label.create-report": "Bericht erstellen", "label.create-team": "Team erstellen", "label.create-user": "Benutzer erstellen", "label.created": "Erstellt", @@ -37,43 +37,43 @@ "label.custom-range": "Benutzerdefinierter Bereich", "label.dashboard": "Übersicht", "label.data": "Daten", - "label.date": "Date", + "label.date": "Datum", "label.date-range": "Datumsbereich", - "label.day": "Day", + "label.day": "Tag", "label.default-date-range": "Voreingestellter Datumsbereich", "label.delete": "Löschen", "label.delete-team": "Team löschen", "label.delete-user": "Benutzer löschen", - "label.delete-website": "Webseite löschen", + "label.delete-website": "Website löschen", "label.description": "Beschreibung", "label.desktop": "Desktop", "label.details": "Details", - "label.device": "Device", + "label.device": "Gerät", "label.devices": "Geräte", "label.dismiss": "Verwerfen", - "label.does-not-contain": "Does not contain", + "label.does-not-contain": "Enthält nicht", "label.domain": "Domain", "label.dropoff": "Dropoff", "label.edit": "Bearbeiten", "label.edit-dashboard": "Dashboard bearbeiten", "label.enable-share-url": "Freigabe-URL aktivieren", "label.event": "Event", - "label.event-data": "Event daten", + "label.event-data": "Eventdaten", "label.events": "Ereignisse", - "label.false": "False", - "label.field": "Field", - "label.fields": "Fields", + "label.false": "Falsch", + "label.field": "Feld", + "label.fields": "Felder", "label.filter-combined": "Kombiniert", "label.filter-raw": "Rohdaten", - "label.filters": "Filters", + "label.filters": "Filter", "label.funnel": "Funnel", "label.greater-than": "Größer als", "label.greater-than-equals": "Größer oder gleich", "label.insights": "Insights", - "label.is": "Is", - "label.is-not": "Is not", - "label.is-not-set": "Is not set", - "label.is-set": "Is set", + "label.is": "Ist", + "label.is-not": "Ist nicht", + "label.is-not-set": "Ist nicht gesetzt", + "label.is-set": "Ist gesetzt", "label.join": "Beitreten", "label.join-team": "Team beitreten", "label.language": "Sprache", @@ -92,16 +92,16 @@ "label.min": "Min", "label.mobile": "Handy", "label.more": "Mehr", - "label.my-websites": "My websites", + "label.my-websites": "Meine Websites", "label.name": "Name", "label.new-password": "Neues Passwort", "label.none": "Keine", "label.os": "OS", "label.overview": "Übersicht", "label.owner": "Besitzer", - "label.page-of": "Page {current} of {total}", + "label.page-of": "Seite {current} von {total}", "label.page-views": "Seitenaufrufe", - "label.pageTitle": "Page title", + "label.pageTitle": "Seitentitel", "label.pages": "Seiten", "label.password": "Passwort", "label.powered-by": "Betrieben durch {name}", @@ -117,7 +117,7 @@ "label.region": "Region", "label.regions": "Regionen", "label.remove": "Entfernen", - "label.reports": "Reporte", + "label.reports": "Berichte", "label.required": "Erforderlich", "label.reset": "Zurücksetzen", "label.reset-website": "Statistik zurücksetzen", @@ -128,19 +128,19 @@ "label.screens": "Bildschirmauflösungen", "label.select-date": "Datum auswählen", "label.select-website": "Website auswählen", - "label.sessions": "Sessions", + "label.sessions": "Sitzungen", "label.settings": "Einstellungen", "label.share-url": "Freigabe-URL", "label.single-day": "Ein Tag", "label.sum": "Summe", "label.tablet": "Tablet", "label.team": "Team", - "label.team-guest": "Team Gast", - "label.team-id": "Team ID", - "label.team-member": "Team Mitglied", - "label.team-name": "Team name", - "label.team-owner": "Team Eigentümer", - "label.team-websites": "Team websites", + "label.team-guest": "Gast des Teams", + "label.team-id": "Team-ID", + "label.team-member": "Team-Mitglied", + "label.team-name": "Name des Teams", + "label.team-owner": "Team-Eigentümer", + "label.team-websites": "Team-Websites", "label.teams": "Teams", "label.theme": "Thema", "label.this-month": "Diesen Monat", @@ -150,61 +150,61 @@ "label.title": "Titel", "label.today": "Heute", "label.toggle-charts": "Schaubilder umschalten", - "label.total": "Total", - "label.total-records": "Total records", + "label.total": "Gesamt", + "label.total-records": "Datensätze insgesamt", "label.tracking-code": "Tracking Code", - "label.true": "True", - "label.type": "Type", - "label.unique": "Unique", + "label.true": "Wahr", + "label.type": "Typ", + "label.unique": "Eindeutig", "label.unique-visitors": "Eindeutige Besucher", "label.unknown": "Unbekannt", - "label.untitled": "Untitled", + "label.untitled": "Unbenannt", "label.url": "URL", "label.urls": "URLs", "label.user": "Benutzer", "label.username": "Benutzername", "label.users": "Benutzer", - "label.value": "Value", + "label.value": "Wert", "label.view": "Anzeigen", "label.view-details": "Details anzeigen", - "label.view-only": "View only", + "label.view-only": "Nur ansehen", "label.views": "Aufrufe", "label.visitors": "Besucher", - "label.website": "Webseite", - "label.website-id": "Webseite ID", - "label.websites": "Webseiten", - "label.window": "Window", + "label.website": "Website", + "label.website-id": "Website ID", + "label.websites": "Websites", + "label.window": "Fenster", "label.yesterday": "Gestern", "message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}", "message.confirm-delete": "Sind Sie sich sicher, {target} zu löschen?", "message.confirm-leave": "Sind Sie sicher, dass die {target} verlassen möchten?", "message.confirm-reset": "Sind Sie sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?", - "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", - "message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", + "message.delete-account": "Um dieses Konto zu löschen, geben Sie zur Bestätigung {confirmation} in das Feld unten ein.", + "message.delete-website": "Um diese Website zu löschen, geben Sie zur Bestätigung {confirmation} in das Feld unten ein.", "message.delete-website-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.", "message.error": "Es ist ein Fehler aufgetreten.", "message.event-log": "{event} auf {url}", "message.go-to-settings": "Zu den Einstellungen", "message.incorrect-username-password": "Falsches Passwort oder Benutzername.", "message.invalid-domain": "Ungültige Domain", - "message.min-password-length": "Minimale länge von {n} Zeichen", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.min-password-length": "Minimale Länge von {n} Zeichen", + "message.new-version-available": "Eine neue Version von Umami ist verfügbar: {version}", "message.no-data-available": "Keine Daten vorhanden.", - "message.no-event-data": "No event data is available.", + "message.no-event-data": "Es sind keine Ereignisdaten verfügbar.", "message.no-match-password": "Passwörter stimmen nicht überein", "message.no-results-found": "Keine Ergebnisse gefunden.", "message.no-team-websites": "Diesem Team sind keine Websites zugeordnet.", "message.no-teams": "Bisher wurden keine Teams erstellt.", "message.no-users": "Hier gibt es keine Benutzer.", - "message.no-websites-configured": "Es ist keine Webseite vorhanden.", + "message.no-websites-configured": "Es ist keine Website vorhanden.", "message.page-not-found": "Seite nicht gefunden.", - "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", - "message.reset-website-warning": "Alle Daten für diese Webseite werden gelöscht, jedoch bleibt der Tracking Code bestehen.", + "message.reset-website": "Um diese Website zurückzusetzen, geben Sie zur Bestätigung {confirmation} in das Feld unten ein.", + "message.reset-website-warning": "Alle Daten für diese Website werden gelöscht, jedoch bleibt der Tracking Code bestehen.", "message.saved": "Erfolgreich gespeichert.", - "message.share-url": "Ihre Webseitenstatistik ist unter der folgenden URL öffentlich zugänglich:", + "message.share-url": "Die Statistiken Ihrer Website sind unter folgender URL öffentlich zugänglich:", "message.team-already-member": "Sie sind bereits Mitglied des Teams.", "message.team-not-found": "Team nicht gefunden.", - "message.team-websites-info": "Webseiten können von jedem im Team eingesehen werden.", + "message.team-websites-info": "Websites können von jedem im Team eingesehen werden.", "message.tracking-code": "Tracking Code", "message.user-deleted": "Benutzer gelöscht.", "message.visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}" From be886db0f271a70dd6136ba94d56e1b288f38507 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Thu, 31 Aug 2023 16:32:17 +0000 Subject: [PATCH 164/357] update lang scripts --- package.json | 2 +- scripts/merge-messages.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 9759b2b88..8bb372013 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "check-db": "node scripts/check-db.js", "check-env": "node scripts/check-env.js", "copy-db-files": "node scripts/copy-db-files.js", - "extract-messages": "formatjs extract \"{pages,components}/**/*.js\" --out-file build/messages.json", + "extract-messages": "formatjs extract \"src/{pages,components}/**/*.js\" --out-file build/messages.json", "merge-messages": "node scripts/merge-messages.js", "generate-lang": "npm-run-all extract-messages merge-messages", "format-lang": "node scripts/format-lang.js", diff --git a/scripts/merge-messages.js b/scripts/merge-messages.js index a74153146..572e9a7ed 100644 --- a/scripts/merge-messages.js +++ b/scripts/merge-messages.js @@ -4,7 +4,7 @@ const path = require('path'); const prettier = require('prettier'); const messages = require('../build/messages.json'); -const dest = path.resolve(__dirname, '../lang'); +const dest = path.resolve(__dirname, '../src/lang'); const files = fs.readdirSync(dest); const keys = Object.keys(messages).sort(); @@ -14,7 +14,7 @@ with the existing files under `lang`. Any newly added keys will be printed to the console. */ files.forEach(file => { - const lang = require(`../lang/${file}`); + const lang = require(`../src/lang/${file}`); console.log(`Merging ${file}`); From 440676ef210d451f58110d48c3e57b5487930369 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Fri, 1 Sep 2023 09:14:51 +0000 Subject: [PATCH 165/357] add lang labels --- src/components/common/SettingsTable.js | 4 +- src/components/messages.js | 14 +++ .../pages/reports/ReportTemplates.js | 10 ++- src/lang/am-ET.json | 6 ++ src/lang/ar-SA.json | 6 ++ src/lang/be-BY.json | 6 ++ src/lang/bn-BD.json | 6 ++ src/lang/ca-ES.json | 6 ++ src/lang/cs-CZ.json | 6 ++ src/lang/da-DK.json | 6 ++ src/lang/de-CH.json | 6 ++ src/lang/de-DE.json | 6 ++ src/lang/el-GR.json | 6 ++ src/lang/en-GB.json | 6 ++ src/lang/en-US.json | 6 ++ src/lang/es-ES.json | 6 ++ src/lang/es-MX.json | 6 ++ src/lang/fa-IR.json | 6 ++ src/lang/fi-FI.json | 6 ++ src/lang/fo-FO.json | 6 ++ src/lang/fr-FR.json | 6 ++ src/lang/ga-ES.json | 6 ++ src/lang/he-IL.json | 6 ++ src/lang/hi-IN.json | 6 ++ src/lang/hr-HR.json | 6 ++ src/lang/hu-HU.json | 6 ++ src/lang/id-ID.json | 6 ++ src/lang/it-IT.json | 6 ++ src/lang/ja-JP.json | 6 ++ src/lang/km-KH.json | 6 ++ src/lang/ko-KR.json | 6 ++ src/lang/lt-LT.json | 6 ++ src/lang/mn-MN.json | 6 ++ src/lang/ms-MY.json | 6 ++ src/lang/my-MM.json | 90 ++++++++++++------- src/lang/nb-NO.json | 6 ++ src/lang/nl-NL.json | 6 ++ src/lang/pl-PL.json | 6 ++ src/lang/pt-BR.json | 6 ++ src/lang/pt-PT.json | 6 ++ src/lang/ro-RO.json | 6 ++ src/lang/ru-RU.json | 6 ++ src/lang/si-LK.json | 6 ++ src/lang/sk-SK.json | 6 ++ src/lang/sl-SI.json | 13 ++- src/lang/sv-SE.json | 6 ++ src/lang/ta-IN.json | 6 ++ src/lang/th-TH.json | 6 ++ src/lang/tr-TR.json | 6 ++ src/lang/uk-UA.json | 6 ++ src/lang/ur-PK.json | 6 ++ src/lang/vi-VN.json | 6 ++ src/lang/zh-CN.json | 6 ++ src/lang/zh-TW.json | 6 ++ 54 files changed, 381 insertions(+), 44 deletions(-) diff --git a/src/components/common/SettingsTable.js b/src/components/common/SettingsTable.js index 2df3b391b..a74581cae 100644 --- a/src/components/common/SettingsTable.js +++ b/src/components/common/SettingsTable.js @@ -25,7 +25,7 @@ export function SettingsTable({ onPageSizeChange, filterValue, }) { - const { formatMessage, messages } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); const [filter, setFilter] = useState(filterValue); const { data: value, page, count, pageSize } = data; @@ -42,7 +42,7 @@ export function SettingsTable({ delay={1000} value={filter} autoFocus={true} - placeholder="Search" + placeholder={formatMessage(labels.search)} style={{ maxWidth: '300px', marginBottom: '10px' }} /> )} diff --git a/src/components/messages.js b/src/components/messages.js index f52ed5c5e..01c7e779c 100644 --- a/src/components/messages.js +++ b/src/components/messages.js @@ -129,6 +129,10 @@ export const labels = defineMessages({ reports: { id: 'label.reports', defaultMessage: 'Reports' }, eventData: { id: 'label.event-data', defaultMessage: 'Event data' }, funnel: { id: 'label.funnel', defaultMessage: 'Funnel' }, + funnelDescription: { + id: 'label.funnel-description', + defaultMessage: 'Understand the conversion and drop-off rate of users.', + }, url: { id: 'label.url', defaultMessage: 'URL' }, urls: { id: 'label.urls', defaultMessage: 'URLs' }, add: { id: 'label.add', defaultMessage: 'Add' }, @@ -167,7 +171,15 @@ export const labels = defineMessages({ overview: { id: 'label.overview', defaultMessage: 'Overview' }, totalRecords: { id: 'label.total-records', defaultMessage: 'Total records' }, insights: { id: 'label.insights', defaultMessage: 'Insights' }, + insightsDescription: { + id: 'label.insights-description', + defaultMessage: 'Dive deeper into your data by using segments and filters.', + }, retention: { id: 'label.retention', defaultMessage: 'Retention' }, + retentionDescription: { + id: 'label.retention-description', + defaultMessage: 'Measure you website stickiness by tracking how often users return.', + }, dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' }, referrer: { id: 'label.referrer', defaultMessage: 'Referrer' }, country: { id: 'label.country', defaultMessage: 'Country' }, @@ -179,6 +191,8 @@ export const labels = defineMessages({ day: { id: 'label.day', defaultMessage: 'Day' }, date: { id: 'label.date', defaultMessage: 'Date' }, pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' }, + create: { id: 'label.create', defaultMessage: 'Create' }, + search: { id: 'label.search', defaultMessage: 'Search' }, }); export const messages = defineMessages({ diff --git a/src/components/pages/reports/ReportTemplates.js b/src/components/pages/reports/ReportTemplates.js index 2b934434e..59cc8b310 100644 --- a/src/components/pages/reports/ReportTemplates.js +++ b/src/components/pages/reports/ReportTemplates.js @@ -9,6 +9,8 @@ import styles from './ReportTemplates.module.css'; import { useMessages } from 'components/hooks'; function ReportItem({ title, description, url, icon }) { + const { formatMessage, labels } = useMessages(); + return (
@@ -22,7 +24,7 @@ function ReportItem({ title, description, url, icon }) { - Create + {formatMessage(labels.create)}
@@ -36,19 +38,19 @@ export function ReportTemplates({ showHeader = true }) { const reports = [ { title: formatMessage(labels.insights), - description: 'Dive deeper into your data by using segments and filters.', + description: formatMessage(labels.insightsDescription), url: '/reports/insights', icon: , }, { title: formatMessage(labels.funnel), - description: 'Understand the conversion and drop-off rate of users.', + description: formatMessage(labels.funnelDescription), url: '/reports/funnel', icon: , }, { title: formatMessage(labels.retention), - description: 'Measure you website stickiness by tracking how often users return.', + description: formatMessage(labels.retentionDescription), url: '/reports/retention', icon: , }, diff --git a/src/lang/am-ET.json b/src/lang/am-ET.json index 7bed14236..3765e5ba6 100644 --- a/src/lang/am-ET.json +++ b/src/lang/am-ET.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Countries", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combined", "label.filter-raw": "Raw", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Save", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/ar-SA.json b/src/lang/ar-SA.json index 0efdfee73..ccfee1f33 100644 --- a/src/lang/ar-SA.json +++ b/src/lang/ar-SA.json @@ -29,6 +29,7 @@ "label.continue": "متابعة", "label.countries": "الدول", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "انشاء مجموعة", "label.create-user": "انشاء مستخدم", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "مجمعة", "label.filter-raw": "مفصلة", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "اعادة تعيين", "label.reset-website": "اعادة تعيين الإحصائيات", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "الصلاحية", "label.run-query": "Run query", "label.save": "حفظ", "label.screens": "الشاشات", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "اختيار موقع", "label.sessions": "الزيارات", diff --git a/src/lang/be-BY.json b/src/lang/be-BY.json index 32693ebd4..88f673b9c 100644 --- a/src/lang/be-BY.json +++ b/src/lang/be-BY.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Краіны", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Камбініаваны", "label.filter-raw": "Сырыя", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Скінуць", "label.reset-website": "Скінуць статыстыку", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Захаваць", "label.screens": "Экраны", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/bn-BD.json b/src/lang/bn-BD.json index 483d10080..29e1bb6f6 100644 --- a/src/lang/bn-BD.json +++ b/src/lang/bn-BD.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "দেশ", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "সম্মিলিত", "label.filter-raw": "অপরিশোধিত", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "রিসেট", "label.reset-website": "ওয়েবসাইট রিসেট করুন", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "সংরক্ষণ", "label.screens": "স্ক্রিনগুলি", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/ca-ES.json b/src/lang/ca-ES.json index 51aee79dd..271b9245a 100644 --- a/src/lang/ca-ES.json +++ b/src/lang/ca-ES.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Països", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combinat", "label.filter-raw": "En cru", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Restableix", "label.reset-website": "Restableix estadístiques", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Desa", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/cs-CZ.json b/src/lang/cs-CZ.json index 548ee817d..73bc15b70 100644 --- a/src/lang/cs-CZ.json +++ b/src/lang/cs-CZ.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Země", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Kombinace", "label.filter-raw": "Nezpracované", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Uložit", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/da-DK.json b/src/lang/da-DK.json index 9d4fe50ee..2fba4f7a7 100644 --- a/src/lang/da-DK.json +++ b/src/lang/da-DK.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Lande", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Kombineret", "label.filter-raw": "Rå", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Nulstil", "label.reset-website": "Nulstil statistikker", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Gem", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/de-CH.json b/src/lang/de-CH.json index 5c6c45d14..c07ddd2cb 100644 --- a/src/lang/de-CH.json +++ b/src/lang/de-CH.json @@ -29,6 +29,7 @@ "label.continue": "Wiiter", "label.countries": "Länder", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Team erstelle", "label.create-user": "Benutzer erstelle", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Kombiniert", "label.filter-raw": "Rohdate", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Zruggsetze", "label.reset-website": "Statistik zruggsetze", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Rollä", "label.run-query": "Run query", "label.save": "Speichere", "label.screens": "Bildschirmuflösige", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Websiite uuswähle", "label.sessions": "Sessions", diff --git a/src/lang/de-DE.json b/src/lang/de-DE.json index 3307dfa40..7bac16255 100644 --- a/src/lang/de-DE.json +++ b/src/lang/de-DE.json @@ -29,6 +29,7 @@ "label.continue": "Weiter", "label.countries": "Länder", "label.country": "Country", + "label.create": "Create", "label.create-report": "Report erstellen", "label.create-team": "Team erstellen", "label.create-user": "Benutzer erstellen", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Kombiniert", "label.filter-raw": "Rohdaten", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Größer als", "label.greater-than-equals": "Größer oder gleich", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Zurücksetzen", "label.reset-website": "Statistik zurücksetzen", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Rolle", "label.run-query": "Abfrage starten", "label.save": "Speichern", "label.screens": "Bildschirmauflösungen", + "label.search": "Search", "label.select-date": "Datum auswählen", "label.select-website": "Website auswählen", "label.sessions": "Sessions", diff --git a/src/lang/el-GR.json b/src/lang/el-GR.json index dd95c7772..c5cea1bee 100644 --- a/src/lang/el-GR.json +++ b/src/lang/el-GR.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Χώρες", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Σε συνδυασμό", "label.filter-raw": "Ακατέργαστο", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Επαναφορά", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Αποθήκευση", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/en-GB.json b/src/lang/en-GB.json index 4efaec5df..e93b3b23a 100644 --- a/src/lang/en-GB.json +++ b/src/lang/en-GB.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Countries", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combined", "label.filter-raw": "Raw", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Save", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/en-US.json b/src/lang/en-US.json index b7c77a694..16dfa1888 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Countries", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combined", "label.filter-raw": "Raw", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Reset", "label.reset-website": "Reset website", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Save", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/es-ES.json b/src/lang/es-ES.json index 7a401e51d..001b94064 100644 --- a/src/lang/es-ES.json +++ b/src/lang/es-ES.json @@ -29,6 +29,7 @@ "label.continue": "Continuar", "label.countries": "Países", "label.country": "Country", + "label.create": "Create", "label.create-report": "Crear reporte", "label.create-team": "Crear equipo", "label.create-user": "Crear usuario", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Campo", "label.fields": "Campos", + "label.filter": "Filter", "label.filter-combined": "Combinado", "label.filter-raw": "En crudo", "label.filters": "Filtros", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Mayor que", "label.greater-than-equals": "Mayor que o igual a", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Es igual a", "label.is-not": "No es igual a", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Reiniciar", "label.reset-website": "Reiniciar estadísticas", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Rol", "label.run-query": "Ejecutar consulta", "label.save": "Guardar", "label.screens": "Pantallas", + "label.search": "Search", "label.select-date": "Seleccionar fecha", "label.select-website": "Seleccionar sitio web", "label.sessions": "Sesiones", diff --git a/src/lang/es-MX.json b/src/lang/es-MX.json index 499b2533f..c1dacfa44 100644 --- a/src/lang/es-MX.json +++ b/src/lang/es-MX.json @@ -29,6 +29,7 @@ "label.continue": "Continuar", "label.countries": "Países", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Crear equipo", "label.create-user": "Crear usuario", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combinado", "label.filter-raw": "Personalizado", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Reiniciar", "label.reset-website": "Reiniciar estadísticas", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Rol", "label.run-query": "Run query", "label.save": "Guardar", "label.screens": "Pantallas", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Seleccionar sitio web", "label.sessions": "Sesiones", diff --git a/src/lang/fa-IR.json b/src/lang/fa-IR.json index b263a7d1a..32d985e3e 100644 --- a/src/lang/fa-IR.json +++ b/src/lang/fa-IR.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "کشورها", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "ترکیب شده", "label.filter-raw": "خام", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "بازنشانی", "label.reset-website": "بازنشانی آمار", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "ذخیره", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/fi-FI.json b/src/lang/fi-FI.json index 9e9c1de03..8c447f8a1 100644 --- a/src/lang/fi-FI.json +++ b/src/lang/fi-FI.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Maat", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Yhdistetty", "label.filter-raw": "Käsittelemätön", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Nollaa", "label.reset-website": "Nollaa tilastot", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Tallenna", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/fo-FO.json b/src/lang/fo-FO.json index 6259a5557..e2626f1fb 100644 --- a/src/lang/fo-FO.json +++ b/src/lang/fo-FO.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Lond", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Samansett", "label.filter-raw": "Óviðgjørt", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Nulstilla", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Goym", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/fr-FR.json b/src/lang/fr-FR.json index 558c1cd26..878e078aa 100644 --- a/src/lang/fr-FR.json +++ b/src/lang/fr-FR.json @@ -29,6 +29,7 @@ "label.continue": "Continuer", "label.countries": "Pays", "label.country": "Country", + "label.create": "Create", "label.create-report": "Créer un rapport", "label.create-team": "Créer une équipe", "label.create-user": "Créer un utilisateur", @@ -63,13 +64,16 @@ "label.false": "Faux", "label.field": "Champ", "label.fields": "Champs", + "label.filter": "Filter", "label.filter-combined": "Combiné", "label.filter-raw": "Brut", "label.filters": "Filtres", "label.funnel": "Entonnoir", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Supérieur à", "label.greater-than-equals": "Supérieur ou égal à", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Est égal", "label.is-not": "N'est pas égal", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Réinitialiser", "label.reset-website": "Réinitialiser les statistiques", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Rôle", "label.run-query": "Éxécuter la requête", "label.save": "Enregistrer", "label.screens": "Résolutions d'écran", + "label.search": "Search", "label.select-date": "Choisir une période", "label.select-website": "Choisir un site", "label.sessions": "Sessions", diff --git a/src/lang/ga-ES.json b/src/lang/ga-ES.json index e6ceda8a9..ae46fa2c5 100644 --- a/src/lang/ga-ES.json +++ b/src/lang/ga-ES.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Países", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combinado", "label.filter-raw": "Raw", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Restablecer", "label.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Gardar", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/he-IL.json b/src/lang/he-IL.json index fd3e0b8b8..b953fda60 100644 --- a/src/lang/he-IL.json +++ b/src/lang/he-IL.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "מדינות", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "משותף", "label.filter-raw": "גולמי", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "איפוס", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "שמירה", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/hi-IN.json b/src/lang/hi-IN.json index 6e268aa6e..e296d87bf 100644 --- a/src/lang/hi-IN.json +++ b/src/lang/hi-IN.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "देश", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "संयुक्त", "label.filter-raw": "रॉ", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "रीसेट", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "सहेजें", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/hr-HR.json b/src/lang/hr-HR.json index ecde7100e..75bd8a4f3 100644 --- a/src/lang/hr-HR.json +++ b/src/lang/hr-HR.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Countries", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combined", "label.filter-raw": "Raw", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Resetirati", "label.reset-website": "Resetirati web stranicu", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Spremi", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/hu-HU.json b/src/lang/hu-HU.json index 0401afff7..c6224b01c 100644 --- a/src/lang/hu-HU.json +++ b/src/lang/hu-HU.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Országok", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Összevont", "label.filter-raw": "Nyers", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Visszaállítás", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Mentés", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/id-ID.json b/src/lang/id-ID.json index d0b8a0643..856727650 100644 --- a/src/lang/id-ID.json +++ b/src/lang/id-ID.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Negara", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Gabungan", "label.filter-raw": "Mentah", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Atur ulang", "label.reset-website": "Atur ulang statistik", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Simpan", "label.screens": "Layar", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/it-IT.json b/src/lang/it-IT.json index 57d6d5ba3..8bce2f4e0 100644 --- a/src/lang/it-IT.json +++ b/src/lang/it-IT.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Nazioni", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Aggregati", "label.filter-raw": "Raw", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Reset", "label.reset-website": "Resetta le statistiche", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Salva", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/ja-JP.json b/src/lang/ja-JP.json index 770f6f076..3af2f7b0a 100644 --- a/src/lang/ja-JP.json +++ b/src/lang/ja-JP.json @@ -29,6 +29,7 @@ "label.continue": "続ける", "label.countries": "国名", "label.country": "国", + "label.create": "Create", "label.create-report": "レポートの作成", "label.create-team": "チームの作成", "label.create-user": "ユーザーの作成", @@ -63,13 +64,16 @@ "label.false": "偽", "label.field": "フィールド", "label.fields": "フィールド", + "label.filter": "Filter", "label.filter-combined": "統合", "label.filter-raw": "RAW", "label.filters": "フィルター", "label.funnel": "分析", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "超過", "label.greater-than-equals": "以上", "label.insights": "見通し", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "に等しい", "label.is-not": "に等しくない", "label.is-not-set": "未設定", @@ -122,10 +126,12 @@ "label.reset": "リセット", "label.reset-website": "Webサイトをリセットする", "label.retention": "保持", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "ロール", "label.run-query": "クエリ実行", "label.save": "保存", "label.screens": "画面サイズ", + "label.search": "Search", "label.select-date": "日付を選択", "label.select-website": "Webサイトを選択", "label.sessions": "セッション", diff --git a/src/lang/km-KH.json b/src/lang/km-KH.json index 58f7926f3..3f7d089d1 100644 --- a/src/lang/km-KH.json +++ b/src/lang/km-KH.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "ប្រទេស", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "រួមបញ្ចូលគ្នា", "label.filter-raw": "ដើម", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "កំណត់ឡើងវិញ", "label.reset-website": "កំណត់ស្ថិតិឡើងវិញ", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "រក្សាទុក", "label.screens": "ប្រភេទឧបករណ៍", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/ko-KR.json b/src/lang/ko-KR.json index 767e8e220..dcbdd77c5 100644 --- a/src/lang/ko-KR.json +++ b/src/lang/ko-KR.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "국가", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "합쳐서 보기", "label.filter-raw": "전체 보기", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "리셋", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "저장", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/lt-LT.json b/src/lang/lt-LT.json index c8161f1db..aa75f10f7 100644 --- a/src/lang/lt-LT.json +++ b/src/lang/lt-LT.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Šalys", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Kombinuoti", "label.filter-raw": "Neapdoroti", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Atstatyti", "label.reset-website": "Atstatyti statistikos duomenis", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Išsaugoti", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/mn-MN.json b/src/lang/mn-MN.json index 1478c0792..aad28ef20 100644 --- a/src/lang/mn-MN.json +++ b/src/lang/mn-MN.json @@ -29,6 +29,7 @@ "label.continue": "Үргэлжлүүлэх", "label.countries": "Улс", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Баг үүсгэх", "label.create-user": "Хэрэглэгч үүсгэх", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Нэгтгэсэн", "label.filter-raw": "Түүхий", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Дахин эхлүүлэх", "label.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Эрх", "label.run-query": "Run query", "label.save": "Хадгалах", "label.screens": "Дэлгэц", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Веб сонгох", "label.sessions": "Sessions", diff --git a/src/lang/ms-MY.json b/src/lang/ms-MY.json index 5b8769c58..af5c34f07 100644 --- a/src/lang/ms-MY.json +++ b/src/lang/ms-MY.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Negara", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Digabungkan", "label.filter-raw": "Mentah", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Tetapkan semula", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Simpan", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/my-MM.json b/src/lang/my-MM.json index de92b275d..8253705ab 100644 --- a/src/lang/my-MM.json +++ b/src/lang/my-MM.json @@ -6,21 +6,31 @@ "label.add-description": "အကြောင်းအရာဖော်ပြချက် ထည့်မည်", "label.add-website": "ဝက်ဘ်ဆိုဒ်ထည့်မည်", "label.admin": "အက်ဒမင်", + "label.after": "After", "label.all": "အားလုံး", "label.all-time": "အချိန်အစမှအခုထိ", "label.analytics": "အန်နလစ်တစ်", + "label.average": "Average", "label.average-visit-time": "ဝဘက်ဘ်ဆိုဒ်တွင် ပျမ်းမျှကုန်ဆုံးချိန်", "label.back": "နောက်သို့", + "label.before": "Before", "label.bounce-rate": "Bounce နှုန်း", + "label.breakdown": "Breakdown", + "label.browser": "Browser", "label.browsers": "ဝက်ဘ်ဘရောင်ဇာများ", "label.cancel": "မလုပ်တော့ပါ", "label.change-password": "စကားဝှက် ပြောင်းမည်", "label.cities": "မြို့များ", + "label.city": "City", "label.clear-all": "အားလုံးကိုဖျက်မည်", "label.confirm": "အတည်ပြုသည်", "label.confirm-password": "စကားဝှက်အတည်ပြုသည်", + "label.contains": "Contains", "label.continue": "ဆက်သွားမည်", "label.countries": "နိုင်ငံများ", + "label.country": "Country", + "label.create": "Create", + "label.create-report": "Create report", "label.create-team": "Team ပြုလုပ်မည်", "label.create-user": "အသုံးပြုသူထည့်မည်", "label.created": "ပြုလုပ်ပြီးသော", @@ -28,16 +38,21 @@ "label.custom-range": "အချိန်အပိုင်းအခြားရွေးရန်", "label.dashboard": "ဒက်ရှ်ဘုတ်", "label.data": "ဒေတာ", + "label.date": "Date", "label.date-range": "ရက်အပိုင်းအခြား", + "label.day": "Day", "label.default-date-range": "ပုံသေ ရက်အပိုင်းအခြား", "label.delete": "ဖျက်မည်", "label.delete-team": "Team ကိုဖျက်မည်", "label.delete-user": "အသုံးပြုသူကိုဖျက်မည်", "label.delete-website": "ဝက်ဘ်ဆိုဒ်ကိုဖျက်မည်", + "label.description": "Description", "label.desktop": "စားပွဲတင်ကွန်ပျူတာ", "label.details": "အသေးစိတ်", + "label.device": "Device", "label.devices": "အသုံးပြုသည့် ကိရိယာများ", "label.dismiss": "ပိတ်ပါ", + "label.does-not-contain": "Does not contain", "label.domain": "ဒိုမိန်း", "label.dropoff": "Dropoff", "label.edit": "ပြုပြင်မည်", @@ -46,12 +61,23 @@ "label.event": "အဖြစ်အပျက်", "label.event-data": "အဖြစ်အပျက် ဒေတာ", "label.events": "အဖြစ်အပျက်များ", + "label.false": "False", "label.field": "Field အမည်", "label.fields": "Field အမည်များ", + "label.filter": "Filter", "label.filter-combined": "ပေါင်းစပ်ပြီး", "label.filter-raw": "အရှိအတိုင်း", + "label.filters": "Filters", "label.funnel": "ဖန်နယ်", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", + "label.greater-than": "Greater than", + "label.greater-than-equals": "Greater than or equals", "label.insights": "အသေးစိတ်သိမြင်နိုင်ရန်", + "label.insights-description": "Dive deeper into your data by using segments and filters.", + "label.is": "Is", + "label.is-not": "Is not", + "label.is-not-set": "Is not set", + "label.is-set": "Is set", "label.join": "ဝင်မည်", "label.join-team": "အသင်းဝင်မည်", "label.language": "ဘာသာစကား", @@ -61,17 +87,25 @@ "label.last-hours": "လွန်ခဲ့သော {x} နာရီက", "label.leave": "ထွက်မည်", "label.leave-team": "အသင်းမှထွက်မည်", + "label.less-than": "Less than", + "label.less-than-equals": "Less than or equals", "label.login": "လော့ဂ်အင်", "label.logout": "လော့ဂ်အောက်လုပ်မည်", + "label.max": "Max", "label.members": "အဖွဲ့ဝင်များ", + "label.min": "Min", "label.mobile": "မိုဘိုင်း", "label.more": "နောက်ထပ်", + "label.my-websites": "My websites", "label.name": "အမည်", "label.new-password": "စကားဝှက်အသစ်", "label.none": "မရှိပါ", - "label.operating-systems": "ကွန်ပျူတာလည်ပတ်မှုစနစ်", + "label.os": "OS", + "label.overview": "Overview", "label.owner": "ပိုင်ဆိုင်သူ", + "label.page-of": "Page {current} of {total}", "label.page-views": "ဝင်ရောက်ကြည့်ရှုသူ", + "label.pageTitle": "Page title", "label.pages": "စာမျက်နှာများ", "label.password": "စကားဝှက်", "label.powered-by": "{name} ထောက်ပံ့သည်", @@ -80,31 +114,39 @@ "label.query": "Query (ကွာရီ)", "label.query-parameters": "Query parameters (ကွာရီပါရာမီတာများ)", "label.realtime": "အချိန်နှင့်တပြေးညီ", + "label.referrer": "Referrer", "label.referrers": "ရည်ညွှန်းမှုများ", "label.refresh": "Refresh လုပ်မည်", "label.regenerate": "ပြန်ထုတ်မည်", + "label.region": "Region", "label.regions": "ဒေသများ", "label.remove": "ဖျက်မည်", "label.reports": "တင်ပြမှုများ", "label.required": "လိုအပ်သည်", "label.reset": "ပြန်စမည်", "label.reset-website": "ဝက်ဘ်ဆိုဒ်ဒေတာကိုဖျက်မည်", + "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "အခန်းကဏ္ဍ", "label.run-query": "Query ကိုလုပ်ဆောင်မည်", "label.save": "သိမ်းဆည်းမည်", "label.screens": "မြင်ကွင်းများ", + "label.search": "Search", "label.select-date": "ရက်ရွေးပါ", "label.select-website": "ဝဘက်ဘ်ဆိုဒ်ရွေးပါ", "label.sessions": "ဆက်ရှင်များ", "label.settings": "ဆက်တင်များ", "label.share-url": "URL ကိုရှဲမည်", "label.single-day": "တစ်ရက်အတွင်း", + "label.sum": "Sum", "label.tablet": "တက်ဘလက်", "label.team": "အသင်း", "label.team-guest": "အသင်း ဧည့်သည်", "label.team-id": "အသင်း အိုင်ဒီ", "label.team-member": "အသင်းဝင်", + "label.team-name": "Team name", "label.team-owner": "အသင်းကိုပိုင်ဆိုင်သူ", + "label.team-websites": "Team websites", "label.teams": "အသင်းများ", "label.theme": "Theme (အပြင်အဆင်)", "label.this-month": "ယခုလ", @@ -114,14 +156,21 @@ "label.title": "ခေါင်းစဥ်", "label.today": "ယနေ့", "label.toggle-charts": "ဇယားများကို အဖွင့်အပိတ်လုပ်မည်", + "label.total": "Total", + "label.total-records": "Total records", "label.tracking-code": "ထရက်လုပ်သည့် ကုဒ်", + "label.true": "True", + "label.type": "Type", + "label.unique": "Unique", "label.unique-visitors": "ဝင်ရောက်သူ (ထပ်ခြင်းမရှိ)", "label.unknown": "မသိသော", + "label.untitled": "Untitled", "label.url": "URL", "label.urls": "URL များ", "label.user": "အသုံးပြုသူ", "label.username": "အသုံးပြုသူအမည်", "label.users": "အသုံးပြုသူများ", + "label.value": "Value", "label.view": "ဝင်ရောက်ကြည့်ရှုမှု", "label.view-details": "အသေးစိတ်ကို ကြည့်ရှုမည်", "label.view-only": "ဝင်ရောက်ကြည့်ရှုမှုများသာ", @@ -132,33 +181,6 @@ "label.websites": "ဝက်ဘ်ဆိုဒ်များ", "label.window": "ဝင်းဒိုး", "label.yesterday": "မနေ့က", - "labels.after": "ပြီးနောက်", - "labels.average": "ပျမ်းမျှ", - "labels.before": "မတိုင်မီ", - "labels.breakdown": "ခွဲခြမ်းစိတ်ဖြာမှု", - "labels.contains": "ပါဝင်သည်", - "labels.create-report": "ရီပို့လုပ်မည်", - "labels.description": "ရှင်းပြချက်", - "labels.does-not-contain": "မပါဝင်ပါ", - "labels.does-not-equal": "မတူညီပါ", - "labels.equals": "တူညီသည်", - "labels.false": "မှားသည်", - "labels.filters": "Filter များ", - "labels.greater-than": "ထက်ပို၍ကြီးသည်", - "labels.greater-than-equals": "ထက်ပို၍ကြီးသည်သို့မဟုတ်တူသည်", - "labels.less-than": "ထက်ပို၍ငယ်သည်", - "labels.less-than-equals": "ထက်ပို၍ငယ်သည်သို့မဟုတ်တူသည်", - "labels.max": "အများဆုံး", - "labels.min": "အနည်းဆုံး", - "labels.overview": "အပေါ်ယံမြင်ကွင်း", - "labels.sum": "ပေါင်းလဒ်", - "labels.total": "စုစုပေါင်း", - "labels.total-records": "မှတ်တမ်းစုစုပေါင်း", - "labels.true": "မှန်သည်", - "labels.type": "အမျိုးအစား", - "labels.unique": "Unique", - "labels.untitled": "ခေါင်းစဉ်မရှိ", - "labels.value": "တန်ဖိုး", "message.active-users": "{x} လက်ရှိအသုံးပြုနေသူ {x, plural, one {ယောက်} other {ယောက်}}", "message.confirm-delete": "{target} ကို ဖျက်ရန် သေချာပါသလား?", "message.confirm-leave": "{target} ကို ထွက်ရန် သေချာပါသလား?", @@ -172,11 +194,15 @@ "message.incorrect-username-password": "အသုံးပြုသူအမည် သို့မဟုတ် စကားဝှက် မှားနေသည်", "message.invalid-domain": "ဒိုမိန်း မမှန်ပါ http/https. မပါရပါ", "message.min-password-length": "အနည်းဆုံး {n} character ရှိရမည်", + "message.new-version-available": "အူမာမီ {version} အသစ်ထွက်နေပါပြီ", "message.no-data-available": "ဒေတာ မရှိပါ", "message.no-event-data": "အဖြစ်အပျက်ဒေတာ မရှိပါ", "message.no-match-password": "စကားဝှက် မှားနေသည်", + "message.no-results-found": "ရလဒ်မရှိပါ", + "message.no-team-websites": "ဤအသင်းတွင် ဝက်ဘ်ဆိုက်မရှိသေးပါ", "message.no-teams": "အသင်း မပြုလုပ်ရသေးပါ", "message.no-users": "အသုံးပြုသူ မရှိသေးပါ", + "message.no-websites-configured": "ဝက်ဘ်ဆိုဒ်တစ်ခုမှ မထည့်ရသေးပါ", "message.page-not-found": "ဤစာမျက်နှာသည် မရှိပါ", "message.reset-website": "ဤ ဝက်ဘ်ဆိုဒ်ဒေတာကိုဖျက်၍ ပြန်စလုပ်ရန် အောက်တွင် {confirmation} ကို ရိုက်ထည့်ပေးပါ", "message.reset-website-warning": "ဤဝက်ဘ်ဆိုဒ်က စာရင်းအချက်အလက်များကို ဖျက်မည်၊ ဆက်တင်ဒေတာများ မပါပါ", @@ -184,12 +210,8 @@ "message.share-url": "သင့်ဝက်ဆိုဒ်ဘ်၏ စာရင်းအချက်အလက်များကို အောက်ပါ URL တွင် ဝင်ရောက်ကြည့်ရှုနိုင်သည်", "message.team-already-member": "ဤအသင်းတွင် ဝင်ပြီးသားဖြစ်နေသည်", "message.team-not-found": "အသင်း မရှိပါ", + "message.team-websites-info": "ဤဝက်ဘ်ဆိုဒ်များကို အသင်းထဲမှ လူတိုင်းဝင်ကြည့်နိုင်သည်", "message.tracking-code": "ဤဝက်ဘ်ဆိုဒ်၏ ဒေတာကိုကောက်ခံရန် အောက်ပါ code ကို သင်၏ HTML တွင်ထည့်ပါ", "message.user-deleted": "အသုံးပြုသူ ဖျက်ပြီးပါပြီ", - "message.visitor-log": "{country} မှ {browser} ဖြင့် {os} {device} တွင် ဝင်ရောက်ကြည့်ရှုသူ", - "message.no-results-found": "ရလဒ်မရှိပါ", - "message.no-team-websites": "ဤအသင်းတွင် ဝက်ဘ်ဆိုက်မရှိသေးပါ", - "message.no-websites-configured": "ဝက်ဘ်ဆိုဒ်တစ်ခုမှ မထည့်ရသေးပါ", - "message.team-websites-info": "ဤဝက်ဘ်ဆိုဒ်များကို အသင်းထဲမှ လူတိုင်းဝင်ကြည့်နိုင်သည်", - "message.new-version-available": "အူမာမီ {version} အသစ်ထွက်နေပါပြီ" + "message.visitor-log": "{country} မှ {browser} ဖြင့် {os} {device} တွင် ဝင်ရောက်ကြည့်ရှုသူ" } diff --git a/src/lang/nb-NO.json b/src/lang/nb-NO.json index 654c3c791..18fa765ee 100644 --- a/src/lang/nb-NO.json +++ b/src/lang/nb-NO.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Land", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Kombinert", "label.filter-raw": "Rå", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Nullstill", "label.reset-website": "Nullstill statistikk", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Lagre", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/nl-NL.json b/src/lang/nl-NL.json index ad30cf36b..d0c3f6782 100644 --- a/src/lang/nl-NL.json +++ b/src/lang/nl-NL.json @@ -29,6 +29,7 @@ "label.continue": "Doorgaan", "label.countries": "Landen", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Team aanmaken", "label.create-user": "Gebruiker maken", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Gecombineerd", "label.filter-raw": "Ruw", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Opnieuw instellen", "label.reset-website": "Statistieken opnieuw instellen", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Gebruikersrol", "label.run-query": "Run query", "label.save": "Opslaan", "label.screens": "Schermen", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Website selecteren", "label.sessions": "Sessies", diff --git a/src/lang/pl-PL.json b/src/lang/pl-PL.json index eb9406139..8045af067 100644 --- a/src/lang/pl-PL.json +++ b/src/lang/pl-PL.json @@ -29,6 +29,7 @@ "label.continue": "Kontynuuj", "label.countries": "Kraje", "label.country": "Country", + "label.create": "Create", "label.create-report": "Stwórz raport", "label.create-team": "Utwórz zespół", "label.create-user": "Utwórz użytkownika", @@ -63,13 +64,16 @@ "label.false": "Fałsz", "label.field": "Pole", "label.fields": "Pola", + "label.filter": "Filter", "label.filter-combined": "Połączone", "label.filter-raw": "Surowe dane", "label.filters": "Filtry", "label.funnel": "Lejek", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Większe niż", "label.greater-than-equals": "Większe niż lub równe", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Równe", "label.is-not": "Nie jest równe", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Zresetuj", "label.reset-website": "Zresetuj statystyki", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Uruchom zapytanie", "label.save": "Zapisz", "label.screens": "Ekrany", + "label.search": "Search", "label.select-date": "Wybierz datę", "label.select-website": "Wybierz witrynę", "label.sessions": "Sesje", diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json index b68d96154..22169e3ae 100644 --- a/src/lang/pt-BR.json +++ b/src/lang/pt-BR.json @@ -29,6 +29,7 @@ "label.continue": "Continuar", "label.countries": "Países", "label.country": "Country", + "label.create": "Create", "label.create-report": "Criar relatório", "label.create-team": "Criar time", "label.create-user": "Criar usuário", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Campo", "label.fields": "Campos", + "label.filter": "Filter", "label.filter-combined": "Combinado", "label.filter-raw": "Dados brutos", "label.filters": "Filters", "label.funnel": "Funil", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Maior que", "label.greater-than-equals": "Maior que ou igual", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Redefinir", "label.reset-website": "Redefinir estatísticas", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Papel", "label.run-query": "Executar query", "label.save": "Salvar", "label.screens": "Telas", + "label.search": "Search", "label.select-date": "Selecionar data", "label.select-website": "Selecionar site", "label.sessions": "Sessões", diff --git a/src/lang/pt-PT.json b/src/lang/pt-PT.json index fcf7ff03f..28a798e31 100644 --- a/src/lang/pt-PT.json +++ b/src/lang/pt-PT.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Países", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combinado", "label.filter-raw": "Dados brutos", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Repor", "label.reset-website": "Repor estatísticas", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Guardar", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/ro-RO.json b/src/lang/ro-RO.json index 43a78ecd2..9c8c8c3c1 100644 --- a/src/lang/ro-RO.json +++ b/src/lang/ro-RO.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Țări", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combinat", "label.filter-raw": "Brut", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Resetează", "label.reset-website": "Resetează statisticile pentru site", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Salvează", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/ru-RU.json b/src/lang/ru-RU.json index b9129beb4..6eed37257 100644 --- a/src/lang/ru-RU.json +++ b/src/lang/ru-RU.json @@ -29,6 +29,7 @@ "label.continue": "Продолжить", "label.countries": "Страны", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Создать команду", "label.create-user": "Создать пользователя", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Объединенные", "label.filter-raw": "Сырые данные", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Сбросить", "label.reset-website": "Сбросить статистику", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Роль", "label.run-query": "Run query", "label.save": "Сохранить", "label.screens": "Экраны", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Выбрать сайт", "label.sessions": "Сессии", diff --git a/src/lang/si-LK.json b/src/lang/si-LK.json index 6f6dda6d1..92bbe7360 100644 --- a/src/lang/si-LK.json +++ b/src/lang/si-LK.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Countries", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Combined", "label.filter-raw": "Raw", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "යළි පිහිටුවන්න", "label.reset-website": "සංඛ්යා ලේඛන නැවත සකසන්න", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "සුරකින්න", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/sk-SK.json b/src/lang/sk-SK.json index 3f0339236..534ace423 100644 --- a/src/lang/sk-SK.json +++ b/src/lang/sk-SK.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Zem", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Kombinácie", "label.filter-raw": "Nezpracované", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Uložiť", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index 440b9fcad..f73aa265f 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -29,6 +29,7 @@ "label.continue": "Nadaljuj", "label.countries": "Države", "label.country": "Država", + "label.create": "Create", "label.create-report": "Ustvari poročilo", "label.create-team": "Ustvari ekipo", "label.create-user": "Ustvari uporabnika", @@ -63,13 +64,16 @@ "label.false": "Napačno", "label.field": "Polje", "label.fields": "Polja", + "label.filter": "Filter", "label.filter-combined": "Skupaj", "label.filter-raw": "Neobdelano", "label.filters": "Filtri", "label.funnel": "Prodajni lijak", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Večje od", "label.greater-than-equals": "Večje ali enako kot", "label.insights": "Vpogled", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Je", "label.is-not": "Ni", "label.is-not-set": "Ni nastavljeno", @@ -122,13 +126,14 @@ "label.reset": "Ponastavi", "label.reset-website": "Ponastavi statistiko", "label.retention": "Ohranjanje uporabnikov", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Vloga", "label.run-query": "Izvedi poizvedbo", "label.save": "Shrani", - "label.screens": "Zasloni", - "label.select-date": "Izberi datum", - "label.select-website": "Izberi spletno mesto", - "label.sessions": "Seje", + "label.screens": "Screens", + "label.select-date": "Select date", + "label.select-website": "Select website", + "label.sessions": "Sessions", "label.settings": "Nastavitve", "label.share-url": "Deli povezavo", "label.single-day": "En dan", diff --git a/src/lang/sv-SE.json b/src/lang/sv-SE.json index e6abb5bf7..532b9039d 100644 --- a/src/lang/sv-SE.json +++ b/src/lang/sv-SE.json @@ -29,6 +29,7 @@ "label.continue": "Fortsätt", "label.countries": "Länder", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Skapa team", "label.create-user": "Skapa användare", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Kombinerade", "label.filter-raw": "Rådata", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Återställ", "label.reset-website": "Återställ statistik", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Roll", "label.run-query": "Run query", "label.save": "Spara", "label.screens": "Upplösning", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Välj webbsajt", "label.sessions": "Sessions", diff --git a/src/lang/ta-IN.json b/src/lang/ta-IN.json index be3d5e815..64b6bf2ba 100644 --- a/src/lang/ta-IN.json +++ b/src/lang/ta-IN.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "நாடுகள்", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "ஒருங்கிணைந்த", "label.filter-raw": "மூல", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "மீட்டமை", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "சேமி", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/th-TH.json b/src/lang/th-TH.json index 43f2f7587..1ddaa4d01 100644 --- a/src/lang/th-TH.json +++ b/src/lang/th-TH.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "ประเทศ", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "ข้อมูลรวม", "label.filter-raw": "ข้อมูลดิบ", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "รีเซต", "label.reset-website": "รีเซตข้อมูลสถิติ", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "บันทึก", "label.screens": "ขนาดหน้าจอ", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/tr-TR.json b/src/lang/tr-TR.json index 0ec10e0b9..b333131fb 100644 --- a/src/lang/tr-TR.json +++ b/src/lang/tr-TR.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Ülkeler", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Birleşik", "label.filter-raw": "Ham", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Sıfırla", "label.reset-website": "Reset statistics", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Kaydet", "label.screens": "Ekranlar", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/uk-UA.json b/src/lang/uk-UA.json index 89079eff2..e95815f13 100644 --- a/src/lang/uk-UA.json +++ b/src/lang/uk-UA.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Країни", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Об'єднані", "label.filter-raw": "Сирі дані", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Скинути", "label.reset-website": "Скинути статистику сайту", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Зберегти", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/ur-PK.json b/src/lang/ur-PK.json index 4d585dcbc..9fbfb0797 100644 --- a/src/lang/ur-PK.json +++ b/src/lang/ur-PK.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "ممالک", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "مشترکہ", "label.filter-raw": "خام", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "دوبارہ ترتیب دیں", "label.reset-website": "اعدادوشمار کو دوبارہ ترتیب دیں", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "محفوظ کریں", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/vi-VN.json b/src/lang/vi-VN.json index e9bce2d3c..ff2915c79 100644 --- a/src/lang/vi-VN.json +++ b/src/lang/vi-VN.json @@ -29,6 +29,7 @@ "label.continue": "Continue", "label.countries": "Quốc gia", "label.country": "Country", + "label.create": "Create", "label.create-report": "Create report", "label.create-team": "Create team", "label.create-user": "Create user", @@ -63,13 +64,16 @@ "label.false": "False", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "Kết hợp", "label.filter-raw": "Gốc", "label.filters": "Filters", "label.funnel": "Funnel", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "Insights", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", "label.is-not": "Is not", "label.is-not-set": "Is not set", @@ -122,10 +126,12 @@ "label.reset": "Tái thiết lập", "label.reset-website": "Tái thiết lập thống kê", "label.retention": "Retention", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Lưu", "label.screens": "Screens", + "label.search": "Search", "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", diff --git a/src/lang/zh-CN.json b/src/lang/zh-CN.json index bab833c02..2ebe75f2e 100644 --- a/src/lang/zh-CN.json +++ b/src/lang/zh-CN.json @@ -29,6 +29,7 @@ "label.continue": "继续", "label.countries": "国家/地区", "label.country": "国家/地区", + "label.create": "Create", "label.create-report": "创建报告", "label.create-team": "创建团队", "label.create-user": "创建用户", @@ -63,13 +64,16 @@ "label.false": "否", "label.field": "Field", "label.fields": "Fields", + "label.filter": "Filter", "label.filter-combined": "合并", "label.filter-raw": "原始", "label.filters": "筛选", "label.funnel": "分析", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "Greater than", "label.greater-than-equals": "Greater than or equals", "label.insights": "见解", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "等于", "label.is-not": "不等于", "label.is-not-set": "未设置", @@ -122,10 +126,12 @@ "label.reset": "重置", "label.reset-website": "重置统计数据", "label.retention": "保留", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "角色", "label.run-query": "查询", "label.save": "保存", "label.screens": "屏幕尺寸", + "label.search": "Search", "label.select-date": "选择数据", "label.select-website": "选择网站", "label.sessions": "会话", diff --git a/src/lang/zh-TW.json b/src/lang/zh-TW.json index 05b61c806..22291ed06 100644 --- a/src/lang/zh-TW.json +++ b/src/lang/zh-TW.json @@ -29,6 +29,7 @@ "label.continue": "繼續", "label.countries": "國家", "label.country": "國家", + "label.create": "Create", "label.create-report": "建立報告", "label.create-team": "建立團隊", "label.create-user": "建立使用者", @@ -63,13 +64,16 @@ "label.false": "否", "label.field": "欄位", "label.fields": "欄位", + "label.filter": "Filter", "label.filter-combined": "組合", "label.filter-raw": "原始", "label.filters": "篩選器", "label.funnel": "漏斗", + "label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.greater-than": "大於", "label.greater-than-equals": "大於或等於", "label.insights": "洞察", + "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "是", "label.is-not": "不是", "label.is-not-set": "未設定", @@ -122,10 +126,12 @@ "label.reset": "重設", "label.reset-website": "重設網站", "label.retention": "保留", + "label.retention-description": "Measure you website stickiness by tracking how often users return.", "label.role": "角色", "label.run-query": "執行查詢", "label.save": "儲存", "label.screens": "螢幕", + "label.search": "Search", "label.select-date": "選擇日期", "label.select-website": "選擇網站", "label.sessions": "工作階段", From aa341d1dd295d69adb400960ba5f3fb0317e64f2 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Fri, 1 Sep 2023 09:45:59 +0000 Subject: [PATCH 166/357] ensure translation of default report name --- src/components/hooks/useReport.js | 14 ++++++++------ src/components/pages/reports/ReportHeader.js | 5 +++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/hooks/useReport.js b/src/components/hooks/useReport.js index 72f90af33..7c698b4e7 100644 --- a/src/components/hooks/useReport.js +++ b/src/components/hooks/useReport.js @@ -2,18 +2,20 @@ import { produce } from 'immer'; import { useCallback, useEffect, useState } from 'react'; import { useTimezone } from './useTimezone'; import useApi from './useApi'; - -const baseParameters = { - name: 'Untitled', - description: '', - parameters: {}, -}; +import useMessages from './useMessages'; export function useReport(reportId, defaultParameters) { const [report, setReport] = useState(null); const [isRunning, setIsRunning] = useState(false); const { get, post } = useApi(); const [timezone] = useTimezone(); + const { formatMessage, labels } = useMessages(); + + const baseParameters = { + name: formatMessage(labels.untitled), + description: '', + parameters: {}, + }; const loadReport = async id => { const data = await get(`/reports/${id}`); diff --git a/src/components/pages/reports/ReportHeader.js b/src/components/pages/reports/ReportHeader.js index e81d6ece6..930f745b9 100644 --- a/src/components/pages/reports/ReportHeader.js +++ b/src/components/pages/reports/ReportHeader.js @@ -20,6 +20,7 @@ export function ReportHeader({ icon }) { const { name, description, parameters } = report || {}; const { websiteId, dateRange } = parameters || {}; + const defaultName = formatMessage(labels.untitled); const handleSave = async () => { if (!report.id) { @@ -39,7 +40,7 @@ export function ReportHeader({ icon }) { }; const handleNameChange = name => { - updateReport({ name: name || 'Untitled' }); + updateReport({ name: name || defaultName }); }; const handleDescriptionChange = description => { @@ -54,7 +55,7 @@ export function ReportHeader({ icon }) { key={name} name="name" value={name} - placeholder={formatMessage(labels.untitled)} + placeholder={defaultName} onCommit={handleNameChange} /> From 34d3d166415aa0037d87baa91e26348c873ee96a Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Fri, 1 Sep 2023 12:25:13 +0000 Subject: [PATCH 167/357] display localized type in reports table --- src/components/pages/reports/ReportsTable.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/pages/reports/ReportsTable.js b/src/components/pages/reports/ReportsTable.js index 4073fbec0..52488c11e 100644 --- a/src/components/pages/reports/ReportsTable.js +++ b/src/components/pages/reports/ReportsTable.js @@ -5,6 +5,7 @@ import { useMessages } from 'components/hooks'; import useUser from 'components/hooks/useUser'; import { useState } from 'react'; import { Button, Flexbox, Icon, Icons, Modal, Text } from 'react-basics'; +import { REPORT_TYPES } from 'lib/constants'; export function ReportsTable({ data = [], @@ -34,6 +35,15 @@ export function ReportsTable({ { name: 'action', label: ' ' }, ]; + const cellRender = (row, data, key) => { + if (key === 'type') { + return formatMessage( + labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)], + ); + } + return data[key]; + }; + const handleConfirm = () => { onDelete(report.id); }; @@ -42,6 +52,7 @@ export function ReportsTable({ <> Date: Fri, 1 Sep 2023 14:15:57 +0000 Subject: [PATCH 168/357] localize date in retention report --- src/components/pages/reports/retention/RetentionTable.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/reports/retention/RetentionTable.js b/src/components/pages/reports/retention/RetentionTable.js index 09ef2948f..ad1eaa6f9 100644 --- a/src/components/pages/reports/retention/RetentionTable.js +++ b/src/components/pages/reports/retention/RetentionTable.js @@ -3,11 +3,13 @@ import classNames from 'classnames'; import { ReportContext } from '../Report'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import { useMessages } from 'components/hooks'; +import { useLocale } from 'components/hooks'; import { formatDate } from 'lib/date'; import styles from './RetentionTable.module.css'; export function RetentionTable() { const { formatMessage, labels } = useMessages(); + const { locale } = useLocale(); const { report } = useContext(ReportContext); const { data } = report || {}; @@ -51,7 +53,7 @@ export function RetentionTable() { {rows.map(({ date, visitors, records }, rowIndex) => { return (
-
{formatDate(`${date} 00:00:00`, 'PP')}
+
{formatDate(`${date} 00:00:00`, 'PP', locale)}
{visitors}
{days.map(day => { if (totalDays - rowIndex < day) { From ee6fcae152405262ff1b65ab0d46421f5e833c75 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Fri, 1 Sep 2023 14:20:50 +0000 Subject: [PATCH 169/357] fix typo --- src/components/messages.js | 2 +- src/lang/am-ET.json | 2 +- src/lang/ar-SA.json | 2 +- src/lang/be-BY.json | 2 +- src/lang/bn-BD.json | 2 +- src/lang/ca-ES.json | 2 +- src/lang/cs-CZ.json | 2 +- src/lang/da-DK.json | 2 +- src/lang/de-CH.json | 2 +- src/lang/de-DE.json | 2 +- src/lang/el-GR.json | 2 +- src/lang/en-GB.json | 2 +- src/lang/en-US.json | 2 +- src/lang/es-ES.json | 2 +- src/lang/es-MX.json | 2 +- src/lang/fa-IR.json | 2 +- src/lang/fi-FI.json | 2 +- src/lang/fo-FO.json | 2 +- src/lang/fr-FR.json | 2 +- src/lang/ga-ES.json | 2 +- src/lang/he-IL.json | 2 +- src/lang/hi-IN.json | 2 +- src/lang/hr-HR.json | 2 +- src/lang/hu-HU.json | 2 +- src/lang/id-ID.json | 2 +- src/lang/it-IT.json | 2 +- src/lang/ja-JP.json | 2 +- src/lang/km-KH.json | 2 +- src/lang/ko-KR.json | 2 +- src/lang/lt-LT.json | 2 +- src/lang/mn-MN.json | 2 +- src/lang/ms-MY.json | 2 +- src/lang/my-MM.json | 2 +- src/lang/nb-NO.json | 2 +- src/lang/nl-NL.json | 2 +- src/lang/pl-PL.json | 2 +- src/lang/pt-BR.json | 2 +- src/lang/pt-PT.json | 2 +- src/lang/ro-RO.json | 2 +- src/lang/ru-RU.json | 2 +- src/lang/si-LK.json | 2 +- src/lang/sk-SK.json | 2 +- src/lang/sl-SI.json | 2 +- src/lang/sv-SE.json | 2 +- src/lang/ta-IN.json | 2 +- src/lang/th-TH.json | 2 +- src/lang/tr-TR.json | 2 +- src/lang/uk-UA.json | 2 +- src/lang/ur-PK.json | 2 +- src/lang/vi-VN.json | 2 +- src/lang/zh-CN.json | 2 +- src/lang/zh-TW.json | 2 +- 52 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/components/messages.js b/src/components/messages.js index 01c7e779c..7f432eb3e 100644 --- a/src/components/messages.js +++ b/src/components/messages.js @@ -178,7 +178,7 @@ export const labels = defineMessages({ retention: { id: 'label.retention', defaultMessage: 'Retention' }, retentionDescription: { id: 'label.retention-description', - defaultMessage: 'Measure you website stickiness by tracking how often users return.', + defaultMessage: 'Measure your website stickiness by tracking how often users return.', }, dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' }, referrer: { id: 'label.referrer', defaultMessage: 'Referrer' }, diff --git a/src/lang/am-ET.json b/src/lang/am-ET.json index 3765e5ba6..e79afb487 100644 --- a/src/lang/am-ET.json +++ b/src/lang/am-ET.json @@ -126,7 +126,7 @@ "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Save", diff --git a/src/lang/ar-SA.json b/src/lang/ar-SA.json index ccfee1f33..311e54305 100644 --- a/src/lang/ar-SA.json +++ b/src/lang/ar-SA.json @@ -126,7 +126,7 @@ "label.reset": "اعادة تعيين", "label.reset-website": "اعادة تعيين الإحصائيات", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "الصلاحية", "label.run-query": "Run query", "label.save": "حفظ", diff --git a/src/lang/be-BY.json b/src/lang/be-BY.json index 88f673b9c..dc0be21d5 100644 --- a/src/lang/be-BY.json +++ b/src/lang/be-BY.json @@ -126,7 +126,7 @@ "label.reset": "Скінуць", "label.reset-website": "Скінуць статыстыку", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Захаваць", diff --git a/src/lang/bn-BD.json b/src/lang/bn-BD.json index 29e1bb6f6..201157ac1 100644 --- a/src/lang/bn-BD.json +++ b/src/lang/bn-BD.json @@ -126,7 +126,7 @@ "label.reset": "রিসেট", "label.reset-website": "ওয়েবসাইট রিসেট করুন", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "সংরক্ষণ", diff --git a/src/lang/ca-ES.json b/src/lang/ca-ES.json index 271b9245a..e55d90da4 100644 --- a/src/lang/ca-ES.json +++ b/src/lang/ca-ES.json @@ -126,7 +126,7 @@ "label.reset": "Restableix", "label.reset-website": "Restableix estadístiques", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Desa", diff --git a/src/lang/cs-CZ.json b/src/lang/cs-CZ.json index 73bc15b70..86f2043a8 100644 --- a/src/lang/cs-CZ.json +++ b/src/lang/cs-CZ.json @@ -126,7 +126,7 @@ "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Uložit", diff --git a/src/lang/da-DK.json b/src/lang/da-DK.json index 2fba4f7a7..8df7dd6bf 100644 --- a/src/lang/da-DK.json +++ b/src/lang/da-DK.json @@ -126,7 +126,7 @@ "label.reset": "Nulstil", "label.reset-website": "Nulstil statistikker", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Gem", diff --git a/src/lang/de-CH.json b/src/lang/de-CH.json index c07ddd2cb..95ba3b6a8 100644 --- a/src/lang/de-CH.json +++ b/src/lang/de-CH.json @@ -126,7 +126,7 @@ "label.reset": "Zruggsetze", "label.reset-website": "Statistik zruggsetze", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Rollä", "label.run-query": "Run query", "label.save": "Speichere", diff --git a/src/lang/de-DE.json b/src/lang/de-DE.json index 7bac16255..0724c181b 100644 --- a/src/lang/de-DE.json +++ b/src/lang/de-DE.json @@ -126,7 +126,7 @@ "label.reset": "Zurücksetzen", "label.reset-website": "Statistik zurücksetzen", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Rolle", "label.run-query": "Abfrage starten", "label.save": "Speichern", diff --git a/src/lang/el-GR.json b/src/lang/el-GR.json index c5cea1bee..f93742fdd 100644 --- a/src/lang/el-GR.json +++ b/src/lang/el-GR.json @@ -126,7 +126,7 @@ "label.reset": "Επαναφορά", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Αποθήκευση", diff --git a/src/lang/en-GB.json b/src/lang/en-GB.json index e93b3b23a..b6cb2c9a3 100644 --- a/src/lang/en-GB.json +++ b/src/lang/en-GB.json @@ -126,7 +126,7 @@ "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Save", diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 16dfa1888..e1d592303 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -126,7 +126,7 @@ "label.reset": "Reset", "label.reset-website": "Reset website", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Save", diff --git a/src/lang/es-ES.json b/src/lang/es-ES.json index 001b94064..ef8c31a80 100644 --- a/src/lang/es-ES.json +++ b/src/lang/es-ES.json @@ -126,7 +126,7 @@ "label.reset": "Reiniciar", "label.reset-website": "Reiniciar estadísticas", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Rol", "label.run-query": "Ejecutar consulta", "label.save": "Guardar", diff --git a/src/lang/es-MX.json b/src/lang/es-MX.json index c1dacfa44..d8110b404 100644 --- a/src/lang/es-MX.json +++ b/src/lang/es-MX.json @@ -126,7 +126,7 @@ "label.reset": "Reiniciar", "label.reset-website": "Reiniciar estadísticas", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Rol", "label.run-query": "Run query", "label.save": "Guardar", diff --git a/src/lang/fa-IR.json b/src/lang/fa-IR.json index 32d985e3e..0458e3c69 100644 --- a/src/lang/fa-IR.json +++ b/src/lang/fa-IR.json @@ -126,7 +126,7 @@ "label.reset": "بازنشانی", "label.reset-website": "بازنشانی آمار", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "ذخیره", diff --git a/src/lang/fi-FI.json b/src/lang/fi-FI.json index 8c447f8a1..52d882386 100644 --- a/src/lang/fi-FI.json +++ b/src/lang/fi-FI.json @@ -126,7 +126,7 @@ "label.reset": "Nollaa", "label.reset-website": "Nollaa tilastot", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Tallenna", diff --git a/src/lang/fo-FO.json b/src/lang/fo-FO.json index e2626f1fb..ef5b0bc1b 100644 --- a/src/lang/fo-FO.json +++ b/src/lang/fo-FO.json @@ -126,7 +126,7 @@ "label.reset": "Nulstilla", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Goym", diff --git a/src/lang/fr-FR.json b/src/lang/fr-FR.json index 878e078aa..092837eb5 100644 --- a/src/lang/fr-FR.json +++ b/src/lang/fr-FR.json @@ -126,7 +126,7 @@ "label.reset": "Réinitialiser", "label.reset-website": "Réinitialiser les statistiques", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Rôle", "label.run-query": "Éxécuter la requête", "label.save": "Enregistrer", diff --git a/src/lang/ga-ES.json b/src/lang/ga-ES.json index ae46fa2c5..2a2d16c9c 100644 --- a/src/lang/ga-ES.json +++ b/src/lang/ga-ES.json @@ -126,7 +126,7 @@ "label.reset": "Restablecer", "label.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Gardar", diff --git a/src/lang/he-IL.json b/src/lang/he-IL.json index b953fda60..04912335d 100644 --- a/src/lang/he-IL.json +++ b/src/lang/he-IL.json @@ -126,7 +126,7 @@ "label.reset": "איפוס", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "שמירה", diff --git a/src/lang/hi-IN.json b/src/lang/hi-IN.json index e296d87bf..c872fd1a9 100644 --- a/src/lang/hi-IN.json +++ b/src/lang/hi-IN.json @@ -126,7 +126,7 @@ "label.reset": "रीसेट", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "सहेजें", diff --git a/src/lang/hr-HR.json b/src/lang/hr-HR.json index 75bd8a4f3..55feb48bd 100644 --- a/src/lang/hr-HR.json +++ b/src/lang/hr-HR.json @@ -126,7 +126,7 @@ "label.reset": "Resetirati", "label.reset-website": "Resetirati web stranicu", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Spremi", diff --git a/src/lang/hu-HU.json b/src/lang/hu-HU.json index c6224b01c..407687f2e 100644 --- a/src/lang/hu-HU.json +++ b/src/lang/hu-HU.json @@ -126,7 +126,7 @@ "label.reset": "Visszaállítás", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Mentés", diff --git a/src/lang/id-ID.json b/src/lang/id-ID.json index 856727650..64fed9e1f 100644 --- a/src/lang/id-ID.json +++ b/src/lang/id-ID.json @@ -126,7 +126,7 @@ "label.reset": "Atur ulang", "label.reset-website": "Atur ulang statistik", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Simpan", diff --git a/src/lang/it-IT.json b/src/lang/it-IT.json index 8bce2f4e0..29c664b5b 100644 --- a/src/lang/it-IT.json +++ b/src/lang/it-IT.json @@ -126,7 +126,7 @@ "label.reset": "Reset", "label.reset-website": "Resetta le statistiche", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Salva", diff --git a/src/lang/ja-JP.json b/src/lang/ja-JP.json index 3af2f7b0a..8e26205e4 100644 --- a/src/lang/ja-JP.json +++ b/src/lang/ja-JP.json @@ -126,7 +126,7 @@ "label.reset": "リセット", "label.reset-website": "Webサイトをリセットする", "label.retention": "保持", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "ロール", "label.run-query": "クエリ実行", "label.save": "保存", diff --git a/src/lang/km-KH.json b/src/lang/km-KH.json index 3f7d089d1..2333fe6b8 100644 --- a/src/lang/km-KH.json +++ b/src/lang/km-KH.json @@ -126,7 +126,7 @@ "label.reset": "កំណត់ឡើងវិញ", "label.reset-website": "កំណត់ស្ថិតិឡើងវិញ", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "រក្សាទុក", diff --git a/src/lang/ko-KR.json b/src/lang/ko-KR.json index dcbdd77c5..b4b7d566b 100644 --- a/src/lang/ko-KR.json +++ b/src/lang/ko-KR.json @@ -126,7 +126,7 @@ "label.reset": "리셋", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "저장", diff --git a/src/lang/lt-LT.json b/src/lang/lt-LT.json index aa75f10f7..13f2b06b5 100644 --- a/src/lang/lt-LT.json +++ b/src/lang/lt-LT.json @@ -126,7 +126,7 @@ "label.reset": "Atstatyti", "label.reset-website": "Atstatyti statistikos duomenis", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Išsaugoti", diff --git a/src/lang/mn-MN.json b/src/lang/mn-MN.json index aad28ef20..ecb4e1fb2 100644 --- a/src/lang/mn-MN.json +++ b/src/lang/mn-MN.json @@ -126,7 +126,7 @@ "label.reset": "Дахин эхлүүлэх", "label.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Эрх", "label.run-query": "Run query", "label.save": "Хадгалах", diff --git a/src/lang/ms-MY.json b/src/lang/ms-MY.json index af5c34f07..256a4d0b6 100644 --- a/src/lang/ms-MY.json +++ b/src/lang/ms-MY.json @@ -126,7 +126,7 @@ "label.reset": "Tetapkan semula", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Simpan", diff --git a/src/lang/my-MM.json b/src/lang/my-MM.json index 8253705ab..bdfa3c6ab 100644 --- a/src/lang/my-MM.json +++ b/src/lang/my-MM.json @@ -126,7 +126,7 @@ "label.reset": "ပြန်စမည်", "label.reset-website": "ဝက်ဘ်ဆိုဒ်ဒေတာကိုဖျက်မည်", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "အခန်းကဏ္ဍ", "label.run-query": "Query ကိုလုပ်ဆောင်မည်", "label.save": "သိမ်းဆည်းမည်", diff --git a/src/lang/nb-NO.json b/src/lang/nb-NO.json index 18fa765ee..5744d211c 100644 --- a/src/lang/nb-NO.json +++ b/src/lang/nb-NO.json @@ -126,7 +126,7 @@ "label.reset": "Nullstill", "label.reset-website": "Nullstill statistikk", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Lagre", diff --git a/src/lang/nl-NL.json b/src/lang/nl-NL.json index d0c3f6782..9dfcbb92a 100644 --- a/src/lang/nl-NL.json +++ b/src/lang/nl-NL.json @@ -126,7 +126,7 @@ "label.reset": "Opnieuw instellen", "label.reset-website": "Statistieken opnieuw instellen", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Gebruikersrol", "label.run-query": "Run query", "label.save": "Opslaan", diff --git a/src/lang/pl-PL.json b/src/lang/pl-PL.json index 8045af067..37a77b2e6 100644 --- a/src/lang/pl-PL.json +++ b/src/lang/pl-PL.json @@ -126,7 +126,7 @@ "label.reset": "Zresetuj", "label.reset-website": "Zresetuj statystyki", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Uruchom zapytanie", "label.save": "Zapisz", diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json index 22169e3ae..ae3459863 100644 --- a/src/lang/pt-BR.json +++ b/src/lang/pt-BR.json @@ -126,7 +126,7 @@ "label.reset": "Redefinir", "label.reset-website": "Redefinir estatísticas", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Papel", "label.run-query": "Executar query", "label.save": "Salvar", diff --git a/src/lang/pt-PT.json b/src/lang/pt-PT.json index 28a798e31..3e1df4de8 100644 --- a/src/lang/pt-PT.json +++ b/src/lang/pt-PT.json @@ -126,7 +126,7 @@ "label.reset": "Repor", "label.reset-website": "Repor estatísticas", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Guardar", diff --git a/src/lang/ro-RO.json b/src/lang/ro-RO.json index 9c8c8c3c1..d3b9d3db8 100644 --- a/src/lang/ro-RO.json +++ b/src/lang/ro-RO.json @@ -126,7 +126,7 @@ "label.reset": "Resetează", "label.reset-website": "Resetează statisticile pentru site", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Salvează", diff --git a/src/lang/ru-RU.json b/src/lang/ru-RU.json index 6eed37257..3063cbfa9 100644 --- a/src/lang/ru-RU.json +++ b/src/lang/ru-RU.json @@ -126,7 +126,7 @@ "label.reset": "Сбросить", "label.reset-website": "Сбросить статистику", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Роль", "label.run-query": "Run query", "label.save": "Сохранить", diff --git a/src/lang/si-LK.json b/src/lang/si-LK.json index 92bbe7360..d9a61eb0a 100644 --- a/src/lang/si-LK.json +++ b/src/lang/si-LK.json @@ -126,7 +126,7 @@ "label.reset": "යළි පිහිටුවන්න", "label.reset-website": "සංඛ්යා ලේඛන නැවත සකසන්න", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "සුරකින්න", diff --git a/src/lang/sk-SK.json b/src/lang/sk-SK.json index 534ace423..7d9073dd6 100644 --- a/src/lang/sk-SK.json +++ b/src/lang/sk-SK.json @@ -126,7 +126,7 @@ "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Uložiť", diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index f73aa265f..1863eb879 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -126,7 +126,7 @@ "label.reset": "Ponastavi", "label.reset-website": "Ponastavi statistiko", "label.retention": "Ohranjanje uporabnikov", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Vloga", "label.run-query": "Izvedi poizvedbo", "label.save": "Shrani", diff --git a/src/lang/sv-SE.json b/src/lang/sv-SE.json index 532b9039d..1496594f8 100644 --- a/src/lang/sv-SE.json +++ b/src/lang/sv-SE.json @@ -126,7 +126,7 @@ "label.reset": "Återställ", "label.reset-website": "Återställ statistik", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Roll", "label.run-query": "Run query", "label.save": "Spara", diff --git a/src/lang/ta-IN.json b/src/lang/ta-IN.json index 64b6bf2ba..53b7659e8 100644 --- a/src/lang/ta-IN.json +++ b/src/lang/ta-IN.json @@ -126,7 +126,7 @@ "label.reset": "மீட்டமை", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "சேமி", diff --git a/src/lang/th-TH.json b/src/lang/th-TH.json index 1ddaa4d01..ee2fc19c4 100644 --- a/src/lang/th-TH.json +++ b/src/lang/th-TH.json @@ -126,7 +126,7 @@ "label.reset": "รีเซต", "label.reset-website": "รีเซตข้อมูลสถิติ", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "บันทึก", diff --git a/src/lang/tr-TR.json b/src/lang/tr-TR.json index b333131fb..6eac527b3 100644 --- a/src/lang/tr-TR.json +++ b/src/lang/tr-TR.json @@ -126,7 +126,7 @@ "label.reset": "Sıfırla", "label.reset-website": "Reset statistics", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Kaydet", diff --git a/src/lang/uk-UA.json b/src/lang/uk-UA.json index e95815f13..959956487 100644 --- a/src/lang/uk-UA.json +++ b/src/lang/uk-UA.json @@ -126,7 +126,7 @@ "label.reset": "Скинути", "label.reset-website": "Скинути статистику сайту", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Зберегти", diff --git a/src/lang/ur-PK.json b/src/lang/ur-PK.json index 9fbfb0797..5edaddd81 100644 --- a/src/lang/ur-PK.json +++ b/src/lang/ur-PK.json @@ -126,7 +126,7 @@ "label.reset": "دوبارہ ترتیب دیں", "label.reset-website": "اعدادوشمار کو دوبارہ ترتیب دیں", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "محفوظ کریں", diff --git a/src/lang/vi-VN.json b/src/lang/vi-VN.json index ff2915c79..9c629100b 100644 --- a/src/lang/vi-VN.json +++ b/src/lang/vi-VN.json @@ -126,7 +126,7 @@ "label.reset": "Tái thiết lập", "label.reset-website": "Tái thiết lập thống kê", "label.retention": "Retention", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "Role", "label.run-query": "Run query", "label.save": "Lưu", diff --git a/src/lang/zh-CN.json b/src/lang/zh-CN.json index 2ebe75f2e..c41602802 100644 --- a/src/lang/zh-CN.json +++ b/src/lang/zh-CN.json @@ -126,7 +126,7 @@ "label.reset": "重置", "label.reset-website": "重置统计数据", "label.retention": "保留", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "角色", "label.run-query": "查询", "label.save": "保存", diff --git a/src/lang/zh-TW.json b/src/lang/zh-TW.json index 22291ed06..6a56ea78d 100644 --- a/src/lang/zh-TW.json +++ b/src/lang/zh-TW.json @@ -126,7 +126,7 @@ "label.reset": "重設", "label.reset-website": "重設網站", "label.retention": "保留", - "label.retention-description": "Measure you website stickiness by tracking how often users return.", + "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.role": "角色", "label.run-query": "執行查詢", "label.save": "儲存", From f4f869f91977b922af0ff962162e7c31e51af605 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Sat, 2 Sep 2023 08:58:25 +0000 Subject: [PATCH 170/357] localize visitors in map tooltip --- src/components/common/WorldMap.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/common/WorldMap.js b/src/components/common/WorldMap.js index b593099b7..6ae84677e 100644 --- a/src/components/common/WorldMap.js +++ b/src/components/common/WorldMap.js @@ -8,6 +8,7 @@ import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants'; import useTheme from 'components/hooks/useTheme'; import useCountryNames from 'components/hooks/useCountryNames'; import useLocale from 'components/hooks/useLocale'; +import useMessages from 'components/hooks/useMessages'; import { formatLongNumber } from 'lib/format'; import { percentFilter } from 'lib/filters'; import styles from './WorldMap.module.css'; @@ -17,7 +18,9 @@ export function WorldMap({ data, className }) { const [tooltip, setTooltipPopup] = useState(); const { theme, colors } = useTheme(); const { locale } = useLocale(); + const { formatMessage, labels } = useMessages(); const countryNames = useCountryNames(locale); + const visitorsLabel = formatMessage(labels.visitors).toLocaleLowerCase(locale); const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]); function getFillColor(code) { @@ -40,7 +43,7 @@ export function WorldMap({ data, className }) { function handleHover(code) { if (code === 'AQ') return; const country = metrics?.find(({ x }) => x === code); - setTooltipPopup(`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} visitors`); + setTooltipPopup(`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} ${visitorsLabel}`); } return ( From e41a83316adf2e99e750c6ba10afad37f8cf84d2 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Sat, 2 Sep 2023 11:05:06 +0000 Subject: [PATCH 171/357] update fr-FR --- src/lang/fr-FR.json | 62 ++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/lang/fr-FR.json b/src/lang/fr-FR.json index 092837eb5..bb4a56a55 100644 --- a/src/lang/fr-FR.json +++ b/src/lang/fr-FR.json @@ -16,20 +16,20 @@ "label.before": "Avant", "label.bounce-rate": "Taux de rebond", "label.breakdown": "Répartition", - "label.browser": "Browser", + "label.browser": "Navigateur", "label.browsers": "Navigateurs", "label.cancel": "Annuler", "label.change-password": "Changer le mot de passe", "label.cities": "Villes", - "label.city": "City", + "label.city": "Ville", "label.clear-all": "Réinitialiser", "label.confirm": "Confirmer", "label.confirm-password": "Confirmation du mot de passe", "label.contains": "Contient", "label.continue": "Continuer", "label.countries": "Pays", - "label.country": "Country", - "label.create": "Create", + "label.country": "Pays", + "label.create": "Créer", "label.create-report": "Créer un rapport", "label.create-team": "Créer une équipe", "label.create-user": "Créer un utilisateur", @@ -40,7 +40,7 @@ "label.data": "Données", "label.date": "Date", "label.date-range": "Période", - "label.day": "Day", + "label.day": "Jour", "label.default-date-range": "Période par défaut", "label.delete": "Supprimer", "label.delete-team": "Supprimer l'équipe", @@ -49,35 +49,35 @@ "label.description": "Description", "label.desktop": "Ordinateur", "label.details": "Détails", - "label.device": "Device", + "label.device": "Appareil", "label.devices": "Appareils", "label.dismiss": "Ignorer", "label.does-not-contain": "Ne contient pas", "label.domain": "Domaine", - "label.dropoff": "Dropoff", + "label.dropoff": "Abandons", "label.edit": "Modifier", "label.edit-dashboard": "Modifier le tableau de bord", "label.enable-share-url": "Activer l'URL de partage", - "label.event": "Event", - "label.event-data": "Données d'événements", - "label.events": "Événements", + "label.event": "Évènement", + "label.event-data": "Données d'évènements", + "label.events": "Évènements", "label.false": "Faux", "label.field": "Champ", "label.fields": "Champs", - "label.filter": "Filter", + "label.filter": "Filtrer", "label.filter-combined": "Combiné", "label.filter-raw": "Brut", "label.filters": "Filtres", "label.funnel": "Entonnoir", - "label.funnel-description": "Understand the conversion and drop-off rate of users.", + "label.funnel-description": "Suivi des conversions et des taux d'abandons.", "label.greater-than": "Supérieur à", "label.greater-than-equals": "Supérieur ou égal à", "label.insights": "Insights", - "label.insights-description": "Dive deeper into your data by using segments and filters.", - "label.is": "Est égal", - "label.is-not": "N'est pas égal", - "label.is-not-set": "Is not set", - "label.is-set": "Is set", + "label.insights-description": "Analyse précise des données en utilisant des segments et des filtres.", + "label.is": "Est", + "label.is-not": "N'est pas", + "label.is-not-set": "N'est pas défini", + "label.is-set": "Est défini", "label.join": "Rejoindre", "label.join-team": "Rejoindre une équipe", "label.language": "Langue", @@ -96,42 +96,42 @@ "label.min": "Min", "label.mobile": "Téléphone", "label.more": "Plus", - "label.my-websites": "My websites", + "label.my-websites": "Mes sites", "label.name": "Nom", "label.new-password": "Nouveau mot de passe", "label.none": "Aucun·e", "label.os": "OS", "label.overview": "Vue d'ensemble", "label.owner": "Propriétaire", - "label.page-of": "Page {current} of {total}", + "label.page-of": "Page {current} sur {total}", "label.page-views": "Pages vues", - "label.pageTitle": "Page title", + "label.pageTitle": "Titre de page", "label.pages": "Pages", "label.password": "Mot de passe", "label.powered-by": "Propulsé par {name}", "label.profile": "Profil", "label.queries": "Requêtes", "label.query": "Requête", - "label.query-parameters": "Paramètres d'URL", + "label.query-parameters": "Paramètres de requête", "label.realtime": "Temps réel", - "label.referrer": "Referrer", + "label.referrer": "Site référent", "label.referrers": "Sites référents", "label.refresh": "Rafraîchir", "label.regenerate": "Régénérer", - "label.region": "Region", + "label.region": "Région", "label.regions": "Régions", "label.remove": "Retirer", "label.reports": "Rapports", "label.required": "Requis", "label.reset": "Réinitialiser", "label.reset-website": "Réinitialiser les statistiques", - "label.retention": "Retention", - "label.retention-description": "Measure your website stickiness by tracking how often users return.", + "label.retention": "Rétention", + "label.retention-description": "Mesure de l'atractivité du site en visualisant les taux d'utilisateurs qui reviennent.", "label.role": "Rôle", "label.run-query": "Éxécuter la requête", "label.save": "Enregistrer", "label.screens": "Résolutions d'écran", - "label.search": "Search", + "label.search": "Rechercher", "label.select-date": "Choisir une période", "label.select-website": "Choisir un site", "label.sessions": "Sessions", @@ -144,9 +144,9 @@ "label.team-guest": "Invité dans l'équipe", "label.team-id": "ID d'équipe", "label.team-member": "Membre de l'équipe", - "label.team-name": "Team name", + "label.team-name": "Nom de l'équipe", "label.team-owner": "Propriétaire de l'équipe", - "label.team-websites": "Team websites", + "label.team-websites": "Sites d'équipes", "label.teams": "Équipes", "label.theme": "Thème", "label.this-month": "Ce mois", @@ -176,7 +176,7 @@ "label.view-only": "Consultation", "label.views": "Vues", "label.visitors": "Visiteurs", - "label.website": "Website", + "label.website": "Site", "label.website-id": "ID de site", "label.websites": "Sites", "label.window": "Fenêtre", @@ -194,7 +194,7 @@ "message.incorrect-username-password": "Nom d'utilisateur/Mot de passe incorrect.", "message.invalid-domain": "Domaine invalide", "message.min-password-length": "Taille minimale de {n} caractères", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.new-version-available": "Une nouvelle version d'Umami {version} est disponible !", "message.no-data-available": "Aucune donnée disponible.", "message.no-event-data": "Aucune donnée d'événement disponible.", "message.no-match-password": "Les mots de passe ne correspondent pas", @@ -206,7 +206,7 @@ "message.page-not-found": "Page non trouvée.", "message.reset-website": "Pour réinitialiser ce site, taper {confirmation} ci-dessous pour confirmer.", "message.reset-website-warning": "Toutes les statistiques pour ce site seront supprimées, mais votre code de suivi restera intact.", - "message.saved": "Enregistré avec succès.", + "message.saved": "Enregistré.", "message.share-url": "Les statistiques de votre site sont accessibles publiquement sur cette URL :", "message.team-already-member": "Vous êtes déjà membre de cette équipe.", "message.team-not-found": "Équipe non trouvée.", From 105b3c1e3180a5d3a2ce4eac63a79a91407fc67e Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Sat, 2 Sep 2023 12:39:23 +0000 Subject: [PATCH 172/357] restore lost translations --- src/lang/my-MM.json | 50 ++++++++++++++++++++++----------------------- src/lang/sl-SI.json | 8 ++++---- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/lang/my-MM.json b/src/lang/my-MM.json index bdfa3c6ab..704e29496 100644 --- a/src/lang/my-MM.json +++ b/src/lang/my-MM.json @@ -6,16 +6,16 @@ "label.add-description": "အကြောင်းအရာဖော်ပြချက် ထည့်မည်", "label.add-website": "ဝက်ဘ်ဆိုဒ်ထည့်မည်", "label.admin": "အက်ဒမင်", - "label.after": "After", + "label.after": "ပြီးနောက်", "label.all": "အားလုံး", "label.all-time": "အချိန်အစမှအခုထိ", "label.analytics": "အန်နလစ်တစ်", - "label.average": "Average", + "label.average": "ပျမ်းမျှ", "label.average-visit-time": "ဝဘက်ဘ်ဆိုဒ်တွင် ပျမ်းမျှကုန်ဆုံးချိန်", "label.back": "နောက်သို့", - "label.before": "Before", + "label.before": "မတိုင်မီ", "label.bounce-rate": "Bounce နှုန်း", - "label.breakdown": "Breakdown", + "label.breakdown": "ခွဲခြမ်းစိတ်ဖြာမှု", "label.browser": "Browser", "label.browsers": "ဝက်ဘ်ဘရောင်ဇာများ", "label.cancel": "မလုပ်တော့ပါ", @@ -25,12 +25,12 @@ "label.clear-all": "အားလုံးကိုဖျက်မည်", "label.confirm": "အတည်ပြုသည်", "label.confirm-password": "စကားဝှက်အတည်ပြုသည်", - "label.contains": "Contains", + "label.contains": "ပါဝင်သည်", "label.continue": "ဆက်သွားမည်", "label.countries": "နိုင်ငံများ", "label.country": "Country", "label.create": "Create", - "label.create-report": "Create report", + "label.create-report": "ရီပို့လုပ်မည်", "label.create-team": "Team ပြုလုပ်မည်", "label.create-user": "အသုံးပြုသူထည့်မည်", "label.created": "ပြုလုပ်ပြီးသော", @@ -46,13 +46,13 @@ "label.delete-team": "Team ကိုဖျက်မည်", "label.delete-user": "အသုံးပြုသူကိုဖျက်မည်", "label.delete-website": "ဝက်ဘ်ဆိုဒ်ကိုဖျက်မည်", - "label.description": "Description", + "label.description": "ရှင်းပြချက်", "label.desktop": "စားပွဲတင်ကွန်ပျူတာ", "label.details": "အသေးစိတ်", "label.device": "Device", "label.devices": "အသုံးပြုသည့် ကိရိယာများ", "label.dismiss": "ပိတ်ပါ", - "label.does-not-contain": "Does not contain", + "label.does-not-contain": "မပါဝင်ပါ", "label.domain": "ဒိုမိန်း", "label.dropoff": "Dropoff", "label.edit": "ပြုပြင်မည်", @@ -61,17 +61,17 @@ "label.event": "အဖြစ်အပျက်", "label.event-data": "အဖြစ်အပျက် ဒေတာ", "label.events": "အဖြစ်အပျက်များ", - "label.false": "False", + "label.false": "မှားသည်", "label.field": "Field အမည်", "label.fields": "Field အမည်များ", "label.filter": "Filter", "label.filter-combined": "ပေါင်းစပ်ပြီး", "label.filter-raw": "အရှိအတိုင်း", - "label.filters": "Filters", + "label.filters": "Filter များ", "label.funnel": "ဖန်နယ်", "label.funnel-description": "Understand the conversion and drop-off rate of users.", - "label.greater-than": "Greater than", - "label.greater-than-equals": "Greater than or equals", + "label.greater-than": "ထက်ပို၍ကြီးသည်", + "label.greater-than-equals": "ထက်ပို၍ကြီးသည်သို့မဟုတ်တူသည်", "label.insights": "အသေးစိတ်သိမြင်နိုင်ရန်", "label.insights-description": "Dive deeper into your data by using segments and filters.", "label.is": "Is", @@ -87,21 +87,21 @@ "label.last-hours": "လွန်ခဲ့သော {x} နာရီက", "label.leave": "ထွက်မည်", "label.leave-team": "အသင်းမှထွက်မည်", - "label.less-than": "Less than", - "label.less-than-equals": "Less than or equals", + "label.less-than": "ထက်ပို၍ငယ်သည်", + "label.less-than-equals": "ထက်ပို၍ငယ်သည်သို့မဟုတ်တူသည်", "label.login": "လော့ဂ်အင်", "label.logout": "လော့ဂ်အောက်လုပ်မည်", - "label.max": "Max", + "label.max": "အများဆုံး", "label.members": "အဖွဲ့ဝင်များ", - "label.min": "Min", + "label.min": "အနည်းဆုံး", "label.mobile": "မိုဘိုင်း", "label.more": "နောက်ထပ်", "label.my-websites": "My websites", "label.name": "အမည်", "label.new-password": "စကားဝှက်အသစ်", "label.none": "မရှိပါ", - "label.os": "OS", - "label.overview": "Overview", + "label.os": "ကွန်ပျူတာလည်ပတ်မှုစနစ်", + "label.overview": "အပေါ်ယံမြင်ကွင်း", "label.owner": "ပိုင်ဆိုင်သူ", "label.page-of": "Page {current} of {total}", "label.page-views": "ဝင်ရောက်ကြည့်ရှုသူ", @@ -138,7 +138,7 @@ "label.settings": "ဆက်တင်များ", "label.share-url": "URL ကိုရှဲမည်", "label.single-day": "တစ်ရက်အတွင်း", - "label.sum": "Sum", + "label.sum": "ပေါင်းလဒ်", "label.tablet": "တက်ဘလက်", "label.team": "အသင်း", "label.team-guest": "အသင်း ဧည့်သည်", @@ -156,21 +156,21 @@ "label.title": "ခေါင်းစဥ်", "label.today": "ယနေ့", "label.toggle-charts": "ဇယားများကို အဖွင့်အပိတ်လုပ်မည်", - "label.total": "Total", - "label.total-records": "Total records", + "label.total": "စုစုပေါင်း", + "label.total-records": "မှတ်တမ်းစုစုပေါင်း", "label.tracking-code": "ထရက်လုပ်သည့် ကုဒ်", - "label.true": "True", - "label.type": "Type", + "label.true": "မှန်သည်", + "label.type": "အမျိုးအစား", "label.unique": "Unique", "label.unique-visitors": "ဝင်ရောက်သူ (ထပ်ခြင်းမရှိ)", "label.unknown": "မသိသော", - "label.untitled": "Untitled", + "label.untitled": "ခေါင်းစဉ်မရှိ", "label.url": "URL", "label.urls": "URL များ", "label.user": "အသုံးပြုသူ", "label.username": "အသုံးပြုသူအမည်", "label.users": "အသုံးပြုသူများ", - "label.value": "Value", + "label.value": "တန်ဖိုး", "label.view": "ဝင်ရောက်ကြည့်ရှုမှု", "label.view-details": "အသေးစိတ်ကို ကြည့်ရှုမည်", "label.view-only": "ဝင်ရောက်ကြည့်ရှုမှုများသာ", diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index 1863eb879..e7347bc43 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -130,10 +130,10 @@ "label.role": "Vloga", "label.run-query": "Izvedi poizvedbo", "label.save": "Shrani", - "label.screens": "Screens", - "label.select-date": "Select date", - "label.select-website": "Select website", - "label.sessions": "Sessions", + "label.screens": "Zasloni", + "label.select-date": "Izberi datum", + "label.select-website": "Izberi spletno mesto", + "label.sessions": "Seje", "label.settings": "Nastavitve", "label.share-url": "Deli povezavo", "label.single-day": "En dan", From a591eb867dcad30fddeabd07492c3138beeec0b9 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Sat, 2 Sep 2023 12:50:18 +0000 Subject: [PATCH 173/357] follow-up: add missing new label in sl-SI --- src/lang/sl-SI.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index e7347bc43..66ce08e08 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -131,6 +131,7 @@ "label.run-query": "Izvedi poizvedbo", "label.save": "Shrani", "label.screens": "Zasloni", + "label.search": "Search", "label.select-date": "Izberi datum", "label.select-website": "Izberi spletno mesto", "label.sessions": "Seje", From 4bce72350b234e4baebc5215246df66b7c41eff2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 2 Sep 2023 16:53:26 -0700 Subject: [PATCH 174/357] Include HOSTNAME variable. --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index e0c7e8c37..6674163a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,6 +53,7 @@ USER nextjs EXPOSE 3000 +ENV HOSTNAME 0.0.0.0 ENV PORT 3000 CMD ["yarn", "start-docker"] From cec186e4c177ccfaa1a141de236802b3cd216d8d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 4 Sep 2023 13:42:39 -0700 Subject: [PATCH 175/357] Added version to menu. --- src/components/input/ProfileButton.js | 4 +++- src/components/input/ProfileButton.module.css | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/input/ProfileButton.js b/src/components/input/ProfileButton.js index 015c0ad8d..35b0eb454 100644 --- a/src/components/input/ProfileButton.js +++ b/src/components/input/ProfileButton.js @@ -3,8 +3,9 @@ import { useRouter } from 'next/router'; import Icons from 'components/icons'; import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; -import styles from './ProfileButton.module.css'; import useLocale from 'components/hooks/useLocale'; +import { CURRENT_VERSION } from 'lib/constants'; +import styles from './ProfileButton.module.css'; export function ProfileButton() { const { formatMessage, labels } = useMessages(); @@ -51,6 +52,7 @@ export function ProfileButton() { {formatMessage(labels.logout)} )} +
{`v${CURRENT_VERSION}`}
diff --git a/src/components/input/ProfileButton.module.css b/src/components/input/ProfileButton.module.css index 8b1897bd5..e78433448 100644 --- a/src/components/input/ProfileButton.module.css +++ b/src/components/input/ProfileButton.module.css @@ -8,3 +8,11 @@ gap: 12px; background: var(--base50); } + +.version { + font-family: monospace; + font-size: 11px; + color: var(--base600); + text-align: right; + margin-right: 10px; +} From ea43872dd0ea82c66325b92f2686e217c0d978b0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 4 Sep 2023 22:50:53 -0700 Subject: [PATCH 176/357] Update message bundles. --- public/intl/messages/am-ET.json | 36 +++ public/intl/messages/ar-SA.json | 36 +++ public/intl/messages/be-BY.json | 36 +++ public/intl/messages/bn-BD.json | 36 +++ public/intl/messages/ca-ES.json | 36 +++ public/intl/messages/cs-CZ.json | 36 +++ public/intl/messages/da-DK.json | 36 +++ public/intl/messages/de-CH.json | 36 +++ public/intl/messages/de-DE.json | 158 ++++++----- public/intl/messages/el-GR.json | 36 +++ public/intl/messages/en-GB.json | 36 +++ public/intl/messages/en-US.json | 36 +++ public/intl/messages/es-ES.json | 36 +++ public/intl/messages/es-MX.json | 36 +++ public/intl/messages/fa-IR.json | 36 +++ public/intl/messages/fi-FI.json | 36 +++ public/intl/messages/fo-FO.json | 36 +++ public/intl/messages/fr-FR.json | 88 ++++-- public/intl/messages/ga-ES.json | 36 +++ public/intl/messages/he-IL.json | 36 +++ public/intl/messages/hi-IN.json | 36 +++ public/intl/messages/hr-HR.json | 36 +++ public/intl/messages/hu-HU.json | 36 +++ public/intl/messages/id-ID.json | 36 +++ public/intl/messages/it-IT.json | 36 +++ public/intl/messages/ja-JP.json | 36 +++ public/intl/messages/km-KH.json | 36 +++ public/intl/messages/ko-KR.json | 36 +++ public/intl/messages/lt-LT.json | 36 +++ public/intl/messages/mn-MN.json | 36 +++ public/intl/messages/ms-MY.json | 36 +++ public/intl/messages/my-MM.json | 470 +++++++++++++++++++++----------- public/intl/messages/nb-NO.json | 36 +++ public/intl/messages/nl-NL.json | 36 +++ public/intl/messages/pl-PL.json | 36 +++ public/intl/messages/pt-BR.json | 36 +++ public/intl/messages/pt-PT.json | 36 +++ public/intl/messages/ro-RO.json | 36 +++ public/intl/messages/ru-RU.json | 36 +++ public/intl/messages/si-LK.json | 36 +++ public/intl/messages/sk-SK.json | 36 +++ public/intl/messages/sl-SI.json | 36 +++ public/intl/messages/sv-SE.json | 36 +++ public/intl/messages/ta-IN.json | 36 +++ public/intl/messages/th-TH.json | 36 +++ public/intl/messages/tr-TR.json | 36 +++ public/intl/messages/uk-UA.json | 36 +++ public/intl/messages/ur-PK.json | 36 +++ public/intl/messages/vi-VN.json | 36 +++ public/intl/messages/zh-CN.json | 36 +++ public/intl/messages/zh-TW.json | 36 +++ src/lang/de-DE.json | 4 +- 52 files changed, 2195 insertions(+), 253 deletions(-) diff --git a/public/intl/messages/am-ET.json b/public/intl/messages/am-ET.json index f48fe83c4..cb9ff4f99 100644 --- a/public/intl/messages/am-ET.json +++ b/public/intl/messages/am-ET.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/ar-SA.json b/public/intl/messages/ar-SA.json index a9a12404e..17a859167 100644 --- a/public/intl/messages/ar-SA.json +++ b/public/intl/messages/ar-SA.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "الشاشات" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/be-BY.json b/public/intl/messages/be-BY.json index 4978aa45c..c8081e373 100644 --- a/public/intl/messages/be-BY.json +++ b/public/intl/messages/be-BY.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Экраны" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/bn-BD.json b/public/intl/messages/bn-BD.json index 938f6f98f..6b8875adf 100644 --- a/public/intl/messages/bn-BD.json +++ b/public/intl/messages/bn-BD.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "স্ক্রিনগুলি" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/ca-ES.json b/public/intl/messages/ca-ES.json index 694b49c22..f21a739cc 100644 --- a/public/intl/messages/ca-ES.json +++ b/public/intl/messages/ca-ES.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/cs-CZ.json b/public/intl/messages/cs-CZ.json index 3fd34c31d..e316e9731 100644 --- a/public/intl/messages/cs-CZ.json +++ b/public/intl/messages/cs-CZ.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/da-DK.json b/public/intl/messages/da-DK.json index d8da1c3eb..05b0c572a 100644 --- a/public/intl/messages/da-DK.json +++ b/public/intl/messages/da-DK.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/de-CH.json b/public/intl/messages/de-CH.json index aa0b2d942..ecc153361 100644 --- a/public/intl/messages/de-CH.json +++ b/public/intl/messages/de-CH.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Bildschirmuflösige" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/de-DE.json b/public/intl/messages/de-DE.json index 136cd31dd..5e74647c1 100644 --- a/public/intl/messages/de-DE.json +++ b/public/intl/messages/de-DE.json @@ -20,7 +20,7 @@ "label.add": [ { "type": 0, - "value": "Add" + "value": "Hinzufügen" } ], "label.add-description": [ @@ -32,7 +32,7 @@ "label.add-website": [ { "type": 0, - "value": "Webseite hinzufügen" + "value": "Website hinzufügen" } ], "label.admin": [ @@ -44,7 +44,7 @@ "label.after": [ { "type": 0, - "value": "After" + "value": "Nach" } ], "label.all": [ @@ -86,7 +86,7 @@ "label.before": [ { "type": 0, - "value": "Before" + "value": "Vor" } ], "label.bounce-rate": [ @@ -134,7 +134,7 @@ "label.city": [ { "type": 0, - "value": "City" + "value": "Stadt" } ], "label.clear-all": [ @@ -158,7 +158,7 @@ "label.contains": [ { "type": 0, - "value": "Contains" + "value": "Enthält" } ], "label.continue": [ @@ -176,13 +176,19 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "Land" + } + ], + "label.create": [ + { + "type": 0, + "value": "Create" } ], "label.create-report": [ { "type": 0, - "value": "Report erstellen" + "value": "Bericht erstellen" } ], "label.create-team": [ @@ -230,7 +236,7 @@ "label.date": [ { "type": 0, - "value": "Date" + "value": "Datum" } ], "label.date-range": [ @@ -242,7 +248,7 @@ "label.day": [ { "type": 0, - "value": "Day" + "value": "Tag" } ], "label.default-date-range": [ @@ -272,7 +278,7 @@ "label.delete-website": [ { "type": 0, - "value": "Webseite löschen" + "value": "Website löschen" } ], "label.description": [ @@ -296,7 +302,7 @@ "label.device": [ { "type": 0, - "value": "Device" + "value": "Gerät" } ], "label.devices": [ @@ -314,7 +320,7 @@ "label.does-not-contain": [ { "type": 0, - "value": "Does not contain" + "value": "Enthält nicht" } ], "label.domain": [ @@ -356,7 +362,7 @@ "label.event-data": [ { "type": 0, - "value": "Event daten" + "value": "Eventdaten" } ], "label.events": [ @@ -368,19 +374,25 @@ "label.false": [ { "type": 0, - "value": "False" + "value": "Falsch" } ], "label.field": [ { "type": 0, - "value": "Field" + "value": "Feld" } ], "label.fields": [ { "type": 0, - "value": "Fields" + "value": "Felder" + } + ], + "label.filter": [ + { + "type": 0, + "value": "Filter" } ], "label.filter-combined": [ @@ -398,7 +410,7 @@ "label.filters": [ { "type": 0, - "value": "Filters" + "value": "Filter" } ], "label.funnel": [ @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,28 +443,34 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, - "value": "Is" + "value": "Ist" } ], "label.is-not": [ { "type": 0, - "value": "Is not" + "value": "Ist nicht" } ], "label.is-not-set": [ { "type": 0, - "value": "Is not set" + "value": "Ist nicht gesetzt" } ], "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "Ist gesetzt" } ], "label.join": [ @@ -576,7 +600,7 @@ "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "Meine Websites" } ], "label.name": [ @@ -618,7 +642,7 @@ "label.page-of": [ { "type": 0, - "value": "Page " + "value": "Seite " }, { "type": 1, @@ -626,7 +650,7 @@ }, { "type": 0, - "value": " of " + "value": " von " }, { "type": 1, @@ -642,7 +666,7 @@ "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "Seitentitel" } ], "label.pages": [ @@ -742,7 +766,7 @@ "label.reports": [ { "type": 0, - "value": "Reporte" + "value": "Berichte" } ], "label.required": [ @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Bildschirmauflösungen" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, @@ -808,7 +844,7 @@ "label.sessions": [ { "type": 0, - "value": "Sessions" + "value": "Sitzungen" } ], "label.settings": [ @@ -850,37 +886,37 @@ "label.team-guest": [ { "type": 0, - "value": "Team Gast" + "value": "Gast des Teams" } ], "label.team-id": [ { "type": 0, - "value": "Team ID" + "value": "Team-ID" } ], "label.team-member": [ { "type": 0, - "value": "Team Mitglied" + "value": "Team-Mitglied" } ], "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "Name des Teams" } ], "label.team-owner": [ { "type": 0, - "value": "Team Eigentümer" + "value": "Team-Eigentümer" } ], "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "Team-Websites" } ], "label.teams": [ @@ -940,13 +976,13 @@ "label.total": [ { "type": 0, - "value": "Total" + "value": "Gesamt" } ], "label.total-records": [ { "type": 0, - "value": "Total records" + "value": "Datensätze insgesamt" } ], "label.tracking-code": [ @@ -958,19 +994,19 @@ "label.true": [ { "type": 0, - "value": "True" + "value": "Wahr" } ], "label.type": [ { "type": 0, - "value": "Type" + "value": "Typ" } ], "label.unique": [ { "type": 0, - "value": "Unique" + "value": "Eindeutig" } ], "label.unique-visitors": [ @@ -988,7 +1024,7 @@ "label.untitled": [ { "type": 0, - "value": "Untitled" + "value": "Unbenannt" } ], "label.url": [ @@ -1024,7 +1060,7 @@ "label.value": [ { "type": 0, - "value": "Value" + "value": "Wert" } ], "label.view": [ @@ -1042,7 +1078,7 @@ "label.view-only": [ { "type": 0, - "value": "View only" + "value": "Nur ansehen" } ], "label.views": [ @@ -1060,25 +1096,25 @@ "label.website": [ { "type": 0, - "value": "Webseite" + "value": "Website" } ], "label.website-id": [ { "type": 0, - "value": "Webseite ID" + "value": "Website ID" } ], "label.websites": [ { "type": 0, - "value": "Webseiten" + "value": "Websites" } ], "label.window": [ { "type": 0, - "value": "Window" + "value": "Fenster" } ], "label.yesterday": [ @@ -1166,7 +1202,7 @@ "message.delete-account": [ { "type": 0, - "value": "To delete this account, type " + "value": "Um dieses Konto zu löschen, geben Sie zur Bestätigung " }, { "type": 1, @@ -1174,13 +1210,13 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " in das Feld unten ein." } ], "message.delete-website": [ { "type": 0, - "value": "To delete this website, type " + "value": "Um diese Website zu löschen, geben Sie zur Bestätigung " }, { "type": 1, @@ -1188,7 +1224,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " in das Feld unten ein." } ], "message.delete-website-warning": [ @@ -1238,7 +1274,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimale länge von " + "value": "Minimale Länge von " }, { "type": 1, @@ -1252,15 +1288,11 @@ "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "Eine neue Version von Umami ist verfügbar: " }, { "type": 1, "value": "version" - }, - { - "type": 0, - "value": " is available!" } ], "message.no-data-available": [ @@ -1272,7 +1304,7 @@ "message.no-event-data": [ { "type": 0, - "value": "No event data is available." + "value": "Es sind keine Ereignisdaten verfügbar." } ], "message.no-match-password": [ @@ -1308,7 +1340,7 @@ "message.no-websites-configured": [ { "type": 0, - "value": "Es ist keine Webseite vorhanden." + "value": "Es ist keine Website vorhanden." } ], "message.page-not-found": [ @@ -1320,7 +1352,7 @@ "message.reset-website": [ { "type": 0, - "value": "To reset this website, type " + "value": "Um diese Website zurückzusetzen, geben Sie zur Bestätigung " }, { "type": 1, @@ -1328,13 +1360,13 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " in das Feld unten ein." } ], "message.reset-website-warning": [ { "type": 0, - "value": "Alle Daten für diese Webseite werden gelöscht, jedoch bleibt der Tracking Code bestehen." + "value": "Alle Daten für diese Website werden gelöscht, jedoch bleibt der Tracking Code bestehen." } ], "message.saved": [ @@ -1346,7 +1378,7 @@ "message.share-url": [ { "type": 0, - "value": "Ihre Webseitenstatistik ist unter der folgenden URL öffentlich zugänglich:" + "value": "Die Statistiken Ihrer Website sind unter folgender URL öffentlich zugänglich:" } ], "message.team-already-member": [ @@ -1364,7 +1396,7 @@ "message.team-websites-info": [ { "type": 0, - "value": "Webseiten können von jedem im Team eingesehen werden." + "value": "Websites können von jedem im Team eingesehen werden." } ], "message.tracking-code": [ diff --git a/public/intl/messages/el-GR.json b/public/intl/messages/el-GR.json index d3ff5e424..eb6b73ce0 100644 --- a/public/intl/messages/el-GR.json +++ b/public/intl/messages/el-GR.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/en-GB.json b/public/intl/messages/en-GB.json index 0e6ac6149..68f242481 100644 --- a/public/intl/messages/en-GB.json +++ b/public/intl/messages/en-GB.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index 64a99ae1b..0ee5b1e64 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/es-ES.json b/public/intl/messages/es-ES.json index 43e101709..0e1316218 100644 --- a/public/intl/messages/es-ES.json +++ b/public/intl/messages/es-ES.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Campos" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Pantallas" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/es-MX.json b/public/intl/messages/es-MX.json index c238951fe..c3ef099df 100644 --- a/public/intl/messages/es-MX.json +++ b/public/intl/messages/es-MX.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Pantallas" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/fa-IR.json b/public/intl/messages/fa-IR.json index 757b5ae8e..e09b155b6 100644 --- a/public/intl/messages/fa-IR.json +++ b/public/intl/messages/fa-IR.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/fi-FI.json b/public/intl/messages/fi-FI.json index 5fdf5b193..2130f82cd 100644 --- a/public/intl/messages/fi-FI.json +++ b/public/intl/messages/fi-FI.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/fo-FO.json b/public/intl/messages/fo-FO.json index 3eb3f452c..bc11c56e1 100644 --- a/public/intl/messages/fo-FO.json +++ b/public/intl/messages/fo-FO.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/fr-FR.json b/public/intl/messages/fr-FR.json index 326c99a46..99937de70 100644 --- a/public/intl/messages/fr-FR.json +++ b/public/intl/messages/fr-FR.json @@ -104,7 +104,7 @@ "label.browser": [ { "type": 0, - "value": "Browser" + "value": "Navigateur" } ], "label.browsers": [ @@ -134,7 +134,7 @@ "label.city": [ { "type": 0, - "value": "City" + "value": "Ville" } ], "label.clear-all": [ @@ -176,7 +176,13 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "Pays" + } + ], + "label.create": [ + { + "type": 0, + "value": "Créer" } ], "label.create-report": [ @@ -242,7 +248,7 @@ "label.day": [ { "type": 0, - "value": "Day" + "value": "Jour" } ], "label.default-date-range": [ @@ -296,7 +302,7 @@ "label.device": [ { "type": 0, - "value": "Device" + "value": "Appareil" } ], "label.devices": [ @@ -326,7 +332,7 @@ "label.dropoff": [ { "type": 0, - "value": "Dropoff" + "value": "Abandons" } ], "label.edit": [ @@ -350,19 +356,19 @@ "label.event": [ { "type": 0, - "value": "Event" + "value": "Évènement" } ], "label.event-data": [ { "type": 0, - "value": "Données d'événements" + "value": "Données d'évènements" } ], "label.events": [ { "type": 0, - "value": "Événements" + "value": "Évènements" } ], "label.false": [ @@ -383,6 +389,12 @@ "value": "Champs" } ], + "label.filter": [ + { + "type": 0, + "value": "Filtrer" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Entonnoir" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Suivi des conversions et des taux d'abandons." + } + ], "label.greater-than": [ { "type": 0, @@ -425,28 +443,34 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Analyse précise des données en utilisant des segments et des filtres." + } + ], "label.is": [ { "type": 0, - "value": "Est égal" + "value": "Est" } ], "label.is-not": [ { "type": 0, - "value": "N'est pas égal" + "value": "N'est pas" } ], "label.is-not-set": [ { "type": 0, - "value": "Is not set" + "value": "N'est pas défini" } ], "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "Est défini" } ], "label.join": [ @@ -568,7 +592,7 @@ "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "Mes sites" } ], "label.name": [ @@ -618,7 +642,7 @@ }, { "type": 0, - "value": " of " + "value": " sur " }, { "type": 1, @@ -634,7 +658,7 @@ "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "Titre de page" } ], "label.pages": [ @@ -680,7 +704,7 @@ "label.query-parameters": [ { "type": 0, - "value": "Paramètres d'URL" + "value": "Paramètres de requête" } ], "label.realtime": [ @@ -692,7 +716,7 @@ "label.referrer": [ { "type": 0, - "value": "Referrer" + "value": "Site référent" } ], "label.referrers": [ @@ -716,7 +740,7 @@ "label.region": [ { "type": 0, - "value": "Region" + "value": "Région" } ], "label.regions": [ @@ -758,7 +782,13 @@ "label.retention": [ { "type": 0, - "value": "Retention" + "value": "Rétention" + } + ], + "label.retention-description": [ + { + "type": 0, + "value": "Mesure de l'atractivité du site en visualisant les taux d'utilisateurs qui reviennent." } ], "label.role": [ @@ -785,6 +815,12 @@ "value": "Résolutions d'écran" } ], + "label.search": [ + { + "type": 0, + "value": "Rechercher" + } + ], "label.select-date": [ { "type": 0, @@ -860,7 +896,7 @@ "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "Nom de l'équipe" } ], "label.team-owner": [ @@ -872,7 +908,7 @@ "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "Sites d'équipes" } ], "label.teams": [ @@ -1052,7 +1088,7 @@ "label.website": [ { "type": 0, - "value": "Website" + "value": "Site" } ], "label.website-id": [ @@ -1248,7 +1284,7 @@ "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "Une nouvelle version d'Umami " }, { "type": 1, @@ -1256,7 +1292,7 @@ }, { "type": 0, - "value": " is available!" + "value": " est disponible !" } ], "message.no-data-available": [ @@ -1336,7 +1372,7 @@ "message.saved": [ { "type": 0, - "value": "Enregistré avec succès." + "value": "Enregistré." } ], "message.share-url": [ diff --git a/public/intl/messages/ga-ES.json b/public/intl/messages/ga-ES.json index d086b57f8..b5fabeffe 100644 --- a/public/intl/messages/ga-ES.json +++ b/public/intl/messages/ga-ES.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -777,6 +801,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -801,6 +831,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/he-IL.json b/public/intl/messages/he-IL.json index dc206268c..16f625257 100644 --- a/public/intl/messages/he-IL.json +++ b/public/intl/messages/he-IL.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -761,6 +785,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -785,6 +815,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/hi-IN.json b/public/intl/messages/hi-IN.json index 91f1f0267..df3bcb401 100644 --- a/public/intl/messages/hi-IN.json +++ b/public/intl/messages/hi-IN.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/hr-HR.json b/public/intl/messages/hr-HR.json index cd8d4d38b..8388dd486 100644 --- a/public/intl/messages/hr-HR.json +++ b/public/intl/messages/hr-HR.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/hu-HU.json b/public/intl/messages/hu-HU.json index e39182b18..c3da1af06 100644 --- a/public/intl/messages/hu-HU.json +++ b/public/intl/messages/hu-HU.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/id-ID.json b/public/intl/messages/id-ID.json index 97526840b..eda5c0b3e 100644 --- a/public/intl/messages/id-ID.json +++ b/public/intl/messages/id-ID.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -761,6 +785,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -785,6 +815,12 @@ "value": "Layar" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/it-IT.json b/public/intl/messages/it-IT.json index a93715d3d..bdc015f5a 100644 --- a/public/intl/messages/it-IT.json +++ b/public/intl/messages/it-IT.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/ja-JP.json b/public/intl/messages/ja-JP.json index 63b49aa59..4e2a8fc47 100644 --- a/public/intl/messages/ja-JP.json +++ b/public/intl/messages/ja-JP.json @@ -179,6 +179,12 @@ "value": "国" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "フィールド" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "分析" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "見通し" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "保持" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "画面サイズ" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/km-KH.json b/public/intl/messages/km-KH.json index 1f7b82ca4..68c71ebbd 100644 --- a/public/intl/messages/km-KH.json +++ b/public/intl/messages/km-KH.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -761,6 +785,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -785,6 +815,12 @@ "value": "ប្រភេទឧបករណ៍" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/ko-KR.json b/public/intl/messages/ko-KR.json index 26413708b..5a3c90348 100644 --- a/public/intl/messages/ko-KR.json +++ b/public/intl/messages/ko-KR.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -773,6 +797,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -797,6 +827,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/lt-LT.json b/public/intl/messages/lt-LT.json index 21610b7bd..6369cd3c1 100644 --- a/public/intl/messages/lt-LT.json +++ b/public/intl/messages/lt-LT.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -874,6 +898,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -898,6 +928,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json index 013e5c88c..fd7294f3b 100644 --- a/public/intl/messages/mn-MN.json +++ b/public/intl/messages/mn-MN.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Дэлгэц" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/ms-MY.json b/public/intl/messages/ms-MY.json index e022e1227..874f3a3dd 100644 --- a/public/intl/messages/ms-MY.json +++ b/public/intl/messages/ms-MY.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -761,6 +785,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -785,6 +815,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/my-MM.json b/public/intl/messages/my-MM.json index 3995a22b0..33b449793 100644 --- a/public/intl/messages/my-MM.json +++ b/public/intl/messages/my-MM.json @@ -41,6 +41,12 @@ "value": "အက်ဒမင်" } ], + "label.after": [ + { + "type": 0, + "value": "ပြီးနောက်" + } + ], "label.all": [ { "type": 0, @@ -59,6 +65,12 @@ "value": "အန်နလစ်တစ်" } ], + "label.average": [ + { + "type": 0, + "value": "ပျမ်းမျှ" + } + ], "label.average-visit-time": [ { "type": 0, @@ -71,12 +83,30 @@ "value": "နောက်သို့" } ], + "label.before": [ + { + "type": 0, + "value": "မတိုင်မီ" + } + ], "label.bounce-rate": [ { "type": 0, "value": "Bounce နှုန်း" } ], + "label.breakdown": [ + { + "type": 0, + "value": "ခွဲခြမ်းစိတ်ဖြာမှု" + } + ], + "label.browser": [ + { + "type": 0, + "value": "Browser" + } + ], "label.browsers": [ { "type": 0, @@ -101,6 +131,12 @@ "value": "မြို့များ" } ], + "label.city": [ + { + "type": 0, + "value": "City" + } + ], "label.clear-all": [ { "type": 0, @@ -119,6 +155,12 @@ "value": "စကားဝှက်အတည်ပြုသည်" } ], + "label.contains": [ + { + "type": 0, + "value": "ပါဝင်သည်" + } + ], "label.continue": [ { "type": 0, @@ -131,6 +173,24 @@ "value": "နိုင်ငံများ" } ], + "label.country": [ + { + "type": 0, + "value": "Country" + } + ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], + "label.create-report": [ + { + "type": 0, + "value": "ရီပို့လုပ်မည်" + } + ], "label.create-team": [ { "type": 0, @@ -173,12 +233,24 @@ "value": "ဒေတာ" } ], + "label.date": [ + { + "type": 0, + "value": "Date" + } + ], "label.date-range": [ { "type": 0, "value": "ရက်အပိုင်းအခြား" } ], + "label.day": [ + { + "type": 0, + "value": "Day" + } + ], "label.default-date-range": [ { "type": 0, @@ -209,6 +281,12 @@ "value": "ဝက်ဘ်ဆိုဒ်ကိုဖျက်မည်" } ], + "label.description": [ + { + "type": 0, + "value": "ရှင်းပြချက်" + } + ], "label.desktop": [ { "type": 0, @@ -221,6 +299,12 @@ "value": "အသေးစိတ်" } ], + "label.device": [ + { + "type": 0, + "value": "Device" + } + ], "label.devices": [ { "type": 0, @@ -233,6 +317,12 @@ "value": "ပိတ်ပါ" } ], + "label.does-not-contain": [ + { + "type": 0, + "value": "မပါဝင်ပါ" + } + ], "label.domain": [ { "type": 0, @@ -281,6 +371,12 @@ "value": "အဖြစ်အပျက်များ" } ], + "label.false": [ + { + "type": 0, + "value": "မှားသည်" + } + ], "label.field": [ { "type": 0, @@ -293,6 +389,12 @@ "value": "Field အမည်များ" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -305,18 +407,72 @@ "value": "အရှိအတိုင်း" } ], + "label.filters": [ + { + "type": 0, + "value": "Filter များ" + } + ], "label.funnel": [ { "type": 0, "value": "ဖန်နယ်" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], + "label.greater-than": [ + { + "type": 0, + "value": "ထက်ပို၍ကြီးသည်" + } + ], + "label.greater-than-equals": [ + { + "type": 0, + "value": "ထက်ပို၍ကြီးသည်သို့မဟုတ်တူသည်" + } + ], "label.insights": [ { "type": 0, "value": "အသေးစိတ်သိမြင်နိုင်ရန်" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], + "label.is": [ + { + "type": 0, + "value": "Is" + } + ], + "label.is-not": [ + { + "type": 0, + "value": "Is not" + } + ], + "label.is-not-set": [ + { + "type": 0, + "value": "Is not set" + } + ], + "label.is-set": [ + { + "type": 0, + "value": "Is set" + } + ], "label.join": [ { "type": 0, @@ -387,6 +543,18 @@ "value": "အသင်းမှထွက်မည်" } ], + "label.less-than": [ + { + "type": 0, + "value": "ထက်ပို၍ငယ်သည်" + } + ], + "label.less-than-equals": [ + { + "type": 0, + "value": "ထက်ပို၍ငယ်သည်သို့မဟုတ်တူသည်" + } + ], "label.login": [ { "type": 0, @@ -399,12 +567,24 @@ "value": "လော့ဂ်အောက်လုပ်မည်" } ], + "label.max": [ + { + "type": 0, + "value": "အများဆုံး" + } + ], "label.members": [ { "type": 0, "value": "အဖွဲ့ဝင်များ" } ], + "label.min": [ + { + "type": 0, + "value": "အနည်းဆုံး" + } + ], "label.mobile": [ { "type": 0, @@ -417,6 +597,12 @@ "value": "နောက်ထပ်" } ], + "label.my-websites": [ + { + "type": 0, + "value": "My websites" + } + ], "label.name": [ { "type": 0, @@ -435,24 +621,54 @@ "value": "မရှိပါ" } ], - "label.operating-systems": [ + "label.os": [ { "type": 0, "value": "ကွန်ပျူတာလည်ပတ်မှုစနစ်" } ], + "label.overview": [ + { + "type": 0, + "value": "အပေါ်ယံမြင်ကွင်း" + } + ], "label.owner": [ { "type": 0, "value": "ပိုင်ဆိုင်သူ" } ], + "label.page-of": [ + { + "type": 0, + "value": "Page " + }, + { + "type": 1, + "value": "current" + }, + { + "type": 0, + "value": " of " + }, + { + "type": 1, + "value": "total" + } + ], "label.page-views": [ { "type": 0, "value": "ဝင်ရောက်ကြည့်ရှုသူ" } ], + "label.pageTitle": [ + { + "type": 0, + "value": "Page title" + } + ], "label.pages": [ { "type": 0, @@ -505,6 +721,12 @@ "value": "အချိန်နှင့်တပြေးညီ" } ], + "label.referrer": [ + { + "type": 0, + "value": "Referrer" + } + ], "label.referrers": [ { "type": 0, @@ -523,6 +745,12 @@ "value": "ပြန်ထုတ်မည်" } ], + "label.region": [ + { + "type": 0, + "value": "Region" + } + ], "label.regions": [ { "type": 0, @@ -559,6 +787,18 @@ "value": "ဝက်ဘ်ဆိုဒ်ဒေတာကိုဖျက်မည်" } ], + "label.retention": [ + { + "type": 0, + "value": "Retention" + } + ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -583,6 +823,12 @@ "value": "မြင်ကွင်းများ" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, @@ -619,6 +865,12 @@ "value": "တစ်ရက်အတွင်း" } ], + "label.sum": [ + { + "type": 0, + "value": "ပေါင်းလဒ်" + } + ], "label.tablet": [ { "type": 0, @@ -649,12 +901,24 @@ "value": "အသင်းဝင်" } ], + "label.team-name": [ + { + "type": 0, + "value": "Team name" + } + ], "label.team-owner": [ { "type": 0, "value": "အသင်းကိုပိုင်ဆိုင်သူ" } ], + "label.team-websites": [ + { + "type": 0, + "value": "Team websites" + } + ], "label.teams": [ { "type": 0, @@ -709,12 +973,42 @@ "value": "ဇယားများကို အဖွင့်အပိတ်လုပ်မည်" } ], + "label.total": [ + { + "type": 0, + "value": "စုစုပေါင်း" + } + ], + "label.total-records": [ + { + "type": 0, + "value": "မှတ်တမ်းစုစုပေါင်း" + } + ], "label.tracking-code": [ { "type": 0, "value": "ထရက်လုပ်သည့် ကုဒ်" } ], + "label.true": [ + { + "type": 0, + "value": "မှန်သည်" + } + ], + "label.type": [ + { + "type": 0, + "value": "အမျိုးအစား" + } + ], + "label.unique": [ + { + "type": 0, + "value": "Unique" + } + ], "label.unique-visitors": [ { "type": 0, @@ -727,6 +1021,12 @@ "value": "မသိသော" } ], + "label.untitled": [ + { + "type": 0, + "value": "ခေါင်းစဉ်မရှိ" + } + ], "label.url": [ { "type": 0, @@ -757,6 +1057,12 @@ "value": "အသုံးပြုသူများ" } ], + "label.value": [ + { + "type": 0, + "value": "တန်ဖိုး" + } + ], "label.view": [ { "type": 0, @@ -817,168 +1123,6 @@ "value": "မနေ့က" } ], - "labels.after": [ - { - "type": 0, - "value": "ပြီးနောက်" - } - ], - "labels.average": [ - { - "type": 0, - "value": "ပျမ်းမျှ" - } - ], - "labels.before": [ - { - "type": 0, - "value": "မတိုင်မီ" - } - ], - "labels.breakdown": [ - { - "type": 0, - "value": "ခွဲခြမ်းစိတ်ဖြာမှု" - } - ], - "labels.contains": [ - { - "type": 0, - "value": "ပါဝင်သည်" - } - ], - "labels.create-report": [ - { - "type": 0, - "value": "ရီပို့လုပ်မည်" - } - ], - "labels.description": [ - { - "type": 0, - "value": "ရှင်းပြချက်" - } - ], - "labels.does-not-contain": [ - { - "type": 0, - "value": "မပါဝင်ပါ" - } - ], - "labels.does-not-equal": [ - { - "type": 0, - "value": "မတူညီပါ" - } - ], - "labels.equals": [ - { - "type": 0, - "value": "တူညီသည်" - } - ], - "labels.false": [ - { - "type": 0, - "value": "မှားသည်" - } - ], - "labels.filters": [ - { - "type": 0, - "value": "Filter များ" - } - ], - "labels.greater-than": [ - { - "type": 0, - "value": "ထက်ပို၍ကြီးသည်" - } - ], - "labels.greater-than-equals": [ - { - "type": 0, - "value": "ထက်ပို၍ကြီးသည်သို့မဟုတ်တူသည်" - } - ], - "labels.less-than": [ - { - "type": 0, - "value": "ထက်ပို၍ငယ်သည်" - } - ], - "labels.less-than-equals": [ - { - "type": 0, - "value": "ထက်ပို၍ငယ်သည်သို့မဟုတ်တူသည်" - } - ], - "labels.max": [ - { - "type": 0, - "value": "အများဆုံး" - } - ], - "labels.min": [ - { - "type": 0, - "value": "အနည်းဆုံး" - } - ], - "labels.overview": [ - { - "type": 0, - "value": "အပေါ်ယံမြင်ကွင်း" - } - ], - "labels.sum": [ - { - "type": 0, - "value": "ပေါင်းလဒ်" - } - ], - "labels.total": [ - { - "type": 0, - "value": "စုစုပေါင်း" - } - ], - "labels.total-records": [ - { - "type": 0, - "value": "မှတ်တမ်းစုစုပေါင်း" - } - ], - "labels.true": [ - { - "type": 0, - "value": "မှန်သည်" - } - ], - "labels.type": [ - { - "type": 0, - "value": "အမျိုးအစား" - } - ], - "labels.unique": [ - { - "type": 0, - "value": "Unique" - } - ], - "labels.untitled": [ - { - "type": 0, - "value": "ခေါင်းစဉ်မရှိ" - } - ], - "labels.value": [ - { - "type": 0, - "value": "တန်ဖိုး" - } - ], "message.active-users": [ { "type": 1, diff --git a/public/intl/messages/nb-NO.json b/public/intl/messages/nb-NO.json index 82576ff89..010bd2ad4 100644 --- a/public/intl/messages/nb-NO.json +++ b/public/intl/messages/nb-NO.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/nl-NL.json b/public/intl/messages/nl-NL.json index 5ee25206b..66c5f4faf 100644 --- a/public/intl/messages/nl-NL.json +++ b/public/intl/messages/nl-NL.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Schermen" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/pl-PL.json b/public/intl/messages/pl-PL.json index 6da1ff7ad..4621a918d 100644 --- a/public/intl/messages/pl-PL.json +++ b/public/intl/messages/pl-PL.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Pola" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Lejek" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Ekrany" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/pt-BR.json b/public/intl/messages/pt-BR.json index ba508a504..790e13161 100644 --- a/public/intl/messages/pt-BR.json +++ b/public/intl/messages/pt-BR.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Campos" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funil" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Telas" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/pt-PT.json b/public/intl/messages/pt-PT.json index a6431fb3b..511d74fc6 100644 --- a/public/intl/messages/pt-PT.json +++ b/public/intl/messages/pt-PT.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/ro-RO.json b/public/intl/messages/ro-RO.json index 1438ab413..9181c1035 100644 --- a/public/intl/messages/ro-RO.json +++ b/public/intl/messages/ro-RO.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/ru-RU.json b/public/intl/messages/ru-RU.json index b3213e679..93b89291d 100644 --- a/public/intl/messages/ru-RU.json +++ b/public/intl/messages/ru-RU.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Экраны" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/si-LK.json b/public/intl/messages/si-LK.json index f4e5bca2f..578a8abed 100644 --- a/public/intl/messages/si-LK.json +++ b/public/intl/messages/si-LK.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/sk-SK.json b/public/intl/messages/sk-SK.json index b7e2914a9..6b375afed 100644 --- a/public/intl/messages/sk-SK.json +++ b/public/intl/messages/sk-SK.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/sl-SI.json b/public/intl/messages/sl-SI.json index ee62a3153..6d435020c 100644 --- a/public/intl/messages/sl-SI.json +++ b/public/intl/messages/sl-SI.json @@ -179,6 +179,12 @@ "value": "Država" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Polja" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Prodajni lijak" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Vpogled" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Ohranjanje uporabnikov" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Zasloni" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/sv-SE.json b/public/intl/messages/sv-SE.json index 4a7f4130c..9824be155 100644 --- a/public/intl/messages/sv-SE.json +++ b/public/intl/messages/sv-SE.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Upplösning" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/ta-IN.json b/public/intl/messages/ta-IN.json index 90fb9ebf1..9726b46d5 100644 --- a/public/intl/messages/ta-IN.json +++ b/public/intl/messages/ta-IN.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/th-TH.json b/public/intl/messages/th-TH.json index c30a9d61f..6988c653b 100644 --- a/public/intl/messages/th-TH.json +++ b/public/intl/messages/th-TH.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -761,6 +785,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -785,6 +815,12 @@ "value": "ขนาดหน้าจอ" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/tr-TR.json b/public/intl/messages/tr-TR.json index 138681adb..f15a3b732 100644 --- a/public/intl/messages/tr-TR.json +++ b/public/intl/messages/tr-TR.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Ekranlar" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/uk-UA.json b/public/intl/messages/uk-UA.json index bdc2d345b..2c602c947 100644 --- a/public/intl/messages/uk-UA.json +++ b/public/intl/messages/uk-UA.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/ur-PK.json b/public/intl/messages/ur-PK.json index 2005bc719..23195ffc0 100644 --- a/public/intl/messages/ur-PK.json +++ b/public/intl/messages/ur-PK.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -769,6 +793,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -793,6 +823,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/vi-VN.json b/public/intl/messages/vi-VN.json index 9fe0dd4ed..4c3ef05cb 100644 --- a/public/intl/messages/vi-VN.json +++ b/public/intl/messages/vi-VN.json @@ -179,6 +179,12 @@ "value": "Country" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "Funnel" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "Insights" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -761,6 +785,12 @@ "value": "Retention" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -785,6 +815,12 @@ "value": "Screens" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index ec5c441b2..acc98be29 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -179,6 +179,12 @@ "value": "国家/地区" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "Fields" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "分析" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "见解" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -777,6 +801,12 @@ "value": "保留" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -801,6 +831,12 @@ "value": "屏幕尺寸" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/public/intl/messages/zh-TW.json b/public/intl/messages/zh-TW.json index c980c4bb9..49e438216 100644 --- a/public/intl/messages/zh-TW.json +++ b/public/intl/messages/zh-TW.json @@ -179,6 +179,12 @@ "value": "國家" } ], + "label.create": [ + { + "type": 0, + "value": "Create" + } + ], "label.create-report": [ { "type": 0, @@ -383,6 +389,12 @@ "value": "欄位" } ], + "label.filter": [ + { + "type": 0, + "value": "Filter" + } + ], "label.filter-combined": [ { "type": 0, @@ -407,6 +419,12 @@ "value": "漏斗" } ], + "label.funnel-description": [ + { + "type": 0, + "value": "Understand the conversion and drop-off rate of users." + } + ], "label.greater-than": [ { "type": 0, @@ -425,6 +443,12 @@ "value": "洞察" } ], + "label.insights-description": [ + { + "type": 0, + "value": "Dive deeper into your data by using segments and filters." + } + ], "label.is": [ { "type": 0, @@ -773,6 +797,12 @@ "value": "保留" } ], + "label.retention-description": [ + { + "type": 0, + "value": "Measure your website stickiness by tracking how often users return." + } + ], "label.role": [ { "type": 0, @@ -797,6 +827,12 @@ "value": "螢幕" } ], + "label.search": [ + { + "type": 0, + "value": "Search" + } + ], "label.select-date": [ { "type": 0, diff --git a/src/lang/de-DE.json b/src/lang/de-DE.json index 688898033..1e98b14cc 100644 --- a/src/lang/de-DE.json +++ b/src/lang/de-DE.json @@ -29,6 +29,7 @@ "label.continue": "Weiter", "label.countries": "Länder", "label.country": "Land", + "label.create": "Create", "label.create-report": "Bericht erstellen", "label.create-team": "Team erstellen", "label.create-user": "Benutzer erstellen", @@ -62,7 +63,8 @@ "label.events": "Ereignisse", "label.false": "Falsch", "label.field": "Feld", - "label.fields": "Felder", "label.filter": "Filter", + "label.fields": "Felder", + "label.filter": "Filter", "label.filter-combined": "Kombiniert", "label.filter-raw": "Rohdaten", "label.filters": "Filter", From 661fc9223097a24d841f86661d8d84da7476b60b Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 5 Sep 2023 11:09:27 -0700 Subject: [PATCH 177/357] fix session metrics to show visitors instead of views --- src/queries/analytics/sessions/getSessionMetrics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queries/analytics/sessions/getSessionMetrics.ts b/src/queries/analytics/sessions/getSessionMetrics.ts index af358c525..43d9ef5a4 100644 --- a/src/queries/analytics/sessions/getSessionMetrics.ts +++ b/src/queries/analytics/sessions/getSessionMetrics.ts @@ -31,7 +31,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query ` select ${column} x, - count(*) y + count(distinct website_event.session_id) y ${includeCountry ? ', country' : ''} from website_event ${joinSession} From 99cfc68e88222b2452a9cb989d55794f3571953f Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 5 Sep 2023 12:22:51 -0700 Subject: [PATCH 178/357] exclude website domain from referrers table --- .../analytics/pageviews/getPageviewMetrics.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/queries/analytics/pageviews/getPageviewMetrics.ts b/src/queries/analytics/pageviews/getPageviewMetrics.ts index b365d3f6b..3cf6c122d 100644 --- a/src/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/src/queries/analytics/pageviews/getPageviewMetrics.ts @@ -24,6 +24,12 @@ async function relationalQuery(websiteId: string, column: string, filters: Query { joinSession: SESSION_COLUMNS.includes(column) }, ); + let excludeDomain = ''; + if (column === 'referrer_domain') { + excludeDomain = + 'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)'; + } + return rawQuery( ` select ${column} x, count(*) y @@ -32,6 +38,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} and event_type = {{eventType}} + ${excludeDomain} ${filterQuery} group by 1 order by 2 desc @@ -48,6 +55,11 @@ async function clickhouseQuery(websiteId: string, column: string, filters: Query eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, }); + let excludeDomain = ''; + if (column === 'referrer_domain') { + excludeDomain = 'and referrer_domain != {websiteDomain:String}'; + } + return rawQuery( ` select ${column} x, count(*) y @@ -55,6 +67,7 @@ async function clickhouseQuery(websiteId: string, column: string, filters: Query where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} and event_type = {eventType:UInt32} + ${excludeDomain} ${filterQuery} group by x order by y desc From da3cc9b06567c17ae6c7726a99c9bdc3be9ab040 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 5 Sep 2023 13:53:58 -0700 Subject: [PATCH 179/357] fix isLoading param for SubmitButton --- src/components/pages/reports/event-data/EventDataParameters.js | 2 +- src/components/pages/reports/funnel/FunnelParameters.js | 2 +- src/components/pages/reports/insights/InsightsParameters.js | 2 +- src/components/pages/reports/retention/RetentionParameters.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/pages/reports/event-data/EventDataParameters.js b/src/components/pages/reports/event-data/EventDataParameters.js index a01a29722..e0fadb8b8 100644 --- a/src/components/pages/reports/event-data/EventDataParameters.js +++ b/src/components/pages/reports/event-data/EventDataParameters.js @@ -134,7 +134,7 @@ export function EventDataParameters() { ); })} - + {formatMessage(labels.runQuery)} diff --git a/src/components/pages/reports/funnel/FunnelParameters.js b/src/components/pages/reports/funnel/FunnelParameters.js index 2c99a032e..a3fbe6634 100644 --- a/src/components/pages/reports/funnel/FunnelParameters.js +++ b/src/components/pages/reports/funnel/FunnelParameters.js @@ -80,7 +80,7 @@ export function FunnelParameters() { - + {formatMessage(labels.runQuery)} diff --git a/src/components/pages/reports/insights/InsightsParameters.js b/src/components/pages/reports/insights/InsightsParameters.js index db0c1d4ea..3ddc0367d 100644 --- a/src/components/pages/reports/insights/InsightsParameters.js +++ b/src/components/pages/reports/insights/InsightsParameters.js @@ -137,7 +137,7 @@ export function InsightsParameters() { ); })} - + {formatMessage(labels.runQuery)} diff --git a/src/components/pages/reports/retention/RetentionParameters.js b/src/components/pages/reports/retention/RetentionParameters.js index aa81cf472..e87108d13 100644 --- a/src/components/pages/reports/retention/RetentionParameters.js +++ b/src/components/pages/reports/retention/RetentionParameters.js @@ -35,7 +35,7 @@ export function RetentionParameters() { - + {formatMessage(labels.runQuery)} From acb7fdcae8eba2152d8246517d711e4af67bf4bf Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 5 Sep 2023 14:27:06 -0700 Subject: [PATCH 180/357] add version endpoint --- src/pages/api/version.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/pages/api/version.ts diff --git a/src/pages/api/version.ts b/src/pages/api/version.ts new file mode 100644 index 000000000..4453b56f0 --- /dev/null +++ b/src/pages/api/version.ts @@ -0,0 +1,17 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { ok, methodNotAllowed } from 'next-basics'; +import { CURRENT_VERSION } from 'lib/constants'; + +export interface VersionResponse { + version: string; +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'GET') { + return ok(res, { + version: CURRENT_VERSION, + }); + } + + return methodNotAllowed(res); +}; From 0e0788b0a1b13e133840fe7c12576cfba2e4325c Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 6 Sep 2023 11:34:12 -0700 Subject: [PATCH 181/357] Fix v1 check error showing in postgres logs --- scripts/check-db.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/check-db.js b/scripts/check-db.js index a84a775ce..757843ac3 100644 --- a/scripts/check-db.js +++ b/scripts/check-db.js @@ -66,12 +66,16 @@ async function checkDatabaseVersion() { async function checkV1Tables() { try { - await prisma.$queryRaw`select * from account limit 1`; + // check for v1 migrations before v2 release date + const record = + await prisma.$queryRaw`select * from _prisma_migrations where started_at < '2023-04-17'`; - error( - 'Umami v1 tables detected. For how to upgrade from v1 to v2 go to https://umami.is/docs/migrate-v1-v2.', - ); - process.exit(1); + if (record.length > 0) { + error( + 'Umami v1 tables detected. For how to upgrade from v1 to v2 go to https://umami.is/docs/migrate-v1-v2.', + ); + process.exit(1); + } } catch (e) { // Ignore } From c5dd3390d61cc77169f17a92463d59f2b05710de Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 6 Sep 2023 11:37:41 -0700 Subject: [PATCH 182/357] Fix teamRole showing. --- .../pages/settings/teams/TeamMembersTable.js | 4 +++- src/pages/api/teams/[id]/index.ts | 4 ++-- src/queries/admin/user.ts | 13 ++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/pages/settings/teams/TeamMembersTable.js b/src/components/pages/settings/teams/TeamMembersTable.js index 4f620cf77..0755c1932 100644 --- a/src/components/pages/settings/teams/TeamMembersTable.js +++ b/src/components/pages/settings/teams/TeamMembersTable.js @@ -29,7 +29,9 @@ export function TeamMembersTable({ } if (key === 'role') { return formatMessage( - labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown], + labels[ + Object.keys(ROLES).find(key => ROLES[key] === row?.teamUser[0]?.role) || labels.unknown + ], ); } return data[key]; diff --git a/src/pages/api/teams/[id]/index.ts b/src/pages/api/teams/[id]/index.ts index 31c47b2f1..a5527580d 100644 --- a/src/pages/api/teams/[id]/index.ts +++ b/src/pages/api/teams/[id]/index.ts @@ -22,8 +22,8 @@ const schema = { }), POST: yup.object().shape({ id: yup.string().uuid().required(), - name: yup.string().max(50).required(), - accessCode: yup.string().max(50).required(), + name: yup.string().max(50), + accessCode: yup.string().max(50), }), DELETE: yup.object().shape({ id: yup.string().uuid().required(), diff --git a/src/queries/admin/user.ts b/src/queries/admin/user.ts index dfe8ea283..ee6f778b0 100644 --- a/src/queries/admin/user.ts +++ b/src/queries/admin/user.ts @@ -97,7 +97,18 @@ export async function getUsers( } export async function getUsersByTeamId(teamId: string, filter?: UserSearchFilter) { - return getUsers({ teamId, ...filter }); + return getUsers( + { teamId, ...filter }, + { + include: { + teamUser: { + select: { + role: true, + }, + }, + }, + }, + ); } export async function createUser(data: { From 36bae3f709d7fa8b3f26b123afa4410a38e37436 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 6 Sep 2023 12:52:08 -0700 Subject: [PATCH 183/357] Fix default page size. --- src/pages/api/reports/index.ts | 2 +- src/pages/api/teams/[id]/users/index.ts | 2 +- src/pages/api/teams/[id]/websites/index.ts | 2 +- src/pages/api/teams/index.ts | 6 +++++- src/pages/api/users/[id]/teams.ts | 2 +- src/pages/api/users/[id]/websites.ts | 2 +- src/pages/api/websites/[id]/reports.ts | 2 +- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pages/api/reports/index.ts b/src/pages/api/reports/index.ts index e62a1cc57..b36c5638d 100644 --- a/src/pages/api/reports/index.ts +++ b/src/pages/api/reports/index.ts @@ -57,7 +57,7 @@ export default async ( const data = await getReportsByUserId(userId, { page, filter, - pageSize: +pageSize || null, + pageSize: +pageSize || undefined, includeTeams: true, }); diff --git a/src/pages/api/teams/[id]/users/index.ts b/src/pages/api/teams/[id]/users/index.ts index 52b25da68..d0efba25f 100644 --- a/src/pages/api/teams/[id]/users/index.ts +++ b/src/pages/api/teams/[id]/users/index.ts @@ -32,7 +32,7 @@ export default async ( const users = await getUsersByTeamId(teamId, { page, filter, - pageSize: +pageSize || null, + pageSize: +pageSize || undefined, }); return ok(res, users); diff --git a/src/pages/api/teams/[id]/websites/index.ts b/src/pages/api/teams/[id]/websites/index.ts index 4de327092..d4b70b7f4 100644 --- a/src/pages/api/teams/[id]/websites/index.ts +++ b/src/pages/api/teams/[id]/websites/index.ts @@ -49,7 +49,7 @@ export default async ( const websites = await getWebsitesByTeamId(teamId, { page, filter, - pageSize: +pageSize || null, + pageSize: +pageSize || undefined, }); return ok(res, websites); diff --git a/src/pages/api/teams/index.ts b/src/pages/api/teams/index.ts index dd742b9e2..7ef6f533e 100644 --- a/src/pages/api/teams/index.ts +++ b/src/pages/api/teams/index.ts @@ -41,7 +41,11 @@ export default async ( if (req.method === 'GET') { const { page, filter, pageSize } = req.query; - const results = await getTeamsByUserId(userId, { page, filter, pageSize: +pageSize || null }); + const results = await getTeamsByUserId(userId, { + page, + filter, + pageSize: +pageSize || undefined, + }); return ok(res, results); } diff --git a/src/pages/api/users/[id]/teams.ts b/src/pages/api/users/[id]/teams.ts index eb34410cb..72b99b869 100644 --- a/src/pages/api/users/[id]/teams.ts +++ b/src/pages/api/users/[id]/teams.ts @@ -45,7 +45,7 @@ export default async ( const teams = await getTeamsByUserId(userId, { page, filter, - pageSize: +pageSize || null, + pageSize: +pageSize || undefined, }); return ok(res, teams); diff --git a/src/pages/api/users/[id]/websites.ts b/src/pages/api/users/[id]/websites.ts index 65e9a0e8a..ab7d88ef6 100644 --- a/src/pages/api/users/[id]/websites.ts +++ b/src/pages/api/users/[id]/websites.ts @@ -42,7 +42,7 @@ export default async ( const websites = await getWebsitesByUserId(userId, { page, filter, - pageSize: +pageSize || null, + pageSize: +pageSize || undefined, includeTeams, onlyTeams, }); diff --git a/src/pages/api/websites/[id]/reports.ts b/src/pages/api/websites/[id]/reports.ts index 738f6b372..2c7707e8d 100644 --- a/src/pages/api/websites/[id]/reports.ts +++ b/src/pages/api/websites/[id]/reports.ts @@ -38,7 +38,7 @@ export default async ( const data = await getReportsByWebsiteId(websiteId, { page, filter, - pageSize: +pageSize || null, + pageSize: +pageSize || undefined, }); return ok(res, data); From 9b501d03e9b60ecb1d56e3862db7a7bd3c8e3fb7 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Sat, 9 Sep 2023 09:38:16 +0000 Subject: [PATCH 184/357] update date range parser and date picker --- src/components/metrics/DatePickerForm.js | 6 +++--- src/lib/date.ts | 12 +++--------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/metrics/DatePickerForm.js b/src/components/metrics/DatePickerForm.js index fa45b64ac..9eb3c52e9 100644 --- a/src/components/metrics/DatePickerForm.js +++ b/src/components/metrics/DatePickerForm.js @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Button, ButtonGroup, Calendar } from 'react-basics'; -import { isAfter, isBefore, isSameDay } from 'date-fns'; +import { isAfter, isBefore, isSameDay, startOfDay, endOfDay } from 'date-fns'; import useLocale from 'components/hooks/useLocale'; import { getDateLocale } from 'lib/lang'; import { FILTER_DAY, FILTER_RANGE } from 'lib/constants'; @@ -31,9 +31,9 @@ export function DatePickerForm({ const handleSave = () => { if (selected === FILTER_DAY) { - onChange(`range:${singleDate.getTime()}:${singleDate.getTime()}`); + onChange(`range:${startOfDay(singleDate).getTime()}:${endOfDay(singleDate).getTime()}`); } else { - onChange(`range:${startDate.getTime()}:${endDate.getTime()}`); + onChange(`range:${startOfDay(startDate).getTime()}:${endOfDay(endDate).getTime()}`); } }; diff --git a/src/lib/date.ts b/src/lib/date.ts index 14f0e13ce..bfbfc0b91 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -78,7 +78,9 @@ export function parseDateRange(value, locale = 'en-US') { const endDate = new Date(+endTime); return { - ...getDateRangeValues(startDate, endDate), + startDate, + endDate, + unit: getMinimumUnit(startDate, endDate), value, }; } @@ -255,14 +257,6 @@ export function getMinimumUnit(startDate, endDate) { return 'year'; } -export function getDateRangeValues(startDate, endDate) { - return { - startDate: startOfDay(startDate), - endDate: endOfDay(endDate), - unit: getMinimumUnit(startDate, endDate), - }; -} - export function getDateFromString(str) { const [ymd, hms] = str.split(' '); const [year, month, day] = ymd.split('-'); From eec871dc4aff6a67e8fda4a757a856b179d329c7 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Sat, 9 Sep 2023 09:51:58 +0000 Subject: [PATCH 185/357] use available dateLocale --- src/components/input/MonthSelect.js | 7 +++---- src/components/metrics/DatePickerForm.js | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/input/MonthSelect.js b/src/components/input/MonthSelect.js index fb1bf5354..312c6854a 100644 --- a/src/components/input/MonthSelect.js +++ b/src/components/input/MonthSelect.js @@ -12,11 +12,10 @@ import { startOfMonth, endOfMonth } from 'date-fns'; import Icons from 'components/icons'; import { useLocale } from 'components/hooks'; import { formatDate } from 'lib/date'; -import { getDateLocale } from 'lib/lang'; import styles from './MonthSelect.module.css'; export function MonthSelect({ date = new Date(), onChange }) { - const { locale } = useLocale(); + const { locale, dateLocale } = useLocale(); const month = formatDate(date, 'MMMM', locale); const year = date.getFullYear(); const ref = useRef(); @@ -40,7 +39,7 @@ export function MonthSelect({ date = new Date(), onChange }) { {close => ( )} @@ -57,7 +56,7 @@ export function MonthSelect({ date = new Date(), onChange }) { {close => ( )} diff --git a/src/components/metrics/DatePickerForm.js b/src/components/metrics/DatePickerForm.js index 9eb3c52e9..2dcca77a1 100644 --- a/src/components/metrics/DatePickerForm.js +++ b/src/components/metrics/DatePickerForm.js @@ -2,7 +2,6 @@ import { useState } from 'react'; import { Button, ButtonGroup, Calendar } from 'react-basics'; import { isAfter, isBefore, isSameDay, startOfDay, endOfDay } from 'date-fns'; import useLocale from 'components/hooks/useLocale'; -import { getDateLocale } from 'lib/lang'; import { FILTER_DAY, FILTER_RANGE } from 'lib/constants'; import useMessages from 'components/hooks/useMessages'; import styles from './DatePickerForm.module.css'; @@ -21,7 +20,7 @@ export function DatePickerForm({ const [singleDate, setSingleDate] = useState(defaultStartDate); const [startDate, setStartDate] = useState(defaultStartDate); const [endDate, setEndDate] = useState(defaultEndDate); - const { locale } = useLocale(); + const { dateLocale } = useLocale(); const { formatMessage, labels } = useMessages(); const disabled = @@ -60,14 +59,14 @@ export function DatePickerForm({ date={startDate} minDate={minDate} maxDate={endDate} - locale={getDateLocale(locale)} + locale={dateLocale} onChange={setStartDate} /> From d457e6ea16f8861a8e8ff1ccc9848dbc88137bdd Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Sat, 9 Sep 2023 09:53:22 +0000 Subject: [PATCH 186/357] localize single day calendar in date picker --- src/components/metrics/DatePickerForm.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/metrics/DatePickerForm.js b/src/components/metrics/DatePickerForm.js index 2dcca77a1..5e1906c3c 100644 --- a/src/components/metrics/DatePickerForm.js +++ b/src/components/metrics/DatePickerForm.js @@ -50,6 +50,7 @@ export function DatePickerForm({ date={singleDate} minDate={minDate} maxDate={maxDate} + locale={dateLocale} onChange={setSingleDate} /> )} From 7b97209d56761d128c6c1d044c25c57ed3564bba Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 13 Sep 2023 13:27:07 -0700 Subject: [PATCH 187/357] Fix CH script. --- db/clickhouse/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 94b560c3e..44428e94c 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -66,7 +66,7 @@ CREATE TABLE umami.website_event_queue ( ) ENGINE = Kafka SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list - kafka_topic_list = 'events', + kafka_topic_list = 'event', kafka_group_name = 'event_consumer_group', kafka_format = 'JSONEachRow', kafka_max_block_size = 1048576, From e73865b880a4f4d502f2dcd44c0cf935ce6937f3 Mon Sep 17 00:00:00 2001 From: AkashRajpurohit Date: Sun, 17 Sep 2023 19:34:35 +0530 Subject: [PATCH 188/357] fix: :bug: gulp error so that error in sending data does not affect the action triggering it --- src/tracker/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tracker/index.js b/src/tracker/index.js index 1686df429..491eef7da 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -187,7 +187,8 @@ headers, }) .then(res => res.text()) - .then(text => (cache = text)); + .then(text => (cache = text)) + .catch(() => {}); // no-op, gulp error }; const track = (obj, data) => { From 84e13a2a105ee74c02420c54586624cf3470540c Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 18 Sep 2023 09:51:43 -0700 Subject: [PATCH 189/357] Add external analytics url. --- .gitignore | 5 +---- src/components/pages/settings/websites/TrackingCode.js | 6 ++++-- src/components/pages/settings/websites/WebsiteSettings.js | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 99087ab50..32e3ef0b8 100644 --- a/.gitignore +++ b/.gitignore @@ -33,10 +33,7 @@ yarn-debug.log* yarn-error.log* # local env files -.env -.env.development.local -.env.test.local -.env.production.local +*.env *.dev.yml diff --git a/src/components/pages/settings/websites/TrackingCode.js b/src/components/pages/settings/websites/TrackingCode.js index d22f0d59e..a666476d9 100644 --- a/src/components/pages/settings/websites/TrackingCode.js +++ b/src/components/pages/settings/websites/TrackingCode.js @@ -3,7 +3,7 @@ import useMessages from 'components/hooks/useMessages'; import useConfig from 'components/hooks/useConfig'; import { useRouter } from 'next/router'; -export function TrackingCode({ websiteId }) { +export function TrackingCode({ websiteId, analyticsUrl }) { const { formatMessage, messages } = useMessages(); const { basePath } = useRouter(); const config = useConfig(); @@ -13,7 +13,9 @@ export function TrackingCode({ websiteId }) { const url = trackerScriptName?.startsWith('http') ? trackerScriptName - : `${process.env.analyticsUrl || location.origin}${basePath}/${trackerScriptName}`; + : `${ + analyticsUrl || process.env.analyticsUrl || location.origin + }${basePath}/${trackerScriptName}`; const code = ``; diff --git a/src/components/pages/settings/websites/WebsiteSettings.js b/src/components/pages/settings/websites/WebsiteSettings.js index f73e0a878..3cd4185c9 100644 --- a/src/components/pages/settings/websites/WebsiteSettings.js +++ b/src/components/pages/settings/websites/WebsiteSettings.js @@ -11,7 +11,7 @@ import ShareUrl from 'components/pages/settings/websites/ShareUrl'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; -export function WebsiteSettings({ websiteId, openExternal = false }) { +export function WebsiteSettings({ websiteId, openExternal = false, analyticsUrl }) { const router = useRouter(); const { formatMessage, labels, messages } = useMessages(); const { get, useQuery } = useApi(); @@ -68,7 +68,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { {tab === 'details' && ( )} - {tab === 'tracking' && } + {tab === 'tracking' && } {tab === 'share' && } {tab === 'data' && } From 3f6fb4654b4aaf72747bae9452c33de1dde78601 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 20 Sep 2023 12:43:59 -0700 Subject: [PATCH 190/357] Fix resetAt date parsing error for CH --- src/lib/clickhouse.ts | 2 +- src/queries/admin/website.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index bc10a6d4e..186a085d1 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -112,7 +112,7 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio params: { ...normalizeFilters(filters), websiteId, - startDate: maxDate(filters.startDate, website.resetAt), + startDate: maxDate(filters.startDate, new Date(website.resetAt)), websiteDomain: website.domain, }, }; diff --git a/src/queries/admin/website.ts b/src/queries/admin/website.ts index cf4570cfc..6417ade6f 100644 --- a/src/queries/admin/website.ts +++ b/src/queries/admin/website.ts @@ -297,7 +297,7 @@ export async function resetWebsite( }), ]).then(async data => { if (cache.enabled) { - await cache.storeWebsite(data[2]); + await cache.storeWebsite(data[3]); } return data; From e9ae4903d452dfe120f956c9b869da97501dbd47 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 20 Sep 2023 14:36:18 -0700 Subject: [PATCH 191/357] add regions to useFormat --- src/components/hooks/useFormat.js | 9 ++++++++- src/pages/api/websites/[id]/values.ts | 7 ++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/hooks/useFormat.js b/src/components/hooks/useFormat.js index 3fd10ec8e..0e609c488 100644 --- a/src/components/hooks/useFormat.js +++ b/src/components/hooks/useFormat.js @@ -2,6 +2,7 @@ import useMessages from './useMessages'; import { BROWSERS } from 'lib/constants'; import useLocale from './useLocale'; import useCountryNames from './useCountryNames'; +import regions from 'public/iso-3166-2.json'; export function useFormat() { const { formatMessage, labels } = useMessages(); @@ -16,6 +17,10 @@ export function useFormat() { return countryNames[value] || value; }; + const formatRegion = value => { + return regions[value] ? regions[value] : value; + }; + const formatDevice = value => { return formatMessage(labels[value] || labels.unknown); }; @@ -26,6 +31,8 @@ export function useFormat() { return formatBrowser(value); case 'country': return formatCountry(value); + case 'region': + return formatRegion(value); case 'device': return formatDevice(value); default: @@ -33,7 +40,7 @@ export function useFormat() { } }; - return { formatBrowser, formatCountry, formatDevice, formatValue }; + return { formatBrowser, formatCountry, formatRegion, formatDevice, formatValue }; } export default useFormat; diff --git a/src/pages/api/websites/[id]/values.ts b/src/pages/api/websites/[id]/values.ts index d90a16822..1f479aebe 100644 --- a/src/pages/api/websites/[id]/values.ts +++ b/src/pages/api/websites/[id]/values.ts @@ -6,7 +6,7 @@ import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; import { getValues } from 'queries'; -export interface WebsiteResetRequestQuery { +export interface ValuesRequestQuery { id: string; } @@ -17,10 +17,7 @@ const schema = { }), }; -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { +export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { await useCors(req, res); await useAuth(req, res); From f7b2f50e4f234e0752c48d8147fab808ad252df0 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 20 Sep 2023 15:26:49 -0700 Subject: [PATCH 192/357] Fix TS error. --- src/lib/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index edf3e9292..8259677dc 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -21,7 +21,7 @@ const log = debug('umami:middleware'); export const useCors = createMiddleware( cors({ // Cache CORS preflight request 24 hours by default - maxAge: process.env.CORS_MAX_AGE || 86400, + maxAge: Number(process.env.CORS_MAX_AGE) || 86400, }), ); From b3f9dfa01a0525eafdd298742e0c9dc86c0c433d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 21 Sep 2023 01:01:58 -0700 Subject: [PATCH 193/357] Update packages. --- package.json | 8 +- src/lib/prisma.ts | 2 +- yarn.lock | 1300 +++++++++++++++++++++++++++------------------ 3 files changed, 795 insertions(+), 515 deletions(-) diff --git a/package.json b/package.json index 8bb372013..8350a060f 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ ], "dependencies": { "@fontsource/inter": "^4.5.15", - "@prisma/client": "5.2.0", + "@prisma/client": "5.3.1", "@tanstack/react-query": "^4.33.0", "@umami/prisma-client": "^0.2.0", "@umami/redis-client": "^0.5.0", @@ -91,12 +91,12 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.4.19", + "next": "13.5.2", "next-basics": "^0.36.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.98.0", + "react-basics": "^0.100.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", @@ -146,7 +146,7 @@ "postcss-preset-env": "7.8.3", "postcss-rtlcss": "^4.0.1", "prettier": "^2.6.2", - "prisma": "5.2.0", + "prisma": "5.3.1", "prompts": "2.4.2", "rollup": "^3.28.0", "rollup-plugin-copy": "^3.4.0", diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index a9832c28c..59638dbd6 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -161,7 +161,7 @@ async function rawQuery(sql: string, data: object): Promise { return Promise.reject(new Error('Unknown database.')); } - const query = sql?.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*\}\}/g, (...args) => { + const query = sql?.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*}}/g, (...args) => { const [, name, type] = args; params.push(data[name]); diff --git a/yarn.lock b/yarn.lock index 5d99a1355..8687c4326 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,7 +15,15 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.5": +"@babel/code-frame@^7.0.0": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.5": version "7.22.5" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz" integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== @@ -272,11 +280,16 @@ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== -"@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.22.5": +"@babel/helper-validator-identifier@^7.19.1": version "7.22.5" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz" integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== +"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0", "@babel/helper-validator-option@^7.22.5": version "7.22.5" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz" @@ -301,13 +314,13 @@ "@babel/traverse" "^7.22.5" "@babel/types" "^7.22.5" -"@babel/highlight@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" - integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== +"@babel/highlight@^7.22.13", "@babel/highlight@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== dependencies: - "@babel/helper-validator-identifier" "^7.22.5" - chalk "^2.0.0" + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" js-tokens "^4.0.0" "@babel/parser@^7.1.0", "@babel/parser@^7.16.4", "@babel/parser@^7.22.5": @@ -1023,11 +1036,11 @@ regenerator-runtime "^0.13.11" "@babel/runtime@^7.12.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz" - integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" + integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== dependencies: - regenerator-runtime "^0.13.11" + regenerator-runtime "^0.14.0" "@babel/runtime@^7.21.0": version "7.21.5" @@ -1077,20 +1090,20 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@csstools/css-parser-algorithms@^2.3.0": +"@csstools/css-parser-algorithms@^2.3.1": version "2.3.1" resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz#ec4fc764ba45d2bb7ee2774667e056aa95003f3a" integrity sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA== -"@csstools/css-tokenizer@^2.1.1": +"@csstools/css-tokenizer@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.0.tgz#9d70e6dcbe94e44c7400a2929928db35c4de32b5" integrity sha512-wErmsWCbsmig8sQKkM6pFhr/oPha1bHfvxsUY5CYSQxwyhA9Ulrs8EqCgClhg4Tgg2XapVstGqSVcz0xOYizZA== -"@csstools/media-query-list-parser@^2.1.2": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.3.tgz#4471ebd436a22019378fe9c8ac8c0f30c4fbb796" - integrity sha512-ATul1u+pic4aVpstgueqxEv4MsObEbszAxfTXpx9LHaeD3LAh+wFqdCteyegWmjk0k5rkSCAvIOaJe9U3DD09w== +"@csstools/media-query-list-parser@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.4.tgz#0017f99945f6c16dd81a7aacf6821770933c3a5c" + integrity sha512-V/OUXYX91tAC1CDsiY+HotIcJR+vPtzrX8pCplCpT++i8ThZZsq5F5dzZh/bDM3WUOjrvC1ljed1oSJxMfjqhw== "@csstools/postcss-cascade-layers@^1.1.1": version "1.1.1" @@ -1325,15 +1338,20 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/regexpp@^4.4.0": version "4.6.2" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== -"@eslint/eslintrc@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93" - integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA== +"@eslint-community/regexpp@^4.6.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.1.tgz#8c4bb756cc2aa7eaf13cfa5e69c83afb3260c20c" + integrity sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ== + +"@eslint/eslintrc@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" + integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1345,10 +1363,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^8.46.0": - version "8.46.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" - integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== +"@eslint/js@8.49.0": + version "8.49.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333" + integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w== "@fastify/accept-negotiator@^1.1.0": version "1.1.0" @@ -1494,10 +1512,10 @@ tslib "^2.0.1" typescript "^4.0" -"@humanwhocodes/config-array@^0.11.10": - version "0.11.10" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" - integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== +"@humanwhocodes/config-array@^0.11.11": + version "0.11.11" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" + integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -1709,30 +1727,43 @@ dependencies: is-promise "^4.0.0" -"@netlify/ipx@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@netlify/ipx/-/ipx-1.4.1.tgz#5b0e7508e343b960e7e97d2094089dd1585c88f6" - integrity sha512-wOuemSttKv8h4XMC9+RNNv21tn0uIC5avvnwmuFOyPOGzI1nsip01UkxS5lHRX+KX7TdIQxwBzSqde15h/jdRA== +"@netlify/functions@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-2.0.2.tgz#b5162058434fd00e280e4b28283f6bb8a2840c5c" + integrity sha512-goWRtaIPUK/q47qLYtfGGj7HgJIRaT0snw7zZ0yeoNTfQfCRwQwvRrMAsXkCsCtq2N2Oo81L26SpkMxEQMk9hg== dependencies: - "@netlify/functions" "^1.6.0" + "@netlify/serverless-functions-api" "1.7.3" + is-promise "^4.0.0" + +"@netlify/ipx@^1.4.3": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@netlify/ipx/-/ipx-1.4.4.tgz#b939e18639749f444eac1b88cb2a85790e7d13a1" + integrity sha512-UM2pFatBDg9ehrUwA/PwpAo/PpF447uXFgKR/2QtqlPriwJlNXJ7sj/YPEblC/8UzNJjhSIv2gA6T9bcVqxe7Q== + dependencies: + "@netlify/functions" "^2.0.1" etag "^1.8.1" fs-extra "^11.0.0" ipx "^1.0.0" micromatch "^4.0.5" - mkdirp "^1.0.4" + mkdirp "^3.0.0" murmurhash "^2.0.0" node-fetch "^2.0.0" ufo "^1.0.0" unstorage "^1.0.0" +"@netlify/node-cookies@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@netlify/node-cookies/-/node-cookies-0.1.0.tgz#dda912ba618527695cf519fafa221c5e6777c612" + integrity sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g== + "@netlify/plugin-nextjs@^4.27.3": - version "4.39.1" - resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-4.39.1.tgz#e9d5abe281389e30d59cf5839496bb2a14a196db" - integrity sha512-gdpTDRYJg8g55MQBy1JMcVJ3UgHC71M/u7BccI9oqvizVtV7ESgyCdJgGFPfZJJGCgWe2ANfQCk1wgA0yosnow== + version "4.40.1" + resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-4.40.1.tgz#ccb10b08fe9081df29437f709ad4448cae2bd329" + integrity sha512-FVXQEzBCymJHu2JYnJs03hUXjJAIAHcLMsrbcrnSjqmib4yF65YZ9yJYmrajV2D6LRqN/TxWhzTEgGr9eYpRHQ== dependencies: "@netlify/esbuild" "0.14.39" "@netlify/functions" "^1.6.0" - "@netlify/ipx" "^1.4.1" + "@netlify/ipx" "^1.4.3" "@vercel/node-bridge" "^2.1.0" chalk "^4.1.2" chokidar "^3.5.3" @@ -1754,10 +1785,18 @@ slash "^3.0.0" tiny-glob "^0.2.9" -"@next/env@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.19.tgz#46905b4e6f62da825b040343cbc233144e9578d3" - integrity sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ== +"@netlify/serverless-functions-api@1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@netlify/serverless-functions-api/-/serverless-functions-api-1.7.3.tgz#74efc650fcbca7c25211bb49df826795cf53f072" + integrity sha512-n6/7cJlSWvvbBlUOEAbkGyEld80S6KbG/ldQI9OhLfe1lTatgKmrTNIgqVNpaWpUdTgP2OHWFjmFBzkxxBWs5w== + dependencies: + "@netlify/node-cookies" "^0.1.0" + urlpattern-polyfill "8.0.2" + +"@next/env@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.2.tgz#1c09e6cf1df8b1edf3cf0ca9c0e0119a49802a5d" + integrity sha512-dUseBIQVax+XtdJPzhwww4GetTjlkRSsXeQnisIJWBaHsnxYcN2RGzsPHi58D6qnkATjnhuAtQTJmR1hKYQQPg== "@next/eslint-plugin-next@12.3.4": version "12.3.4" @@ -1766,50 +1805,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz#77ad462b5ced4efdc26cb5a0053968d2c7dac1b6" - integrity sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ== +"@next/swc-darwin-arm64@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.2.tgz#f099a36fdd06b1949eb4e190aee95a52b97d3885" + integrity sha512-7eAyunAWq6yFwdSQliWMmGhObPpHTesiKxMw4DWVxhm5yLotBj8FCR4PXGkpRP2tf8QhaWuVba+/fyAYggqfQg== -"@next/swc-darwin-x64@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz#aebe38713a4ce536ee5f2a291673e14b715e633a" - integrity sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw== +"@next/swc-darwin-x64@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.2.tgz#b8950fbe150db6f82961619e31fc6e9232fce8f4" + integrity sha512-WxXYWE7zF1ch8rrNh5xbIWzhMVas6Vbw+9BCSyZvu7gZC5EEiyZNJsafsC89qlaSA7BnmsDXVWQmc+s1feSYbQ== -"@next/swc-linux-arm64-gnu@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz#ec54db65b587939c7b94f9a84800f003a380f5a6" - integrity sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg== +"@next/swc-linux-arm64-gnu@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.2.tgz#8134d31fa9ad6848561b6969d27a8c07ab090974" + integrity sha512-URSwhRYrbj/4MSBjLlefPTK3/tvg95TTm6mRaiZWBB6Za3hpHKi8vSdnCMw5D2aP6k0sQQIEG6Pzcfwm+C5vrg== -"@next/swc-linux-arm64-musl@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz#1f5e2c1ea6941e7d530d9f185d5d64be04279d86" - integrity sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA== +"@next/swc-linux-arm64-musl@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.2.tgz#56233fe5140ed437c638194f0a01a3f89821ca89" + integrity sha512-HefiwAdIygFyNmyVsQeiJp+j8vPKpIRYDlmTlF9/tLdcd3qEL/UEBswa1M7cvO8nHcr27ZTKXz5m7dkd56/Esg== -"@next/swc-linux-x64-gnu@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz#96b0882492a2f7ffcce747846d3680730f69f4d1" - integrity sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g== +"@next/swc-linux-x64-gnu@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.2.tgz#1947a9dc603e6d5d5a8e99db7d42e2240c78e713" + integrity sha512-htGVVroW0tdHgMYwKWkxWvVoG2RlAdDXRO1RQxYDvOBQsaV0nZsgKkw0EJJJ3urTYnwKskn/MXm305cOgRxD2w== -"@next/swc-linux-x64-musl@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz#f276b618afa321d2f7b17c81fc83f429fb0fd9d8" - integrity sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q== +"@next/swc-linux-x64-musl@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.2.tgz#83eea3985eed84fbbbb1004a555d2f093d4ed245" + integrity sha512-UBD333GxbHVGi7VDJPPDD1bKnx30gn2clifNJbla7vo5nmBV+x5adyARg05RiT9amIpda6yzAEEUu+s774ldkw== -"@next/swc-win32-arm64-msvc@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz#1599ae0d401da5ffca0947823dac577697cce577" - integrity sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw== +"@next/swc-win32-arm64-msvc@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.2.tgz#c3734235e85458b76ec170dd0d6c13c2fdfac5ed" + integrity sha512-Em9ApaSFIQnWXRT3K6iFnr9uBXymixLc65Xw4eNt7glgH0eiXpg+QhjmgI2BFyc7k4ZIjglfukt9saNpEyolWA== -"@next/swc-win32-ia32-msvc@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz#55cdd7da90818f03e4da16d976f0cb22045d16fd" - integrity sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA== +"@next/swc-win32-ia32-msvc@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.2.tgz#cf16184af9be8b8f7750833a441c116b7a76b273" + integrity sha512-TBACBvvNYU+87X0yklSuAseqdpua8m/P79P0SG1fWUvWDDA14jASIg7kr86AuY5qix47nZLEJ5WWS0L20jAUNw== -"@next/swc-win32-x64-msvc@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz#648f79c4e09279212ac90d871646ae12d80cdfce" - integrity sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw== +"@next/swc-win32-x64-msvc@13.5.2": + version "13.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.2.tgz#cf8db00763d9219567655b90853b7d484f3fcad6" + integrity sha512-LfTHt+hTL8w7F9hnB3H4nRasCzLD/fP+h4/GUVBTxrkMJOnh/7OZ0XbYDKO/uuWwryJS9kZjhxcruBiYwc5UDw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1832,22 +1871,114 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@prisma/client@5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.2.0.tgz#cbfdd440614b38736563a7999f39922fcde0ed50" - integrity sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ== +"@parcel/watcher-android-arm64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.3.0.tgz#d82e74bb564ebd4d8a88791d273a3d2bd61e27ab" + integrity sha512-f4o9eA3dgk0XRT3XhB0UWpWpLnKgrh1IwNJKJ7UJek7eTYccQ8LR7XUWFKqw6aEq5KUNlCcGvSzKqSX/vtWVVA== + +"@parcel/watcher-darwin-arm64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.3.0.tgz#c9cd03f8f233d512fcfc873d5b4e23f1569a82ad" + integrity sha512-mKY+oijI4ahBMc/GygVGvEdOq0L4DxhYgwQqYAz/7yPzuGi79oXrZG52WdpGA1wLBPrYb0T8uBaGFo7I6rvSKw== + +"@parcel/watcher-darwin-x64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.3.0.tgz#83c902994a2a49b9e1ab5050dba24876fdc2c219" + integrity sha512-20oBj8LcEOnLE3mgpy6zuOq8AplPu9NcSSSfyVKgfOhNAc4eF4ob3ldj0xWjGGbOF7Dcy1Tvm6ytvgdjlfUeow== + +"@parcel/watcher-freebsd-x64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.3.0.tgz#7a0f4593a887e2752b706aff2dae509aef430cf6" + integrity sha512-7LftKlaHunueAEiojhCn+Ef2CTXWsLgTl4hq0pkhkTBFI3ssj2bJXmH2L67mKpiAD5dz66JYk4zS66qzdnIOgw== + +"@parcel/watcher-linux-arm-glibc@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.3.0.tgz#3fc90c3ebe67de3648ed2f138068722f9b1d47da" + integrity sha512-1apPw5cD2xBv1XIHPUlq0cO6iAaEUQ3BcY0ysSyD9Kuyw4MoWm1DV+W9mneWI+1g6OeP6dhikiFE6BlU+AToTQ== + +"@parcel/watcher-linux-arm64-glibc@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.3.0.tgz#f7bbbf2497d85fd11e4c9e9c26ace8f10ea9bcbc" + integrity sha512-mQ0gBSQEiq1k/MMkgcSB0Ic47UORZBmWoAWlMrTW6nbAGoLZP+h7AtUM7H3oDu34TBFFvjy4JCGP43JlylkTQA== + +"@parcel/watcher-linux-arm64-musl@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.3.0.tgz#de131a9fcbe1fa0854e9cbf4c55bed3b35bcff43" + integrity sha512-LXZAExpepJew0Gp8ZkJ+xDZaTQjLHv48h0p0Vw2VMFQ8A+RKrAvpFuPVCVwKJCr5SE+zvaG+Etg56qXvTDIedw== + +"@parcel/watcher-linux-x64-glibc@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.3.0.tgz#193dd1c798003cdb5a1e59470ff26300f418a943" + integrity sha512-P7Wo91lKSeSgMTtG7CnBS6WrA5otr1K7shhSjKHNePVmfBHDoAOHYRXgUmhiNfbcGk0uMCHVcdbfxtuiZCHVow== + +"@parcel/watcher-linux-x64-musl@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.3.0.tgz#6dbdb86d96e955ab0fe4a4b60734ec0025a689dd" + integrity sha512-+kiRE1JIq8QdxzwoYY+wzBs9YbJ34guBweTK8nlzLKimn5EQ2b2FSC+tAOpq302BuIMjyuUGvBiUhEcLIGMQ5g== + +"@parcel/watcher-wasm@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-wasm/-/watcher-wasm-2.3.0.tgz#73b66c6fbd2a3326ae86a1ec77eab7139d0dd725" + integrity sha512-ejBAX8H0ZGsD8lSICDNyMbSEtPMWgDL0WFCt/0z7hyf5v8Imz4rAM8xY379mBsECkq/Wdqa5WEDLqtjZ+6NxfA== dependencies: - "@prisma/engines-version" "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f" + is-glob "^4.0.3" + micromatch "^4.0.5" + napi-wasm "^1.1.0" -"@prisma/engines-version@5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f": - version "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f.tgz#11366e7ff031c908debf4983248d40046016de37" - integrity sha512-jsnKT5JIDIE01lAeCj2ghY9IwxkedhKNvxQeoyLs6dr4ZXynetD0vTy7u6wMJt8vVPv8I5DPy/I4CFaoXAgbtg== +"@parcel/watcher-win32-arm64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.3.0.tgz#59da26a431da946e6c74fa6b0f30b120ea6650b6" + integrity sha512-35gXCnaz1AqIXpG42evcoP2+sNL62gZTMZne3IackM+6QlfMcJLy3DrjuL6Iks7Czpd3j4xRBzez3ADCj1l7Aw== -"@prisma/engines@5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.2.0.tgz#e5dff48eb324c8137393933292d44ea5c3bc2ce7" - integrity sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig== +"@parcel/watcher-win32-ia32@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.3.0.tgz#3ee6a18b08929cd3b788e8cc9547fd9a540c013a" + integrity sha512-FJS/IBQHhRpZ6PiCjFt1UAcPr0YmCLHRbTc00IBTrelEjlmmgIVLeOx4MSXzx2HFEy5Jo5YdhGpxCuqCyDJ5ow== + +"@parcel/watcher-win32-x64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.3.0.tgz#14e7246289861acc589fd608de39fe5d8b4bb0a7" + integrity sha512-dLx+0XRdMnVI62kU3wbXvbIRhLck4aE28bIGKbRGS7BJNt54IIj9+c/Dkqb+7DJEbHUZAX1bwaoM8PqVlHJmCA== + +"@parcel/watcher@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.3.0.tgz#803517abbc3981a1a1221791d9f59dc0590d50f9" + integrity sha512-pW7QaFiL11O0BphO+bq3MgqeX/INAk9jgBldVDYjlQPO4VddoZnF22TcF9onMhnLVHuNqBJeRf+Fj7eezi/+rQ== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.3.0" + "@parcel/watcher-darwin-arm64" "2.3.0" + "@parcel/watcher-darwin-x64" "2.3.0" + "@parcel/watcher-freebsd-x64" "2.3.0" + "@parcel/watcher-linux-arm-glibc" "2.3.0" + "@parcel/watcher-linux-arm64-glibc" "2.3.0" + "@parcel/watcher-linux-arm64-musl" "2.3.0" + "@parcel/watcher-linux-x64-glibc" "2.3.0" + "@parcel/watcher-linux-x64-musl" "2.3.0" + "@parcel/watcher-win32-arm64" "2.3.0" + "@parcel/watcher-win32-ia32" "2.3.0" + "@parcel/watcher-win32-x64" "2.3.0" + +"@prisma/client@5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.3.1.tgz#fc7fc2d91e814cc4fe18a4bc5e78bf851c26985e" + integrity sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q== + dependencies: + "@prisma/engines-version" "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59" + +"@prisma/engines-version@5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59": + version "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz#7eb6f5c6b7628b8b39df55c903f411528a6f761c" + integrity sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w== + +"@prisma/engines@5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.3.1.tgz#53cc72a5ed176dc27d22305fe5569c64cc78b381" + integrity sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA== "@react-spring/animated@~9.7.3": version "9.7.3" @@ -1998,9 +2129,9 @@ "@rollup/pluginutils" "^5.0.1" "@rollup/plugin-node-resolve@^15.2.0": - version "15.2.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.0.tgz#982053b237f81471aace570472e88a456d211621" - integrity sha512-mKur03xNGT8O9ODO6FtT43ITGqHWZbKPdVJHZb+iV9QYcdlhUUB0wgknvA4KCUmC5oHJF6O2W1EgmyOQyVUI4Q== + version "15.2.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz#a15b14fb7969229e26a30feff2816d39eff503f0" + integrity sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w== dependencies: "@rollup/pluginutils" "^5.0.1" "@types/resolve" "1.20.2" @@ -2017,7 +2148,16 @@ "@rollup/pluginutils" "^5.0.1" magic-string "^0.27.0" -"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.0.2": +"@rollup/pluginutils@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.4.tgz#74f808f9053d33bafec0cc98e7b835c9667d32ba" + integrity sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + +"@rollup/pluginutils@^5.0.2": version "5.0.2" resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz" integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA== @@ -2244,24 +2384,24 @@ "@svgr/plugin-jsx" "^6.5.1" "@svgr/plugin-svgo" "^6.5.1" -"@swc/helpers@0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" - integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== +"@swc/helpers@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" + integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== dependencies: tslib "^2.4.0" -"@tanstack/query-core@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.33.0.tgz#7756da9a75a424e521622b1d84eb55b7a2b33715" - integrity sha512-qYu73ptvnzRh6se2nyBIDHGBQvPY1XXl3yR769B7B6mIDD7s+EZhdlWHQ67JI6UOTFRaI7wupnTnwJ3gE0Mr/g== +"@tanstack/query-core@4.35.3": + version "4.35.3" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.35.3.tgz#1c127e66b4ad1beeac052db83a812d7cb67369e1" + integrity sha512-PS+WEjd9wzKTyNjjQymvcOe1yg8f3wYc6mD+vb6CKyZAKvu4sIJwryfqfBULITKCla7P9C4l5e9RXePHvZOZeQ== "@tanstack/react-query@^4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.33.0.tgz#e927b0343a6ecaa948fee59e9ca98fe561062638" - integrity sha512-97nGbmDK0/m0B86BdiXzx3EW9RcDYKpnyL2+WwyuLHEgpfThYAnXFaMMmnTDuAO4bQJXEhflumIEUfKmP7ESGA== + version "4.35.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.35.3.tgz#03d726ef6a19d426166427c6539b003dd9606d1b" + integrity sha512-UgTPioip/rGG3EQilXfA2j4BJkhEQsR+KAbF+KIuvQ7j4MkgnTCJF01SfRpIRNtQTlEfz/+IL7+jP8WA8bFbsw== dependencies: - "@tanstack/query-core" "4.33.0" + "@tanstack/query-core" "4.35.3" use-sync-external-store "^1.2.0" "@trysound/sax@0.2.0": @@ -2331,7 +2471,7 @@ "@types/estree@*", "@types/estree@^1.0.0": version "1.0.1" - resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== "@types/estree@^0.0.50": @@ -2340,9 +2480,9 @@ integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== "@types/fs-extra@^8.0.1": - version "8.1.2" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.2.tgz#7125cc2e4bdd9bd2fc83005ffdb1d0ba00cca61f" - integrity sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg== + version "8.1.3" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.3.tgz#4807768c0b0a5a5f4746d8fde2f7ab0137076eea" + integrity sha512-7IdV01N0u/CaVO0fuY1YmEg14HQN3+EW8mpNgg6NEfxEl/lzCa5OxlBu3iFsCAdamnYOcTQ7oEi43Xc/67Rgzw== dependencies: "@types/node" "*" @@ -2355,7 +2495,7 @@ "@types/glob@^7.1.1": version "7.2.0" - resolved "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== dependencies: "@types/minimatch" "*" @@ -2391,7 +2531,7 @@ "@types/minimatch@*": version "5.1.2" - resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== "@types/minimist@^1.2.0", "@types/minimist@^1.2.2": @@ -2400,9 +2540,9 @@ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/node@*": - version "18.15.10" - resolved "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz" - integrity sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ== + version "20.6.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.3.tgz#5b763b321cd3b80f6b8dde7a37e1a77ff9358dd9" + integrity sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA== "@types/node@14": version "14.18.20" @@ -2415,9 +2555,9 @@ integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g== "@types/node@^18.11.9": - version "18.17.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.6.tgz#0296e9a30b22d2a8fcaa48d3c45afe51474ca55b" - integrity sha512-fGmT/P7z7ecA6bv/ia5DlaWCH4YeZvAQMNpUhrJjtAhOhZfoxS1VLUgU2pdk63efSjQaOJWdXMuAJsws+8I6dg== + version "18.17.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.18.tgz#acae19ad9011a2ab3d792232501c95085ba1838f" + integrity sha512-/4QOuy3ZpV7Ya1GTRz5CYSz3DgkKpyUptXuQ5PPce7uuyJAOR7r9FhkmxJfvcNUXyklbC63a+YvB3jxy7s9ngw== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -2430,9 +2570,9 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prop-types@*": - version "15.7.5" - resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + version "15.7.6" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.6.tgz#bbf819813d6be21011b8f5801058498bec555572" + integrity sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg== "@types/react-dom@^18.0.8": version "18.2.7" @@ -2461,9 +2601,9 @@ csstype "^3.0.2" "@types/react@^18.0.25": - version "18.2.20" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.20.tgz#1605557a83df5c8a2cc4eeb743b3dfc0eb6aaeb2" - integrity sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw== + version "18.2.22" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.22.tgz#abe778a1c95a07fa70df40a52d7300a40b949ccb" + integrity sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2471,13 +2611,13 @@ "@types/resolve@1.20.2": version "1.20.2" - resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== "@types/scheduler@*": - version "0.16.2" - resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" - integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + version "0.16.3" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" + integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== "@types/schema-utils@^2.4.0": version "2.4.0" @@ -2757,6 +2897,11 @@ acorn@^6.4.1: resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== +acorn@^8.10.0, acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + acorn@^8.4.1: version "8.8.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" @@ -2767,11 +2912,6 @@ acorn@^8.8.2: resolved "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz" integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== -acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== - aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" @@ -2911,7 +3051,7 @@ array-find-index@^1.0.1: resolved "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz" integrity sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw== -array-includes@^3.1.5, array-includes@^3.1.6: +array-includes@^3.1.5: version "3.1.6" resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz" integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== @@ -2922,40 +3062,51 @@ array-includes@^3.1.5, array-includes@^3.1.6: get-intrinsic "^1.1.3" is-string "^1.0.7" +array-includes@^3.1.6: + version "3.1.7" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" + integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-string "^1.0.7" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== array.prototype.findlastindex@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz#bc229aef98f6bd0533a2bc61ff95209875526c9b" - integrity sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw== + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207" + integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" - get-intrinsic "^1.1.3" + get-intrinsic "^1.2.1" array.prototype.flat@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" - integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" array.prototype.flatmap@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" - integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" array.prototype.tosorted@^1.1.1: @@ -2969,14 +3120,15 @@ array.prototype.tosorted@^1.1.1: es-shim-unscopables "^1.0.0" get-intrinsic "^1.1.3" -arraybuffer.prototype.slice@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb" - integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw== +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== dependencies: array-buffer-byte-length "^1.0.0" call-bind "^1.0.2" define-properties "^1.2.0" + es-abstract "^1.22.1" get-intrinsic "^1.2.1" is-array-buffer "^3.0.2" is-shared-array-buffer "^1.0.2" @@ -3211,7 +3363,7 @@ buffer@^5.5.0: builtin-modules@^3.3.0: version "3.3.0" - resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== busboy@1.6.0: @@ -3283,7 +3435,7 @@ caseless@~0.12.0: resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -3301,9 +3453,9 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: supports-color "^7.1.0" chart.js@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.3.2.tgz#904ebe0376eb368a659a92d2050df47336847e4c" - integrity sha512-pvQNyFOY1QmbmIr8oDORL16/FFivfxj8V26VFpFilMo4cNvkV5WXLJetDio365pd9gKUHGdirUTbqJfw8tr+Dg== + version "4.4.0" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.0.tgz#df843fdd9ec6bd88d7f07e2b95348d221bd2698c" + integrity sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ== dependencies: "@kurkle/color" "^0.3.0" @@ -3334,7 +3486,7 @@ chownr@^1.1.1: chownr@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== ci-info@^3.2.0: @@ -3342,10 +3494,10 @@ ci-info@^3.2.0: resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz" integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== -citty@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.2.tgz#bae07bfd1962439735d7698c7954f76025690603" - integrity sha512-Me9nf0/BEmMOnuQzMOVXgpzkMUNbd0Am8lTl/13p0aRGAoLGk5T5sdet/42CrIGmWdG67BgHUhcKK1my1ujUEg== +citty@^0.1.3, citty@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.4.tgz#91091be06ae4951dffa42fd443de7fe72245f2e0" + integrity sha512-Q3bK1huLxzQrvj7hImJ7Z1vKYJRPQCDnd0EjXfHMidcjecGOMuLrmuQmtWmFkuKLcMThlGh1yCKG8IEc6VeNXQ== dependencies: consola "^3.2.3" @@ -3511,7 +3663,7 @@ concat-with-sourcemaps@^1.1.0: dependencies: source-map "^0.6.1" -consola@^3.1.0, consola@^3.2.3: +consola@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== @@ -3573,13 +3725,13 @@ cosmiconfig@^8.1.3: path-type "^4.0.0" cosmiconfig@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" - integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== dependencies: - import-fresh "^3.2.1" + import-fresh "^3.3.0" js-yaml "^4.1.0" - parse-json "^5.0.0" + parse-json "^5.2.0" path-type "^4.0.0" create-require@^1.1.0: @@ -3785,9 +3937,9 @@ csstype@^2.6.8: integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA== csstype@^3.0.2: - version "3.1.1" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" - integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== currently-unhandled@^0.4.1: version "0.4.1" @@ -3965,11 +4117,21 @@ deepmerge@^4.2.2, deepmerge@^4.3.1: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== +define-data-property@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451" + integrity sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g== dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -4016,15 +4178,15 @@ denque@^2.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== -destr@^1.1.1, destr@^1.2.2: +destr@^1.1.1: version "1.2.2" resolved "https://registry.yarnpkg.com/destr/-/destr-1.2.2.tgz#7ba9befcafb645a50e76b260449c63927b51e22f" integrity sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA== -destr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.0.tgz#60847d02b211de6e252fc72806f4ec39ec257e7b" - integrity sha512-FJ9RDpf3GicEBvzI3jxc2XhHzbqD8p4ANw/1kPsFBfTvP1b7Gn/Lg1vO7R9J4IVgoMbyUmFrFGZafJ1hPZpvlg== +destr@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.1.tgz#2fc7bddc256fed1183e03f8d148391dde4023cb2" + integrity sha512-M1Ob1zPSIvlARiJUkKqvAZ3VAqQY6Jcuth/pBKQ2b1dX/Qx0OnJ8Vux6J2H5PTMQeRzWrrbTu70VxBfv/OPDJA== detect-browser@^5.2.0: version "5.3.0" @@ -4036,6 +4198,11 @@ detect-indent@^6.0.0: resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + detect-libc@^2.0.0, detect-libc@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" @@ -4201,51 +4368,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2: - version "1.22.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" - integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== - dependencies: - array-buffer-byte-length "^1.0.0" - arraybuffer.prototype.slice "^1.0.1" - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-set-tostringtag "^2.0.1" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.2.1" - get-symbol-description "^1.0.0" - globalthis "^1.0.3" - gopd "^1.0.1" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-proto "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.5" - is-array-buffer "^3.0.2" - is-callable "^1.2.7" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-typed-array "^1.1.10" - is-weakref "^1.0.2" - object-inspect "^1.12.3" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.0" - safe-array-concat "^1.0.0" - safe-regex-test "^1.0.0" - string.prototype.trim "^1.2.7" - string.prototype.trimend "^1.0.6" - string.prototype.trimstart "^1.0.6" - typed-array-buffer "^1.0.0" - typed-array-byte-length "^1.0.0" - typed-array-byte-offset "^1.0.0" - typed-array-length "^1.0.4" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.10" - es-abstract@^1.19.1: version "1.21.1" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz" @@ -4285,6 +4407,51 @@ es-abstract@^1.19.1: unbox-primitive "^1.0.2" which-typed-array "^1.1.9" +es-abstract@^1.20.4, es-abstract@^1.22.1: + version "1.22.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.2.tgz#90f7282d91d0ad577f505e423e52d4c1d93c1b8a" + integrity sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.2" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.1" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.12" + is-weakref "^1.0.2" + object-inspect "^1.12.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.11" + es-module-lexer@^1.0.5: version "1.2.1" resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz" @@ -4345,7 +4512,7 @@ esbuild@^0.17.17: escalade@^3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-string-regexp@^1.0.5: @@ -4374,16 +4541,16 @@ eslint-config-next@^12.2.4: eslint-plugin-react-hooks "^4.5.0" eslint-config-prettier@^8.5.0: - version "8.9.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.9.0.tgz#094b6254b2804b0544f7cee535f802b6d29ee10b" - integrity sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA== + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" + integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== eslint-import-resolver-alias@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz" integrity sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w== -eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.7: +eslint-import-resolver-node@^0.3.6: version "0.3.7" resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz" integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== @@ -4392,6 +4559,15 @@ eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.7: is-core-module "^2.11.0" resolve "^1.22.1" +eslint-import-resolver-node@^0.3.7: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + eslint-import-resolver-typescript@^2.7.1: version "2.7.1" resolved "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz" @@ -4411,9 +4587,9 @@ eslint-module-utils@^2.8.0: debug "^3.2.7" eslint-plugin-import@^2.26.0: - version "2.28.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz#8d66d6925117b06c4018d491ae84469eb3cb1005" - integrity sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q== + version "2.28.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz#63b8b5b3c409bfc75ebaf8fb206b07ab435482c4" + integrity sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A== dependencies: array-includes "^3.1.6" array.prototype.findlastindex "^1.2.2" @@ -4424,13 +4600,12 @@ eslint-plugin-import@^2.26.0: eslint-import-resolver-node "^0.3.7" eslint-module-utils "^2.8.0" has "^1.0.3" - is-core-module "^2.12.1" + is-core-module "^2.13.0" is-glob "^4.0.3" minimatch "^3.1.2" object.fromentries "^2.0.6" object.groupby "^1.0.0" object.values "^1.1.6" - resolve "^1.22.3" semver "^6.3.1" tsconfig-paths "^3.14.2" @@ -4502,21 +4677,21 @@ eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" - integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint@^8.33.0: - version "8.46.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552" - integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg== + version "8.49.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.49.0.tgz#09d80a89bdb4edee2efcf6964623af1054bf6d42" + integrity sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.1" - "@eslint/js" "^8.46.0" - "@humanwhocodes/config-array" "^0.11.10" + "@eslint/eslintrc" "^2.1.2" + "@eslint/js" "8.49.0" + "@humanwhocodes/config-array" "^0.11.11" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" ajv "^6.12.4" @@ -4526,7 +4701,7 @@ eslint@^8.33.0: doctrine "^3.0.0" escape-string-regexp "^4.0.0" eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.2" + eslint-visitor-keys "^3.4.3" espree "^9.6.1" esquery "^1.4.2" esutils "^2.0.2" @@ -4595,7 +4770,7 @@ estree-walker@^0.6.1: estree-walker@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== esutils@^2.0.2: @@ -4685,14 +4860,14 @@ fast-equals@^3.0.1: integrity sha512-NCe8qxnZFARSHGztGMZOO/PC1qa5MIFB5Hp66WdzbCRAz8U8US3bx1UTgLS49efBQPcUtO9gf5oVEY8o7y/7Kg== fast-fifo@^1.1.0, fast-fifo@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.0.tgz#03e381bcbfb29932d7c3afde6e15e83e05ab4d8b" - integrity sha512-IgfweLvEpwyA4WgiQe9Nx6VV2QkML2NkvZnk1oKnIzXgXdWxuhF7zw4DvLTPZJn6PIUneiAXPF24QmoEqHTjyw== + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== -fast-glob@^3.0.3, fast-glob@^3.2.7: - version "3.2.12" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== +fast-glob@^3.0.3, fast-glob@^3.2.9, fast-glob@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -4700,10 +4875,10 @@ fast-glob@^3.0.3, fast-glob@^3.2.7: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.2.9, fast-glob@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== +fast-glob@^3.2.7: + version "3.2.12" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -4772,11 +4947,12 @@ find-up@^5.0.0: path-exists "^4.0.0" flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.1.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f" + integrity sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew== dependencies: - flatted "^3.1.0" + flatted "^3.2.7" + keyv "^4.5.3" rimraf "^3.0.2" flat@^5.0.0: @@ -4784,15 +4960,15 @@ flat@^5.0.0: resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flatted@^3.2.7: + version "3.2.9" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== follow-redirects@^1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== for-each@^0.3.3: version "0.3.3" @@ -4871,7 +5047,7 @@ fs-extra@^9.0.0: fs-minipass@^2.0.0: version "2.1.0" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== dependencies: minipass "^3.0.0" @@ -4882,26 +5058,26 @@ fs.realpath@^1.0.0: integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" - integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - functions-have-names "^1.2.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" -functions-have-names@^1.2.2, functions-have-names@^1.2.3: +functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -4938,10 +5114,10 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== -get-port-please@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.0.1.tgz#a24953a41dc249f76869ac25e81d6623e61ab010" - integrity sha512-R5pcVO8Z1+pVDu8Ml3xaJCEkBiiy1VQN9za0YqH8GIi1nIqD4IzQhzY6dDzMRtdS1lyiGlucRzm8IN8wtLIXng== +get-port-please@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.1.1.tgz#2556623cddb4801d823c0a6a15eec038abb483be" + integrity sha512-3UBAyM3u4ZBVYDsxOQfJDxEa6XTbpBDrOjp4mf7ExFRt5BKs/QywQQiJsh2B+hxcZLSapWqCRvElUe8DnKcFHA== get-stream@^6.0.0: version "6.0.1" @@ -5044,9 +5220,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: - version "13.20.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" - integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + version "13.22.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.22.0.tgz#0c9fcb9c48a2494fbb5edbfee644285543eba9d8" + integrity sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw== dependencies: type-fest "^0.20.2" @@ -5129,18 +5305,19 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -h3@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/h3/-/h3-1.7.1.tgz#fc9328adf5da1d29cbb2d97b81ae3dd9b426463e" - integrity sha512-A9V2NEDNHet7v1gCg7CMwerSigLi0SRbhTy7C3lGb0N4YKIpPmLDjedTUopqp4dnn7COHfqUjjaz3zbtz4QduA== +h3@^1.7.1, h3@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/h3/-/h3-1.8.1.tgz#e36371407982b0a37475412d64697c521dd3ce43" + integrity sha512-m5rFuu+5bpwBBHqqS0zexjK+Q8dhtFRvO9JXQG0RvSPL6QrIT6vv42vuBM22SLOgGMoZYsHk0y7VPidt9s+nkw== dependencies: cookie-es "^1.0.0" defu "^6.1.2" - destr "^2.0.0" - iron-webcrypto "^0.7.0" - radix3 "^1.0.1" - ufo "^1.1.2" + destr "^2.0.1" + iron-webcrypto "^0.8.0" + radix3 "^1.1.0" + ufo "^1.3.0" uncrypto "^0.1.3" + unenv "^1.7.4" har-schema@^2.0.0: version "2.0.0" @@ -5291,7 +5468,7 @@ import-cwd@^3.0.0: dependencies: import-from "^3.0.0" -import-fresh@^3.2.1: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -5413,27 +5590,27 @@ ipaddr.js@^2.0.1: integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== ipx@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ipx/-/ipx-1.2.0.tgz#23130eed071a786158b86774980deca6e5d5fa9e" - integrity sha512-FkEP56C08HdlqlWKm3pMhatywPtDBTlePTdzskksCR1+7xnB6fQs6pXOTXPTG5i+gGPgCOxbNMUSZEH/DQcWDA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/ipx/-/ipx-1.3.0.tgz#bd70e3c3ce45995cf50d9a01ece819736374e36c" + integrity sha512-Jfu+zQ0NGZwSeZ11CGMOnqWFlIyVcT8dW48e5UxKnMjQXXDy8VLTl8FIP7vRIJ9hd3ZPaJ/RIXXLJfZmBqRXWQ== dependencies: "@fastify/accept-negotiator" "^1.1.0" - consola "^3.1.0" + consola "^3.2.3" defu "^6.1.2" - destr "^1.2.2" + destr "^2.0.1" etag "^1.8.1" image-meta "^0.1.1" - listhen "^1.0.4" - node-fetch-native "^1.1.1" + listhen "^1.4.4" + node-fetch-native "^1.4.0" pathe "^1.1.1" - sharp "^0.32.1" - ufo "^1.1.2" + sharp "^0.32.5" + ufo "^1.3.0" xss "^1.0.14" -iron-webcrypto@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-0.7.1.tgz#7323e1f32fbc5d3e1f25a228fdbf0bcde3c276c6" - integrity sha512-K/UmlEhPCPXEHV5hAtH5C0tI5JnFuOrv4yO/j7ODPl3HaiiHBLbOLTde+ieUaAyfCATe4LoAnclyF+hmSCOVmQ== +iron-webcrypto@^0.8.0: + version "0.8.2" + resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-0.8.2.tgz#c7b60c20ffc22f880f12540ba44b4db615ce515e" + integrity sha512-jGiwmpgTuF19Vt4hn3+AzaVFGpVZt7A1ysd5ivFel2r4aNVFwqaYa6aU6qsF1PM7b+WFivZHz3nipwUOXaOnHg== is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: version "3.0.2" @@ -5478,7 +5655,7 @@ is-boolean-object@^1.1.0: is-builtin-module@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== dependencies: builtin-modules "^3.3.0" @@ -5495,7 +5672,14 @@ is-ci@^3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.11.0, is-core-module@^2.12.0, is-core-module@^2.12.1, is-core-module@^2.5.0, is-core-module@^2.9.0: +is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.5.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== + dependencies: + has "^1.0.3" + +is-core-module@^2.9.0: version "2.12.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== @@ -5543,7 +5727,7 @@ is-localhost-ip@^1.4.0: is-module@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== is-negative-zero@^2.0.2: @@ -5649,7 +5833,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typed-array@^1.1.10, is-typed-array@^1.1.9: +is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: version "1.1.12" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== @@ -5681,9 +5865,9 @@ isarray@^2.0.5: integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== isbot@^3.4.5: - version "3.6.13" - resolved "https://registry.yarnpkg.com/isbot/-/isbot-3.6.13.tgz#e060f727cb53475a4dab1569321ced0f84633d63" - integrity sha512-uoP4uK5Dc2CrabmK+Gue1jTL+scHiCc1c9rblRpJwG8CPxjLIv8jmGyyGRGkbPOweayhkskdZsEQXG6p+QCQrg== + version "3.7.0" + resolved "https://registry.yarnpkg.com/isbot/-/isbot-3.7.0.tgz#c68eb005c03e3d225a0ea559211da2bff94bb1ce" + integrity sha512-9BcjlI89966BqWJmYdTnRub85sit931MyCthSIPtgoOsTjoW7A2MVa09HzPpYE2+G4vyAxfDvR0AbUGV0FInQg== isexe@^2.0.0: version "2.0.0" @@ -5704,10 +5888,10 @@ jest-worker@^26.2.1: merge-stream "^2.0.0" supports-color "^7.0.0" -jiti@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.1.tgz#fa99e4b76a23053e0e7cde098efe1704a14c16f1" - integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== +jiti@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42" + integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA== joycon@^3.1.1: version "3.1.1" @@ -5749,6 +5933,11 @@ jsesc@~0.5.0: resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" @@ -5810,7 +5999,7 @@ json5@^2.2.2: jsonc-parser@^3.2.0: version "3.2.0" - resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== jsonfile@^4.0.0: @@ -5889,6 +6078,13 @@ kafkajs@^2.1.0: resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz" integrity sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA== +keyv@^4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" + integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -5899,10 +6095,10 @@ kleur@^3.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -known-css-properties@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5" - integrity sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg== +known-css-properties@^0.28.0: + version "0.28.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.28.0.tgz#8a8be010f368b3036fe6ab0ef4bbbed972bd6274" + integrity sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ== language-subtag-registry@^0.3.20: version "0.3.22" @@ -5954,22 +6150,28 @@ lint-staged@^11.0.0: stringify-object "3.3.0" supports-color "8.1.1" -listhen@^1.0.4: - version "1.1.2" - resolved "https://registry.yarnpkg.com/listhen/-/listhen-1.1.2.tgz#c95082b0fdbd50d73abeab748893768dbefa7db8" - integrity sha512-rLX5V57oonazmc6zoZ2LzfbSOfGzDOLdQ/eTEh/d3f1xYMACH1yIU8nr0YGl2WiR+l31o3QCN4/VH2dUNyYvTA== +listhen@^1.2.2, listhen@^1.4.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/listhen/-/listhen-1.5.5.tgz#58915512af70f770aa3e9fb19367adf479bb58c4" + integrity sha512-LXe8Xlyh3gnxdv4tSjTjscD1vpr/2PRpzq8YIaMJgyKzRG8wdISlWVWnGThJfHnlJ6hmLt2wq1yeeix0TEbuoA== dependencies: - citty "^0.1.2" + "@parcel/watcher" "^2.3.0" + "@parcel/watcher-wasm" "2.3.0" + citty "^0.1.4" clipboardy "^3.0.0" consola "^3.2.3" defu "^6.1.2" - get-port-please "^3.0.1" + get-port-please "^3.1.1" + h3 "^1.8.1" http-shutdown "^1.2.2" - jiti "^1.19.1" - mlly "^1.4.0" + jiti "^1.20.0" + mlly "^1.4.2" node-forge "^1.3.1" pathe "^1.1.1" - ufo "^1.1.2" + std-env "^3.4.3" + ufo "^1.3.0" + untun "^0.1.2" + uqr "^0.1.2" listr2@^3.12.2: version "3.14.0" @@ -6117,9 +6319,9 @@ lower-case@^2.0.2: tslib "^2.0.3" lru-cache@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" - integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== + version "10.0.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" + integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== lru-cache@^5.1.1: version "5.1.1" @@ -6286,6 +6488,11 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.52.0" +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -6331,19 +6538,19 @@ minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: minipass@^3.0.0: version "3.3.6" - resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== dependencies: yallist "^4.0.0" minipass@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== minizlib@^2.1.1: version "2.1.2" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== dependencies: minipass "^3.0.0" @@ -6354,20 +6561,25 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^1.0.3, mkdirp@^1.0.4: +mkdirp@^1.0.3: version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mlly@^1.2.0, mlly@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.0.tgz#830c10d63f1f97bd8785377b24dc2a15d972832b" - integrity sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg== +mkdirp@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + +mlly@^1.2.0, mlly@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e" + integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg== dependencies: - acorn "^8.9.0" + acorn "^8.10.0" pathe "^1.1.1" pkg-types "^1.0.3" - ufo "^1.1.2" + ufo "^1.3.0" mmdb-lib@2.0.2: version "2.0.2" @@ -6436,6 +6648,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-wasm@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/napi-wasm/-/napi-wasm-1.1.0.tgz#bbe617823765ae9c1bc12ff5942370eae7b2ba4e" + integrity sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -6455,13 +6672,13 @@ next-basics@^0.36.0: jsonwebtoken "^9.0.0" pure-rand "^6.0.2" -next@13.4.19: - version "13.4.19" - resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f" - integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw== +next@13.5.2: + version "13.5.2" + resolved "https://registry.yarnpkg.com/next/-/next-13.5.2.tgz#809dd84e481049e298fe79d28b1d66b587483fca" + integrity sha512-vog4UhUaMYAzeqfiAAmgB/QWLW7p01/sg+2vn6bqc/CxHFYizMzLv6gjxKzl31EVFkfl/F+GbxlKizlkTE9RdA== dependencies: - "@next/env" "13.4.19" - "@swc/helpers" "0.5.1" + "@next/env" "13.5.2" + "@swc/helpers" "0.5.2" busboy "1.6.0" caniuse-lite "^1.0.30001406" postcss "8.4.14" @@ -6469,15 +6686,15 @@ next@13.4.19: watchpack "2.4.0" zod "3.21.4" optionalDependencies: - "@next/swc-darwin-arm64" "13.4.19" - "@next/swc-darwin-x64" "13.4.19" - "@next/swc-linux-arm64-gnu" "13.4.19" - "@next/swc-linux-arm64-musl" "13.4.19" - "@next/swc-linux-x64-gnu" "13.4.19" - "@next/swc-linux-x64-musl" "13.4.19" - "@next/swc-win32-arm64-msvc" "13.4.19" - "@next/swc-win32-ia32-msvc" "13.4.19" - "@next/swc-win32-x64-msvc" "13.4.19" + "@next/swc-darwin-arm64" "13.5.2" + "@next/swc-darwin-x64" "13.5.2" + "@next/swc-linux-arm64-gnu" "13.5.2" + "@next/swc-linux-arm64-musl" "13.5.2" + "@next/swc-linux-x64-gnu" "13.5.2" + "@next/swc-linux-x64-musl" "13.5.2" + "@next/swc-win32-arm64-msvc" "13.5.2" + "@next/swc-win32-ia32-msvc" "13.5.2" + "@next/swc-win32-x64-msvc" "13.5.2" nice-try@^1.0.4: version "1.0.5" @@ -6493,9 +6710,9 @@ no-case@^3.0.4: tslib "^2.0.3" node-abi@^3.3.0: - version "3.45.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.45.0.tgz#f568f163a3bfca5aacfce1fbeee1fa2cc98441f5" - integrity sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ== + version "3.47.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.47.0.tgz#6cbfa2916805ae25c2b7156ca640131632eb05e8" + integrity sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A== dependencies: semver "^7.3.5" @@ -6504,20 +6721,25 @@ node-addon-api@^6.1.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== +node-addon-api@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.0.0.tgz#8136add2f510997b3b94814f4af1cce0b0e3962e" + integrity sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA== + node-domexception@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch-native@^1.1.1, node-fetch-native@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.2.0.tgz#13ec6df98f33168958dbfb6945f10aedf42e7ea8" - integrity sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ== +node-fetch-native@^1.2.0, node-fetch-native@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.4.0.tgz#fbe8ac033cb6aa44bd106b5e4fd2b6277ba70fa1" + integrity sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA== node-fetch@^2.0.0, node-fetch@^2.6.6: - version "2.6.12" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" - integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" @@ -6649,22 +6871,22 @@ object.entries@^1.1.6: es-abstract "^1.20.4" object.fromentries@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz" - integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -object.groupby@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.0.tgz#cb29259cf90f37e7bac6437686c1ea8c916d12a9" - integrity sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw== + version "2.0.7" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" + integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== dependencies: call-bind "^1.0.2" define-properties "^1.2.0" - es-abstract "^1.21.2" + es-abstract "^1.22.1" + +object.groupby@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee" + integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" get-intrinsic "^1.2.1" object.hasown@^1.1.2: @@ -6676,22 +6898,22 @@ object.hasown@^1.1.2: es-abstract "^1.20.4" object.values@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" - integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" + integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" ofetch@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.1.1.tgz#a0e5117500f4ac02e2c61ec1bb754bc54d5ba44d" - integrity sha512-SSMoktrp9SNLi20BWfB/BnnKcL0RDigXThD/mZBeQxkIRv1xrd9183MtLdsqRYLYSqW0eTr5t8w8MqjNhvoOQQ== + version "1.3.3" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.3.3.tgz#588cb806a28e5c66c2c47dd8994f9059a036d8c0" + integrity sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg== dependencies: - destr "^2.0.0" - node-fetch-native "^1.2.0" - ufo "^1.1.2" + destr "^2.0.1" + node-fetch-native "^1.4.0" + ufo "^1.3.0" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -7402,9 +7624,9 @@ postcss-resolve-nested-selector@^0.1.1: integrity sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw== postcss-rtlcss@^4.0.1: - version "4.0.6" - resolved "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-4.0.6.tgz" - integrity sha512-YNm6g2Y7Gngqtrpq3GC7cUkzH5Gq7aB+Lw9MSgF9s2ro1BDY7W4zqnd15g2ueatUUpSTg2/F5KDjQoTdjhbAKg== + version "4.0.8" + resolved "https://registry.yarnpkg.com/postcss-rtlcss/-/postcss-rtlcss-4.0.8.tgz#d5e2923dcee7bec54e9ab0db7357b0bc3f6a8ae3" + integrity sha512-CR2sY889PHnX6K8rjW9FG4Qvm9UJsIekDakMtEYGH3zgFp9XADMeaKcA0hPOmkClNh0jWbkaPBm0jZ6fHmqkJQ== dependencies: rtlcss "4.1.0" @@ -7474,10 +7696,10 @@ postcss@^8.1.10: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.21, postcss@^8.4.25: - version "8.4.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" - integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== +postcss@^8.4.21, postcss@^8.4.27: + version "8.4.30" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.30.tgz#0e0648d551a606ef2192a26da4cabafcc09c1aa7" + integrity sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" @@ -7523,12 +7745,12 @@ pretty-bytes@^5.6.0: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== -prisma@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.2.0.tgz#a302dc2635cdec1d22d552ece837fb29a03563b9" - integrity sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ== +prisma@5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.3.1.tgz#a0932c1c1a5ed4ff449d064b193d9c7e94e8bf77" + integrity sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A== dependencies: - "@prisma/engines" "5.2.0" + "@prisma/engines" "5.3.1" promise.series@^0.2.0: version "0.2.0" @@ -7615,10 +7837,10 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -radix3@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.0.1.tgz#de0ac16234f8a63288645854a54fc26e45a4a8eb" - integrity sha512-y+AcwZ3HcUIGc9zGsNVf5+BY/LxL+z+4h4J3/pp8jxSmy1STaCocPS3qrj4tA5ehUSzqtqK+0Aygvz/r/8vy4g== +radix3@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.1.0.tgz#9745df67a49c522e94a33d0a93cf743f104b6e0d" + integrity sha512-pNsHDxbGORSvuSScqNJ+3Km6QAVqk8CfsCBIEoDgpqLrkD2f3QM4I7d1ozJJ172OmIcoUcerZaNWqtLkRXTV3A== raf-schd@^4.0.2: version "4.0.3" @@ -7642,10 +7864,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.98.0: - version "0.98.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.98.0.tgz#b207bedbd9dac749d28ea6de2197a0efe648b78c" - integrity sha512-ebUigu+s6Iusq14EZTFTTUzdDPYFQEZjeD4feeq3o7dE+ndOVnajEdQ2va/x6CsRBUsWgjLJipfQi0XIrxYupA== +react-basics@^0.100.0: + version "0.100.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.100.0.tgz#14a36769af89f3e01641997f897e4073f16f5035" + integrity sha512-ET6DX/FYAcjGRauBE4jwqwVpd/hKmA2Nu/fi1dakwsv17hkyV5FEAhdWhQAxJX3VnaCH//QysN8+ae12KuNA9g== dependencies: classnames "^2.3.1" date-fns "^2.29.3" @@ -7675,9 +7897,9 @@ react-dom@^18.2.0: scheduler "^0.23.0" react-error-boundary@^4.0.4: - version "4.0.10" - resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.10.tgz" - integrity sha512-pvVKdi77j2OoPHo+p3rorgE43OjDWiqFkaqkJz8sJKK6uf/u8xtzuaVfj5qJ2JnDLIgF1De3zY5AJDijp+LVPA== + version "4.0.11" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c" + integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw== dependencies: "@babel/runtime" "^7.12.5" @@ -7926,9 +8148,14 @@ regenerate@^1.4.0, regenerate@^1.4.2: regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.11: version "0.13.11" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz" @@ -7941,7 +8168,7 @@ regexp-tree@^0.1.24: resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== -regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: +regexp.prototype.flags@^1.4.3: version "1.5.0" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== @@ -7950,6 +8177,15 @@ regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: define-properties "^1.2.0" functions-have-names "^1.2.3" +regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + regexpu-core@4.5.4: version "4.5.4" resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz" @@ -8056,7 +8292,7 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.0, resolve@^1.22.1: +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.0: version "1.22.2" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz" integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== @@ -8065,12 +8301,12 @@ resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.22.3: - version "1.22.3" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.3.tgz#4b4055349ffb962600972da1fdc33c46a4eb3283" - integrity sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw== +resolve@^1.22.1, resolve@^1.22.4: + version "1.22.6" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362" + integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw== dependencies: - is-core-module "^2.12.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -8109,9 +8345,9 @@ rimraf@^3.0.0, rimraf@^3.0.2: glob "^7.1.3" rollup-plugin-copy@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz#f1228a3ffb66ffad8606e2f3fb7ff23141ed3286" - integrity sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ== + version "3.5.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz#7ffa2a7a8303e143876fa64fb5eed9022d304eeb" + integrity sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA== dependencies: "@types/fs-extra" "^8.0.1" colorette "^1.1.0" @@ -8188,15 +8424,15 @@ rollup-pluginutils@^2.8.2: estree-walker "^0.6.1" rollup@^3.28.0: - version "3.28.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.28.0.tgz#a3c70004b01934760c0cb8df717c7a1d932389a2" - integrity sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw== + version "3.29.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.2.tgz#cbc76cd5b03b9f9e93be991d23a1dff9c6d5b740" + integrity sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A== optionalDependencies: fsevents "~2.3.2" rtlcss@4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-4.1.0.tgz#f69b78d9752c970ddfe2aa590896f44353ab1c98" integrity sha512-W+N4hh0nVqVrrn3mRkHakxpB+c9cQ4CRT67O39kgA+1DjyhrdsqyCqIuHXyvWaXn4/835n+oX3fYJCi4+G/06A== dependencies: escalade "^3.1.1" @@ -8218,13 +8454,13 @@ rxjs@^7.5.1: dependencies: tslib "^2.1.0" -safe-array-concat@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" - integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== +safe-array-concat@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== dependencies: call-bind "^1.0.2" - get-intrinsic "^1.2.0" + get-intrinsic "^1.2.1" has-symbols "^1.0.3" isarray "^2.0.5" @@ -8319,10 +8555,19 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" -sharp@^0.32.1: - version "0.32.4" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.4.tgz#0354653b7924f2520b2264ac9bcd10a58bf411b6" - integrity sha512-exUnZewqVZC6UXqXuQ8fyJJv0M968feBi04jb9GcUHrWtkRoAKnbJt8IfwT4NJs7FskArbJ14JAFGVuooszoGg== +set-function-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + +sharp@^0.32.5: + version "0.32.6" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.6.tgz#6ad30c0b7cd910df65d5f355f774aa4fce45732a" + integrity sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w== dependencies: color "^4.2.3" detect-libc "^2.0.2" @@ -8377,9 +8622,9 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== signal-exit@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" - integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== simple-concat@^1.0.0: version "1.0.1" @@ -8495,9 +8740,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.13" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz#7189a474c46f8d47c7b0da4b987bb45e908bd2d5" - integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w== + version "3.0.15" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz#142460aabaca062bc7cd4cc87b7d50725ed6a4ba" + integrity sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ== sprintf-js@~1.0.2: version "1.0.3" @@ -8529,6 +8774,11 @@ standard-as-callback@^2.1.0: resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== +std-env@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" + integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== + stream2asynciter@1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/stream2asynciter/-/stream2asynciter-1.0.3.tgz" @@ -8540,9 +8790,9 @@ streamsearch@^1.1.0: integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== streamx@^2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.0.tgz#f58c92e6f726b5390dcabd6dd9094d29a854d698" - integrity sha512-HcxY6ncGjjklGs1xsP1aR71INYcsXFJet5CU1CHqihQ2J5nOsbd4OjgjHO42w/4QNv9gZb3BueV+Vxok5pLEXg== + version "2.15.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.1.tgz#396ad286d8bc3eeef8f5cea3f029e81237c024c6" + integrity sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA== dependencies: fast-fifo "^1.1.0" queue-tick "^1.0.1" @@ -8589,32 +8839,32 @@ string.prototype.padend@^3.0.0: define-properties "^1.1.3" es-abstract "^1.19.1" -string.prototype.trim@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" - integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" -string.prototype.trimend@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" - integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== +string.prototype.trimend@^1.0.6, string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" -string.prototype.trimstart@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" - integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== +string.prototype.trimstart@^1.0.6, string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" string_decoder@^1.1.1: version "1.3.0" @@ -8731,13 +8981,13 @@ stylelint-config-recommended@^9.0.0: postcss-value-parser "^4.2.0" stylelint@^15.10.1: - version "15.10.2" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.2.tgz#0ee5a8371d3a2e1ff27fefd48309d3ddef7c3405" - integrity sha512-UxqSb3hB74g4DTO45QhUHkJMjKKU//lNUAOWyvPBVPZbCknJ5HjOWWZo+UDuhHa9FLeVdHBZXxu43eXkjyIPWg== + version "15.10.3" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.3.tgz#995e4512fdad450fb83e13f3472001f6edb6469c" + integrity sha512-aBQMMxYvFzJJwkmg+BUUg3YfPyeuCuKo2f+LOw7yYbU8AZMblibwzp9OV4srHVeQldxvSFdz0/Xu8blq2AesiA== dependencies: - "@csstools/css-parser-algorithms" "^2.3.0" - "@csstools/css-tokenizer" "^2.1.1" - "@csstools/media-query-list-parser" "^2.1.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/media-query-list-parser" "^2.1.4" "@csstools/selector-specificity" "^3.0.0" balanced-match "^2.0.0" colord "^2.9.3" @@ -8745,7 +8995,7 @@ stylelint@^15.10.1: css-functions-list "^3.2.0" css-tree "^2.3.1" debug "^4.3.4" - fast-glob "^3.3.0" + fast-glob "^3.3.1" fastest-levenshtein "^1.0.16" file-entry-cache "^6.0.1" global-modules "^2.0.0" @@ -8756,13 +9006,13 @@ stylelint@^15.10.1: import-lazy "^4.0.0" imurmurhash "^0.1.4" is-plain-object "^5.0.0" - known-css-properties "^0.27.0" + known-css-properties "^0.28.0" mathml-tag-names "^2.1.3" meow "^10.1.5" micromatch "^4.0.5" normalize-path "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.25" + postcss "^8.4.27" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^6.0.0" postcss-selector-parser "^6.0.13" @@ -8896,9 +9146,9 @@ tar-stream@^3.1.5: streamx "^2.15.0" tar@^6.1.2: - version "6.1.15" - resolved "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz" - integrity sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A== + version "6.2.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73" + integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -9167,14 +9417,14 @@ typescript@^4.0, typescript@^4.5: integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== typescript@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" - integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== -ufo@^1.0.0, ufo@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.2.tgz#d0d9e0fa09dece0c31ffd57bd363f030a35cfe76" - integrity sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ== +ufo@^1.0.0, ufo@^1.2.0, ufo@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.0.tgz#c92f8ac209daff607c57bbd75029e190930a0019" + integrity sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw== unbox-primitive@^1.0.2: version "1.0.2" @@ -9191,6 +9441,17 @@ uncrypto@^0.1.3: resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== +unenv@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.7.4.tgz#a0e5a78de2c7c3c4563c06ba9763c96c59db3333" + integrity sha512-fjYsXYi30It0YCQYqLOcT6fHfMXsBr2hw9XC7ycf8rTG7Xxpe3ZssiqUnD0khrjiZEmkBXWLwm42yCSCH46fMw== + dependencies: + consola "^3.2.3" + defu "^6.1.2" + mime "^3.0.0" + node-fetch-native "^1.4.0" + pathe "^1.1.1" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz" @@ -9253,21 +9514,30 @@ universalify@^2.0.0: integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== unstorage@^1.0.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.8.0.tgz#fa90a5a82c35183257acc3f0461fd982f42dfc9a" - integrity sha512-Wl6a0fYIIPx8yWIHAVNzsNRcIpagVnBV05UXeIFCNqPZ5tu0w0MPE+eTjpRe/yxCD60K7qX55K5Px/PeKvNntw== + version "1.9.0" + resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.9.0.tgz#0c1977f4e769a48344339ac97ec3f2feea94d43d" + integrity sha512-VpD8ZEYc/le8DZCrny3bnqKE4ZjioQxBRnWE+j5sGNvziPjeDlaS1NaFFHzl/kkXaO3r7UaF8MGQrs14+1B4pQ== dependencies: anymatch "^3.1.3" chokidar "^3.5.3" - destr "^2.0.0" + destr "^2.0.1" h3 "^1.7.1" ioredis "^5.3.2" - listhen "^1.0.4" + listhen "^1.2.2" lru-cache "^10.0.0" mri "^1.2.0" node-fetch-native "^1.2.0" ofetch "^1.1.1" - ufo "^1.1.2" + ufo "^1.2.0" + +untun@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/untun/-/untun-0.1.2.tgz#fa42a62ae24c1c5c6f3209692a2b0e1f573f1353" + integrity sha512-wLAMWvxfqyTiBODA1lg3IXHQtjggYLeTK7RnSfqtOXixWJ3bAa2kK/HHmOOg19upteqO3muLvN6O/icbyQY33Q== + dependencies: + citty "^0.1.3" + consola "^3.2.3" + pathe "^1.1.1" update-browserslist-db@^1.0.11: version "1.0.11" @@ -9277,6 +9547,11 @@ update-browserslist-db@^1.0.11: escalade "^3.1.1" picocolors "^1.0.0" +uqr@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/uqr/-/uqr-0.1.2.tgz#5c6cd5dcff9581f9bb35b982cb89e2c483a41d7d" + integrity sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -9284,6 +9559,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +urlpattern-polyfill@8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz#99f096e35eff8bf4b5a2aa7d58a1523d6ebc7ce5" + integrity sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ== + use-memo-one@^1.1.1: version "1.1.3" resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz" @@ -9305,9 +9585,9 @@ uuid@3.4.0, uuid@^3.3.2: integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== uuid@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== v8-compile-cache-lib@^3.0.1: version "3.0.1" @@ -9384,7 +9664,7 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" -which-typed-array@^1.1.10, which-typed-array@^1.1.11, which-typed-array@^1.1.9: +which-typed-array@^1.1.11, which-typed-array@^1.1.9: version "1.1.11" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== @@ -9527,8 +9807,8 @@ zod@3.21.4: integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== zustand@^4.3.8: - version "4.3.9" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.9.tgz#a7d4332bbd75dfd25c6848180b3df1407217f2ad" - integrity sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw== + version "4.4.1" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.1.tgz#0cd3a3e4756f21811bd956418fdc686877e8b3b0" + integrity sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw== dependencies: use-sync-external-store "1.2.0" From 55487ca72528aad931facd68f09e6525af00351b Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 21 Sep 2023 13:51:16 -0700 Subject: [PATCH 194/357] Fix search on settings table from dissappearing --- src/components/common/SettingsTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/common/SettingsTable.js b/src/components/common/SettingsTable.js index a74581cae..701dbe13b 100644 --- a/src/components/common/SettingsTable.js +++ b/src/components/common/SettingsTable.js @@ -36,7 +36,7 @@ export function SettingsTable({ return ( <> - {showSearch && !!value.length && ( + {showSearch && (value.length > 0 || filterValue) && ( Date: Thu, 21 Sep 2023 16:30:15 -0700 Subject: [PATCH 195/357] Update redis package. --- package.json | 2 +- .../pages/settings/websites/TrackingCode.js | 6 ++-- src/lib/cache.ts | 28 +++++++++---------- src/lib/middleware.ts | 2 +- src/pages/api/auth/login.ts | 5 ++-- src/pages/api/auth/logout.ts | 2 +- src/pages/api/auth/sso.ts | 2 +- yarn.lock | 8 +++--- 8 files changed, 26 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 8350a060f..6617b4557 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@prisma/client": "5.3.1", "@tanstack/react-query": "^4.33.0", "@umami/prisma-client": "^0.2.0", - "@umami/redis-client": "^0.5.0", + "@umami/redis-client": "^0.15.0", "chalk": "^4.1.1", "chart.js": "^4.2.1", "chartjs-adapter-date-fns": "^3.0.0", diff --git a/src/components/pages/settings/websites/TrackingCode.js b/src/components/pages/settings/websites/TrackingCode.js index a666476d9..03dd6a1d8 100644 --- a/src/components/pages/settings/websites/TrackingCode.js +++ b/src/components/pages/settings/websites/TrackingCode.js @@ -3,7 +3,7 @@ import useMessages from 'components/hooks/useMessages'; import useConfig from 'components/hooks/useConfig'; import { useRouter } from 'next/router'; -export function TrackingCode({ websiteId, analyticsUrl }) { +export function TrackingCode({ websiteId, baseUrl }) { const { formatMessage, messages } = useMessages(); const { basePath } = useRouter(); const config = useConfig(); @@ -13,9 +13,7 @@ export function TrackingCode({ websiteId, analyticsUrl }) { const url = trackerScriptName?.startsWith('http') ? trackerScriptName - : `${ - analyticsUrl || process.env.analyticsUrl || location.origin - }${basePath}/${trackerScriptName}`; + : `${baseUrl || location.origin}${basePath}/${trackerScriptName}`; const code = ``; diff --git a/src/lib/cache.ts b/src/lib/cache.ts index c54eda2ee..a2e4ea5a9 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -2,60 +2,58 @@ import { User, Website } from '@prisma/client'; import redis from '@umami/redis-client'; import { getSession, getUserById, getWebsiteById } from '../queries'; -const { fetchObject, storeObject, deleteObject, expire } = redis; - async function fetchWebsite(id): Promise { - return fetchObject(`website:${id}`, () => getWebsiteById(id), 86400); + return redis.fetchObject(`website:${id}`, () => getWebsiteById(id), 86400); } async function storeWebsite(data) { const { id } = data; const key = `website:${id}`; - const obj = await storeObject(key, data); - await expire(key, 86400); + const obj = await redis.storeObject(key, data); + await redis.expire(key, 86400); return obj; } async function deleteWebsite(id) { - return deleteObject(`website:${id}`); + return redis.deleteObject(`website:${id}`); } async function fetchUser(id): Promise { - return fetchObject(`user:${id}`, () => getUserById(id, { includePassword: true }), 86400); + return redis.fetchObject(`user:${id}`, () => getUserById(id, { includePassword: true }), 86400); } async function storeUser(data) { const { id } = data; const key = `user:${id}`; - const obj = await storeObject(key, data); - await expire(key, 86400); + const obj = await redis.storeObject(key, data); + await redis.expire(key, 86400); return obj; } async function deleteUser(id) { - return deleteObject(`user:${id}`); + return redis.deleteObject(`user:${id}`); } async function fetchSession(id) { - return fetchObject(`session:${id}`, () => getSession(id), 86400); + return redis.fetchObject(`session:${id}`, () => getSession(id), 86400); } async function storeSession(data) { const { id } = data; const key = `session:${id}`; - const obj = await storeObject(key, data); - await expire(key, 86400); + const obj = await redis.storeObject(key, data); + await redis.expire(key, 86400); return obj; } async function deleteSession(id) { - return deleteObject(`session:${id}`); + return redis.deleteObject(`session:${id}`); } async function fetchUserBlock(userId: string) { @@ -80,5 +78,5 @@ export default { deleteSession, fetchUserBlock, incrementUserBlock, - enabled: redis.enabled, + enabled: !!redis, }; diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 8259677dc..4be958b6b 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -55,7 +55,7 @@ export const useAuth = createMiddleware(async (req, res, next) => { if (isUuid(userId)) { user = await getUserById(userId); - } else if (redis.enabled && authKey) { + } else if (redis && authKey) { user = await redis.get(authKey); } diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts index 47521084b..74661e33e 100644 --- a/src/pages/api/auth/login.ts +++ b/src/pages/api/auth/login.ts @@ -52,17 +52,18 @@ export default async ( const user = await getUserByUsername(username, { includePassword: true }); if (user && checkPassword(password, user.password)) { - if (redis.enabled) { + if (redis) { const token = await setAuthKey(user); return ok(res, { token, user }); } const token = createSecureToken({ userId: user.id }, secret()); + const { id, username, role, createdAt } = user; return ok(res, { token, - user: { id: user.id, username: user.username, role: user.role, createdAt: user.createdAt }, + user: { id, username, role, createdAt }, }); } diff --git a/src/pages/api/auth/logout.ts b/src/pages/api/auth/logout.ts index cbccce8d7..e6222e491 100644 --- a/src/pages/api/auth/logout.ts +++ b/src/pages/api/auth/logout.ts @@ -8,7 +8,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { await useAuth(req, res); if (req.method === 'POST') { - if (redis.enabled) { + if (redis) { await redis.del(getAuthToken(req)); } diff --git a/src/pages/api/auth/sso.ts b/src/pages/api/auth/sso.ts index 66cbd8e27..a7992666e 100644 --- a/src/pages/api/auth/sso.ts +++ b/src/pages/api/auth/sso.ts @@ -8,7 +8,7 @@ import { setAuthKey } from 'lib/auth'; export default async (req: NextApiRequestAuth, res: NextApiResponse) => { await useAuth(req, res); - if (redis.enabled && req.auth.user) { + if (redis && req.auth.user) { const token = await setAuthKey(req.auth.user, 86400); return ok(res, { user: req.auth.user, token }); diff --git a/yarn.lock b/yarn.lock index 8687c4326..ecb1a7ebe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2766,10 +2766,10 @@ dependencies: debug "^4.3.4" -"@umami/redis-client@^0.5.0": - version "0.5.0" - resolved "https://registry.npmjs.org/@umami/redis-client/-/redis-client-0.5.0.tgz" - integrity sha512-x7wx/pMjyg3AAYzgjGOw031bNhyZ81h6tRMAl60RQQI9xlJaJEA1r0TEUrWfFi21gHAvdBLJGYCsvHzpix4LKQ== +"@umami/redis-client@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@umami/redis-client/-/redis-client-0.15.0.tgz#55e9c4ede28fdd3b6a169378d391a5d2cc039e51" + integrity sha512-+Ei6i4qx9Md4o92Mlzvh9rTgkfllgmSwFu1687DEqFnNrHd+KNVxgNNDiyyCwzfC0t/DAaq7PoOFw4NjJYo9wQ== dependencies: debug "^4.3.4" redis "^4.5.1" From a3bba63b09fff35128e43752a27a483f48ef6a48 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 21 Sep 2023 16:45:46 -0700 Subject: [PATCH 196/357] update ignore. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 32e3ef0b8..050397c90 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,8 @@ yarn-debug.log* yarn-error.log* # local env files -*.env +.env +.env.* *.dev.yml From a3a24e76c94ac4028265cce1289484dc126265ab Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 21 Sep 2023 17:30:40 -0700 Subject: [PATCH 197/357] Pass analyticsUrl prop. --- src/components/pages/settings/websites/ShareUrl.js | 7 ++----- src/components/pages/settings/websites/TrackingCode.js | 4 ++-- .../pages/settings/websites/WebsiteSettings.js | 9 ++++++++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/pages/settings/websites/ShareUrl.js b/src/components/pages/settings/websites/ShareUrl.js index a7b7fc2f2..f4569ca38 100644 --- a/src/components/pages/settings/websites/ShareUrl.js +++ b/src/components/pages/settings/websites/ShareUrl.js @@ -16,7 +16,7 @@ import useMessages from 'components/hooks/useMessages'; const generateId = () => getRandomChars(16); -export function ShareUrl({ websiteId, data, onSave }) { +export function ShareUrl({ websiteId, data, analyticsUrl, onSave }) { const { formatMessage, labels, messages } = useMessages(); const { name, shareId } = data; const [id, setId] = useState(shareId); @@ -27,10 +27,7 @@ export function ShareUrl({ websiteId, data, onSave }) { ); const ref = useRef(null); const url = useMemo( - () => - `${process.env.analyticsUrl || location.origin}${basePath}/share/${id}/${encodeURIComponent( - name, - )}`, + () => `${analyticsUrl || location.origin}${basePath}/share/${id}/${encodeURIComponent(name)}`, [id, name, basePath], ); diff --git a/src/components/pages/settings/websites/TrackingCode.js b/src/components/pages/settings/websites/TrackingCode.js index 03dd6a1d8..298cd17aa 100644 --- a/src/components/pages/settings/websites/TrackingCode.js +++ b/src/components/pages/settings/websites/TrackingCode.js @@ -3,7 +3,7 @@ import useMessages from 'components/hooks/useMessages'; import useConfig from 'components/hooks/useConfig'; import { useRouter } from 'next/router'; -export function TrackingCode({ websiteId, baseUrl }) { +export function TrackingCode({ websiteId, analyticsUrl }) { const { formatMessage, messages } = useMessages(); const { basePath } = useRouter(); const config = useConfig(); @@ -13,7 +13,7 @@ export function TrackingCode({ websiteId, baseUrl }) { const url = trackerScriptName?.startsWith('http') ? trackerScriptName - : `${baseUrl || location.origin}${basePath}/${trackerScriptName}`; + : `${analyticsUrl || location.origin}${basePath}/${trackerScriptName}`; const code = ``; diff --git a/src/components/pages/settings/websites/WebsiteSettings.js b/src/components/pages/settings/websites/WebsiteSettings.js index 3cd4185c9..63e898148 100644 --- a/src/components/pages/settings/websites/WebsiteSettings.js +++ b/src/components/pages/settings/websites/WebsiteSettings.js @@ -69,7 +69,14 @@ export function WebsiteSettings({ websiteId, openExternal = false, analyticsUrl )} {tab === 'tracking' && } - {tab === 'share' && } + {tab === 'share' && ( + + )} {tab === 'data' && } ); From 424f3e5323240e13e3705e3783452f8dc9580ccc Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 21 Sep 2023 18:37:40 -0700 Subject: [PATCH 198/357] Fixed login redirect. --- package.components.json | 2 +- src/components/hooks/useRequireLogin.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.components.json b/package.components.json index feb3fc2e6..41e72579f 100644 --- a/package.components.json +++ b/package.components.json @@ -1,6 +1,6 @@ { "name": "@umami/components", - "version": "0.11.0", + "version": "0.1.0", "description": "Umami React components.", "author": "Mike Cao ", "license": "MIT", diff --git a/src/components/hooks/useRequireLogin.ts b/src/components/hooks/useRequireLogin.ts index 950bb60ac..d2f540d45 100644 --- a/src/components/hooks/useRequireLogin.ts +++ b/src/components/hooks/useRequireLogin.ts @@ -4,7 +4,7 @@ import useApi from 'components/hooks/useApi'; import useUser from 'components/hooks/useUser'; export function useRequireLogin(handler: (data?: object) => void) { - const router = useRouter(); + const { basePath } = useRouter(); const { get } = useApi(); const { user, setUser } = useUser(); @@ -15,7 +15,7 @@ export function useRequireLogin(handler: (data?: object) => void) { setUser(typeof handler === 'function' ? handler(data) : (data as any)?.user); } catch { - await router.push('/login'); + location.href = `${basePath}/login`; } } From 6846355c6355687d9dff1b8e4437ececb797554a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 22 Sep 2023 00:59:00 -0700 Subject: [PATCH 199/357] DataTable refactor. --- src/components/common/DataTable.js | 6 +-- .../pages/settings/websites/WebsitesList.js | 20 ++++---- .../pages/settings/websites/WebsitesTable.js | 5 +- src/lib/schema.ts | 13 +++++ src/lib/types.ts | 8 ++-- src/lib/yup.ts | 19 -------- src/pages/api/me/teams.ts | 4 +- src/pages/api/me/websites.ts | 4 +- src/pages/api/reports/index.ts | 4 +- src/pages/api/teams/[id]/websites/index.ts | 7 ++- src/pages/api/teams/index.ts | 9 ++-- src/pages/api/users/[id]/teams.ts | 13 ++--- src/pages/api/users/[id]/websites.ts | 8 ++-- src/pages/api/users/index.ts | 4 +- src/pages/api/websites/index.ts | 4 +- src/queries/admin/team.ts | 33 ++++++------- src/queries/admin/website.ts | 47 +++++++------------ 17 files changed, 94 insertions(+), 114 deletions(-) create mode 100644 src/lib/schema.ts delete mode 100644 src/lib/yup.ts diff --git a/src/components/common/DataTable.js b/src/components/common/DataTable.js index cb7393445..2662fa2c4 100644 --- a/src/components/common/DataTable.js +++ b/src/components/common/DataTable.js @@ -25,13 +25,13 @@ export function DataTable({ const { page, pageSize, count } = pageInfo || {}; const noResults = Boolean(query && data?.length === 0); - const handleChange = () => { - onChange?.({ query, page }); + const handleChange = value => { + onChange?.({ query: value, page }); }; const handleSearch = value => { setQuery(value); - handleChange(); + handleChange(value); }; const handlePageChange = page => { diff --git a/src/components/pages/settings/websites/WebsitesList.js b/src/components/pages/settings/websites/WebsitesList.js index 70bbbd92d..4761ad0a0 100644 --- a/src/components/pages/settings/websites/WebsitesList.js +++ b/src/components/pages/settings/websites/WebsitesList.js @@ -7,7 +7,7 @@ import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; import { ROLES } from 'lib/constants'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; export function WebsitesList({ showTeam, @@ -20,16 +20,20 @@ export function WebsitesList({ const { user } = useUser(); const [params, setParams] = useState({}); const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery( - ['websites', includeTeams, onlyTeams], - () => - get(`/users/${user?.id}/websites`, { + const count = useRef(0); + const q = useQuery( + ['websites', includeTeams, onlyTeams, params], + () => { + count.current += 1; + return get(`/users/${user?.id}/websites`, { includeTeams, onlyTeams, ...params, - }), + }); + }, { enabled: !!user }, ); + const { data, refetch, isLoading, error } = q; const { showToast } = useToasts(); const handleChange = params => { @@ -60,10 +64,10 @@ export function WebsitesList({ ); return ( - + {showHeader && {addButton}} {showTable && ( - + {showTeam && ( diff --git a/src/lib/schema.ts b/src/lib/schema.ts new file mode 100644 index 000000000..739128b37 --- /dev/null +++ b/src/lib/schema.ts @@ -0,0 +1,13 @@ +import * as yup from 'yup'; + +export const dateRange = { + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), +}; + +export const pageInfo = { + query: yup.string(), + page: yup.number().integer().positive(), + pageSize: yup.number().integer().positive().max(200), + orderBy: yup.string(), +}; diff --git a/src/lib/types.ts b/src/lib/types.ts index 3685753e9..58e6aa9e6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -54,11 +54,11 @@ export interface ReportSearchFilter extends SearchFilter } export interface SearchFilter { - filter?: string; - filterType?: T; - pageSize: number; - page: number; + query?: string; + page?: number; + pageSize?: number; orderBy?: string; + data?: T; } export interface FilterResult { diff --git a/src/lib/yup.ts b/src/lib/yup.ts deleted file mode 100644 index a9d210285..000000000 --- a/src/lib/yup.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as yup from 'yup'; - -export function getDateRangeValidation() { - return { - startAt: yup.number().integer().required(), - endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - }; -} - -// ex: /funnel|insights|retention/i -export function getFilterValidation(matchRegex) { - return { - filter: yup.string(), - filterType: yup.string().matches(matchRegex), - pageSize: yup.number().integer().positive().max(200), - page: yup.number().integer().positive(), - orderBy: yup.string(), - }; -} diff --git a/src/pages/api/me/teams.ts b/src/pages/api/me/teams.ts index d394ef07d..131cb2621 100644 --- a/src/pages/api/me/teams.ts +++ b/src/pages/api/me/teams.ts @@ -1,6 +1,6 @@ import { useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userTeams from 'pages/api/users/[id]/teams'; @@ -12,7 +12,7 @@ export interface MyTeamsRequestQuery extends SearchFilter const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Name|Owner/i), + ...pageInfo, }), }; diff --git a/src/pages/api/me/websites.ts b/src/pages/api/me/websites.ts index d4a803a0d..749af3169 100644 --- a/src/pages/api/me/websites.ts +++ b/src/pages/api/me/websites.ts @@ -1,6 +1,6 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userWebsites from 'pages/api/users/[id]/websites'; @@ -12,7 +12,7 @@ export interface MyWebsitesRequestQuery extends SearchFilter const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Name|Owner/i), + ...pageInfo, }), POST: yup.object().shape({ name: yup.string().max(50).required(), @@ -39,12 +39,11 @@ export default async ( } = req.auth; if (req.method === 'GET') { - const { page, filter, pageSize } = req.query; + const { page, query } = req.query; const results = await getTeamsByUserId(userId, { page, - filter, - pageSize: +pageSize || undefined, + query, }); return ok(res, results); diff --git a/src/pages/api/users/[id]/teams.ts b/src/pages/api/users/[id]/teams.ts index 72b99b869..34a31a0e2 100644 --- a/src/pages/api/users/[id]/teams.ts +++ b/src/pages/api/users/[id]/teams.ts @@ -1,10 +1,11 @@ +import * as yup from 'yup'; import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getTeamsByUserId } from 'queries'; -import * as yup from 'yup'; + export interface UserTeamsRequestQuery extends SearchFilter { id: string; } @@ -18,7 +19,7 @@ export interface UserTeamsRequestBody { const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), - ...getFilterValidation('/All|Name|Owner/i'), + ...pageInfo, }), }; @@ -40,12 +41,12 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; + const { page, query, pageSize } = req.query; const teams = await getTeamsByUserId(userId, { + query, page, - filter, - pageSize: +pageSize || undefined, + pageSize, }); return ok(res, teams); diff --git a/src/pages/api/users/[id]/websites.ts b/src/pages/api/users/[id]/websites.ts index ab7d88ef6..cc264e7dd 100644 --- a/src/pages/api/users/[id]/websites.ts +++ b/src/pages/api/users/[id]/websites.ts @@ -1,6 +1,6 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsitesByUserId } from 'queries'; @@ -17,7 +17,7 @@ const schema = { id: yup.string().uuid().required(), includeTeams: yup.boolean(), onlyTeams: yup.boolean(), - ...getFilterValidation(/All|Name|Domain/i), + ...pageInfo, }), }; @@ -32,7 +32,7 @@ export default async ( await useValidate(req, res); const { user } = req.auth; - const { id: userId, page, filter, pageSize, includeTeams, onlyTeams } = req.query; + const { id: userId, page, pageSize, query, includeTeams, onlyTeams } = req.query; if (req.method === 'GET') { if (!user.isAdmin && user.id !== userId) { @@ -40,8 +40,8 @@ export default async ( } const websites = await getWebsitesByUserId(userId, { + query, page, - filter, pageSize: +pageSize || undefined, includeTeams, onlyTeams, diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts index 991986e87..d37add2fe 100644 --- a/src/pages/api/users/index.ts +++ b/src/pages/api/users/index.ts @@ -3,7 +3,7 @@ import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUserByUsername, getUsers } from 'queries'; @@ -19,7 +19,7 @@ export interface UsersRequestBody { import * as yup from 'yup'; const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Username/i), + ...pageInfo, }), POST: yup.object().shape({ username: yup.string().max(255).required(), diff --git a/src/pages/api/websites/index.ts b/src/pages/api/websites/index.ts index d6009caf6..a90f8e46d 100644 --- a/src/pages/api/websites/index.ts +++ b/src/pages/api/websites/index.ts @@ -7,7 +7,7 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; import userWebsites from 'pages/api/users/[id]/websites'; import * as yup from 'yup'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; export interface WebsitesRequestQuery extends SearchFilter {} @@ -19,7 +19,7 @@ export interface WebsitesRequestBody { const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Name|Domain/i), + ...pageInfo, }), POST: yup.object().shape({ name: yup.string().max(100).required(), diff --git a/src/queries/admin/team.ts b/src/queries/admin/team.ts index cf731ad42..9947b9a3b 100644 --- a/src/queries/admin/team.ts +++ b/src/queries/admin/team.ts @@ -1,5 +1,5 @@ import { Prisma, Team } from '@prisma/client'; -import { ROLES, TEAM_FILTER_TYPES } from 'lib/constants'; +import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import prisma from 'lib/prisma'; import { FilterResult, TeamSearchFilter } from 'lib/types'; @@ -82,10 +82,10 @@ export async function deleteTeam( } export async function getTeams( - TeamSearchFilter: TeamSearchFilter, + filters: TeamSearchFilter, options?: { include?: Prisma.TeamInclude }, ): Promise> { - const { userId, filter, filterType = TEAM_FILTER_TYPES.all } = TeamSearchFilter; + const { userId, query } = filters; const mode = prisma.getSearchMode(); const where: Prisma.TeamWhereInput = { @@ -94,29 +94,24 @@ export async function getTeams( some: { userId }, }, }), - ...(filter && { + ...(query && { AND: { OR: [ { - ...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES.name) && { - name: { startsWith: filter, ...mode }, - }), + name: { startsWith: query, ...mode }, }, { - ...((filterType === TEAM_FILTER_TYPES.all || - filterType === TEAM_FILTER_TYPES['user:username']) && { - teamUser: { - some: { - role: ROLES.teamOwner, - user: { - username: { - startsWith: filter, - ...mode, - }, + teamUser: { + some: { + role: ROLES.teamOwner, + user: { + username: { + startsWith: query, + ...mode, }, }, }, - }), + }, }, ], }, @@ -125,7 +120,7 @@ export async function getTeams( const [pageFilters, getParameters] = prisma.getPageFilters({ orderBy: 'name', - ...TeamSearchFilter, + ...filters, }); const teams = await prisma.client.team.findMany({ diff --git a/src/queries/admin/website.ts b/src/queries/admin/website.ts index 6417ade6f..f4444b533 100644 --- a/src/queries/admin/website.ts +++ b/src/queries/admin/website.ts @@ -1,6 +1,6 @@ import { Prisma, Website } from '@prisma/client'; import cache from 'lib/cache'; -import { ROLES, WEBSITE_FILTER_TYPES } from 'lib/constants'; +import { ROLES } from 'lib/constants'; import prisma from 'lib/prisma'; import { FilterResult, WebsiteSearchFilter } from 'lib/types'; @@ -19,17 +19,10 @@ export async function getWebsiteByShareId(shareId: string) { } export async function getWebsites( - WebsiteSearchFilter: WebsiteSearchFilter, + filters: WebsiteSearchFilter, options?: { include?: Prisma.WebsiteInclude }, ): Promise> { - const { - userId, - teamId, - includeTeams, - onlyTeams, - filter, - filterType = WEBSITE_FILTER_TYPES.all, - } = WebsiteSearchFilter; + const { userId, teamId, includeTeams, onlyTeams, query } = filters; const mode = prisma.getSearchMode(); const where: Prisma.WebsiteWhereInput = { @@ -76,27 +69,23 @@ export async function getWebsites( ], }, { - OR: [ - { - ...((filterType === WEBSITE_FILTER_TYPES.all || - filterType === WEBSITE_FILTER_TYPES.name) && { - name: { startsWith: filter, ...mode }, - }), - }, - { - ...((filterType === WEBSITE_FILTER_TYPES.all || - filterType === WEBSITE_FILTER_TYPES.domain) && { - domain: { startsWith: filter, ...mode }, - }), - }, - ], + OR: query + ? [ + { + name: { startsWith: query, ...mode }, + }, + { + domain: { startsWith: query, ...mode }, + }, + ] + : [], }, ], }; const [pageFilters, getParameters] = prisma.getPageFilters({ orderBy: 'name', - ...WebsiteSearchFilter, + ...filters, }); const websites = await prisma.client.website.findMany({ @@ -115,10 +104,10 @@ export async function getWebsites( export async function getWebsitesByUserId( userId: string, - filter?: WebsiteSearchFilter, + filters?: WebsiteSearchFilter, ): Promise> { return getWebsites( - { userId, ...filter }, + { userId, ...filters }, { include: { teamWebsite: { @@ -143,12 +132,12 @@ export async function getWebsitesByUserId( export async function getWebsitesByTeamId( teamId: string, - filter?: WebsiteSearchFilter, + filters?: WebsiteSearchFilter, ): Promise> { return getWebsites( { teamId, - ...filter, + ...filters, includeTeams: true, }, { From 2ccb8d0a3cdfc5c47e78498e803874fbafe01e78 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 22 Sep 2023 17:24:15 -0700 Subject: [PATCH 200/357] Bump version v2.7.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6617b4557..79960eb23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.6.2", + "version": "2.7.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", From ae01a50d1104681973e7682dcfa5b753826ff64b Mon Sep 17 00:00:00 2001 From: Juanga Covas <1177241+juangacovas@users.noreply.github.com> Date: Sat, 23 Sep 2023 20:25:37 +0200 Subject: [PATCH 201/357] Update es-ES.json Missing translations, improvements --- src/lang/es-ES.json | 66 ++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/lang/es-ES.json b/src/lang/es-ES.json index ef8c31a80..e2827bec5 100644 --- a/src/lang/es-ES.json +++ b/src/lang/es-ES.json @@ -16,21 +16,21 @@ "label.before": "Antes", "label.bounce-rate": "Porcentaje de rebote", "label.breakdown": "Desglose", - "label.browser": "Browser", + "label.browser": "Navegador", "label.browsers": "Navegadores", "label.cancel": "Cancelar", "label.change-password": "Cambiar contraseña", "label.cities": "Ciudades", - "label.city": "City", + "label.city": "Ciudad", "label.clear-all": "Limpiar todo", "label.confirm": "Confirmar", "label.confirm-password": "Confirmar contraseña", "label.contains": "Contiene", "label.continue": "Continuar", "label.countries": "Países", - "label.country": "Country", - "label.create": "Create", - "label.create-report": "Crear reporte", + "label.country": "País", + "label.create": "Crear", + "label.create-report": "Crear informe", "label.create-team": "Crear equipo", "label.create-user": "Crear usuario", "label.created": "Creado", @@ -38,38 +38,38 @@ "label.custom-range": "Intervalo personalizado", "label.dashboard": "Panel de control", "label.data": "Datos", - "label.date": "Date", + "label.date": "Fecha", "label.date-range": "Intervalo de fechas", - "label.day": "Day", + "label.day": "Día", "label.default-date-range": "Intervalo por defecto", "label.delete": "Eliminar", "label.delete-team": "Eliminar equipo", "label.delete-user": "Eliminar usuario", "label.delete-website": "Eliminar sitio", - "label.description": "Descripciones", + "label.description": "Descripción", "label.desktop": "Escritorio", "label.details": "Detalles", - "label.device": "Device", + "label.device": "Dispositivo", "label.devices": "Dispositivos", - "label.dismiss": "Ignorar", + "label.dismiss": "Cerrar", "label.does-not-contain": "No contiene", "label.domain": "Dominio", - "label.dropoff": "Dropoff", + "label.dropoff": "Abandono", "label.edit": "Editar", "label.edit-dashboard": "Editar panel", "label.enable-share-url": "Habilitar compartir URL", "label.event": "Evento", "label.event-data": "Datos de evento", "label.events": "Eventos", - "label.false": "False", + "label.false": "Falso", "label.field": "Campo", "label.fields": "Campos", - "label.filter": "Filter", + "label.filter": "Filtro", "label.filter-combined": "Combinado", "label.filter-raw": "En crudo", "label.filters": "Filtros", "label.funnel": "Funnel", - "label.funnel-description": "Understand the conversion and drop-off rate of users.", + "label.funnel-description": "Comprender conversión y abandono de usuarios.", "label.greater-than": "Mayor que", "label.greater-than-equals": "Mayor que o igual a", "label.insights": "Insights", @@ -77,7 +77,7 @@ "label.is": "Es igual a", "label.is-not": "No es igual a", "label.is-not-set": "Is not set", - "label.is-set": "Is set", + "label.is-set": "Está establecido", "label.join": "Unir", "label.join-team": "Unirse al equipo", "label.language": "Idioma", @@ -96,57 +96,57 @@ "label.min": "Mín", "label.mobile": "Móvil", "label.more": "Más", - "label.my-websites": "My websites", + "label.my-websites": "Mis sitios web", "label.name": "Nombre", "label.new-password": "Nueva contraseña", "label.none": "Ninguno", - "label.os": "OS", + "label.os": "Sistema", "label.overview": "Resumen", "label.owner": "Propietario", - "label.page-of": "Page {current} of {total}", + "label.page-of": "Página {current} de {total}", "label.page-views": "Vistas", - "label.pageTitle": "Page title", + "label.pageTitle": "Título de página", "label.pages": "Páginas", "label.password": "Contraseña", - "label.powered-by": "Con la ayuda de {name}", + "label.powered-by": "Analíticas de {name}", "label.profile": "Perfil", "label.queries": "Consultas", - "label.query": "Query", + "label.query": "Consulta", "label.query-parameters": "Parámetros de petición", "label.realtime": "Tiempo real", - "label.referrer": "Referrer", + "label.referrer": "Referido", "label.referrers": "Referido desde", "label.refresh": "Actualizar", "label.regenerate": "Regenerar", "label.region": "Region", "label.regions": "Regiones", "label.remove": "Quitar", - "label.reports": "Reportes", + "label.reports": "Informes", "label.required": "Obligatorio", "label.reset": "Reiniciar", - "label.reset-website": "Reiniciar estadísticas", - "label.retention": "Retention", - "label.retention-description": "Measure your website stickiness by tracking how often users return.", + "label.reset-website": "Reiniciar analíticas", + "label.retention": "Retención", + "label.retention-description": "Medir la frecuencia con la que los usuarios vuelven a tu sitio web.", "label.role": "Rol", "label.run-query": "Ejecutar consulta", "label.save": "Guardar", "label.screens": "Pantallas", - "label.search": "Search", + "label.search": "Buscar", "label.select-date": "Seleccionar fecha", "label.select-website": "Seleccionar sitio web", "label.sessions": "Sesiones", - "label.settings": "Configuraciones", + "label.settings": "Ajustes", "label.share-url": "Compartir URL", "label.single-day": "Un solo día", "label.sum": "Suma", "label.tablet": "Tableta", "label.team": "Equipo", "label.team-guest": "Invitado al equipo", - "label.team-id": "ID de equipo", + "label.team-id": "ID del equipo", "label.team-member": "Miembro del equipo", - "label.team-name": "Team name", + "label.team-name": "Nombre del equipo", "label.team-owner": "Admin. del equipo", - "label.team-websites": "Team websites", + "label.team-websites": "Sitios web del equipo", "label.teams": "Equipos", "label.theme": "Tema", "label.this-month": "Este mes", @@ -194,7 +194,7 @@ "message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.", "message.invalid-domain": "Dominio inválido", "message.min-password-length": "Longitud mínima de {n} caracteres", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.new-version-available": "Una nueva versión de Umami {version} está disponible", "message.no-data-available": "No hay información disponible.", "message.no-event-data": "No hay datos de eventos disponibles.", "message.no-match-password": "Las contraseñas no coinciden", @@ -206,7 +206,7 @@ "message.page-not-found": "Página no encontrada", "message.reset-website": "Para reiniciar este sitio web, escribe {confirmation} a continuación para confirmar.", "message.reset-website-warning": "Todas las estadísticas de esta página serán eliminadas, pero el código de rastreo permanecerá intacto.", - "message.saved": "Guardado.", + "message.saved": "Guardado", "message.share-url": "Esta es la URL pública para {target}.", "message.team-already-member": "Ya eres miembro de este equipo.", "message.team-not-found": "Equipo no encontrado.", From ce2a83a09fb10cf28d4c408b5b08ae8b0e4afc25 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 25 Sep 2023 13:19:56 -0700 Subject: [PATCH 202/357] More yup validations. --- src/lib/yup.ts | 17 +++++---- src/pages/api/reports/retention.ts | 7 ++-- src/pages/api/teams/[id]/users/[userId].ts | 1 + src/pages/api/teams/[id]/users/index.ts | 17 +++++---- src/pages/api/websites/[id]/events.ts | 6 ++-- src/pages/api/websites/[id]/index.ts | 10 +++--- src/pages/api/websites/[id]/metrics.ts | 12 +++++++ src/pages/api/websites/[id]/pageviews.ts | 31 ++++++++++------ src/pages/api/websites/[id]/reports.ts | 2 ++ src/pages/api/websites/[id]/reset.ts | 5 ++- src/pages/api/websites/[id]/stats.ts | 35 +++++++++++++------ src/queries/analytics/reports/getRetention.ts | 6 ++-- 12 files changed, 99 insertions(+), 50 deletions(-) diff --git a/src/lib/yup.ts b/src/lib/yup.ts index a9d210285..8b2eceee1 100644 --- a/src/lib/yup.ts +++ b/src/lib/yup.ts @@ -1,11 +1,10 @@ +import moment from 'moment'; import * as yup from 'yup'; -export function getDateRangeValidation() { - return { - startAt: yup.number().integer().required(), - endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - }; -} +export const DateRangeValidation = { + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), +}; // ex: /funnel|insights|retention/i export function getFilterValidation(matchRegex) { @@ -17,3 +16,9 @@ export function getFilterValidation(matchRegex) { orderBy: yup.string(), }; } + +export const TimezoneTest = yup.string().test( + 'timezone', + () => `Invalid timezone`, + value => !moment.tz.zone(value), +); diff --git a/src/pages/api/reports/retention.ts b/src/pages/api/reports/retention.ts index 4006ab128..c7a5e9af3 100644 --- a/src/pages/api/reports/retention.ts +++ b/src/pages/api/reports/retention.ts @@ -1,6 +1,7 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; +import { TimezoneTest } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRetention } from 'queries'; @@ -8,7 +9,7 @@ import * as yup from 'yup'; export interface RetentionRequestBody { websiteId: string; - dateRange: { startDate: string; endDate: string }; + dateRange: { startDate: string; endDate: string; timezone: string }; } const schema = { @@ -19,6 +20,7 @@ const schema = { .shape({ startDate: yup.date().required(), endDate: yup.date().required(), + timezone: TimezoneTest, }) .required(), }), @@ -37,7 +39,7 @@ export default async ( if (req.method === 'POST') { const { websiteId, - dateRange: { startDate, endDate }, + dateRange: { startDate, endDate, timezone }, } = req.body; if (!(await canViewWebsite(req.auth, websiteId))) { @@ -47,6 +49,7 @@ export default async ( const data = await getRetention(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), + timezone, }); return ok(res, data); diff --git a/src/pages/api/teams/[id]/users/[userId].ts b/src/pages/api/teams/[id]/users/[userId].ts index adb635d52..107aba64e 100644 --- a/src/pages/api/teams/[id]/users/[userId].ts +++ b/src/pages/api/teams/[id]/users/[userId].ts @@ -5,6 +5,7 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteTeamUser } from 'queries'; import * as yup from 'yup'; + export interface TeamUserRequestQuery { id: string; userId: string; diff --git a/src/pages/api/teams/[id]/users/index.ts b/src/pages/api/teams/[id]/users/index.ts index d0efba25f..36e9f3206 100644 --- a/src/pages/api/teams/[id]/users/index.ts +++ b/src/pages/api/teams/[id]/users/index.ts @@ -1,24 +1,27 @@ import { canViewTeam } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getUsersByTeamId } from 'queries'; - +import * as yup from 'yup'; export interface TeamUserRequestQuery extends SearchFilter { id: string; } -export interface TeamUserRequestBody { - email: string; - roleId: string; -} +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); const { id: teamId } = req.query; diff --git a/src/pages/api/websites/[id]/events.ts b/src/pages/api/websites/[id]/events.ts index 427cb40ea..422200f80 100644 --- a/src/pages/api/websites/[id]/events.ts +++ b/src/pages/api/websites/[id]/events.ts @@ -6,6 +6,8 @@ import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getEventMetrics } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import * as yup from 'yup'; +import { TimezoneTest } from 'lib/yup'; const unitTypes = ['year', 'month', 'hour', 'day']; @@ -18,15 +20,13 @@ export interface WebsiteEventsRequestQuery { url: string; } -import * as yup from 'yup'; - const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), startAt: yup.number().integer().required(), endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), unit: yup.string().required(), - timezone: yup.string().required(), + timezone: TimezoneTest.required(), url: yup.string(), }), }; diff --git a/src/pages/api/websites/[id]/index.ts b/src/pages/api/websites/[id]/index.ts index 0e5aacceb..e7c7e004c 100644 --- a/src/pages/api/websites/[id]/index.ts +++ b/src/pages/api/websites/[id]/index.ts @@ -22,6 +22,12 @@ const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + name: yup.string().required(), + domain: yup.string().required(), + shareId: yup.string().matches(SHARE_ID_REGEX, { excludeEmptyString: true }), + }), }; export default async ( @@ -55,10 +61,6 @@ export default async ( let website; - if (shareId && !shareId.match(SHARE_ID_REGEX)) { - return serverError(res, 'Invalid share ID.'); - } - try { website = await updateWebsite(websiteId, { name, domain, shareId }); } catch (e: any) { diff --git a/src/pages/api/websites/[id]/metrics.ts b/src/pages/api/websites/[id]/metrics.ts index b8c37339d..89f90fc47 100644 --- a/src/pages/api/websites/[id]/metrics.ts +++ b/src/pages/api/websites/[id]/metrics.ts @@ -33,6 +33,18 @@ const schema = { type: yup.string().required(), startAt: yup.number().required(), endAt: yup.number().required(), + url: yup.string(), + referrer: yup.string(), + title: yup.string(), + query: yup.string(), + os: yup.string(), + browser: yup.string(), + device: yup.string(), + country: yup.string(), + region: yup.string(), + city: yup.string(), + language: yup.string(), + event: yup.string(), }), }; diff --git a/src/pages/api/websites/[id]/pageviews.ts b/src/pages/api/websites/[id]/pageviews.ts index 9985ca892..8c10ffeba 100644 --- a/src/pages/api/websites/[id]/pageviews.ts +++ b/src/pages/api/websites/[id]/pageviews.ts @@ -1,18 +1,17 @@ -import moment from 'moment-timezone'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getPageviewStats, getSessionStats } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getPageviewStats, getSessionStats } from 'queries'; export interface WebsitePageviewRequestQuery { id: string; startAt: number; endAt: number; - unit: string; - timezone: string; + unit?: string; + timezone?: string; url?: string; referrer?: string; title?: string; @@ -24,10 +23,24 @@ export interface WebsitePageviewRequestQuery { city?: string; } +import { TimezoneTest } from 'lib/yup'; import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), + startAt: yup.number().required(), + endAt: yup.number().required(), + unit: yup.string(), + timezone: TimezoneTest, + url: yup.string(), + referrer: yup.string(), + title: yup.string(), + os: yup.string(), + browser: yup.string(), + device: yup.string(), + country: yup.string(), + region: yup.string(), + city: yup.string(), }), }; @@ -62,10 +75,6 @@ export default async ( const { startDate, endDate, unit } = await parseDateRangeQuery(req); - if (!moment.tz.zone(timezone)) { - return badRequest(res); - } - const filters = { startDate, endDate, diff --git a/src/pages/api/websites/[id]/reports.ts b/src/pages/api/websites/[id]/reports.ts index 2c7707e8d..36e97a462 100644 --- a/src/pages/api/websites/[id]/reports.ts +++ b/src/pages/api/websites/[id]/reports.ts @@ -1,6 +1,7 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getReportsByWebsiteId } from 'queries'; @@ -13,6 +14,7 @@ import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), + ...getFilterValidation(/All|Name|Description|Type|Username|Website Name|Website Domain/i), }), }; diff --git a/src/pages/api/websites/[id]/reset.ts b/src/pages/api/websites/[id]/reset.ts index cfd5e7679..b17fdade9 100644 --- a/src/pages/api/websites/[id]/reset.ts +++ b/src/pages/api/websites/[id]/reset.ts @@ -4,14 +4,14 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { resetWebsite } from 'queries'; +import * as yup from 'yup'; export interface WebsiteResetRequestQuery { id: string; } -import * as yup from 'yup'; const schema = { - GET: yup.object().shape({ + POST: yup.object().shape({ id: yup.string().uuid().required(), }), }; @@ -22,7 +22,6 @@ export default async ( ) => { await useCors(req, res); await useAuth(req, res); - req.yup = schema; await useValidate(req, res); diff --git a/src/pages/api/websites/[id]/stats.ts b/src/pages/api/websites/[id]/stats.ts index caf549103..e0c71e404 100644 --- a/src/pages/api/websites/[id]/stats.ts +++ b/src/pages/api/websites/[id]/stats.ts @@ -11,23 +11,36 @@ export interface WebsiteStatsRequestQuery { id: string; startAt: number; endAt: number; - url: string; - referrer: string; - title: string; - query: string; - event: string; - os: string; - browser: string; - device: string; - country: string; - region: string; - city: string; + url?: string; + referrer?: string; + title?: string; + query?: string; + event?: string; + os?: string; + browser?: string; + device?: string; + country?: string; + region?: string; + city?: string; } import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), + startAt: yup.number().required(), + endAt: yup.number().required(), + url: yup.string(), + referrer: yup.string(), + title: yup.string(), + query: yup.string(), + event: yup.string(), + os: yup.string(), + browser: yup.string(), + device: yup.string(), + country: yup.string(), + region: yup.string(), + city: yup.string(), }), }; diff --git a/src/queries/analytics/reports/getRetention.ts b/src/queries/analytics/reports/getRetention.ts index 3c384b6e5..7526644f7 100644 --- a/src/queries/analytics/reports/getRetention.ts +++ b/src/queries/analytics/reports/getRetention.ts @@ -8,7 +8,7 @@ export async function getRetention( filters: { startDate: Date; endDate: Date; - timezone: string; + timezone?: string; }, ] ) { @@ -23,7 +23,7 @@ async function relationalQuery( filters: { startDate: Date; endDate: Date; - timezone: string; + timezone?: string; }, ): Promise< { @@ -103,7 +103,7 @@ async function clickhouseQuery( filters: { startDate: Date; endDate: Date; - timezone: string; + timezone?: string; }, ): Promise< { From e6eb9a487e9e0eac32d707b01d07c802dd8e014c Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 25 Sep 2023 13:31:25 -0700 Subject: [PATCH 203/357] Create unit test. --- src/lib/constants.ts | 2 ++ src/lib/yup.ts | 9 ++++++++- src/pages/api/websites/[id]/events.ts | 25 +++++++++--------------- src/pages/api/websites/[id]/pageviews.ts | 4 ++-- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 888c14843..a548826ad 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -30,6 +30,8 @@ export const FILTER_RANGE = 'filter-range'; export const FILTER_REFERRERS = 'filter-referrers'; export const FILTER_PAGES = 'filter-pages'; +export const UNIT_TYPES = ['year', 'month', 'hour', 'day']; + export const USER_FILTER_TYPES = { all: 'All', username: 'Username', diff --git a/src/lib/yup.ts b/src/lib/yup.ts index 8b2eceee1..a2ea46d8e 100644 --- a/src/lib/yup.ts +++ b/src/lib/yup.ts @@ -1,5 +1,6 @@ import moment from 'moment'; import * as yup from 'yup'; +import { UNIT_TYPES } from './constants'; export const DateRangeValidation = { startAt: yup.number().integer().required(), @@ -20,5 +21,11 @@ export function getFilterValidation(matchRegex) { export const TimezoneTest = yup.string().test( 'timezone', () => `Invalid timezone`, - value => !moment.tz.zone(value), + value => !value || !moment.tz.zone(value), +); + +export const UnitTypeTest = yup.string().test( + 'unit', + () => `Invalid unit`, + value => !value || !UNIT_TYPES.includes(value), ); diff --git a/src/pages/api/websites/[id]/events.ts b/src/pages/api/websites/[id]/events.ts index 422200f80..32288aa52 100644 --- a/src/pages/api/websites/[id]/events.ts +++ b/src/pages/api/websites/[id]/events.ts @@ -1,22 +1,19 @@ -import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import moment from 'moment-timezone'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventMetrics } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types'; +import { TimezoneTest, UnitTypeTest } from 'lib/yup'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getEventMetrics } from 'queries'; import * as yup from 'yup'; -import { TimezoneTest } from 'lib/yup'; - -const unitTypes = ['year', 'month', 'hour', 'day']; export interface WebsiteEventsRequestQuery { id: string; startAt: string; endAt: string; - unit: string; - timezone: string; + unit?: string; + timezone?: string; url: string; } @@ -25,8 +22,8 @@ const schema = { id: yup.string().uuid().required(), startAt: yup.number().integer().required(), endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - unit: yup.string().required(), - timezone: TimezoneTest.required(), + unit: UnitTypeTest, + timezone: TimezoneTest, url: yup.string(), }), }; @@ -49,10 +46,6 @@ export default async ( return unauthorized(res); } - if (!moment.tz.zone(timezone) || !unitTypes.includes(unit)) { - return badRequest(res); - } - const events = await getEventMetrics(websiteId, { startDate, endDate, diff --git a/src/pages/api/websites/[id]/pageviews.ts b/src/pages/api/websites/[id]/pageviews.ts index 8c10ffeba..0f034cc2a 100644 --- a/src/pages/api/websites/[id]/pageviews.ts +++ b/src/pages/api/websites/[id]/pageviews.ts @@ -23,14 +23,14 @@ export interface WebsitePageviewRequestQuery { city?: string; } -import { TimezoneTest } from 'lib/yup'; +import { TimezoneTest, UnitTypeTest } from 'lib/yup'; import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), startAt: yup.number().required(), endAt: yup.number().required(), - unit: yup.string(), + unit: UnitTypeTest, timezone: TimezoneTest, url: yup.string(), referrer: yup.string(), From febf085aca77eb130669d4505798e253062602c2 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 26 Sep 2023 12:30:35 -0700 Subject: [PATCH 204/357] Fix yup. --- src/lib/yup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/yup.ts b/src/lib/yup.ts index a2ea46d8e..6c19b0895 100644 --- a/src/lib/yup.ts +++ b/src/lib/yup.ts @@ -21,11 +21,11 @@ export function getFilterValidation(matchRegex) { export const TimezoneTest = yup.string().test( 'timezone', () => `Invalid timezone`, - value => !value || !moment.tz.zone(value), + value => moment.tz.zone(value) !== null, ); export const UnitTypeTest = yup.string().test( 'unit', () => `Invalid unit`, - value => !value || !UNIT_TYPES.includes(value), + value => UNIT_TYPES.includes(value), ); From 8e8bf41eb3b2f0bc5034918030ecd08ecd76cf5a Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 26 Sep 2023 13:29:49 -0700 Subject: [PATCH 205/357] css updates for pager / page --- src/components/common/Pager.module.css | 1 + src/components/layout/Page.module.css | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/common/Pager.module.css b/src/components/common/Pager.module.css index 99eb70ce0..70fe2019c 100644 --- a/src/components/common/Pager.module.css +++ b/src/components/common/Pager.module.css @@ -1,5 +1,6 @@ .container { margin-top: 20px; + margin-bottom: 20px; } .text { diff --git a/src/components/layout/Page.module.css b/src/components/layout/Page.module.css index c546971b6..100be5bb4 100644 --- a/src/components/layout/Page.module.css +++ b/src/components/layout/Page.module.css @@ -4,4 +4,5 @@ flex-direction: column; background: var(--base50); position: relative; + height: 100%; } From 7e626dcd525318020ca33a62af1f4cc5fddac8d0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 26 Sep 2023 23:20:29 -0700 Subject: [PATCH 206/357] Added useFilterQuery. Converted websites and reports pages. --- src/components/common/DataTable.js | 56 +++--- src/components/common/DataTable.module.css | 23 ++- src/components/common/Pager.js | 36 ++-- src/components/common/Pager.module.css | 23 +++ src/components/hooks/useDataTable.js | 13 -- src/components/hooks/useFilterQuery.js | 16 ++ src/components/hooks/usePaging.js | 9 - src/components/messages.js | 4 + src/components/pages/reports/ReportsPage.js | 61 +++--- src/components/pages/reports/ReportsTable.js | 126 ++++++------- .../pages/settings/websites/WebsitesList.js | 70 +++---- .../pages/settings/websites/WebsitesTable.js | 176 ++++-------------- src/lib/constants.ts | 17 +- src/lib/prisma.ts | 10 +- src/lib/types.ts | 24 +-- src/pages/api/me/teams.ts | 4 +- src/pages/api/me/websites.ts | 4 +- src/pages/api/reports/index.ts | 9 +- src/pages/api/scripts/telemetry.js | 17 +- src/pages/api/teams/[id]/users/index.ts | 9 +- src/pages/api/teams/[id]/websites/index.ts | 12 +- src/pages/api/teams/index.ts | 6 +- src/pages/api/users/[id]/teams.ts | 4 +- src/pages/api/users/[id]/websites.ts | 11 +- src/pages/api/users/index.ts | 8 +- src/pages/api/websites/[id]/reports.ts | 9 +- src/pages/api/websites/index.ts | 4 +- src/queries/admin/report.ts | 107 ++++------- src/queries/admin/website.ts | 4 +- 29 files changed, 373 insertions(+), 499 deletions(-) delete mode 100644 src/components/hooks/useDataTable.js create mode 100644 src/components/hooks/useFilterQuery.js delete mode 100644 src/components/hooks/usePaging.js diff --git a/src/components/common/DataTable.js b/src/components/common/DataTable.js index 2662fa2c4..94b27281d 100644 --- a/src/components/common/DataTable.js +++ b/src/components/common/DataTable.js @@ -1,46 +1,46 @@ -import { createContext } from 'react'; -import { SearchField } from 'react-basics'; -import { useDataTable } from 'components/hooks/useDataTable'; +import { Banner, Loading, SearchField } from 'react-basics'; import { useMessages } from 'components/hooks'; import Empty from 'components/common/Empty'; import Pager from 'components/common/Pager'; import styles from './DataTable.module.css'; +import classNames from 'classnames'; -const DEFAULT_SEARCH_DELAY = 1000; +const DEFAULT_SEARCH_DELAY = 600; export const DataTableStyles = styles; -export const DataTableContext = createContext(null); - export function DataTable({ + data = {}, + params = {}, + setParams, + isLoading, + error, searchDelay, showSearch = true, showPaging = true, children, - onChange, }) { const { formatMessage, labels, messages } = useMessages(); - const dataTable = useDataTable(); - const { query, setQuery, data, pageInfo, setPageInfo } = dataTable; - const { page, pageSize, count } = pageInfo || {}; - const noResults = Boolean(query && data?.length === 0); + const { pageSize, count } = data; + const { query, page } = params; + const hasData = Boolean(!isLoading && data?.data?.length); + const noResults = Boolean(!isLoading && query && !hasData); - const handleChange = value => { - onChange?.({ query: value, page }); - }; - - const handleSearch = value => { - setQuery(value); - handleChange(value); + const handleSearch = query => { + setParams({ ...params, query }); }; const handlePageChange = page => { - setPageInfo(state => ({ ...state, page })); + setParams({ ...params, page }); }; + if (error) { + return {formatMessage(messages.error)}; + } + return ( - - {showSearch && ( + <> + {(hasData || query || isLoading) && showSearch && ( )} - {noResults && } -
{children}
+
+ {hasData && typeof children === 'function' ? children(data) : children} + {isLoading && } + {!isLoading && !hasData && !query && ( + + )} + {noResults && } +
{showPaging && ( )} -
+ ); } diff --git a/src/components/common/DataTable.module.css b/src/components/common/DataTable.module.css index 883110dad..b7426a7cb 100644 --- a/src/components/common/DataTable.module.css +++ b/src/components/common/DataTable.module.css @@ -1,3 +1,12 @@ +.table { + grid-template-rows: repeat(auto-fit, max-content); +} + +.table td { + align-items: center; + max-height: max-content; +} + .search { max-width: 300px; margin: 20px 0; @@ -8,10 +17,22 @@ gap: 5px; } +.body { + display: flex; + position: relative; +} + .body td { align-items: center; } .pager { - margin-top: 20px; + margin: 20px 0; +} + +.status { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; } diff --git a/src/components/common/Pager.js b/src/components/common/Pager.js index 3f94edb01..f35c2ab0c 100644 --- a/src/components/common/Pager.js +++ b/src/components/common/Pager.js @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { Button, Flexbox, Icon, Icons } from 'react-basics'; +import { Button, Icon, Icons } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; import styles from './Pager.module.css'; @@ -25,21 +25,25 @@ export function Pager({ page, pageSize, count, onPageChange, className }) { } return ( - - - - {formatMessage(labels.pageOf, { current: page, total: maxPage })} - - - +
+
{formatMessage(labels.numberOfRecords, { x: count })}
+
+ +
+ {formatMessage(labels.pageOf, { current: page, total: maxPage })} +
+ +
+
+
); } diff --git a/src/components/common/Pager.module.css b/src/components/common/Pager.module.css index 9c22f5971..0ed5e1f45 100644 --- a/src/components/common/Pager.module.css +++ b/src/components/common/Pager.module.css @@ -1,4 +1,27 @@ +.pager { + display: grid; + grid-template-columns: repeat(3, 1fr); + align-items: center; +} + +.nav { + display: flex; + align-items: center; + justify-content: center; +} + .text { font-size: var(--font-size-md); margin: 0 16px; + justify-content: center; +} + +@media only screen and (max-width: 992px) { + .pager { + grid-template-columns: repeat(2, 1fr); + } + + .nav { + justify-content: end; + } } diff --git a/src/components/hooks/useDataTable.js b/src/components/hooks/useDataTable.js deleted file mode 100644 index 83aa3d683..000000000 --- a/src/components/hooks/useDataTable.js +++ /dev/null @@ -1,13 +0,0 @@ -import { useState } from 'react'; -import { usePaging } from 'components/hooks/usePaging'; - -export function useDataTable(config = {}) { - const { initialData, initialQuery, initialPageInfo } = config; - const [data, setData] = useState(initialData ?? null); - const [query, setQuery] = useState(initialQuery ?? ''); - const { pageInfo, setPageInfo } = usePaging(initialPageInfo); - - return { data, setData, query, setQuery, pageInfo, setPageInfo }; -} - -export default useDataTable; diff --git a/src/components/hooks/useFilterQuery.js b/src/components/hooks/useFilterQuery.js new file mode 100644 index 000000000..5dd9b3dff --- /dev/null +++ b/src/components/hooks/useFilterQuery.js @@ -0,0 +1,16 @@ +import { useState } from 'react'; +import { useApi } from 'components/hooks/useApi'; + +export function useFilterQuery(key, fn, options) { + const [params, setParams] = useState({ + query: '', + page: 1, + }); + const { useQuery } = useApi(); + + const result = useQuery([...key, params], fn.bind(null, params), options); + + return { ...result, params, setParams }; +} + +export default useFilterQuery; diff --git a/src/components/hooks/usePaging.js b/src/components/hooks/usePaging.js deleted file mode 100644 index 17c231536..000000000 --- a/src/components/hooks/usePaging.js +++ /dev/null @@ -1,9 +0,0 @@ -import { useState } from 'react'; - -const DEFAULT_PAGE_INFO = { page: 1, pageSize: 10, total: 0 }; - -export function usePaging(initialPageInfo) { - const [pageInfo, setPageInfo] = useState(initialPageInfo ?? { ...DEFAULT_PAGE_INFO }); - - return { pageInfo, setPageInfo }; -} diff --git a/src/components/messages.js b/src/components/messages.js index 7f432eb3e..04a29a4c9 100644 --- a/src/components/messages.js +++ b/src/components/messages.js @@ -193,6 +193,10 @@ export const labels = defineMessages({ pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' }, create: { id: 'label.create', defaultMessage: 'Create' }, search: { id: 'label.search', defaultMessage: 'Search' }, + numberOfRecords: { + id: 'label.number-of-records', + defaultMessage: '{x} {x, plural, one {record} other {records}}', + }, }); export const messages = defineMessages({ diff --git a/src/components/pages/reports/ReportsPage.js b/src/components/pages/reports/ReportsPage.js index bbb15a365..9a48d7806 100644 --- a/src/components/pages/reports/ReportsPage.js +++ b/src/components/pages/reports/ReportsPage.js @@ -1,28 +1,39 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; -import { useMessages, useReports } from 'components/hooks'; +import { useMessages, useApi } from 'components/hooks'; import Link from 'next/link'; import { Button, Icon, Icons, Text } from 'react-basics'; import ReportsTable from './ReportsTable'; +import useFilterQuery from 'components/hooks/useFilterQuery'; +import DataTable from 'components/common/DataTable'; + +function useReports() { + const { get, del, useMutation } = useApi(); + const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); + const reports = useFilterQuery(['reports'], params => get(`/reports`, params)); + + const deleteReport = id => { + mutate(id, { + onSuccess: () => { + reports.refetch(); + }, + }); + }; + + return { reports, deleteReport }; +} export function ReportsPage() { const { formatMessage, labels } = useMessages(); - const { - reports, - error, - isLoading, - deleteReport, - filter, - handleFilterChange, - handlePageChange, - handlePageSizeChange, - } = useReports(); + const { reports, deleteReport } = useReports(); - const hasData = (reports && reports?.data.length !== 0) || filter; + const handleDelete = async (id, callback) => { + await deleteReport(id); + await reports.refetch(); + callback?.(); + }; return ( - + <> - - {hasData && ( - - )} - {!hasData && } - + + {({ data }) => } + + ); } diff --git a/src/components/pages/reports/ReportsTable.js b/src/components/pages/reports/ReportsTable.js index 52488c11e..72b0c273c 100644 --- a/src/components/pages/reports/ReportsTable.js +++ b/src/components/pages/reports/ReportsTable.js @@ -1,96 +1,74 @@ import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm'; import LinkButton from 'components/common/LinkButton'; -import SettingsTable from 'components/common/SettingsTable'; import { useMessages } from 'components/hooks'; import useUser from 'components/hooks/useUser'; -import { useState } from 'react'; -import { Button, Flexbox, Icon, Icons, Modal, Text } from 'react-basics'; +import { + Button, + Flexbox, + GridColumn, + GridTable, + Icon, + Icons, + Modal, + ModalTrigger, + Text, +} from 'react-basics'; import { REPORT_TYPES } from 'lib/constants'; -export function ReportsTable({ - data = [], - onDelete = () => {}, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, - showDomain, -}) { - const [report, setReport] = useState(null); +export function ReportsTable({ data = [], onDelete, showDomain }) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); - const domainColumn = [ - { - name: 'domain', - label: formatMessage(labels.domain), - }, - ]; - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'description', label: formatMessage(labels.description) }, - { name: 'type', label: formatMessage(labels.type) }, - ...(showDomain ? domainColumn : []), - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'type') { - return formatMessage( - labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)], - ); - } - return data[key]; - }; - - const handleConfirm = () => { - onDelete(report.id); + const handleConfirm = (id, callback) => { + onDelete?.(id, callback); }; return ( - <> - + + + + {row => { - const { id, userId: reportOwnerId, website } = row; - if (showDomain) { - row.domain = website.domain; - } - + return formatMessage( + labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)], + ); + }} + + {showDomain && ( + + {row => row.website.domain} + + )} + + {row => { + const { id, name, userId, website } = row; return ( {formatMessage(labels.view)} - {!showDomain || user.id === reportOwnerId || user.id === website?.userId} - + {(user.id === userId || user.id === website?.userId) && ( + + + + {close => ( + + )} + + + )} ); }} - - {report && ( - - setReport(null)} - /> - - )} - +
+
); } diff --git a/src/components/pages/settings/websites/WebsitesList.js b/src/components/pages/settings/websites/WebsitesList.js index 4761ad0a0..0dd3aa775 100644 --- a/src/components/pages/settings/websites/WebsitesList.js +++ b/src/components/pages/settings/websites/WebsitesList.js @@ -1,13 +1,13 @@ -import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; -import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; import { ROLES } from 'lib/constants'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import { useRef, useState } from 'react'; +import useApi from 'components/hooks/useApi'; +import DataTable from 'components/common/DataTable'; +import useFilterQuery from 'components/hooks/useFilterQuery'; export function WebsitesList({ showTeam, @@ -18,13 +18,10 @@ export function WebsitesList({ }) { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); - const [params, setParams] = useState({}); - const { get, useQuery } = useApi(); - const count = useRef(0); - const q = useQuery( - ['websites', includeTeams, onlyTeams, params], - () => { - count.current += 1; + const { get } = useApi(); + const filterQuery = useFilterQuery( + ['websites', { includeTeams, onlyTeams }], + params => { return get(`/users/${user?.id}/websites`, { includeTeams, onlyTeams, @@ -33,46 +30,41 @@ export function WebsitesList({ }, { enabled: !!user }, ); - const { data, refetch, isLoading, error } = q; + const { refetch } = filterQuery; const { showToast } = useToasts(); - const handleChange = params => { - setParams(params); - }; - const handleSave = async () => { await refetch(); showToast({ message: formatMessage(messages.saved), variant: 'success' }); }; const addButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - + + + + {close => } + + ); return ( - - {showHeader && {addButton}} - - + <> + {showHeader && ( + + {user.role !== ROLES.viewOnly && addButton} + + )} + + {({ data }) => ( + + )} + + ); } diff --git a/src/components/pages/settings/websites/WebsitesTable.js b/src/components/pages/settings/websites/WebsitesTable.js index 12f942007..3739de646 100644 --- a/src/components/pages/settings/websites/WebsitesTable.js +++ b/src/components/pages/settings/websites/WebsitesTable.js @@ -1,154 +1,58 @@ import Link from 'next/link'; -import { Button, Text, Icon, Icons, GridTable, GridColumn } from 'react-basics'; -import SettingsTable from 'components/common/SettingsTable'; -import Empty from 'components/common/Empty'; +import { Button, Text, Icon, Icons, GridTable, GridColumn, Flexbox } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; -import DataTable, { DataTableStyles } from 'components/common/DataTable'; -export function WebsitesTable({ - data = [], - showTeam, - showEditButton, - openExternal = false, - onChange, -}) { +export function WebsitesTable({ data = [], showTeam, showEditButton }) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); - const showTable = data.length !== 0; - return ( - - {showTable && ( - - - - {showTeam && ( - - {row => row.teamWebsite[0]?.team.name} - - )} - {showTeam && ( - - {row => row.user.username} - - )} - - {row => { - const { - id, - user: { id: ownerId }, - } = row; - - return ( - <> - {showEditButton && (!showTeam || ownerId === user.id) && ( - - - - )} - - - - - ); - }} - - + + + + {showTeam && ( + + {row => row.teamWebsite[0]?.team.name} + )} - - ); -} + {showTeam && ( + + {row => row.user.username} + + )} + + {row => { + const { + id, + user: { id: ownerId }, + } = row; -export function WebsitesTable2({ - data = [], - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, - showTeam, - showEditButton, - openExternal = false, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - - const showTable = data && (filterValue || data?.data?.length !== 0); - - const teamColumns = [ - { name: 'teamName', label: formatMessage(labels.teamName) }, - { name: 'owner', label: formatMessage(labels.owner) }, - ]; - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'domain', label: formatMessage(labels.domain) }, - ...(showTeam ? teamColumns : []), - { name: 'action', label: ' ' }, - ]; - - return ( - <> - {showTable && ( - - {row => { - const { - id, - teamWebsite, - user: { username, id: ownerId }, - } = row; - if (showTeam) { - row.teamName = teamWebsite[0]?.team.name; - row.owner = username; - } - - return ( - <> - {showEditButton && (!showTeam || ownerId === user.id) && ( - - - - )} - + return ( + + {showEditButton && (!showTeam || ownerId === user.id) && ( + - - ); - }} - - )} - {!showTable && } - + )} + + + + + ); + }} + +
); } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 888c14843..9ea76d934 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -19,6 +19,7 @@ export const DEFAULT_ANIMATION_DURATION = 300; export const DEFAULT_DATE_RANGE = '24hour'; export const DEFAULT_WEBSITE_LIMIT = 10; export const DEFAULT_RESET_DATE = '2000-01-01'; +export const DEFAULT_PAGE_SIZE = 10; export const REALTIME_RANGE = 30; export const REALTIME_INTERVAL = 5000; @@ -30,22 +31,6 @@ export const FILTER_RANGE = 'filter-range'; export const FILTER_REFERRERS = 'filter-referrers'; export const FILTER_PAGES = 'filter-pages'; -export const USER_FILTER_TYPES = { - all: 'All', - username: 'Username', -} as const; -export const WEBSITE_FILTER_TYPES = { all: 'All', name: 'Name', domain: 'Domain' } as const; -export const TEAM_FILTER_TYPES = { all: 'All', name: 'Name', 'user:username': 'Owner' } as const; -export const REPORT_FILTER_TYPES = { - all: 'All', - name: 'Name', - description: 'Description', - type: 'Type', - 'user:username': 'Username', - 'website:name': 'Website Name', - 'website:domain': 'Website Domain', -} as const; - export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event']; export const SESSION_COLUMNS = [ diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 59638dbd6..f75ea1fea 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,11 +1,11 @@ +import { Prisma } from '@prisma/client'; import prisma from '@umami/prisma-client'; import moment from 'moment-timezone'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; -import { FILTER_COLUMNS, SESSION_COLUMNS, OPERATORS } from './constants'; +import { FILTER_COLUMNS, SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; import { QueryFilters, QueryOptions, SearchFilter } from './types'; -import { Prisma } from '@prisma/client'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -171,7 +171,7 @@ async function rawQuery(sql: string, data: object): Promise { return prisma.rawQuery(query, params); } -function getPageFilters(filters: SearchFilter): [ +function getPageFilters(filters: SearchFilter): [ { orderBy: { [x: string]: string; @@ -185,7 +185,7 @@ function getPageFilters(filters: SearchFilter): [ orderBy: string; }, ] { - const { pageSize = 10, page = 1, orderBy } = filters || {}; + const { page = 1, pageSize = DEFAULT_PAGE_SIZE, orderBy } = filters || {}; return [ { @@ -198,7 +198,7 @@ function getPageFilters(filters: SearchFilter): [ ], }), }, - { pageSize, page: +page, orderBy }, + { page: +page, pageSize, orderBy }, ]; } diff --git a/src/lib/types.ts b/src/lib/types.ts index 58e6aa9e6..98fbc29ba 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,12 +5,8 @@ import { EVENT_TYPE, KAFKA_TOPIC, PERMISSIONS, - REPORT_FILTER_TYPES, REPORT_TYPES, ROLES, - TEAM_FILTER_TYPES, - USER_FILTER_TYPES, - WEBSITE_FILTER_TYPES, } from './constants'; import * as yup from 'yup'; import { TIME_UNIT } from './date'; @@ -27,46 +23,42 @@ export type DynamicDataType = ObjectValues; export type KafkaTopic = ObjectValues; export type ReportType = ObjectValues; -export type ReportSearchFilterType = ObjectValues; -export type UserSearchFilterType = ObjectValues; -export type WebsiteSearchFilterType = ObjectValues; -export type TeamSearchFilterType = ObjectValues; - -export interface WebsiteSearchFilter extends SearchFilter { +export interface WebsiteSearchFilter extends SearchFilter { userId?: string; teamId?: string; includeTeams?: boolean; onlyTeams?: boolean; } -export interface UserSearchFilter extends SearchFilter { +export interface UserSearchFilter extends SearchFilter { teamId?: string; } -export interface TeamSearchFilter extends SearchFilter { +export interface TeamSearchFilter extends SearchFilter { userId?: string; } -export interface ReportSearchFilter extends SearchFilter { +export interface ReportSearchFilter extends SearchFilter { userId?: string; websiteId?: string; includeTeams?: boolean; } -export interface SearchFilter { +export interface SearchFilter { query?: string; page?: number; pageSize?: number; orderBy?: string; - data?: T; + sortDescending?: boolean; } export interface FilterResult { data: T; count: number; - pageSize: number; page: number; + pageSize: number; orderBy?: string; + sortDescending?: boolean; } export interface DynamicData { diff --git a/src/pages/api/me/teams.ts b/src/pages/api/me/teams.ts index 131cb2621..14602157a 100644 --- a/src/pages/api/me/teams.ts +++ b/src/pages/api/me/teams.ts @@ -1,12 +1,12 @@ import { useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userTeams from 'pages/api/users/[id]/teams'; import * as yup from 'yup'; -export interface MyTeamsRequestQuery extends SearchFilter { +export interface MyTeamsRequestQuery extends SearchFilter { id: string; } diff --git a/src/pages/api/me/websites.ts b/src/pages/api/me/websites.ts index 749af3169..ec6a55569 100644 --- a/src/pages/api/me/websites.ts +++ b/src/pages/api/me/websites.ts @@ -1,12 +1,12 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userWebsites from 'pages/api/users/[id]/websites'; import * as yup from 'yup'; -export interface MyWebsitesRequestQuery extends SearchFilter { +export interface MyWebsitesRequestQuery extends SearchFilter { id: string; } diff --git a/src/pages/api/reports/index.ts b/src/pages/api/reports/index.ts index 3c975b76f..911d729c3 100644 --- a/src/pages/api/reports/index.ts +++ b/src/pages/api/reports/index.ts @@ -1,13 +1,13 @@ import { uuid } from 'lib/crypto'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok } from 'next-basics'; import { createReport, getReportsByUserId } from 'queries'; import * as yup from 'yup'; -export interface ReportsRequestQuery extends SearchFilter {} +export interface ReportsRequestQuery extends SearchFilter {} export interface ReportRequestBody { websiteId: string; @@ -52,12 +52,11 @@ export default async ( } = req.auth; if (req.method === 'GET') { - const { page, filter, pageSize } = req.query; + const { page, query } = req.query; const data = await getReportsByUserId(userId, { page, - filter, - pageSize: +pageSize || undefined, + query, includeTeams: true, }); diff --git a/src/pages/api/scripts/telemetry.js b/src/pages/api/scripts/telemetry.js index 954d50586..6a249de0c 100644 --- a/src/pages/api/scripts/telemetry.js +++ b/src/pages/api/scripts/telemetry.js @@ -1,18 +1,23 @@ +import { ok } from 'next-basics'; import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants'; export default function handler(req, res) { - res.setHeader('content-type', 'text/javascript'); + if (process.env.NODE_ENV === 'production') { + res.setHeader('content-type', 'text/javascript'); - if (process.env.DISABLE_TELEMETRY) { - return res.send('/* telemetry disabled */'); - } + if (process.env.DISABLE_TELEMETRY) { + return res.send('/* telemetry disabled */'); + } - const script = ` + const script = ` (()=>{const i=document.createElement('img'); i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}'); i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;'); document.body.appendChild(i);})(); `; - return res.send(script.replace(/\s\s+/g, '')); + return res.send(script.replace(/\s\s+/g, '')); + } + + return ok(res); } diff --git a/src/pages/api/teams/[id]/users/index.ts b/src/pages/api/teams/[id]/users/index.ts index d0efba25f..1c9e83520 100644 --- a/src/pages/api/teams/[id]/users/index.ts +++ b/src/pages/api/teams/[id]/users/index.ts @@ -1,11 +1,11 @@ import { canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getUsersByTeamId } from 'queries'; -export interface TeamUserRequestQuery extends SearchFilter { +export interface TeamUserRequestQuery extends SearchFilter { id: string; } @@ -27,12 +27,11 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; + const { query, page } = req.query; const users = await getUsersByTeamId(teamId, { + query, page, - filter, - pageSize: +pageSize || undefined, }); return ok(res, users); diff --git a/src/pages/api/teams/[id]/websites/index.ts b/src/pages/api/teams/[id]/websites/index.ts index 23c7390b7..4d14c4e9a 100644 --- a/src/pages/api/teams/[id]/websites/index.ts +++ b/src/pages/api/teams/[id]/websites/index.ts @@ -1,14 +1,14 @@ import * as yup from 'yup'; import { canViewTeam } from 'lib/auth'; import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsitesByTeamId } from 'queries'; import { createTeamWebsites } from 'queries/admin/teamWebsite'; -export interface TeamWebsiteRequestQuery extends SearchFilter { +export interface TeamWebsiteRequestQuery extends SearchFilter { id: string; } @@ -43,13 +43,7 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; - - const websites = await getWebsitesByTeamId(teamId, { - page, - filter, - pageSize: +pageSize || undefined, - }); + const websites = await getWebsitesByTeamId(teamId, { ...req.query }); return ok(res, websites); } diff --git a/src/pages/api/teams/index.ts b/src/pages/api/teams/index.ts index 084d09a27..74cb532e3 100644 --- a/src/pages/api/teams/index.ts +++ b/src/pages/api/teams/index.ts @@ -2,19 +2,19 @@ import { Team } from '@prisma/client'; import { canCreateTeam } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createTeam, getTeamsByUserId } from 'queries'; import * as yup from 'yup'; -export interface TeamsRequestQuery extends SearchFilter {} +export interface TeamsRequestQuery extends SearchFilter {} export interface TeamsRequestBody { name: string; } -export interface MyTeamsRequestQuery extends SearchFilter {} +export interface MyTeamsRequestQuery extends SearchFilter {} const schema = { GET: yup.object().shape({ diff --git a/src/pages/api/users/[id]/teams.ts b/src/pages/api/users/[id]/teams.ts index 34a31a0e2..f9d7f5ea2 100644 --- a/src/pages/api/users/[id]/teams.ts +++ b/src/pages/api/users/[id]/teams.ts @@ -1,12 +1,12 @@ import * as yup from 'yup'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getTeamsByUserId } from 'queries'; -export interface UserTeamsRequestQuery extends SearchFilter { +export interface UserTeamsRequestQuery extends SearchFilter { id: string; } diff --git a/src/pages/api/users/[id]/websites.ts b/src/pages/api/users/[id]/websites.ts index cc264e7dd..227d1c98a 100644 --- a/src/pages/api/users/[id]/websites.ts +++ b/src/pages/api/users/[id]/websites.ts @@ -1,12 +1,12 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsitesByUserId } from 'queries'; import * as yup from 'yup'; -export interface UserWebsitesRequestQuery extends SearchFilter { +export interface UserWebsitesRequestQuery extends SearchFilter { id: string; includeTeams?: boolean; onlyTeams?: boolean; @@ -32,7 +32,7 @@ export default async ( await useValidate(req, res); const { user } = req.auth; - const { id: userId, page, pageSize, query, includeTeams, onlyTeams } = req.query; + const { id: userId, page, query, includeTeams, onlyTeams } = req.query; if (req.method === 'GET') { if (!user.isAdmin && user.id !== userId) { @@ -40,9 +40,8 @@ export default async ( } const websites = await getWebsitesByUserId(userId, { - query, - page, - pageSize: +pageSize || undefined, + page: +page, + query: query as string, includeTeams, onlyTeams, }); diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts index d37add2fe..670ddd5d3 100644 --- a/src/pages/api/users/index.ts +++ b/src/pages/api/users/index.ts @@ -2,13 +2,13 @@ import { canCreateUser, canViewUsers } from 'lib/auth'; import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUserByUsername, getUsers } from 'queries'; -export interface UsersRequestQuery extends SearchFilter {} +export interface UsersRequestQuery extends SearchFilter {} export interface UsersRequestBody { username: string; password: string; @@ -46,9 +46,9 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; + const { page, query } = req.query; - const users = await getUsers({ page, filter, pageSize: pageSize ? +pageSize : null }); + const users = await getUsers({ page, query }); return ok(res, users); } diff --git a/src/pages/api/websites/[id]/reports.ts b/src/pages/api/websites/[id]/reports.ts index 2c7707e8d..ec8109f89 100644 --- a/src/pages/api/websites/[id]/reports.ts +++ b/src/pages/api/websites/[id]/reports.ts @@ -1,11 +1,11 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getReportsByWebsiteId } from 'queries'; -export interface ReportsRequestQuery extends SearchFilter { +export interface ReportsRequestQuery extends SearchFilter { id: string; } @@ -33,12 +33,11 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; + const { page, query } = req.query; const data = await getReportsByWebsiteId(websiteId, { page, - filter, - pageSize: +pageSize || undefined, + query, }); return ok(res, data); diff --git a/src/pages/api/websites/index.ts b/src/pages/api/websites/index.ts index a90f8e46d..dc9ec36d0 100644 --- a/src/pages/api/websites/index.ts +++ b/src/pages/api/websites/index.ts @@ -1,7 +1,7 @@ import { canCreateWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; @@ -9,7 +9,7 @@ import userWebsites from 'pages/api/users/[id]/websites'; import * as yup from 'yup'; import { pageInfo } from 'lib/schema'; -export interface WebsitesRequestQuery extends SearchFilter {} +export interface WebsitesRequestQuery extends SearchFilter {} export interface WebsitesRequestBody { name: string; diff --git a/src/queries/admin/report.ts b/src/queries/admin/report.ts index 59eb70356..2f987681f 100644 --- a/src/queries/admin/report.ts +++ b/src/queries/admin/report.ts @@ -1,5 +1,4 @@ import { Prisma, Report } from '@prisma/client'; -import { REPORT_FILTER_TYPES } from 'lib/constants'; import prisma from 'lib/prisma'; import { FilterResult, ReportSearchFilter } from 'lib/types'; @@ -27,27 +26,21 @@ export async function deleteReport(reportId: string): Promise { } export async function getReports( - ReportSearchFilter: ReportSearchFilter, + params: ReportSearchFilter, options?: { include?: Prisma.ReportInclude }, ): Promise> { - const { - userId, - websiteId, - includeTeams, - filter, - filterType = REPORT_FILTER_TYPES.all, - } = ReportSearchFilter; + const { query, userId, websiteId, includeTeams } = params; const mode = prisma.getSearchMode(); const where: Prisma.ReportWhereInput = { - ...(userId && { userId: userId }), - ...(websiteId && { websiteId: websiteId }), + userId, + websiteId, AND: [ { OR: [ { - ...(userId && { userId: userId }), + userId, }, { ...(includeTeams && { @@ -71,71 +64,53 @@ export async function getReports( { OR: [ { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES.name) && { + name: { + contains: query, + ...mode, + }, + }, + { + description: { + contains: query, + ...mode, + }, + }, + { + type: { + contains: query, + ...mode, + }, + }, + { + user: { + username: { + contains: query, + ...mode, + }, + }, + }, + { + website: { name: { - startsWith: filter, + contains: query, ...mode, }, - }), + }, }, { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES.description) && { - description: { - startsWith: filter, + website: { + domain: { + contains: query, ...mode, }, - }), - }, - { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES.type) && { - type: { - startsWith: filter, - ...mode, - }, - }), - }, - { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES['user:username']) && { - user: { - username: { - startsWith: filter, - ...mode, - }, - }, - }), - }, - { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES['website:name']) && { - website: { - name: { - startsWith: filter, - ...mode, - }, - }, - }), - }, - { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES['website:domain']) && { - website: { - domain: { - startsWith: filter, - ...mode, - }, - }, - }), + }, }, ], }, ], }; - const [pageFilters, getParameters] = prisma.getPageFilters(ReportSearchFilter); + const [pageFilters, pageInfo] = prisma.getPageFilters(params); const reports = await prisma.client.report.findMany({ where, @@ -150,13 +125,13 @@ export async function getReports( return { data: reports, count, - ...getParameters, + ...pageInfo, }; } export async function getReportsByUserId( userId: string, - filter: ReportSearchFilter, + filter?: ReportSearchFilter, ): Promise> { return getReports( { userId, ...filter }, diff --git a/src/queries/admin/website.ts b/src/queries/admin/website.ts index f4444b533..0e7f5124f 100644 --- a/src/queries/admin/website.ts +++ b/src/queries/admin/website.ts @@ -72,10 +72,10 @@ export async function getWebsites( OR: query ? [ { - name: { startsWith: query, ...mode }, + name: { contains: query, ...mode }, }, { - domain: { startsWith: query, ...mode }, + domain: { contains: query, ...mode }, }, ] : [], From 49ad536f246f82c89a3139a652bf74b7784d9894 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 28 Sep 2023 13:14:15 -0700 Subject: [PATCH 207/357] Auto stash before merge of "dev" and "origin/dev" --- src/lib/prisma.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index f75ea1fea..442ee2022 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -185,7 +185,7 @@ function getPageFilters(filters: SearchFilter): [ orderBy: string; }, ] { - const { page = 1, pageSize = DEFAULT_PAGE_SIZE, orderBy } = filters || {}; + const { page = 1, pageSize = DEFAULT_PAGE_SIZE, orderBy, sortDescending = false } = filters || {}; return [ { @@ -193,7 +193,7 @@ function getPageFilters(filters: SearchFilter): [ ...(orderBy && { orderBy: [ { - [orderBy]: 'asc', + [orderBy]: sortDescending ? 'desc' : 'asc', }, ], }), From 35d45334df5237afc6f5d06002a36a9ff1e22285 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 28 Sep 2023 16:45:25 -0700 Subject: [PATCH 208/357] update package --- package.json | 2 +- yarn.lock | 333 +++------------------------------------------------ 2 files changed, 18 insertions(+), 317 deletions(-) diff --git a/package.json b/package.json index 79960eb23..6fc413d44 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ ".next/cache" ], "dependencies": { + "@clickhouse/client": "^0.2.2", "@fontsource/inter": "^4.5.15", "@prisma/client": "5.3.1", "@tanstack/react-query": "^4.33.0", @@ -70,7 +71,6 @@ "chart.js": "^4.2.1", "chartjs-adapter-date-fns": "^3.0.0", "classnames": "^2.3.1", - "clickhouse": "^2.5.0", "colord": "^2.9.2", "cors": "^2.8.5", "cross-spawn": "^7.0.3", diff --git a/yarn.lock b/yarn.lock index ecb1a7ebe..d054fefd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1083,6 +1083,18 @@ "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" +"@clickhouse/client-common@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@clickhouse/client-common/-/client-common-0.2.2.tgz#0690046241140a51ba5b0c0b9298c3cb3cf20974" + integrity sha512-jlom9zLfcDzX9E3off93ZD3CPOkClyM213Y7TN1datkuRGKMvVyj1k0KXaMekhbRev+FTe85CqfoD5eq6qOnPg== + +"@clickhouse/client@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@clickhouse/client/-/client-0.2.2.tgz#a6358aa2342ee3f2850cdb2f47a9e1d6fbde5757" + integrity sha512-2faBnDS4x7ZkcOZqi3f6H967kH+nOfJLhBTWWjz0wTSBnEJBXRtePhN/ZY0NJIKc9Ga5w41Pf67mQgm6Dm/1/w== + dependencies: + "@clickhouse/client-common" "0.2.2" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" @@ -2869,14 +2881,6 @@ resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.2.36.tgz" integrity sha512-JtB41wXl7Au3+Nl3gD16Cfpj7k/6aCroZ6BbOiCMFCMvrOpkg/qQUXTso2XowaNqBbnkuGHurLAqkLBxNGc1hQ== -JSONStream@1.3.4: - version "1.3.4" - resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.4.tgz" - integrity sha512-Y7vfi3I5oMOYIr+WxV8NZxDSwcbNgzdKYsTNInmycOq9bUYwGg9ryu57Wg5NLmCjqdFPNUmpMBo3kSJN9tCbXg== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - acorn-dynamic-import@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz" @@ -2939,7 +2943,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.3, ajv@^6.12.4: +ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3138,18 +3142,6 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== - ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" @@ -3160,11 +3152,6 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" @@ -3187,16 +3174,6 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" - integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - axe-core@^4.4.3: version "4.5.2" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz" @@ -3267,13 +3244,6 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - bcryptjs@^2.4.3: version "2.4.3" resolved "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz" @@ -3430,11 +3400,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001426, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001506.tgz" integrity sha512-6XNEcpygZMCKaufIcgpQNZNf00GEqc7VQON+9Rd0K1bMYo8xhMZRAo5zpbnbMNizi4YNgIDAFrdykWsvY3H4Hw== -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" - integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== - chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" @@ -3526,20 +3491,6 @@ cli-truncate@2.1.0, cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" -clickhouse@^2.5.0: - version "2.6.0" - resolved "https://registry.npmjs.org/clickhouse/-/clickhouse-2.6.0.tgz" - integrity sha512-HC5OV99GJOup4qZsTuWWPpXlj+847Z0OeygDU2x22rNYost0V/vWapzFWYZdV/5iRbGMrhFQPOyQEzmGvoaWRQ== - dependencies: - JSONStream "1.3.4" - lodash "4.17.21" - querystring "0.2.0" - request "2.88.0" - stream2asynciter "1.0.3" - through "2.3.8" - tsv "0.2.0" - uuid "3.4.0" - client-only@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" @@ -3619,13 +3570,6 @@ colorette@^2.0.16: resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - commander@2, commander@^2.20.0, commander@^2.20.3: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" @@ -3690,11 +3634,6 @@ core-js-pure@^3.25.1: resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz" integrity sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ== -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - cors@^2.8.5: version "2.8.5" resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" @@ -4029,13 +3968,6 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== - dependencies: - assert-plus "^1.0.0" - data-uri-to-buffer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" @@ -4168,11 +4100,6 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" @@ -4302,14 +4229,6 @@ dotenv@^10.0.0: resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - ecdsa-sig-formatter@1.0.11: version "1.0.11" resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" @@ -4808,11 +4727,6 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - extract-react-intl-messages@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/extract-react-intl-messages/-/extract-react-intl-messages-4.1.1.tgz" @@ -4834,16 +4748,6 @@ extract-react-intl-messages@^4.1.1: sort-keys "^4.0.0" write-json-file "^4.3.0" -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -4977,20 +4881,6 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - formdata-polyfill@^4.0.10: version "4.0.10" resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" @@ -5132,13 +5022,6 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.0.0" - github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -5319,19 +5202,6 @@ h3@^1.7.1, h3@^1.8.1: uncrypto "^0.1.3" unenv "^1.7.4" -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" - integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== - -har-validator@~5.1.0: - version "5.1.5" - resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - hard-rejection@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" @@ -5412,15 +5282,6 @@ http-shutdown@^1.2.2: resolved "https://registry.yarnpkg.com/http-shutdown/-/http-shutdown-1.2.2.tgz#41bc78fc767637c4c95179bc492f312c0ae64c5f" integrity sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw== -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" - integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -5840,7 +5701,7 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: dependencies: which-typed-array "^1.1.11" -is-typedarray@^1.0.0, is-typedarray@~1.0.0: +is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== @@ -5874,11 +5735,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== - jest-worker@^26.2.1: version "26.6.2" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" @@ -5918,11 +5774,6 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" @@ -5958,11 +5809,6 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -5975,11 +5821,6 @@ json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -6023,11 +5864,6 @@ jsonify@~0.0.0: resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" integrity sha512-trvBk1ki43VZptdBI5rIlG4YOzyeH/WefQt5rj1grasPn4iiZWKet8nkgc4GlsAylaztn0qZfUYOiTsASJFdNA== -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - jsonwebtoken@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz" @@ -6038,16 +5874,6 @@ jsonwebtoken@^9.0.0: ms "^2.1.1" semver "^7.3.8" -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: version "3.3.3" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" @@ -6281,7 +6107,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@4.17.21, lodash@^4.17.21: +lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6476,18 +6302,6 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - mime@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" @@ -6831,11 +6645,6 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - object-assign@^4, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" @@ -7085,11 +6894,6 @@ pathe@^1.1.0, pathe@^1.1.1: resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -7779,11 +7583,6 @@ property-expr@^2.0.4: resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz" integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== -psl@^1.1.24: - version "1.9.0" - resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -7792,11 +7591,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - punycode@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" @@ -7807,16 +7601,6 @@ pure-rand@^6.0.2: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz" integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" - integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -8251,32 +8035,6 @@ request-ip@^3.3.0: resolved "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz" integrity sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA== -request@2.88.0: - version "2.88.0" - resolved "https://registry.npmjs.org/request/-/request-2.88.0.tgz" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" @@ -8464,7 +8222,7 @@ safe-array-concat@^1.0.1: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -8483,11 +8241,6 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - scheduler@^0.23.0: version "0.23.0" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz" @@ -8749,21 +8502,6 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - stable@^0.1.8: version "0.1.8" resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" @@ -8779,11 +8517,6 @@ std-env@^3.4.3: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== -stream2asynciter@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/stream2asynciter/-/stream2asynciter-1.0.3.tgz" - integrity sha512-9/dEZW+LQjuW6ub5hmWi4n9Pn8W8qA8k7NAE1isecesA164e73xTdy1CJ3S9o9YS+O21HuiK7T+4uS7FgKDy4w== - streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -9177,7 +8910,7 @@ thenby@^1.3.4: resolved "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz" integrity sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ== -through@2.3.8, "through@>=2.2.7 <3", through@^2.3.8: +through@^2.3.8: version "2.3.8" resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -9232,14 +8965,6 @@ toposort@^2.0.2: resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz" integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -9311,11 +9036,6 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tsv@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/tsv/-/tsv-0.2.0.tgz" - integrity sha512-GG6xbOP85giXXom0dS6z9uyDsxktznjpa1AuDlPrIXDqDnbhjr9Vk6Us8iz6U1nENL4CPS2jZDvIjEdaZsmc4Q== - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -9323,11 +9043,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -9579,11 +9294,6 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@3.4.0, uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - uuid@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" @@ -9607,15 +9317,6 @@ vary@^1: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -verror@1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - vue@^3.2.23: version "3.2.36" resolved "https://registry.npmjs.org/vue/-/vue-3.2.36.tgz" From 9a52cdd2e11a296022bde2ff1317945b4c3c181b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 29 Sep 2023 05:29:22 -0700 Subject: [PATCH 209/357] Refactored to use app folder. --- next.config.js | 32 +- package.json | 16 +- src/app/(app)/NavBar.js | 58 + .../layout => app/(app)}/NavBar.module.css | 37 +- src/app/(app)/Shell.tsx | 27 + .../(app)}/console/TestConsole.js | 4 +- .../(app)}/console/TestConsole.module.css | 0 src/app/(app)/console/[[...id]]/page.tsx | 15 + .../(app)}/dashboard/Dashboard.js | 24 +- .../(app)}/dashboard/DashboardEdit.js | 0 .../(app)}/dashboard/DashboardEdit.module.css | 0 .../dashboard/DashboardSettingsButton.js | 0 .../DashboardSettingsButton.module.css | 0 src/app/(app)/dashboard/page.tsx | 10 + .../(app)/layout.module.css} | 0 src/app/(app)/layout.tsx | 20 + .../(app)}/reports/BaseParameters.js | 0 .../(app)}/reports/FieldAddForm.js | 0 .../(app)}/reports/FieldAddForm.module.css | 0 .../(app)}/reports/FieldAggregateForm.js | 0 .../(app)}/reports/FieldFilterForm.js | 0 .../(app)}/reports/FieldFilterForm.module.css | 0 .../(app)}/reports/FieldSelectForm.js | 0 .../(app)}/reports/FieldSelectForm.module.css | 0 .../(app)}/reports/FilterSelectForm.js | 0 .../(app)}/reports/ParameterList.js | 0 .../(app)}/reports/ParameterList.module.css | 0 .../pages => app/(app)}/reports/PopupForm.js | 0 .../(app)}/reports/PopupForm.module.css | 0 .../pages => app/(app)}/reports/Report.js | 10 +- .../(app)/reports/Report.module.css} | 0 .../pages => app/(app)}/reports/ReportBody.js | 2 +- .../(app)}/reports/ReportHeader.js | 4 +- .../(app)}/reports/ReportHeader.module.css | 0 .../pages => app/(app)}/reports/ReportMenu.js | 2 +- src/app/(app)/reports/ReportsHeader.js | 24 + src/app/(app)/reports/ReportsList.js | 37 + .../(app)}/reports/ReportsTable.js | 0 src/app/(app)/reports/[id]/ReportDetails.js | 26 + src/app/(app)/reports/[id]/page.tsx | 14 + .../(app)/reports/create}/ReportTemplates.js | 6 +- .../create}/ReportTemplates.module.css | 0 src/app/(app)/reports/create/page.tsx | 10 + .../reports/event-data/EventDataParameters.js | 2 +- .../event-data/EventDataParameters.module.css | 0 .../reports/event-data/EventDataReport.js | 0 .../reports/event-data/EventDataTable.js | 0 .../(app)}/reports/funnel/FunnelChart.js | 0 .../reports/funnel/FunnelChart.module.css | 0 .../(app)}/reports/funnel/FunnelParameters.js | 2 +- .../(app)}/reports/funnel/FunnelReport.js | 1 + .../reports/funnel/FunnelReport.module.css | 0 .../(app)}/reports/funnel/FunnelTable.js | 0 .../(app)}/reports/funnel/UrlAddForm.js | 0 .../reports/funnel/UrlAddForm.module.css | 0 src/app/(app)/reports/funnel/page.tsx | 10 + .../reports/insights/InsightsParameters.js | 2 +- .../insights/InsightsParameters.module.css | 0 .../(app)}/reports/insights/InsightsReport.js | 1 + .../(app)}/reports/insights/InsightsTable.js | 0 src/app/(app)/reports/insights/page.tsx | 10 + src/app/(app)/reports/page.tsx | 14 + .../reports/retention/RetentionParameters.js | 2 +- .../reports/retention/RetentionReport.js | 1 + .../retention/RetentionReport.module.css | 0 .../reports/retention/RetentionTable.js | 0 .../retention/RetentionTable.module.css | 0 src/app/(app)/reports/retention/page.js | 9 + .../layout => app/(app)/settings}/SideNav.js | 6 +- .../(app)/settings}/SideNav.module.css | 0 .../(app)/settings/layout.module.css} | 9 +- .../(app)/settings/layout.tsx} | 26 +- .../settings/profile/DateRangeSetting.js | 0 .../settings/profile/LanguageSetting.js | 0 .../settings/profile/PasswordChangeButton.js | 2 +- .../settings/profile/PasswordEditForm.js | 0 .../(app)/settings/profile/ProfileHeader.js | 11 + .../settings/profile/ProfileSettings.js} | 13 +- .../(app)}/settings/profile/ThemeSetting.js | 0 .../settings/profile/ThemeSetting.module.css | 0 .../settings/profile/TimezoneSetting.js | 0 src/app/(app)/settings/profile/page.js | 11 + .../(app)}/settings/teams/TeamAddForm.js | 0 .../(app)/settings/teams/TeamDeleteButton.js | 25 + .../(app)}/settings/teams/TeamDeleteForm.js | 0 .../(app)}/settings/teams/TeamJoinForm.js | 0 .../(app)/settings/teams/TeamLeaveButton.js | 35 + .../(app)}/settings/teams/TeamLeaveForm.js | 0 .../settings/teams/TeamWebsiteRemoveButton.js | 0 .../(app)/settings/teams/TeamsAddButton.js | 24 + src/app/(app)/settings/teams/TeamsHeader.js | 24 + .../(app)/settings/teams/TeamsJoinButton.js | 29 + src/app/(app)/settings/teams/TeamsList.js | 19 + src/app/(app)/settings/teams/TeamsTable.js | 46 + .../(app)}/settings/teams/WebsiteTags.js | 0 .../settings/teams/WebsiteTags.module.css | 0 .../teams/[id]}/TeamAddWebsiteForm.js | 2 +- .../settings/teams/[id]}/TeamEditForm.js | 0 .../teams/[id]}/TeamMemberRemoveButton.js | 0 .../(app)/settings/teams/[id]}/TeamMembers.js | 2 +- .../settings/teams/[id]}/TeamMembersTable.js | 0 .../settings/teams/[id]}/TeamSettings.js | 12 +- .../settings/teams/[id]}/TeamWebsites.js | 4 +- .../settings/teams/[id]}/TeamWebsitesTable.js | 2 +- src/app/(app)/settings/teams/[id]/page.js | 9 + src/app/(app)/settings/teams/page.js | 15 + .../(app)}/settings/users/UserAddButton.js | 0 .../(app)}/settings/users/UserAddForm.js | 0 .../(app)/settings/users/UserDeleteButton.js | 27 + .../(app)}/settings/users/UserDeleteForm.js | 0 .../(app)}/settings/users/UserEditForm.js | 0 .../(app)}/settings/users/UserWebsites.js | 2 +- src/app/(app)/settings/users/UsersHeader.js | 16 + src/app/(app)/settings/users/UsersList.js | 25 + src/app/(app)/settings/users/UsersTable.js | 57 + .../settings/users/[id]}/UserSettings.js | 16 +- src/app/(app)/settings/users/[id]/page.js | 9 + src/app/(app)/settings/users/page.tsx | 13 + .../settings/websites/WebsiteAddButton.js | 29 + .../settings/websites/WebsiteAddForm.js | 0 .../settings/websites/WebsiteSettings.js | 25 +- .../(app)/settings/websites/WebsitesHeader.js | 16 + .../(app)/settings/websites/WebsitesList.js | 43 + .../websites/WebsitesList.module.css} | 0 .../(app)}/settings/websites/WebsitesTable.js | 0 .../websites/WebsitesTable.module.css | 0 .../(app)/settings/websites/[id]}/ShareUrl.js | 9 +- .../settings/websites/[id]}/TrackingCode.js | 4 +- .../settings/websites/[id]}/WebsiteData.js | 4 +- .../websites/[id]}/WebsiteDeleteForm.js | 0 .../websites/[id]}/WebsiteEditForm.js | 0 .../websites/[id]}/WebsiteResetForm.js | 0 src/app/(app)/settings/websites/[id]/page.js | 15 + src/app/(app)/settings/websites/page.js | 9 + .../(app)}/websites/WebsiteTableView.js | 0 .../websites/WebsiteTableView.module.css | 0 .../(app)/websites/[id]}/WebsiteChart.js | 0 .../websites/[id]}/WebsiteChart.module.css | 0 .../(app)/websites/[id]}/WebsiteChartList.js | 5 +- .../(app)/websites/[id]/WebsiteDetails.js} | 23 +- .../(app)/websites/[id]}/WebsiteHeader.js | 5 +- .../websites/[id]}/WebsiteHeader.module.css | 0 .../(app)/websites/[id]}/WebsiteMenuView.js | 2 +- .../websites/[id]}/WebsiteMenuView.module.css | 0 .../(app)/websites/[id]}/WebsiteMetricsBar.js | 4 +- .../[id]}/WebsiteMetricsBar.module.css | 0 .../[id]}/event-data/EventDataMetricsBar.js | 0 .../event-data/EventDataMetricsBar.module.css | 0 .../[id]}/event-data/EventDataTable.js | 0 .../[id]}/event-data/EventDataValueTable.js | 0 .../[id]/event-data}/WebsiteEventData.js | 7 +- .../event-data}/WebsiteEventData.module.css | 0 .../(app)/websites/[id]/event-data/page.js | 15 + src/app/(app)/websites/[id]/page.tsx | 9 + .../(app)/websites/[id]/realtime/Realtime.js | 122 ++ .../[id]/realtime/Realtime.module.css} | 0 .../[id]}/realtime/RealtimeCountries.js | 9 +- .../realtime/RealtimeCountries.module.css | 0 .../websites/[id]}/realtime/RealtimeHeader.js | 0 .../[id]}/realtime/RealtimeHeader.module.css | 0 .../websites/[id]}/realtime/RealtimeHome.js | 2 +- .../websites/[id]}/realtime/RealtimeLog.js | 0 .../[id]}/realtime/RealtimeLog.module.css | 1 - .../websites/[id]}/realtime/RealtimePage.js | 12 +- .../websites/[id]}/realtime/RealtimeUrls.js | 0 src/app/(app)/websites/[id]/realtime/page.tsx | 9 + .../websites/[id]/reports/WebsiteReports.js} | 19 +- src/app/(app)/websites/[id]/reports/page.tsx | 9 + src/app/(app)/websites/page.js | 30 + src/app/Providers.tsx | 39 + src/app/layout.tsx | 36 + .../pages => app}/login/LoginForm.js | 3 +- .../pages => app}/login/LoginForm.module.css | 0 .../login/page.module.css} | 3 +- src/app/login/page.tsx | 25 + src/{pages/logout.js => app/logout/page.tsx} | 16 +- src/app/not-found.tsx | 13 + src/app/page.tsx | 6 + .../layout => app/share/[...id]}/Footer.js | 0 .../share/[...id]}/Footer.module.css | 0 src/app/share/[...id]/Header.js | 29 + .../share/[...id]}/Header.module.css | 0 src/app/share/[...id]/page.tsx | 17 + src/{pages/sso.js => app/sso/page.tsx} | 7 +- .../common/{DataTable.js => DataTable.tsx} | 37 +- src/components/common/{Empty.js => Empty.tsx} | 7 +- src/components/common/MobileMenu.js | 4 +- src/components/common/UpdateNotice.js | 5 +- src/components/common/WorldMap.js | 4 +- src/components/hooks/useApi.ts | 4 +- src/components/hooks/useCountryNames.js | 4 +- src/components/hooks/useFilterQuery.js | 16 - src/components/hooks/useFilterQuery.ts | 26 + src/components/hooks/useLanguageNames.js | 4 +- src/components/hooks/useLocale.js | 4 +- src/components/hooks/usePageQuery.js | 26 +- src/components/hooks/useRequireLogin.ts | 6 +- src/components/input/LogoutButton.js | 2 +- src/components/input/ProfileButton.js | 2 +- src/components/input/SettingsButton.js | 4 +- src/components/layout/AppLayout.js | 32 - src/components/layout/Header.js | 31 - src/components/layout/NavBar.js | 63 - src/components/layout/NavGroup.js | 4 +- src/components/layout/Page.module.css | 3 + src/components/layout/{Page.js => Page.tsx} | 15 +- .../layout/{PageHeader.js => PageHeader.tsx} | 10 +- src/components/layout/ReportsLayout.js | 23 - .../layout/ReportsLayout.module.css | 23 - src/components/layout/ShareLayout.js | 15 - src/components/metrics/BrowsersTable.js | 4 +- src/components/metrics/CitiesTable.js | 4 +- src/components/metrics/CountriesTable.js | 7 +- src/components/metrics/DevicesTable.js | 4 +- src/components/metrics/MetricsTable.js | 3 +- src/components/metrics/OSTable.js | 4 +- src/components/metrics/RegionsTable.js | 7 +- src/components/pages/login/LoginLayout.js | 18 - src/components/pages/reports/ReportDetails.js | 17 - src/components/pages/reports/ReportsPage.js | 54 - .../pages/settings/profile/ProfileSettings.js | 17 - .../pages/settings/teams/TeamsList.js | 118 -- .../pages/settings/teams/TeamsTable.js | 111 -- .../pages/settings/users/UsersList.js | 68 -- .../pages/settings/users/UsersTable.js | 93 -- .../pages/settings/websites/WebsitesList.js | 71 -- .../pages/websites/WebsiteEventDataPage.js | 12 - src/components/pages/websites/WebsitesPage.js | 77 -- src/index.ts | 50 +- src/lib/middleware.ts | 23 +- src/pages/404.js | 19 - src/pages/_app.js | 69 -- src/pages/api/auth/login.ts | 3 +- src/pages/console/[[...id]].js | 22 - src/pages/dashboard/index.js | 13 - src/pages/index.js | 12 - src/pages/login.js | 22 - src/pages/reports/[id].js | 24 - src/pages/reports/create.js | 13 - src/pages/reports/funnel.js | 13 - src/pages/reports/index.js | 13 - src/pages/reports/insights.js | 13 - src/pages/reports/retention.js | 13 - src/pages/settings/profile/index.js | 15 - src/pages/settings/teams/[id].js | 31 - src/pages/settings/teams/index.js | 27 - src/pages/settings/users/[id].js | 31 - src/pages/settings/users/index.js | 27 - src/pages/settings/websites/[id].js | 31 - src/pages/settings/websites/index.js | 27 - src/pages/share/[...id].js | 21 - src/pages/websites/[id]/event-data.js | 20 - src/pages/websites/[id]/index.js | 20 - src/pages/websites/[id]/realtime.js | 18 - src/pages/websites/[id]/reports.js | 18 - src/pages/websites/index.js | 13 - tsconfig.json | 9 +- yarn.lock | 1036 ++++++++--------- 258 files changed, 2025 insertions(+), 2258 deletions(-) create mode 100644 src/app/(app)/NavBar.js rename src/{components/layout => app/(app)}/NavBar.module.css (75%) create mode 100644 src/app/(app)/Shell.tsx rename src/{components/pages => app/(app)}/console/TestConsole.js (97%) rename src/{components/pages => app/(app)}/console/TestConsole.module.css (100%) create mode 100644 src/app/(app)/console/[[...id]]/page.tsx rename src/{components/pages => app/(app)}/dashboard/Dashboard.js (79%) rename src/{components/pages => app/(app)}/dashboard/DashboardEdit.js (100%) rename src/{components/pages => app/(app)}/dashboard/DashboardEdit.module.css (100%) rename src/{components/pages => app/(app)}/dashboard/DashboardSettingsButton.js (100%) rename src/{components/pages => app/(app)}/dashboard/DashboardSettingsButton.module.css (100%) create mode 100644 src/app/(app)/dashboard/page.tsx rename src/{components/layout/AppLayout.module.css => app/(app)/layout.module.css} (100%) create mode 100644 src/app/(app)/layout.tsx rename src/{components/pages => app/(app)}/reports/BaseParameters.js (100%) rename src/{components/pages => app/(app)}/reports/FieldAddForm.js (100%) rename src/{components/pages => app/(app)}/reports/FieldAddForm.module.css (100%) rename src/{components/pages => app/(app)}/reports/FieldAggregateForm.js (100%) rename src/{components/pages => app/(app)}/reports/FieldFilterForm.js (100%) rename src/{components/pages => app/(app)}/reports/FieldFilterForm.module.css (100%) rename src/{components/pages => app/(app)}/reports/FieldSelectForm.js (100%) rename src/{components/pages => app/(app)}/reports/FieldSelectForm.module.css (100%) rename src/{components/pages => app/(app)}/reports/FilterSelectForm.js (100%) rename src/{components/pages => app/(app)}/reports/ParameterList.js (100%) rename src/{components/pages => app/(app)}/reports/ParameterList.module.css (100%) rename src/{components/pages => app/(app)}/reports/PopupForm.js (100%) rename src/{components/pages => app/(app)}/reports/PopupForm.module.css (100%) rename src/{components/pages => app/(app)}/reports/Report.js (69%) rename src/{components/pages/reports/reports.module.css => app/(app)/reports/Report.module.css} (100%) rename src/{components/pages => app/(app)}/reports/ReportBody.js (75%) rename src/{components/pages => app/(app)}/reports/ReportHeader.js (96%) rename src/{components/pages => app/(app)}/reports/ReportHeader.module.css (100%) rename src/{components/pages => app/(app)}/reports/ReportMenu.js (75%) create mode 100644 src/app/(app)/reports/ReportsHeader.js create mode 100644 src/app/(app)/reports/ReportsList.js rename src/{components/pages => app/(app)}/reports/ReportsTable.js (100%) create mode 100644 src/app/(app)/reports/[id]/ReportDetails.js create mode 100644 src/app/(app)/reports/[id]/page.tsx rename src/{components/pages/reports => app/(app)/reports/create}/ReportTemplates.js (96%) rename src/{components/pages/reports => app/(app)/reports/create}/ReportTemplates.module.css (100%) create mode 100644 src/app/(app)/reports/create/page.tsx rename src/{components/pages => app/(app)}/reports/event-data/EventDataParameters.js (98%) rename src/{components/pages => app/(app)}/reports/event-data/EventDataParameters.module.css (100%) rename src/{components/pages => app/(app)}/reports/event-data/EventDataReport.js (100%) rename src/{components/pages => app/(app)}/reports/event-data/EventDataTable.js (100%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelChart.js (100%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelChart.module.css (100%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelParameters.js (97%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelReport.js (98%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelReport.module.css (100%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelTable.js (100%) rename src/{components/pages => app/(app)}/reports/funnel/UrlAddForm.js (100%) rename src/{components/pages => app/(app)}/reports/funnel/UrlAddForm.module.css (100%) create mode 100644 src/app/(app)/reports/funnel/page.tsx rename src/{components/pages => app/(app)}/reports/insights/InsightsParameters.js (98%) rename src/{components/pages => app/(app)}/reports/insights/InsightsParameters.module.css (100%) rename src/{components/pages => app/(app)}/reports/insights/InsightsReport.js (98%) rename src/{components/pages => app/(app)}/reports/insights/InsightsTable.js (100%) create mode 100644 src/app/(app)/reports/insights/page.tsx create mode 100644 src/app/(app)/reports/page.tsx rename src/{components/pages => app/(app)}/reports/retention/RetentionParameters.js (95%) rename src/{components/pages => app/(app)}/reports/retention/RetentionReport.js (98%) rename src/{components/pages => app/(app)}/reports/retention/RetentionReport.module.css (100%) rename src/{components/pages => app/(app)}/reports/retention/RetentionTable.js (100%) rename src/{components/pages => app/(app)}/reports/retention/RetentionTable.module.css (100%) create mode 100644 src/app/(app)/reports/retention/page.js rename src/{components/layout => app/(app)/settings}/SideNav.js (85%) rename src/{components/layout => app/(app)/settings}/SideNav.module.css (100%) rename src/{components/layout/SettingsLayout.module.css => app/(app)/settings/layout.module.css} (67%) rename src/{components/layout/SettingsLayout.js => app/(app)/settings/layout.tsx} (64%) rename src/{components/pages => app/(app)}/settings/profile/DateRangeSetting.js (100%) rename src/{components/pages => app/(app)}/settings/profile/LanguageSetting.js (100%) rename src/{components/pages => app/(app)}/settings/profile/PasswordChangeButton.js (91%) rename src/{components/pages => app/(app)}/settings/profile/PasswordEditForm.js (100%) create mode 100644 src/app/(app)/settings/profile/ProfileHeader.js rename src/{components/pages/settings/profile/ProfileDetails.js => app/(app)/settings/profile/ProfileSettings.js} (79%) rename src/{components/pages => app/(app)}/settings/profile/ThemeSetting.js (100%) rename src/{components/pages => app/(app)}/settings/profile/ThemeSetting.module.css (100%) rename src/{components/pages => app/(app)}/settings/profile/TimezoneSetting.js (100%) create mode 100644 src/app/(app)/settings/profile/page.js rename src/{components/pages => app/(app)}/settings/teams/TeamAddForm.js (100%) create mode 100644 src/app/(app)/settings/teams/TeamDeleteButton.js rename src/{components/pages => app/(app)}/settings/teams/TeamDeleteForm.js (100%) rename src/{components/pages => app/(app)}/settings/teams/TeamJoinForm.js (100%) create mode 100644 src/app/(app)/settings/teams/TeamLeaveButton.js rename src/{components/pages => app/(app)}/settings/teams/TeamLeaveForm.js (100%) rename src/{components/pages => app/(app)}/settings/teams/TeamWebsiteRemoveButton.js (100%) create mode 100644 src/app/(app)/settings/teams/TeamsAddButton.js create mode 100644 src/app/(app)/settings/teams/TeamsHeader.js create mode 100644 src/app/(app)/settings/teams/TeamsJoinButton.js create mode 100644 src/app/(app)/settings/teams/TeamsList.js create mode 100644 src/app/(app)/settings/teams/TeamsTable.js rename src/{components/pages => app/(app)}/settings/teams/WebsiteTags.js (100%) rename src/{components/pages => app/(app)}/settings/teams/WebsiteTags.module.css (100%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamAddWebsiteForm.js (98%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamEditForm.js (100%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamMemberRemoveButton.js (100%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamMembers.js (94%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamMembersTable.js (100%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamSettings.js (92%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamWebsites.js (92%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamWebsitesTable.js (96%) create mode 100644 src/app/(app)/settings/teams/[id]/page.js create mode 100644 src/app/(app)/settings/teams/page.js rename src/{components/pages => app/(app)}/settings/users/UserAddButton.js (100%) rename src/{components/pages => app/(app)}/settings/users/UserAddForm.js (100%) create mode 100644 src/app/(app)/settings/users/UserDeleteButton.js rename src/{components/pages => app/(app)}/settings/users/UserDeleteForm.js (100%) rename src/{components/pages => app/(app)}/settings/users/UserEditForm.js (100%) rename src/{components/pages => app/(app)}/settings/users/UserWebsites.js (92%) create mode 100644 src/app/(app)/settings/users/UsersHeader.js create mode 100644 src/app/(app)/settings/users/UsersList.js create mode 100644 src/app/(app)/settings/users/UsersTable.js rename src/{components/pages/settings/users => app/(app)/settings/users/[id]}/UserSettings.js (84%) create mode 100644 src/app/(app)/settings/users/[id]/page.js create mode 100644 src/app/(app)/settings/users/page.tsx create mode 100644 src/app/(app)/settings/websites/WebsiteAddButton.js rename src/{components/pages => app/(app)}/settings/websites/WebsiteAddForm.js (100%) rename src/{components/pages => app/(app)}/settings/websites/WebsiteSettings.js (80%) create mode 100644 src/app/(app)/settings/websites/WebsitesHeader.js create mode 100644 src/app/(app)/settings/websites/WebsitesList.js rename src/{components/pages/websites/WebsiteList.module.css => app/(app)/settings/websites/WebsitesList.module.css} (100%) rename src/{components/pages => app/(app)}/settings/websites/WebsitesTable.js (100%) rename src/{components/pages => app/(app)}/settings/websites/WebsitesTable.module.css (100%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/ShareUrl.js (91%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/TrackingCode.js (82%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/WebsiteData.js (89%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/WebsiteDeleteForm.js (100%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/WebsiteEditForm.js (100%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/WebsiteResetForm.js (100%) create mode 100644 src/app/(app)/settings/websites/[id]/page.js create mode 100644 src/app/(app)/settings/websites/page.js rename src/{components/pages => app/(app)}/websites/WebsiteTableView.js (100%) rename src/{components/pages => app/(app)}/websites/WebsiteTableView.module.css (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteChart.js (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteChart.module.css (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteChartList.js (90%) rename src/{components/pages/websites/WebsiteDetailsPage.js => app/(app)/websites/[id]/WebsiteDetails.js} (74%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteHeader.js (95%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteHeader.module.css (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteMenuView.js (98%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteMenuView.module.css (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteMetricsBar.js (97%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteMetricsBar.module.css (100%) rename src/{components/pages => app/(app)/websites/[id]}/event-data/EventDataMetricsBar.js (100%) rename src/{components/pages => app/(app)/websites/[id]}/event-data/EventDataMetricsBar.module.css (100%) rename src/{components/pages => app/(app)/websites/[id]}/event-data/EventDataTable.js (100%) rename src/{components/pages => app/(app)/websites/[id]}/event-data/EventDataValueTable.js (100%) rename src/{components/pages/websites => app/(app)/websites/[id]/event-data}/WebsiteEventData.js (83%) rename src/{components/pages/websites => app/(app)/websites/[id]/event-data}/WebsiteEventData.module.css (100%) create mode 100644 src/app/(app)/websites/[id]/event-data/page.js create mode 100644 src/app/(app)/websites/[id]/page.tsx create mode 100644 src/app/(app)/websites/[id]/realtime/Realtime.js rename src/{components/pages/realtime/RealtimePage.module.css => app/(app)/websites/[id]/realtime/Realtime.module.css} (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeCountries.js (81%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeCountries.module.css (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeHeader.js (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeHeader.module.css (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeHome.js (95%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeLog.js (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeLog.module.css (98%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimePage.js (90%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeUrls.js (100%) create mode 100644 src/app/(app)/websites/[id]/realtime/page.tsx rename src/{components/pages/websites/WebsiteReportsPage.js => app/(app)/websites/[id]/reports/WebsiteReports.js} (79%) create mode 100644 src/app/(app)/websites/[id]/reports/page.tsx create mode 100644 src/app/(app)/websites/page.js create mode 100644 src/app/Providers.tsx create mode 100644 src/app/layout.tsx rename src/{components/pages => app}/login/LoginForm.js (96%) rename src/{components/pages => app}/login/LoginForm.module.css (100%) rename src/{components/pages/login/LoginLayout.module.css => app/login/page.module.css} (76%) create mode 100644 src/app/login/page.tsx rename src/{pages/logout.js => app/logout/page.tsx} (73%) create mode 100644 src/app/not-found.tsx create mode 100644 src/app/page.tsx rename src/{components/layout => app/share/[...id]}/Footer.js (100%) rename src/{components/layout => app/share/[...id]}/Footer.module.css (100%) create mode 100644 src/app/share/[...id]/Header.js rename src/{components/layout => app/share/[...id]}/Header.module.css (100%) create mode 100644 src/app/share/[...id]/page.tsx rename src/{pages/sso.js => app/sso/page.tsx} (71%) rename src/components/common/{DataTable.js => DataTable.tsx} (70%) rename src/components/common/{Empty.js => Empty.tsx} (72%) delete mode 100644 src/components/hooks/useFilterQuery.js create mode 100644 src/components/hooks/useFilterQuery.ts delete mode 100644 src/components/layout/AppLayout.js delete mode 100644 src/components/layout/Header.js delete mode 100644 src/components/layout/NavBar.js rename src/components/layout/{Page.js => Page.tsx} (69%) rename src/components/layout/{PageHeader.js => PageHeader.tsx} (58%) delete mode 100644 src/components/layout/ReportsLayout.js delete mode 100644 src/components/layout/ReportsLayout.module.css delete mode 100644 src/components/layout/ShareLayout.js delete mode 100644 src/components/pages/login/LoginLayout.js delete mode 100644 src/components/pages/reports/ReportDetails.js delete mode 100644 src/components/pages/reports/ReportsPage.js delete mode 100644 src/components/pages/settings/profile/ProfileSettings.js delete mode 100644 src/components/pages/settings/teams/TeamsList.js delete mode 100644 src/components/pages/settings/teams/TeamsTable.js delete mode 100644 src/components/pages/settings/users/UsersList.js delete mode 100644 src/components/pages/settings/users/UsersTable.js delete mode 100644 src/components/pages/settings/websites/WebsitesList.js delete mode 100644 src/components/pages/websites/WebsiteEventDataPage.js delete mode 100644 src/components/pages/websites/WebsitesPage.js delete mode 100644 src/pages/404.js delete mode 100644 src/pages/_app.js delete mode 100644 src/pages/console/[[...id]].js delete mode 100644 src/pages/dashboard/index.js delete mode 100644 src/pages/index.js delete mode 100644 src/pages/login.js delete mode 100644 src/pages/reports/[id].js delete mode 100644 src/pages/reports/create.js delete mode 100644 src/pages/reports/funnel.js delete mode 100644 src/pages/reports/index.js delete mode 100644 src/pages/reports/insights.js delete mode 100644 src/pages/reports/retention.js delete mode 100644 src/pages/settings/profile/index.js delete mode 100644 src/pages/settings/teams/[id].js delete mode 100644 src/pages/settings/teams/index.js delete mode 100644 src/pages/settings/users/[id].js delete mode 100644 src/pages/settings/users/index.js delete mode 100644 src/pages/settings/websites/[id].js delete mode 100644 src/pages/settings/websites/index.js delete mode 100644 src/pages/share/[...id].js delete mode 100644 src/pages/websites/[id]/event-data.js delete mode 100644 src/pages/websites/[id]/index.js delete mode 100644 src/pages/websites/[id]/realtime.js delete mode 100644 src/pages/websites/[id]/reports.js delete mode 100644 src/pages/websites/index.js diff --git a/next.config.js b/next.config.js index cc3cde7c6..2ef1c05e5 100644 --- a/next.config.js +++ b/next.config.js @@ -6,7 +6,7 @@ const pkg = require('./package.json'); const contentSecurityPolicy = ` default-src 'self'; img-src *; - script-src 'self' 'unsafe-eval'; + script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' api.umami.is; frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS}; @@ -74,16 +74,20 @@ if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN }); } +const basePath = process.env.BASE_PATH; + +/** @type {import('next').NextConfig} */ const config = { env: { - cloudMode: process.env.CLOUD_MODE, + basePath: basePath || '', + cloudMode: !!process.env.CLOUD_MODE, cloudUrl: process.env.CLOUD_URL, configUrl: '/config', currentVersion: pkg.version, defaultLocale: process.env.DEFAULT_LOCALE, isProduction: process.env.NODE_ENV === 'production', }, - basePath: process.env.BASE_PATH, + basePath, output: 'standalone', eslint: { ignoreDuringBuilds: true, @@ -92,11 +96,23 @@ const config = { ignoreBuildErrors: true, }, webpack(config) { - config.module.rules.push({ - test: /\.svg$/, - issuer: /\.{js|jsx|ts|tsx}$/, - use: ['@svgr/webpack'], - }); + const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg')); + + config.module.rules.push( + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, + }, + { + test: /\.svg$/i, + issuer: fileLoaderRule.issuer, + resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, + use: ['@svgr/webpack'], + }, + ); + + fileLoaderRule.exclude = /\.svg$/i; config.resolve.alias['public'] = path.resolve('./public'); diff --git a/package.json b/package.json index 79960eb23..5b005f66a 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@fontsource/inter": "^4.5.15", "@prisma/client": "5.3.1", "@tanstack/react-query": "^4.33.0", - "@umami/prisma-client": "^0.2.0", + "@umami/prisma-client": "^0.3.0", "@umami/redis-client": "^0.15.0", "chalk": "^4.1.1", "chart.js": "^4.2.1", @@ -91,7 +91,7 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.5.2", + "next": "13.5.3", "next-basics": "^0.36.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", @@ -100,7 +100,7 @@ "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", - "react-intl": "^5.24.7", + "react-intl": "^6.4.7", "react-simple-maps": "^2.3.0", "react-spring": "^9.4.4", "react-use-measure": "^2.0.4", @@ -123,12 +123,12 @@ "@rollup/plugin-node-resolve": "^15.2.0", "@rollup/plugin-replace": "^5.0.2", "@svgr/rollup": "^8.1.0", - "@svgr/webpack": "^6.2.1", + "@svgr/webpack": "^8.1.0", "@types/node": "^18.11.9", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.8", - "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", + "@typescript-eslint/eslint-plugin": "^6.7.3", + "@typescript-eslint/parser": "^6.7.3", "cross-env": "^7.0.3", "esbuild": "^0.17.17", "eslint": "^8.33.0", @@ -138,8 +138,8 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", "extract-react-intl-messages": "^4.1.1", - "husky": "^7.0.0", - "lint-staged": "^11.0.0", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", "postcss": "^8.4.21", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^15.1.0", diff --git a/src/app/(app)/NavBar.js b/src/app/(app)/NavBar.js new file mode 100644 index 000000000..211adf5fb --- /dev/null +++ b/src/app/(app)/NavBar.js @@ -0,0 +1,58 @@ +'use client'; +import { Icon, Text } from 'react-basics'; +import Link from 'next/link'; +import classNames from 'classnames'; +import Icons from 'components/icons'; +import ThemeButton from 'components/input/ThemeButton'; +import LanguageButton from 'components/input/LanguageButton'; +import ProfileButton from 'components/input/ProfileButton'; +import useMessages from 'components/hooks/useMessages'; +import HamburgerButton from 'components/common/HamburgerButton'; +import { usePathname } from 'next/navigation'; +import styles from './NavBar.module.css'; + +export function NavBar() { + const pathname = usePathname(); + const { formatMessage, labels } = useMessages(); + + const links = [ + { label: formatMessage(labels.dashboard), url: '/dashboard' }, + { label: formatMessage(labels.websites), url: '/websites' }, + { label: formatMessage(labels.reports), url: '/reports' }, + { label: formatMessage(labels.settings), url: '/settings' }, + ].filter(n => n); + + return ( +
+
+ + + + umami +
+
+ {links.map(({ url, label }) => { + return ( + + {label} + + ); + })} +
+
+ + + +
+
+ +
+
+ ); +} + +export default NavBar; diff --git a/src/components/layout/NavBar.module.css b/src/app/(app)/NavBar.module.css similarity index 75% rename from src/components/layout/NavBar.module.css rename to src/app/(app)/NavBar.module.css index dd5085a03..fd022ecab 100644 --- a/src/components/layout/NavBar.module.css +++ b/src/app/(app)/NavBar.module.css @@ -1,7 +1,7 @@ .navbar { + display: grid; + grid-template-columns: max-content 1fr 1fr; position: relative; - display: flex; - flex-direction: row; align-items: center; height: 60px; background: var(--base75); @@ -9,17 +9,6 @@ padding: 0 20px; } -.left, -.right { - display: flex; - flex-direction: row; - align-items: center; -} - -.right { - justify-content: flex-end; -} - .logo { display: flex; flex-direction: row; @@ -35,29 +24,24 @@ flex-direction: row; gap: 30px; padding: 0 40px; - flex: 1; font-weight: 700; + max-height: 60px; } -.links a { - display: flex; - align-items: center; - gap: 10px; - line-height: 60px; +.links a, +.links a:active, +.links a:visited { color: var(--font-color200); + line-height: 60px; border-bottom: 2px solid transparent; } -.links span { - white-space: nowrap; -} - .links a:hover { color: var(--font-color100); border-bottom: 2px solid var(--primary400); } -.links .selected { +.links a.selected { color: var(--font-color100); border-bottom: 2px solid var(--primary400); } @@ -68,7 +52,6 @@ flex-direction: row; align-items: center; justify-content: flex-end; - min-width: 0; } .mobile { @@ -76,6 +59,10 @@ } @media only screen and (max-width: 768px) { + .navbar { + grid-template-columns: repeat(2, 1fr); + } + .links, .actions { display: none; diff --git a/src/app/(app)/Shell.tsx b/src/app/(app)/Shell.tsx new file mode 100644 index 000000000..980abb621 --- /dev/null +++ b/src/app/(app)/Shell.tsx @@ -0,0 +1,27 @@ +'use client'; +import Script from 'next/script'; +import { usePathname } from 'next/navigation'; +import UpdateNotice from 'components/common/UpdateNotice'; +import { useRequireLogin, useConfig } from 'components/hooks'; + +export function Shell({ children }) { + const { user } = useRequireLogin(); + const config = useConfig(); + const pathname = usePathname(); + + if (!user || !config) { + return null; + } + + return ( + <> + {children} + + {process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && ( + `; diff --git a/src/components/pages/settings/websites/WebsiteData.js b/src/app/(app)/settings/websites/[id]/WebsiteData.js similarity index 89% rename from src/components/pages/settings/websites/WebsiteData.js rename to src/app/(app)/settings/websites/[id]/WebsiteData.js index 08d6702e1..07dc92575 100644 --- a/src/components/pages/settings/websites/WebsiteData.js +++ b/src/app/(app)/settings/websites/[id]/WebsiteData.js @@ -1,6 +1,6 @@ import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; -import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm'; -import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm'; +import WebsiteDeleteForm from './WebsiteDeleteForm'; +import WebsiteResetForm from './WebsiteResetForm'; import useMessages from 'components/hooks/useMessages'; export function WebsiteData({ websiteId, onSave }) { diff --git a/src/components/pages/settings/websites/WebsiteDeleteForm.js b/src/app/(app)/settings/websites/[id]/WebsiteDeleteForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteDeleteForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteDeleteForm.js diff --git a/src/components/pages/settings/websites/WebsiteEditForm.js b/src/app/(app)/settings/websites/[id]/WebsiteEditForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteEditForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteEditForm.js diff --git a/src/components/pages/settings/websites/WebsiteResetForm.js b/src/app/(app)/settings/websites/[id]/WebsiteResetForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteResetForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteResetForm.js diff --git a/src/app/(app)/settings/websites/[id]/page.js b/src/app/(app)/settings/websites/[id]/page.js new file mode 100644 index 000000000..bdf3b076f --- /dev/null +++ b/src/app/(app)/settings/websites/[id]/page.js @@ -0,0 +1,15 @@ +import WebsiteSettings from '../WebsiteSettings'; + +async function getDisabled() { + return !!process.env.CLOUD_MODE; +} + +export default async function WebsiteSettingsPage({ params }) { + const disabled = await getDisabled(); + + if (!params.id || disabled) { + return null; + } + + return ; +} diff --git a/src/app/(app)/settings/websites/page.js b/src/app/(app)/settings/websites/page.js new file mode 100644 index 000000000..ade3e3adf --- /dev/null +++ b/src/app/(app)/settings/websites/page.js @@ -0,0 +1,9 @@ +import WebsitesList from 'app/(app)/settings/websites/WebsitesList'; + +export default function () { + if (process.env.cloudMode) { + return null; + } + + return ; +} diff --git a/src/components/pages/websites/WebsiteTableView.js b/src/app/(app)/websites/WebsiteTableView.js similarity index 100% rename from src/components/pages/websites/WebsiteTableView.js rename to src/app/(app)/websites/WebsiteTableView.js diff --git a/src/components/pages/websites/WebsiteTableView.module.css b/src/app/(app)/websites/WebsiteTableView.module.css similarity index 100% rename from src/components/pages/websites/WebsiteTableView.module.css rename to src/app/(app)/websites/WebsiteTableView.module.css diff --git a/src/components/pages/websites/WebsiteChart.js b/src/app/(app)/websites/[id]/WebsiteChart.js similarity index 100% rename from src/components/pages/websites/WebsiteChart.js rename to src/app/(app)/websites/[id]/WebsiteChart.js diff --git a/src/components/pages/websites/WebsiteChart.module.css b/src/app/(app)/websites/[id]/WebsiteChart.module.css similarity index 100% rename from src/components/pages/websites/WebsiteChart.module.css rename to src/app/(app)/websites/[id]/WebsiteChart.module.css diff --git a/src/components/pages/websites/WebsiteChartList.js b/src/app/(app)/websites/[id]/WebsiteChartList.js similarity index 90% rename from src/components/pages/websites/WebsiteChartList.js rename to src/app/(app)/websites/[id]/WebsiteChartList.js index 56cbe157b..23764dbb8 100644 --- a/src/components/pages/websites/WebsiteChartList.js +++ b/src/app/(app)/websites/[id]/WebsiteChartList.js @@ -2,9 +2,8 @@ import { Button, Text, Icon } from 'react-basics'; import { useMemo } from 'react'; import { firstBy } from 'thenby'; import Link from 'next/link'; -import WebsiteChart from 'components/pages/websites/WebsiteChart'; +import WebsiteChart from './WebsiteChart'; import useDashboard from 'store/dashboard'; -import styles from './WebsiteList.module.css'; import WebsiteHeader from './WebsiteHeader'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { useMessages, useLocale } from 'components/hooks'; @@ -27,7 +26,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
{ordered.map(({ id }, index) => { return index < limit ? ( -
+
- - - - {({ data }) => } - - - ); -} - -export default ReportsPage; diff --git a/src/components/pages/settings/profile/ProfileSettings.js b/src/components/pages/settings/profile/ProfileSettings.js deleted file mode 100644 index a217e52ca..000000000 --- a/src/components/pages/settings/profile/ProfileSettings.js +++ /dev/null @@ -1,17 +0,0 @@ -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import ProfileDetails from './ProfileDetails'; -import useMessages from 'components/hooks/useMessages'; - -export function ProfileSettings() { - const { formatMessage, labels } = useMessages(); - - return ( - - - - - ); -} - -export default ProfileSettings; diff --git a/src/components/pages/settings/teams/TeamsList.js b/src/components/pages/settings/teams/TeamsList.js deleted file mode 100644 index 76a87b0cc..000000000 --- a/src/components/pages/settings/teams/TeamsList.js +++ /dev/null @@ -1,118 +0,0 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import Icons from 'components/icons'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import TeamAddForm from 'components/pages/settings/teams/TeamAddForm'; -import TeamsTable from 'components/pages/settings/teams/TeamsTable'; -import useApi from 'components/hooks/useApi'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { useState } from 'react'; -import { Button, Flexbox, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import TeamJoinForm from './TeamJoinForm'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function TeamsList() { - const { user } = useUser(); - const { formatMessage, labels, messages } = useMessages(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - const [update, setUpdate] = useState(0); - - const { get, useQuery } = useApi(); - const { data, isLoading, error } = useQuery(['teams', update, filter, page, pageSize], () => { - return get(`/teams`, { - filter, - page, - pageSize, - }); - }); - - const hasData = data && data?.data.length !== 0; - const isFiltered = filter; - - const { showToast } = useToasts(); - - const handleSave = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const handleJoin = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const handleDelete = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const joinButton = ( - - - - {close => } - - - ); - - const createButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - - ); - - return ( - - - {(hasData || isFiltered) && ( - - {joinButton} - {createButton} - - )} - - - {(hasData || isFiltered) && ( - - )} - - {!hasData && !isFiltered && ( - - - {joinButton} - {createButton} - - - )} - - ); -} - -export default TeamsList; diff --git a/src/components/pages/settings/teams/TeamsTable.js b/src/components/pages/settings/teams/TeamsTable.js deleted file mode 100644 index e17107830..000000000 --- a/src/components/pages/settings/teams/TeamsTable.js +++ /dev/null @@ -1,111 +0,0 @@ -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'components/hooks/useLocale'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import Link from 'next/link'; -import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import TeamDeleteForm from './TeamDeleteForm'; -import TeamLeaveForm from './TeamLeaveForm'; - -export function TeamsTable({ - data = { data: [] }, - onDelete, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - const { dir } = useLocale(); - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'owner', label: formatMessage(labels.owner) }, - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'owner') { - return row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username; - } - return data[key]; - }; - - return ( - - {row => { - const { id, teamUser } = row; - const owner = teamUser.find(({ role }) => role === ROLES.teamOwner); - const showDelete = user.id === owner?.userId; - - return ( - <> - - - - {showDelete && ( - - - - {close => ( - - )} - - - )} - {!showDelete && ( - - - - {close => ( - - )} - - - )} - - ); - }} - - ); -} - -export default TeamsTable; diff --git a/src/components/pages/settings/users/UsersList.js b/src/components/pages/settings/users/UsersList.js deleted file mode 100644 index 0bc8612e5..000000000 --- a/src/components/pages/settings/users/UsersList.js +++ /dev/null @@ -1,68 +0,0 @@ -import { useToasts } from 'react-basics'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import UsersTable from './UsersTable'; -import UserAddButton from './UserAddButton'; -import useApi from 'components/hooks/useApi'; -import useUser from 'components/hooks/useUser'; -import useMessages from 'components/hooks/useMessages'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function UsersList() { - const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - - const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery( - ['user', filter, page, pageSize], - () => - get(`/users`, { - filter, - page, - pageSize, - }), - { - enabled: !!user, - }, - ); - const { showToast } = useToasts(); - const hasData = data && data.length !== 0; - - const handleSave = () => { - refetch().then(() => showToast({ message: formatMessage(messages.saved), variant: 'success' })); - }; - - const handleDelete = () => { - refetch().then(() => - showToast({ message: formatMessage(messages.userDeleted), variant: 'success' }), - ); - }; - - return ( - - - - - {(hasData || filter) && ( - - )} - {!hasData && !filter && ( - - - - )} - - ); -} - -export default UsersList; diff --git a/src/components/pages/settings/users/UsersTable.js b/src/components/pages/settings/users/UsersTable.js deleted file mode 100644 index 1a93710d0..000000000 --- a/src/components/pages/settings/users/UsersTable.js +++ /dev/null @@ -1,93 +0,0 @@ -import { Button, Text, Icon, Icons, ModalTrigger, Modal } from 'react-basics'; -import { formatDistance } from 'date-fns'; -import Link from 'next/link'; -import useUser from 'components/hooks/useUser'; -import UserDeleteForm from './UserDeleteForm'; -import { ROLES } from 'lib/constants'; -import useMessages from 'components/hooks/useMessages'; -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'components/hooks/useLocale'; - -export function UsersTable({ - data = { data: [] }, - onDelete, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - const { dateLocale } = useLocale(); - - const columns = [ - { name: 'username', label: formatMessage(labels.username) }, - { name: 'role', label: formatMessage(labels.role) }, - { name: 'created', label: formatMessage(labels.created) }, - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'created') { - return formatDistance(new Date(row.createdAt), new Date(), { - addSuffix: true, - locale: dateLocale, - }); - } - if (key === 'role') { - return formatMessage( - labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown, - ); - } - return data[key]; - }; - - return ( - - {row => { - return ( - <> - - - - - - - {close => ( - - )} - - - - ); - }} - - ); -} - -export default UsersTable; diff --git a/src/components/pages/settings/websites/WebsitesList.js b/src/components/pages/settings/websites/WebsitesList.js deleted file mode 100644 index 0dd3aa775..000000000 --- a/src/components/pages/settings/websites/WebsitesList.js +++ /dev/null @@ -1,71 +0,0 @@ -import PageHeader from 'components/layout/PageHeader'; -import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; -import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import useApi from 'components/hooks/useApi'; -import DataTable from 'components/common/DataTable'; -import useFilterQuery from 'components/hooks/useFilterQuery'; - -export function WebsitesList({ - showTeam, - showEditButton = true, - showHeader = true, - includeTeams, - onlyTeams, -}) { - const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); - const { get } = useApi(); - const filterQuery = useFilterQuery( - ['websites', { includeTeams, onlyTeams }], - params => { - return get(`/users/${user?.id}/websites`, { - includeTeams, - onlyTeams, - ...params, - }); - }, - { enabled: !!user }, - ); - const { refetch } = filterQuery; - const { showToast } = useToasts(); - - const handleSave = async () => { - await refetch(); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const addButton = ( - - - - {close => } - - - ); - - return ( - <> - {showHeader && ( - - {user.role !== ROLES.viewOnly && addButton} - - )} - - {({ data }) => ( - - )} - - - ); -} - -export default WebsitesList; diff --git a/src/components/pages/websites/WebsiteEventDataPage.js b/src/components/pages/websites/WebsiteEventDataPage.js deleted file mode 100644 index 08acafb58..000000000 --- a/src/components/pages/websites/WebsiteEventDataPage.js +++ /dev/null @@ -1,12 +0,0 @@ -import Page from 'components/layout/Page'; -import WebsiteHeader from './WebsiteHeader'; -import WebsiteEventData from './WebsiteEventData'; - -export default function WebsiteEventDataPage({ websiteId }) { - return ( - - - - - ); -} diff --git a/src/components/pages/websites/WebsitesPage.js b/src/components/pages/websites/WebsitesPage.js deleted file mode 100644 index 4c1ee4091..000000000 --- a/src/components/pages/websites/WebsitesPage.js +++ /dev/null @@ -1,77 +0,0 @@ -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; -import WebsiteList from 'components/pages/settings/websites/WebsitesList'; -import { useMessages } from 'components/hooks'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { useState } from 'react'; -import { - Button, - Icon, - Icons, - Item, - Modal, - ModalTrigger, - Tabs, - Text, - useToasts, -} from 'react-basics'; - -const TABS = { - myWebsites: 'my-websites', - teamWebsites: 'team-websites', -}; - -export function WebsitesPage() { - const { formatMessage, labels, messages } = useMessages(); - const [tab, setTab] = useState(TABS.myWebsites); - const { user } = useUser(); - const { showToast } = useToasts(); - const cloudMode = Boolean(process.env.cloudMode); - - const handleSave = () => { - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const addButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - - ); - - return ( - - {!cloudMode && addButton} - - {formatMessage(labels.myWebsites)} - {formatMessage(labels.teamWebsites)} - - {tab === TABS.myWebsites && ( - - )} - {tab === TABS.teamWebsites && ( - - )} - - ); -} - -export default WebsitesPage; diff --git a/src/index.ts b/src/index.ts index f2ef13cab..72fe733b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,29 +89,29 @@ export * from 'components/hooks/useUser'; export * from 'components/hooks/useWebsite'; export * from 'components/hooks/useWebsiteReports'; -export * from 'components/pages/settings/teams/TeamAddForm'; -export * from 'components/pages/settings/teams/TeamAddWebsiteForm'; -export * from 'components/pages/settings/teams/TeamDeleteForm'; -export * from 'components/pages/settings/teams/TeamEditForm'; -export * from 'components/pages/settings/teams/TeamJoinForm'; -export * from 'components/pages/settings/teams/TeamLeaveForm'; -export * from 'components/pages/settings/teams/TeamMemberRemoveButton'; -export * from 'components/pages/settings/teams/TeamMembers'; -export * from 'components/pages/settings/teams/TeamMembersTable'; -export * from 'components/pages/settings/teams/TeamSettings'; -export * from 'components/pages/settings/teams/TeamsList'; -export * from 'components/pages/settings/teams/TeamsTable'; -export * from 'components/pages/settings/teams/TeamWebsiteRemoveButton'; -export * from 'components/pages/settings/teams/TeamWebsites'; -export * from 'components/pages/settings/teams/TeamWebsitesTable'; -export * from 'components/pages/settings/teams/WebsiteTags'; +export * from 'app/(app)/settings/teams/TeamAddForm'; +export * from 'app/(app)/settings/teams/[id]/TeamAddWebsiteForm'; +export * from 'app/(app)/settings/teams/TeamDeleteForm'; +export * from 'app/(app)/settings/teams/[id]/TeamEditForm'; +export * from 'app/(app)/settings/teams/TeamJoinForm'; +export * from 'app/(app)/settings/teams/TeamLeaveForm'; +export * from 'app/(app)/settings/teams/[id]/TeamMemberRemoveButton'; +export * from 'app/(app)/settings/teams/[id]/TeamMembers'; +export * from 'app/(app)/settings/teams/[id]/TeamMembersTable'; +export * from 'app/(app)/settings/teams/[id]/TeamSettings'; +export * from 'app/(app)/settings/teams/TeamsList'; +export * from 'app/(app)/settings/teams/TeamsTable'; +export * from 'app/(app)/settings/teams/TeamWebsiteRemoveButton'; +export * from 'app/(app)/settings/teams/[id]/TeamWebsites'; +export * from 'app/(app)/settings/teams/[id]/TeamWebsitesTable'; +export * from 'app/(app)/settings/teams/WebsiteTags'; -export * from 'components/pages/settings/websites/ShareUrl'; -export * from 'components/pages/settings/websites/TrackingCode'; -export * from 'components/pages/settings/websites/WebsiteAddForm'; -export * from 'components/pages/settings/websites/WebsiteDeleteForm'; -export * from 'components/pages/settings/websites/WebsiteEditForm'; -export * from 'components/pages/settings/websites/WebsiteResetForm'; -export * from 'components/pages/settings/websites/WebsiteSettings'; -export * from 'components/pages/settings/websites/WebsitesList'; -export * from 'components/pages/settings/websites/WebsitesTable'; +export * from 'app/(app)/settings/websites/[id]/ShareUrl'; +export * from 'app/(app)/settings/websites/[id]/TrackingCode'; +export * from 'app/(app)/settings/websites/WebsiteAddForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteDeleteForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteEditForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteResetForm'; +export * from 'app/(app)/settings/websites/WebsiteSettings'; +export * from 'app/(app)/settings/websites/WebsitesList'; +export * from 'app/(app)/settings/websites/WebsitesTable'; diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 4be958b6b..e1e2a38b9 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -14,7 +14,6 @@ import { } from 'next-basics'; import { NextApiRequestCollect } from 'pages/api/send'; import { getUserById } from '../queries'; -import { NextApiRequestQueryBody } from './types'; const log = debug('umami:middleware'); @@ -83,14 +82,18 @@ export const useAuth = createMiddleware(async (req, res, next) => { next(); }); -export const useValidate = createMiddleware(async (req: any, res, next) => { - try { - const { yup } = req as NextApiRequestQueryBody; +export const useValidate = async (schema, req, res) => { + return createMiddleware(async (req: any, res, next) => { + try { + const rules = schema[req.method]; - yup[req.method] && yup[req.method].validateSync({ ...req.query, ...req.body }); - } catch (e: any) { - return badRequest(res, e.message); - } + if (rules) { + rules.validateSync(req.method === 'GET' ? { ...req.query } : { ...req.body }); + } + } catch (e: any) { + return badRequest(res, e.message); + } - next(); -}); + next(); + })(req, res); +}; diff --git a/src/pages/404.js b/src/pages/404.js deleted file mode 100644 index 8fa13a9c7..000000000 --- a/src/pages/404.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Row, Column, Flexbox } from 'react-basics'; -import AppLayout from 'components/layout/AppLayout'; -import useMessages from 'components/hooks/useMessages'; - -export default function Custom404() { - const { formatMessage, labels } = useMessages(); - - return ( - - - - -

{formatMessage(labels.pageNotFound)}

-
-
-
-
- ); -} diff --git a/src/pages/_app.js b/src/pages/_app.js deleted file mode 100644 index 7022772c7..000000000 --- a/src/pages/_app.js +++ /dev/null @@ -1,69 +0,0 @@ -import { IntlProvider } from 'react-intl'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactBasicsProvider } from 'react-basics'; -import Head from 'next/head'; -import Script from 'next/script'; -import { useRouter } from 'next/router'; -import ErrorBoundary from 'components/common/ErrorBoundary'; -import useLocale from 'components/hooks/useLocale'; -import '@fontsource/inter/400.css'; -import '@fontsource/inter/700.css'; -import 'react-basics/dist/styles.css'; -import 'styles/variables.css'; -import 'styles/locale.css'; -import 'styles/index.css'; -import 'chartjs-adapter-date-fns'; - -const client = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - refetchOnWindowFocus: false, - }, - }, -}); - -export default function App({ Component, pageProps }) { - const { locale, messages } = useLocale(); - const { basePath, pathname } = useRouter(); - - return ( - - null}> - - - - - - - - - - - - - - - - - {!pathname.includes('/share/') && `; diff --git a/src/app/(main)/settings/websites/[id]/WebsiteData.tsx b/src/app/(main)/settings/websites/[id]/WebsiteData.tsx index b4bfe6093..0ad3b5596 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteData.tsx +++ b/src/app/(main)/settings/websites/[id]/WebsiteData.tsx @@ -29,7 +29,7 @@ export function WebsiteData({ - {close => ( + {(close: () => void) => ( )} @@ -42,7 +42,7 @@ export function WebsiteData({ - {close => ( + {(close: () => void) => ( )} diff --git a/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx b/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx index c3b5d74ad..e0f71041d 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx +++ b/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx @@ -9,6 +9,8 @@ import { } from 'react-basics'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; +import { useContext } from 'react'; +import SettingsContext from '../../SettingsContext'; const CONFIRM_VALUE = 'DELETE'; @@ -22,12 +24,13 @@ export function WebsiteDeleteForm({ onClose?: () => void; }) { const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { del, useMutation } = useApi(); const { mutate, error } = useMutation({ - mutationFn: (data: any) => del(`/websites/${websiteId}`, data), + mutationFn: (data: any) => del(`${websitesUrl}/${websiteId}`, data), }); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { onSave(); diff --git a/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx b/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx index 9c05905cb..80b36cae1 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx +++ b/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx @@ -1,8 +1,9 @@ import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics'; -import { useRef } from 'react'; +import { useContext, useRef } from 'react'; import useApi from 'components/hooks/useApi'; import { DOMAIN_REGEX } from 'lib/constants'; import useMessages from 'components/hooks/useMessages'; +import SettingsContext from '../../SettingsContext'; export function WebsiteEditForm({ websiteId, @@ -14,13 +15,14 @@ export function WebsiteEditForm({ onSave?: (data: any) => void; }) { const { formatMessage, labels, messages } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { post, useMutation } = useApi(); const { mutate, error } = useMutation({ - mutationFn: (data: any) => post(`/websites/${websiteId}`, data), + mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}`, data), }); const ref = useRef(null); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { ref.current.reset(data); diff --git a/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx index 76c2bc472..0c02c77bc 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx +++ b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx @@ -9,6 +9,8 @@ import { } from 'react-basics'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; +import { useContext } from 'react'; +import SettingsContext from '../../SettingsContext'; const CONFIRM_VALUE = 'RESET'; @@ -22,9 +24,10 @@ export function WebsiteResetForm({ onClose?: () => void; }) { const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { post, useMutation } = useApi(); const { mutate, error } = useMutation({ - mutationFn: (data: any) => post(`/websites/${websiteId}/reset`, data), + mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}/reset`, data), }); const handleSubmit = async (data: any) => { diff --git a/src/app/sso/page.tsx b/src/app/sso/page.tsx index 75ea945d4..e577767a8 100644 --- a/src/app/sso/page.tsx +++ b/src/app/sso/page.tsx @@ -18,5 +18,5 @@ export default function SSOPage() { } }, [router, url, token]); - return ; + return ; } diff --git a/src/components/layout/Page.tsx b/src/components/layout/Page.tsx index 2f7020128..e32a09a3a 100644 --- a/src/components/layout/Page.tsx +++ b/src/components/layout/Page.tsx @@ -23,7 +23,7 @@ export function Page({ } if (isLoading) { - return ; + return ; } return
{children}
; diff --git a/src/index.ts b/src/index.ts index de5550518..7b1920547 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,8 @@ export * from 'app/(main)/settings/websites/WebsiteSettings'; export * from 'app/(main)/settings/websites/WebsitesDataTable'; export * from 'app/(main)/settings/websites/WebsitesTable'; +export * from 'app/(main)/settings/SettingsContext'; + export * from 'components/common/ConfirmDeleteForm'; export * from 'components/common/DataTable'; export * from 'components/common/Empty'; From c8eb76c7af8c6aee1a084c805314ff17eb4301c9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 6 Dec 2023 01:26:58 -0800 Subject: [PATCH 340/357] Fixed share url. --- src/app/(main)/settings/layout.tsx | 3 +-- src/app/(main)/settings/websites/WebsiteSettings.tsx | 2 +- src/app/(main)/settings/websites/[id]/ShareUrl.tsx | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/settings/layout.tsx b/src/app/(main)/settings/layout.tsx index f96123613..1c30d2dba 100644 --- a/src/app/(main)/settings/layout.tsx +++ b/src/app/(main)/settings/layout.tsx @@ -21,7 +21,7 @@ export default function SettingsLayout({ children }) { const getKey = () => items.find(({ url }) => pathname === url)?.key; - if (cloudMode && pathname != '/settings/profile') { + if (cloudMode && pathname !== '/settings/profile') { return null; } @@ -29,7 +29,6 @@ export default function SettingsLayout({ children }) { const config = { settingsUrl: '/settings/websites', - hostUrl, shareUrl: hostUrl, trackingCodeUrl: hostUrl, websitesUrl: `/websites`, diff --git a/src/app/(main)/settings/websites/WebsiteSettings.tsx b/src/app/(main)/settings/websites/WebsiteSettings.tsx index e925d1cf4..4607b423c 100644 --- a/src/app/(main)/settings/websites/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/WebsiteSettings.tsx @@ -51,7 +51,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { }, [data]); if (isLoading || !values) { - return ; + return ; } return ( diff --git a/src/app/(main)/settings/websites/[id]/ShareUrl.tsx b/src/app/(main)/settings/websites/[id]/ShareUrl.tsx index 07a590faa..90ce6ea69 100644 --- a/src/app/(main)/settings/websites/[id]/ShareUrl.tsx +++ b/src/app/(main)/settings/websites/[id]/ShareUrl.tsx @@ -17,15 +17,15 @@ import SettingsContext from '../../SettingsContext'; const generateId = () => getRandomChars(16); export function ShareUrl({ websiteId, data, onSave }) { + const ref = useRef(null); + const { shareUrl, websitesUrl } = useContext(SettingsContext); const { formatMessage, labels, messages } = useMessages(); const { name, shareId } = data; const [id, setId] = useState(shareId); const { post, useMutation } = useApi(); const { mutate, error } = useMutation({ - mutationFn: (data: any) => post(`/websites/${websiteId}`, data), + mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}`, data), }); - const ref = useRef(null); - const { shareUrl } = useContext(SettingsContext); const url = useMemo( () => `${shareUrl}${process.env.basePath}/share/${id}/${encodeURIComponent(name)}`, [id, name], From e67282d7d895a7c430174a6b343d05df8fe54c88 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 8 Dec 2023 22:14:55 -0800 Subject: [PATCH 341/357] Render correct OS names. --- src/components/input/SettingsButton.tsx | 7 +------ src/components/metrics/OSTable.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/input/SettingsButton.tsx b/src/components/input/SettingsButton.tsx index 46c725973..2a076d42b 100644 --- a/src/components/input/SettingsButton.tsx +++ b/src/components/input/SettingsButton.tsx @@ -15,12 +15,7 @@ export function SettingsButton() { - e.stopPropagation()} - > + diff --git a/src/components/metrics/OSTable.tsx b/src/components/metrics/OSTable.tsx index e8b8e81f7..c39cba226 100644 --- a/src/components/metrics/OSTable.tsx +++ b/src/components/metrics/OSTable.tsx @@ -2,14 +2,20 @@ import MetricsTable from './MetricsTable'; import FilterLink from 'components/common/FilterLink'; import useMessages from 'components/hooks/useMessages'; +const names = { + 'Mac OS': 'macOS', + 'Chrome OS': 'ChromeOS', + 'Sun OS': 'SunOS', +}; + export function OSTable({ websiteId, limit }: { websiteId: string; limit?: number }) { const { formatMessage, labels } = useMessages(); function renderLink({ x: os }) { return ( - + {os} Date: Sat, 9 Dec 2023 00:35:54 -0800 Subject: [PATCH 342/357] Convert realtime components to TS. --- ...bsiteEventData.js => WebsiteEventData.tsx} | 2 +- .../realtime/{Realtime.js => Realtime.tsx} | 22 +++++----- ...timeCountries.js => RealtimeCountries.tsx} | 0 .../{RealtimeHeader.js => RealtimeHeader.tsx} | 5 ++- .../{RealtimeHome.js => RealtimeHome.tsx} | 0 .../{RealtimeLog.js => RealtimeLog.tsx} | 4 +- .../{RealtimeUrls.js => RealtimeUrls.tsx} | 21 ++++++---- .../{WebsiteReports.js => WebsiteReports.tsx} | 0 src/components/hooks/useDateRange.ts | 14 ++++--- src/components/metrics/RealtimeChart.tsx | 7 ++-- src/lib/date.ts | 40 ++++++++++--------- src/lib/types.ts | 11 ++++- 12 files changed, 75 insertions(+), 51 deletions(-) rename src/app/(main)/websites/[id]/event-data/{WebsiteEventData.js => WebsiteEventData.tsx} (96%) rename src/app/(main)/websites/[id]/realtime/{Realtime.js => Realtime.tsx} (84%) rename src/app/(main)/websites/[id]/realtime/{RealtimeCountries.js => RealtimeCountries.tsx} (100%) rename src/app/(main)/websites/[id]/realtime/{RealtimeHeader.js => RealtimeHeader.tsx} (85%) rename src/app/(main)/websites/[id]/realtime/{RealtimeHome.js => RealtimeHome.tsx} (100%) rename src/app/(main)/websites/[id]/realtime/{RealtimeLog.js => RealtimeLog.tsx} (97%) rename src/app/(main)/websites/[id]/realtime/{RealtimeUrls.js => RealtimeUrls.tsx} (85%) rename src/app/(main)/websites/[id]/reports/{WebsiteReports.js => WebsiteReports.tsx} (100%) diff --git a/src/app/(main)/websites/[id]/event-data/WebsiteEventData.js b/src/app/(main)/websites/[id]/event-data/WebsiteEventData.tsx similarity index 96% rename from src/app/(main)/websites/[id]/event-data/WebsiteEventData.js rename to src/app/(main)/websites/[id]/event-data/WebsiteEventData.tsx index b67ee95e4..61a4dc622 100644 --- a/src/app/(main)/websites/[id]/event-data/WebsiteEventData.js +++ b/src/app/(main)/websites/[id]/event-data/WebsiteEventData.tsx @@ -6,7 +6,7 @@ import { EventDataMetricsBar } from './EventDataMetricsBar'; import { useDateRange, useApi, useNavigation } from 'components/hooks'; import styles from './WebsiteEventData.module.css'; -function useData(websiteId, event) { +function useData(websiteId: string, event: string) { const [dateRange] = useDateRange(websiteId); const { startDate, endDate } = dateRange; const { get, useQuery } = useApi(); diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.js b/src/app/(main)/websites/[id]/realtime/Realtime.tsx similarity index 84% rename from src/app/(main)/websites/[id]/realtime/Realtime.js rename to src/app/(main)/websites/[id]/realtime/Realtime.tsx index 37df458ca..6de65d7ae 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.js +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -1,7 +1,7 @@ 'use client'; import { useMemo, useState, useEffect } from 'react'; import { subMinutes, startOfMinute } from 'date-fns'; -import firstBy from 'thenby'; +import thenby from 'thenby'; import { Grid, GridRow } from 'components/layout/Grid'; import Page from 'components/layout/Page'; import RealtimeChart from 'components/metrics/RealtimeChart'; @@ -15,9 +15,10 @@ import useApi from 'components/hooks/useApi'; import { percentFilter } from 'lib/filters'; import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; import { useWebsite } from 'components/hooks'; +import { RealtimeData } from 'lib/types'; import styles from './Realtime.module.css'; -function mergeData(state = [], data = [], time) { +function mergeData(state = [], data = [], time: number) { const ids = state.map(({ __id }) => __id); return state .concat(data.filter(({ __id }) => !ids.includes(__id))) @@ -25,7 +26,7 @@ function mergeData(state = [], data = [], time) { } export function Realtime({ websiteId }) { - const [currentData, setCurrentData] = useState(); + const [currentData, setCurrentData] = useState(); const { get, useQuery } = useApi(); const { data: website } = useWebsite(websiteId); const { data, isLoading, error } = useQuery({ @@ -33,7 +34,6 @@ export function Realtime({ websiteId }) { queryFn: () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }), enabled: !!(websiteId && website), refetchInterval: REALTIME_INTERVAL, - cache: false, }); useEffect(() => { @@ -50,9 +50,9 @@ export function Realtime({ websiteId }) { } }, [data]); - const realtimeData = useMemo(() => { + const realtimeData: RealtimeData = useMemo(() => { if (!currentData) { - return { pageviews: [], sessions: [], events: [], countries: [], visitors: [] }; + return { pageviews: [], sessions: [], events: [], countries: [], visitors: [], timestamp: 0 }; } currentData.countries = percentFilter( @@ -75,7 +75,7 @@ export function Realtime({ websiteId }) { } return arr; }, []) - .sort(firstBy('y', -1)), + .sort(thenby.firstBy('y', -1)), ); currentData.visitors = currentData.sessions.reduce((arr, val) => { @@ -89,18 +89,18 @@ export function Realtime({ websiteId }) { }, [currentData]); if (isLoading || error) { - return ; + return ; } return ( <> - + - - + + diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeCountries.js b/src/app/(main)/websites/[id]/realtime/RealtimeCountries.tsx similarity index 100% rename from src/app/(main)/websites/[id]/realtime/RealtimeCountries.js rename to src/app/(main)/websites/[id]/realtime/RealtimeCountries.tsx diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeHeader.js b/src/app/(main)/websites/[id]/realtime/RealtimeHeader.tsx similarity index 85% rename from src/app/(main)/websites/[id]/realtime/RealtimeHeader.js rename to src/app/(main)/websites/[id]/realtime/RealtimeHeader.tsx index 75f2f2d43..ad03efd14 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeHeader.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeHeader.tsx @@ -1,10 +1,11 @@ import MetricCard from 'components/metrics/MetricCard'; import useMessages from 'components/hooks/useMessages'; +import { RealtimeData } from 'lib/types'; import styles from './RealtimeHeader.module.css'; -export function RealtimeHeader({ data = {} }) { +export function RealtimeHeader({ data }: { data: RealtimeData }) { const { formatMessage, labels } = useMessages(); - const { pageviews, visitors, events, countries } = data; + const { pageviews, visitors, events, countries } = data || {}; return (
diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeHome.js b/src/app/(main)/websites/[id]/realtime/RealtimeHome.tsx similarity index 100% rename from src/app/(main)/websites/[id]/realtime/RealtimeHome.js rename to src/app/(main)/websites/[id]/realtime/RealtimeHome.tsx diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.js b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx similarity index 97% rename from src/app/(main)/websites/[id]/realtime/RealtimeLog.js rename to src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx index b388b37b5..e33320ec7 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react'; import { StatusLight, Icon, Text } from 'react-basics'; import { FixedSizeList } from 'react-window'; -import firstBy from 'thenby'; +import thenby from 'thenby'; import FilterButtons from 'components/common/FilterButtons'; import Empty from 'components/common/Empty'; import useLocale from 'components/hooks/useLocale'; @@ -130,7 +130,7 @@ export function RealtimeLog({ data, websiteDomain }) { } const { pageviews, visitors, events } = data; - const logs = [...pageviews, ...visitors, ...events].sort(firstBy('createdAt', -1)); + const logs = [...pageviews, ...visitors, ...events].sort(thenby.firstBy('createdAt', -1)); if (filter !== TYPE_ALL) { return logs.filter(({ __type }) => __type === filter); diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeUrls.js b/src/app/(main)/websites/[id]/realtime/RealtimeUrls.tsx similarity index 85% rename from src/app/(main)/websites/[id]/realtime/RealtimeUrls.js rename to src/app/(main)/websites/[id]/realtime/RealtimeUrls.tsx index 674858b2d..27a9ec5a6 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeUrls.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeUrls.tsx @@ -1,15 +1,22 @@ -import { useMemo, useState } from 'react'; +import { Key, useMemo, useState } from 'react'; import { ButtonGroup, Button, Flexbox } from 'react-basics'; -import firstBy from 'thenby'; +import thenby from 'thenby'; import { percentFilter } from 'lib/filters'; import ListTable from 'components/metrics/ListTable'; import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants'; import useMessages from 'components/hooks/useMessages'; +import { RealtimeData } from 'lib/types'; -export function RealtimeUrls({ websiteDomain, data = {} }) { +export function RealtimeUrls({ + websiteDomain, + data, +}: { + websiteDomain: string; + data: RealtimeData; +}) { const { formatMessage, labels } = useMessages(); - const { pageviews } = data; - const [filter, setFilter] = useState(FILTER_REFERRERS); + const { pageviews } = data || {}; + const [filter, setFilter] = useState(FILTER_REFERRERS); const limit = 15; const buttons = [ @@ -48,7 +55,7 @@ export function RealtimeUrls({ websiteDomain, data = {} }) { } return arr; }, []) - .sort(firstBy('y', -1)) + .sort(thenby.firstBy('y', -1)) .slice(0, limit), ); @@ -64,7 +71,7 @@ export function RealtimeUrls({ websiteDomain, data = {} }) { } return arr; }, []) - .sort(firstBy('y', -1)) + .sort(thenby.firstBy('y', -1)) .slice(0, limit), ); diff --git a/src/app/(main)/websites/[id]/reports/WebsiteReports.js b/src/app/(main)/websites/[id]/reports/WebsiteReports.tsx similarity index 100% rename from src/app/(main)/websites/[id]/reports/WebsiteReports.js rename to src/app/(main)/websites/[id]/reports/WebsiteReports.tsx diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index 71361b69c..d8a493316 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -1,9 +1,10 @@ import { getMinimumUnit, parseDateRange } from 'lib/date'; import { setItem } from 'next-basics'; import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants'; -import useLocale from './useLocale'; import websiteStore, { setWebsiteDateRange } from 'store/websites'; import appStore, { setDateRange } from 'store/app'; +import { DateRange } from 'lib/types'; +import useLocale from './useLocale'; import useApi from './useApi'; export function useDateRange(websiteId?: string) { @@ -14,9 +15,9 @@ export function useDateRange(websiteId?: string) { const globalConfig = appStore(state => state.dateRange); const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale); - const saveDateRange = async value => { + const saveDateRange = async (value: DateRange | string) => { if (websiteId) { - let dateRange = value; + let dateRange: DateRange | string = value; if (typeof value === 'string') { if (value === 'all') { @@ -37,14 +38,17 @@ export function useDateRange(websiteId?: string) { } } - setWebsiteDateRange(websiteId, dateRange); + setWebsiteDateRange(websiteId, dateRange as DateRange); } else { setItem(DATE_RANGE_CONFIG, value); setDateRange(value); } }; - return [dateRange, saveDateRange]; + return [dateRange, saveDateRange] as [ + { startDate: Date; endDate: Date }, + (value: string | DateRange) => void, + ]; } export default useDateRange; diff --git a/src/components/metrics/RealtimeChart.tsx b/src/components/metrics/RealtimeChart.tsx index f1a781bdc..1ca0719a3 100644 --- a/src/components/metrics/RealtimeChart.tsx +++ b/src/components/metrics/RealtimeChart.tsx @@ -3,6 +3,7 @@ import { format, startOfMinute, subMinutes, isBefore } from 'date-fns'; import PageviewsChart from './PageviewsChart'; import { getDateArray } from 'lib/date'; import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants'; +import { RealtimeData } from 'lib/types'; function mapData(data: any[]) { let last = 0; @@ -24,11 +25,9 @@ function mapData(data: any[]) { } export interface RealtimeChartProps { - data: { - pageviews: any[]; - visitors: any[]; - }; + data: RealtimeData; unit: string; + className?: string; } export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) { diff --git a/src/lib/date.ts b/src/lib/date.ts index 510573092..81c37e699 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -32,6 +32,7 @@ import { subWeeks, } from 'date-fns'; import { getDateLocale } from 'lib/lang'; +import { DateRange } from 'lib/types'; export const TIME_UNIT = { minute: 'minute', @@ -54,13 +55,13 @@ export function getTimezone() { return moment.tz.guess(); } -export function getLocalTime(t) { +export function getLocalTime(t: string | number | Date) { return addMinutes(new Date(t), new Date().getTimezoneOffset()); } -export function parseDateRange(value, locale = 'en-US') { +export function parseDateRange(value: string | object, locale = 'en-US'): DateRange { if (typeof value === 'object') { - return value; + return value as DateRange; } if (value === 'all') { @@ -93,7 +94,7 @@ export function parseDateRange(value, locale = 'en-US') { if (!match) return null; const { num, unit } = match.groups; - const selectedUnit = { num, unit }; + const selectedUnit = { num: +num, unit }; if (+num === 1) { switch (unit) { @@ -172,7 +173,7 @@ export function parseDateRange(value, locale = 'en-US') { switch (unit) { case 'day': return { - startDate: subDays(startOfDay(now), num - 1), + startDate: subDays(startOfDay(now), +num - 1), endDate: endOfDay(now), unit, value, @@ -180,7 +181,7 @@ export function parseDateRange(value, locale = 'en-US') { }; case 'hour': return { - startDate: subHours(startOfHour(now), num - 1), + startDate: subHours(startOfHour(now), +num - 1), endDate: endOfHour(now), unit, value, @@ -189,7 +190,10 @@ export function parseDateRange(value, locale = 'en-US') { } } -export function incrementDateRange(value, increment) { +export function incrementDateRange( + value: { startDate: any; endDate: any; selectedUnit: any }, + increment: number, +) { const { startDate, endDate, selectedUnit } = value; const { num, unit } = selectedUnit; @@ -235,7 +239,7 @@ export function incrementDateRange(value, increment) { } } -export function getAllowedUnits(startDate, endDate) { +export function getAllowedUnits(startDate: Date, endDate: Date) { const units = ['minute', 'hour', 'day', 'month', 'year']; const minUnit = getMinimumUnit(startDate, endDate); const index = units.indexOf(minUnit === 'year' ? 'month' : minUnit); @@ -243,7 +247,7 @@ export function getAllowedUnits(startDate, endDate) { return index >= 0 ? units.splice(index) : []; } -export function getMinimumUnit(startDate, endDate) { +export function getMinimumUnit(startDate: number | Date, endDate: number | Date) { if (differenceInMinutes(endDate, startDate) <= 60) { return 'minute'; } else if (differenceInHours(endDate, startDate) <= 48) { @@ -257,25 +261,25 @@ export function getMinimumUnit(startDate, endDate) { return 'year'; } -export function getDateFromString(str) { +export function getDateFromString(str: string) { const [ymd, hms] = str.split(' '); const [year, month, day] = ymd.split('-'); if (hms) { const [hour, min, sec] = hms.split(':'); - return new Date(year, month - 1, day, hour, min, sec); + return new Date(+year, +month - 1, +day, +hour, +min, +sec); } - return new Date(year, month - 1, day); + return new Date(+year, +month - 1, +day); } -export function getDateArray(data, startDate, endDate, unit) { +export function getDateArray(data: any[], startDate: Date, endDate: Date, unit: string) { const arr = []; const [diff, add, normalize] = dateFuncs[unit]; const n = diff(endDate, startDate) + 1; - function findData(date) { + function findData(date: Date) { const d = data.find(({ x }) => { return normalize(getDateFromString(x)).getTime() === date.getTime(); }); @@ -293,7 +297,7 @@ export function getDateArray(data, startDate, endDate, unit) { return arr; } -export function getDateLength(startDate, endDate, unit) { +export function getDateLength(startDate: Date, endDate: Date, unit: string | number) { const [diff] = dateFuncs[unit]; return diff(endDate, startDate) + 1; } @@ -310,7 +314,7 @@ export const CUSTOM_FORMATS = { }, }; -export function formatDate(date, str, locale = 'en-US') { +export function formatDate(date: string | number | Date, str: string, locale = 'en-US') { return format( typeof date === 'string' ? new Date(date) : date, CUSTOM_FORMATS?.[locale]?.[str] || str, @@ -320,10 +324,10 @@ export function formatDate(date, str, locale = 'en-US') { ); } -export function maxDate(...args) { +export function maxDate(...args: Date[]) { return max(args.filter(n => isDate(n))); } -export function minDate(...args) { +export function minDate(...args: any[]) { return min(args.filter(n => isDate(n))); } diff --git a/src/lib/types.ts b/src/lib/types.ts index a7fab1e7d..900f2e5a6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -180,7 +180,7 @@ export interface DateRange { endDate: Date; value: string; unit?: TimeUnit; - selectedUnit?: TimeUnit; + selectedUnit?: { num: number; unit: TimeUnit }; } export interface QueryFilters { @@ -207,3 +207,12 @@ export interface QueryOptions { joinSession?: boolean; columns?: { [key: string]: string }; } + +export interface RealtimeData { + pageviews: any[]; + sessions: any[]; + events: any[]; + timestamp: number; + countries?: any[]; + visitors?: any[]; +} From c520a329d25a7c8b66cf8c21372cabebca5adb9e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 01:25:02 -0800 Subject: [PATCH 343/357] Fixed activity log timestamp. --- package.json | 1 + src/app/(main)/websites/[id]/realtime/Realtime.tsx | 2 +- src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx | 6 +++--- yarn.lock | 7 +++++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a158696cf..782e039bf 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "@types/node": "^20.9.0", "@types/react": "^18.2.41", "@types/react-dom": "^18.2.17", + "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "cross-env": "^7.0.3", diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index 6de65d7ae..285d98454 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -95,7 +95,7 @@ export function Realtime({ websiteId }) { return ( <> - + diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx index e33320ec7..7cae0abca 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx @@ -8,11 +8,11 @@ import useLocale from 'components/hooks/useLocale'; import useCountryNames from 'components/hooks/useCountryNames'; import { BROWSERS } from 'lib/constants'; import { stringToColor } from 'lib/format'; -import { formatDate } from 'lib/date'; import { safeDecodeURI } from 'next-basics'; import Icons from 'components/icons'; import styles from './RealtimeLog.module.css'; import useMessages from 'components/hooks/useMessages'; +import { format } from 'date-fns'; const TYPE_ALL = 'all'; const TYPE_PAGEVIEW = 'pageview'; @@ -50,7 +50,7 @@ export function RealtimeLog({ data, websiteDomain }) { }, ]; - const getTime = ({ createdAt }) => formatDate(new Date(createdAt), 'pp', locale); + const getTime = ({ timestamp }) => format(timestamp, 'h:mm:ss'); const getColor = ({ id, sessionId }) => stringToColor(sessionId || id); @@ -146,7 +146,7 @@ export function RealtimeLog({ data, websiteDomain }) {
{logs?.length === 0 && } {logs?.length > 0 && ( - + {Row} )} diff --git a/yarn.lock b/yarn.lock index bdb0d4843..1ecdaaf31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2431,6 +2431,13 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" +"@types/react-window@^1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@16 || 17 || 18": version "18.2.30" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.30.tgz#b84f786864fc46f18545364a54d5e1316308e59b" From 7a5f28870f3f87452cc0d0e33ecba933308228f2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 01:41:07 -0800 Subject: [PATCH 344/357] Fixed realtime chart rendering of initial payload. --- .../websites/[id]/realtime/Realtime.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index 285d98454..e77af2898 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -38,15 +38,19 @@ export function Realtime({ websiteId }) { useEffect(() => { if (data) { - const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); - const time = date.getTime(); + if (!currentData) { + setCurrentData(data); + } else { + const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); + const time = date.getTime(); - setCurrentData(state => ({ - pageviews: mergeData(state?.pageviews, data.pageviews, time), - sessions: mergeData(state?.sessions, data.sessions, time), - events: mergeData(state?.events, data.events, time), - timestamp: data.timestamp, - })); + setCurrentData(state => ({ + pageviews: mergeData(state?.pageviews, data.pageviews, time), + sessions: mergeData(state?.sessions, data.sessions, time), + events: mergeData(state?.events, data.events, time), + timestamp: data.timestamp, + })); + } } }, [data]); From 92a513e4d06c861968d337e120eafac837242c6e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 20:55:50 -0800 Subject: [PATCH 345/357] Fixed realtime chart rendering. --- .../websites/[id]/realtime/Realtime.tsx | 27 +++++++--------- src/pages/api/realtime/[id].ts | 6 ++-- src/queries/analytics/events/getEvents.ts | 4 +++ src/queries/analytics/getRealtimeData.ts | 32 ++++++++++++++----- src/queries/analytics/sessions/getSessions.ts | 6 +++- 5 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index e77af2898..34e292812 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -19,9 +19,9 @@ import { RealtimeData } from 'lib/types'; import styles from './Realtime.module.css'; function mergeData(state = [], data = [], time: number) { - const ids = state.map(({ __id }) => __id); + const ids = state.map(({ id }) => id); return state - .concat(data.filter(({ __id }) => !ids.includes(__id))) + .concat(data.filter(({ id }) => !ids.includes(id))) .filter(({ timestamp }) => timestamp >= time); } @@ -38,21 +38,18 @@ export function Realtime({ websiteId }) { useEffect(() => { if (data) { - if (!currentData) { - setCurrentData(data); - } else { - const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); - const time = date.getTime(); + const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); + const time = date.getTime(); + const { pageviews, sessions, events, timestamp } = data; - setCurrentData(state => ({ - pageviews: mergeData(state?.pageviews, data.pageviews, time), - sessions: mergeData(state?.sessions, data.sessions, time), - events: mergeData(state?.events, data.events, time), - timestamp: data.timestamp, - })); - } + setCurrentData(state => ({ + pageviews: mergeData(state?.pageviews, pageviews, time), + sessions: mergeData(state?.sessions, sessions, time), + events: mergeData(state?.events, events, time), + timestamp, + })); } - }, [data]); + }, [data, currentData]); const realtimeData: RealtimeData = useMemo(() => { if (!currentData) { diff --git a/src/pages/api/realtime/[id].ts b/src/pages/api/realtime/[id].ts index 212d4a0f2..0edd589d8 100644 --- a/src/pages/api/realtime/[id].ts +++ b/src/pages/api/realtime/[id].ts @@ -1,4 +1,4 @@ -import { subMinutes } from 'date-fns'; +import { startOfMinute, subMinutes } from 'date-fns'; import { canViewWebsite } from 'lib/auth'; import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, RealtimeInit } from 'lib/types'; @@ -6,6 +6,8 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRealtimeData } from 'queries'; import * as yup from 'yup'; +import { REALTIME_RANGE } from 'lib/constants'; + export interface RealtimeRequestQuery { id: string; startAt: number; @@ -32,7 +34,7 @@ export default async ( return unauthorized(res); } - let startTime = subMinutes(new Date(), 30); + let startTime = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); if (+startAt > startTime.getTime()) { startTime = new Date(+startAt); diff --git a/src/queries/analytics/events/getEvents.ts b/src/queries/analytics/events/getEvents.ts index fe074ec28..9ef279734 100644 --- a/src/queries/analytics/events/getEvents.ts +++ b/src/queries/analytics/events/getEvents.ts @@ -18,6 +18,9 @@ function relationalQuery(websiteId: string, startDate: Date, eventType: number) gte: startDate, }, }, + orderBy: { + createdAt: 'asc', + }, }); } @@ -39,6 +42,7 @@ function clickhouseQuery(websiteId: string, startDate: Date, eventType: number) where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime64} and event_type = {eventType:UInt32} + order by created_at asc `, { websiteId, diff --git a/src/queries/analytics/getRealtimeData.ts b/src/queries/analytics/getRealtimeData.ts index 337e74751..868a5c70e 100644 --- a/src/queries/analytics/getRealtimeData.ts +++ b/src/queries/analytics/getRealtimeData.ts @@ -1,4 +1,3 @@ -import { md5 } from 'next-basics'; import { getSessions, getEvents } from 'queries/index'; import { EVENT_TYPE } from 'lib/constants'; @@ -9,18 +8,35 @@ export async function getRealtimeData(websiteId: string, startDate: Date) { getEvents(websiteId, startDate, EVENT_TYPE.customEvent), ]); - const decorate = (id: string, data: any[]) => { - return data.map((props: { [key: string]: any }) => ({ - ...props, - __id: md5(id, ...Object.values(props)), - __type: id, - timestamp: props.timestamp ? props.timestamp * 1000 : new Date(props.createdAt).getTime(), + const decorate = (type: string, data: any[]) => { + return data.map((values: { [key: string]: any }) => ({ + ...values, + __type: type, + timestamp: values.timestamp ? values.timestamp * 1000 : new Date(values.createdAt).getTime(), })); }; + const set = new Set(); + const uniques = (type: string, data: any[]) => { + return data.reduce((arr, values: { [key: string]: any }) => { + if (!set.has(values.id)) { + set.add(values.id); + + return arr.concat({ + ...values, + __type: type, + timestamp: values.timestamp + ? values.timestamp * 1000 + : new Date(values.createdAt).getTime(), + }); + } + return arr; + }, []); + }; + return { pageviews: decorate('pageview', pageviews), - sessions: decorate('session', sessions), + sessions: uniques('session', sessions), events: decorate('event', events), timestamp: Date.now(), }; diff --git a/src/queries/analytics/sessions/getSessions.ts b/src/queries/analytics/sessions/getSessions.ts index d67edd5ef..b92e3af99 100644 --- a/src/queries/analytics/sessions/getSessions.ts +++ b/src/queries/analytics/sessions/getSessions.ts @@ -17,6 +17,9 @@ async function relationalQuery(websiteId: string, startDate: Date) { gte: startDate, }, }, + orderBy: { + createdAt: 'asc', + }, }); } @@ -25,7 +28,7 @@ async function clickhouseQuery(websiteId: string, startDate: Date) { return rawQuery( ` - select distinct + select session_id as id, website_id as websiteId, created_at as createdAt, @@ -43,6 +46,7 @@ async function clickhouseQuery(websiteId: string, startDate: Date) { from website_event where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime64} + order by created_at asc `, { websiteId, From 44e243ad12b181bcaf05d644feb10e53f8b92acd Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 21:30:57 -0800 Subject: [PATCH 346/357] Moved SettingsProvider to root Providers component. --- src/app/(main)/settings/layout.tsx | 28 ++++++------------- .../websites/[id]/realtime/Realtime.tsx | 2 +- src/app/Providers.tsx | 26 +++++++++++++---- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/app/(main)/settings/layout.tsx b/src/app/(main)/settings/layout.tsx index 1c30d2dba..e36b5b53d 100644 --- a/src/app/(main)/settings/layout.tsx +++ b/src/app/(main)/settings/layout.tsx @@ -4,7 +4,6 @@ import useUser from 'components/hooks/useUser'; import useMessages from 'components/hooks/useMessages'; import SideNav from 'components/layout/SideNav'; import styles from './layout.module.css'; -import SettingsContext from './SettingsContext'; export default function SettingsLayout({ children }) { const { user } = useUser(); @@ -25,25 +24,14 @@ export default function SettingsLayout({ children }) { return null; } - const hostUrl = process.env.hostUrl || location.origin; - - const config = { - settingsUrl: '/settings/websites', - shareUrl: hostUrl, - trackingCodeUrl: hostUrl, - websitesUrl: `/websites`, - }; - return ( - -
- {!cloudMode && ( -
- -
- )} -
{children}
-
-
+
+ {!cloudMode && ( +
+ +
+ )} +
{children}
+
); } diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index 34e292812..7f17190dd 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -64,7 +64,7 @@ export function Realtime({ websiteId }) { } return arr; }, []) - .reduce((arr, { country }) => { + .reduce((arr: { x: any; y: number }[], { country }: any) => { if (country) { const row = arr.find(({ x }) => x === country); diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx index c3d626993..0abebf867 100644 --- a/src/app/Providers.tsx +++ b/src/app/Providers.tsx @@ -3,6 +3,7 @@ import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactBasicsProvider } from 'react-basics'; import ErrorBoundary from 'components/common/ErrorBoundary'; +import SettingsContext from 'app/(main)/settings/SettingsContext'; import useLocale from 'components/hooks/useLocale'; import 'chartjs-adapter-date-fns'; @@ -24,14 +25,29 @@ function MessagesProvider({ children }) { ); } +function SettingsProvider({ children }) { + const hostUrl = process.env.hostUrl || location.origin; + + const config = { + settingsUrl: '/settings/websites', + shareUrl: hostUrl, + trackingCodeUrl: hostUrl, + websitesUrl: `/websites`, + }; + + return {children}; +} + export function Providers({ children }) { return ( - - - {children} - - + + + + {children} + + + ); } From 08bd9e835720901f6d3c8867adfa492d4eb5c128 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 22:18:47 -0800 Subject: [PATCH 347/357] Upgrade to Next 14. --- package.json | 2 +- .../websites/[id]/realtime/Realtime.tsx | 10 +- src/app/Providers.tsx | 19 +-- yarn.lock | 111 +++++++++--------- 4 files changed, 74 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index 782e039bf..db10755c9 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.5.6", + "next": "14.0.4", "next-basics": "^0.39.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index 7f17190dd..ee710d737 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -6,16 +6,16 @@ import { Grid, GridRow } from 'components/layout/Grid'; import Page from 'components/layout/Page'; import RealtimeChart from 'components/metrics/RealtimeChart'; import WorldMap from 'components/metrics/WorldMap'; +import useApi from 'components/hooks/useApi'; +import { useWebsite } from 'components/hooks'; +import { percentFilter } from 'lib/filters'; +import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; +import { RealtimeData } from 'lib/types'; import RealtimeLog from './RealtimeLog'; import RealtimeHeader from './RealtimeHeader'; import RealtimeUrls from './RealtimeUrls'; import RealtimeCountries from './RealtimeCountries'; import WebsiteHeader from '../WebsiteHeader'; -import useApi from 'components/hooks/useApi'; -import { percentFilter } from 'lib/filters'; -import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; -import { useWebsite } from 'components/hooks'; -import { RealtimeData } from 'lib/types'; import styles from './Realtime.module.css'; function mergeData(state = [], data = [], time: number) { diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx index 0abebf867..c2ddd2ff7 100644 --- a/src/app/Providers.tsx +++ b/src/app/Providers.tsx @@ -1,4 +1,5 @@ 'use client'; +import { useEffect, useState } from 'react'; import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactBasicsProvider } from 'react-basics'; @@ -26,14 +27,18 @@ function MessagesProvider({ children }) { } function SettingsProvider({ children }) { - const hostUrl = process.env.hostUrl || location.origin; + const [config, setConfig] = useState({}); - const config = { - settingsUrl: '/settings/websites', - shareUrl: hostUrl, - trackingCodeUrl: hostUrl, - websitesUrl: `/websites`, - }; + useEffect(() => { + const hostUrl = process.env.hostUrl || window?.location.origin; + + setConfig({ + settingsUrl: '/settings/websites', + shareUrl: hostUrl, + trackingCodeUrl: hostUrl, + websitesUrl: `/websites`, + }); + }, []); return {children}; } diff --git a/yarn.lock b/yarn.lock index 1ecdaaf31..b24830a3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1771,10 +1771,10 @@ "@netlify/node-cookies" "^0.1.0" urlpattern-polyfill "8.0.2" -"@next/env@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.6.tgz#c1148e2e1aa166614f05161ee8f77ded467062bc" - integrity sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw== +"@next/env@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" + integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== "@next/eslint-plugin-next@12.3.4": version "12.3.4" @@ -1783,50 +1783,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz#b15d139d8971360fca29be3bdd703c108c9a45fb" - integrity sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA== +"@next/swc-darwin-arm64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618" + integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg== -"@next/swc-darwin-x64@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz#9c72ee31cc356cb65ce6860b658d807ff39f1578" - integrity sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA== +"@next/swc-darwin-x64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b" + integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw== -"@next/swc-linux-arm64-gnu@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz#59f5f66155e85380ffa26ee3d95b687a770cfeab" - integrity sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg== +"@next/swc-linux-arm64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21" + integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w== -"@next/swc-linux-arm64-musl@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz#f012518228017052736a87d69bae73e587c76ce2" - integrity sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q== +"@next/swc-linux-arm64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd" + integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ== -"@next/swc-linux-x64-gnu@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz#339b867a7e9e7ee727a700b496b269033d820df4" - integrity sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw== +"@next/swc-linux-x64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32" + integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A== -"@next/swc-linux-x64-musl@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz#ae0ae84d058df758675830bcf70ca1846f1028f2" - integrity sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ== +"@next/swc-linux-x64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247" + integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw== -"@next/swc-win32-arm64-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz#a5cc0c16920485a929a17495064671374fdbc661" - integrity sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg== +"@next/swc-win32-arm64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3" + integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w== -"@next/swc-win32-ia32-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz#6a2409b84a2cbf34bf92fe714896455efb4191e4" - integrity sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg== +"@next/swc-win32-ia32-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600" + integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg== -"@next/swc-win32-x64-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz#4a3e2a206251abc729339ba85f60bc0433c2865d" - integrity sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ== +"@next/swc-win32-x64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1" + integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -4949,7 +4949,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: +graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -6330,28 +6330,29 @@ next-basics@^0.39.0: jsonwebtoken "^9.0.0" pure-rand "^6.0.2" -next@13.5.6: - version "13.5.6" - resolved "https://registry.yarnpkg.com/next/-/next-13.5.6.tgz#e964b5853272236c37ce0dd2c68302973cf010b1" - integrity sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw== +next@14.0.4: + version "14.0.4" + resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc" + integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA== dependencies: - "@next/env" "13.5.6" + "@next/env" "14.0.4" "@swc/helpers" "0.5.2" busboy "1.6.0" caniuse-lite "^1.0.30001406" + graceful-fs "^4.2.11" postcss "8.4.31" styled-jsx "5.1.1" watchpack "2.4.0" optionalDependencies: - "@next/swc-darwin-arm64" "13.5.6" - "@next/swc-darwin-x64" "13.5.6" - "@next/swc-linux-arm64-gnu" "13.5.6" - "@next/swc-linux-arm64-musl" "13.5.6" - "@next/swc-linux-x64-gnu" "13.5.6" - "@next/swc-linux-x64-musl" "13.5.6" - "@next/swc-win32-arm64-msvc" "13.5.6" - "@next/swc-win32-ia32-msvc" "13.5.6" - "@next/swc-win32-x64-msvc" "13.5.6" + "@next/swc-darwin-arm64" "14.0.4" + "@next/swc-darwin-x64" "14.0.4" + "@next/swc-linux-arm64-gnu" "14.0.4" + "@next/swc-linux-arm64-musl" "14.0.4" + "@next/swc-linux-x64-gnu" "14.0.4" + "@next/swc-linux-x64-musl" "14.0.4" + "@next/swc-win32-arm64-msvc" "14.0.4" + "@next/swc-win32-ia32-msvc" "14.0.4" + "@next/swc-win32-x64-msvc" "14.0.4" nice-try@^1.0.4: version "1.0.5" From 3a28fea8aca027a8c54dfdc943a20d2810229392 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 22:45:55 -0800 Subject: [PATCH 348/357] Fixed broken links behavior. --- src/app/(main)/websites/[id]/realtime/Realtime.tsx | 2 +- src/app/share/[...id]/Header.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index ee710d737..bd9f74bc1 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -49,7 +49,7 @@ export function Realtime({ websiteId }) { timestamp, })); } - }, [data, currentData]); + }, [data]); const realtimeData: RealtimeData = useMemo(() => { if (!currentData) { diff --git a/src/app/share/[...id]/Header.tsx b/src/app/share/[...id]/Header.tsx index 41e93f52e..2b82908df 100644 --- a/src/app/share/[...id]/Header.tsx +++ b/src/app/share/[...id]/Header.tsx @@ -19,8 +19,8 @@ export function Header() {
- - + +
From cad719fd235f400d78fe0c4459864498b411f669 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 10 Dec 2023 02:02:24 -0800 Subject: [PATCH 349/357] Added search to metrics table. --- .../(main)/websites/[id]/WebsiteDetails.tsx | 4 +- ...ule.css => WebsiteExpandedView.module.css} | 0 ...teMenuView.tsx => WebsiteExpandedView.tsx} | 11 ++--- src/components/hooks/useDateRange.ts | 2 +- src/components/hooks/useFormat.ts | 11 ++++- src/components/layout/SideNav.tsx | 2 +- src/components/metrics/CitiesTable.tsx | 4 +- .../metrics/MetricsTable.module.css | 20 +++++++++ src/components/metrics/MetricsTable.tsx | 38 +++++++++++++--- src/components/metrics/OSTable.tsx | 9 ++-- src/components/metrics/PagesTable.tsx | 25 +++++------ .../metrics/QueryParametersTable.tsx | 45 +++++++++---------- 12 files changed, 111 insertions(+), 60 deletions(-) rename src/app/(main)/websites/[id]/{WebsiteMenuView.module.css => WebsiteExpandedView.module.css} (100%) rename src/app/(main)/websites/[id]/{WebsiteMenuView.tsx => WebsiteExpandedView.tsx} (93%) diff --git a/src/app/(main)/websites/[id]/WebsiteDetails.tsx b/src/app/(main)/websites/[id]/WebsiteDetails.tsx index 4d3a18e79..7d8d2d999 100644 --- a/src/app/(main)/websites/[id]/WebsiteDetails.tsx +++ b/src/app/(main)/websites/[id]/WebsiteDetails.tsx @@ -6,7 +6,7 @@ import FilterTags from 'components/metrics/FilterTags'; import useNavigation from 'components/hooks/useNavigation'; import { useWebsite } from 'components/hooks'; import WebsiteChart from './WebsiteChart'; -import WebsiteMenuView from './WebsiteMenuView'; +import WebsiteExpandedView from './WebsiteExpandedView'; import WebsiteHeader from './WebsiteHeader'; import WebsiteMetricsBar from './WebsiteMetricsBar'; import WebsiteTableView from './WebsiteTableView'; @@ -34,7 +34,7 @@ export default function WebsiteDetails({ websiteId }: { websiteId: string }) { {website && ( <> {!view && } - {view && } + {view && } )} diff --git a/src/app/(main)/websites/[id]/WebsiteMenuView.module.css b/src/app/(main)/websites/[id]/WebsiteExpandedView.module.css similarity index 100% rename from src/app/(main)/websites/[id]/WebsiteMenuView.module.css rename to src/app/(main)/websites/[id]/WebsiteExpandedView.module.css diff --git a/src/app/(main)/websites/[id]/WebsiteMenuView.tsx b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx similarity index 93% rename from src/app/(main)/websites/[id]/WebsiteMenuView.tsx rename to src/app/(main)/websites/[id]/WebsiteExpandedView.tsx index 670ea469d..e97cd002c 100644 --- a/src/app/(main)/websites/[id]/WebsiteMenuView.tsx +++ b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx @@ -15,7 +15,7 @@ import SideNav from 'components/layout/SideNav'; import useNavigation from 'components/hooks/useNavigation'; import useMessages from 'components/hooks/useMessages'; import LinkButton from 'components/common/LinkButton'; -import styles from './WebsiteMenuView.module.css'; +import styles from './WebsiteExpandedView.module.css'; const views = { url: PagesTable, @@ -33,7 +33,7 @@ const views = { query: QueryParametersTable, }; -export default function WebsiteMenuView({ +export default function WebsiteExpandedView({ websiteId, websiteDomain, }: { @@ -113,11 +113,11 @@ export default function WebsiteMenuView({ const DetailsComponent = views[view] || (() => null); - const handleChange = view => { + const handleChange = (view: any) => { router.push(makeUrl({ view })); }; - const renderValue = value => items.find(({ key }) => key === value)?.label; + const renderValue = (value: string) => items.find(({ key }) => key === value)?.label; return (
@@ -146,9 +146,10 @@ export default function WebsiteMenuView({ websiteDomain={websiteDomain} limit={false} animate={false} - showFilters={true} virtualize={true} itemCount={25} + allowFilter={true} + allowSearch={true} />
diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index d8a493316..efaa717fc 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -46,7 +46,7 @@ export function useDateRange(websiteId?: string) { }; return [dateRange, saveDateRange] as [ - { startDate: Date; endDate: Date }, + { startDate: Date; endDate: Date; modified?: number }, (value: string | DateRange) => void, ]; } diff --git a/src/components/hooks/useFormat.ts b/src/components/hooks/useFormat.ts index f804eb728..06585e497 100644 --- a/src/components/hooks/useFormat.ts +++ b/src/components/hooks/useFormat.ts @@ -18,14 +18,19 @@ export function useFormat() { }; const formatRegion = (value: string): string => { - return regions[value] ? regions[value] : value; + const [country] = value.split('-'); + return regions[value] ? `${regions[value]}, ${countryNames[country]}` : value; + }; + + const formatCity = (value: string, country?: string): string => { + return `${value}, ${countryNames[country]}`; }; const formatDevice = (value: string): string => { return formatMessage(labels[value] || labels.unknown); }; - const formatValue = (value: string, type: string): string => { + const formatValue = (value: string, type: string, data?: { [key: string]: any }): string => { switch (type) { case 'browser': return formatBrowser(value); @@ -33,6 +38,8 @@ export function useFormat() { return formatCountry(value); case 'region': return formatRegion(value); + case 'city': + return formatCity(value, data?.country); case 'device': return formatDevice(value); default: diff --git a/src/components/layout/SideNav.tsx b/src/components/layout/SideNav.tsx index f38bdba07..0b5c98562 100644 --- a/src/components/layout/SideNav.tsx +++ b/src/components/layout/SideNav.tsx @@ -9,7 +9,7 @@ export interface SideNavProps { items: any[]; shallow?: boolean; scroll?: boolean; - className?: boolean; + className?: string; onSelect?: () => void; } diff --git a/src/components/metrics/CitiesTable.tsx b/src/components/metrics/CitiesTable.tsx index 69b899628..067e07e93 100644 --- a/src/components/metrics/CitiesTable.tsx +++ b/src/components/metrics/CitiesTable.tsx @@ -11,8 +11,8 @@ export function CitiesTable(props: MetricsTableProps) { const countryNames = useCountryNames(locale); const renderLabel = (city: string, country: string) => { - const name = countryNames[country]; - return name ? `${city}, ${name}` : city; + const countryName = countryNames[country]; + return countryName ? `${city}, ${countryName}` : city; }; const renderLink = ({ x: city, country }) => { diff --git a/src/components/metrics/MetricsTable.module.css b/src/components/metrics/MetricsTable.module.css index c00e4356c..f04d9ae43 100644 --- a/src/components/metrics/MetricsTable.module.css +++ b/src/components/metrics/MetricsTable.module.css @@ -6,13 +6,33 @@ flex: 1; } +.actions { + display: flex; + gap: 20px; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + .footer { display: flex; justify-content: center; } +.search { + max-width: 300px; +} + @media only screen and (max-width: 992px) { .container { min-height: auto; } + + .actions { + flex-direction: column; + } + + .search { + max-width: 100%; + } } diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index d4ad793d6..48beac687 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -1,5 +1,5 @@ -import { useMemo } from 'react'; -import { Loading, Icon, Text } from 'react-basics'; +import { ReactNode, useMemo, useState } from 'react'; +import { Loading, Icon, Text, SearchField } from 'react-basics'; import classNames from 'classnames'; import useApi from 'components/hooks/useApi'; import { percentFilter } from 'lib/filters'; @@ -12,6 +12,7 @@ import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; import Icons from 'components/icons'; import useMessages from 'components/hooks/useMessages'; import useLocale from 'components/hooks/useLocale'; +import useFormat from 'components//hooks/useFormat'; import styles from './MetricsTable.module.css'; export interface MetricsTableProps extends ListTableProps { @@ -22,6 +23,9 @@ export interface MetricsTableProps extends ListTableProps { limit?: number; delay?: number; onDataLoad?: (data: any) => void; + onSearch?: (search: string) => void; + allowSearch?: boolean; + children?: ReactNode; } export function MetricsTable({ @@ -32,8 +36,12 @@ export function MetricsTable({ limit, onDataLoad, delay = null, + allowSearch = false, + children, ...props }: MetricsTableProps) { + const [search, setSearch] = useState(''); + const { formatValue } = useFormat(); const [{ startDate, endDate, modified }] = useDateRange(websiteId); const { makeUrl, @@ -42,7 +50,6 @@ export function MetricsTable({ const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); const { dir } = useLocale(); - const { data, isLoading, isFetched, error } = useQuery({ queryKey: [ 'websites:metrics', @@ -94,24 +101,43 @@ export function MetricsTable({ } } + if (search) { + items = items.filter(({ x, ...data }) => { + const value = formatValue(x, type, data); + + return value.toLowerCase().includes(search.toLowerCase()); + }); + } + items = percentFilter(items); if (limit) { - items = items.filter((e, i) => i < limit); + items = items.slice(0, limit - 1); } return items; } return []; - }, [data, error, dataFilter, limit]); + }, [data, dataFilter, search, limit, formatValue, type]); return (
- {!data && isLoading && !isFetched && } {error && } +
+ {allowSearch && ( + + )} + {children} +
{data && !error && ( )} + {!data && isLoading && !isFetched && }
{data && !error && limit && ( diff --git a/src/components/metrics/OSTable.tsx b/src/components/metrics/OSTable.tsx index c39cba226..102bafd36 100644 --- a/src/components/metrics/OSTable.tsx +++ b/src/components/metrics/OSTable.tsx @@ -1,4 +1,4 @@ -import MetricsTable from './MetricsTable'; +import MetricsTable, { MetricsTableProps } from './MetricsTable'; import FilterLink from 'components/common/FilterLink'; import useMessages from 'components/hooks/useMessages'; @@ -8,7 +8,7 @@ const names = { 'Sun OS': 'SunOS', }; -export function OSTable({ websiteId, limit }: { websiteId: string; limit?: number }) { +export function OSTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); function renderLink({ x: os }) { @@ -28,12 +28,11 @@ export function OSTable({ websiteId, limit }: { websiteId: string; limit?: numbe return ( ); } diff --git a/src/components/metrics/PagesTable.tsx b/src/components/metrics/PagesTable.tsx index 236764676..11379a2e8 100644 --- a/src/components/metrics/PagesTable.tsx +++ b/src/components/metrics/PagesTable.tsx @@ -6,10 +6,10 @@ import useNavigation from 'components/hooks/useNavigation'; import { emptyFilter } from 'lib/filters'; export interface PagesTableProps extends MetricsTableProps { - showFilters?: boolean; + allowFilter?: boolean; } -export function PagesTable({ showFilters, ...props }: PagesTableProps) { +export function PagesTable({ allowFilter, ...props }: PagesTableProps) { const { router, makeUrl, @@ -37,17 +37,16 @@ export function PagesTable({ showFilters, ...props }: PagesTableProps) { }; return ( - <> - {showFilters && } - - + + {allowFilter && } + ); } diff --git a/src/components/metrics/QueryParametersTable.tsx b/src/components/metrics/QueryParametersTable.tsx index 65cac6640..904894603 100644 --- a/src/components/metrics/QueryParametersTable.tsx +++ b/src/components/metrics/QueryParametersTable.tsx @@ -13,9 +13,9 @@ const filters = { }; export function QueryParametersTable({ - showFilters, + allowFilter, ...props -}: { showFilters: boolean } & MetricsTableProps) { +}: { allowFilter: boolean } & MetricsTableProps) { const [filter, setFilter] = useState(FILTER_COMBINED); const { formatMessage, labels } = useMessages(); @@ -28,27 +28,26 @@ export function QueryParametersTable({ ]; return ( - <> - {showFilters && } - - filter === FILTER_RAW ? ( - x - ) : ( -
-
{safeDecodeURI(p)}
-
{safeDecodeURI(v)}
-
- ) - } - delay={0} - /> - + + filter === FILTER_RAW ? ( + x + ) : ( +
+
{safeDecodeURI(p)}
+
{safeDecodeURI(v)}
+
+ ) + } + delay={0} + > + {allowFilter && } +
); } From 765874731d51337c7bdac449e11176e6bc8af2e0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 10 Dec 2023 20:12:13 -0800 Subject: [PATCH 350/357] Added search to real-time activity log. --- .../[id]/realtime/RealtimeLog.module.css | 22 +++++++++ .../websites/[id]/realtime/RealtimeLog.tsx | 49 +++++++++++++++---- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css b/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css index f400cc1bc..e9c0fc1b5 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css @@ -66,3 +66,25 @@ .row .link:hover { color: var(--primary400); } + +.search { + max-width: 300px; +} + +.actions { + display: flex; + gap: 20px; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +@media only screen and (max-width: 992px) { + .actions { + flex-direction: column; + } + + .search { + max-width: 100%; + } +} diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx index 7cae0abca..5293c1f06 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx @@ -1,18 +1,19 @@ import { useMemo, useState } from 'react'; -import { StatusLight, Icon, Text } from 'react-basics'; +import { StatusLight, Icon, Text, SearchField } from 'react-basics'; import { FixedSizeList } from 'react-window'; +import { format } from 'date-fns'; import thenby from 'thenby'; +import { safeDecodeURI } from 'next-basics'; import FilterButtons from 'components/common/FilterButtons'; import Empty from 'components/common/Empty'; import useLocale from 'components/hooks/useLocale'; import useCountryNames from 'components/hooks/useCountryNames'; +import Icons from 'components/icons'; +import useMessages from 'components/hooks/useMessages'; +import useFormat from 'components//hooks/useFormat'; import { BROWSERS } from 'lib/constants'; import { stringToColor } from 'lib/format'; -import { safeDecodeURI } from 'next-basics'; -import Icons from 'components/icons'; import styles from './RealtimeLog.module.css'; -import useMessages from 'components/hooks/useMessages'; -import { format } from 'date-fns'; const TYPE_ALL = 'all'; const TYPE_PAGEVIEW = 'pageview'; @@ -26,7 +27,9 @@ const icons = { }; export function RealtimeLog({ data, websiteDomain }) { + const [search, setSearch] = useState(''); const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatValue } = useFormat(); const { locale } = useLocale(); const countryNames = useCountryNames(locale); const [filter, setFilter] = useState(TYPE_ALL); @@ -56,7 +59,15 @@ export function RealtimeLog({ data, websiteDomain }) { const getIcon = ({ __type }) => icons[__type]; - const getDetail = log => { + const getDetail = (log: { + __type: any; + eventName: any; + urlPath: any; + browser: any; + os: any; + country: any; + device: any; + }) => { const { __type, eventName, urlPath: url, browser, os, country, device } = log; if (__type === TYPE_EVENT) { @@ -130,18 +141,38 @@ export function RealtimeLog({ data, websiteDomain }) { } const { pageviews, visitors, events } = data; - const logs = [...pageviews, ...visitors, ...events].sort(thenby.firstBy('createdAt', -1)); + let logs = [...pageviews, ...visitors, ...events].sort(thenby.firstBy('createdAt', -1)); + + if (search) { + logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => { + return [ + eventName, + urlPath, + os, + formatValue(browser, 'browser'), + formatValue(country, 'country'), + formatValue(device, 'device'), + ] + .filter(n => n) + .map(n => n.toLowerCase()) + .join('') + .includes(search.toLowerCase()); + }); + } if (filter !== TYPE_ALL) { return logs.filter(({ __type }) => __type === filter); } return logs; - }, [data, filter]); + }, [data, filter, formatValue, search]); return (
- +
+ + +
{formatMessage(labels.activityLog)}
{logs?.length === 0 && } From a851ebf1247ac65e6246a20b72bafd07f31c61d2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 11 Dec 2023 00:15:26 -0800 Subject: [PATCH 351/357] Bump version 2.9.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db10755c9..0f437c355 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.8.0", + "version": "2.9.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", From 907685b96e0caae51846fc6dfe55d9ddfe16d7cd Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 12 Dec 2023 19:00:44 -0800 Subject: [PATCH 352/357] Added limit to metrics queries. --- .../(main)/websites/[id]/WebsiteExpandedView.tsx | 1 - src/components/metrics/MetricsTable.tsx | 1 + src/lib/prisma.ts | 6 +++++- src/lib/types.ts | 1 + src/pages/api/websites/[id]/metrics.ts | 7 +++++-- .../analytics/pageviews/getPageviewMetrics.ts | 14 ++++++++++---- .../analytics/sessions/getSessionMetrics.ts | 14 ++++++++++---- 7 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx index e97cd002c..9fb1b3f6f 100644 --- a/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx +++ b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx @@ -144,7 +144,6 @@ export default function WebsiteExpandedView({ relationalQuery(...args), @@ -13,7 +13,12 @@ export async function getPageviewMetrics( }); } -async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) { +async function relationalQuery( + websiteId: string, + column: string, + filters: QueryFilters, + limit: number = 100, +) { const { rawQuery, parseFilters } = prisma; const { filterQuery, joinSession, params } = await parseFilters( websiteId, @@ -42,7 +47,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query ${filterQuery} group by 1 order by 2 desc - limit 100 + limit ${limit} `, params, ); @@ -52,6 +57,7 @@ async function clickhouseQuery( websiteId: string, column: string, filters: QueryFilters, + limit: number = 100, ): Promise<{ x: string; y: number }[]> { const { rawQuery, parseFilters } = clickhouse; const { filterQuery, params } = await parseFilters(websiteId, { @@ -75,7 +81,7 @@ async function clickhouseQuery( ${filterQuery} group by x order by y desc - limit 100 + limit ${limit} `, params, ).then(a => { diff --git a/src/queries/analytics/sessions/getSessionMetrics.ts b/src/queries/analytics/sessions/getSessionMetrics.ts index 3573ac1e8..c6877a3f1 100644 --- a/src/queries/analytics/sessions/getSessionMetrics.ts +++ b/src/queries/analytics/sessions/getSessionMetrics.ts @@ -5,7 +5,7 @@ import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getSessionMetrics( - ...args: [websiteId: string, column: string, filters: QueryFilters] + ...args: [websiteId: string, column: string, filters: QueryFilters, limit?: number] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -13,7 +13,12 @@ export async function getSessionMetrics( }); } -async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) { +async function relationalQuery( + websiteId: string, + column: string, + filters: QueryFilters, + limit: number = 100, +) { const { parseFilters, rawQuery } = prisma; const { filterQuery, joinSession, params } = await parseFilters( websiteId, @@ -42,7 +47,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query group by 1 ${includeCountry ? ', 3' : ''} order by 2 desc - limit 100`, + limit ${limit}`, params, ); } @@ -51,6 +56,7 @@ async function clickhouseQuery( websiteId: string, column: string, filters: QueryFilters, + limit: number = 100, ): Promise<{ x: string; y: number }[]> { const { parseFilters, rawQuery } = clickhouse; const { filterQuery, params } = await parseFilters(websiteId, { @@ -73,7 +79,7 @@ async function clickhouseQuery( group by x ${includeCountry ? ', country' : ''} order by y desc - limit 100 + limit ${limit} `, params, ).then(a => { From 9735769413abab1a966249621459d5e9f8b2e5fe Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 12 Dec 2023 19:20:34 -0800 Subject: [PATCH 353/357] Removed Node 16 from GH workflow. --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 775f9ecf5..66e16a03e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,6 @@ jobs: strategy: matrix: include: - - node-version: 16.x - db-type: postgresql - - node-version: 16.x - db-type: mysql - node-version: 18.x db-type: postgresql - node-version: 18.x From e1c65cdf2ac6db01497a2bab922d8497a94ab457 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 12 Dec 2023 20:05:45 -0800 Subject: [PATCH 354/357] Updated loading for reports. --- src/app/(main)/reports/[id]/Report.tsx | 5 +++-- src/app/(main)/reports/[id]/ReportBody.tsx | 8 ++++++++ src/app/(main)/reports/[id]/ReportMenu.tsx | 8 ++++++++ src/components/hooks/useReport.ts | 10 +++++----- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/app/(main)/reports/[id]/Report.tsx b/src/app/(main)/reports/[id]/Report.tsx index b100ad8e1..c1cc502f9 100644 --- a/src/app/(main)/reports/[id]/Report.tsx +++ b/src/app/(main)/reports/[id]/Report.tsx @@ -1,5 +1,6 @@ 'use client'; import { createContext, ReactNode } from 'react'; +import { Loading } from 'react-basics'; import { useReport } from 'components/hooks'; import styles from './Report.module.css'; import classNames from 'classnames'; @@ -17,11 +18,11 @@ export function Report({ reportId, defaultParameters, children, className }: Rep const report = useReport(reportId, defaultParameters); if (!report) { - return null; + return reportId ? : null; } return ( - +
{children}
); diff --git a/src/app/(main)/reports/[id]/ReportBody.tsx b/src/app/(main)/reports/[id]/ReportBody.tsx index a116bf8ec..6f4627f68 100644 --- a/src/app/(main)/reports/[id]/ReportBody.tsx +++ b/src/app/(main)/reports/[id]/ReportBody.tsx @@ -1,6 +1,14 @@ import styles from './ReportBody.module.css'; +import { useContext } from 'react'; +import { ReportContext } from './Report'; export function ReportBody({ children }) { + const { report } = useContext(ReportContext); + + if (!report) { + return null; + } + return
{children}
; } diff --git a/src/app/(main)/reports/[id]/ReportMenu.tsx b/src/app/(main)/reports/[id]/ReportMenu.tsx index 72bc197aa..9478a9039 100644 --- a/src/app/(main)/reports/[id]/ReportMenu.tsx +++ b/src/app/(main)/reports/[id]/ReportMenu.tsx @@ -1,6 +1,14 @@ import styles from './ReportMenu.module.css'; +import { useContext } from 'react'; +import { ReportContext } from './Report'; export function ReportMenu({ children }) { + const { report } = useContext(ReportContext); + + if (!report) { + return null; + } + return
{children}
; } diff --git a/src/components/hooks/useReport.ts b/src/components/hooks/useReport.ts index 1686e222f..7769ed6c9 100644 --- a/src/components/hooks/useReport.ts +++ b/src/components/hooks/useReport.ts @@ -4,7 +4,7 @@ import { useTimezone } from './useTimezone'; import useApi from './useApi'; import useMessages from './useMessages'; -export function useReport(reportId, defaultParameters) { +export function useReport(reportId: string, defaultParameters: { [key: string]: any }) { const [report, setReport] = useState(null); const [isRunning, setIsRunning] = useState(false); const { get, post } = useApi(); @@ -17,7 +17,7 @@ export function useReport(reportId, defaultParameters) { parameters: {}, }; - const loadReport = async id => { + const loadReport = async (id: string) => { const data: any = await get(`/reports/${id}`); const { dateRange } = data?.parameters || {}; @@ -32,7 +32,7 @@ export function useReport(reportId, defaultParameters) { }; const runReport = useCallback( - async parameters => { + async (parameters: { [key: string]: any }) => { setIsRunning(true); const { type } = report; @@ -50,11 +50,11 @@ export function useReport(reportId, defaultParameters) { setIsRunning(false); }, - [report], + [report, timezone], ); const updateReport = useCallback( - async data => { + async (data: { [x: string]: any; parameters: any }) => { setReport( produce((state: any) => { const { parameters, ...rest } = data; From 442ad61779c80224e124a580b68df12715e32100 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 12 Dec 2023 21:23:12 -0800 Subject: [PATCH 355/357] Created admin API endpoints. --- .../(main)/settings/users/UsersDataTable.tsx | 2 +- .../settings/websites/WebsiteSettings.tsx | 6 +- .../settings/websites/WebsitesTable.tsx | 6 +- src/app/Providers.tsx | 5 +- src/pages/api/admin/users.ts | 53 +++++++++++++++ src/pages/api/admin/websites.ts | 66 +++++++++++++++++++ src/pages/api/users/index.ts | 16 +---- src/pages/api/websites/index.ts | 28 +------- 8 files changed, 133 insertions(+), 49 deletions(-) create mode 100644 src/pages/api/admin/users.ts create mode 100644 src/pages/api/admin/websites.ts diff --git a/src/app/(main)/settings/users/UsersDataTable.tsx b/src/app/(main)/settings/users/UsersDataTable.tsx index b77164515..2495d0232 100644 --- a/src/app/(main)/settings/users/UsersDataTable.tsx +++ b/src/app/(main)/settings/users/UsersDataTable.tsx @@ -11,7 +11,7 @@ export function UsersDataTable() { const modified = useCache((state: any) => state?.users); const queryResult = useFilterQuery({ queryKey: ['users', { modified }], - queryFn: (params: { [key: string]: any }) => get(`/users`, params), + queryFn: (params: { [key: string]: any }) => get(`/admin/users`, params), }); return ( diff --git a/src/app/(main)/settings/websites/WebsiteSettings.tsx b/src/app/(main)/settings/websites/WebsiteSettings.tsx index 4607b423c..0c5ce6142 100644 --- a/src/app/(main)/settings/websites/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/WebsiteSettings.tsx @@ -17,7 +17,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { const { formatMessage, labels, messages } = useMessages(); const { get, useQuery } = useApi(); const { showToast } = useToasts(); - const { websitesUrl, settingsUrl } = useContext(SettingsContext); + const { websitesUrl, websitesPath, settingsPath } = useContext(SettingsContext); const { data, isLoading } = useQuery({ queryKey: ['website', websiteId], queryFn: () => get(`${websitesUrl}/${websiteId}`), @@ -38,7 +38,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { const handleReset = async (value: string) => { if (value === 'delete') { - router.push(settingsUrl); + router.push(settingsPath); } else if (value === 'reset') { showSuccess(); } @@ -57,7 +57,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { return ( <> - +