diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx
index f15ddd337ae61..b011eac45a3ee 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx
@@ -18,6 +18,10 @@
*/
import React from 'react';
import { styledMount as mount } from 'spec/helpers/theming';
+import { act } from 'react-dom/test-utils';
+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';
@@ -26,6 +30,9 @@ import ActivityTable from 'src/views/CRUD/welcome/ActivityTable';
const mockStore = configureStore([thunk]);
const store = mockStore({});
+const chartsEndpoint = 'glob:*/api/v1/chart/?*';
+const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
+
const mockData = {
Viewed: [
{
@@ -36,7 +43,7 @@ const mockData = {
table: {},
},
],
- Edited: [
+ Created: [
{
dashboard_title: 'Dashboard_Test',
changed_on_utc: '24 Feb 2014 10:13:14',
@@ -44,7 +51,22 @@ const mockData = {
id: '3',
},
],
- Created: [
+};
+
+fetchMock.get(chartsEndpoint, {
+ result: [
+ {
+ slice_name: 'ChartyChart',
+ changed_on_utc: '24 Feb 2014 10:13:14',
+ url: '/fakeUrl/explore',
+ id: '4',
+ table: {},
+ },
+ ],
+});
+
+fetchMock.get(dashboardsEndpoint, {
+ result: [
{
dashboard_title: 'Dashboard_Test',
changed_on_utc: '24 Feb 2014 10:13:14',
@@ -52,22 +74,27 @@ const mockData = {
id: '3',
},
],
-};
+});
describe('ActivityTable', () => {
const activityProps = {
- activeChild: 'Edited',
+ activeChild: 'Created',
activityData: mockData,
setActiveChild: jest.fn(),
user: { userId: '1' },
loading: false,
};
- const wrapper = mount(, {
- context: { store },
- });
+
+ let wrapper: ReactWrapper;
beforeAll(async () => {
- await waitForComponentToPaint(wrapper);
+ await act(async () => {
+ wrapper = mount(
+
+
+ ,
+ );
+ });
});
it('the component renders', () => {
@@ -79,4 +106,32 @@ describe('ActivityTable', () => {
it('renders ActivityCards', async () => {
expect(wrapper.find('ListViewCard')).toExist();
});
+ it('calls the getEdited batch call when edited tab is clicked', async () => {
+ act(() => {
+ const handler = wrapper.find('li.no-router a').at(1).prop('onClick');
+ if (handler) {
+ 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);
+ });
+ it('show empty state if there is data', () => {
+ const activityProps = {
+ activeChild: 'Created',
+ activityData: {},
+ setActiveChild: jest.fn(),
+ user: { userId: '1' },
+ loading: false,
+ };
+ const wrapper = mount(
+
+
+ ,
+ );
+ expect(wrapper.find('EmptyState')).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 53ba3575ad858..a58df6bcf51b5 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx
@@ -67,7 +67,10 @@ fetchMock.get(savedQueryEndpoint, {
result: [],
});
-fetchMock.get(recentActivityEndpoint, {});
+fetchMock.get(recentActivityEndpoint, {
+ Created: [],
+ Viewed: [],
+});
fetchMock.get(chartInfoEndpoint, {
permissions: [],
@@ -122,10 +125,14 @@ describe('Welcome', () => {
expect(wrapper.find('CollapsePanel')).toHaveLength(8);
});
- it('calls batch method on page load', () => {
+ it('calls api methods in parallel on page load', () => {
const chartCall = fetchMock.calls(/chart\/\?q/);
+ const savedQueryCall = fetchMock.calls(/saved_query\/\?q/);
+ const recentCall = fetchMock.calls(/superset\/recent_activity\/*/);
const dashboardCall = fetchMock.calls(/dashboard\/\?q/);
- expect(chartCall).toHaveLength(2);
- expect(dashboardCall).toHaveLength(2);
+ expect(chartCall).toHaveLength(1);
+ expect(recentCall).toHaveLength(1);
+ expect(savedQueryCall).toHaveLength(1);
+ expect(dashboardCall).toHaveLength(1);
});
});
diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts
index 1545c82406f94..fb16713c6d1c7 100644
--- a/superset-frontend/src/views/CRUD/hooks.ts
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -56,11 +56,12 @@ export function useListViewResource(
infoEnable = true,
defaultCollectionValue: D[] = [],
baseFilters?: FilterValue[], // must be memoized
+ initialLoadingState = true,
) {
const [state, setState] = useState>({
count: 0,
collection: defaultCollectionValue,
- loading: true,
+ loading: initialLoadingState,
lastFetchDataConfig: null,
permissions: [],
bulkSelectEnabled: false,
diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts
index b9908c756b480..4de3055c1dc65 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -23,6 +23,12 @@ export type FavoriteStatus = {
[id: number]: boolean;
};
+export type Filters = {
+ col: string;
+ opr: string;
+ value: string;
+};
+
export interface DashboardTableProps {
addDangerToast: (message: string) => void;
addSuccessToast: (message: string) => void;
diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx
index c60c1413f1709..e70416149f70c 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -27,7 +27,7 @@ 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';
+import { Dashboard, Filters } from './types';
const createFetchResourceMethod = (method: string) => (
resource: string,
@@ -61,25 +61,20 @@ const createFetchResourceMethod = (method: string) => (
return [];
};
-export const getRecentAcitivtyObjs = (
- userId: string | number,
- recent: string,
- addDangerToast: (arg1: string, arg2: any) => any,
-) => {
- const getParams = (filters?: Array) => {
- const params = {
- order_column: 'changed_on_delta_humanized',
- order_direction: 'desc',
- page: 0,
- page_size: 3,
- filters,
- };
- if (!filters) delete params.filters;
- return rison.encode(params);
+const getParams = (filters?: Array) => {
+ const params = {
+ order_column: 'changed_on_delta_humanized',
+ order_direction: 'desc',
+ page: 0,
+ page_size: 3,
+ filters,
};
+ if (!filters) delete params.filters;
+ return rison.encode(params);
+};
+
+export const getEditedObjects = (userId: string | number) => {
const filters = {
- // chart and dashbaord uses same filters
- // for edited and created
edited: [
{
col: 'changed_by',
@@ -87,76 +82,74 @@ export const getRecentAcitivtyObjs = (
value: `${userId}`,
},
],
- created: [
- {
- col: 'created_by',
- opr: 'rel_o_m',
- value: `${userId}`,
- },
- ],
};
- const baseBatch = [
- SupersetClient.get({ endpoint: recent }),
+ const batch = [
SupersetClient.get({
endpoint: `/api/v1/dashboard/?q=${getParams(filters.edited)}`,
}),
SupersetClient.get({
endpoint: `/api/v1/chart/?q=${getParams(filters.edited)}`,
}),
- SupersetClient.get({
- endpoint: `/api/v1/dashboard/?q=${getParams(filters.created)}`,
- }),
- SupersetClient.get({
- endpoint: `/api/v1/chart/?q=${getParams(filters.created)}`,
- }),
- SupersetClient.get({
- endpoint: `/api/v1/saved_query/?q=${getParams(filters.created)}`,
- }),
];
- return Promise.all(baseBatch).then(
- ([
- recentsRes,
- editedDash,
- editedChart,
- createdByDash,
- createdByChart,
- createdByQuery,
- ]) => {
- const res: any = {
- editedDash: editedDash.json?.result.slice(0, 3),
- editedChart: editedChart.json?.result.slice(0, 3),
- createdByDash: createdByDash.json?.result.slice(0, 3),
- createdByChart: createdByChart.json?.result.slice(0, 3),
- createdByQuery: createdByQuery.json?.result.slice(0, 3),
+ return Promise.all(batch)
+ .then(([editedCharts, editedDashboards]) => {
+ const res = {
+ editedDash: editedDashboards.json?.result.slice(0, 3),
+ editedChart: editedCharts.json?.result.slice(0, 3),
};
- if (recentsRes.json.length === 0) {
- const newBatch = [
- SupersetClient.get({ endpoint: `/api/v1/chart/?q=${getParams()}` }),
- SupersetClient.get({
- endpoint: `/api/v1/dashboard/?q=${getParams()}`,
- }),
- ];
- return Promise.all(newBatch)
- .then(([chartRes, dashboardRes]) => {
- res.examples = [
- ...chartRes.json.result,
- ...dashboardRes.json.result,
- ];
- return res;
- })
- .catch(errMsg =>
- addDangerToast(
- t('There was an error fetching your recent activity:'),
- errMsg,
- ),
- );
- }
- res.viewed = recentsRes.json;
return res;
- },
- );
+ })
+ .catch(err => err);
+};
+
+export const getUserOwnedObjects = (
+ userId: string | number,
+ resource: string,
+) => {
+ const filters = {
+ created: [
+ {
+ col: 'created_by',
+ opr: 'rel_o_m',
+ value: `${userId}`,
+ },
+ ],
+ };
+ return SupersetClient.get({
+ endpoint: `/api/v1/${resource}/?q=${getParams(filters.created)}`,
+ }).then(res => res.json?.result);
};
+export const getRecentAcitivtyObjs = (
+ userId: string | number,
+ recent: string,
+ addDangerToast: (arg1: string, arg2: any) => any,
+) =>
+ SupersetClient.get({ endpoint: recent }).then(recentsRes => {
+ const res: any = {};
+ if (recentsRes.json.length === 0) {
+ const newBatch = [
+ SupersetClient.get({ endpoint: `/api/v1/chart/?q=${getParams()}` }),
+ SupersetClient.get({
+ endpoint: `/api/v1/dashboard/?q=${getParams()}`,
+ }),
+ ];
+ return Promise.all(newBatch)
+ .then(([chartRes, dashboardRes]) => {
+ res.examples = [...chartRes.json.result, ...dashboardRes.json.result];
+ return res;
+ })
+ .catch(errMsg =>
+ addDangerToast(
+ t('There was an error fetching your recent activity:'),
+ errMsg,
+ ),
+ );
+ }
+ res.viewed = recentsRes.json;
+ return res;
+ });
+
export const createFetchRelated = createFetchResourceMethod('related');
export const createFetchDistinct = createFetchResourceMethod('distinct');
diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
index b52dfe2cd8561..7cea906aa7d9c 100644
--- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React from 'react';
+import React, { useState } from 'react';
import moment from 'moment';
import { styled, t } from '@superset-ui/core';
@@ -25,7 +25,7 @@ import ListViewCard from 'src/components/ListViewCard';
import SubMenu from 'src/components/Menu/SubMenu';
import { Chart } from 'src/types/Chart';
import { Dashboard, SavedQueryObject } from 'src/views/CRUD/types';
-import { mq, CardStyles } from 'src/views/CRUD/utils';
+import { mq, CardStyles, getEditedObjects } from 'src/views/CRUD/utils';
import { ActivityData } from './Welcome';
import EmptyState from './EmptyState';
@@ -66,7 +66,6 @@ interface ActivityProps {
};
activeChild: string;
setActiveChild: (arg0: string) => void;
- loading: boolean;
activityData: ActivityData;
}
@@ -156,17 +155,27 @@ const getEntityLastActionOn = (entity: ActivityObject) => {
};
export default function ActivityTable({
- loading,
activeChild,
setActiveChild,
activityData,
+ user,
}: ActivityProps) {
+ const [editedObjs, setEditedObjs] = useState>();
+ const [loadingState, setLoadingState] = useState(false);
+ const getEditedCards = () => {
+ setLoadingState(true);
+ getEditedObjects(user.userId).then(r => {
+ setEditedObjs([...r.editedChart, ...r.editedDash]);
+ setLoadingState(false);
+ });
+ };
const tabs = [
{
name: 'Edited',
label: t('Edited'),
onClick: () => {
setActiveChild('Edited');
+ getEditedCards();
},
},
{
@@ -197,30 +206,32 @@ export default function ActivityTable({
}
const renderActivity = () =>
- activityData[activeChild].map((entity: ActivityObject) => {
- const url = getEntityUrl(entity);
- const lastActionOn = getEntityLastActionOn(entity);
- return (
- {
- window.location.href = url;
- }}
- key={url}
- >
- >}
- url={url}
- title={getEntityTitle(entity)}
- description={lastActionOn}
- avatar={getEntityIconName(entity)}
- actions={null}
- />
-
- );
- });
-
- if (loading) return ;
+ (activeChild !== 'Edited' ? activityData[activeChild] : editedObjs).map(
+ (entity: ActivityObject) => {
+ const url = getEntityUrl(entity);
+ const lastActionOn = getEntityLastActionOn(entity);
+ return (
+ {
+ window.location.href = url;
+ }}
+ key={url}
+ >
+ >}
+ url={url}
+ title={getEntityTitle(entity)}
+ description={lastActionOn}
+ avatar={getEntityIconName(entity)}
+ actions={null}
+ />
+
+ );
+ },
+ );
+ if (loadingState && !editedObjs) {
+ return ;
+ }
return (
<>
<>
- {activityData[activeChild]?.length > 0 ? (
+ {activityData[activeChild]?.length > 0 ||
+ (activeChild === 'Edited' && editedObjs && editedObjs.length > 0) ? (
{renderActivity()}
) : (
diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
index 8eebf1f631e6c..5eb0b294d3803 100644
--- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
@@ -29,6 +29,7 @@ import PropertiesModal from 'src/explore/components/PropertiesModal';
import { User } from 'src/types/bootstrapTypes';
import ChartCard from 'src/views/CRUD/chart/ChartCard';
import Chart from 'src/types/Chart';
+import Loading from 'src/components/Loading';
import ErrorBoundary from 'src/components/ErrorBoundary';
import SubMenu from 'src/components/Menu/SubMenu';
import EmptyState from './EmptyState';
@@ -53,7 +54,7 @@ function ChartTable({
}: ChartTableProps) {
const history = useHistory();
const {
- state: { resourceCollection: charts, bulkSelectEnabled },
+ state: { loading, resourceCollection: charts, bulkSelectEnabled },
setResourceCollection: setCharts,
hasPerm,
refreshData,
@@ -64,7 +65,10 @@ function ChartTable({
addDangerToast,
true,
mine,
+ [],
+ false,
);
+
const chartIds = useMemo(() => charts.map(c => c.id), [charts]);
const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
'chart',
@@ -112,6 +116,7 @@ function ChartTable({
filters: getFilters(filter),
});
+ if (loading) return ;
return (
{sliceCurrentlyEditing && (
diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
index 5b8f933ba9e74..27a41d4dc766a 100644
--- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
@@ -22,6 +22,7 @@ import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import { Dashboard, DashboardTableProps } from 'src/views/CRUD/types';
import { useHistory } from 'react-router-dom';
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';
@@ -55,6 +56,8 @@ function DashboardTable({
addDangerToast,
true,
mine,
+ [],
+ false,
);
const dashboardIds = useMemo(() => dashboards.map(c => c.id), [dashboards]);
const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
@@ -125,6 +128,7 @@ function DashboardTable({
filters: getFilters(filter),
});
+ if (loading) return ;
return (
<>
{
const {
- state: { resourceCollection: queries },
+ state: { loading, resourceCollection: queries },
hasPerm,
fetchData,
refreshData,
@@ -121,6 +122,8 @@ const SavedQueries = ({
addDangerToast,
true,
mine,
+ [],
+ false,
);
const [queryFilter, setQueryFilter] = useState('Mine');
const [queryDeleteModal, setQueryDeleteModal] = useState(false);
@@ -227,6 +230,8 @@ const SavedQueries = ({
)}
);
+
+ if (loading) return ;
return (
<>
{queryDeleteModal && (
diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
index 50fbe6fad1c35..0d1f3ba663f51 100644
--- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
@@ -27,6 +27,7 @@ import {
createErrorHandler,
getRecentAcitivtyObjs,
mq,
+ getUserOwnedObjects,
} from 'src/views/CRUD/utils';
import ActivityTable from './ActivityTable';
@@ -44,9 +45,6 @@ export interface ActivityData {
Edited?: Array