diff --git a/superset-frontend/src/views/CRUD/storageKeys.ts b/superset-frontend/src/views/CRUD/storageKeys.ts new file mode 100644 index 000000000..cd8f47640 --- /dev/null +++ b/superset-frontend/src/views/CRUD/storageKeys.ts @@ -0,0 +1,23 @@ +/** + * 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. + */ + +// storage keys for welcome page sticky tabs.. +export const HOMEPAGE_CHART_FILTER = 'homepage_chart_filter'; +export const HOMEPAGE_ACTIVITY_FILTER = 'homepage_activity_filter'; +export const HOMEPAGE_DASHBOARD_FILTER = 'homepage_dashboard_filter'; diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts index a312c4000..659686e8c 100644 --- a/superset-frontend/src/views/CRUD/types.ts +++ b/superset-frontend/src/views/CRUD/types.ts @@ -23,6 +23,11 @@ export type FavoriteStatus = { [id: number]: boolean; }; +export enum TableTabTypes { + FAVORITE = 'Favorite', + MINE = 'Mine', +} + export type Filters = { col: string; opr: string; diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.test.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.test.tsx index 9e08dbcdd..2fd7feb97 100644 --- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.test.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.test.tsx @@ -23,7 +23,6 @@ import { ReactWrapper } from 'enzyme'; import { Provider } from 'react-redux'; import fetchMock from 'fetch-mock'; import thunk from 'redux-thunk'; -import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; import configureStore from 'redux-mock-store'; import ActivityTable from 'src/views/CRUD/welcome/ActivityTable'; @@ -82,7 +81,7 @@ describe('ActivityTable', () => { activityData: mockData, setActiveChild: jest.fn(), user: { userId: '1' }, - loading: false, + loadedCount: 3, }; let wrapper: ReactWrapper; @@ -113,11 +112,13 @@ describe('ActivityTable', () => { handler({} as any); } }); - await waitForComponentToPaint(wrapper); const dashboardCall = fetchMock.calls(/dashboard\/\?q/); const chartCall = fetchMock.calls(/chart\/\?q/); - expect(chartCall).toHaveLength(1); - expect(dashboardCall).toHaveLength(1); + // waitforcomponenttopaint does not work here in this instance... + setTimeout(() => { + expect(chartCall).toHaveLength(1); + expect(dashboardCall).toHaveLength(1); + }); }); it('show empty state if there is no data', () => { const activityProps = { @@ -125,7 +126,7 @@ describe('ActivityTable', () => { activityData: {}, setActiveChild: jest.fn(), user: { userId: '1' }, - loading: false, + loadedCount: 3, }; const wrapper = mount( diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx index b613463c5..288488d2d 100644 --- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx @@ -24,9 +24,10 @@ import { setInLocalStorage } from 'src/utils/localStorageHelpers'; import Loading from 'src/components/Loading'; import ListViewCard from 'src/components/ListViewCard'; import SubMenu from 'src/components/Menu/SubMenu'; +import { mq, CardStyles, getEditedObjects } from 'src/views/CRUD/utils'; +import { HOMEPAGE_ACTIVITY_FILTER } from 'src/views/CRUD/storageKeys'; import { Chart } from 'src/types/Chart'; import { Dashboard, SavedQueryObject } from 'src/views/CRUD/types'; -import { mq, CardStyles, getEditedObjects } from 'src/views/CRUD/utils'; import { ActivityData } from './Welcome'; import EmptyState from './EmptyState'; @@ -51,6 +52,12 @@ interface RecentDashboard extends RecentActivity { item_type: 'dashboard'; } +enum SetTabType { + EDITED = 'Edited', + CREATED = 'Created', + VIEWED = 'Viewed', + EXAMPLE = 'Examples', +} /** * Recent activity objects fetched by `getRecentAcitivtyObjs`. */ @@ -68,6 +75,7 @@ interface ActivityProps { activeChild: string; setActiveChild: (arg0: string) => void; activityData: ActivityData; + loadedCount: number; } const ActivityContainer = styled.div` @@ -161,20 +169,11 @@ export default function ActivityTable({ setActiveChild, activityData, user, + loadedCount, }: ActivityProps) { const [editedObjs, setEditedObjs] = useState>(); const [loadingState, setLoadingState] = useState(false); - useEffect(() => { - if (activeChild === 'Edited') { - setLoadingState(true); - getEditedObjects(user.userId).then(r => { - setEditedObjs([...r.editedChart, ...r.editedDash]); - setLoadingState(false); - }); - } - }, []); - const getEditedCards = () => { setLoadingState(true); getEditedObjects(user.userId).then(r => { @@ -182,14 +181,21 @@ export default function ActivityTable({ setLoadingState(false); }); }; + + useEffect(() => { + if (activeChild === 'Edited') { + setLoadingState(true); + getEditedCards(); + } + }, [activeChild]); + const tabs = [ { name: 'Edited', label: t('Edited'), onClick: () => { setActiveChild('Edited'); - setInLocalStorage('activity', { activity: 'Edited' }); - getEditedCards(); + setInLocalStorage(HOMEPAGE_ACTIVITY_FILTER, SetTabType.EDITED); }, }, { @@ -197,7 +203,7 @@ export default function ActivityTable({ label: t('Created'), onClick: () => { setActiveChild('Created'); - setInLocalStorage('activity', { activity: 'Created' }); + setInLocalStorage(HOMEPAGE_ACTIVITY_FILTER, SetTabType.CREATED); }, }, ]; @@ -208,7 +214,7 @@ export default function ActivityTable({ label: t('Viewed'), onClick: () => { setActiveChild('Viewed'); - setInLocalStorage('activity', { activity: 'Viewed' }); + setInLocalStorage(HOMEPAGE_ACTIVITY_FILTER, SetTabType.VIEWED); }, }); } else { @@ -217,7 +223,7 @@ export default function ActivityTable({ label: t('Examples'), onClick: () => { setActiveChild('Examples'); - setInLocalStorage('activity', { activity: 'Examples' }); + setInLocalStorage(HOMEPAGE_ACTIVITY_FILTER, SetTabType.EXAMPLE); }, }); } @@ -246,16 +252,15 @@ export default function ActivityTable({ ); }, ); - if (loadingState && !editedObjs) { + + const doneFetching = loadedCount < 3; + + if ((loadingState && !editedObjs) || doneFetching) { return ; } return ( <> - + {activityData[activeChild]?.length > 0 || (activeChild === 'Edited' && editedObjs && editedObjs.length > 0) ? ( {renderActivity()} diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.test.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.test.tsx index 3af468a60..986a5bf94 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.test.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.test.tsx @@ -85,11 +85,21 @@ describe('ChartTable', () => { } }); await waitForComponentToPaint(wrapper); - expect(fetchMock.calls(chartsEndpoint)).toHaveLength(1); + expect(fetchMock.calls(chartsEndpoint)).toHaveLength(3); expect(wrapper.find('ChartCard')).toExist(); }); it('display EmptyState if there is no data', async () => { + await act(async () => { + wrapper = mount( + , + ); + }); expect(wrapper.find('EmptyState')).toExist(); }); }); diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx index dd574413f..3fa7caad1 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx @@ -29,8 +29,11 @@ import { } from 'src/utils/localStorageHelpers'; import withToasts from 'src/messageToasts/enhancers/withToasts'; import { useHistory } from 'react-router-dom'; +import { TableTabTypes } from 'src/views/CRUD/types'; import PropertiesModal from 'src/explore/components/PropertiesModal'; import { User } from 'src/types/bootstrapTypes'; +import { CardContainer } from 'src/views/CRUD/utils'; +import { HOMEPAGE_CHART_FILTER } from 'src/views/CRUD/storageKeys'; import ChartCard from 'src/views/CRUD/chart/ChartCard'; import Chart from 'src/types/Chart'; import handleResourceExport from 'src/utils/export'; @@ -38,7 +41,6 @@ import Loading from 'src/components/Loading'; import ErrorBoundary from 'src/components/ErrorBoundary'; import SubMenu from 'src/components/Menu/SubMenu'; import EmptyState from './EmptyState'; -import { CardContainer } from '../utils'; const PAGE_SIZE = 3; @@ -60,6 +62,9 @@ function ChartTable({ showThumbnails, }: ChartTableProps) { const history = useHistory(); + const filterStore = getFromLocalStorage(HOMEPAGE_CHART_FILTER, null); + const initialFilter = filterStore || TableTabTypes.MINE; + const { state: { loading, resourceCollection: charts, bulkSelectEnabled }, setResourceCollection: setCharts, @@ -71,12 +76,11 @@ function ChartTable({ t('chart'), addDangerToast, true, - mine, + initialFilter === 'Favorite' ? [] : mine, [], false, ); - useEffect(() => {}); const chartIds = useMemo(() => charts.map(c => c.id), [charts]); const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus( 'chart', @@ -90,15 +94,12 @@ function ChartTable({ closeChartEditModal, } = useChartEditModal(setCharts, charts); - const [chartFilter, setChartFilter] = useState('Mine'); + const [chartFilter, setChartFilter] = useState(initialFilter); const [preparingExport, setPreparingExport] = useState(false); useEffect(() => { - const filter = getFromLocalStorage('chart', null); - if (!filter) { - setChartFilter('Mine'); - } else setChartFilter(filter.tab); - }, []); + getData(chartFilter); + }, [chartFilter]); const handleBulkChartExport = (chartsToExport: Chart[]) => { const ids = chartsToExport.map(({ id }) => id); @@ -159,20 +160,18 @@ function ChartTable({ { name: 'Favorite', label: t('Favorite'), - onClick: () => - getData('Favorite').then(() => { - setChartFilter('Favorite'); - setInLocalStorage('chart', { tab: 'Favorite' }); - }), + onClick: () => { + setChartFilter('Favorite'); + setInLocalStorage(HOMEPAGE_CHART_FILTER, TableTabTypes.FAVORITE); + }, }, { name: 'Mine', label: t('Mine'), - onClick: () => - getData('Mine').then(() => { - setChartFilter('Mine'); - setInLocalStorage('chart', { tab: 'Mine' }); - }), + onClick: () => { + setChartFilter('Mine'); + setInLocalStorage(HOMEPAGE_CHART_FILTER, TableTabTypes.MINE); + }, }, ]} buttons={[ diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx index f53408a0f..8db07c98a 100644 --- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx @@ -19,20 +19,26 @@ import React, { useState, useMemo, useEffect } 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'; +import { + Dashboard, + DashboardTableProps, + TableTabTypes, +} from 'src/views/CRUD/types'; import handleResourceExport from 'src/utils/export'; import { useHistory } from 'react-router-dom'; import { setInLocalStorage, getFromLocalStorage, } from 'src/utils/localStorageHelpers'; +import { createErrorHandler, CardContainer } from 'src/views/CRUD/utils'; +import { HOMEPAGE_DASHBOARD_FILTER } from 'src/views/CRUD/storageKeys'; + import withToasts from 'src/messageToasts/enhancers/withToasts'; import Loading from 'src/components/Loading'; import PropertiesModal from 'src/dashboard/components/PropertiesModal'; import DashboardCard from 'src/views/CRUD/dashboard/DashboardCard'; import SubMenu from 'src/components/Menu/SubMenu'; import EmptyState from './EmptyState'; -import { createErrorHandler, CardContainer } from '../utils'; const PAGE_SIZE = 3; @@ -50,6 +56,9 @@ function DashboardTable({ showThumbnails, }: DashboardTableProps) { const history = useHistory(); + const filterStore = getFromLocalStorage(HOMEPAGE_DASHBOARD_FILTER, null); + const defaultFilter = filterStore || TableTabTypes.MINE; + const { state: { loading, resourceCollection: dashboards }, setResourceCollection: setDashboards, @@ -61,7 +70,7 @@ function DashboardTable({ t('dashboard'), addDangerToast, true, - mine, + defaultFilter === 'Favorite' ? [] : mine, [], false, ); @@ -71,16 +80,14 @@ function DashboardTable({ dashboardIds, addDangerToast, ); + const [editModal, setEditModal] = useState(); - const [dashboardFilter, setDashboardFilter] = useState('Mine'); + const [dashboardFilter, setDashboardFilter] = useState(defaultFilter); const [preparingExport, setPreparingExport] = useState(false); useEffect(() => { - const filter = getFromLocalStorage('dashboard', null); - if (!filter) { - setDashboardFilter('Mine'); - } else setDashboardFilter(filter.tab); - }, []); + getData(dashboardFilter); + }, [dashboardFilter]); const handleBulkDashboardExport = (dashboardsToExport: Dashboard[]) => { const ids = dashboardsToExport.map(({ id }) => id); @@ -128,14 +135,6 @@ function DashboardTable({ } return filters; }; - const subMenus = []; - if (dashboards.length > 0 && dashboardFilter === 'favorite') { - subMenus.push({ - name: 'Favorite', - label: t('Favorite'), - onClick: () => setDashboardFilter('Favorite'), - }); - } const getData = (filter: string) => fetchData({ @@ -160,20 +159,19 @@ function DashboardTable({ name: 'Favorite', label: t('Favorite'), onClick: () => { - getData('Favorite').then(() => { - setDashboardFilter('Favorite'); - setInLocalStorage('dashboard', { tab: 'Favorite' }); - }); + setDashboardFilter(TableTabTypes.FAVORITE); + setInLocalStorage( + HOMEPAGE_DASHBOARD_FILTER, + TableTabTypes.FAVORITE, + ); }, }, { name: 'Mine', label: t('Mine'), onClick: () => { - getData('Mine').then(() => { - setDashboardFilter('Mine'); - setInLocalStorage('dashboard', { tab: 'Mine' }); - }); + setDashboardFilter(TableTabTypes.MINE); + setInLocalStorage(HOMEPAGE_DASHBOARD_FILTER, TableTabTypes.MINE); }, }, ]} diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.test.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.test.tsx index d71864e63..85c953017 100644 --- a/superset-frontend/src/views/CRUD/welcome/Welcome.test.tsx +++ b/superset-frontend/src/views/CRUD/welcome/Welcome.test.tsx @@ -132,10 +132,10 @@ describe('Welcome', () => { const savedQueryCall = fetchMock.calls(/saved_query\/\?q/); const recentCall = fetchMock.calls(/superset\/recent_activity\/*/); const dashboardCall = fetchMock.calls(/dashboard\/\?q/); - expect(chartCall).toHaveLength(1); + expect(chartCall).toHaveLength(2); expect(recentCall).toHaveLength(1); expect(savedQueryCall).toHaveLength(1); - expect(dashboardCall).toHaveLength(1); + expect(dashboardCall).toHaveLength(2); }); }); diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx index 860ac96cc..a0ce320bd 100644 --- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx +++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx @@ -33,6 +33,7 @@ import { mq, getUserOwnedObjects, } from 'src/views/CRUD/utils'; +import { HOMEPAGE_ACTIVITY_FILTER } from 'src/views/CRUD/storageKeys'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { Switch } from 'src/common/components'; @@ -65,7 +66,7 @@ const WelcomeContainer = styled.div` margin: 0px ${({ theme }) => theme.gridUnit * 6}px; position: relative; width: 100%; - ${[mq[1]]} { + ${mq[1]} { margin-top: 5px; margin: 0px 2px; } @@ -104,7 +105,7 @@ const WelcomeNav = styled.div` function Welcome({ user, addDangerToast }: WelcomeProps) { const recent = `/superset/recent_activity/${user.userId}/?limit=6`; - const [activeChild, setActiveChild] = useState('Viewed'); + const [activeChild, setActiveChild] = useState('Loading'); const [checked, setChecked] = useState(true); const [activityData, setActivityData] = useState(null); const [chartData, setChartData] = useState | null>(null); @@ -112,12 +113,14 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { const [dashboardData, setDashboardData] = useState | null>( null, ); + const [loadedCount, setLoadedCount] = useState(0); const userid = user.userId; const id = userid.toString(); useEffect(() => { const userKey = getFromLocalStorage(id, null); + const activeTab = getFromLocalStorage(HOMEPAGE_ACTIVITY_FILTER, null); if (userKey && !userKey.thumbnails) setChecked(false); getRecentAcitivtyObjs(user.userId, recent, addDangerToast) .then(res => { @@ -125,13 +128,14 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { if (res.viewed) { const filtered = reject(res.viewed, ['item_url', null]).map(r => r); data.Viewed = filtered; - const savedActivity = getFromLocalStorage('activity', null); - if (!savedActivity) { + if (!activeTab) { setActiveChild('Viewed'); - } else setActiveChild(savedActivity.activity); + } else setActiveChild(activeTab); } else { data.Examples = res.examples; - setActiveChild('Examples'); + if (activeTab === 'Viewed' || !activeTab) { + setActiveChild('Examples'); + } else setActiveChild(activeTab); } setActivityData(activityData => ({ ...activityData, ...data })); }) @@ -145,12 +149,15 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { ); // Sets other activity data in parallel with recents api call + getUserOwnedObjects(id, 'dashboard') .then(r => { setDashboardData(r); + setLoadedCount(loadedCount => loadedCount + 1); }) .catch((err: unknown) => { setDashboardData([]); + setLoadedCount(loadedCount => loadedCount + 1); addDangerToast( t('There was an issues fetching your dashboards: %s', err), ); @@ -158,17 +165,21 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { getUserOwnedObjects(id, 'chart') .then(r => { setChartData(r); + setLoadedCount(loadedCount => loadedCount + 1); }) .catch((err: unknown) => { setChartData([]); + setLoadedCount(loadedCount => loadedCount + 1); addDangerToast(t('There was an issues fetching your chart: %s', err)); }); getUserOwnedObjects(id, 'saved_query') .then(r => { setQueryData(r); + setLoadedCount(loadedCount => loadedCount + 1); }) .catch((err: unknown) => { setQueryData([]); + setLoadedCount(loadedCount => loadedCount + 1); addDangerToast( t('There was an issues fetching your saved queries: %s', err), ); @@ -204,12 +215,17 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { - {activityData && (activityData.Viewed || activityData.Examples) ? ( + {activityData && + (activityData.Viewed || + activityData.Examples || + activityData.Created) && + activeChild !== 'Loading' ? ( ) : (