diff --git a/superset-frontend/images/empty-charts.png b/superset-frontend/images/empty-charts.png deleted file mode 100644 index b814d35749..0000000000 Binary files a/superset-frontend/images/empty-charts.png and /dev/null differ diff --git a/superset-frontend/images/empty-charts.svg b/superset-frontend/images/empty-charts.svg new file mode 100644 index 0000000000..b4cdd99086 --- /dev/null +++ b/superset-frontend/images/empty-charts.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/superset-frontend/images/empty-dashboard.png b/superset-frontend/images/empty-dashboard.png deleted file mode 100644 index b0d44626d7..0000000000 Binary files a/superset-frontend/images/empty-dashboard.png and /dev/null differ diff --git a/superset-frontend/images/empty-dashboard.svg b/superset-frontend/images/empty-dashboard.svg new file mode 100644 index 0000000000..c76eca0c30 --- /dev/null +++ b/superset-frontend/images/empty-dashboard.svg @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/superset-frontend/images/empty-queries.png b/superset-frontend/images/empty-queries.png deleted file mode 100644 index adc51c0814..0000000000 Binary files a/superset-frontend/images/empty-queries.png and /dev/null differ diff --git a/superset-frontend/images/empty-queries.svg b/superset-frontend/images/empty-queries.svg new file mode 100644 index 0000000000..2239c0ae8e --- /dev/null +++ b/superset-frontend/images/empty-queries.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + diff --git a/superset-frontend/images/star-circle.png b/superset-frontend/images/star-circle.png deleted file mode 100644 index 77fd94d0fe..0000000000 Binary files a/superset-frontend/images/star-circle.png and /dev/null differ diff --git a/superset-frontend/images/star-circle.svg b/superset-frontend/images/star-circle.svg new file mode 100644 index 0000000000..a46a1dd0fb --- /dev/null +++ b/superset-frontend/images/star-circle.svg @@ -0,0 +1,22 @@ + + + + + diff --git a/superset-frontend/images/union.png b/superset-frontend/images/union.png index af94c0793e..48fa801603 100644 Binary files a/superset-frontend/images/union.png and b/superset-frontend/images/union.png differ diff --git a/superset-frontend/images/union.svg b/superset-frontend/images/union.svg new file mode 100644 index 0000000000..6ac0e0f016 --- /dev/null +++ b/superset-frontend/images/union.svg @@ -0,0 +1,22 @@ + + + + + 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 eba5d66862..0193303f1a 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx @@ -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(, { 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(); }); }); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx index f8cd0531eb..a2979296de 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx @@ -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(); }); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx index 1ee8d06127..491c582ffd 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx @@ -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(, { 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(, { - context: { store }, - }); + const wrapper = mount( + , + { + context: { store }, + }, + ); expect(wrapper.find('EmptyState')).toExist(); }); }); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx index 2e290158a7..081cafdaa3 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx @@ -74,11 +74,22 @@ describe('SavedQueries', () => { user: { userId: '1', }, + mine: mockqueries, }; const wrapper = mount(, { 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); }); }); 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 4cd051c947..7bb22a067c 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx @@ -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(, { + const wrapper = mount(, { 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); }); }); diff --git a/superset-frontend/src/components/FacePile/index.tsx b/superset-frontend/src/components/FacePile/index.tsx index 9c2af3d7cc..6b0137daa2 100644 --- a/superset-frontend/src/components/FacePile/index.tsx +++ b/superset-frontend/src/components/FacePile/index.tsx @@ -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()} ); diff --git a/superset-frontend/src/components/ListViewCard/ImageLoader.tsx b/superset-frontend/src/components/ListViewCard/ImageLoader.tsx index 1bf6f57d90..ba4664327c 100644 --- a/superset-frontend/src/components/ListViewCard/ImageLoader.tsx +++ b/superset-frontend/src/components/ListViewCard/ImageLoader.tsx @@ -40,7 +40,7 @@ interface ImageLoaderProps > { fallback: string; src: string; - isLoading: boolean; + isLoading?: boolean; position: BackgroundPosition; } diff --git a/superset-frontend/src/components/ListViewCard/index.tsx b/superset-frontend/src/components/ListViewCard/index.tsx index b63f3afeaa..fee7d0bda8 100644 --- a/superset-frontend/src/components/ListViewCard/index.tsx +++ b/superset-frontend/src/components/ListViewCard/index.tsx @@ -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; diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx index fd307a42bc..34a91c1404 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx @@ -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({ ); return ( - } - coverRight={ - - } - actions={ - - - - - - - } - /> + { + window.location.href = chart.url; + }} + > + } + coverRight={ + + } + actions={ + { + e.stopPropagation(); + e.preventDefault(); + }} + > + + + + + + } + /> + ); } diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx index 2ae3ab7dc3..e45d7fa6be 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx @@ -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({ ); return ( - {dashboard.published ? 'published' : 'draft'}} - 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={} - actions={ - - - - - - - } - /> + { + window.location.href = dashboard.url; + }} + > + {dashboard.published ? 'published' : 'draft'} + } + 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={} + actions={ + { + e.stopPropagation(); + e.preventDefault(); + }} + > + + + + + + } + /> + ); } diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx index 7e2e6ae6cc..59853bc21c 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx @@ -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', diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts index 054b1e5138..b6963d4032 100644 --- a/superset-frontend/src/views/CRUD/hooks.ts +++ b/superset-frontend/src/views/CRUD/hooks.ts @@ -39,10 +39,11 @@ export function useListViewResource( resourceLabel: string, // resourceLabel for translations handleErrorMsg: (errorMsg: string) => void, infoEnable = true, + defaultCollectionValue: D[] = [], ) { const [state, setState] = useState>({ count: 0, - collection: [], + collection: defaultCollectionValue, loading: true, lastFetchDataConfig: null, permissions: [], @@ -164,10 +165,14 @@ export function useListViewResource( hasPerm, fetchData, toggleBulkSelect, - refreshData: () => { + refreshData: (provideConfig?: FetchDataConfig) => { if (state.lastFetchDataConfig) { - fetchData(state.lastFetchDataConfig); + return fetchData(state.lastFetchDataConfig); } + if (provideConfig) { + return fetchData(provideConfig); + } + return null; }, }; } diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts index 39d0c81367..4ce59123ff 100644 --- a/superset-frontend/src/views/CRUD/types.ts +++ b/superset-frontend/src/views/CRUD/types.ts @@ -28,6 +28,7 @@ export interface DashboardTableProps { addSuccessToast: (message: string) => void; search: string; user?: User; + mine: Array; } export interface Dashboard { diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 27e32df3f5..675e2b3fea 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -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` diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx index 6a3387c460..28d3cb9c53 100644 --- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx @@ -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; - Edited?: Array; - Viewed?: Array; - Examples?: Array; + 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({}); - 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) => ( - } - 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 ( + { + window.location.href = getRecentRef(e); + }} + key={e.id} + > + } + 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} + /> + + ); + }); }; - if (loading) return <>loading ...; + if (loading) return ; return ( <> ; } function ChartTable({ user, addDangerToast, addSuccessToast, + mine, }: ChartTableProps) { const { - state: { loading, resourceCollection: charts, bulkSelectEnabled }, + state: { resourceCollection: charts, bulkSelectEnabled }, setResourceCollection: setCharts, hasPerm, refreshData, fetchData, - } = useListViewResource('chart', t('chart'), addDangerToast); + } = useListViewResource( + '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({ 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 && ( {dashboards.map(e => ( setEditModal(dashboard)} + openDashboardEditModal={(dashboard: Dashboard) => + setEditModal(dashboard) + } saveFavoriteStatus={saveFavoriteStatus} favoriteStatus={favoriteStatus[e.id]} /> ))} - ) : ( + )} + {dashboards.length === 0 && ( )} diff --git a/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx index f145dfe79c..045ce7152b 100644 --- a/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx +++ b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx @@ -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 = (
{`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 ( - - {tableName !== 'RECENTS' && ( - - - - )} - + + + + )} + + ); } // Favorite tab empty state return ( - - {t("You don't have any favorites yet!")} -
- } - > - - + + + ); } diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx index f7b65094a5..6de2f3c8e4 100644 --- a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx +++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx @@ -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; } 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('saved_query', t('query'), addDangerToast); + } = useListViewResource( + 'saved_query', + t('query'), + addDangerToast, + true, + mine, + ); const [queryFilter, setQueryFilter] = useState('Mine'); const [queryDeleteModal, setQueryDeleteModal] = useState(false); const [currentlyEdited, setCurrentlyEdited] = useState({}); - + 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) => ( @@ -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 ? ( {queries.map(q => ( - -
-
{t('Tables')}
-
{q?.sql_tables?.length}
-
-
-
{t('Datasource Name')}
-
{q?.sql_tables && q.sql_tables[0]?.table}
-
- - } - actions={ - - - - - - } - /> + { + window.location.href = `/superset/sqllab?savedQueryId=${q.id}`; + }} + key={q.id} + > + +
+
{t('Tables')}
+
{q?.sql_tables?.length}
+
+
+
{t('Datasource Name')}
+
{q?.sql_tables && q.sql_tables[0]?.table}
+
+ + } + actions={ + { + e.stopPropagation(); + e.preventDefault(); + }} + > + + + + + } + /> +
))}
) : ( diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx index f35effcbf9..237a2c1dfe 100644 --- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx +++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx @@ -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; + Edited?: Array; + Viewed?: Array; + Examples?: Array; + myChart?: Array; + myDash?: Array; + myQuery?: Array; } 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({}); + 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 ( - + - + {loading ? ( + + ) : ( + + )} - + {loading ? ( + + ) : ( + + )} - + {loading ? ( + + ) : ( + + )} ); } + +export default withToasts(Welcome);