diff --git a/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts b/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts index bf050a2c4c49..82e68efcf0e9 100644 --- a/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts +++ b/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts @@ -45,7 +45,9 @@ export type Extensions = Partial<{ 'embedded.documentation.url': string; 'dashboard.nav.right': React.ComponentType; 'navbar.right': React.ComponentType; + 'welcome.message': React.ComponentType; 'welcome.banner': React.ComponentType; + 'welcome.main.replacement': React.ComponentType; }>; /** diff --git a/superset-frontend/src/components/ListViewCard/index.tsx b/superset-frontend/src/components/ListViewCard/index.tsx index 3078db8ea4ba..ac38a06ad9f0 100644 --- a/superset-frontend/src/components/ListViewCard/index.tsx +++ b/superset-frontend/src/components/ListViewCard/index.tsx @@ -71,7 +71,7 @@ const Cover = styled.div` const TitleContainer = styled.div` display: flex; justify-content: flex-start; - flex-direction: row; + flex-direction: column; .card-actions { margin-left: auto; @@ -82,6 +82,12 @@ const TitleContainer = styled.div` align-items: center; } } + + .titleRow { + display: flex; + justify-content: flex-start; + flex-direction: row; + } `; const TitleLink = styled.span` @@ -141,6 +147,7 @@ const AnchorLink: React.FC = ({ to, children }) => ( interface CardProps { title?: React.ReactNode; + subtitle?: React.ReactNode; url?: string; linkComponent?: React.ComponentType; imgURL?: string; @@ -161,6 +168,7 @@ interface CardProps { function ListViewCard({ title, + subtitle, url, linkComponent, titleRight, @@ -245,24 +253,27 @@ function ListViewCard({ - - - - {certifiedBy && ( - <> - {' '} - - )} - {title} - - - - {titleRight && {titleRight}} -
- {actions} + {subtitle || null} +
+ + + + {certifiedBy && ( + <> + {' '} + + )} + {title} + + + + {titleRight && {titleRight}} +
+ {actions} +
} diff --git a/superset-frontend/src/components/Radio/index.tsx b/superset-frontend/src/components/Radio/index.tsx index 9ab656e4aa80..f06392d27879 100644 --- a/superset-frontend/src/components/Radio/index.tsx +++ b/superset-frontend/src/components/Radio/index.tsx @@ -57,4 +57,5 @@ const StyledGroup = styled(AntdRadio.Group)` export const Radio = Object.assign(StyledRadio, { Group: StyledGroup, + Button: AntdRadio.Button, }); diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx index 8f403b999bf7..ddbd0b26d382 100644 --- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx +++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx @@ -179,7 +179,11 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { setItem(LocalStorageKeys.homepage_collapse_state, state); }; + const WelcomeMessageExtension = extensionsRegistry.get('welcome.message'); const WelcomeTopExtension = extensionsRegistry.get('welcome.banner'); + const WelcomeMainExtension = extensionsRegistry.get( + 'welcome.main.replacement', + ); useEffect(() => { const activeTab = getItem(LocalStorageKeys.homepage_activity_filter, null); @@ -282,71 +286,82 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { !activityData?.Examples && !activityData?.Viewed; return ( + {WelcomeMessageExtension && } {WelcomeTopExtension && } - -

Home

- {isFeatureEnabled(FeatureFlag.THUMBNAILS) ? ( -
- - Thumbnails -
- ) : null} -
- - - {activityData && - (activityData.Viewed || - activityData.Examples || - activityData.Created) && - activeChild !== 'Loading' ? ( - - ) : ( - - )} - - - {!dashboardData || isRecentActivityLoading ? ( - - ) : ( - - )} - - - {!chartData || isRecentActivityLoading ? ( - - ) : ( - - )} - - - {!queryData ? ( - - ) : ( - - )} - - + {WelcomeMainExtension && } + {(!WelcomeTopExtension || !WelcomeMainExtension) && ( + <> + +

Home

+ {isFeatureEnabled(FeatureFlag.THUMBNAILS) ? ( +
+ + Thumbnails +
+ ) : null} +
+ + + {activityData && + (activityData.Viewed || + activityData.Examples || + activityData.Created) && + activeChild !== 'Loading' ? ( + + ) : ( + + )} + + + {!dashboardData || isRecentActivityLoading ? ( + + ) : ( + + )} + + + {!chartData || isRecentActivityLoading ? ( + + ) : ( + + )} + + + {!queryData ? ( + + ) : ( + + )} + + + + )}
); } diff --git a/superset/charts/api.py b/superset/charts/api.py index e7e511d4fdbd..09cc7352bffd 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -114,16 +114,20 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]: "cache_timeout", "certified_by", "certification_details", + "changed_on_delta_humanized", "dashboards.dashboard_title", "dashboards.id", "dashboards.json_metadata", "description", + "id", "owners.first_name", "owners.id", "owners.last_name", "owners.username", "params", "slice_name", + "thumbnail_url", + "url", "viz_type", "query_context", "is_managed_externally", diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py index a82a3dd8efcf..1f2088d7598a 100644 --- a/superset/queries/saved_queries/api.py +++ b/superset/queries/saved_queries/api.py @@ -81,6 +81,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi): base_filters = [["id", SavedQueryFilter, lambda: []]] show_columns = [ + "changed_on_delta_humanized", "created_by.first_name", "created_by.id", "created_by.last_name", diff --git a/tests/integration_tests/charts/api_tests.py b/tests/integration_tests/charts/api_tests.py index aabe0f719a6b..c3546f32b966 100644 --- a/tests/integration_tests/charts/api_tests.py +++ b/tests/integration_tests/charts/api_tests.py @@ -17,6 +17,7 @@ # isort:skip_file """Unit tests for Superset""" import json +import logging from io import BytesIO from zipfile import is_zipfile, ZipFile @@ -762,7 +763,19 @@ def test_get_chart(self): "is_managed_externally": False, } data = json.loads(rv.data.decode("utf-8")) - self.assertEqual(data["result"], expected_result) + self.assertIn("changed_on_delta_humanized", data["result"]) + self.assertIn("id", data["result"]) + self.assertIn("thumbnail_url", data["result"]) + self.assertIn("url", data["result"]) + for key, value in data["result"].items(): + # We can't assert timestamp values or id/urls + if key not in ( + "changed_on_delta_humanized", + "id", + "thumbnail_url", + "url", + ): + self.assertEqual(value, expected_result[key]) db.session.delete(chart) db.session.commit() diff --git a/tests/integration_tests/queries/saved_queries/api_tests.py b/tests/integration_tests/queries/saved_queries/api_tests.py index 2659bc224ff3..2569e7af406e 100644 --- a/tests/integration_tests/queries/saved_queries/api_tests.py +++ b/tests/integration_tests/queries/saved_queries/api_tests.py @@ -525,8 +525,10 @@ def test_get_saved_query(self): "label": "label1", } data = json.loads(rv.data.decode("utf-8")) + self.assertIn("changed_on_delta_humanized", data["result"]) for key, value in data["result"].items(): - assert value == expected_result[key] + if key not in ("changed_on_delta_humanized",): + assert value == expected_result[key] def test_get_saved_query_not_found(self): """