You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by di...@apache.org on 2022/08/22 13:17:56 UTC

[superset] 07/36: feat: add extension point for workspace home page (#21033)

This is an automated email from the ASF dual-hosted git repository.

diegopucci pushed a commit to branch chore/drill-to-detail-modal-tests
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 3c793b291294274b9f1ad1d48053062212c16a28
Author: Moriah Kreeger <mo...@gmail.com>
AuthorDate: Mon Aug 15 12:16:40 2022 -0700

    feat: add extension point for workspace home page (#21033)
    
    * updates to allow insertion of workspace home sidescroll/table UI
    
    * fix types
    
    * fix User type import
    
    * add welcome message to ui registry
    
    * add extra fields to individual chart/query GET results (for workspace home required info)
    
    * update list view card to support a subtitle
    
    * add id to individual chart fetch
    
    * update chart api test
    
    * another test fix
    
    * fix saved query test
    
    * update extension types + insert point
    
    * fix typing
    
    * fix type name
---
 .../src/ui-overrides/ExtensionsRegistry.ts         |   2 +
 .../src/components/ListViewCard/index.tsx          |  49 ++++---
 superset-frontend/src/components/Radio/index.tsx   |   1 +
 .../src/views/CRUD/welcome/Welcome.tsx             | 143 ++++++++++++---------
 superset/charts/api.py                             |   4 +
 superset/queries/saved_queries/api.py              |   1 +
 tests/integration_tests/charts/api_tests.py        |  15 ++-
 .../queries/saved_queries/api_tests.py             |   4 +-
 8 files changed, 134 insertions(+), 85 deletions(-)

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 bf050a2c4c..82e68efcf0 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 3078db8ea4..ac38a06ad9 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<LinkProps> = ({ to, children }) => (
 
 interface CardProps {
   title?: React.ReactNode;
+  subtitle?: React.ReactNode;
   url?: string;
   linkComponent?: React.ComponentType<LinkProps>;
   imgURL?: string;
@@ -161,6 +168,7 @@ interface CardProps {
 
 function ListViewCard({
   title,
+  subtitle,
   url,
   linkComponent,
   titleRight,
@@ -245,24 +253,27 @@ function ListViewCard({
         <AntdCard.Meta
           title={
             <TitleContainer>
-              <Tooltip title={title}>
-                <TitleLink>
-                  <Link to={url!}>
-                    {certifiedBy && (
-                      <>
-                        <CertifiedBadge
-                          certifiedBy={certifiedBy}
-                          details={certificationDetails}
-                        />{' '}
-                      </>
-                    )}
-                    {title}
-                  </Link>
-                </TitleLink>
-              </Tooltip>
-              {titleRight && <TitleRight>{titleRight}</TitleRight>}
-              <div className="card-actions" data-test="card-actions">
-                {actions}
+              {subtitle || null}
+              <div className="titleRow">
+                <Tooltip title={title}>
+                  <TitleLink>
+                    <Link to={url!}>
+                      {certifiedBy && (
+                        <>
+                          <CertifiedBadge
+                            certifiedBy={certifiedBy}
+                            details={certificationDetails}
+                          />{' '}
+                        </>
+                      )}
+                      {title}
+                    </Link>
+                  </TitleLink>
+                </Tooltip>
+                {titleRight && <TitleRight>{titleRight}</TitleRight>}
+                <div className="card-actions" data-test="card-actions">
+                  {actions}
+                </div>
               </div>
             </TitleContainer>
           }
diff --git a/superset-frontend/src/components/Radio/index.tsx b/superset-frontend/src/components/Radio/index.tsx
index 9ab656e4aa..f06392d278 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 8f403b999b..ddbd0b26d3 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 (
     <WelcomeContainer>
+      {WelcomeMessageExtension && <WelcomeMessageExtension />}
       {WelcomeTopExtension && <WelcomeTopExtension />}
-      <WelcomeNav>
-        <h1 className="welcome-header">Home</h1>
-        {isFeatureEnabled(FeatureFlag.THUMBNAILS) ? (
-          <div className="switch">
-            <AntdSwitch checked={checked} onChange={handleToggle} />
-            <span>Thumbnails</span>
-          </div>
-        ) : null}
-      </WelcomeNav>
-      <Collapse activeKey={activeState} onChange={handleCollapse} ghost bigger>
-        <Collapse.Panel header={t('Recents')} key="1">
-          {activityData &&
-          (activityData.Viewed ||
-            activityData.Examples ||
-            activityData.Created) &&
-          activeChild !== 'Loading' ? (
-            <ActivityTable
-              user={{ userId: user.userId! }} // user is definitely not a guest user on this page
-              activeChild={activeChild}
-              setActiveChild={setActiveChild}
-              activityData={activityData}
-              loadedCount={loadedCount}
-            />
-          ) : (
-            <LoadingCards />
-          )}
-        </Collapse.Panel>
-        <Collapse.Panel header={t('Dashboards')} key="2">
-          {!dashboardData || isRecentActivityLoading ? (
-            <LoadingCards cover={checked} />
-          ) : (
-            <DashboardTable
-              user={user}
-              mine={dashboardData}
-              showThumbnails={checked}
-              examples={activityData?.Examples}
-            />
-          )}
-        </Collapse.Panel>
-        <Collapse.Panel header={t('Charts')} key="3">
-          {!chartData || isRecentActivityLoading ? (
-            <LoadingCards cover={checked} />
-          ) : (
-            <ChartTable
-              showThumbnails={checked}
-              user={user}
-              mine={chartData}
-              examples={activityData?.Examples}
-            />
-          )}
-        </Collapse.Panel>
-        <Collapse.Panel header={t('Saved queries')} key="4">
-          {!queryData ? (
-            <LoadingCards cover={checked} />
-          ) : (
-            <SavedQueries
-              showThumbnails={checked}
-              user={user}
-              mine={queryData}
-              featureFlag={isFeatureEnabled(FeatureFlag.THUMBNAILS)}
-            />
-          )}
-        </Collapse.Panel>
-      </Collapse>
+      {WelcomeMainExtension && <WelcomeMainExtension />}
+      {(!WelcomeTopExtension || !WelcomeMainExtension) && (
+        <>
+          <WelcomeNav>
+            <h1 className="welcome-header">Home</h1>
+            {isFeatureEnabled(FeatureFlag.THUMBNAILS) ? (
+              <div className="switch">
+                <AntdSwitch checked={checked} onChange={handleToggle} />
+                <span>Thumbnails</span>
+              </div>
+            ) : null}
+          </WelcomeNav>
+          <Collapse
+            activeKey={activeState}
+            onChange={handleCollapse}
+            ghost
+            bigger
+          >
+            <Collapse.Panel header={t('Recents')} key="1">
+              {activityData &&
+              (activityData.Viewed ||
+                activityData.Examples ||
+                activityData.Created) &&
+              activeChild !== 'Loading' ? (
+                <ActivityTable
+                  user={{ userId: user.userId! }} // user is definitely not a guest user on this page
+                  activeChild={activeChild}
+                  setActiveChild={setActiveChild}
+                  activityData={activityData}
+                  loadedCount={loadedCount}
+                />
+              ) : (
+                <LoadingCards />
+              )}
+            </Collapse.Panel>
+            <Collapse.Panel header={t('Dashboards')} key="2">
+              {!dashboardData || isRecentActivityLoading ? (
+                <LoadingCards cover={checked} />
+              ) : (
+                <DashboardTable
+                  user={user}
+                  mine={dashboardData}
+                  showThumbnails={checked}
+                  examples={activityData?.Examples}
+                />
+              )}
+            </Collapse.Panel>
+            <Collapse.Panel header={t('Charts')} key="3">
+              {!chartData || isRecentActivityLoading ? (
+                <LoadingCards cover={checked} />
+              ) : (
+                <ChartTable
+                  showThumbnails={checked}
+                  user={user}
+                  mine={chartData}
+                  examples={activityData?.Examples}
+                />
+              )}
+            </Collapse.Panel>
+            <Collapse.Panel header={t('Saved queries')} key="4">
+              {!queryData ? (
+                <LoadingCards cover={checked} />
+              ) : (
+                <SavedQueries
+                  showThumbnails={checked}
+                  user={user}
+                  mine={queryData}
+                  featureFlag={isFeatureEnabled(FeatureFlag.THUMBNAILS)}
+                />
+              )}
+            </Collapse.Panel>
+          </Collapse>
+        </>
+      )}
     </WelcomeContainer>
   );
 }
diff --git a/superset/charts/api.py b/superset/charts/api.py
index e7e511d4fd..09cc7352bf 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -114,16 +114,20 @@ class ChartRestApi(BaseSupersetModelRestApi):
         "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 a82a3dd8ef..1f2088d759 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 aabe0f719a..c3546f32b9 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 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
             "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 2659bc224f..2569e7af40 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 @@ class TestSavedQueryApi(SupersetTestCase):
             "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):
         """