From 3e4c3bddb977b85c645fd6302f4f24d2f56baebe Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Thu, 25 Mar 2021 10:21:04 -0700 Subject: [PATCH] feat(homescreen and cards): Toggle thumbnails off or on and feature flag (#13683) --- .../views/CRUD/welcome/Welcome_spec.tsx | 67 ++++++++++++++---- .../src/views/CRUD/chart/ChartCard.tsx | 5 ++ .../views/CRUD/dashboard/DashboardCard.tsx | 5 ++ superset-frontend/src/views/CRUD/types.ts | 2 + .../src/views/CRUD/welcome/ChartTable.tsx | 6 ++ .../src/views/CRUD/welcome/DashboardTable.tsx | 4 ++ .../src/views/CRUD/welcome/SavedQueries.tsx | 10 ++- .../src/views/CRUD/welcome/Welcome.tsx | 69 +++++++++++++++++-- 8 files changed, 150 insertions(+), 18 deletions(-) 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 a58df6bcf..d71864e63 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx +++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx @@ -23,8 +23,10 @@ import thunk from 'redux-thunk'; import fetchMock from 'fetch-mock'; import { act } from 'react-dom/test-utils'; import configureStore from 'redux-mock-store'; +import * as featureFlags from 'src/featureFlags'; import Welcome from 'src/views/CRUD/welcome/Welcome'; import { ReactWrapper } from 'enzyme'; +import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; const mockStore = configureStore([thunk]); const store = mockStore({}); @@ -92,19 +94,19 @@ fetchMock.get(savedQueryInfoEndpoint, { permissions: [], }); -describe('Welcome', () => { - const mockedProps = { - user: { - username: 'alpha', - firstName: 'alpha', - lastName: 'alpha', - createdOn: '2016-11-11T12:34:17', - userId: 5, - email: 'alpha@alpha.com', - isActive: true, - }, - }; +const mockedProps = { + user: { + username: 'alpha', + firstName: 'alpha', + lastName: 'alpha', + createdOn: '2016-11-11T12:34:17', + userId: 5, + email: 'alpha@alpha.com', + isActive: true, + }, +}; +describe('Welcome', () => { let wrapper: ReactWrapper; beforeAll(async () => { @@ -136,3 +138,44 @@ describe('Welcome', () => { expect(dashboardCall).toHaveLength(1); }); }); + +async function mountAndWait(props = mockedProps) { + const wrapper = mount( + + + , + ); + await waitForComponentToPaint(wrapper); + return wrapper; +} + +describe('Welcome page with toggle switch', () => { + let wrapper: ReactWrapper; + let isFeatureEnabledMock: any; + + beforeAll(async () => { + isFeatureEnabledMock = jest + .spyOn(featureFlags, 'isFeatureEnabled') + .mockReturnValue(true); + await act(async () => { + wrapper = await mountAndWait(); + }); + }); + + afterAll(() => { + isFeatureEnabledMock.restore(); + }); + + it('shows a toggle button when feature flags is turned on', async () => { + await waitForComponentToPaint(wrapper); + expect(wrapper.find('Switch')).toExist(); + }); + it('does not show thumbnails when switch is off', async () => { + act(() => { + // @ts-ignore + wrapper.find('button[role="switch"]').props().onClick(); + }); + await waitForComponentToPaint(wrapper); + expect(wrapper.find('ImageLoader')).not.toExist(); + }); +}); diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx index 457f46b70..c8ce8523b 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx @@ -44,6 +44,8 @@ interface ChartCardProps { favoriteStatus: boolean; chartFilter?: string; userId?: number; + showThumbnails?: boolean; + featureFlag?: boolean; } export default function ChartCard({ @@ -55,10 +57,12 @@ export default function ChartCard({ addSuccessToast, refreshData, loading, + showThumbnails, saveFavoriteStatus, favoriteStatus, chartFilter, userId, + featureFlag, }: ChartCardProps) { const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); @@ -138,6 +142,7 @@ export default function ChartCard({ : null} url={bulkSelectEnabled ? undefined : chart.url} imgURL={chart.thumbnail_url || ''} imgFallbackURL="/static/assets/images/chart-card-fallback.svg" diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx index 1431eb81b..ea7c070d7 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx @@ -46,6 +46,8 @@ interface DashboardCardProps { favoriteStatus: boolean; dashboardFilter?: string; userId?: number; + showThumbnails?: boolean; + featureFlag?: boolean; } function DashboardCard({ @@ -60,6 +62,8 @@ function DashboardCard({ openDashboardEditModal, favoriteStatus, saveFavoriteStatus, + showThumbnails, + featureFlag, }: DashboardCardProps) { const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); @@ -146,6 +150,7 @@ function DashboardCard({ titleRight={ } + cover={!featureFlag || !showThumbnails ? <> : null} url={bulkSelectEnabled ? undefined : dashboard.url} imgURL={dashboard.thumbnail_url} imgFallbackURL="/static/assets/images/dashboard-card-fallback.svg" diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts index 4de3055c1..6d369d864 100644 --- a/superset-frontend/src/views/CRUD/types.ts +++ b/superset-frontend/src/views/CRUD/types.ts @@ -35,6 +35,8 @@ export interface DashboardTableProps { search: string; user?: User; mine: Array; + showThumbnails?: boolean; + featureFlag?: boolean; } export interface Dashboard { diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx index 5eb0b294d..addfaafec 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx @@ -44,6 +44,8 @@ interface ChartTableProps { chartFilter?: string; user?: User; mine: Array; + showThumbnails: boolean; + featureFlag: boolean; } function ChartTable({ @@ -51,6 +53,8 @@ function ChartTable({ addDangerToast, addSuccessToast, mine, + showThumbnails, + featureFlag, }: ChartTableProps) { const history = useHistory(); const { @@ -180,7 +184,9 @@ function ChartTable({ chart={e} userId={user?.userId} hasPerm={hasPerm} + showThumbnails={showThumbnails} bulkSelectEnabled={bulkSelectEnabled} + featureFlag={featureFlag} refreshData={refreshData} addDangerToast={addDangerToast} addSuccessToast={addSuccessToast} diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx index 27a41d4dc..64d1f8019 100644 --- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx @@ -42,6 +42,8 @@ function DashboardTable({ addDangerToast, addSuccessToast, mine, + showThumbnails, + featureFlag, }: DashboardTableProps) { const history = useHistory(); const { @@ -191,6 +193,8 @@ function DashboardTable({ dashboard={e} hasPerm={hasPerm} bulkSelectEnabled={false} + featureFlag={featureFlag} + showThumbnails={showThumbnails} dashboardFilter={dashboardFilter} refreshData={refreshData} addDangerToast={addDangerToast} diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx index 47a55fa66..e8ec84acd 100644 --- a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx +++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx @@ -58,6 +58,8 @@ interface SavedQueriesProps { addDangerToast: (arg0: string) => void; addSuccessToast: (arg0: string) => void; mine: Array; + showThumbnails: boolean; + featureFlag: boolean; } export const CardStyles = styled.div` @@ -110,6 +112,8 @@ const SavedQueries = ({ addDangerToast, addSuccessToast, mine, + showThumbnails, + featureFlag, }: SavedQueriesProps) => { const { state: { loading, resourceCollection: queries }, @@ -307,7 +311,7 @@ const SavedQueries = ({ imgFallbackURL="/static/assets/images/empty-query.svg" description={t('Last run %s', q.changed_on_delta_humanized)} cover={ - q?.sql?.length ? ( + q?.sql?.length && showThumbnails && featureFlag ? ( - ) : ( + ) : showThumbnails && !q?.sql?.length ? ( false + ) : ( + <> ) } actions={ diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx index 0d1f3ba66..bb9da4a6a 100644 --- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx +++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx @@ -21,6 +21,10 @@ import { styled, t } from '@superset-ui/core'; import Collapse from 'src/common/components/Collapse'; import { User } from 'src/types/bootstrapTypes'; import { reject } from 'lodash'; +import { + getFromLocalStorage, + setInLocalStorage, +} from 'src/utils/localStorageHelpers'; import withToasts from 'src/messageToasts/enhancers/withToasts'; import Loading from 'src/components/Loading'; import { @@ -29,6 +33,8 @@ import { mq, getUserOwnedObjects, } from 'src/views/CRUD/utils'; +import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; +import { Switch } from 'src/common/components'; import ActivityTable from './ActivityTable'; import ChartTable from './ChartTable'; @@ -83,9 +89,31 @@ const WelcomeContainer = styled.div` } `; +const WelcomeNav = styled.div` + height: 50px; + background-color: white; + margin-top: ${({ theme }) => theme.gridUnit * -4 - 1}px; + .navbar-brand { + margin-left: ${({ theme }) => theme.gridUnit * 2}px; + font-weight: ${({ theme }) => theme.typography.weights.bold}; + } + .switch { + float: right; + margin: ${({ theme }) => theme.gridUnit * 5}px; + display: flex; + flex-direction: row; + span { + display: block; + margin: ${({ theme }) => theme.gridUnit * 1}px; + line-height: 1; + } + } +`; + function Welcome({ user, addDangerToast }: WelcomeProps) { const recent = `/superset/recent_activity/${user.userId}/?limit=6`; const [activeChild, setActiveChild] = useState('Viewed'); + const [checked, setChecked] = useState(true); const [activityData, setActivityData] = useState(null); const [chartData, setChartData] = useState | null>(null); const [queryData, setQueryData] = useState | null>(null); @@ -93,7 +121,12 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { null, ); + const userid = user.userId; + const id = userid.toString(); + useEffect(() => { + const userKey = getFromLocalStorage(id, null); + if (userKey && !userKey.thumbnails) setChecked(false); getRecentAcitivtyObjs(user.userId, recent, addDangerToast) .then(res => { const data: ActivityData | null = {}; @@ -117,7 +150,6 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { ); // Sets other activity data in parallel with recents api call - const id = user.userId; getUserOwnedObjects(id, 'dashboard') .then(r => { setDashboardData(r); @@ -148,6 +180,11 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { }); }, []); + const handleToggle = () => { + setChecked(!checked); + setInLocalStorage(id, { thumbnails: !checked }); + }; + useEffect(() => { setActivityData(activityData => ({ ...activityData, @@ -161,6 +198,15 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { return ( + + Home + {isFeatureEnabled(FeatureFlag.THUMBNAILS) ? ( +
+ + Thumbnails +
+ ) : null} +
{activityData && (activityData.Viewed || activityData.Examples) ? ( @@ -178,21 +224,36 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { {!dashboardData ? ( ) : ( - + )} {!queryData ? ( ) : ( - + )} {!chartData ? ( ) : ( - + )}