Browse Source

[frontend] Add iceberg icon to tables in left assist popover

Info about iceberg is at the time only available when calling describe per table so the iceberg icon is not showing in the initial table list.
Björn Alm 3 years ago
parent
commit
6242c23e5c

+ 8 - 1
desktop/core/src/desktop/js/catalog/DataCatalogEntry.ts

@@ -180,7 +180,7 @@ export interface NavigatorMeta extends TimestampedData {
 }
 
 export interface TableAnalysis extends TimestampedData {
-  cols: { comment?: string | null; type: string; name: string };
+  cols: { comment?: string | null; type: string; name: string }[];
   comment?: string | null;
   details: {
     properties: { [propertyKey: string]: string };
@@ -1414,6 +1414,13 @@ export default class DataCatalogEntry {
     return false;
   }
 
+  /**
+   * Returns true if the entry is an Iceberg table
+   */
+  isIcebergTable(): boolean {
+    return this.analysis?.details?.stats?.table_type === 'ICEBERG';
+  }
+
   /**
    * Returns true if the entry is a view. It will be accurate once the source meta has been loaded.
    */

+ 90 - 1
desktop/core/src/desktop/js/catalog/dataCatalogEntry.test.ts

@@ -16,7 +16,7 @@
 
 import { CancellablePromise } from 'api/cancellablePromise';
 import dataCatalog from 'catalog/dataCatalog';
-import DataCatalogEntry, { Sample } from 'catalog/DataCatalogEntry';
+import DataCatalogEntry, { Sample, TableAnalysis } from 'catalog/DataCatalogEntry';
 import { Compute, Connector, Namespace } from 'config/types';
 import * as CatalogApi from './api';
 
@@ -60,6 +60,95 @@ describe('dataCatalogEntry.ts', () => {
 
   afterAll(clearStorage);
 
+  describe('getAnalysis', () => {
+    const emptyAnalysisApiSpy = () => {
+      jest.spyOn(CatalogApi, 'fetchDescribe').mockReturnValue(
+        CancellablePromise.resolve<TableAnalysis>({
+          message: '',
+          name: 'iceberg-table-test',
+          partition_keys: [],
+          cols: [{ name: 'i', type: 'int', comment: '' }],
+          path_location: 'test',
+          hdfs_link: '/test',
+          is_view: false,
+          properties: [],
+          details: {
+            stats: {
+              table_type: 'ICEBERG'
+            },
+            properties: {}
+          },
+          stats: [],
+          primary_keys: []
+        })
+      );
+    };
+
+    it('should return true for isIcebergTable after the analysis has has been loaded', async () => {
+      emptyAnalysisApiSpy();
+      const entry = await getEntry('someDb.someIcebergTable');
+
+      expect(entry.isIcebergTable()).toBeFalsy();
+
+      await entry.getAnalysis();
+      expect(entry.isIcebergTable()).toBeTruthy();
+    });
+
+    it('rejects a cachedOnly request if there is no previous promise', async () => {
+      emptyAnalysisApiSpy();
+      const entryA = await getEntry('someDb.someTable');
+      let rejected = false;
+      await entryA.getAnalysis({ cachedOnly: true }).catch(() => {
+        rejected = true;
+      });
+
+      expect(rejected).toBeTruthy();
+    });
+
+    it('should return the same analysis promise for the same entry', async () => {
+      emptyAnalysisApiSpy();
+      const entryA = await getEntry('someDb.someTable');
+      const entryB = await getEntry('someDb.someTable');
+      expect(entryA.getAnalysis()).toEqual(entryB.getAnalysis());
+    });
+
+    it('should not return the same analysis promise for different entries', async () => {
+      emptyAnalysisApiSpy();
+      const entryA = await getEntry('someDb.someTableOne');
+      const entryB = await getEntry('someDb.someTableTwo');
+      expect(entryA.getAnalysis()).not.toEqual(entryB.getAnalysis());
+    });
+
+    it('should keep the analysis promise for future session use', async () => {
+      emptyAnalysisApiSpy();
+      const entryA = await getEntry('someDb.someTable');
+      await entryA.clearCache();
+      const analysisPromise = entryA.getAnalysis();
+      expect(entryA.analysisPromise).toEqual(analysisPromise);
+      const entryB = await getEntry('someDb.someTable');
+      expect(entryB.analysisPromise).toEqual(analysisPromise);
+    });
+
+    it('should not cancel when cancellable option is not set to true', async () => {
+      emptyAnalysisApiSpy();
+      const entryA = await getEntry('someDb.someTable');
+      const analysisPromise = entryA.getAnalysis({ cancellable: false });
+      await analysisPromise.cancel();
+      expect(analysisPromise.cancelled).toBeFalsy();
+      expect(entryA.analysisPromise).toEqual(analysisPromise);
+    });
+
+    it('should not return a cancelled analysis promise', async () => {
+      emptyAnalysisApiSpy();
+      const entryA = await getEntry('someDb.someTable');
+      const cancelledPromise = entryA.getAnalysis({ cancellable: true });
+      await cancelledPromise.cancel();
+      const newPromise = entryA.getAnalysis();
+      expect(cancelledPromise.cancelled).toBeTruthy();
+      expect(newPromise).not.toEqual(cancelledPromise);
+    });
+  });
+
   describe('getSample', () => {
     const emptySampleApiSpy = () => {
       jest.spyOn(CatalogApi, 'fetchSample').mockReturnValue(

+ 1 - 3
desktop/core/src/desktop/js/ko/components/contextPopover/dataCatalogContext.js

@@ -124,9 +124,6 @@ class DataCatalogContext {
         .catch(() => {
           self.hasErrors(true);
         })
-        .finally(() => {
-          self.loading(false);
-        })
     );
 
     // TODO: Use connector attributes in dataCatalogContext
@@ -177,6 +174,7 @@ class DataCatalogContext {
     );
 
     $.when.apply($, self.activePromises).always(() => {
+      self.loading(false);
       self.activePromises.length = 0;
     });
   }

+ 25 - 1
desktop/core/src/desktop/js/ko/components/contextPopover/ko.contextPopover.js

@@ -276,7 +276,31 @@ const SUPPORT_TEMPLATES = `
 
   <script type="text/html" id="context-catalog-entry-title">
     <div class="hue-popover-title">
-      <i class="hue-popover-title-icon fa muted" data-bind="css: catalogEntry() && (catalogEntry().isView() || parentIsView()) ? 'fa-eye' : (catalogEntry().isDatabase() ? 'fa-database' : (catalogEntry().isModel() ? 'fa-puzzle-piece' : 'fa-table'))"></i>
+      <!-- ko if: catalogEntry().isTable() -->
+        <span class="hue-popover-title-secondary-icon-container">
+          <!-- ko hueSpinner: { spin: loading, inline: true } --><!-- /ko -->
+          <!-- ko ifnot: loading -->  
+            <!-- ko if:catalogEntry().isIcebergTable() -->  
+              <i class="hue-popover-title-icon fa muted fa-snowflake-o"  title="${I18n(
+                'Iceberg table'
+              )}"></i>
+            <!-- /ko -->
+            <!-- ko ifnot:catalogEntry().isIcebergTable() -->  
+              <i class="hue-popover-title-icon fa muted fa-table"></i>
+            <!-- /ko -->            
+          <!-- /ko -->
+        </span>
+      <!-- /ko -->
+      <!-- ko ifnot: catalogEntry().isTable() -->
+        <i class="hue-popover-title-icon fa muted" data-bind="css: 
+        catalogEntry() && (catalogEntry().isView() || parentIsView()) 
+        ? 'fa-eye' 
+        : (catalogEntry().isDatabase() 
+        ? 'fa-database' 
+        : (catalogEntry().isModel() 
+        ? 'fa-puzzle-piece' 
+        : 'fa-table'))"></i>      
+      <!-- /ko -->
       <span class="hue-popover-title-text" data-bind="foreach: breadCrumbs">
         <!-- ko ifnot: isActive --><div><a href="javascript: void(0);" data-bind="click: makeActive, text: name"></a>.</div><!-- /ko -->
         <!-- ko if: isActive -->

File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue3-extra.css


+ 14 - 4
desktop/core/src/desktop/static/desktop/less/components/hue-popover.less

@@ -52,7 +52,7 @@
 .hue-popover.hue-popover-top .hue-popover-arrow::after {
   bottom: 1px;
   margin-left: -3px;
-  content: "";
+  content: '';
   border-top-color: @hue-primary-color-dark;
   border-bottom-width: 0;
 }
@@ -72,7 +72,7 @@
 .hue-popover.hue-popover-right .hue-popover-arrow::after {
   bottom: -3px;
   left: 1px;
-  content: "";
+  content: '';
   border-right-color: @hue-primary-color-dark;
   border-left-width: 0;
 }
@@ -92,7 +92,7 @@
 .hue-popover.hue-popover-bottom .hue-popover-arrow::after {
   top: 3px;
   margin-left: -3px;
-  content: "";
+  content: '';
   border-top-width: 0;
   border-bottom-color: @hue-primary-color-dark;
 }
@@ -112,7 +112,7 @@
 .hue-popover.hue-popover-left .hue-popover-arrow::after {
   right: 2px;
   bottom: -3px;
-  content: "";
+  content: '';
   border-right-width: 0;
   border-left-color: @hue-primary-color-dark;
 }
@@ -131,6 +131,16 @@
     margin-top: 3px;
   }
 
+  .hue-popover-title-secondary-icon-container {
+    width: 20px;
+    display: inline-block;
+    margin-top: 3px;
+
+    .hue-popover-title-icon {
+      margin-top: 0;
+    }
+  }
+
   .hue-popover-title-text {
     padding-left: 4px;
     font-size: 0;

Some files were not shown because too many files changed in this diff