This is an automated email from the ASF dual-hosted git repository. yjc pushed a commit to branch home-screen-mvp in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit 1b46423c112742728919bb91e1430147e7384ee8 Author: Phillip Kelley-Dotson <[email protected]> AuthorDate: Mon Oct 19 07:28:25 2020 -0700 update branch --- superset-frontend/images/star-circle.png | Bin 0 -> 2705 bytes superset-frontend/images/union.png | Bin 0 -> 1131 bytes .../views/CRUD/chart/ChartList_spec.jsx | 1 + .../views/CRUD/welcome/ActivityTable_spec.tsx | 93 +++++++++ .../views/CRUD/welcome/ChartTable_spec.tsx | 1 - .../views/CRUD/welcome/DashboardTable_spec.tsx | 78 +++++--- .../views/CRUD/welcome/SavedQueries_spec.tsx | 30 ++- .../views/CRUD/welcome/Welcome_spec.tsx | 14 +- .../src/components/ListViewCard/index.tsx | 31 ++- .../src/views/CRUD/chart/ChartCard.tsx | 13 +- .../src/views/CRUD/dashboard/DashboardCard.tsx | 13 +- .../views/CRUD/data/savedquery/SavedQueryList.tsx | 15 +- superset-frontend/src/views/CRUD/hooks.ts | 49 ++++- superset-frontend/src/views/CRUD/types.ts | 15 +- superset-frontend/src/views/CRUD/utils.tsx | 130 +++++++++++-- .../src/views/CRUD/welcome/ActivityTable.tsx | 181 +++++++++++------ .../src/views/CRUD/welcome/ChartTable.tsx | 81 ++++++-- .../src/views/CRUD/welcome/DashboardTable.tsx | 148 ++++++++++---- .../src/views/CRUD/welcome/EmptyState.tsx | 112 +++++++++++ .../src/views/CRUD/welcome/SavedQueries.tsx | 177 ++++++++++++++--- .../src/views/CRUD/welcome/Welcome.tsx | 215 +++------------------ 21 files changed, 974 insertions(+), 423 deletions(-) diff --git a/superset-frontend/images/star-circle.png b/superset-frontend/images/star-circle.png new file mode 100644 index 0000000..77fd94d Binary files /dev/null and b/superset-frontend/images/star-circle.png differ diff --git a/superset-frontend/images/union.png b/superset-frontend/images/union.png new file mode 100644 index 0000000..ca1dbe5 Binary files /dev/null and b/superset-frontend/images/union.png differ diff --git a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx index 3872358..3abedc3 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx @@ -54,6 +54,7 @@ const mockCharts = [...new Array(3)].map((_, i) => ({ fetchMock.get(chartsInfoEndpoint, { permissions: ['can_list', 'can_edit', 'can_delete'], }); + fetchMock.get(chartssOwnersEndpoint, { result: [], }); diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx new file mode 100644 index 0000000..2f54d58 --- /dev/null +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx @@ -0,0 +1,93 @@ +/** + * 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. + */ +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'; +import ListViewCard from 'src/components/ListViewCard'; + +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('ActivityTable', () => { + const activityProps = { + user: { + userId: '1', + }, + activityFilter: 'Edited', + }; + const wrapper = mount(<ActivityTable {...activityProps} />, { + context: { store }, + }); + + beforeAll(async () => { + await waitForComponentToPaint(wrapper); + }); + + it('renders', () => { + expect(wrapper.find(ActivityTable)).toExist(); + }); + + it('renders a EmptyState', () => { + console.log('wrapper', wrapper.debug()) + }); + + it('calls batch method and renders ListViewCArd', () => { + const chartCall = fetchMock.calls(/chart\/\?q/); + const dashboardCall = fetchMock.calls(/dashboard\/\?q/); + expect(chartCall).toHaveLength(2); + expect(dashboardCall).toHaveLength(2); + expect(wrapper.find(ListViewCard)).toHaveLength(2); + }); +}); 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 ed0666e..a631a78 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx @@ -21,7 +21,6 @@ import { mount } from 'enzyme'; import thunk from 'redux-thunk'; import fetchMock from 'fetch-mock'; -import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; import configureStore from 'redux-mock-store'; import ChartTable from 'src/views/CRUD/welcome/ChartTable'; import ChartCard from 'src/views/CRUD/chart/ChartCard'; 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 4d2eb9e..dd1e92a 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx @@ -17,48 +17,78 @@ * under the License. */ import React from 'react'; -import { mount } from 'enzyme'; +import { styledMount as mount } from 'spec/helpers/theming'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; -import { supersetTheme, ThemeProvider } from '@superset-ui/core'; +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/welcome/DashboardCard'; +import DashboardCard from 'src/views/CRUD/dashboard/DashboardCard'; // store needed for withToasts(DashboardTable) const mockStore = configureStore([thunk]); const store = mockStore({}); -const dashboardsEndpoint = 'glob:*/api/v1/dashboard/*'; -const mockDashboards = [{ id: 1, url: 'url', dashboard_title: 'title' }]; +const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*'; +const chartsInfoEndpoint = 'glob:*/api/v1/chart/_info*'; +const mockDashboards = [ + { + id: 1, + url: 'url', + dashboard_title: 'title', + changed_on_utc: '24 Feb 2014 10:13:14', + }, +]; fetchMock.get(dashboardsEndpoint, { result: mockDashboards }); +//fetchMock.get(dashboardsEndpointNoData, { result: [] }); +fetchMock.get(chartsInfoEndpoint, { + permissions: ['can_list', 'can_edit', 'can_delete'], +}); -function setup() { - // use mount because data fetching is triggered on mount - return mount(<DashboardTable />, { +describe('DashboardTable', () => { + const dashboardProps = { + dashboardFilter: 'Favorite', + user: { + userId: '2', + }, + }; + const wrapper = mount(<DashboardTable {...dashboardProps} />, { context: { store }, - wrappingComponent: ThemeProvider, - wrappingComponentProps: { theme: supersetTheme }, }); -} + // console.log('wrapper', wrapper.debug()) + // beforeEach(fetchMock.resetHistory); -describe('DashboardTable', () => { - beforeEach(fetchMock.resetHistory); + beforeAll(async () => { + await waitForComponentToPaint(wrapper); + }); - it('fetches dashboards and renders a ', () => { - return new Promise(done => { - const wrapper = setup(); + it('renders', () => { + expect(wrapper.find(DashboardTable)).toExist(); + console.log('wrapper', wrapper.debug()) + }); - setTimeout(() => { - expect(fetchMock.calls(dashboardsEndpoint)).toHaveLength(1); - // there's a delay between response and updating state, so manually set it - // rather than adding a timeout which could introduce flakiness - wrapper.setState({ dashboards: mockDashboards }); - expect(wrapper.find(DashboardCard)).toExist(); - done(); - }); + it('render a submenu with clickable tabs and buttons', async () => { + expect(wrapper.find(SubMenu)).toExist(); + expect(wrapper.find('MenuItem')).toHaveLength(2); + expect(wrapper.find('Button')).toHaveLength(2); + act(() => { + wrapper.find('MenuItem').at(1).simulate('click'); }); + 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 }); + expect(wrapper.find(DashboardCard)).toExist(); }); + /*it('display EmptyState if there is no data', ()=>{ + (const wrapper = mount(<DashboardTable {...dashboardProps} />) + console.log('wrapper', wrapper); + });*/ }); 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 d5c61b5..fd367a1 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx @@ -17,16 +17,30 @@ * under the License. */ import React from 'react'; -import { SavedQueries } from 'src/views/CRUD/welcome/SavedQueries' -import { shallow } from 'enzyme'; +import SavedQueries from 'src/views/CRUD/welcome/SavedQueries'; +import { mount } from 'enzyme'; +import thunk from 'redux-thunk'; + +import configureStore from 'redux-mock-store'; +// store needed for withToasts(DashboardTable) +const mockStore = configureStore([thunk]); +const store = mockStore({}); describe('SavedQueries', () => { - const + const savedQueryProps = { + user: { + userId: '1', + }, + activityFilter: 'Edit', + }; + + const wrapper = mount(<SavedQueries {...savedQueryProps} />); + it('is valid', () => { expect(React.isValidElement(<SavedQueries />)).toBe(true); }); - it('SaveQueries renders up to three saved queries', ()=>{ - //expect() - }) - -}); \ No newline at end of file + it('takes in props', () => { + + // expect() + }); +}); 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 13c27b7..38891c0 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { shallow } from 'enzyme'; +import { styledMount as mount } from 'spec/helpers/theming'; import Welcome from 'src/views/CRUD/welcome/Welcome'; @@ -33,12 +33,12 @@ describe('Welcome', () => { isActive: true, }, }; - it('is valid', () => { - expect(React.isValidElement(<Welcome {...mockedProps} />)).toBe(true); + const wrapper = mount(<Welcome {...mockedProps} />); + it('is renders', () => { + expect(wrapper.find(Welcome)).toExist(); }); - it('renders 3 submenu components', () => { - const wrapper = shallow(<Welcome {...mockedProps} />); - expect(wrapper.find('SubMenu')).toHaveLength(4); + it('renders first submenu on page load', () => { + expect(wrapper.find('SubMenu')).toHaveLength(1); + expect(wrapper.find('PanelContent')).toHaveLength(4); }); - console.log('wrapper', ) }); diff --git a/superset-frontend/src/components/ListViewCard/index.tsx b/superset-frontend/src/components/ListViewCard/index.tsx index 427beba..674985b 100644 --- a/superset-frontend/src/components/ListViewCard/index.tsx +++ b/superset-frontend/src/components/ListViewCard/index.tsx @@ -139,11 +139,26 @@ const SkeletonActions = styled(Skeleton.Button)` width: ${({ theme }) => theme.gridUnit * 10}px; `; +const QueryData = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + border-bottom: 1px solid #e0e0e0; + .title { + font-weight: 500; + color: #b2b2b2; + } + .holder { + margin: 10px 10px 10px 10px; + } +`; + const paragraphConfig = { rows: 1, width: 150 }; interface CardProps { title: React.ReactNode; url?: string; imgURL: string; + tables?: string | number; imgFallbackURL: string; imgPosition?: BackgroundPosition; description: string; @@ -167,6 +182,7 @@ function ListViewCard({ imgFallbackURL, description, coverLeft, + tables, coverRight, actions, avatar, @@ -174,7 +190,6 @@ function ListViewCard({ loading, imgPosition = 'top', showImg = true, - rows, isRecent, }: CardProps) { return ( @@ -205,16 +220,12 @@ function ListViewCard({ </Cover> ) : ( <QueryData> - <div> - <div>Tables</div> - <div>{}</div> - </div> - <div> - <div>Rows</div> - <div>{rows}</div> + <div className="holder"> + <div className="title">Tables</div> + <div>{tables}</div> </div> - <div> - <div>Datasource Name</div> + <div className="holder"> + <div className="title">Datasource Name</div> <div>{tableName}</div> </div> </QueryData> diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx index e1ab61f..d5cd625 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx @@ -54,11 +54,12 @@ export default function ChartCard({ }: ChartCardProps) { const canEdit = hasPerm('can_edit'); const canDelete = hasPerm('can_delete'); - const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus( - {}, - FAVESTAR_BASE_URL, - addDangerToast, - ); + const [ + favoriteStatusRef, + fetchFaveStar, + saveFaveStar, + favoriteStatus, + ] = useFavoriteStatus({}, FAVESTAR_BASE_URL, addDangerToast); function handleChartDelete({ id, slice_name: sliceName }: Chart) { SupersetClient.delete({ @@ -139,7 +140,7 @@ export default function ChartCard({ itemId={chart.id} fetchFaveStar={fetchFaveStar} saveFaveStar={saveFaveStar} - isStarred={!!favoriteStatusRef.current[chart.id]} + isStarred={!!favoriteStatus[chart.id]} height={20} width={20} /> diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx index e2e3b1f..2a09f70 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx @@ -27,11 +27,12 @@ function DashboardCard({ const canEdit = hasPerm('can_edit'); const canDelete = hasPerm('can_delete'); const canExport = hasPerm('can_mulexport'); - const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus( - {}, - FAVESTAR_BASE_URL, - addDangerToast, - ); + const [ + favoriteStatusRef, + fetchFaveStar, + saveFaveStar, + favoriteStatus, + ] = useFavoriteStatus({}, FAVESTAR_BASE_URL, addDangerToast); function handleDashboardDelete({ id, @@ -126,7 +127,7 @@ function DashboardCard({ itemId={dashboard.id} fetchFaveStar={fetchFaveStar} saveFaveStar={saveFaveStar} - isStarred={!!favoriteStatusRef.current[dashboard.id]} + isStarred={!!favoriteStatus[dashboard.id]} /> <Dropdown overlay={menu}> <Icon name="more-horiz" /> diff --git a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx index f887af5..bfb4719 100644 --- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx +++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx @@ -39,6 +39,7 @@ import DeleteModal from 'src/components/DeleteModal'; import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; import { IconName } from 'src/components/Icon'; import { commonMenuData } from 'src/views/CRUD/data/common'; +import { SavedQueryObject } from 'src/views/CRUD/types'; import SavedQueryPreviewModal from './SavedQueryPreviewModal'; const PAGE_SIZE = 25; @@ -48,20 +49,6 @@ interface SavedQueryListProps { addSuccessToast: (msg: string) => void; } -type SavedQueryObject = { - database: { - database_name: string; - id: number; - }; - db_id: number; - description?: string; - id: number; - label: string; - schema: string; - sql: string; - sql_tables: Array<{ catalog?: string; schema: string; table: string }>; -}; - const StyledTableLabel = styled.div` .count { margin-left: 5px; diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts index 80de0a7..9dbdbc2 100644 --- a/superset-frontend/src/views/CRUD/hooks.ts +++ b/superset-frontend/src/views/CRUD/hooks.ts @@ -330,6 +330,7 @@ export function useFavoriteStatus( endpoint: `${baseURL}/${id}/count/`, }).then( ({ json }) => { + console.log('json', json); updateFavoriteStatus({ [id]: json.count > 0 }); }, createErrorHandler(errMsg => @@ -357,7 +358,12 @@ export function useFavoriteStatus( ); }; - return [favoriteStatusRef, fetchFaveStar, saveFaveStar] as const; + return [ + favoriteStatusRef, + fetchFaveStar, + saveFaveStar, + favoriteStatus, + ] as const; } export const useChartEditModal = ( @@ -397,3 +403,44 @@ export const useChartEditModal = ( closeChartEditModal, }; }; + +export const copyQueryLink = ( + id: number, + addDangerToast: (arg0: string) => void, + addSuccessToast: (arg0: string) => void, +) => { + const selection: Selection | null = document.getSelection(); + + if (selection) { + selection.removeAllRanges(); + const range = document.createRange(); + const span = document.createElement('span'); + span.textContent = `${window.location.origin}/superset/sqllab?savedQueryId=${id}`; + span.style.position = 'fixed'; + span.style.top = '0'; + span.style.clip = 'rect(0, 0, 0, 0)'; + span.style.whiteSpace = 'pre'; + + document.body.appendChild(span); + range.selectNode(span); + selection.addRange(range); + + try { + if (!document.execCommand('copy')) { + throw new Error(t('Not successful')); + } + } catch (err) { + addDangerToast(t('Sorry, your browser does not support copying.')); + } + + document.body.removeChild(span); + if (selection.removeRange) { + selection.removeRange(range); + } else { + selection.removeAllRanges(); + } + console.log('-----Success Toast--------') + addSuccessToast(t('Link Copied!')); + console.log('-----Success Toast--------') + } +}; diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts index afd1956..1070f57 100644 --- a/superset-frontend/src/views/CRUD/types.ts +++ b/superset-frontend/src/views/CRUD/types.ts @@ -27,7 +27,6 @@ export interface DashboardTableProps { addDangerToast: (message: string) => void; addSuccessToast: (message: string) => void; search: string; - dashboardFilter?: string; user?: User; } @@ -56,3 +55,17 @@ export interface DashboardCardProps { addSuccessToast: (msg: string) => void; openDashboardEditModal?: (d: Dashboard) => void; } + +export type SavedQueryObject = { + database: { + database_name: string; + id: number; + }; + db_id: number; + description?: string; + id: number; + label: string; + schema: string; + sql: string; + sql_tables: Array<{ catalog?: string; schema: string; table: string }>; +}; \ No newline at end of file diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 6caabb9..2208cc6 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -20,6 +20,7 @@ import { SupersetClient, SupersetClientResponse, logging, + styled, } from '@superset-ui/core'; import rison from 'rison'; import getClientErrorObject from 'src/utils/getClientErrorObject'; @@ -53,27 +54,82 @@ const createFetchResourceMethod = (method: string) => ( return []; }; -export const createBatchMethod = (queryParams: string, created?: string) => { +export const getBatchData = (userId: string, recent: string) => { + const getParams = (filters?: Array<any>) => { + 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 filters = { + // chart and dashbaord uses same filters + // for edited and created + edited: [ + { + col: 'changed_by', + opr: 'rel_o_m', + value: `${userId}`, + }, + ], + created: [ + { + col: 'created_by', + opr: 'rel_o_m', + value: `${userId}`, + }, + ], + }; const baseBatch = [ - SupersetClient.get({ endpoint: `/api/v1/dashboard/?q=${queryParams}` }), - SupersetClient.get({ endpoint: `/api/v1/chart/?q=${queryParams}` }), + SupersetClient.get({ endpoint: recent }), + 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)}`, + }), ]; - if (created) - baseBatch.push( - SupersetClient.get({ endpoint: `/api/v1/saved_query/?q=${queryParams}` }), - ); - return Promise.all(baseBatch).then(([dashboardRes, chartRes, savedQuery]) => { - const results = []; - const ifQuery = savedQuery ? savedQuery.json?.result.slice(0, 3) : []; - results.push( - ...[ - ...dashboardRes.json?.result.slice(0, 3), - ...chartRes.json?.result.slice(0, 3), - ...ifQuery, - ], - ); - return results; - }); + return Promise.all(baseBatch).then( + // @ts-ignore + ([recentsRes, editedDash, editedChart, createdByDash, createdByChart]) => { + 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), + }; + if (recentsRes.json.length === 0) { + const newBatch = [ + SupersetClient.get({ endpoint: `/api/v1/chart/?q=${getParams()}` }), + SupersetClient.get({ + endpoint: `/api/v1/dashboard/?q=${getParams()}`, + }), + ]; + // @ts-ignore + return Promise.all(newBatch) + .then(([chartRes, dashboardRes]) => { + res.examples = [ + ...chartRes.json.result, + ...dashboardRes.json.result, + ]; + return res; + }) + .catch(e => console.log('err', e)); + } + res.viewed = recentsRes.json; + return res; + }, + ); }; export const createFetchRelated = createFetchResourceMethod('related'); @@ -86,3 +142,39 @@ export function createErrorHandler(handleErrorFunc: (errMsg?: string) => void) { handleErrorFunc(parsedError.message || parsedError.error); }; } + +export function handleDashboardDelete(id: string) { + return SupersetClient.delete({ + endpoint: `/api/v1/dashboard/${id}`, + }); +} + +const breakpoints = [576, 768, 992, 1200]; +export const mq = breakpoints.map(bp => `@media (max-width: ${bp}px)`); + +export const CardContainer = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(31%, max-content)); + ${[mq[3]]} { + grid-template-columns: repeat(auto-fit, minmax(31%, max-content)); + } + + ${[mq[2]]} { + grid-template-columns: repeat(auto-fit, minmax(48%, max-content)); + } + + ${[mq[1]]} { + grid-template-columns: repeat(auto-fit, minmax(48%, max-content)); + } + grid-gap: ${({ theme }) => theme.gridUnit * 8}px; + justify-content: left; + padding: ${({ theme }) => theme.gridUnit * 2}px + ${({ theme }) => theme.gridUnit * 6}px; +`; + +export const IconContainer = styled.div` + svg { + vertical-align: -7px; + color: ${({ theme }) => theme.colors.primary.dark1}; + } +`; diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx index 669838d..02d2cbc 100644 --- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx @@ -17,11 +17,15 @@ * under the License. */ import React, { useEffect, useState } from 'react'; -import rison from 'rison'; -import moment from 'moment'; +import moment from 'antd/node_modules/moment'; +import { styled, t } from '@superset-ui/core'; + import ListViewCard from 'src/components/ListViewCard'; import { addDangerToast } from 'src/messageToasts/actions'; -import { createBatchMethod, createErrorHandler } from '../utils'; +import SubMenu from 'src/components/Menu/SubMenu'; +import { reject } from 'lodash'; +import { getBatchData, createErrorHandler } from '../utils'; +import EmptyState from './EmptyState'; interface MapProps { action?: string; @@ -35,88 +39,129 @@ interface MapProps { label: string; id: string; table: object; + item_url: string; } interface ActivityProps { user: { - userId: string | number; + userId: string; }; - activityFilter: string; } -export default function ActivityTable({ user, activityFilter }: ActivityProps) { - const [active, setActiveState] = useState([]); - const [loading, setLoading] = useState(false); - // this API uses Log for data which in some cases is can be empty - // const recent = `/superset/recent_activity/${user.userId}/?limit=5`; - const filters = { - // Chart and dashbaord uses same filters - // for edited and created - edited: [ - { - col: 'changed_by', - opr: 'rel_o_m', - value: `${user.userId}`, - }, - ], - created: [ - { - col: 'created_by', - opr: 'rel_o_m', - value: `${user.userId}`, - }, - ], - }; +interface ActivityData { + Created: Array<object>; + Edited: Array<object>; + Viewed: Array<object>; + Examples: Array<object>; +} - const setBatchData = (q: string, created?: string) => { - createBatchMethod(q, created) - .then((res: Array<object>) => - // @ts-ignore - setActiveState(res), - ) - .catch(() => addDangerToast('Oops something went wrong')); - }; +const ActivityContainer = styled.div` + margin-left: 10px; + margin-top: -15px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(31%, max-content)); + grid-gap: ${({ theme }) => theme.gridUnit * 8}px; + justify-content: left; + padding: ${({ theme }) => theme.gridUnit * 2}px + ${({ theme }) => theme.gridUnit * 4}px; + .ant-card-meta-avatar { + margin-top: 5px; + } + .ant-card-meta-title { + font-weight: 700; + } +`; + +export default function ActivityTable({ user }: ActivityProps) { + const [activityData, setActivityData] = useState<ActivityData | {}>({}); + const [loading, setLoading] = useState(true); + const [activeChild, setActiveChild] = useState('Viewed'); + // this api uses log for data which in some cases is can be empty + const recent = `/superset/recent_activity/${user.userId}/?limit=5`; const getFilterTitle = (e: MapProps) => { if (e.dashboard_title) return e.dashboard_title; if (e.label) return e.label; if (e.url && !e.table) return e.item_title; + if (e.item_title) return e.item_title; return e.slice_name; }; const getIconName = (e: MapProps) => { if (e.sql) return 'sql'; - if (e.url.indexOf('dashboard') !== -1) { + if (e.url?.indexOf('dashboard') !== -1) { return 'nav-dashboard'; } - if (e.url.indexOf('explore') !== -1) { + if (e.url?.indexOf('explore') !== -1) { + return 'nav-charts'; + } + if (e.item_url?.indexOf('explore') !== -1) { return 'nav-charts'; } return ''; }; - const getData = () => { - const queryParams = rison.encode({ - order_column: 'changed_on_delta_humanized', - order_direction: 'desc', - page: 0, - page_size: 0, - filters: activityFilter !== 'Created' ? filters.edited : filters.created, + const tabs = [ + { + name: 'Edited', + label: t('Edited'), + onClick: () => { + setActiveChild('Edited'); + }, + }, + { + name: 'Created', + label: t('Created'), + onClick: () => { + setActiveChild('Created'); + }, + }, + ]; + + if (activityData.Viewed) { + tabs.unshift({ + name: 'Viewed', + label: t('Viewed'), + onClick: () => { + setActiveChild('Viewed'); + }, }); - if (activityFilter === 'Edited') { - setBatchData(queryParams); - } - if (activityFilter === 'Created') { - setBatchData(queryParams, 'createdBy'); - } - }; + } else { + tabs.unshift({ + name: 'Examples', + label: t('Examples'), + onClick: () => { + setActiveChild('Examples'); + }, + }); + } useEffect(() => { - getData(); - }, [activityFilter]); + getBatchData(user.userId, recent) + .then(r => { + const data: any = { + Created: [...r.createdByChart, ...r.createdByDash], + Edited: [...r.editedChart, ...r.editedDash], + }; + if (r.viewed) { + const filtered = reject(r.viewed, ['item_url', null]).map(r => r); + data.Viewed = filtered; + setActiveChild('Viewed'); + } else { + data.Examples = r.examples; + setActiveChild('Examples'); + } + setActivityData(data); + setLoading(false); + }) + .catch(e => { + setLoading(false); + addDangerToast(`e ${e}`); + }); + }, []); const renderActivity = () => { - return active.map((e: MapProps, i) => ( + return activityData[activeChild].map((e: MapProps, i: any) => ( <ListViewCard key={`${i}`} isRecent @@ -125,12 +170,32 @@ export default function ActivityTable({ user, activityFilter }: ActivityProps) { imgFallbackURL="" url={e.sql ? `/supserset/sqllab?queryId=${e.id}` : e.url} title={getFilterTitle(e)} - description={moment.utc(e.changed_on_utc).fromNow()} + description={`Last Edited: ${moment(e.changed_on_utc).format( + 'MM/DD/YYYY HH:mm:ss', + )}`} avatar={getIconName(e)} actions={null} /> )); }; - - return <> {renderActivity()} </>; + if (loading) return <>loading ...</>; + return ( + <> + <> + <SubMenu + activeChild={activeChild} + name="" + // eslint-disable-next-line react/no-children-prop + children={tabs} + /> + <> + {activityData[activeChild]?.length > 0 ? ( + <ActivityContainer>{renderActivity()}</ActivityContainer> + ) : ( + <EmptyState tableName="RECENTS" tab="Mine" /> + )} + </> + </> + </> + ); } diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx index d0ccaee..99d51c0 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx @@ -16,15 +16,18 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { t } from '@superset-ui/core'; import { useListViewResource, useChartEditModal } from 'src/views/CRUD/hooks'; import withToasts from 'src/messageToasts/enhancers/withToasts'; import PropertiesModal from 'src/explore/components/PropertiesModal'; import { User } from 'src/types/bootstrapTypes'; -import Owner from 'src/types/Owner'; +import Icon from 'src/components/Icon'; import ChartCard from 'src/views/CRUD/chart/ChartCard'; import Chart from 'src/types/Chart'; +import SubMenu from 'src/components/Menu/SubMenu'; +import EmptyState from './EmptyState'; +import { CardContainer, IconContainer } from '../utils'; const PAGE_SIZE = 3; @@ -37,7 +40,6 @@ interface ChartTableProps { } function ChartTable({ - chartFilter, user, addDangerToast, addSuccessToast, @@ -57,6 +59,8 @@ function ChartTable({ closeChartEditModal, } = useChartEditModal(setCharts, charts); + const [chartFilter, setChartFilter] = useState('Favorite'); + const getFilters = () => { const filters = []; @@ -110,19 +114,64 @@ function ChartTable({ /> )} - {charts.map((e, i) => ( - <ChartCard - key={`${i}`} - openChartEditModal={openChartEditModal} - loading={loading} - chart={e} - hasPerm={hasPerm} - bulkSelectEnabled={bulkSelectEnabled} - refreshData={refreshData} - addDangerToast={addDangerToast} - addSuccessToast={addSuccessToast} - /> - ))} + <SubMenu + activeChild={chartFilter} + name="" + // eslint-disable-next-line react/no-children-prop + children={[ + { + name: 'Favorite', + label: t('Favorite'), + onClick: () => setChartFilter('Favorite'), + }, + { + name: 'Mine', + label: t('Mine'), + onClick: () => setChartFilter('Mine'), + }, + ]} + buttons={[ + { + name: ( + <IconContainer> + <Icon name="plus-small" /> Chart{' '} + </IconContainer> + ), + buttonStyle: 'tertiary', + onClick: () => { + // @ts-ignore + window.location = '/chart/add'; + }, + }, + { + name: 'View All »', + buttonStyle: 'link', + onClick: () => { + // @ts-ignore + window.location = '/chart/list'; + }, + }, + ]} + /> + {charts.length ? ( + <CardContainer> + {charts.map((e, i) => ( + <ChartCard + key={`${i}`} + openChartEditModal={openChartEditModal} + loading={loading} + chart={e} + hasPerm={hasPerm} + bulkSelectEnabled={bulkSelectEnabled} + refreshData={refreshData} + addDangerToast={addDangerToast} + addSuccessToast={addSuccessToast} + /> + ))} + </CardContainer> + ) : ( + <EmptyState tableName="CHARTS" tab={chartFilter} /> + )} </> ); } diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx index c47b2dd..ae3daeb 100644 --- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx @@ -16,30 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useEffect } from 'react'; -import { t } from '@superset-ui/core'; -import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks'; +import React, { useEffect, useState } from 'react'; +import { SupersetClient, t } from '@superset-ui/core'; +import { useListViewResource } from 'src/views/CRUD/hooks'; +import { Dashboard, DashboardTableProps } from 'src/views/CRUD/types'; import withToasts from 'src/messageToasts/enhancers/withToasts'; -import Owner from 'src/types/Owner'; -import { DashboardTableProps } from 'src/views/CRUD/types'; +import PropertiesModal from 'src/dashboard/components/PropertiesModal'; import DashboardCard from 'src/views/CRUD/dashboard/DashboardCard'; +import SubMenu from 'src/components/Menu/SubMenu'; +import Icon from 'src/components/Icon'; +import EmptyState from './EmptyState'; +import { createErrorHandler, CardContainer, IconContainer } from '../utils'; const PAGE_SIZE = 3; -interface Dashboard { - changed_by_name: string; - changed_by_url: string; - changed_on_delta_humanized: string; - changed_by: string; - dashboard_title: string; - id: number; - published: boolean; - url: string; - thumbnail_url: string; - owners: Owner[]; - loading: boolean; -} - export interface FilterValue { col: string; operator: string; @@ -47,14 +37,13 @@ export interface FilterValue { } function DashboardTable({ - dashboardFilter, user, addDangerToast, addSuccessToast, - search, }: DashboardTableProps) { const { state: { loading, resourceCollection: dashboards, bulkSelectEnabled }, + setResourceCollection: setDashboards, hasPerm, refreshData, fetchData, @@ -64,6 +53,31 @@ function DashboardTable({ addDangerToast, ); + const [editModal, setEditModal] = useState<Dashboard | null>(null); + const [dashboardFilter, setDashboardFilter] = useState('Favorite'); + + const handleDashboardEdit = (edits: Dashboard) => { + return SupersetClient.get({ + endpoint: `/api/v1/dashboard/${edits.id}`, + }).then( + ({ json = {} }) => { + setDashboards( + dashboards.map(dashboard => { + if (dashboard.id === json.id) { + return json.result; + } + return dashboard; + }), + ); + }, + createErrorHandler(errMsg => + addDangerToast( + t('An error occurred while fetching dashboards: %s', errMsg), + ), + ), + ); + }; + const getFilters = () => { const filters = []; @@ -80,15 +94,16 @@ function DashboardTable({ value: true, }); } - filters.concat([ - { - id: 'dashboard_title', - operator: 'ct', - value: search, - }, - ]); return filters; }; + const subMenus = []; + if (dashboards.length > 0 && dashboardFilter === 'favorite') { + subMenus.push({ + name: 'Favorite', + label: t('Favorite'), + onClick: () => setDashboardFilter('Favorite'), + }); + } useEffect(() => { fetchData({ @@ -106,18 +121,73 @@ function DashboardTable({ return ( <> - {dashboards.map(e => ( - <DashboardCard - {...{ - dashboard: e, - hasPerm, - bulkSelectEnabled, - refreshData, - addDangerToast, - addSuccessToast, - }} + <SubMenu + activeChild={dashboardFilter} + name="" + // eslint-disable-next-line react/no-children-prop + children={[ + { + name: 'Favorite', + label: t('Favorite'), + onClick: () => setDashboardFilter('Favorite'), + }, + { + name: 'Mine', + label: t('Mine'), + onClick: () => setDashboardFilter('Mine'), + }, + ]} + buttons={[ + { + name: ( + <IconContainer> + <Icon name="plus-small" /> Dashboard{' '} + </IconContainer> + ), + buttonStyle: 'tertiary', + onClick: () => { + // @ts-ignore + window.location = '/dashboard/new'; + }, + }, + { + name: 'View All »', + buttonStyle: 'link', + onClick: () => { + // @ts-ignore + window.location = '/dashboard/list/'; + }, + }, + ]} + /> + {editModal && ( + <PropertiesModal + dashboardId={editModal?.id} + show + onHide={() => setEditModal(null)} + onSubmit={handleDashboardEdit} /> - ))} + )} + {dashboards.length > 0 ? ( + <CardContainer> + {dashboards.map(e => ( + <DashboardCard + {...{ + dashboard: e, + hasPerm, + bulkSelectEnabled, + refreshData, + addDangerToast, + addSuccessToast, + loading, + openDashboardEditModal: dashboard => setEditModal(dashboard), + }} + /> + ))} + </CardContainer> + ) : ( + <EmptyState tableName="DASHBOARDS" tab={dashboardFilter} /> + )} </> ); } diff --git a/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx new file mode 100644 index 0000000..e1c2936 --- /dev/null +++ b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx @@ -0,0 +1,112 @@ +/** + * 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. + */ +import React from 'react'; +import Button from 'src/components/Button'; +import { styled } from '@superset-ui/core'; +import Icon from 'src/components/Icon'; +import { IconContainer } from '../utils'; + +interface EmptyStateProps { + tableName: string; + tab?: string; +} + +const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + img { + width: 87px; + display: block; + margin: 0 auto; + } + div:nth-child(2) { + text-align: center; + margin-top: 15px; + color: ${({ theme }) => theme.colors.grayscale.dark1}; + font-weight: 400; + } + button { + margin: 0 auto; + padding: 6px 27px; + margin-top: 10px; + svg { + color: white; + } + } +`; + +export default function EmptyState({ tableName, tab }: EmptyStateProps) { + const mineRedirects = { + DASHBOARDS: '/dashboard/new', + CHARTS: '/chart/add', + SAVED_QUERIES: '/superset/sqllab', + }; + const favRedirects = { + DASHBOARDS: '/dashboard/list/', + CHARTS: '/chart/list', + SAVED_QUERIES: '/savedqueryview/list/', + }; + const Mine = ( + <div> + <div>{`No ${tableName.toLowerCase()} yet`}</div> + <Button + buttonStyle="primary" + onClick={() => { + window.location = mineRedirects[tableName]; + }} + > + <IconContainer> + <Icon name="plus-small" /> {tableName} + </IconContainer> + </Button> + </div> + ); + const span = ( + <div> + Recently viewed charts, dashboards, and saved queries will appear here + </div> + ); + + if (tab === 'Mine') { + return ( + <Container> + <img src="/static/assets/images/union.png" alt="union.png" /> + {tableName === 'RECENTS' ? span : Mine} + </Container> + ); + } + + return ( + <Container> + <img src="/static/assets/images/star-circle.png" alt="star.png" /> + <div> + <div>You don't have any favorites yets!</div> + <Button + buttonStyle="primary" + onClick={() => { + window.location = favRedirects[tableName]; + }} + > + SEE ALL {tableName} + </Button> + </div> + </Container> + ); +} diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx index c60d4fb..333d394 100644 --- a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx +++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx @@ -17,18 +17,24 @@ * under the License. */ import React, { useEffect, useState } from 'react'; -import { t } from '@superset-ui/core'; +import { t, styled, SupersetClient } from '@superset-ui/core'; import withToasts from 'src/messageToasts/enhancers/withToasts'; import { Dropdown, Menu } from 'src/common/components'; -import { useListViewResource } from 'src/views/CRUD/hooks'; +import { useListViewResource, copyQueryLink } from 'src/views/CRUD/hooks'; import ListViewCard from 'src/components/ListViewCard'; +import DeleteModal from 'src/components/DeleteModal'; import Icon from 'src/components/Icon'; -import { addDangerToast } from 'src/messageToasts/actions'; +import { SavedQueryObject } from 'src/views/CRUD/types'; +import SubMenu from 'src/components/Menu/SubMenu'; +import EmptyState from './EmptyState'; + +import { IconContainer, CardContainer, createErrorHandler } from '../utils'; const PAGE_SIZE = 3; interface Query { - sql_tables: array; + id: number; + sql_tables: Array<any>; database: { database_name: string; }; @@ -36,6 +42,7 @@ interface Query { description: string; end_time: string; addDangerToast: () => void; + label: string; } interface SavedQueriesProps { @@ -45,11 +52,46 @@ interface SavedQueriesProps { queryFilter: string; } -const SavedQueries = ({ user, queryFilter }: SavedQueriesProps) => { +const NoData = styled.div` + .create-your-query { + display: block; + margin: 0 auto; + } +`; + +const SavedQueries = ({ + user, + addDangerToast, + addSuccessToast, +}: SavedQueriesProps) => { const { state: { loading, resourceCollection: queries }, + hasPerm, fetchData, + refreshData, } = useListViewResource<Query>('saved_query', t('query'), addDangerToast); + const [queryFilter, setQueryFilter] = useState('Favorite'); + const [queryDeleteModal, setQueryDeleteModal] = useState(false); + const [currentlyEdited, setCurrentlyEdited] = useState(null); + + const canEdit = hasPerm('can_edit'); + const canDelete = hasPerm('can_delete'); + + const handleQueryDelete = ({ id, label }: SavedQueryObject) => { + SupersetClient.delete({ + endpoint: `/api/v1/saved_query/${id}`, + }).then( + () => { + refreshData(); + setQueryDeleteModal(false); + addSuccessToast(t('Deleted: %s', label)); + }, + createErrorHandler(errMsg => + addDangerToast(t('There was an issue deleting %s: %s', label, errMsg)), + ), + ); + }; + const getFilters = () => { const filters = []; @@ -83,36 +125,117 @@ const SavedQueries = ({ user, queryFilter }: SavedQueriesProps) => { }); }, [queryFilter]); - const menu = ( + const renderMenu = (query: Query) => ( <Menu> - <Menu.Item>Delete</Menu.Item> + {canEdit && ( + <Menu.Item + onClick={() => { + // @ts-ignore + window.location = `/superset/sqllab?savedQueryId=${query.id}`; + }} + > + Edit + </Menu.Item> + )} + <Menu.Item + onClick={() => copyQueryLink(query.id, addDangerToast, addSuccessToast)} + >Share</Menu.Item> + {canDelete && ( + <Menu.Item + onClick={() => { + setQueryDeleteModal(true); + setCurrentlyEdited(query); + }} + > + Delete + </Menu.Item> + )} </Menu> ); return ( <> - {queries ? ( - queries.map(q => ( - <ListViewCard - imgFallbackURL="/static/assets/images/dashboard-card-fallback.png" - imgURL="" - title={q.database.database_name} - rows={q.rows} - tableName={q.sql_tables[0].table} - loading={loading} - description={t('Last run ', q.end_time)} - showImg={false} - actions={ - <ListViewCard.Actions> - <Dropdown overlay={menu}> - <Icon name="more-horiz" /> - </Dropdown> - </ListViewCard.Actions> + {queryDeleteModal && ( + <DeleteModal + description={t( + 'This action will permanently delete the saved query.', + )} + onConfirm={() => { + if (queryDeleteModal) { + handleQueryDelete(currentlyEdited); } - /> - )) + }} + onHide={() => { + setQueryDeleteModal(false); + }} + open + title={t('Delete Query?')} + /> + )} + <SubMenu + activeChild={queryFilter} + name="" + // eslint-disable-next-line react/no-children-prop + children={[ + { + name: 'Favorite', + label: t('Favorite'), + onClick: () => setQueryFilter('Favorite'), + }, + { + name: 'Mine', + label: t('Mine'), + onClick: () => setQueryFilter('Mine'), + }, + ]} + buttons={[ + { + name: ( + <IconContainer> + <Icon name="plus-small" /> SQL Query{' '} + </IconContainer> + ), + buttonStyle: 'tertiary', + onClick: () => { + // @ts-ignore + window.location = '/superset/sqllab'; + }, + }, + { + name: 'View All »', + buttonStyle: 'link', + onClick: () => { + // @ts-ignore + window.location = '/savedqueryview/list'; + }, + }, + ]} + /> + {queries.length > 0 ? ( + <CardContainer> + {queries.map(q => ( + <ListViewCard + imgFallbackURL="" + imgURL="" + title={q.label} + rows={q.rows} + tableName={q.sql_tables[0]?.table} + tables={q.sql_tables?.length} + loading={loading} + description={t('Last run ', q.end_time)} + showImg={false} + actions={ + <ListViewCard.Actions> + <Dropdown overlay={renderMenu(q)}> + <Icon name="more-horiz" /> + </Dropdown> + </ListViewCard.Actions> + } + /> + ))} + </CardContainer> ) : ( - <span>You have no Saved Queries!</span> + <EmptyState tableName="SAVED_QUERIES" tab={queryFilter} /> )} </> ); diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx index f0934e1..7f7464b 100644 --- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx +++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState } from 'react'; -import SubMenu from 'src/components/Menu/SubMenu'; +import React from 'react'; import { styled, t } from '@superset-ui/core'; import { Collapse } from 'src/common/components'; import { User } from 'src/types/bootstrapTypes'; @@ -35,213 +34,57 @@ interface WelcomeProps { } const WelcomeContainer = styled.div` + background-color: ${({ theme }) => theme.colors.grayscale.light4}; nav { + margin-top: -15px; background-color: ${({ theme }) => theme.colors.grayscale.light4}; &:after { content: ''; display: block; border: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; margin: 0px 26px; + position: relative; + top: -13px; + } + .nav.navbar-nav { + & > li:nth-child(1), + & > li:nth-child(2), + & > li:nth-child(3) { + margin-top: 8px; + } + } + button { + padding: 3px 21px; + } + .navbar-right { + position: relative; + top: 11px; } } .ant-card.ant-card-bordered { border: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; } -`; - -const ActivityContainer = styled.div` - display: grid; - grid-template-columns: repeat(auto-fit, minmax(31%, max-content)); - grid-gap: ${({ theme }) => theme.gridUnit * 8}px; - justify-content: center; - padding: ${({ theme }) => theme.gridUnit * 2}px - ${({ theme }) => theme.gridUnit * 4}px; -`; - -const IconContainer = styled.div` - svg { - vertical-align: -7px; - color: ${({ theme }) => theme.colors.primary.dark1}; + .ant-collapse-header { + font-weight: 500; + font-size: 16px; } `; -export const CardContainer = styled.div` - display: grid; - grid-template-columns: repeat(auto-fit, minmax(459px, 1fr)); - grid-gap: ${({ theme }) => theme.gridUnit * 8}px; - justify-content: left; - padding: ${({ theme }) => theme.gridUnit * 2}px - ${({ theme }) => theme.gridUnit * 6}px; -`; export default function Welcome({ user }: WelcomeProps) { - const [queryFilter, setQueryFilter] = useState('Favorite'); - const [activityFilter, setActivityFilter] = useState('Edited'); - const [dashboardFilter, setDashboardFilter] = useState('Favorite'); - const [chartFilter, setChartFilter] = useState('Favorite'); - - function ExpandIcon(): React.ReactNode { - return <Icon name="caret-right" />; - } - return ( <WelcomeContainer> <Collapse defaultActiveKey={['1']} ghost> <Panel header={t('Recents')} key="1"> - <SubMenu - activeChild={activityFilter} - name="" - // eslint-disable-next-line react/no-children-prop - children={[ - { - name: 'Edited', - label: t('Edited'), - onClick: () => setActivityFilter('Edited'), - }, - { - name: 'Created', - label: t('Created'), - onClick: () => setActivityFilter('Created'), - }, - ]} - /> - <ActivityContainer> - <ActivityTable user={user} activityFilter={activityFilter} /> - </ActivityContainer> + <ActivityTable user={user} /> </Panel> - - <Panel header={t('Dashboards')} key="2"> - <SubMenu - activeChild={dashboardFilter} - name="" - // eslint-disable-next-line react/no-children-prop - children={[ - { - name: 'Favorite', - label: t('Favorite'), - onClick: () => setDashboardFilter('Favorite'), - }, - { - name: 'Mine', - label: t('Mine'), - onClick: () => setDashboardFilter('Mine'), - }, - ]} - buttons={[ - { - name: ( - <IconContainer> - <Icon name="plus-small" /> Dashboard{' '} - </IconContainer> - ), - buttonStyle: 'tertiary', - onClick: () => { - // @ts-ignore - window.location = '/dashboard/new'; - }, - }, - { - name: 'View All »', - buttonStyle: 'link', - onClick: () => { - // @ts-ignore - window.location = '/dashboard/list/'; - }, - }, - ]} - /> - <CardContainer> - <DashboardTable dashboardFilter={dashboardFilter} user={user} /> - </CardContainer> + <Panel header={t('Dashboards')} key="1"> + <DashboardTable user={user} /> </Panel> - - <Panel header={t('Saved Queries')} key="3"> - <SubMenu - activeChild={queryFilter} - name="" - // eslint-disable-next-line react/no-children-prop - children={[ - { - name: 'Favorite', - label: t('Favorite'), - onClick: () => setQueryFilter('Favorite'), - }, - { - name: 'Mine', - label: t('Mine'), - onClick: () => setQueryFilter('Mine'), - }, - ]} - buttons={[ - { - name: ( - <IconContainer> - <Icon name="plus-small" /> SQL Query{' '} - </IconContainer> - ), - buttonStyle: 'tertiary', - onClick: () => { - // @ts-ignore - window.location = '/superset/sqllab'; - }, - }, - { - name: 'View All »', - buttonStyle: 'link', - onClick: () => { - // @ts-ignore - window.location = '/savedqueryview/list'; - }, - }, - ]} - /> - <CardContainer> - <SavedQueries user={user} queryFilter={queryFilter} /> - </CardContainer> + <Panel header={t('Saved Queries')} key="1"> + <SavedQueries user={user} /> </Panel> - <Panel header={t('Charts')} key="4"> - <SubMenu - activeChild={chartFilter} - name="" - // eslint-disable-next-line react/no-children-prop - children={[ - { - name: 'Favorite', - label: t('Favorite'), - onClick: () => setChartFilter('Favorite'), - }, - { - name: 'Mine', - label: t('Mine'), - onClick: () => setChartFilter('Mine'), - }, - ]} - buttons={[ - { - name: ( - <IconContainer> - <Icon name="plus-small" /> Chart{' '} - </IconContainer> - ), - buttonStyle: 'tertiary', - onClick: () => { - // @ts-ignore - window.location = '/chart/add'; - }, - }, - { - name: 'View All »', - buttonStyle: 'link', - onClick: () => { - // @ts-ignore - window.location = '/chart/list'; - }, - }, - ]} - /> - - <CardContainer> - <ChartTable chartFilter={chartFilter} user={user} /> - </CardContainer> + <Panel header={t('Charts')} key="1"> + <ChartTable user={user} /> </Panel> </Collapse> </WelcomeContainer>
