Browse Source

[frontend] Hide import button for transactional hive tables (#2910)

* [frontend] Hide import button for transactional hive tables

Move logic from mako template and create unit tests

* [frontend] Hide import button for transactional hive tables using dialect
Bjorn Alm 3 years ago
parent
commit
5ab7cf3612

+ 1 - 1
apps/metastore/src/metastore/templates/metastore.mako

@@ -902,7 +902,7 @@ ${ components.menubar(is_embeddable) }
                         <a class="btn btn-default" data-bind="attr: { 'href': '/metastore/table/'+ catalogEntry.path.join('/') + '/read' }" title="${_('Browse Data')}"><i class="fa fa-play fa-fw"></i> ${_('Browse Data')}</a>
                       % endif
                       % if has_write_access:
-                        <a href="javascript: void(0);" class="btn btn-default" data-bind="click: showImportData, visible: $root.source().type !== 'sparksql' && tableDetails() && !catalogEntry.isView()" title="${_('Import Data')}"><i class="fa fa-upload fa-fw"></i> ${_('Import')}</a>
+                        <a href="javascript: void(0);" class="btn btn-default" data-bind="click: showImportData, visible: enableImport($root.source().type)" title="${_('Import Data')}"><i class="fa fa-upload fa-fw"></i> ${_('Import')}</a>
                       % endif
                       % if has_write_access:
                         <a href="#dropSingleTable" data-toggle="modal" class="btn btn-default" data-bind="attr: { 'title' : tableDetails() && catalogEntry.isView() ? '${_('Drop View')}' : '${_('Drop Table')}' }"><i class="fa fa-times fa-fw"></i> ${_('Drop')}</a>

+ 14 - 0
desktop/core/src/desktop/js/apps/tableBrowser/metastoreTable.js

@@ -28,6 +28,9 @@ import I18n from 'utils/i18n';
 
 let contextPopoverTimeout = -1;
 
+export const DIALECT_HIVE = 'hive';
+export const DIALECT_SPARK = 'spark sql';
+
 class MetastoreTable {
   /**
    * @param {Object} options
@@ -391,6 +394,17 @@ class MetastoreTable {
     this.catalogEntry.clearCache();
   }
 
+  enableImport() {
+    const detailsLoaded = !!this.tableDetails();
+    const dialect = this.catalogEntry.getDialect();
+    const isView = this.catalogEntry.isView();
+    const isTransactionalHive =
+      dialect === DIALECT_HIVE && this.catalogEntry.isTransactionalTable();
+    const isSpark = dialect === DIALECT_SPARK;
+
+    return detailsLoaded && !(isSpark || isView || isTransactionalHive);
+  }
+
   showImportData() {
     $('#import-data-modal')
       .empty()

+ 122 - 0
desktop/core/src/desktop/js/apps/tableBrowser/metastoreTable.test.js

@@ -0,0 +1,122 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { merge } from 'lodash';
+
+import { default as MetastoreTable, DIALECT_HIVE, DIALECT_SPARK } from './metastoreTable';
+
+describe('metastoreTable.js', () => {
+  const generateOptions = customOptions => {
+    const defaultOptions = {
+      catalogEntry: {
+        hasResolvedComment: () => false,
+        isModel: () => false,
+        isTransactionalTable: () => false,
+        isView: () => false,
+        getDialect: () => DIALECT_HIVE,
+        getAnalysis: () =>
+          Promise.resolve({
+            details: {
+              stats: {}
+            },
+            partition_keys: []
+          }),
+        getComment: () => Promise.resolve(''),
+        getConnector: () => ({
+          buttonName: '',
+          displayName: '',
+          id: 'connectorA',
+          page: '',
+          tooltip: '',
+          type: ''
+        }),
+        getSample: () =>
+          Promise.resolve({
+            data: [],
+            meta: []
+          }),
+        getTopJoins: () => Promise.resolve({})
+      },
+      database: {
+        catalogEntry: {
+          loadNavigatorMetaForChildren: () => Promise.resolve({})
+        }
+      },
+      navigatorEnabled: false,
+      sqlAnalyzerEnabled: false
+    };
+
+    return merge({}, defaultOptions, customOptions);
+  };
+
+  it('should enable import when analysis is loaded', async () => {
+    const metastoreTable = new MetastoreTable(generateOptions({}));
+    await metastoreTable.fetchDetails();
+    expect(metastoreTable.enableImport()).toEqual(true);
+  });
+
+  it('should not enable import before there is an analysis loaded', async () => {
+    const resolve = () => ({
+      details: {
+        stats: {}
+      },
+      partition_keys: []
+    });
+    const unresolvedPromise = new Promise(resolve, () => {});
+    const metastoreTable = new MetastoreTable(
+      generateOptions({ catalogEntry: { getAnalysis: () => unresolvedPromise } })
+    );
+
+    await metastoreTable.fetchDetails();
+    expect(metastoreTable.enableImport()).toEqual(false);
+  });
+
+  it('should not enable import when the table is a view', async () => {
+    const metastoreTable = new MetastoreTable(
+      generateOptions({ catalogEntry: { isView: () => true } })
+    );
+    await metastoreTable.fetchDetails();
+    expect(metastoreTable.enableImport()).toEqual(false);
+  });
+
+  it('should not enable import when the table is transactional and dialect is Hive', async () => {
+    const metastoreTransactionalHiveTable = new MetastoreTable(
+      generateOptions({ catalogEntry: { isTransactionalTable: () => true } })
+    );
+    await metastoreTransactionalHiveTable.fetchDetails();
+    expect(metastoreTransactionalHiveTable.enableImport()).toEqual(false);
+
+    const metastoreTransactionalTable = new MetastoreTable(
+      generateOptions({
+        catalogEntry: { isTransactionalTable: () => true, getDialect: () => 'sql' }
+      })
+    );
+    await metastoreTransactionalTable.fetchDetails();
+    expect(metastoreTransactionalTable.enableImport()).toEqual(true);
+
+    const metastoreHiveTable = new MetastoreTable(generateOptions());
+    await metastoreHiveTable.fetchDetails();
+    expect(metastoreHiveTable.enableImport()).toEqual(true);
+  });
+
+  it('should not enable import when the dialect is spark sql', async () => {
+    const metastoreTable = new MetastoreTable(
+      generateOptions({ catalogEntry: { getDialect: () => DIALECT_SPARK } })
+    );
+    await metastoreTable.fetchDetails();
+    expect(metastoreTable.enableImport()).toEqual(false);
+  });
+});

+ 4 - 0
desktop/core/src/desktop/js/catalog/DataCatalogEntry.ts

@@ -1426,6 +1426,10 @@ export default class DataCatalogEntry {
     return this.analysis?.details?.stats?.table_type === 'ICEBERG';
   }
 
+  isTransactionalTable(): boolean {
+    return this.analysis?.details?.stats?.transactional === 'true';
+  }
+
   /**
    * Returns true if the entry is a view. It will be accurate once the source meta has been loaded.
    */

+ 46 - 21
desktop/core/src/desktop/js/catalog/dataCatalogEntry.test.ts

@@ -14,6 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import { merge } from 'lodash';
+
 import { CancellablePromise } from 'api/cancellablePromise';
 import dataCatalog from 'catalog/dataCatalog';
 import DataCatalogEntry, { Sample, TableAnalysis } from 'catalog/DataCatalogEntry';
@@ -61,27 +63,30 @@ 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: '/filebrowser/view=/warehouse/tablespace/managed/hive/sample_07',
-          is_view: false,
-          properties: [],
-          details: {
-            stats: {
-              table_type: 'ICEBERG'
-            },
-            properties: {}
-          },
-          stats: [],
-          primary_keys: []
-        })
-      );
+    const getAnalysisObj = () => ({
+      message: '',
+      name: 'iceberg-table-test',
+      partition_keys: [],
+      cols: [{ name: 'i', type: 'int', comment: '' }],
+      path_location: 'test',
+      hdfs_link: '/filebrowser/view=/warehouse/tablespace/managed/hive/sample_07',
+      is_view: false,
+      properties: [],
+      details: {
+        stats: {
+          table_type: 'ICEBERG'
+        },
+        properties: {}
+      },
+      stats: [],
+      primary_keys: []
+    });
+
+    const emptyAnalysisApiSpy = (additionalAnalysis: Record<string, unknown> = {}) => {
+      const customAnalysis = merge({}, getAnalysisObj(), additionalAnalysis);
+      jest
+        .spyOn(CatalogApi, 'fetchDescribe')
+        .mockReturnValue(CancellablePromise.resolve<TableAnalysis>(customAnalysis));
     };
 
     it('should return true for isIcebergTable after the analysis has has been loaded', async () => {
@@ -94,6 +99,26 @@ describe('dataCatalogEntry.ts', () => {
       expect(entry.isIcebergTable()).toBeTruthy();
     });
 
+    it('returns true for isTransactionalTable when details.stats.transactional="true"', async () => {
+      emptyAnalysisApiSpy({ details: { stats: { transactional: 'true' } } });
+      const entry = await getEntry('someDb.someTransactionalTable');
+
+      expect(entry.isTransactionalTable()).toBeFalsy();
+
+      await entry.getAnalysis();
+      expect(entry.isTransactionalTable()).toBeTruthy();
+    });
+
+    it('returns false for isTransactionalTable when there is no details.stats.transactional defined', async () => {
+      emptyAnalysisApiSpy();
+      const entry = await getEntry('someDb.someTransactionalTable');
+
+      expect(entry.isTransactionalTable()).toBeFalsy();
+
+      await entry.getAnalysis();
+      expect(entry.isTransactionalTable()).toBeFalsy();
+    });
+
     it('should return the hdfs path based on the hdfs_link', async () => {
       emptyAnalysisApiSpy();
       const entry = await getEntry('someDb.someTable');