chore(home-screen): fixes for loading states, flicker issue, and reduction of api calls (#11557)

* fixes for loading states, flicker issue, api calls

* fix filter bug

* add high res images

* bug fixes for cards and face pile, change imgs to svgs, and address comments

* update from comments

* add stopprop

* fix tests

* add liscenses

* remove unused type

* fix types

* add license

* fix lint
This commit is contained in:
Phillip Kelley-Dotson 2020-11-06 22:35:13 -05:00 committed by GitHub
parent a6bf95e30b
commit d8373f2bb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 730 additions and 324 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,30 @@
<!--
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.
-->
<svg width="119" height="78" viewBox="0 0 119 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="16" y="1" width="86" height="62" rx="3" stroke="#D1D1D1" stroke-width="2"/>
<mask id="path-2-inside-1" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M58 10.6416C58 9.53706 57.1014 8.63294 56.0006 8.72373C43.6813 9.73974 34 20.0601 34 32.6416C34 45.8965 44.7452 56.6416 58 56.6416C70.5815 56.6416 80.9019 46.9604 81.9179 34.641C82.0087 33.5402 81.1046 32.6416 80 32.6416L60 32.6416C58.8954 32.6416 58 31.7462 58 30.6416V10.6416Z"/>
</mask>
<path d="M60 32.6416V30.6416V32.6416ZM81.9179 34.641L83.9111 34.8054L81.9179 34.641ZM36 32.6416C36 21.1096 44.8743 11.6481 56.165 10.717L55.8362 6.7305C42.4882 7.83135 32 19.0106 32 32.6416H36ZM58 54.6416C45.8497 54.6416 36 44.7919 36 32.6416H32C32 47.001 43.6406 58.6416 58 58.6416V54.6416ZM79.9247 34.4766C78.9935 45.7673 69.5321 54.6416 58 54.6416V58.6416C71.631 58.6416 82.8103 48.1535 83.9111 34.8054L79.9247 34.4766ZM80 30.6416L60 30.6416V34.6416L80 34.6416V30.6416ZM60 30.6416V10.6416H56V30.6416H60ZM60 30.6416H56C56 32.8508 57.7909 34.6416 60 34.6416V30.6416ZM83.9111 34.8054C84.1069 32.4312 82.1629 30.6416 80 30.6416V34.6416C80.0053 34.6416 79.9929 34.6429 79.9743 34.6238C79.9641 34.6133 79.9495 34.594 79.9382 34.5638C79.926 34.5312 79.9228 34.499 79.9247 34.4766L83.9111 34.8054ZM56.165 10.717C56.1426 10.7188 56.1104 10.7156 56.0778 10.7034C56.0477 10.6922 56.0283 10.6775 56.0178 10.6673C55.9987 10.6487 56 10.6363 56 10.6416H60C60 8.47875 58.2104 6.53469 55.8362 6.7305L56.165 10.717Z" fill="#D1D1D1" mask="url(#path-2-inside-1)"/>
<mask id="path-4-inside-2" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M83.6717 28.1985C83.8558 29.3482 82.9306 30.3418 81.7663 30.3418H62C60.8954 30.3418 60 29.4464 60 28.3418V8.57554C60 7.41119 60.9937 6.48606 62.1434 6.67017C73.1842 8.43829 81.9035 17.1576 83.6717 28.1985Z"/>
</mask>
<path d="M83.6717 28.1985L81.6968 28.5147L83.6717 28.1985ZM81.7663 28.3418H62V32.3418H81.7663V28.3418ZM62 28.3418V8.57554H58V28.3418H62ZM61.8271 8.64501C72.0149 10.2765 80.0653 18.327 81.6968 28.5147L85.6465 27.8822C83.7418 15.9883 74.3535 6.60007 62.4596 4.69534L61.8271 8.64501ZM62 8.57554C62 8.56963 62.0009 8.58353 61.9803 8.60208C61.9692 8.61217 61.9483 8.62667 61.9159 8.63672C61.8805 8.64767 61.8474 8.64825 61.8271 8.64501L62.4596 4.69534C59.9821 4.29857 58 6.29722 58 8.57554H62ZM62 28.3418H62H58C58 30.551 59.7909 32.3418 62 32.3418V28.3418ZM81.7663 32.3418C84.0446 32.3418 86.0433 30.3597 85.6465 27.8822L81.6968 28.5147C81.6936 28.4945 81.6942 28.4613 81.7051 28.426C81.7152 28.3936 81.7297 28.3727 81.7398 28.3615C81.7583 28.3409 81.7722 28.3418 81.7663 28.3418V32.3418Z" fill="#D1D1D1" mask="url(#path-4-inside-2)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 55.5384C5.66793 57.9222 0 61.061 0 64.5001C0 71.956 26.6391 78.0001 59.5 78.0001C92.3609 78.0001 119 71.956 119 64.5001C119 60.9398 112.926 57.7014 103 55.2893V60C103 62.2092 101.209 64 99 64H19C16.7909 64 15 62.2092 15 60V55.5384Z" fill="#F2F2F2"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,26 @@
<!--
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.
-->
<svg width="119" height="78" viewBox="0 0 119 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17" y="1" width="86" height="62" rx="3" stroke="#D1D1D1" stroke-width="2"/>
<rect x="21" y="5" width="78" height="14" rx="3" stroke="#D1D1D1" stroke-width="2"/>
<rect x="21" y="23" width="38" height="36" rx="3" stroke="#D1D1D1" stroke-width="2"/>
<rect x="63" y="37" width="36" height="22" rx="3" stroke="#D1D1D1" stroke-width="2"/>
<rect x="63" y="23" width="36" height="10" rx="3" stroke="#D1D1D1" stroke-width="2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 55.2892C6.07439 57.7013 0 60.9397 0 64.5C0 71.9559 26.6391 78 59.5 78C92.3609 78 119 71.9559 119 64.5C119 61.0609 113.332 57.9221 104 55.5383V60C104 62.2092 102.209 64 100 64H20C17.7909 64 16 62.2092 16 60V55.2892Z" fill="#F2F2F2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,35 @@
<!--
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.
-->
<svg width="119" height="78" viewBox="0 0 119 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M37 4C37 2.34315 38.3431 1 40 1H70.3431C71.1388 1 71.9019 1.31607 72.4645 1.87868L82.1213 11.5355C82.6839 12.0981 83 12.8612 83 13.6569V60C83 61.6569 81.6569 63 80 63H40C38.3431 63 37 61.6569 37 60V4Z" stroke="#D1D1D1" stroke-width="2"/>
<path d="M71 0.5V9C71 11.2091 72.7909 13 75 13H83.5" stroke="#D1D1D1" stroke-width="2"/>
<path d="M71 27V45C71 46.074 70.1033 47.3156 68.0381 48.3482C66.0326 49.3509 63.1921 50 60 50C56.8079 50 53.9674 49.3509 51.9619 48.3482C49.8967 47.3156 49 46.074 49 45V27C49 25.926 49.8967 24.6844 51.9619 23.6518C53.9674 22.6491 56.8079 22 60 22C63.1921 22 66.0326 22.6491 68.0381 23.6518C70.1033 24.6844 71 25.926 71 27Z" stroke="#D1D1D1" stroke-width="2"/>
<mask id="path-4-inside-1" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M48 39C48 42.3137 53.3726 45 60 45C66.6274 45 72 42.3137 72 39H69.913C69.913 39.5358 69.4382 40.5104 67.5199 41.4695C65.7026 42.3781 63.0473 43 60 43C56.9527 43 54.2974 42.3781 52.4801 41.4695C50.5618 40.5104 50.087 39.5358 50.087 39H48Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48 39C48 42.3137 53.3726 45 60 45C66.6274 45 72 42.3137 72 39H69.913C69.913 39.5358 69.4382 40.5104 67.5199 41.4695C65.7026 42.3781 63.0473 43 60 43C56.9527 43 54.2974 42.3781 52.4801 41.4695C50.5618 40.5104 50.087 39.5358 50.087 39H48Z" fill="black"/>
<path d="M48 39V37H46V39H48ZM72 39H74V37H72V39ZM69.913 39V37H67.913V39H69.913ZM52.4801 41.4695L51.5856 43.2583H51.5856L52.4801 41.4695ZM50.087 39H52.087V37H50.087V39ZM46 39C46 41.8225 48.2361 43.8394 50.6203 45.0315C53.1241 46.2834 56.4431 47 60 47V43C56.9295 43 54.2485 42.3735 52.4091 41.4538C50.4502 40.4743 50 39.4912 50 39H46ZM60 47C63.5569 47 66.8759 46.2834 69.3797 45.0315C71.7639 43.8394 74 41.8225 74 39H70C70 39.4912 69.5498 40.4743 67.5909 41.4538C65.7515 42.3735 63.0705 43 60 43V47ZM72 37H69.913V41H72V37ZM68.4144 43.2583C70.5717 42.1796 71.913 40.6831 71.913 39H67.913C67.913 38.6884 68.0386 38.6285 67.8989 38.7962C67.7638 38.9584 67.4125 39.2871 66.6255 39.6806L68.4144 43.2583ZM60 45C63.2906 45 66.2649 44.333 68.4144 43.2583L66.6255 39.6806C65.1403 40.4232 62.804 41 60 41V45ZM51.5856 43.2583C53.7351 44.333 56.7094 45 60 45V41C57.196 41 54.8597 40.4232 53.3745 39.6806L51.5856 43.2583ZM48.087 39C48.087 40.6831 49.4283 42.1796 51.5856 43.2583L53.3745 39.6806C52.5875 39.2871 52.2362 38.9584 52.1011 38.7962C51.9614 38.6285 52.087 38.6884 52.087 39H48.087ZM50.087 37H48V41H50.087V37Z" fill="#D1D1D1" mask="url(#path-4-inside-1)"/>
<mask id="path-6-inside-2" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M48 33C48 36.3137 53.3726 39 60 39C66.6274 39 72 36.3137 72 33H69.913C69.913 33.5358 69.4382 34.5104 67.5199 35.4695C65.7026 36.3781 63.0473 37 60 37C56.9527 37 54.2974 36.3781 52.4801 35.4695C50.5618 34.5104 50.087 33.5358 50.087 33H48Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48 33C48 36.3137 53.3726 39 60 39C66.6274 39 72 36.3137 72 33H69.913C69.913 33.5358 69.4382 34.5104 67.5199 35.4695C65.7026 36.3781 63.0473 37 60 37C56.9527 37 54.2974 36.3781 52.4801 35.4695C50.5618 34.5104 50.087 33.5358 50.087 33H48Z" fill="black"/>
<path d="M48 33V31H46V33H48ZM72 33H74V31H72V33ZM69.913 33V31H67.913V33H69.913ZM52.4801 35.4695L51.5856 37.2583H51.5856L52.4801 35.4695ZM50.087 33H52.087V31H50.087V33ZM46 33C46 35.8225 48.2361 37.8394 50.6203 39.0315C53.1241 40.2834 56.4431 41 60 41V37C56.9295 37 54.2485 36.3735 52.4091 35.4538C50.4502 34.4743 50 33.4912 50 33H46ZM60 41C63.5569 41 66.8759 40.2834 69.3797 39.0315C71.7639 37.8394 74 35.8225 74 33H70C70 33.4912 69.5498 34.4743 67.5909 35.4538C65.7515 36.3735 63.0705 37 60 37V41ZM72 31H69.913V35H72V31ZM68.4144 37.2583C70.5717 36.1796 71.913 34.6831 71.913 33H67.913C67.913 32.6884 68.0386 32.6285 67.8989 32.7962C67.7638 32.9584 67.4125 33.2871 66.6255 33.6806L68.4144 37.2583ZM60 39C63.2906 39 66.2649 38.333 68.4144 37.2583L66.6255 33.6806C65.1403 34.4232 62.804 35 60 35V39ZM51.5856 37.2583C53.7351 38.333 56.7094 39 60 39V35C57.196 35 54.8597 34.4232 53.3745 33.6806L51.5856 37.2583ZM48.087 33C48.087 34.6831 49.4283 36.1796 51.5856 37.2583L53.3745 33.6806C52.5875 33.2871 52.2362 32.9584 52.1011 32.7962C51.9614 32.6285 52.087 32.6884 52.087 33H48.087ZM50.087 31H48V35H50.087V31Z" fill="#D1D1D1" mask="url(#path-6-inside-2)"/>
<path d="M71 27C71 28.074 70.1033 29.3156 68.0381 30.3482C66.0326 31.3509 63.1921 32 60 32C56.8079 32 53.9674 31.3509 51.9619 30.3482C49.8967 29.3156 49 28.074 49 27C49 25.926 49.8967 24.6844 51.9619 23.6518C53.9674 22.6491 56.8079 22 60 22C63.1921 22 66.0326 22.6491 68.0381 23.6518C70.1033 24.6844 71 25.926 71 27Z" stroke="#D1D1D1" stroke-width="2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M36 51.1343C14.8256 53.2784 0 58.2318 0 64C0 71.732 26.6391 78 59.5 78C92.3609 78 119 71.732 119 64C119 58.323 104.64 53.4353 84 51.2381V60C84 62.2091 82.2091 64 80 64H40C37.7909 64 36 62.2091 36 60V51.1343Z" fill="#F2F2F2"/>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,22 @@
<!--
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.
-->
<svg width="119" height="78" viewBox="0 0 119 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M59.2123 1.55857C59.4575 0.81382 60.5425 0.813801 60.7878 1.55857L67.5616 22.1322C67.9434 23.292 69.0328 24.0653 70.249 24.0653L92.1695 24.0653C92.9982 24.0653 93.2777 25.0719 92.6607 25.5143L74.9266 38.2295C73.9348 38.9406 73.5131 40.2085 73.8958 41.3707L80.6696 61.9444C80.8987 62.6402 80.076 63.3262 79.3906 62.8348L61.6566 50.1196C60.6679 49.4107 59.3321 49.4107 58.3434 50.1196L40.6094 62.8348C39.924 63.3262 39.1013 62.6402 39.3304 61.9444L46.1042 41.3707C46.4869 40.2085 46.0652 38.9406 45.0734 38.2295L27.3393 25.5143C26.7223 25.0719 27.0018 24.0653 27.8305 24.0653L49.751 24.0653C50.9672 24.0653 52.0566 23.292 52.4384 22.1322L59.2123 1.55857Z" stroke="#D5D5D5" stroke-width="2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.6782 51.616C17.5239 53.3343 0 58.4528 0 64.5C0 71.9558 26.6391 78 59.5 78C92.3609 78 119 71.9558 119 64.5C119 58.5387 101.97 53.4798 78.3466 51.6913L81.6194 61.6317C82.1664 63.2928 80.2398 64.6741 78.808 63.6475L61.1757 51.0053C60.619 51.0018 60.0604 51 59.5 51C59.2765 51 59.0534 51.0003 58.8305 51.0008L41.192 63.6475C39.7602 64.6741 37.8337 63.2928 38.3806 61.6317L41.6782 51.616Z" fill="#F2F2F2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,22 @@
<!--
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.
-->
<svg width="119" height="76" viewBox="0 0 119 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M83.1952 1.36598L103 24V62C103 64.2091 101.209 66 99 66H20C17.7909 66 16 64.2091 16 62V24L35.8048 1.36598C36.5643 0.497921 37.6616 0 38.8151 0H80.1849C81.3384 0 82.4357 0.497922 83.1952 1.36598ZM101 26V62C101 63.1046 100.105 64 99 64H20C18.8954 64 18 63.1046 18 62V26H35.25C37.8734 26 40 28.1266 40 30.75C40 34.4779 43.0221 37.5 46.75 37.5H72.25C75.9779 37.5 79 34.4779 79 30.75C79 28.1266 81.1266 26 83.75 26H101ZM100.342 24L81.6901 2.68299C81.3103 2.24896 80.7617 2 80.1849 2H38.8151C38.2383 2 37.6897 2.24896 37.3099 2.68299L18.6575 24H35.25C38.9779 24 42 27.0221 42 30.75C42 33.3734 44.1266 35.5 46.75 35.5H72.25C74.8734 35.5 77 33.3734 77 30.75C77 27.0221 80.0221 24 83.75 24H100.342Z" fill="#D1D1D1"/>
<path d="M16 53.2892C6.07439 55.7013 0 58.9397 0 62.5C0 69.9558 26.6391 76 59.5 76C92.3609 76 119 69.9558 119 62.5C119 58.9397 112.926 55.7013 103 53.2892V62C103 64.2091 101.209 66 99 66H20C17.7909 66 16 64.2091 16 62V53.2892Z" fill="#F2F2F2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -19,8 +19,6 @@
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';
@ -28,12 +26,8 @@ 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: [
const mockData = {
Viewed: [
{
slice_name: 'ChartyChart',
changed_on_utc: '24 Feb 2014 10:13:14',
@ -42,10 +36,7 @@ fetchMock.get(chartsEndpoint, {
table: {},
},
],
});
fetchMock.get(dashboardEndpoint, {
result: [
Edited: [
{
dashboard_title: 'Dashboard_Test',
changed_on_utc: '24 Feb 2014 10:13:14',
@ -53,18 +44,23 @@ fetchMock.get(dashboardEndpoint, {
id: '3',
},
],
});
fetchMock.get(savedQueryEndpoint, {
result: [],
});
Created: [
{
dashboard_title: 'Dashboard_Test',
changed_on_utc: '24 Feb 2014 10:13:14',
url: '/fakeUrl/dashboard',
id: '3',
},
],
};
describe('ActivityTable', () => {
const activityProps = {
user: {
userId: '1',
},
activityFilter: 'Edited',
activeChild: 'Edited',
activityData: mockData,
setActiveChild: jest.fn(),
user: { userId: '1' },
loading: false,
};
const wrapper = mount(<ActivityTable {...activityProps} />, {
context: { store },
@ -77,11 +73,10 @@ describe('ActivityTable', () => {
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);
it('renders tabs with three buttons', () => {
expect(wrapper.find('li')).toHaveLength(3);
});
it('it renders ActivityCards', async () => {
expect(wrapper.find('ListViewCard')).toExist();
});
});

View File

@ -22,6 +22,7 @@ import thunk from 'redux-thunk';
import fetchMock from 'fetch-mock';
import configureStore from 'redux-mock-store';
import { act } from 'react-dom/test-utils';
import ChartTable from 'src/views/CRUD/welcome/ChartTable';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
@ -64,8 +65,14 @@ describe('ChartTable', () => {
});
it('fetches chart favorites and renders chart cards ', async () => {
expect(fetchMock.calls(chartsEndpoint)).toHaveLength(1);
act(() => {
const handler = wrapper.find('li.no-router a').at(0).prop('onClick');
if (handler) {
handler({} as any);
}
});
await waitForComponentToPaint(wrapper);
expect(fetchMock.calls(chartsEndpoint)).toHaveLength(1);
expect(wrapper.find('ChartCard')).toExist();
});

View File

@ -24,7 +24,6 @@ import fetchMock from 'fetch-mock';
import { act } from 'react-dom/test-utils';
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';
@ -34,6 +33,7 @@ const store = mockStore({});
const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
const dashboardInfoEndpoint = 'glob:*/api/v1/dashboard/_info*';
const dashboardFavEndpoint = 'glob:*/api/v1/dashboard/favorite_status?*';
const mockDashboards = [
{
id: 1,
@ -47,6 +47,9 @@ fetchMock.get(dashboardsEndpoint, { result: mockDashboards });
fetchMock.get(dashboardInfoEndpoint, {
permissions: ['can_list', 'can_edit', 'can_delete'],
});
fetchMock.get(dashboardFavEndpoint, {
result: [],
});
describe('DashboardTable', () => {
const dashboardProps = {
@ -54,6 +57,7 @@ describe('DashboardTable', () => {
user: {
userId: '2',
},
mine: mockDashboards,
};
const wrapper = mount(<DashboardTable {...dashboardProps} />, {
context: { store },
@ -68,27 +72,34 @@ describe('DashboardTable', () => {
});
it('render a submenu with clickable tabs and buttons', async () => {
expect(wrapper.find(SubMenu)).toExist();
expect(wrapper.find('SubMenu')).toExist();
expect(wrapper.find('li')).toHaveLength(2);
expect(wrapper.find('Button')).toHaveLength(4);
act(() => {
wrapper.find('li').at(1).simulate('click');
const handler = wrapper.find('li.no-router a').at(1).prop('onClick');
if (handler) {
handler({} as any);
}
});
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 });
it('render DashboardCard', () => {
expect(wrapper.find(DashboardCard)).toExist();
});
it('display EmptyState if there is no data', () => {
fetchMock.resetHistory();
const wrapper = mount(<DashboardTable {...dashboardProps} />, {
context: { store },
});
const wrapper = mount(
<DashboardTable
dashboardFilter="Mine"
user={{ userId: '2' }}
mine={[]}
/>,
{
context: { store },
},
);
expect(wrapper.find('EmptyState')).toExist();
});
});

View File

@ -74,11 +74,22 @@ describe('SavedQueries', () => {
user: {
userId: '1',
},
mine: mockqueries,
};
const wrapper = mount(<SavedQueries {...savedQueryProps} />, {
context: { store },
});
const clickTab = (idx: number) => {
act(() => {
const handler = wrapper.find('li.no-router a').at(idx).prop('onClick');
if (handler) {
handler({} as any);
}
});
};
beforeAll(async () => {
await waitForComponentToPaint(wrapper);
});
@ -87,20 +98,19 @@ describe('SavedQueries', () => {
expect(wrapper.find(SavedQueries)).toExist();
});
it('fetches queries favorites and renders listviewcard cards', async () => {
clickTab(0);
await waitForComponentToPaint(wrapper);
expect(fetchMock.calls(/saved_query\/\?q/)).toHaveLength(1);
expect(wrapper.find('ListViewCard')).toExist();
});
it('it renders a submenu with clickable tables and buttons', async () => {
expect(wrapper.find(SubMenu)).toExist();
expect(wrapper.find('li')).toHaveLength(2);
expect(wrapper.find('button')).toHaveLength(2);
act(() => {
wrapper.find('li').at(1).simulate('click');
});
clickTab(1);
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();
expect(fetchMock.calls(/saved_query\/\?q/)).toHaveLength(2);
});
});

View File

@ -17,14 +17,46 @@
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
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 Welcome from 'src/views/CRUD/welcome/Welcome';
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('Welcome', () => {
const mockedProps = {
user: {
@ -37,7 +69,7 @@ describe('Welcome', () => {
isActive: true,
},
};
const wrapper = shallow(<Welcome {...mockedProps} />, {
const wrapper = mount(<Welcome {...mockedProps} />, {
context: { store },
});
@ -46,6 +78,13 @@ describe('Welcome', () => {
});
it('renders all panels on the page on page load', () => {
expect(wrapper.find('CollapsePanel')).toHaveLength(4);
expect(wrapper.find('CollapsePanel')).toHaveLength(8);
});
it('calls batch method on page load', () => {
const chartCall = fetchMock.calls(/chart\/\?q/);
const dashboardCall = fetchMock.calls(/dashboard\/\?q/);
expect(chartCall).toHaveLength(2);
expect(dashboardCall).toHaveLength(2);
});
});

View File

@ -61,8 +61,8 @@ export default function FacePile({ users, maxCount = 4 }: FacePileProps) {
borderColor: color,
}}
>
{first_name[0].toLocaleUpperCase()}
{last_name[0].toLocaleUpperCase()}
{first_name && first_name[0]?.toLocaleUpperCase()}
{last_name && last_name[0]?.toLocaleUpperCase()}
</StyledAvatar>
</Tooltip>
);

View File

@ -40,7 +40,7 @@ interface ImageLoaderProps
> {
fallback: string;
src: string;
isLoading: boolean;
isLoading?: boolean;
position: BackgroundPosition;
}

View File

@ -147,7 +147,7 @@ interface CardProps {
imgFallbackURL?: string;
imgPosition?: BackgroundPosition;
description: string;
loading: boolean;
loading?: boolean;
titleRight?: React.ReactNode;
coverLeft?: React.ReactNode;
coverRight?: React.ReactNode;

View File

@ -28,7 +28,7 @@ import Label from 'src/components/Label';
import { Dropdown, Menu } from 'src/common/components';
import FaveStar from 'src/components/FaveStar';
import FacePile from 'src/components/FacePile';
import { handleBulkChartExport, handleChartDelete } from '../utils';
import { handleChartDelete, handleBulkChartExport, CardStyles } from '../utils';
interface ChartCardProps {
chart: Chart;
@ -38,9 +38,11 @@ interface ChartCardProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
refreshData: () => void;
loading: boolean;
loading?: boolean;
saveFavoriteStatus: (id: number, isStarred: boolean) => void;
favoriteStatus: boolean;
chartFilter?: string;
userId?: number;
}
export default function ChartCard({
@ -54,6 +56,8 @@ export default function ChartCard({
loading,
saveFavoriteStatus,
favoriteStatus,
chartFilter,
userId,
}: ChartCardProps) {
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');
@ -78,6 +82,8 @@ export default function ChartCard({
addSuccessToast,
addDangerToast,
refreshData,
chartFilter,
userId,
)
}
>
@ -117,29 +123,40 @@ export default function ChartCard({
</Menu>
);
return (
<ListViewCard
loading={loading}
title={chart.slice_name}
url={bulkSelectEnabled ? undefined : chart.url}
imgURL={chart.thumbnail_url || ''}
imgFallbackURL="/static/assets/images/chart-card-fallback.png"
description={t('Last modified %s', chart.changed_on_delta_humanized)}
coverLeft={<FacePile users={chart.owners || []} />}
coverRight={
<Label bsStyle="secondary">{chart.datasource_name_text}</Label>
}
actions={
<ListViewCard.Actions>
<FaveStar
itemId={chart.id}
saveFaveStar={saveFavoriteStatus}
isStarred={favoriteStatus}
/>
<Dropdown overlay={menu}>
<Icon name="more-horiz" />
</Dropdown>
</ListViewCard.Actions>
}
/>
<CardStyles
onClick={() => {
window.location.href = chart.url;
}}
>
<ListViewCard
loading={loading}
title={chart.slice_name}
url={bulkSelectEnabled ? undefined : chart.url}
imgURL={chart.thumbnail_url || ''}
imgFallbackURL="/static/assets/images/chart-card-fallback.png"
description={t('Last modified %s', chart.changed_on_delta_humanized)}
coverLeft={<FacePile users={chart.owners || []} />}
coverRight={
<Label bsStyle="secondary">{chart.datasource_name_text}</Label>
}
actions={
<ListViewCard.Actions
onClick={e => {
e.stopPropagation();
e.preventDefault();
}}
>
<FaveStar
itemId={chart.id}
saveFaveStar={saveFavoriteStatus}
isStarred={favoriteStatus}
/>
<Dropdown overlay={menu}>
<Icon name="more-horiz" />
</Dropdown>
</ListViewCard.Actions>
}
/>
</CardStyles>
);
}

View File

@ -21,6 +21,7 @@ import { t } from '@superset-ui/core';
import {
handleDashboardDelete,
handleBulkDashboardExport,
CardStyles,
} from 'src/views/CRUD/utils';
import { Dropdown, Menu } from 'src/common/components';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
@ -31,7 +32,7 @@ import FacePile from 'src/components/FacePile';
import FaveStar from 'src/components/FaveStar';
import { Dashboard } from 'src/views/CRUD/types';
export interface DashboardCardProps {
interface DashboardCardProps {
isChart?: boolean;
dashboard: Dashboard;
hasPerm: (name: string) => boolean;
@ -43,13 +44,17 @@ export interface DashboardCardProps {
openDashboardEditModal?: (d: Dashboard) => void;
saveFavoriteStatus: (id: number, isStarred: boolean) => void;
favoriteStatus: boolean;
dashboardFilter?: string;
userId?: number;
}
function DashboardCard({
dashboard,
hasPerm,
bulkSelectEnabled,
dashboardFilter,
refreshData,
userId,
addDangerToast,
addSuccessToast,
openDashboardEditModal,
@ -99,6 +104,8 @@ function DashboardCard({
refreshData,
addSuccessToast,
addDangerToast,
dashboardFilter,
userId,
)
}
>
@ -119,28 +126,44 @@ function DashboardCard({
</Menu>
);
return (
<ListViewCard
loading={dashboard.loading || false}
title={dashboard.dashboard_title}
titleRight={<Label>{dashboard.published ? 'published' : 'draft'}</Label>}
url={bulkSelectEnabled ? undefined : dashboard.url}
imgURL={dashboard.thumbnail_url}
imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
description={t('Last modified %s', dashboard.changed_on_delta_humanized)}
coverLeft={<FacePile users={dashboard.owners || []} />}
actions={
<ListViewCard.Actions>
<FaveStar
itemId={dashboard.id}
saveFaveStar={saveFavoriteStatus}
isStarred={favoriteStatus}
/>
<Dropdown overlay={menu}>
<Icon name="more-horiz" />
</Dropdown>
</ListViewCard.Actions>
}
/>
<CardStyles
onClick={() => {
window.location.href = dashboard.url;
}}
>
<ListViewCard
loading={dashboard.loading || false}
title={dashboard.dashboard_title}
titleRight={
<Label>{dashboard.published ? 'published' : 'draft'}</Label>
}
url={bulkSelectEnabled ? undefined : dashboard.url}
imgURL={dashboard.thumbnail_url}
imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
description={t(
'Last modified %s',
dashboard.changed_on_delta_humanized,
)}
coverLeft={<FacePile users={dashboard.owners || []} />}
actions={
<ListViewCard.Actions
onClick={e => {
e.stopPropagation();
e.preventDefault();
}}
>
<FaveStar
itemId={dashboard.id}
saveFaveStar={saveFavoriteStatus}
isStarred={favoriteStatus}
/>
<Dropdown overlay={menu}>
<Icon name="more-horiz" />
</Dropdown>
</ListViewCard.Actions>
}
/>
</CardStyles>
);
}

View File

@ -80,7 +80,6 @@ function DashboardList(props: DashboardListProps) {
t('dashboard'),
props.addDangerToast,
);
const dashboardIds = useMemo(() => dashboards.map(d => d.id), [dashboards]);
const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
'dashboard',

View File

@ -39,10 +39,11 @@ export function useListViewResource<D extends object = any>(
resourceLabel: string, // resourceLabel for translations
handleErrorMsg: (errorMsg: string) => void,
infoEnable = true,
defaultCollectionValue: D[] = [],
) {
const [state, setState] = useState<ListViewResourceState<D>>({
count: 0,
collection: [],
collection: defaultCollectionValue,
loading: true,
lastFetchDataConfig: null,
permissions: [],
@ -164,10 +165,14 @@ export function useListViewResource<D extends object = any>(
hasPerm,
fetchData,
toggleBulkSelect,
refreshData: () => {
refreshData: (provideConfig?: FetchDataConfig) => {
if (state.lastFetchDataConfig) {
fetchData(state.lastFetchDataConfig);
return fetchData(state.lastFetchDataConfig);
}
if (provideConfig) {
return fetchData(provideConfig);
}
return null;
},
};
}

View File

@ -28,6 +28,7 @@ export interface DashboardTableProps {
addSuccessToast: (message: string) => void;
search: string;
user?: User;
mine: Array<Dashboard>;
}
export interface Dashboard {

View File

@ -26,6 +26,7 @@ import {
import Chart from 'src/types/Chart';
import rison from 'rison';
import getClientErrorObject from 'src/utils/getClientErrorObject';
import { FetchDataConfig } from 'src/components/ListView';
import { Dashboard } from './types';
const createFetchResourceMethod = (method: string) => (
@ -168,13 +169,33 @@ export function handleChartDelete(
{ id, slice_name: sliceName }: Chart,
addSuccessToast: (arg0: string) => void,
addDangerToast: (arg0: string) => void,
refreshData: () => void,
refreshData: (arg0?: FetchDataConfig | null) => void,
chartFilter?: string,
userId?: number,
) {
const filters = {
pageIndex: 0,
pageSize: 3,
sortBy: [
{
id: 'changed_on_delta_humanized',
desc: true,
},
],
filters: [
{
id: 'created_by',
operator: 'rel_o_m',
value: `${userId}`,
},
],
};
SupersetClient.delete({
endpoint: `/api/v1/chart/${id}`,
}).then(
() => {
refreshData();
if (chartFilter === 'Mine') refreshData(filters);
else refreshData();
addSuccessToast(t('Deleted: %s', sliceName));
},
() => {
@ -201,15 +222,35 @@ export function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
export function handleDashboardDelete(
{ id, dashboard_title: dashboardTitle }: Dashboard,
refreshData: () => void,
refreshData: (config?: FetchDataConfig | null) => void,
addSuccessToast: (arg0: string) => void,
addDangerToast: (arg0: string) => void,
dashboardFilter?: string,
userId?: number,
) {
return SupersetClient.delete({
endpoint: `/api/v1/dashboard/${id}`,
}).then(
() => {
refreshData();
const filters = {
pageIndex: 0,
pageSize: 3,
sortBy: [
{
id: 'changed_on_delta_humanized',
desc: true,
},
],
filters: [
{
id: 'owners',
operator: 'rel_m_m',
value: `${userId}`,
},
],
};
if (dashboardFilter === 'Mine') refreshData(filters);
else refreshData();
addSuccessToast(t('Deleted: %s', dashboardTitle));
},
createErrorHandler(errMsg =>
@ -220,25 +261,6 @@ export function handleDashboardDelete(
);
}
export function createChartDeleteFunction(
{ id, slice_name: sliceName }: Chart,
addSuccessToast: (arg0: string) => void,
addDangerToast: (arg0: string) => void,
refreshData: () => void,
) {
SupersetClient.delete({
endpoint: `/api/v1/chart/${id}`,
}).then(
() => {
refreshData();
addSuccessToast(t('Deleted: %s', sliceName));
},
() => {
addDangerToast(t('There was an issue deleting: %s', sliceName));
},
);
}
const breakpoints = [576, 768, 992, 1200];
export const mq = breakpoints.map(bp => `@media (max-width: ${bp}px)`);
@ -258,8 +280,15 @@ export const CardContainer = styled.div`
}
grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
justify-content: left;
padding: ${({ theme }) => theme.gridUnit * 2}px
${({ theme }) => theme.gridUnit * 6}px;
padding: ${({ theme }) => theme.gridUnit * 6}px;
padding-top: ${({ theme }) => theme.gridUnit * 2}px;
`;
export const CardStyles = styled.div`
cursor: pointer;
a {
text-decoration: none;
}
`;
export const IconContainer = styled.div`

View File

@ -16,15 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useState } from 'react';
import React from 'react';
import moment from 'moment';
import { styled, t } from '@superset-ui/core';
import { reject } from 'lodash';
import Loading from 'src/components/Loading';
import ListViewCard from 'src/components/ListViewCard';
import { addDangerToast } from 'src/messageToasts/actions';
import SubMenu from 'src/components/Menu/SubMenu';
import { getRecentAcitivtyObjs, mq } from '../utils';
import { ActivityData } from './Welcome';
import { mq, CardStyles } from '../utils';
import EmptyState from './EmptyState';
interface ActivityObjects {
@ -46,13 +46,10 @@ interface ActivityProps {
user: {
userId: string | number;
};
}
interface ActivityData {
Created?: Array<object>;
Edited?: Array<object>;
Viewed?: Array<object>;
Examples?: Array<object>;
activeChild: string;
setActiveChild: (arg0: string) => void;
loading: boolean;
activityData: ActivityData;
}
const ActivityContainer = styled.div`
@ -82,13 +79,12 @@ const ActivityContainer = styled.div`
}
`;
export default function ActivityTable({ user }: ActivityProps) {
const [activityData, setActivityData] = useState<ActivityData>({});
const [loading, setLoading] = useState(true);
const [activeChild, setActiveChild] = useState('Viewed');
// this api uses log for data which in some cases can be empty
const recent = `/superset/recent_activity/${user.userId}/?limit=5`;
export default function ActivityTable({
loading,
activeChild,
setActiveChild,
activityData,
}: ActivityProps) {
const getFilterTitle = (e: ActivityObjects) => {
if (e.dashboard_title) return e.dashboard_title;
if (e.label) return e.label;
@ -99,7 +95,7 @@ export default function ActivityTable({ user }: ActivityProps) {
const getIconName = (e: ActivityObjects) => {
if (e.sql) return 'sql';
if (e.url?.includes('dashboard')) {
if (e.url?.includes('dashboard') || e.item_url?.includes('dashboard')) {
return 'nav-dashboard';
}
if (e.url?.includes('explore') || e.item_url?.includes('explore')) {
@ -125,7 +121,7 @@ export default function ActivityTable({ user }: ActivityProps) {
},
];
if (activityData.Viewed) {
if (activityData?.Viewed) {
tabs.unshift({
name: 'Viewed',
label: t('Viewed'),
@ -143,53 +139,37 @@ export default function ActivityTable({ user }: ActivityProps) {
});
}
useEffect(() => {
getRecentAcitivtyObjs(user.userId, recent, addDangerToast)
.then(res => {
const data: any = {
Created: [
...res.createdByChart,
...res.createdByDash,
...res.createdByQuery,
],
Edited: [...res.editedChart, ...res.editedDash],
};
if (res.viewed) {
const filtered = reject(res.viewed, ['item_url', null]).map(r => r);
data.Viewed = filtered;
setActiveChild('Viewed');
} else {
data.Examples = res.examples;
setActiveChild('Examples');
}
setActivityData(data);
setLoading(false);
})
.catch(e => {
setLoading(false);
addDangerToast(
`There was an issue fetching your recent Acitivity: ${e}`,
);
});
}, []);
const renderActivity = () => {
return activityData[activeChild].map((e: ActivityObjects) => (
<ListViewCard
key={`${e.id}`}
loading={loading}
cover={<></>}
url={e.sql ? `/supserset/sqllab?queryId=${e.id}` : e.url}
title={getFilterTitle(e)}
description={`Last Edited: ${moment(e.changed_on_utc).format(
'MM/DD/YYYY HH:mm:ss',
)}`}
avatar={getIconName(e)}
actions={null}
/>
));
const getRecentRef = (e: ActivityObjects) => {
if (activeChild === 'Viewed') {
return e.item_url;
}
return e.sql ? `/superset/sqllab?savedQueryId=${e.id}` : e.url;
};
return activityData[activeChild].map((e: ActivityObjects) => {
return (
<CardStyles
onClick={() => {
window.location.href = getRecentRef(e);
}}
key={e.id}
>
<ListViewCard
loading={loading}
cover={<></>}
url={e.sql ? `/superset/sqllab?savedQueryId=${e.id}` : e.url}
title={getFilterTitle(e)}
description={`Last Edited: ${moment(e.changed_on_utc).format(
'MM/DD/YYYY HH:mm:ss',
)}`}
avatar={getIconName(e)}
actions={null}
/>
</CardStyles>
);
});
};
if (loading) return <>loading ...</>;
if (loading) return <Loading position="inline" />;
return (
<>
<SubMenu

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useState, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import { t } from '@superset-ui/core';
import {
useListViewResource,
@ -41,20 +41,28 @@ interface ChartTableProps {
search: string;
chartFilter?: string;
user?: User;
mine: Array<any>;
}
function ChartTable({
user,
addDangerToast,
addSuccessToast,
mine,
}: ChartTableProps) {
const {
state: { loading, resourceCollection: charts, bulkSelectEnabled },
state: { resourceCollection: charts, bulkSelectEnabled },
setResourceCollection: setCharts,
hasPerm,
refreshData,
fetchData,
} = useListViewResource<Chart>('chart', t('chart'), addDangerToast);
} = useListViewResource<Chart>(
'chart',
t('chart'),
addDangerToast,
true,
mine,
);
const chartIds = useMemo(() => charts.map(c => c.id), [charts]);
const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
'chart',
@ -70,10 +78,10 @@ function ChartTable({
const [chartFilter, setChartFilter] = useState('Mine');
const getFilters = () => {
const getFilters = (filterName: string) => {
const filters = [];
if (chartFilter === 'Mine') {
if (filterName === 'Mine') {
filters.push({
id: 'created_by',
operator: 'rel_o_m',
@ -89,8 +97,8 @@ function ChartTable({
return filters;
};
useEffect(() => {
fetchData({
const getData = (filter: string) => {
return fetchData({
pageIndex: 0,
pageSize: PAGE_SIZE,
sortBy: [
@ -99,9 +107,9 @@ function ChartTable({
desc: true,
},
],
filters: getFilters(),
filters: getFilters(filter),
});
}, [chartFilter]);
};
return (
<>
@ -121,12 +129,13 @@ function ChartTable({
{
name: 'Favorite',
label: t('Favorite'),
onClick: () => setChartFilter('Favorite'),
onClick: () =>
getData('Favorite').then(() => setChartFilter('Favorite')),
},
{
name: 'Mine',
label: t('Mine'),
onClick: () => setChartFilter('Mine'),
onClick: () => getData('Mine').then(() => setChartFilter('Mine')),
},
]}
buttons={[
@ -157,8 +166,9 @@ function ChartTable({
<ChartCard
key={`${e.id}`}
openChartEditModal={openChartEditModal}
loading={loading}
chartFilter={chartFilter}
chart={e}
userId={user?.userId}
hasPerm={hasPerm}
bulkSelectEnabled={bulkSelectEnabled}
refreshData={refreshData}

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useState, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import { SupersetClient, t } from '@superset-ui/core';
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import { Dashboard, DashboardTableProps } from 'src/views/CRUD/types';
@ -40,6 +40,7 @@ function DashboardTable({
user,
addDangerToast,
addSuccessToast,
mine,
}: DashboardTableProps) {
const {
state: { loading, resourceCollection: dashboards },
@ -51,6 +52,8 @@ function DashboardTable({
'dashboard',
t('dashboard'),
addDangerToast,
true,
mine,
);
const dashboardIds = useMemo(() => dashboards.map(c => c.id), [dashboards]);
const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
@ -83,10 +86,9 @@ function DashboardTable({
);
};
const getFilters = () => {
const getFilters = (filterName: string) => {
const filters = [];
if (dashboardFilter === 'Mine') {
if (filterName === 'Mine') {
filters.push({
id: 'owners',
operator: 'rel_m_m',
@ -110,8 +112,8 @@ function DashboardTable({
});
}
useEffect(() => {
fetchData({
const getData = (filter: string) => {
return fetchData({
pageIndex: 0,
pageSize: PAGE_SIZE,
sortBy: [
@ -120,9 +122,9 @@ function DashboardTable({
desc: true,
},
],
filters: getFilters(),
filters: getFilters(filter),
});
}, [dashboardFilter]);
};
return (
<>
@ -132,12 +134,16 @@ function DashboardTable({
{
name: 'Favorite',
label: t('Favorite'),
onClick: () => setDashboardFilter('Favorite'),
onClick: () => {
getData('Favorite').then(() => setDashboardFilter('Favorite'));
},
},
{
name: 'Mine',
label: t('Mine'),
onClick: () => setDashboardFilter('Mine'),
onClick: () => {
getData('Mine').then(() => setDashboardFilter('Mine'));
},
},
]}
buttons={[
@ -169,24 +175,30 @@ function DashboardTable({
onSubmit={handleDashboardEdit}
/>
)}
{dashboards.length > 0 ? (
{dashboards.length > 0 && (
<CardContainer>
{dashboards.map(e => (
<DashboardCard
key={e.id}
dashboard={e}
hasPerm={hasPerm}
bulkSelectEnabled={false}
dashboardFilter={dashboardFilter}
refreshData={refreshData}
addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast}
userId={user?.userId}
loading={loading}
openDashboardEditModal={dashboard => setEditModal(dashboard)}
openDashboardEditModal={(dashboard: Dashboard) =>
setEditModal(dashboard)
}
saveFavoriteStatus={saveFavoriteStatus}
favoriteStatus={favoriteStatus[e.id]}
/>
))}
</CardContainer>
) : (
)}
{dashboards.length === 0 && (
<EmptyState tableName="DASHBOARDS" tab={dashboardFilter} />
)}
</>

View File

@ -27,7 +27,9 @@ interface EmptyStateProps {
tableName: string;
tab?: string;
}
const EmptyContainer = styled.div`
min-height: 200px;
`;
const ButtonContainer = styled.div`
Button {
svg {
@ -48,10 +50,10 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) {
SAVED_QUERIES: '/savedqueryview/list/',
};
const tableIcon = {
RECENTS: 'union.png',
DASHBOARDS: 'empty-dashboard.png',
CHARTS: 'empty-charts.png',
SAVED_QUERIES: 'empty-queries.png',
RECENTS: 'union.svg',
DASHBOARDS: 'empty-dashboard.svg',
CHARTS: 'empty-charts.svg',
SAVED_QUERIES: 'empty-queries.svg',
};
const mine = (
<div>{`No ${
@ -90,55 +92,59 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) {
// Mine and Recent Activity(all tabs) tab empty state
if (tab === 'Mine' || tableName === 'RECENTS') {
return (
<Empty
image={`/static/assets/images/${tableIcon[tableName]}`}
description={tableName === 'RECENTS' ? recent : mine}
>
{tableName !== 'RECENTS' && (
<ButtonContainer>
<Button
buttonStyle="primary"
onClick={() => {
window.location = mineRedirects[tableName];
}}
>
<IconContainer>
<Icon name="plus-small" />{' '}
{tableName === 'SAVED_QUERIES'
? t('SQL QUERY')
: t(`${tableName
.split('')
.slice(0, tableName.length - 1)
.join('')}
<EmptyContainer>
<Empty
image={`/static/assets/images/${tableIcon[tableName]}`}
description={tableName === 'RECENTS' ? recent : mine}
>
{tableName !== 'RECENTS' && (
<ButtonContainer>
<Button
buttonStyle="primary"
onClick={() => {
window.location = mineRedirects[tableName];
}}
>
<IconContainer>
<Icon name="plus-small" />{' '}
{tableName === 'SAVED_QUERIES'
? t('SQL QUERY')
: t(`${tableName
.split('')
.slice(0, tableName.length - 1)
.join('')}
`)}
</IconContainer>
</Button>
</ButtonContainer>
)}
</Empty>
</IconContainer>
</Button>
</ButtonContainer>
)}
</Empty>
</EmptyContainer>
);
}
// Favorite tab empty state
return (
<Empty
image="/static/assets/images/star-circle.png"
description={
<div className="no-favorites">
{t("You don't have any favorites yet!")}
</div>
}
>
<Button
buttonStyle="primary"
onClick={() => {
window.location = favRedirects[tableName];
}}
<EmptyContainer>
<Empty
image="/static/assets/images/star-circle.svg"
description={
<div className="no-favorites">
{t("You don't have any favorites yet!")}
</div>
}
>
SEE ALL{' '}
{tableName === 'SAVED_QUERIES'
? t('SQL LAB QUERIES')
: t(`${tableName}`)}
</Button>
</Empty>
<Button
buttonStyle="primary"
onClick={() => {
window.location = favRedirects[tableName];
}}
>
SEE ALL{' '}
{tableName === 'SAVED_QUERIES'
? t('SQL LAB QUERIES')
: t(`${tableName}`)}
</Button>
</Empty>
</EmptyContainer>
);
}

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { t, SupersetClient, styled } from '@superset-ui/core';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import { Dropdown, Menu } from 'src/common/components';
@ -26,8 +26,12 @@ import DeleteModal from 'src/components/DeleteModal';
import Icon from 'src/components/Icon';
import SubMenu from 'src/components/Menu/SubMenu';
import EmptyState from './EmptyState';
import { IconContainer, CardContainer, createErrorHandler } from '../utils';
import {
IconContainer,
CardContainer,
createErrorHandler,
CardStyles,
} from '../utils';
const PAGE_SIZE = 3;
@ -50,6 +54,7 @@ interface SavedQueriesProps {
queryFilter: string;
addDangerToast: (arg0: string) => void;
addSuccessToast: (arg0: string) => void;
mine: Array<Query>;
}
const QueryData = styled.div`
@ -59,7 +64,7 @@ const QueryData = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
.title {
font-weight: ${({ theme }) => theme.typography.weights.normal};
color: ${({ theme }) => theme.colors.grayscale.light2};
color: ${({ theme }) => theme.colors.grayscale.light1};
}
.holder {
margin: ${({ theme }) => theme.gridUnit * 2}px;
@ -69,17 +74,24 @@ const SavedQueries = ({
user,
addDangerToast,
addSuccessToast,
mine,
}: SavedQueriesProps) => {
const {
state: { loading, resourceCollection: queries },
state: { resourceCollection: queries },
hasPerm,
fetchData,
refreshData,
} = useListViewResource<Query>('saved_query', t('query'), addDangerToast);
} = useListViewResource<Query>(
'saved_query',
t('query'),
addDangerToast,
true,
mine,
);
const [queryFilter, setQueryFilter] = useState('Mine');
const [queryDeleteModal, setQueryDeleteModal] = useState(false);
const [currentlyEdited, setCurrentlyEdited] = useState<Query>({});
const [ifMine, setMine] = useState(true);
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');
@ -88,7 +100,27 @@ const SavedQueries = ({
endpoint: `/api/v1/saved_query/${id}`,
}).then(
() => {
refreshData();
const queryParams = {
filters: [
{
id: 'created_by',
operator: 'rel_o_m',
value: `${user?.userId}`,
},
],
pageSize: PAGE_SIZE,
sortBy: [
{
id: 'changed_on_delta_humanized',
desc: true,
},
],
pageIndex: 0,
};
// if mine is default there refresh data with current filters
const filter = ifMine ? queryParams : undefined;
refreshData(filter);
setMine(false);
setQueryDeleteModal(false);
addSuccessToast(t('Deleted: %s', label));
},
@ -98,9 +130,9 @@ const SavedQueries = ({
);
};
const getFilters = () => {
const getFilters = (filterName: string) => {
const filters = [];
if (queryFilter === 'Mine') {
if (filterName === 'Mine') {
filters.push({
id: 'created_by',
operator: 'rel_o_m',
@ -116,8 +148,8 @@ const SavedQueries = ({
return filters;
};
useEffect(() => {
fetchData({
const getData = (filter: string) => {
return fetchData({
pageIndex: 0,
pageSize: PAGE_SIZE,
sortBy: [
@ -126,9 +158,9 @@ const SavedQueries = ({
desc: true,
},
],
filters: getFilters(),
filters: getFilters(filter),
});
}, [queryFilter]);
};
const renderMenu = (query: Query) => (
<Menu>
@ -186,12 +218,14 @@ const SavedQueries = ({
{
name: 'Favorite',
label: t('Favorite'),
onClick: () => setQueryFilter('Favorite'),
onClick: () => {
getData('Favorite').then(() => setQueryFilter('Favorite'));
},
},
{
name: 'Mine',
label: t('Mine'),
onClick: () => setQueryFilter('Mine'),
onClick: () => getData('Mine').then(() => setQueryFilter('Mine')),
},
]}
buttons={[
@ -218,35 +252,45 @@ const SavedQueries = ({
{queries.length > 0 ? (
<CardContainer>
{queries.map(q => (
<ListViewCard
key={`${q.id}`}
imgFallbackURL=""
imgURL=""
url={`/superset/sqllab?savedQueryId=${q.id}`}
title={q.label}
rows={q.rows}
loading={loading}
description={t('Last run ', q.end_time)}
cover={
<QueryData>
<div className="holder">
<div className="title">{t('Tables')}</div>
<div>{q?.sql_tables?.length}</div>
</div>
<div className="holder">
<div className="title">{t('Datasource Name')}</div>
<div>{q?.sql_tables && q.sql_tables[0]?.table}</div>
</div>
</QueryData>
}
actions={
<ListViewCard.Actions>
<Dropdown overlay={renderMenu(q)}>
<Icon name="more-horiz" />
</Dropdown>
</ListViewCard.Actions>
}
/>
<CardStyles
onClick={() => {
window.location.href = `/superset/sqllab?savedQueryId=${q.id}`;
}}
key={q.id}
>
<ListViewCard
imgFallbackURL=""
imgURL=""
url={`/superset/sqllab?savedQueryId=${q.id}`}
title={q.label}
rows={q.rows}
description={t('Last run ', q.end_time)}
cover={
<QueryData>
<div className="holder">
<div className="title">{t('Tables')}</div>
<div>{q?.sql_tables?.length}</div>
</div>
<div className="holder">
<div className="title">{t('Datasource Name')}</div>
<div>{q?.sql_tables && q.sql_tables[0]?.table}</div>
</div>
</QueryData>
}
actions={
<ListViewCard.Actions
onClick={e => {
e.stopPropagation();
e.preventDefault();
}}
>
<Dropdown overlay={renderMenu(q)}>
<Icon name="more-horiz" />
</Dropdown>
</ListViewCard.Actions>
}
/>
</CardStyles>
))}
</CardContainer>
) : (

View File

@ -16,11 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useEffect, useState } from 'react';
import { styled, t } from '@superset-ui/core';
import { Collapse } from 'src/common/components';
import { User } from 'src/types/bootstrapTypes';
import { mq } from '../utils';
import { reject } from 'lodash';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import Loading from 'src/components/Loading';
import { getRecentAcitivtyObjs, mq } from '../utils';
import ActivityTable from './ActivityTable';
import ChartTable from './ChartTable';
import SavedQueries from './SavedQueries';
@ -30,6 +34,17 @@ const { Panel } = Collapse;
interface WelcomeProps {
user: User;
addDangerToast: (arg0: string) => void;
}
export interface ActivityData {
Created?: Array<object>;
Edited?: Array<object>;
Viewed?: Array<object>;
Examples?: Array<object>;
myChart?: Array<object>;
myDash?: Array<object>;
myQuery?: Array<object>;
}
const WelcomeContainer = styled.div`
@ -70,25 +85,93 @@ const WelcomeContainer = styled.div`
font-weight: ${({ theme }) => theme.typography.weights.normal};
font-size: ${({ theme }) => theme.gridUnit * 4}px;
}
.ant-collapse-content-box {
min-height: 265px;
.loading.inline {
margin: ${({ theme }) => theme.gridUnit * 12}px auto;
display: block;
}
}
`;
export default function Welcome({ user }: WelcomeProps) {
function Welcome({ user, addDangerToast }: WelcomeProps) {
const recent = `/superset/recent_activity/${user.userId}/?limit=6`;
const [activeChild, setActiveChild] = useState('Viewed');
const [activityData, setActivityData] = useState<ActivityData>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
getRecentAcitivtyObjs(user.userId, recent, addDangerToast)
.then(res => {
const data: any = {
Created: [
...res.createdByChart,
...res.createdByDash,
...res.createdByQuery,
],
myChart: res.createdByChart,
myDash: res.createdByDash,
myQuery: res.createdByQuery,
Edited: [...res.editedChart, ...res.editedDash],
};
if (res.viewed) {
const filtered = reject(res.viewed, ['item_url', null]).map(r => r);
data.Viewed = filtered;
setActiveChild('Viewed');
} else {
data.Examples = res.examples;
setActiveChild('Examples');
}
setActivityData(data);
setLoading(false);
})
.catch(e => {
setLoading(false);
addDangerToast(
`There was an issue fetching your recent acitivity: ${e}`,
);
});
}, []);
return (
<WelcomeContainer>
<Collapse defaultActiveKey={['1', '2', '3', '4']} ghost>
<Panel header={t('Recents')} key="1">
<ActivityTable user={user} />
<ActivityTable
user={user}
activeChild={activeChild}
setActiveChild={setActiveChild}
loading={loading}
activityData={activityData}
/>
</Panel>
<Panel header={t('Dashboards')} key="2">
<DashboardTable user={user} />
{loading ? (
<Loading position="inline" />
) : (
<DashboardTable
user={user}
mine={activityData.myDash}
isLoading={loading}
/>
)}
</Panel>
<Panel header={t('Saved Queries')} key="3">
<SavedQueries user={user} />
{loading ? (
<Loading position="inline" />
) : (
<SavedQueries user={user} mine={activityData.myQuery} />
)}
</Panel>
<Panel header={t('Charts')} key="4">
<ChartTable user={user} />
{loading ? (
<Loading position="inline" />
) : (
<ChartTable user={user} mine={activityData.myChart} />
)}
</Panel>
</Collapse>
</WelcomeContainer>
);
}
export default withToasts(Welcome);