chore: Homepage cleanup (#14823)

* initial commit

* update welcome

* fix lint

* add enum

* more choring

* fix lint

* redo logic for api stick api calls

* fix test

* fix chart test

* lint fix and remove unused code

* fix flicker and add suggestions

* lint

* fix test

* add suggestions

* add suggestions and fix test

* revert packagelock

* fix space
This commit is contained in:
Phillip Kelley-Dotson 2021-06-11 18:47:42 -07:00 committed by GitHub
parent 53df152362
commit 8e6a5a6f52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 139 additions and 82 deletions

View File

@ -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';

View File

@ -23,6 +23,11 @@ export type FavoriteStatus = {
[id: number]: boolean;
};
export enum TableTabTypes {
FAVORITE = 'Favorite',
MINE = 'Mine',
}
export type Filters = {
col: string;
opr: string;

View File

@ -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(
<Provider store={store}>

View File

@ -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<Array<ActivityData>>();
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 <Loading position="inline" />;
}
return (
<>
<SubMenu
activeChild={activeChild}
// eslint-disable-next-line react/no-children-prop
tabs={tabs}
/>
<SubMenu activeChild={activeChild} tabs={tabs} />
{activityData[activeChild]?.length > 0 ||
(activeChild === 'Edited' && editedObjs && editedObjs.length > 0) ? (
<ActivityContainer>{renderActivity()}</ActivityContainer>

View File

@ -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(
<ChartTable
chartFilter="Mine"
user={{ userId: '2' }}
mine={[]}
store={store}
/>,
);
});
expect(wrapper.find('EmptyState')).toExist();
});
});

View File

@ -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<boolean>(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={[

View File

@ -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<Dashboard>();
const [dashboardFilter, setDashboardFilter] = useState('Mine');
const [dashboardFilter, setDashboardFilter] = useState(defaultFilter);
const [preparingExport, setPreparingExport] = useState<boolean>(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);
},
},
]}

View File

@ -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);
});
});

View File

@ -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<ActivityData | null>(null);
const [chartData, setChartData] = useState<Array<object> | null>(null);
@ -112,12 +113,14 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
const [dashboardData, setDashboardData] = useState<Array<object> | 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) {
</WelcomeNav>
<Collapse defaultActiveKey={['1', '2', '3', '4']} ghost bigger>
<Collapse.Panel header={t('Recents')} key="1">
{activityData && (activityData.Viewed || activityData.Examples) ? (
{activityData &&
(activityData.Viewed ||
activityData.Examples ||
activityData.Created) &&
activeChild !== 'Loading' ? (
<ActivityTable
user={user}
activeChild={activeChild}
setActiveChild={setActiveChild}
activityData={activityData}
loadedCount={loadedCount}
/>
) : (
<Loading position="inline" />