From f7051eaaded4e371f8a04f82915969bbaeb3763e Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Thu, 29 Oct 2020 21:59:31 -0700 Subject: [PATCH] feat: home screen mvp (#11206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * step 1: broken stuff! * first steps * more adding and slicing * step 1: broken stuff! * can now filter dashboards/charts for "Edited" tabs (filter by changed_by o_m) * more updates * update recent card * add icon * Adding Expand Icon to Collapse component * more updates * clean up code * remove lock file * remove consoles * fixing subnav button height shift * lil' ascii arrows * update branch * update test part 1 * remove consoles * fix typescript * add images and update emptystate * add changes * update chart card * fix css issues from rebase * add suggestions * more changes * update tests and clear typescript errors * Update superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx Co-authored-by: Evan Rusackas * update from comments * more updates.. * fix rebase * fix pesky type errors * test fixes * lint fix * Update superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx Co-authored-by: Evan Rusackas * Update superset-frontend/src/views/CRUD/welcome/EmptyState.tsx Co-authored-by: Evan Rusackas * Update superset-frontend/src/components/Menu/SubMenu.tsx Co-authored-by: Evan Rusackas * Update superset-frontend/src/components/ListViewCard/index.tsx Co-authored-by: ʈᵃᵢ * Update superset-frontend/src/components/ListViewCard/index.tsx Co-authored-by: ʈᵃᵢ * add suggestions * fix lint * remove unused code * toast getrecentActivityobjs * add some suggestions * remove types for now * cypress fix * remove unused type Co-authored-by: Evan Rusackas Co-authored-by: ʈᵃᵢ --- superset-frontend/images/empty-charts.png | Bin 0 -> 2274 bytes superset-frontend/images/empty-dashboard.png | Bin 0 -> 1467 bytes superset-frontend/images/empty-queries.png | Bin 0 -> 1791 bytes superset-frontend/images/star-circle.png | Bin 0 -> 2705 bytes superset-frontend/images/union.png | Bin 0 -> 1694 bytes .../javascripts/components/SubMenu_spec.jsx | 2 +- .../views/CRUD/chart/ChartList_spec.jsx | 1 + .../views/CRUD/welcome/ActivityTable_spec.tsx | 87 +++++ .../views/CRUD/welcome/ChartTable_spec.tsx | 79 +++++ .../CRUD/welcome/DashboardTable_spec.tsx | 82 +++-- .../views/CRUD/welcome/EmptyState_spec.tsx | 92 ++++++ .../views/CRUD/welcome/SavedQueries_spec.tsx | 106 ++++++ .../views/CRUD/welcome/Welcome_spec.tsx | 23 +- .../src/components/ListViewCard/index.tsx | 60 ++-- .../src/components/Menu/SubMenu.tsx | 45 ++- .../src/views/CRUD/chart/ChartCard.tsx | 138 ++++++++ .../src/views/CRUD/chart/ChartList.tsx | 144 ++------ .../views/CRUD/dashboard/DashboardCard.tsx | 140 ++++++++ .../views/CRUD/dashboard/DashboardList.tsx | 138 ++------ .../src/views/CRUD/data/common.ts | 2 +- .../CRUD/data/savedquery/SavedQueryList.tsx | 15 +- superset-frontend/src/views/CRUD/hooks.ts | 85 ++++- superset-frontend/src/views/CRUD/types.ts | 49 +++ superset-frontend/src/views/CRUD/utils.tsx | 197 +++++++++++ .../src/views/CRUD/welcome/ActivityTable.tsx | 209 ++++++++++++ .../src/views/CRUD/welcome/ChartTable.tsx | 167 ++++++++++ .../src/views/CRUD/welcome/DashboardTable.tsx | 309 +++++++++--------- .../src/views/CRUD/welcome/EmptyState.tsx | 144 ++++++++ .../src/views/CRUD/welcome/SavedQueries.tsx | 260 +++++++++++++++ .../src/views/CRUD/welcome/Welcome.tsx | 177 ++++------ superset/charts/api.py | 1 + superset/dashboards/api.py | 1 + superset/queries/api.py | 3 + superset/queries/saved_queries/api.py | 2 +- 34 files changed, 2184 insertions(+), 574 deletions(-) create mode 100644 superset-frontend/images/empty-charts.png create mode 100644 superset-frontend/images/empty-dashboard.png create mode 100644 superset-frontend/images/empty-queries.png create mode 100644 superset-frontend/images/star-circle.png create mode 100644 superset-frontend/images/union.png create mode 100644 superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx create mode 100644 superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx create mode 100644 superset-frontend/spec/javascripts/views/CRUD/welcome/EmptyState_spec.tsx create mode 100644 superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx create mode 100644 superset-frontend/src/views/CRUD/chart/ChartCard.tsx create mode 100644 superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx create mode 100644 superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx create mode 100644 superset-frontend/src/views/CRUD/welcome/ChartTable.tsx create mode 100644 superset-frontend/src/views/CRUD/welcome/EmptyState.tsx create mode 100644 superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx diff --git a/superset-frontend/images/empty-charts.png b/superset-frontend/images/empty-charts.png new file mode 100644 index 0000000000000000000000000000000000000000..b814d3574961e03990f31b52bf34c78d16347e8e GIT binary patch literal 2274 zcmV<82p#u{P)h+WA-jpRrOE`s1=oOVm4(z2vLK{7y2YA`M+@ii>({Sa_wV0-Dgh&Ca@B2ES3;|N45NFxi}RS$snWVR>^)SfdDQWK zYk2(zjw+pE}4%S-6#BYGuBb+vsSVncWNCy3G`iPLu$~f-{5_c)l`1?n= zjB!vK>_dv5NPq}Va;Zi!;f&ryyyn`8V+*J4tSn!WjA0j_)xc*xp%g>4S7CHL3dy<- zN^(_N_oYv4j7eXmq>+qiOgi3^#ucSMrgmT)T@AG$5CpkgTOm1=)}}tRrGfRfOd0r$ z4!=a=sp(J%Hcze^P5Y3Xn)KJzRjj91x=BH|Xn^1sye}~h^W>@;rfU;lm9!4S0KsMK z+L$-8Oq0uKl6)t2b=H9eC76~p-K;unfORnpv*glvQvbATfmjD!gI#c=X<0p+Bv%cV z2G2Q>)sYsHJ)0v}W4&C<=v$hN?dtlQ z+@DqHHAgPux^AL&0@#^JvnXxDyYVMS-=eEmwd}r?V{?0i;H}cNl?@7=jscF)>9D%ke*% zBABfKGMGwJlZ@a}d$?O9hG2$VOm?KFu}l$IBNrK=i#!n`Z&G&OO3BH6;6#5!DuiUWoSnUhRQwh59YYYfYawg5}y zI`tW4shTw%r-JvbVPtr7NwPyxbEfIv=Hy<2gj|_Yl|D#rNygAvIEgix?rJ88!D1{u zH5CZ>2_pV8ZTD?bm?8J{^fZ|uYf+w`psJ(EaH*y6BYjUwa7hAzcduY}Ri!L$1HOVsYbp2&()C>+PNxg{l2qFK5bhJ}|++2uE!xWD=&xRrMC*h`hcP z=Ab@vgd;}qBA47z!Ehl*Ez>j!C$*xv)mCAST+LICaYTp(H^dllPb4CAFc-zFux=^}T6WuJUV0-}iL|&od)f zYlN9)5lLSihv2H&C6|7$*LZ~Wu-mgBn4gKyCb4Iwq!I{B*XQ~L&)inK>Xrpg6>GN~ z6E(u)zU`4wUT$L$9yXN>+{Xr2+1kv!sBlv6$W&@OINU=6odG5OAFpoM!Vu@hV zY;o8W?#_#Lbuc<2nN$TR_;s!guYxnoA7Ji$o4;}@XbV`TynCgATh4VMr z8qht49hw&Qgc%APx!IT+XlpQ-*8P5w+PLiTtfV!oc@2_W2VHw>KFJEU*^NQ=q5PN! z&8@m$T*p@b2_^OEtheyoysoUQ6!3pY&XR`PcUI&&s3ccG z*t*+e9TCYkBY*A%DtM=2r1hUiF168LIBi3_ba9_OEI}g^Ju)dEG{Ty+u5JMjIXXH@ zo|`qfgYYT_6Djqd!~HXzCzif(7F!|IhexjeF#ALAf!sZ|3$mH^a6jhPhususK zxGT*FA)ze_#!YaUJdhYdxL{DAA!Fj*yLa;W^JnK{n(Opt0Wgk6Ha1zK z41#`Vfacb%TXOgAU6};n<0%ZU#sbuE4c8N;w9qVn`}QqwN^)ceYQ%f@?g@-pnH22m zz5uB`(CExWAi?k7zn4#+K1q&rAgMQR-jq9c?xZ4?V0jKDH#djp6a}x%PL4SOcs(e1 zy@PYi5uld03UGRNEq6!FF*bVS^Y-oAJ7;D(0vL5|Et~tSfdZuRN0px~EiEN`(C2(G zMyS}xI*(2T2O7n@aDRS-CXU^MLmlj#28@xm0I{vw#wR z9u~tM$YiR}?qw2s5i-f3P_mg*%v6^W!f~mwd2bKDSXnlyr9NYRahoq=2{?5A1>6gO)LKMgNg7kEK w^Ffjv5wX6ADGof2wK-^4pG!!+YT`!0|1A)>GBSY(DgXcg07*qoM6N<$f-u=G;Q#;t literal 0 HcmV?d00001 diff --git a/superset-frontend/images/empty-dashboard.png b/superset-frontend/images/empty-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..b0d44626d7590548d3f767eadb22c17bb1988cc6 GIT binary patch literal 1467 zcmV;s1w{IZP) zV{<8r2aM+bO^dQ!lF+3+mfGj5Qnh?5cj?pVmQNqsjxs>E+pU$$zH2s{uF8O;GD5MXQmLy%s%ZvKPfyJ(f|pcA5V41dSc?6K&g`Ny10|$J2g@FI z-*PlW`=d%p=E&{!dLCPdt!EuZgl8al9fJGV9W)w^s!B=b$n_Z)D|2u%rsr6Weh3=w zW;%8yWf*@BhS-)8v9Z9h`kBX!$4(i%!Q z^{OzdLPa4jt5=0u6)FmOTD>aFs!&nL(dt#PR)va!?WR}e}b#88M29D!+kB^V4T8;O&2eBg;LTk6T zx9{;Lr{!|F&#D2u*-pSybN4$`A>}K%=%*H23ws>L`7dl=vHOpDu4}GeUtc?ScXzKw zbfjEH=$Ozip6Bi1?pD-m^7cDaVHN%J?q{#pbGTu*I@Z6a8tNAMe2C|M#{U1<+SuxF za%Xw9c zP0Z!xrFwXH;81alVPRkXxMC7Luf_&J!fS-zI9;r)tf7hxVRWauV4<&#amljYH2B5X*mMzjf1G|X04Z{;H#@Eb$@@agltgi^769U*w{!d zmLPi17`e$VhlRpW@;~nhb^)*566VT%C0GZB<=;#A(0c;QBG?{0`dnLE`}lI1js(W8 zPJ_vR6BHCkWtQupQmK64?TOD|%uw;`glV%=!rb6RF~7k5D@J11zccUywn#1pj8y%t z{68VJGyQmE_Joqra1mjL$!aC!0Y+&DV*kU%@CQf@P5H)7jp=Cq36lAx<(3k@7k(bX z7kWHC^hS{=@9Co}F&NPl-H@m@#!Es8B3(3}Ad_p6Cs7d^Mk=|}XxZD4-Gb-_)0t>% zN{C`YB-4dx|AowR+|@}Va~dOZgBTOuKwH<+YYQfQBDFgvkpf*Em1ibI1~D_aQ9%6Y zcb~NQD#i1*{&g9i0D1nb9hi$p-zkViY%W?Cx39H4-p>^8U{(^tn2X%k&{01Ut?%N+ zTm>y_NY-%xEBf)#NuW=P@e^V_!3pB&`E49Z@T)`v8<1O3=Y>h~5Ghi}y4 VhmU(C((wQQ002ovPDHLkV1geIxW51Z literal 0 HcmV?d00001 diff --git a/superset-frontend/images/empty-queries.png b/superset-frontend/images/empty-queries.png new file mode 100644 index 0000000000000000000000000000000000000000..adc51c0814b80bf565aa82b8db714cb24581bbd2 GIT binary patch literal 1791 zcmV0eGEYG`;}X7eI3YxB+m28J_^;3m7h7dQQM=Z+aZTl0Xmy z8y@oicUNf|B!SeDMpfnfBZ}&G2z{N^Sy@@Fk~pZNqob%?F7GxPjb*G(%jk5B@^xux zsU{w(B#wz-$HvBvWVBG937=HZ=iniAQn{t`)#0;(+iu_Iw5GEL@l5D`17& zrF;aeaC?w%fE8|!@(HlQ?X1*ON58wJzwHZPh1)^sk}*2P0LEs!{XPIzxE%zzHej&@ z>)q_ef)V&w7l-6r_|Zm&5YHX?X9Lh}SZ>!ASt^y{#l^)>;)v98UlA;Go0bVHj=MP? z$5q53ITu@YO>BBm?Y(teJ{IkY)N%us!VOpoH()8;fTeH)mck8KL3V{qG-(Y1h-Cy8 z-sckS@=>M)`x5LsL0>k;dh9~x#~>9HTtKaq%jKA?5 zkB>D#jwJvE0hg9)QeoFDOh%PpnftKH?~uXJ{*>`MKV|yJ`gu7uNSFmlx=cd|wxXdY*%nB2L{Z!Q2 z%z4`Yy2qP^*EHAO6i*d`x4g^}QV!BHa$n>Cju9>G4NdO9_pNr>Y4Pe%p?m|Zh-I*BOnQxML!}NHQ z7hNHiWgPn+-5_?MDmXX27MAz9O*~gHZ~?ALPgk<;%sOGQJA?IRnhXZi+n$^68#8x` zK@1lM-+NV15h@c|ikZ-cx*}PCVhJUZ=S?jwxXuhm6dUB&*YKmGV(8I^4GpAyQ;J+` zSprZ<-tz7Mz2(0o0EOl)pTm**qTX_*;Jjr#LpM1~K)q%4mQ$&>?19uuWs;iZCjCx(H^42)*BBHvHZtG8^=rNyFr?&TTm(z`sY zf_mWzqf}zNWhtP%w=6x%MY!rMtG7H@U0q#8@}pUdI1`WSoDz~GiS(;}z2$y%b8~aZ znS+u|ZAs@Sp&ZZ5%>48lXQ*%u#EJ#Tsg%Zw+9rErp9CPb78Nd(%jM=qKxa#lpEgSH z37{X#kTfv3IglJt`PdF(An=N68vj4P8{Xh+7CfP&I?l}#{*Hb2)r5`v`+LC+AVpLd zE@6(3kFOdplVw^|MP#Rua&Ga4*+$?o$^ps=Zh#avy#GcopnHM~ptMW|)K_VluS{%K z^{4dB0I+0h;fZ9h{{(2(U4ecRt_2F1>!Q{Wpklm-B2VM=)Y0_!)0(P-QM1|9M}TaKt0Feu4Kvk`%c2$EIjT(?Drnz1r;4xY(HZUP9 z5(o6|-Mh5r&%QbIrFE2flTMoLF)=YA4k$8!Ka{SoulI~6?1Ua39^~TULTo;hFJg-` zKR++%`->xrOL)$*fp)41B;d=-OS!wd6W2wkY;9&|XI%nI)twv_;+HfxHz%t8MDJy2 zfos)BwarARg{mlSPq<{vNv1VYi&z$B88{j&?-(Jo9Bm>eW$T1~0E(-IdhMJC;15VIOlI14< zpP)a3DA_NfSdeKl{GdR#(@1hn0X_9DduZK=WLr6{a=T{{=uU zr-|vqgxE>WCKBgnC8WS{6a0cYP4_c16sX7^Z+^g{sUCcQMw{pyA=w!Mi8<&R)wKa; hx2MxmMgguH{Rh;dj}o!JRrCM=002ovPDHLkV1glzR2BdL literal 0 HcmV?d00001 diff --git a/superset-frontend/images/star-circle.png b/superset-frontend/images/star-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..77fd94d0fe1c4161dde5baf890808f72c399f7ed GIT binary patch literal 2705 zcmV;C3U2j@P)1f$;{w-T;sdfY?BgHvq~8uqpHSiwAxv z_yF-LY_K8cv~I&N$o7n+d9`$^h8|hcOlwZx)3Vkd!`Sa(AvuDrN#BuaXci@tiKK}vH7o-c&736}< zahB`r>ofZLFX;j}Pp`NC&v(w~;CD{VfJg@INEzPczYa|)uRzq)9^?j$M z*;#Ufk{k|L@HGgpi{sMlvgKOItu=cSWGs7Ew4l(Cf=yyx5#+se9+<}3CNjBfK%jJFBuXW znOp)^TM2&O6*xmKC6&^O)B4GiC+FjQHzds^aRkng3rQ{O>Aw;alx8?$HNDQHI09$L zwSfqfx1|NxIbWBg*$HwVKYn~gCD~tDS&1dd7N0(Ss#!5UsiddA=s;VG)tBi%CVs60=*HQC?GQ_hjEr2zb*4m< z*HZvzudYZ!7?6Yj3mDYXL|bR40fH;?EmJZPgj8)v5<+5@3&(`n{k}9>=Uw!RU6JL# z70{nwfT}H69|6xPvHm^LSs@VWt*@`o+ITO@>#M7)a~c)uDscP^Yo+67$*e-rrjmRX zsZ9BW92FlwYQnYYwauWb3^_#eG%;_E{ktTL)aG+(#qehDN+TnYvORn1)Tudf9FXB@ z{BEijBVUx^3H)wfQb6jH^-D`j*YN|V^<@QCJm;bns|??^BLolRP zVTNidEg23Y^BgL)@>vjT2l9n=w!#s)cB5sTKrRyGg5IPwoDN!X)HIpQ!7)6BeAlhh zl$UNMH`$Lx91~|Tc8!DFrM4DjOq|u&H4bu_x19GeacJzii27a3%4ymRxlT&Y+n6{H zW7jyy{fYw}6JN$DR}WC8f5XFEmXx6@8H}1_Id+YMLM$htOyhyxz%d~WZ*&;msHBO9 z4HjuJs&z`ALsYEZjJ~TAsOAe9PiUY!u zXbB{FLE@w_Zv9ygzP5k&5aKi6wS-HXlFWXR!3bQht$v%*jO$Df3=G_~b$s*Y4GS(J z^YI?b`+N`!x9N{e?d8}A-j^a0YswY*|TRnjU~yL!4XW&v0l@tpPBgQgWk1km#nO;u&(BF ztMxz)8xi{>*>;C=w<-mahrh6bLqpx4THs}0Lq*hl~XX03~ooD7XL)P0V@JH8h zk19f+_gf`51X_^XEpEVRaXx>~jGb?$bKb0rObr!-o&kihua* z`}gk+!D_{n%bBS2vv!t<*FAFNh@=1?M-6qo1)^(FzqBE_>Ll2QbzUPg9kf=PKZ%@W{((tP5?iMHmG@v(?l6&|Jv>YX5Y!{u#xDPdy-b%Y)?oMj{6xeHqaQdMj1Tb1*eVmSJDV7 zu)p#oC$JAIxTjQ5hVkGj!yYV0jvhT4TZXp+`%qptAk)j-4k`RO8(7I$n#1svV6UGY zvgC+ZN~5)0yQh?<*f5B3#|9-V0ZDnPyzcSi$G?tanS_qUbvtOqcJC_I7w=1sPy)d> zIe^X8j%fi&o~QJwwNB7!Vo-u|NvDa@nFi==9XfQ#STXK@fw{}vw1n^FRGKYDb2sG| zlw`z!N$gde-=X8eu`8{$Sx_QL?B2cGG$_3@OOgd9p3PfXnJ=a>j5f8Y`EEV6R`xHE!KM1Kt7Xat;lGg%pUfIsQS zJo|Y(OfUHTI6i<969h@J*%!Wkh5_Ze5@!F-&*XMFNe?~rP(=I>PK>2B*$Zf*00000 LNkvXXu0mjfgX0-L literal 0 HcmV?d00001 diff --git a/superset-frontend/images/union.png b/superset-frontend/images/union.png new file mode 100644 index 0000000000000000000000000000000000000000..af94c0793e9148478cb1883f824af8d4479277d3 GIT binary patch literal 1694 zcmV;P24VS$P)8h;CtRkO?>MVZR$f~FE=gZ1UGGZC6R;!%L<<5q~VObJFCX;FQ`~CC% z{r#p`L`E#4)9D-`sY!CF6pO{rVhPsBMTsqCv)OCxZezO~34KJlO5=_-=1+&%_W$+FXi7Qsq8Q#J(-POP#3R ze~bG_t9yHU=Mpoqms}OgR=E$cr)d;Rcw9I>zYoE%P91lw#9DHvV%bE7JUU8Xlj~yH;6xPzYiV;` zEE|rfVqh(DT`U`psAAyIa$PK|hbUs;&}}Z{4n;3*v_EY%57-mSQPDvKy9Y9Amb8Hm zX(&HNr`gP*#48sk?5oLH49mb;OASV_w_6a)$crYzUS(rg5@a+F_=o{M1dEAT3_oIW z1WrcttS@xcG`ea->q;&Id2E%*5gaAl8W{G;_Yt`FxUf%bY(i|TmVI!tYB+J1+*fUy zHE=wbDVmQO^C^~Mtz5;0N)N;?iu?z1FU4Z4mOD|^6~r$5X2SLxBDR-Y;tzC&sl(9cx9YGOIo%SF4!PL(I4 zLG^1DhQ!X)?M%<6hpgwJZ4e_$IOeKOEQk8O-rU?YFg)3jm_QZ$i@GJpnqxc$Y(0sP z%U&}|4o@g&E*zmsJVbgZF~j@R*iMsjb0iURi8`SbPECq`FPHu{Ea#M_xn_`^Y6yxNk7$-NyIJqgt$xShd zyDPLxJUl#P`u)ED^uf-~j{GcvQ)fA8V_;q&wJLeUH5vV=nD@mMHQaTtT= zpFhjiYV6Sg}L;+}zy!{92`;1fQ@<5El+57D(+P`LMOM2AC#%?E1vfU-E^VtiX}EVMRR^)4b%jHYH{l-1G&MOUh|@p^w1B0VHmkjb?_8jDKk zJhTMhN;VfGDMNITqa#QQ;#kBRkzCgb=@-cSA8$2-$ebp!AB~z2&UF&sDd~k3lm2F= z!-PZ%^i-*A>kv5$BfoqN@vO$TmG~Ws=W84^F9==$HpkjCr=}q96vRnvo{WCP<6g<* z` ({ fetchMock.get(chartsInfoEndpoint, { permissions: ['can_list', 'can_edit', 'can_delete'], }); + fetchMock.get(chartssOwnersEndpoint, { result: [], }); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx new file mode 100644 index 000000000..eba5d6686 --- /dev/null +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx @@ -0,0 +1,87 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { styledMount as mount } from 'spec/helpers/theming'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; + +import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; +import configureStore from 'redux-mock-store'; +import ActivityTable from 'src/views/CRUD/welcome/ActivityTable'; + +const mockStore = configureStore([thunk]); +const store = mockStore({}); + +const chartsEndpoint = 'glob:*/api/v1/chart/?*'; +const dashboardEndpoint = 'glob:*/api/v1/dashboard/?*'; +const savedQueryEndpoint = 'glob:*/api/v1/saved_query/?*'; + +fetchMock.get(chartsEndpoint, { + result: [ + { + slice_name: 'ChartyChart', + changed_on_utc: '24 Feb 2014 10:13:14', + url: '/fakeUrl/explore', + id: '4', + table: {}, + }, + ], +}); + +fetchMock.get(dashboardEndpoint, { + result: [ + { + dashboard_title: 'Dashboard_Test', + changed_on_utc: '24 Feb 2014 10:13:14', + url: '/fakeUrl/dashboard', + id: '3', + }, + ], +}); + +fetchMock.get(savedQueryEndpoint, { + result: [], +}); + +describe('ActivityTable', () => { + const activityProps = { + user: { + userId: '1', + }, + activityFilter: 'Edited', + }; + const wrapper = mount(, { + context: { store }, + }); + + beforeAll(async () => { + await waitForComponentToPaint(wrapper); + }); + + it('the component renders ', () => { + expect(wrapper.find(ActivityTable)).toExist(); + }); + + it('calls batch method and renders ListViewCArd', async () => { + const chartCall = fetchMock.calls(/chart\/\?q/); + const dashboardCall = fetchMock.calls(/dashboard\/\?q/); + expect(chartCall).toHaveLength(2); + expect(dashboardCall).toHaveLength(2); + }); +}); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx new file mode 100644 index 000000000..f8cd0531e --- /dev/null +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx @@ -0,0 +1,79 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { styledMount as mount } from 'spec/helpers/theming'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; +import configureStore from 'redux-mock-store'; + +import ChartTable from 'src/views/CRUD/welcome/ChartTable'; +import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; + +const mockStore = configureStore([thunk]); +const store = mockStore({}); + +const chartsEndpoint = 'glob:*/api/v1/chart/?*'; +const chartsInfoEndpoint = 'glob:*/api/v1/chart/_info*'; + +const mockCharts = [...new Array(3)].map((_, i) => ({ + changed_on_utc: new Date().toISOString(), + created_by: 'super user', + id: i, + slice_name: `cool chart ${i}`, + url: 'url', + viz_type: 'bar', + datasource_title: `ds${i}`, + thumbnail_url: '', +})); + +fetchMock.get(chartsEndpoint, { + result: mockCharts, +}); + +fetchMock.get(chartsInfoEndpoint, { + permissions: ['can_add', 'can_edit', 'can_delete'], +}); + +describe('ChartTable', () => { + const mockedProps = { + user: { + userId: '2', + }, + }; + const wrapper = mount(, { + context: { store }, + }); + it('it renders', () => { + expect(wrapper.find(ChartTable)).toExist(); + }); + + it('fetches chart favorites and renders chart cards ', async () => { + expect(fetchMock.calls(chartsEndpoint)).toHaveLength(1); + await waitForComponentToPaint(wrapper); + expect(wrapper.find('ChartCard')).toExist(); + }); + + it('display EmptyState if there is no data', () => { + fetchMock.resetHistory(); + const wrapper = mount(, { + context: { store }, + }); + expect(wrapper.find('EmptyState')).toExist(); + }); +}); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx index e09b5fea0..e7f28f3f9 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx @@ -17,48 +17,78 @@ * under the License. */ import React from 'react'; -import { mount } from 'enzyme'; +import { styledMount as mount } from 'spec/helpers/theming'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; -import { supersetTheme, ThemeProvider } from '@superset-ui/core'; +import { act } from 'react-dom/test-utils'; -import ListView from 'src/components/ListView'; +import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; +import SubMenu from 'src/components/Menu/SubMenu'; import DashboardTable from 'src/views/CRUD/welcome/DashboardTable'; +import DashboardCard from 'src/views/CRUD/dashboard/DashboardCard'; // store needed for withToasts(DashboardTable) const mockStore = configureStore([thunk]); const store = mockStore({}); -const dashboardsEndpoint = 'glob:*/api/v1/dashboard/*'; -const mockDashboards = [{ id: 1, url: 'url', dashboard_title: 'title' }]; +const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*'; +const dashboardInfoEndpoint = 'glob:*/api/v1/dashboard/_info*'; +const mockDashboards = [ + { + id: 1, + url: 'url', + dashboard_title: 'title', + changed_on_utc: '24 Feb 2014 10:13:14', + }, +]; fetchMock.get(dashboardsEndpoint, { result: mockDashboards }); - -function setup() { - // use mount because data fetching is triggered on mount - return mount(, { - context: { store }, - wrappingComponent: ThemeProvider, - wrappingComponentProps: { theme: supersetTheme }, - }); -} +fetchMock.get(dashboardInfoEndpoint, { + permissions: ['can_list', 'can_edit', 'can_delete'], +}); describe('DashboardTable', () => { - beforeEach(fetchMock.resetHistory); + const dashboardProps = { + dashboardFilter: 'Favorite', + user: { + userId: '2', + }, + }; + const wrapper = mount(, { + context: { store }, + }); - it('fetches dashboards and renders a ListView', () => { - return new Promise(done => { - const wrapper = setup(); + beforeAll(async () => { + await waitForComponentToPaint(wrapper); + }); - setTimeout(() => { - expect(fetchMock.calls(dashboardsEndpoint)).toHaveLength(1); - // there's a delay between response and updating state, so manually set it - // rather than adding a timeout which could introduce flakiness - wrapper.setState({ dashboards: mockDashboards }); - expect(wrapper.find(ListView)).toExist(); - done(); - }); + it('renders', () => { + expect(wrapper.find(DashboardTable)).toExist(); + }); + + it('render a submenu with clickable tabs and buttons', async () => { + expect(wrapper.find(SubMenu)).toExist(); + expect(wrapper.find('MenuItem')).toHaveLength(2); + expect(wrapper.find('Button')).toHaveLength(4); + act(() => { + wrapper.find('MenuItem').at(1).simulate('click'); }); + await waitForComponentToPaint(wrapper); + expect(fetchMock.calls(/dashboard\/\?q/)).toHaveLength(1); + }); + + it('fetches dashboards and renders a card', () => { + expect(fetchMock.calls(/dashboard\/\?q/)).toHaveLength(1); + wrapper.setState({ dashboards: mockDashboards }); + expect(wrapper.find(DashboardCard)).toExist(); + }); + + it('display EmptyState if there is no data', () => { + fetchMock.resetHistory(); + const wrapper = mount(, { + context: { store }, + }); + expect(wrapper.find('EmptyState')).toExist(); }); }); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/EmptyState_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/EmptyState_spec.tsx new file mode 100644 index 000000000..96ec1ec51 --- /dev/null +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/EmptyState_spec.tsx @@ -0,0 +1,92 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { styledMount as mount } from 'spec/helpers/theming'; +import EmptyState from 'src/views/CRUD/welcome/EmptyState'; + +describe('EmptyState', () => { + const variants = [ + { + tab: 'Favorite', + tableName: 'DASHBOARDS', + }, + { + tab: 'Mine', + tableName: 'DASHBOARDS', + }, + { + tab: 'Favorite', + tableName: 'CHARTS', + }, + { + tab: 'Mine', + tableName: 'CHARTS', + }, + { + tab: 'Favorite', + tableName: 'SAVED_QUERIES', + }, + { + tab: 'Mine', + tableName: 'SAVED_QUEREIS', + }, + ]; + const recents = [ + { + tab: 'Viewed', + tableName: 'RECENTS', + }, + { + tab: 'Edited', + tableName: 'RECENTS', + }, + { + tab: 'Created', + tableName: 'RECENTS', + }, + ]; + variants.forEach(variant => { + it(`it renders an ${variant.tab} ${variant.tableName} empty state`, () => { + const wrapper = mount(); + expect(wrapper).toExist(); + const textContainer = wrapper.find('.ant-empty-description'); + expect(textContainer.text()).toEqual( + variant.tab === 'Favorite' + ? "You don't have any favorites yet!" + : `No ${ + variant.tableName === 'SAVED_QUERIES' + ? 'saved queries' + : variant.tableName.toLowerCase() + } yet`, + ); + expect(wrapper.find('button')).toHaveLength(1); + }); + }); + recents.forEach(recent => { + it(`it renders an ${recent.tab} ${recent.tableName} empty state`, () => { + const wrapper = mount(); + expect(wrapper).toExist(); + const textContainer = wrapper.find('.ant-empty-description'); + expect(wrapper.find('.ant-empty-image').children()).toHaveLength(1); + expect(textContainer.text()).toContain( + `Recently ${recent.tab.toLowerCase()} charts, dashboards, and saved queries will appear here`, + ); + }); + }); +}); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx new file mode 100644 index 000000000..2670482d7 --- /dev/null +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx @@ -0,0 +1,106 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import thunk from 'redux-thunk'; +import { styledMount as mount } from 'spec/helpers/theming'; +import fetchMock from 'fetch-mock'; +import configureStore from 'redux-mock-store'; +import { act } from 'react-dom/test-utils'; + +import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; +import SubMenu from 'src/components/Menu/SubMenu'; +import SavedQueries from 'src/views/CRUD/welcome/SavedQueries'; + +// store needed for withToasts(DashboardTable) +const mockStore = configureStore([thunk]); +const store = mockStore({}); + +const queriesEndpoint = 'glob:*/api/v1/saved_query/?*'; +const savedQueriesInfo = 'glob:*/api/v1/saved_query/_info'; + +const mockqueries = [...new Array(3)].map((_, i) => ({ + created_by: { + id: i, + first_name: `user`, + last_name: `${i}`, + }, + created_on: `${i}-2020`, + database: { + database_name: `db ${i}`, + id: i, + }, + changed_on_delta_humanized: '1 day ago', + db_id: i, + description: `SQL for ${i}`, + id: i, + label: `query ${i}`, + schema: 'public', + sql: `SELECT ${i} FROM table`, + sql_tables: [ + { + catalog: null, + schema: null, + table: `${i}`, + }, + ], +})); + +fetchMock.get(queriesEndpoint, { + result: mockqueries, +}); + +fetchMock.get(savedQueriesInfo, { + permissions: ['can_list', 'can_edit', 'can_delete'], +}); + +describe('SavedQueries', () => { + const savedQueryProps = { + user: { + userId: '1', + }, + }; + + const wrapper = mount(, { + context: { store }, + }); + beforeAll(async () => { + await waitForComponentToPaint(wrapper); + }); + + it('is valid', () => { + expect(wrapper.find(SavedQueries)).toExist(); + }); + + it('it renders a submenu with clickable tables and buttons', async () => { + expect(wrapper.find(SubMenu)).toExist(); + expect(wrapper.find('MenuItem')).toHaveLength(2); + expect(wrapper.find('button')).toHaveLength(2); + act(() => { + wrapper.find('MenuItem').at(1).simulate('click'); + }); + + await waitForComponentToPaint(wrapper); + expect(fetchMock.calls(/saved_query\/\?q/)).toHaveLength(1); + }); + + it('fetches queries favorites and renders listviewcard cards', () => { + expect(fetchMock.calls(/saved_query\/\?q/)).toHaveLength(1); + expect(wrapper.find('ListViewCard')).toExist(); + }); +}); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx index bf23ef11a..4cd051c94 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx @@ -17,11 +17,14 @@ * under the License. */ import React from 'react'; -import { Panel, Row, Tab } from 'react-bootstrap'; import { shallow } from 'enzyme'; - +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; import Welcome from 'src/views/CRUD/welcome/Welcome'; +const mockStore = configureStore([thunk]); +const store = mockStore({}); + describe('Welcome', () => { const mockedProps = { user: { @@ -34,13 +37,15 @@ describe('Welcome', () => { isActive: true, }, }; - it('is valid', () => { - expect(React.isValidElement()).toBe(true); + const wrapper = shallow(, { + context: { store }, }); - it('renders 3 Tab, Panel, and Row components', () => { - const wrapper = shallow(); - expect(wrapper.find(Tab)).toHaveLength(3); - expect(wrapper.find(Panel)).toHaveLength(3); - expect(wrapper.find(Row)).toHaveLength(3); + + it('renders', () => { + expect(wrapper).toExist(); + }); + + it('renders all panels on the page on page load', () => { + expect(wrapper.find('CollapsePanel')).toHaveLength(4); }); }); diff --git a/superset-frontend/src/components/ListViewCard/index.tsx b/superset-frontend/src/components/ListViewCard/index.tsx index 4a0cf53ab..8849b4b00 100644 --- a/superset-frontend/src/components/ListViewCard/index.tsx +++ b/superset-frontend/src/components/ListViewCard/index.tsx @@ -143,15 +143,20 @@ const paragraphConfig = { rows: 1, width: 150 }; interface CardProps { title: React.ReactNode; url?: string; - imgURL: string; - imgFallbackURL: string; + imgURL?: string; + imgFallbackURL?: string; imgPosition?: BackgroundPosition; description: string; loading: boolean; titleRight?: React.ReactNode; coverLeft?: React.ReactNode; coverRight?: React.ReactNode; - actions: React.ReactNode; + actions: React.ReactNode | null; + showImg?: boolean; + rows?: number | string; + avatar?: string; + isRecent?: boolean; + renderCover?: React.ReactNode | null; } function ListViewCard({ @@ -162,35 +167,42 @@ function ListViewCard({ imgFallbackURL, description, coverLeft, + isRecent, coverRight, actions, + avatar, loading, imgPosition = 'top', + renderCover, }: CardProps) { return ( - -
- -
-
- - {!loading && coverLeft && ( - {coverLeft} - )} - {!loading && coverRight && ( - {coverRight} - )} - - + !isRecent + ? renderCover || ( + + +
+ +
+
+ + {!loading && coverLeft && ( + {coverLeft} + )} + {!loading && coverRight && ( + {coverRight} + )} + +
+ ) + : null } > {loading && ( @@ -230,6 +242,8 @@ function ListViewCard({ } description={description} + // @ts-ignore + avatar={avatar ? : null} /> )}
diff --git a/superset-frontend/src/components/Menu/SubMenu.tsx b/superset-frontend/src/components/Menu/SubMenu.tsx index e17ce7a2a..bf626f1c5 100644 --- a/superset-frontend/src/components/Menu/SubMenu.tsx +++ b/superset-frontend/src/components/Menu/SubMenu.tsx @@ -53,10 +53,23 @@ const StyledHeader = styled.header` li.active > a, li.active > div, li > a:hover, + li > a:focus, li > div:hover { - background-color: ${({ theme }) => theme.colors.secondary.light4}; + background: ${({ theme }) => theme.colors.secondary.light4}; border-bottom: none; - border-radius: 4px; + border-radius: ${({ theme }) => theme.borderRadius}px; + margin-bottom: ${({ theme }) => theme.gridUnit * 2}px; + } + } + .navbar-inverse { + .navbar-nav { + & > .active > a { + background: ${({ theme }) => theme.colors.secondary.light4}; + &:hover, + &:focus { + background: ${({ theme }) => theme.colors.secondary.light4}; + } + } } } `; @@ -64,8 +77,9 @@ const StyledHeader = styled.header` type MenuChild = { label: string; name: string; - url: string; + url?: string; usesRouter?: boolean; + onClick?: () => void; }; export interface ButtonProps { @@ -83,8 +97,8 @@ export interface ButtonProps { export interface SubMenuProps { buttons?: Array; - name: string; - children?: MenuChild[]; + name?: string; + tabs?: MenuChild[]; activeChild?: MenuChild['name']; /* If usesRouter is true, a react-router component will be used instead of href. * ONLY set usesRouter to true if SubMenu is wrapped in a react-router ; @@ -108,16 +122,16 @@ const SubMenu: React.FunctionComponent = props => { {props.name}