Эх сурвалжийг харах

[editor] First version of the execution result grid component

Johan Ahlen 4 жил өмнө
parent
commit
a0848bfe0e
20 өөрчлөгдсөн 556 нэмэгдсэн , 122 устгасан
  1. 4 4
      desktop/core/src/desktop/js/apps/notebook2/components/ExecutableActions.test.ts
  2. 6 6
      desktop/core/src/desktop/js/apps/notebook2/components/ExecutableActions.vue
  3. 6 6
      desktop/core/src/desktop/js/apps/notebook2/components/ko.executableProgressBar.js
  4. 6 6
      desktop/core/src/desktop/js/apps/notebook2/components/ko.executableProgressBar.test.js
  5. 5 5
      desktop/core/src/desktop/js/apps/notebook2/components/ko.queryHistory.js
  6. 5 18
      desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetResults.js
  7. 138 0
      desktop/core/src/desktop/js/apps/notebook2/components/result/ExecutionResults.vue
  8. 62 0
      desktop/core/src/desktop/js/apps/notebook2/components/result/ExecutionResultsKoBridge.vue
  9. 53 0
      desktop/core/src/desktop/js/apps/notebook2/components/result/ResultGrid.vue
  10. 2 2
      desktop/core/src/desktop/js/apps/notebook2/components/resultGrid/ko.simpleResultGrid.js
  11. 7 6
      desktop/core/src/desktop/js/apps/notebook2/execution/api.ts
  12. 39 39
      desktop/core/src/desktop/js/apps/notebook2/execution/executable.ts
  13. 5 5
      desktop/core/src/desktop/js/apps/notebook2/execution/executionLogs.ts
  14. 11 5
      desktop/core/src/desktop/js/apps/notebook2/execution/executionResult.ts
  15. 10 10
      desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.test.ts
  16. 6 5
      desktop/core/src/desktop/js/apps/notebook2/snippet.js
  17. 4 3
      desktop/core/src/desktop/js/components/HueTable.d.ts
  18. 21 1
      desktop/core/src/desktop/js/components/HueTable.test.ts
  19. 3 1
      desktop/core/src/desktop/js/components/HueTable.vue
  20. 163 0
      desktop/core/src/desktop/js/components/__snapshots__/HueTable.test.ts.snap

+ 4 - 4
desktop/core/src/desktop/js/apps/notebook2/components/ExecutableActions.test.ts

@@ -17,7 +17,7 @@
 import huePubSub from 'utils/huePubSub';
 import Vue from 'vue';
 import { mount, shallowMount } from '@vue/test-utils';
-import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { EXECUTABLE_UPDATED_EVENT, ExecutionStatus } from 'apps/notebook2/execution/executable';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
 import ExecutableActions from './ExecutableActions.vue';
 
@@ -48,7 +48,7 @@ describe('ExecutableActions.vue', () => {
           id: 'foo'
         })
       },
-      status: EXECUTION_STATUS.ready
+      status: ExecutionStatus.ready
     };
 
     const wrapper = shallowMount(ExecutableActions, {
@@ -90,7 +90,7 @@ describe('ExecutableActions.vue', () => {
           type: 'foo'
         })
       },
-      status: EXECUTION_STATUS.ready
+      status: ExecutionStatus.ready
     };
 
     const wrapper = mount(ExecutableActions, {
@@ -111,7 +111,7 @@ describe('ExecutableActions.vue', () => {
     await Vue.nextTick();
 
     expect(executeCalled).toBeTruthy();
-    mockExecutable.status = EXECUTION_STATUS.running;
+    mockExecutable.status = ExecutionStatus.running;
     huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
 
     await Vue.nextTick();

+ 6 - 6
desktop/core/src/desktop/js/apps/notebook2/components/ExecutableActions.vue

@@ -82,7 +82,7 @@
   import { Prop, Watch } from 'vue-property-decorator';
 
   import { Session } from 'apps/notebook2/execution/api';
-  import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+  import { EXECUTABLE_UPDATED_EVENT, ExecutionStatus } from 'apps/notebook2/execution/executable';
   import sessionManager from 'apps/notebook2/execution/sessionManager';
 
   export const EXECUTE_ACTIVE_EXECUTABLE_EVENT = 'executable.active.executable';
@@ -105,7 +105,7 @@
     partOfRunningExecution = false;
     limit: number | null = null;
     stopping = false;
-    status: string = EXECUTION_STATUS.ready;
+    status: ExecutionStatus = ExecutionStatus.ready;
     hasStatement = false;
 
     mounted(): void {
@@ -139,15 +139,15 @@
         !!this.executable &&
         !this.waiting &&
         !this.loadingSession &&
-        this.status !== EXECUTION_STATUS.running &&
-        this.status !== EXECUTION_STATUS.streaming
+        this.status !== ExecutionStatus.running &&
+        this.status !== ExecutionStatus.streaming
       );
     }
 
     get showStop(): boolean {
       return (
-        this.status === EXECUTION_STATUS.running ||
-        this.status === EXECUTION_STATUS.streaming ||
+        this.status === ExecutionStatus.running ||
+        this.status === ExecutionStatus.streaming ||
         this.waiting
       );
     }

+ 6 - 6
desktop/core/src/desktop/js/apps/notebook2/components/ko.executableProgressBar.js

@@ -20,7 +20,7 @@ import * as ko from 'knockout';
 import 'ko/bindings/ko.publish';
 
 import componentUtils from 'ko/components/componentUtils';
-import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { EXECUTABLE_UPDATED_EVENT, ExecutionStatus } from 'apps/notebook2/execution/executable';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import { sleep } from 'utils/hueUtils';
 
@@ -41,14 +41,14 @@ class ExecutableProgressBar extends DisposableComponent {
     this.progress = ko.observable(0);
 
     this.progressClass = ko.pureComputed(() => {
-      if (this.status() === EXECUTION_STATUS.failed) {
+      if (this.status() === ExecutionStatus.failed) {
         return 'progress-danger';
       }
       if (
         this.progress() === 0 &&
-        (this.status() === EXECUTION_STATUS.running ||
-          this.status() === EXECUTION_STATUS.streaming ||
-          this.status() === EXECUTION_STATUS.starting)
+        (this.status() === ExecutionStatus.running ||
+          this.status() === ExecutionStatus.streaming ||
+          this.status() === ExecutionStatus.starting)
       ) {
         return 'progress-starting';
       }
@@ -61,7 +61,7 @@ class ExecutableProgressBar extends DisposableComponent {
     });
 
     this.progressWidth = ko.pureComputed(() => {
-      if (this.status() === EXECUTION_STATUS.failed) {
+      if (this.status() === ExecutionStatus.failed) {
         return '100%';
       }
       return Math.max(2, this.progress()) + '%';

+ 6 - 6
desktop/core/src/desktop/js/apps/notebook2/components/ko.executableProgressBar.test.js

@@ -17,14 +17,14 @@
 import huePubSub from 'utils/huePubSub';
 import { koSetup } from 'jest/koTestUtils';
 import { NAME } from './ko.executableProgressBar';
-import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { EXECUTABLE_UPDATED_EVENT, ExecutionStatus } from 'apps/notebook2/execution/executable';
 
 describe('ko.executableProgressBar.js', () => {
   const setup = koSetup();
 
   it('should render component', async () => {
     const mockExecutable = {
-      status: EXECUTION_STATUS.ready,
+      status: ExecutionStatus.ready,
       progress: 0
     };
     const activeExecutable = () => mockExecutable;
@@ -38,7 +38,7 @@ describe('ko.executableProgressBar.js', () => {
 
   it('should reflect progress updates', async () => {
     const mockExecutable = {
-      status: EXECUTION_STATUS.ready,
+      status: ExecutionStatus.ready,
       progress: 0
     };
     const activeExecutable = () => mockExecutable;
@@ -51,7 +51,7 @@ describe('ko.executableProgressBar.js', () => {
     // Progress should be 2% initially
     expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('2%');
 
-    mockExecutable.status = EXECUTION_STATUS.running;
+    mockExecutable.status = ExecutionStatus.running;
     mockExecutable.progress = 10;
     huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
     await setup.waitForKoUpdate();
@@ -61,7 +61,7 @@ describe('ko.executableProgressBar.js', () => {
 
   it('should be 100% and have .progress-danger when failed', async () => {
     const mockExecutable = {
-      status: EXECUTION_STATUS.ready,
+      status: ExecutionStatus.ready,
       progress: 0
     };
     const activeExecutable = () => mockExecutable;
@@ -73,7 +73,7 @@ describe('ko.executableProgressBar.js', () => {
     expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('2%');
     expect(wrapper.querySelector('[data-test="' + NAME + '"].progress-danger')).toBeFalsy();
 
-    mockExecutable.status = EXECUTION_STATUS.failed;
+    mockExecutable.status = ExecutionStatus.failed;
     mockExecutable.progress = 10;
     huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
     await setup.waitForKoUpdate();

+ 5 - 5
desktop/core/src/desktop/js/apps/notebook2/components/ko.queryHistory.js

@@ -21,7 +21,7 @@ import apiHelper from 'api/apiHelper';
 import componentUtils from 'ko/components/componentUtils';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import I18n from 'utils/i18n';
-import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { ExecutionStatus } from 'apps/notebook2/execution/executable';
 import { sleep } from 'utils/hueUtils';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
@@ -293,10 +293,10 @@ class QueryHistory extends DisposableComponent {
 
     this.loadingHistory(false);
     this.refreshStatus(
-      [EXECUTION_STATUS.starting, EXECUTION_STATUS.running, EXECUTION_STATUS.streaming],
+      [ExecutionStatus.starting, ExecutionStatus.running, ExecutionStatus.streaming],
       STARTING_RUNNING_INTERVAL
     );
-    this.refreshStatus([EXECUTION_STATUS.available], AVAILABLE_INTERVAL);
+    this.refreshStatus([ExecutionStatus.available], AVAILABLE_INTERVAL);
   }
 
   importHistory() {
@@ -329,9 +329,9 @@ class QueryHistory extends DisposableComponent {
           silenceErrors: true
         });
         if (response.status === -3) {
-          item.status(EXECUTION_STATUS.expired);
+          item.status(ExecutionStatus.expired);
         } else if (response.status !== 0) {
-          item.status(EXECUTION_STATUS.failed);
+          item.status(ExecutionStatus.failed);
         } else if (response.query_status.status) {
           item.status(response.query_status.status);
         }

+ 5 - 18
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetResults.js

@@ -25,7 +25,7 @@ import { RESULT_CHART_COMPONENT } from 'apps/notebook2/components/resultChart/ko
 import { RESULT_GRID_COMPONENT } from 'apps/notebook2/components/resultGrid/ko.resultGrid';
 import { REDRAW_FIXED_HEADERS_EVENT } from 'apps/notebook2/events';
 import { REDRAW_CHART_EVENT } from 'apps/notebook2/events';
-import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { EXECUTABLE_UPDATED_EVENT, ExecutionStatus } from 'apps/notebook2/execution/executable';
 import { RESULT_TYPE, RESULT_UPDATED_EVENT } from 'apps/notebook2/execution/executionResult';
 import { attachTracker } from 'apps/notebook2/components/executableStateHandler';
 import { defer } from 'utils/hueUtils';
@@ -71,22 +71,9 @@ const TEMPLATE = `
     </div>
     <div class="table-results" data-bind="visible: type() === 'table', css: { 'table-results-notebook': notebookMode }" style="display: none;">
       <div data-bind="visible: !executing() && hasData() && showGrid()" style="display: none; position: relative;">
-        <!-- ko component: {
-          name: '${ RESULT_GRID_COMPONENT }',
-          params: {
-            activeExecutable: activeExecutable,
-            data: data,
-            streaming: streaming,
-            lastFetchedRows: lastFetchedRows,
-            fetchResult: fetchResult.bind($data),
-            hasMore: hasMore,
-            notebookMode: notebookMode,
-            isResultFullScreenMode: isResultFullScreenMode,
-            meta: meta,
-            resultsKlass: resultsKlass,
-            status: status
-          }
-        } --><!-- /ko -->
+        <execution-results-ko-bridge data-bind="vueKoProps: {
+          executableObservable: activeExecutable
+        }"></execution-results-ko-bridge>
       </div>
       <div data-bind="visible: !executing() && hasData() && showChart()" style="display: none; position: relative;">
         <!-- ko component: {
@@ -158,7 +145,7 @@ class SnippetResults extends DisposableComponent {
       }
     });
 
-    this.executing = ko.pureComputed(() => this.status() === EXECUTION_STATUS.running);
+    this.executing = ko.pureComputed(() => this.status() === ExecutionStatus.running);
 
     this.hasData = ko.pureComputed(() => this.data().length);
 

+ 138 - 0
desktop/core/src/desktop/js/apps/notebook2/components/result/ExecutionResults.vue

@@ -0,0 +1,138 @@
+<!--
+  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.
+-->
+
+<template>
+  <ResultGrid :rows="rows" :meta="meta" />
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop, Watch } from 'vue-property-decorator';
+
+  import ResultGrid from './ResultGrid.vue';
+  import { ResultMeta } from 'apps/notebook2/execution/api';
+  import Executable, {
+    EXECUTABLE_UPDATED_EVENT,
+    ExecutionStatus
+  } from 'apps/notebook2/execution/executable';
+  import ExecutionResult, {
+    RESULT_UPDATED_EVENT,
+    ResultRow,
+    ResultType
+  } from 'apps/notebook2/execution/executionResult';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+
+  @Component({
+    components: { ResultGrid }
+  })
+  export default class ExecutionResults extends Vue {
+    @Prop()
+    executable?: Executable;
+
+    subTracker = new SubscriptionTracker();
+
+    status: ExecutionStatus | null = null;
+    type = ResultType.Table;
+
+    fetchedOnce = false;
+    hasResultSet = false;
+    streaming = false;
+    hasMore = false;
+
+    lastRenderedResult?: ExecutionResult;
+
+    images = [];
+    lastFetchedRows: ResultRow[] = [];
+    rows: ResultRow[] = [];
+    meta: ResultMeta[] = [];
+
+    mounted(): void {
+      this.subTracker.subscribe(EXECUTABLE_UPDATED_EVENT, (executable: Executable) => {
+        if (this.executable === executable) {
+          this.updateFromExecutable(executable);
+        }
+      });
+
+      this.subTracker.subscribe(RESULT_UPDATED_EVENT, (executionResult: ExecutionResult) => {
+        if (this.executable === executionResult.executable) {
+          this.handleResultChange();
+        }
+      });
+    }
+
+    @Watch('executable')
+    handleResultChange(): void {
+      if (this.executable && this.executable.result) {
+        const refresh = this.lastRenderedResult !== this.executable.result;
+        this.updateFromExecutionResult(this.executable.result, refresh);
+        this.lastRenderedResult = this.executable.result;
+      } else {
+        this.resetResultData();
+      }
+    }
+
+    resetResultData(): void {
+      this.type = ResultType.Table;
+
+      this.fetchedOnce = false;
+      this.streaming = false;
+      this.hasMore = false;
+
+      this.images = [];
+      this.lastFetchedRows = [];
+      this.rows = [];
+      this.meta = [];
+    }
+
+    updateFromExecutable(executable: Executable): void {
+      this.status = executable.status;
+      this.hasResultSet = !!(executable.handle && executable.handle.has_result_set);
+      if (!this.hasResultSet) {
+        this.resetResultData();
+      }
+    }
+
+    updateFromExecutionResult(executionResult: ExecutionResult, refresh?: boolean): void {
+      if (refresh) {
+        this.resetResultData();
+      }
+
+      if (executionResult) {
+        this.fetchedOnce = executionResult.fetchedOnce;
+        this.hasMore = executionResult.hasMore;
+        this.type = executionResult.type || ResultType.Table;
+        this.streaming = executionResult.streaming;
+
+        if (!this.meta.length && executionResult.meta.length) {
+          this.meta = executionResult.meta;
+        }
+
+        if (refresh) {
+          this.rows = [...executionResult.rows];
+        } else if (
+          executionResult.lastRows.length &&
+          this.rows.length !== executionResult.rows.length
+        ) {
+          this.rows.push(...executionResult.lastRows);
+        }
+        this.lastFetchedRows = executionResult.lastRows;
+      }
+    }
+  }
+</script>

+ 62 - 0
desktop/core/src/desktop/js/apps/notebook2/components/result/ExecutionResultsKoBridge.vue

@@ -0,0 +1,62 @@
+<!--
+  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.
+-->
+
+<template>
+  <ExecutionResults :executable="executable" />
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+  import { wrap } from 'vue/webComponentWrapper';
+
+  import ExecutionResults from './ExecutionResults.vue';
+  import SqlExecutable from 'apps/notebook2/execution/sqlExecutable';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+
+  @Component({
+    components: { ExecutionResults }
+  })
+  export default class ExecutionResultsKoBridge extends Vue {
+    @Prop()
+    executableObservable?: KnockoutObservable<SqlExecutable | undefined>;
+
+    initialized = false;
+    executable: SqlExecutable | null = null;
+
+    subTracker = new SubscriptionTracker();
+
+    updated(): void {
+      if (!this.initialized && this.executableObservable) {
+        this.executable = this.executableObservable() || null;
+        this.subTracker.subscribe(this.executableObservable, executable => {
+          this.executable = executable || null;
+        });
+        this.initialized = true;
+      }
+    }
+
+    destroyed(): void {
+      this.subTracker.dispose();
+    }
+  }
+
+  export const COMPONENT_NAME = 'execution-results-ko-bridge';
+  wrap(COMPONENT_NAME, ExecutionResultsKoBridge);
+</script>

+ 53 - 0
desktop/core/src/desktop/js/apps/notebook2/components/result/ResultGrid.vue

@@ -0,0 +1,53 @@
+<!--
+  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.
+-->
+
+<template>
+  <div><HueTable :columns="tableColumns" :rows="tableRows" /></div>
+</template>
+
+<script lang="ts">
+  import { ResultMeta } from 'apps/notebook2/execution/api';
+  import { ResultRow } from 'apps/notebook2/execution/executionResult';
+  import { Column, Row } from 'components/HueTable';
+  import HueTable from 'components/HueTable.vue';
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+
+  @Component({
+    components: { HueTable }
+  })
+  export default class ResultGrid extends Vue {
+    @Prop()
+    rows!: ResultRow[];
+    @Prop()
+    meta!: ResultMeta[];
+
+    get tableColumns(): Column<ResultRow>[] {
+      return this.meta.map(({ name }, index) => ({
+        label: name,
+        key: index,
+        htmlValue: true
+      }));
+    }
+
+    get tableRows(): Row[] {
+      return this.rows;
+    }
+  }
+</script>

+ 2 - 2
desktop/core/src/desktop/js/apps/notebook2/components/resultGrid/ko.simpleResultGrid.js

@@ -30,7 +30,7 @@ import {
   SHOW_NORMAL_RESULT_EVENT
 } from 'apps/notebook2/events';
 import { trackResult } from 'apps/notebook2/components/executableStateHandler';
-import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { ExecutionStatus } from 'apps/notebook2/execution/executable';
 
 export const SIMPLE_RESULT_GRID_COMPONENT = 'simple-result-grid';
 
@@ -83,7 +83,7 @@ class SimpleResultGrid extends DisposableComponent {
 
     // length > 99 comes from legacy code, likely related to data fetched twice.
     this.isExpired = ko.pureComputed(
-      () => this.status() === EXECUTION_STATUS.expired && this.data().length > 99
+      () => this.status() === ExecutionStatus.expired && this.data().length > 99
     );
 
     this.hueDatatable = undefined;

+ 7 - 6
desktop/core/src/desktop/js/apps/notebook2/execution/api.ts

@@ -17,8 +17,9 @@
 import { extractErrorMessage, post, successResponseIsError } from 'api/utils';
 import Executable, {
   ExecutableContext,
-  EXECUTION_STATUS
+  ExecutionStatus
 } from 'apps/notebook2/execution/executable';
+import { ResultType } from 'apps/notebook2/execution/executionResult';
 
 type SessionPropertyValue = string | number | boolean | null | undefined;
 
@@ -54,7 +55,7 @@ export interface ResultApiResponse {
   has_more: boolean;
   isEscaped: boolean;
   meta: ResultMeta[];
-  type: string;
+  type: ResultType;
 }
 
 export interface ResultSizeApiResponse {
@@ -217,8 +218,8 @@ export const executeStatement = async (options: ExecuteApiOptions): Promise<Exec
   executable.addCancellable({
     cancel: async () => {
       if (
-        executable.status !== EXECUTION_STATUS.running &&
-        executable.status !== EXECUTION_STATUS.streaming
+        executable.status !== ExecutionStatus.running &&
+        executable.status !== ExecutionStatus.streaming
       ) {
         // executable.status seems to have been set to 'canceling' so ignoring for now
         // return;
@@ -304,10 +305,10 @@ export const checkExecutionStatus = async (
   }
 
   if (response.status === -3) {
-    return { status: EXECUTION_STATUS.expired };
+    return { status: ExecutionStatus.expired };
   }
 
-  return { status: EXECUTION_STATUS.failed, message: response.message };
+  return { status: ExecutionStatus.failed, message: response.message };
 };
 
 export const fetchResults = async (options: {

+ 39 - 39
desktop/core/src/desktop/js/apps/notebook2/execution/executable.ts

@@ -40,20 +40,20 @@ import Executor from 'apps/notebook2/execution/executor';
  *
  * @type {{running: string, canceling: string, canceled: string, expired: string, waiting: string, success: string, ready: string, available: string, closed: string, starting: string}}
  */
-export const EXECUTION_STATUS = {
-  available: 'available',
-  failed: 'failed',
-  success: 'success',
-  expired: 'expired',
-  running: 'running',
-  starting: 'starting',
-  waiting: 'waiting',
-  ready: 'ready',
-  streaming: 'streaming',
-  canceled: 'canceled',
-  canceling: 'canceling',
-  closed: 'closed'
-};
+export enum ExecutionStatus {
+  available = 'available',
+  failed = 'failed',
+  success = 'success',
+  expired = 'expired',
+  running = 'running',
+  starting = 'starting',
+  waiting = 'waiting',
+  ready = 'ready',
+  streaming = 'streaming',
+  canceled = 'canceled',
+  canceling = 'canceling',
+  closed = 'closed'
+}
 
 export const EXECUTABLE_UPDATED_EVENT = 'hue.executable.updated';
 export const EXECUTABLE_STATUS_TRANSITION_EVENT = 'hue.executable.status.transitioned';
@@ -79,7 +79,7 @@ export default abstract class Executable {
   handle?: ExecutionHandle;
   operationId?: string;
   history?: ExecutionHistory;
-  status = EXECUTION_STATUS.ready;
+  status = ExecutionStatus.ready;
   progress = 0;
   result?: ExecutionResult;
   logs: ExecutionLogs;
@@ -98,7 +98,7 @@ export default abstract class Executable {
     this.logs = new ExecutionLogs(this);
   }
 
-  setStatus(status: string): void {
+  setStatus(status: ExecutionStatus): void {
     const oldStatus = this.status;
     this.status = status;
     if (oldStatus !== status) {
@@ -133,22 +133,22 @@ export default abstract class Executable {
 
   isReady(): boolean {
     return (
-      this.status === EXECUTION_STATUS.ready ||
-      this.status === EXECUTION_STATUS.closed ||
-      this.status === EXECUTION_STATUS.canceled
+      this.status === ExecutionStatus.ready ||
+      this.status === ExecutionStatus.closed ||
+      this.status === ExecutionStatus.canceled
     );
   }
 
   isRunning(): boolean {
-    return this.status === EXECUTION_STATUS.running || this.status === EXECUTION_STATUS.streaming;
+    return this.status === ExecutionStatus.running || this.status === ExecutionStatus.streaming;
   }
 
   isSuccess(): boolean {
-    return this.status === EXECUTION_STATUS.success || this.status === EXECUTION_STATUS.available;
+    return this.status === ExecutionStatus.success || this.status === ExecutionStatus.available;
   }
 
   isFailed(): boolean {
-    return this.status === EXECUTION_STATUS.failed;
+    return this.status === ExecutionStatus.failed;
   }
 
   isPartOfRunningExecution(): boolean {
@@ -194,7 +194,7 @@ export default abstract class Executable {
     this.edited = false;
     this.executeStarted = Date.now();
 
-    this.setStatus(EXECUTION_STATUS.running);
+    this.setStatus(ExecutionStatus.running);
     this.setProgress(0);
     this.notify(true);
 
@@ -248,7 +248,7 @@ export default abstract class Executable {
       this.checkStatus();
       this.logs.fetchLogs();
     } catch (err) {
-      this.setStatus(EXECUTION_STATUS.failed);
+      this.setStatus(ExecutionStatus.failed);
     }
   }
 
@@ -272,12 +272,12 @@ export default abstract class Executable {
     const queryStatus = await checkExecutionStatus({ executable: this });
 
     switch (queryStatus.status) {
-      case EXECUTION_STATUS.success:
+      case ExecutionStatus.success:
         this.executeEnded = Date.now();
         this.setStatus(queryStatus.status);
         this.setProgress(99); // TODO: why 99 here (from old code)?
         break;
-      case EXECUTION_STATUS.available:
+      case ExecutionStatus.available:
         this.executeEnded = Date.now();
         this.setStatus(queryStatus.status);
         this.setProgress(100);
@@ -292,12 +292,12 @@ export default abstract class Executable {
           this.nextExecutable.execute();
         }
         break;
-      case EXECUTION_STATUS.canceled:
-      case EXECUTION_STATUS.expired:
+      case ExecutionStatus.canceled:
+      case ExecutionStatus.expired:
         this.executeEnded = Date.now();
         this.setStatus(queryStatus.status);
         break;
-      case EXECUTION_STATUS.streaming:
+      case ExecutionStatus.streaming:
         if (!queryStatus.result) {
           return;
         }
@@ -309,9 +309,9 @@ export default abstract class Executable {
           }
           this.result.handleResultResponse(queryStatus.result);
         }
-      case EXECUTION_STATUS.running:
-      case EXECUTION_STATUS.starting:
-      case EXECUTION_STATUS.waiting:
+      case ExecutionStatus.running:
+      case ExecutionStatus.starting:
+      case ExecutionStatus.waiting:
         this.setStatus(queryStatus.status);
         checkStatusTimeout = window.setTimeout(
           () => {
@@ -320,7 +320,7 @@ export default abstract class Executable {
           actualCheckCount > 45 ? 5000 : 1000
         );
         break;
-      case EXECUTION_STATUS.failed:
+      case ExecutionStatus.failed:
         this.executeEnded = Date.now();
         this.setStatus(queryStatus.status);
         if (queryStatus.message) {
@@ -331,7 +331,7 @@ export default abstract class Executable {
         break;
       default:
         this.executeEnded = Date.now();
-        this.setStatus(EXECUTION_STATUS.failed);
+        this.setStatus(ExecutionStatus.failed);
         console.warn('Got unknown status ' + queryStatus.status);
     }
   }
@@ -357,20 +357,20 @@ export default abstract class Executable {
   async cancel(): Promise<void> {
     if (
       this.cancellables.length &&
-      (this.status === EXECUTION_STATUS.running || this.status === EXECUTION_STATUS.streaming)
+      (this.status === ExecutionStatus.running || this.status === ExecutionStatus.streaming)
     ) {
       hueAnalytics.log(
         'notebook',
         'cancel/' + (this.executor.connector() ? this.executor.connector().dialect : '')
       );
-      this.setStatus(EXECUTION_STATUS.canceling);
+      this.setStatus(ExecutionStatus.canceling);
       while (this.cancellables.length) {
         const cancellable = this.cancellables.pop();
         if (cancellable) {
           await cancellable.cancel();
         }
       }
-      this.setStatus(EXECUTION_STATUS.canceled);
+      this.setStatus(ExecutionStatus.canceled);
     }
   }
 
@@ -384,7 +384,7 @@ export default abstract class Executable {
     }
     this.handle = undefined;
     this.setProgress(0);
-    this.setStatus(EXECUTION_STATUS.ready);
+    this.setStatus(ExecutionStatus.ready);
   }
 
   toJs(): ExecutableRaw {
@@ -423,7 +423,7 @@ export default abstract class Executable {
     } catch (err) {
       console.warn('Failed closing statement');
     }
-    this.setStatus(EXECUTION_STATUS.closed);
+    this.setStatus(ExecutionStatus.closed);
   }
 
   async toContext(id?: string): Promise<ExecutableContext> {

+ 5 - 5
desktop/core/src/desktop/js/apps/notebook2/execution/executionLogs.ts

@@ -16,7 +16,7 @@
 
 import { ExecutionJob, fetchLogs } from 'apps/notebook2/execution/api';
 import huePubSub from 'utils/huePubSub';
-import Executable, { EXECUTION_STATUS } from './executable';
+import Executable, { ExecutionStatus } from './executable';
 
 export const LOGS_UPDATED_EVENT = 'hue.executable.logs.updated';
 
@@ -84,11 +84,11 @@ export default class ExecutionLogs {
     if (!finalFetch) {
       const delay = this.executable.getExecutionTime() > 45000 ? 5000 : 1000;
       const fetchLogsTimeout = window.setTimeout(async () => {
-        // TODO: Fetch logs for EXECUTION_STATUS.streaming?
+        // TODO: Fetch logs for ExecutionStatus.streaming?
         await this.fetchLogs(
-          this.executable.status !== EXECUTION_STATUS.running &&
-            this.executable.status !== EXECUTION_STATUS.starting &&
-            this.executable.status !== EXECUTION_STATUS.waiting
+          this.executable.status !== ExecutionStatus.running &&
+            this.executable.status !== ExecutionStatus.starting &&
+            this.executable.status !== ExecutionStatus.waiting
         );
       }, delay);
 

+ 11 - 5
desktop/core/src/desktop/js/apps/notebook2/execution/executionResult.ts

@@ -26,7 +26,7 @@ import * as ko from 'knockout';
 
 import huePubSub from 'utils/huePubSub';
 import { sleep } from 'utils/hueUtils';
-import Executable, { EXECUTION_STATUS } from './executable';
+import Executable, { ExecutionStatus } from './executable';
 
 export const RESULT_UPDATED_EVENT = 'hue.executable.result.updated';
 
@@ -87,12 +87,18 @@ export interface KoEnrichedMeta extends ResultMeta {
   originalIndex: number;
 }
 
+export type ResultRow = (string | number)[];
+
+export enum ResultType {
+  Table = 'table'
+}
+
 export default class ExecutionResult {
   executable: Executable;
   streaming: boolean;
 
-  type?: string;
-  rows: (string | number)[][] = [];
+  type?: ResultType;
+  rows: ResultRow[] = [];
   meta: ResultMeta[] = [];
 
   cleanedMeta: ResultMeta[] = [];
@@ -100,7 +106,7 @@ export default class ExecutionResult {
   cleanedStringMeta: ResultMeta[] = [];
   cleanedNumericMeta: ResultMeta[] = [];
   koEnrichedMeta: KoEnrichedMeta[] = [];
-  lastRows: (string | number)[][] = [];
+  lastRows: ResultRow[] = [];
   images = [];
 
   hasMore = true;
@@ -113,7 +119,7 @@ export default class ExecutionResult {
   }
 
   async fetchResultSize(): Promise<ResultSizeApiResponse | undefined> {
-    if (this.executable.status === EXECUTION_STATUS.failed) {
+    if (this.executable.status === ExecutionStatus.failed) {
       return;
     }
 

+ 10 - 10
desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.test.ts

@@ -19,7 +19,7 @@ import { CHECK_STATUS_API, CREATE_SESSION_API, EXECUTE_API_PREFIX, GET_LOGS_API
 
 import Executor from 'apps/notebook2/execution/executor';
 import SqlExecutable from './sqlExecutable';
-import { EXECUTION_STATUS } from './executable';
+import { ExecutionStatus } from './executable';
 import sessionManager from './sessionManager';
 import * as ApiUtils from 'api/utils';
 import { ParsedSqlStatement } from 'parse/sqlStatementsParser';
@@ -121,7 +121,7 @@ describe('sqlExecutable.js', () => {
           });
         } else if (url.indexOf(CHECK_STATUS_API) !== -1) {
           checkStatusApiHit = currentApiHit;
-          statusResolve({ query_status: { status: EXECUTION_STATUS.available } });
+          statusResolve({ query_status: { status: ExecutionStatus.available } });
           return statusPromise;
         } else if (url.indexOf(GET_LOGS_API) !== -1) {
           getLogsApiHit = currentApiHit;
@@ -133,7 +133,7 @@ describe('sqlExecutable.js', () => {
       }
     );
 
-    expect(subject.status).toEqual(EXECUTION_STATUS.ready);
+    expect(subject.status).toEqual(ExecutionStatus.ready);
 
     await subject.execute();
     await statusPromise;
@@ -144,7 +144,7 @@ describe('sqlExecutable.js', () => {
     expect(checkStatusApiHit).toEqual(3);
     expect(getLogsApiHit).toEqual(4);
 
-    expect(subject.status).toEqual(EXECUTION_STATUS.available);
+    expect(subject.status).toEqual(ExecutionStatus.available);
   });
 
   // xit('should set the correct status after failed execute', done => {
@@ -168,11 +168,11 @@ describe('sqlExecutable.js', () => {
   //     .execute()
   //     .then(fail)
   //     .catch(() => {
-  //       expect(subject.status).toEqual(EXECUTION_STATUS.failed);
+  //       expect(subject.status).toEqual(ExecutionStatus.failed);
   //       done();
   //     });
   //
-  //   expect(subject.status).toEqual(EXECUTION_STATUS.running);
+  //   expect(subject.status).toEqual(ExecutionStatus.running);
   //
   //   simplePostDeferred.reject();
   // });
@@ -194,19 +194,19 @@ describe('sqlExecutable.js', () => {
   //   subject
   //     .execute()
   //     .then(() => {
-  //       expect(subject.status).toEqual(EXECUTION_STATUS.available);
+  //       expect(subject.status).toEqual(ExecutionStatus.available);
   //       subject.cancel().then(() => {
-  //         expect(subject.status).toEqual(EXECUTION_STATUS.canceled);
+  //         expect(subject.status).toEqual(ExecutionStatus.canceled);
   //         done();
   //       });
   //
-  //       expect(subject.status).toEqual(EXECUTION_STATUS.canceling);
+  //       expect(subject.status).toEqual(ExecutionStatus.canceling);
   //
   //       simplePostCancelDeferred.resolve();
   //     })
   //     .catch(fail);
   //
-  //   expect(subject.status).toEqual(EXECUTION_STATUS.running);
+  //   expect(subject.status).toEqual(ExecutionStatus.running);
   //
   //   simplePostExeuteDeferred.resolve({ handle: {} });
   // });

+ 6 - 5
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -28,6 +28,7 @@ import 'apps/notebook2/components/ko.queryHistory';
 import './components/ExecutableActionsKoBridge.vue';
 import './components/EditorResizerKoBridge.vue';
 import './components/aceEditor/AceEditorKoBridge.vue';
+import './components/result/ExecutionResultsKoBridge.vue';
 
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
 import apiHelper from 'api/apiHelper';
@@ -42,7 +43,7 @@ import { HIDE_FIXED_HEADERS_EVENT, REDRAW_FIXED_HEADERS_EVENT } from 'apps/noteb
 import {
   EXECUTABLE_STATUS_TRANSITION_EVENT,
   EXECUTABLE_UPDATED_EVENT,
-  EXECUTION_STATUS
+  ExecutionStatus
 } from 'apps/notebook2/execution/executable';
 import {
   ACTIVE_STATEMENT_CHANGED_EVENT,
@@ -641,7 +642,7 @@ export default class Snippet {
         });
 
         this.executor.executables.forEach(async executable => {
-          if (executable.status !== EXECUTION_STATUS.ready) {
+          if (executable.status !== ExecutionStatus.ready) {
             await executable.checkStatus();
           } else {
             executable.notify();
@@ -661,9 +662,9 @@ export default class Snippet {
     huePubSub.subscribe(EXECUTABLE_STATUS_TRANSITION_EVENT, transitionDetails => {
       if (this.activeExecutable() === transitionDetails.executable) {
         if (
-          (transitionDetails.newStatus === EXECUTION_STATUS.available ||
-            transitionDetails.newStatus === EXECUTION_STATUS.failed ||
-            transitionDetails.newStatus === EXECUTION_STATUS.success) &&
+          (transitionDetails.newStatus === ExecutionStatus.available ||
+            transitionDetails.newStatus === ExecutionStatus.failed ||
+            transitionDetails.newStatus === ExecutionStatus.success) &&
           this.activeExecutable().history &&
           this.activeExecutable().handle
         ) {

+ 4 - 3
desktop/core/src/desktop/js/components/HueTable.d.ts

@@ -16,10 +16,11 @@
 
 export interface Column<T> {
   label: string;
-  key: string;
+  key: string | number;
   cssClass?: string;
+  htmlValue?: boolean;
   headerCssClass?: string;
-  adapter?: (key: string, row: T) => string | number | boolean | undefined;
+  adapter?: (key: string | number, row: T) => string | number | boolean | undefined;
 }
 
-export type Row = { [key: string]: unknown };
+export type Row = { [key: string]: unknown } | unknown[];

+ 21 - 1
desktop/core/src/desktop/js/components/HueTable.test.ts

@@ -39,6 +39,26 @@ describe('HueTable.vue', () => {
     expect(wrapper.element).toMatchSnapshot();
   });
 
+  it('should render a table with array rows', () => {
+    const wrapper = shallowMount(HueTable, {
+      propsData: {
+        columns: <Column<unknown>[]>[
+          { key: 1, label: 'A' },
+          { key: 2, label: 'D' },
+          { key: 3, label: 'C' },
+          { key: 4, label: 'B' }
+        ],
+        rows: <Row[]>[
+          ['1', 5, false, undefined],
+          ['2', 6, true, null],
+          ['3', 7, false],
+          ['4', 8, true]
+        ]
+      }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+
   it('should render a table with a custom component for a cell', () => {
     const wrapper = mount(HueTable, {
       scopedSlots: {
@@ -61,7 +81,7 @@ describe('HueTable.vue', () => {
   it('should render a table with adapter', () => {
     const wrapper = shallowMount(HueTable, {
       propsData: {
-        columns: <Column[]>[
+        columns: <Column<{ [key: string]: unknown }>[]>[
           { key: 'a', label: 'A' },
           {
             key: 'b',

+ 3 - 1
desktop/core/src/desktop/js/components/HueTable.vue

@@ -16,6 +16,7 @@
   limitations under the License.
 -->
 
+<!-- eslint-disable vue/no-v-html -->
 <template>
   <div class="hue-table-container">
     <table class="hue-table">
@@ -43,6 +44,7 @@
         <tr v-for="(row, rowIndex) in rows" :key="rowIndex" @click="$emit('row-clicked', row)">
           <td v-for="(column, colIndex) in columns" :key="colIndex" :class="column.cssClass">
             <slot v-if="hasCellSlot(column)" :name="cellSlotName(column)" v-bind="row" />
+            <div v-else-if="column.htmlValue" v-html="row[column.key]" />
             <template v-else>
               {{ column.adapter ? column.adapter(column.key, row) : row[column.key] }}
             </template>
@@ -53,6 +55,7 @@
     </table>
   </div>
 </template>
+<!-- eslint-enable vue/no-v-html -->
 
 <script lang="ts">
   import Vue from 'vue';
@@ -104,7 +107,6 @@
             padding: 12px;
             border: none;
             text-align: left;
-
             height: 16px;
             max-width: 300px;
 

+ 163 - 0
desktop/core/src/desktop/js/components/__snapshots__/HueTable.test.ts.snap

@@ -338,6 +338,169 @@ exports[`HueTable.vue should render a table with adapter 1`] = `
           
             20
           
+        </td>
+         
+        <td
+          class="column-flush"
+        />
+      </tr>
+    </tbody>
+  </table>
+</div>
+`;
+
+exports[`HueTable.vue should render a table with array rows 1`] = `
+<div
+  class="hue-table-container"
+>
+  <table
+    class="hue-table"
+  >
+    <caption>
+      
+      
+    
+    </caption>
+     
+    <thead>
+      <tr
+        class="header-row"
+      >
+        <th
+          scope="col"
+        >
+          
+          A
+        
+        </th>
+        <th
+          scope="col"
+        >
+          
+          D
+        
+        </th>
+        <th
+          scope="col"
+        >
+          
+          C
+        
+        </th>
+        <th
+          scope="col"
+        >
+          
+          B
+        
+        </th>
+         
+        <th
+          class="column-flush"
+          scope="col"
+        />
+      </tr>
+    </thead>
+     
+    <tbody>
+      <tr>
+        <td>
+          
+            5
+          
+        </td>
+        <td>
+          
+            false
+          
+        </td>
+        <td>
+          
+            
+          
+        </td>
+        <td>
+          
+            
+          
+        </td>
+         
+        <td
+          class="column-flush"
+        />
+      </tr>
+      <tr>
+        <td>
+          
+            6
+          
+        </td>
+        <td>
+          
+            true
+          
+        </td>
+        <td>
+          
+            
+          
+        </td>
+        <td>
+          
+            
+          
+        </td>
+         
+        <td
+          class="column-flush"
+        />
+      </tr>
+      <tr>
+        <td>
+          
+            7
+          
+        </td>
+        <td>
+          
+            false
+          
+        </td>
+        <td>
+          
+            
+          
+        </td>
+        <td>
+          
+            
+          
+        </td>
+         
+        <td
+          class="column-flush"
+        />
+      </tr>
+      <tr>
+        <td>
+          
+            8
+          
+        </td>
+        <td>
+          
+            true
+          
+        </td>
+        <td>
+          
+            
+          
+        </td>
+        <td>
+          
+            
+          
         </td>
          
         <td