Browse Source

[editor] Add a QueryHistoryTable Vue component

Johan Ahlen 4 years ago
parent
commit
512e4363ad

+ 50 - 8
desktop/core/src/desktop/js/apps/editor/api.ts

@@ -14,14 +14,17 @@
 // See the License for the specific language governing permissions and
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // limitations under the License.
 
 
-import axios from 'axios';
+import { ExecutionStatus } from './execution/executable';
+import { CancellablePromise } from 'api/cancellablePromise';
+import { get, post } from 'api/utils';
 
 
-type FormatSqlApiResponse = {
+const FORMAT_SQL_API_URL = '/notebook/api/format';
+const HISTORY_API_URL = '/notebook/api/get_history';
+
+interface FormatSqlApiResponse {
   formatted_statements?: string;
   formatted_statements?: string;
   status: number;
   status: number;
-};
-
-const FORMAT_SQL_API_URL = '/notebook/api/format';
+}
 
 
 export const formatSql = async (options: {
 export const formatSql = async (options: {
   statements: string;
   statements: string;
@@ -30,10 +33,10 @@ export const formatSql = async (options: {
   try {
   try {
     const params = new URLSearchParams();
     const params = new URLSearchParams();
     params.append('statements', options.statements);
     params.append('statements', options.statements);
-    const response = await axios.post<FormatSqlApiResponse>(FORMAT_SQL_API_URL, params);
+    const response = await post<FormatSqlApiResponse>(FORMAT_SQL_API_URL, params);
 
 
-    if (response.data.status !== -1 && response.data.formatted_statements) {
-      return response.data.formatted_statements;
+    if (response.status !== -1 && response.formatted_statements) {
+      return response.formatted_statements;
     }
     }
   } catch (err) {
   } catch (err) {
     if (!options.silenceErrors) {
     if (!options.silenceErrors) {
@@ -42,3 +45,42 @@ export const formatSql = async (options: {
   }
   }
   return options.statements;
   return options.statements;
 };
 };
+
+export interface FetchHistoryOptions {
+  type: string;
+  page?: number;
+  limit?: number;
+  docFilter?: string;
+  isNotificationManager?: boolean;
+}
+
+export interface FetchHistoryResponse {
+  count: number;
+  history: {
+    absoluteUrl: string;
+    data: {
+      lastExecuted: number;
+      parentSavedQueryUuid: string;
+      statement: string;
+      status: ExecutionStatus;
+    };
+    id: number;
+    name: string;
+    type: string;
+    uuid: string;
+  }[];
+  message: string;
+  status: number;
+}
+
+export const fetchHistory = (
+  options: FetchHistoryOptions
+): CancellablePromise<FetchHistoryResponse> => {
+  return get<FetchHistoryResponse>(HISTORY_API_URL, {
+    doc_type: options.type,
+    limit: options.limit || 50,
+    page: options.page || 1,
+    doc_text: options.docFilter,
+    is_notification_manager: options.isNotificationManager
+  });
+};

+ 96 - 0
desktop/core/src/desktop/js/apps/editor/components/ClearQueryHistoryModal.vue

@@ -0,0 +1,96 @@
+<!--
+  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>
+  <Modal
+    v-if="modelValue"
+    :header="I18n('Confirm History Clear')"
+    @close="$emit('update:model-value', false)"
+  >
+    <template #body>
+      <p>{{ I18n('Are you sure you want to clear the query history?') }}</p>
+    </template>
+    <template #footer>
+      <HueButton :disabled="clearingHistory" @click="$emit('update:model-value', false)">
+        {{ I18n('No') }}
+      </HueButton>
+      <HueButton :alert="true" :disabled="clearingHistory" @click="clearHistory">
+        <span v-if="!clearingHistory">{{ I18n('Yes') }}</span>
+        <span v-else>{{ I18n('Clearing...') }}</span>
+      </HueButton>
+    </template>
+  </Modal>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType, ref, toRefs } from 'vue';
+
+  import { post } from 'api/utils';
+  import Modal from 'components/Modal.vue';
+  import HueButton from 'components/HueButton.vue';
+  import { Connector } from 'config/types';
+  import huePubSub from 'utils/huePubSub';
+  import I18n from 'utils/i18n';
+
+  const HISTORY_CLEARED_EVENT = 'query.history.cleared';
+
+  export default defineComponent({
+    name: 'ClearQueryHistoryModal',
+    components: {
+      HueButton,
+      Modal
+    },
+    props: {
+      modelValue: {
+        type: Boolean,
+        default: false
+      },
+      connector: {
+        type: Object as PropType<Connector | undefined>,
+        default: undefined
+      }
+    },
+    emits: ['update:model-value', 'history-cleared'],
+    setup(props, { emit }) {
+      const { connector } = toRefs(props);
+      const clearingHistory = ref(false);
+
+      const clearHistory = async (): Promise<void> => {
+        if (!connector.value) {
+          return;
+        }
+        clearingHistory.value = true;
+
+        try {
+          await post('/notebook/api/clear_history', {
+            doc_type: connector.value.dialect
+          });
+          emit('history-cleared');
+          huePubSub.publish(HISTORY_CLEARED_EVENT);
+        } catch (err) {}
+        clearingHistory.value = false;
+        emit('update:model-value', false);
+      };
+      return {
+        clearHistory,
+        clearingHistory,
+        I18n
+      };
+    }
+  });
+</script>

+ 8 - 8
desktop/core/src/desktop/js/apps/editor/components/ExecuteButton.vue

@@ -17,7 +17,7 @@
 -->
 -->
 
 
 <template>
 <template>
-  <hue-button
+  <HueButton
     v-if="loadingSession"
     v-if="loadingSession"
     key="loading-button"
     key="loading-button"
     :small="true"
     :small="true"
@@ -25,8 +25,8 @@
     :title="I18n('Creating session')"
     :title="I18n('Creating session')"
   >
   >
     <i class="fa fa-fw fa-spinner fa-spin" /> {{ I18n('Loading') }}
     <i class="fa fa-fw fa-spinner fa-spin" /> {{ I18n('Loading') }}
-  </hue-button>
-  <hue-button
+  </HueButton>
+  <HueButton
     v-if="showExecute"
     v-if="showExecute"
     key="execute-button"
     key="execute-button"
     :small="true"
     :small="true"
@@ -35,9 +35,9 @@
     @click="execute"
     @click="execute"
   >
   >
     <i class="fa fa-play fa-fw" /> {{ I18n('Execute') }}
     <i class="fa fa-play fa-fw" /> {{ I18n('Execute') }}
-  </hue-button>
+  </HueButton>
 
 
-  <hue-button
+  <HueButton
     v-if="showStop && !stopping"
     v-if="showStop && !stopping"
     key="stop-button"
     key="stop-button"
     :small="true"
     :small="true"
@@ -47,11 +47,11 @@
     <i class="fa fa-stop fa-fw" />
     <i class="fa fa-stop fa-fw" />
     <span v-if="waiting">{{ I18n('Stop batch') }}</span>
     <span v-if="waiting">{{ I18n('Stop batch') }}</span>
     <span v-else>{{ I18n('Stop') }}</span>
     <span v-else>{{ I18n('Stop') }}</span>
-  </hue-button>
+  </HueButton>
 
 
-  <hue-button v-if="showStop && stopping" key="stopping-button" :small="true" :alert="true">
+  <HueButton v-if="showStop && stopping" key="stopping-button" :small="true" :alert="true">
     <i class="fa fa-fw fa-spinner fa-spin" /> {{ I18n('Stopping') }}
     <i class="fa fa-fw fa-spinner fa-spin" /> {{ I18n('Stopping') }}
-  </hue-button>
+  </HueButton>
 </template>
 </template>
 
 
 <script lang="ts">
 <script lang="ts">

+ 68 - 0
desktop/core/src/desktop/js/apps/editor/components/ExecutionStatusIcon.vue

@@ -0,0 +1,68 @@
+<!--
+  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 v-if="statusSpec" :title="statusSpec.title">
+    <i class="fa fa-fw" :class="statusSpec.faIcon" />
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType, toRefs, computed } from 'vue';
+
+  import { ExecutionStatus } from '../execution/executable';
+  import I18n from 'utils/i18n';
+
+  interface StatusSpec {
+    title: string;
+    faIcon: string;
+  }
+
+  export default defineComponent({
+    name: 'ExecutionStatusIcon',
+    props: {
+      status: {
+        type: Object as PropType<ExecutionStatus | null>,
+        default: null
+      }
+    },
+    setup(props) {
+      const { status } = toRefs(props);
+
+      const statusSpec = computed<StatusSpec | null>(() => {
+        switch (status.value) {
+          case ExecutionStatus.expired:
+            return { title: I18n('Expired'), faIcon: 'fa-unlink' };
+          case ExecutionStatus.available:
+            return { title: I18n('Available'), faIcon: 'fa-check' };
+          case ExecutionStatus.failed:
+            return { title: I18n('Failed'), faIcon: 'fa-exclamation' };
+          case ExecutionStatus.streaming:
+            return { title: I18n('Streaming'), faIcon: 'fa-fighter-jet' };
+          case ExecutionStatus.running:
+            return { title: I18n('Running'), faIcon: 'fa-fighter-jet' };
+        }
+        return null;
+      });
+
+      return {
+        statusSpec
+      };
+    }
+  });
+</script>

+ 4 - 0
desktop/core/src/desktop/js/apps/editor/components/QueryEditorWebComponents.ts

@@ -20,13 +20,16 @@ import AceEditor from './aceEditor/AceEditor.vue';
 import ExecuteButton from 'apps/editor/components/ExecuteButton.vue';
 import ExecuteButton from 'apps/editor/components/ExecuteButton.vue';
 import ExecuteLimitInput from 'apps/editor/components/ExecuteLimitInput.vue';
 import ExecuteLimitInput from 'apps/editor/components/ExecuteLimitInput.vue';
 import ExecutableProgressBar from './ExecutableProgressBar.vue';
 import ExecutableProgressBar from './ExecutableProgressBar.vue';
+import QueryHistoryTable from './QueryHistoryTable.vue';
 import ResultTable from './result/ResultTable.vue';
 import ResultTable from './result/ResultTable.vue';
 import Executor, { ExecutorOptions } from '../execution/executor';
 import Executor, { ExecutorOptions } from '../execution/executor';
 import 'utils/json.bigDataParse';
 import 'utils/json.bigDataParse';
 import { wrap } from 'vue/webComponentWrap';
 import { wrap } from 'vue/webComponentWrap';
+import { hueWindow } from 'types/types';
 
 
 wrap('query-editor', AceEditor);
 wrap('query-editor', AceEditor);
 wrap('query-editor-execute-button', ExecuteButton);
 wrap('query-editor-execute-button', ExecuteButton);
+wrap('query-editor-history-table', QueryHistoryTable);
 wrap('query-editor-limit-input', ExecuteLimitInput);
 wrap('query-editor-limit-input', ExecuteLimitInput);
 wrap('query-editor-progress-bar', ExecutableProgressBar);
 wrap('query-editor-progress-bar', ExecutableProgressBar);
 wrap('query-editor-result-table', ResultTable);
 wrap('query-editor-result-table', ResultTable);
@@ -38,6 +41,7 @@ export interface HueComponentConfig {
 const configure = (config: HueComponentConfig): void => {
 const configure = (config: HueComponentConfig): void => {
   if (config.baseUrl) {
   if (config.baseUrl) {
     axios.defaults.baseURL = config.baseUrl;
     axios.defaults.baseURL = config.baseUrl;
+    (window as hueWindow).HUE_BASE_URL = config.baseUrl;
   }
   }
 };
 };
 
 

+ 82 - 0
desktop/core/src/desktop/js/apps/editor/components/QueryHistoryTable.scss

@@ -0,0 +1,82 @@
+/*
+ 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 '../../../components/styles/mixins.scss';
+
+.query-history-table {
+  @include display-flex;
+  @include flex-direction(column);
+  position: relative;
+  width: 100%;
+  height: 100%;
+
+  .query-history-top-bar {
+    @include flex(0 0 auto);
+
+    padding: 10px 5px;
+
+    .query-history-filter {
+      float:left;
+    }
+
+    .query-history-actions {
+      float:right;
+      button {
+        margin-left: 5px;
+      }
+    }
+  }
+
+  .query-history-table-container {
+    @include flex(0 1 100%);
+    position: relative;
+    overflow: hidden;
+
+    .query-history-table-scrollable {
+      position: relative;
+      width: 100%;
+      height: 100%;
+
+      overflow: auto;
+    }
+
+    .no-history-entries {
+      padding-left: 10px;
+      font-style: italic;
+      font-size: 14px;
+    }
+
+    table {
+      td:nth-child(1) {
+        width: 100px;
+      }
+      td:nth-child(2),
+      td:nth-child(3) {
+        width: 25px;
+      }
+      td:nth-child(4) {
+        width: 100%;
+      }
+
+      td .query-history-last-executed,
+      td .query-history-status {
+        color: $fluid-gray-600;
+      }
+    }
+  }
+}

+ 283 - 0
desktop/core/src/desktop/js/apps/editor/components/QueryHistoryTable.vue

@@ -0,0 +1,283 @@
+<!--
+  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 class="query-history-table">
+    <ClearQueryHistoryModal
+      v-model="clearHistoryModalOpen"
+      :connector="connector"
+      @history-cleared="refresh"
+    />
+    <ImportDocumentsModal
+      v-model="importHistoryModalOpen"
+      :connector="connector"
+      :header="I18n('Import Query History')"
+      @documents-imported="refresh"
+    />
+    <div class="query-history-top-bar">
+      <div class="query-history-filter">
+        <SearchInput v-model="searchFilter" :show-magnify="false" :small="true" />
+      </div>
+      <div class="query-history-actions">
+        <HueButton :small="true" :disabled="!history.length" @click="onClearClick">
+          <i class="fa fa-fw fa-calendar-times-o" /> {{ I18n('Clear') }}
+        </HueButton>
+        <HueButton :small="true" :disabled="!history.length" @click="onExportClick">
+          <i
+            class="fa fa-fw"
+            :class="{ 'fa-download': !exportingHistory, 'fa-spinner fa-spin': exportingHistory }"
+          />
+          {{ I18n('Export') }}
+        </HueButton>
+        <HueButton :small="true" @click="onImportClick">
+          <i class="fa fa-fw fa-upload" /> {{ I18n('import') }}
+        </HueButton>
+      </div>
+    </div>
+    <div class="query-history-table-container">
+      <Spinner
+        :spin="loadingHistory"
+        :center="true"
+        :size="'large'"
+        :overlay="true"
+        :label="I18n('Loading...')"
+      />
+      <div class="query-history-table-scrollable">
+        <HueTable
+          :clickable-rows="true"
+          :columns="columns"
+          :rows="history"
+          :show-header="false"
+          @row-clicked="$emit('history-entry-clicked', $event)"
+        >
+          <template #cell-lastExecuted="historyEntry">
+            <TimeAgo class="query-history-last-executed" :value="historyEntry.lastExecuted" />
+          </template>
+          <template #cell-status="historyEntry">
+            <ExecutionStatusIcon class="query-history-status" :status="historyEntry.status" />
+          </template>
+          <template #cell-query="historyEntry">
+            <SqlText :value="historyEntry.query" :dialect="connector?.dialect" />
+          </template>
+        </HueTable>
+        <div v-if="!loadingHistory && !totalCount" class="no-history-entries">
+          <span v-if="!searchFilter">{{ I18n('Query History is empty') }}</span>
+          <span v-else>{{ I18n('No queries found for: ') }}{{ searchFilter }}</span>
+        </div>
+        <Paginator
+          v-show="!loadingHistory && totalCount"
+          :total-entries="totalCount"
+          @page-changed="onPageChange"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { debounce } from 'lodash';
+  import { defineComponent, PropType, ref, toRefs, watch } from 'vue';
+
+  import ClearQueryHistoryModal from './ClearQueryHistoryModal.vue';
+  import ExecutionStatusIcon from './ExecutionStatusIcon.vue';
+  import './QueryHistoryTable.scss';
+  import { fetchHistory, FetchHistoryResponse } from '../api';
+  import { ExecutionStatus } from '../execution/executable';
+  import { CancellablePromise } from 'api/cancellablePromise';
+  import { Connector } from 'config/types';
+  import HueButton from 'components/HueButton.vue';
+  import { Column } from 'components/HueTable';
+  import HueTable from 'components/HueTable.vue';
+  import ImportDocumentsModal from 'components/ImportDocumentsModal.vue';
+  import Paginator from 'components/Paginator.vue';
+  import { Page } from 'components/Paginator';
+  import SearchInput from 'components/SearchInput.vue';
+  import Spinner from 'components/Spinner.vue';
+  import SqlText from 'components/SqlText.vue';
+  import TimeAgo from 'components/TimeAgo.vue';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import { hueWindow } from 'types/types';
+  import huePubSub from 'utils/huePubSub';
+  import I18n from 'utils/i18n';
+
+  interface HistoryEntry {
+    absoluteUrl?: string;
+    query: string;
+    lastExecuted: number;
+    status: ExecutionStatus;
+    name: string;
+    uuid: string;
+  }
+
+  const ADD_TO_HISTORY_EVENT = 'query.history.add';
+  const IGNORE_NEXT_UNLOAD_EVENT = 'ignore.next.unload';
+
+  const trimEllipsis = (str: string): string =>
+    `${str.substring(0, 1000)}${str.length > 1000 ? '...' : ''}`;
+
+  export default defineComponent({
+    name: 'QueryHistoryTable',
+    components: {
+      ExecutionStatusIcon,
+      ClearQueryHistoryModal,
+      HueButton,
+      HueTable,
+      ImportDocumentsModal,
+      Paginator,
+      SearchInput,
+      Spinner,
+      SqlText,
+      TimeAgo
+    },
+    props: {
+      connector: {
+        type: Object as PropType<Connector | undefined>,
+        default: undefined
+      }
+    },
+    emits: ['history-entry-clicked'],
+    setup(props) {
+      const { connector } = toRefs(props);
+      const subTracker = new SubscriptionTracker();
+      const clearHistoryModalOpen = ref(false);
+      const exportingHistory = ref(false);
+      const history = ref<HistoryEntry[]>([]);
+      const importHistoryModalOpen = ref(false);
+      const loadingHistory = ref(true);
+      const searchFilter = ref('');
+      const totalCount = ref(0);
+
+      const columns: Column<HistoryEntry>[] = [
+        { key: 'lastExecuted' },
+        { key: 'status' },
+        { key: 'name' },
+        { key: 'query' }
+      ];
+
+      let runningPromise: CancellablePromise<FetchHistoryResponse> | undefined = undefined;
+
+      const updateHistory = async (page?: Page) => {
+        if (runningPromise) {
+          runningPromise.cancel();
+        }
+        if (connector.value && connector.value.dialect) {
+          loadingHistory.value = true;
+          runningPromise = fetchHistory({
+            type: connector.value.dialect,
+            page: page?.pageNumber,
+            limit: page?.limit,
+            docFilter: searchFilter.value
+          });
+
+          try {
+            const response = await runningPromise;
+            totalCount.value = response.count;
+            history.value = response.history.map(({ name, data, uuid, absoluteUrl }) => ({
+              name,
+              query: trimEllipsis(data.statement),
+              lastExecuted: data.lastExecuted,
+              status: data.status,
+              uuid,
+              absoluteUrl
+            }));
+          } catch (err) {}
+          runningPromise = undefined;
+          loadingHistory.value = false;
+        }
+      };
+
+      const onClearClick = (): void => {
+        clearHistoryModalOpen.value = true;
+      };
+
+      const onExportClick = async (): Promise<void> => {
+        const dialect = connector.value?.dialect;
+        if (!dialect || exportingHistory.value) {
+          return;
+        }
+        try {
+          exportingHistory.value = true;
+          const historyResponse = await fetchHistory({ type: dialect, page: 1, limit: 500 });
+
+          huePubSub.publish(IGNORE_NEXT_UNLOAD_EVENT);
+
+          if (historyResponse && historyResponse.history) {
+            window.location.href = `${
+              (window as hueWindow).HUE_BASE_URL
+            }/desktop/api2/doc/export?history=true&documents=[${historyResponse.history
+              .map(historyDoc => historyDoc.id)
+              .join(',')}]`;
+          }
+        } catch (err) {}
+        exportingHistory.value = false;
+      };
+
+      const onImportClick = (): void => {
+        importHistoryModalOpen.value = true;
+      };
+
+      const debouncedUpdate = debounce(updateHistory, 300);
+      watch(connector, () => debouncedUpdate(), { immediate: true });
+      watch(searchFilter, () => debouncedUpdate());
+
+      subTracker.subscribe(
+        ADD_TO_HISTORY_EVENT,
+        (details: {
+          absoluteUrl?: string;
+          lastExecuted: number;
+          name: string;
+          statement: string;
+          status: ExecutionStatus;
+          uuid: string;
+        }) => {
+          if (!history.value.some(entry => entry.uuid === details.uuid)) {
+            history.value = [
+              {
+                absoluteUrl: details.absoluteUrl,
+                lastExecuted: details.lastExecuted,
+                name: details.name,
+                query: details.statement,
+                status: details.status,
+                uuid: details.uuid
+              },
+              ...history.value
+            ];
+            totalCount.value++;
+          }
+        }
+      );
+
+      return {
+        I18n,
+        clearHistoryModalOpen,
+        columns,
+        exportingHistory,
+        history,
+        importHistoryModalOpen,
+        loadingHistory,
+        onClearClick,
+        onExportClick,
+        onImportClick,
+        onPageChange: updateHistory,
+        refresh: debouncedUpdate,
+        searchFilter,
+        totalCount
+      };
+    }
+  });
+</script>

+ 64 - 0
desktop/core/src/desktop/js/apps/editor/components/QueryHistoryTableKoBridge.vue

@@ -0,0 +1,64 @@
+<!--
+  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>
+  <QueryHistoryTable
+    :connector="connector"
+    @history-entry-clicked="$emit('history-entry-clicked', $event)"
+  />
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType, ref, toRefs } from 'vue';
+
+  import QueryHistoryTable from './QueryHistoryTable.vue';
+  import { wrap } from 'vue/webComponentWrap';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import { Connector } from 'config/types';
+
+  const QueryHistoryTableKoBridge = defineComponent({
+    name: 'QueryHistoryTableKoBridge',
+    components: {
+      QueryHistoryTable
+    },
+    props: {
+      connectorObservable: {
+        type: Object as PropType<KnockoutObservable<Connector | undefined>>,
+        default: undefined
+      }
+    },
+    emits: ['history-entry-clicked'],
+    setup(props) {
+      const subTracker = new SubscriptionTracker();
+      const { connectorObservable } = toRefs(props);
+
+      const connector = ref<Connector | undefined>(undefined);
+
+      subTracker.trackObservable(connectorObservable, connector);
+
+      return {
+        connector
+      };
+    }
+  });
+
+  export const COMPONENT_NAME = 'query-history-table-ko-bridge';
+  wrap(COMPONENT_NAME, QueryHistoryTableKoBridge);
+
+  export default QueryHistoryTableKoBridge;
+</script>

+ 0 - 47
desktop/core/src/desktop/js/apps/editor/components/__snapshots__/ko.queryHistory.test.js.snap

@@ -1,47 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ko.queryHistory.js should render component 1`] = `
-"<div data-bind=\\"descendantsComplete: descendantsComplete, component: { name: &quot;query-history&quot;, params: params }\\"><div class=\\"clear-history-modal modal hide fade\\">
-  <div class=\\"modal-header\\">
-    <button type=\\"button\\" class=\\"close\\" data-dismiss=\\"modal\\" aria-label=\\"Close\\"><span aria-hidden=\\"true\\">×</span></button>
-    <h2 class=\\"modal-title\\">Confirm History Clear</h2>
-  </div>
-  <div class=\\"modal-body\\">
-    <p>Are you sure you want to clear the query history?</p>
-  </div>
-  <div class=\\"modal-footer\\">
-    <a class=\\"btn\\" data-dismiss=\\"modal\\">No</a>
-    <a class=\\"btn btn-danger disable-feedback\\" data-bind=\\"click: clearHistory.bind($data)\\">Yes</a>
-  </div>
-</div><div class=\\"snippet-tab-actions\\">
-  <form autocomplete=\\"off\\" class=\\"inline-block\\">
-    <input class=\\"input-small search-input\\" type=\\"text\\" autocorrect=\\"off\\" autocomplete=\\"do-not-autocomplete\\" autocapitalize=\\"off\\" spellcheck=\\"false\\" placeholder=\\"Search...\\" data-bind=\\"
-        textInput: historyFilter,
-        clearable: historyFilter
-      \\">
-  </form>
-  <div class=\\"pull-right\\">
-    <div class=\\"btn-group\\">
-      <button class=\\"btn btn-mini btn-editor\\" data-bind=\\"enable: history().length, click: showClearHistoryModal.bind($data)\\" disabled=\\"\\">
-        <i class=\\"fa fa-fw fa-calendar-times-o\\"></i> Clear
-      </button>
-    </div>
-    <div class=\\"btn-group\\">
-      <button class=\\"btn btn-mini btn-editor\\" data-bind=\\"enable: history().length, click: exportHistory.bind($data)\\" disabled=\\"\\">
-        <i class=\\"fa fa-fw fa-download\\"></i> Export
-      </button>
-      <button class=\\"btn btn-mini btn-editor\\" data-bind=\\"click: importHistory.bind($data)\\">
-        <i class=\\"fa fa-fw fa-upload\\"></i> import
-      </button>
-    </div>
-  </div>
-</div><div class=\\"snippet-tab-body\\">
-  <!-- ko if: loadingHistory -->
-    <div>
-      <h1 class=\\"empty\\"><i class=\\"fa fa-spinner fa-spin\\"></i> Loading...</h1>
-    </div>
-  <!-- /ko -->
-
-  <!-- ko ifnot: loadingHistory --><!-- /ko -->
-</div></div>"
-`;

+ 0 - 373
desktop/core/src/desktop/js/apps/editor/components/ko.queryHistory.js

@@ -1,373 +0,0 @@
-// 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 $ from 'jquery';
-import * as ko from 'knockout';
-
-import apiHelper from 'api/apiHelper';
-import componentUtils from 'ko/components/componentUtils';
-import DisposableComponent from 'ko/components/DisposableComponent';
-import I18n from 'utils/i18n';
-import { ExecutionStatus } from 'apps/editor/execution/executable';
-import { sleep } from 'utils/hueUtils';
-import hueAnalytics from 'utils/hueAnalytics';
-import huePubSub from 'utils/huePubSub';
-import { SHOW_EVENT } from 'ko/components/ko.importDocumentsModal';
-
-export const NAME = 'query-history';
-export const HISTORY_CLEARED_EVENT = 'query.history.cleared';
-export const ADD_TO_HISTORY_EVENT = 'query.history.add';
-
-import { NAME as PAGINATOR_COMPONENT } from './ko.paginator';
-
-// prettier-ignore
-const TEMPLATE = `
-<div class="clear-history-modal modal hide fade">
-  <div class="modal-header">
-    <button type="button" class="close" data-dismiss="modal" aria-label="${ I18n('Close') }"><span aria-hidden="true">&times;</span></button>
-    <h2 class="modal-title">${ I18n('Confirm History Clear') }</h2>
-  </div>
-  <div class="modal-body">
-    <p>${ I18n('Are you sure you want to clear the query history?') }</p>
-  </div>
-  <div class="modal-footer">
-    <a class="btn" data-dismiss="modal">${ I18n('No') }</a>
-    <a class="btn btn-danger disable-feedback" data-bind="click: clearHistory.bind($data)">${ I18n('Yes') }</a>
-  </div>
-</div>
-
-<div class="snippet-tab-actions">
-  <form autocomplete="off" class="inline-block">
-    <input class="input-small search-input" type="text" ${ window.PREVENT_AUTOFILL_INPUT_ATTRS } placeholder="${ I18n('Search...') }" data-bind="
-        textInput: historyFilter,
-        clearable: historyFilter
-      "/>
-  </form>
-  <div class="pull-right">
-    <div class="btn-group">
-      <button class="btn btn-mini btn-editor" data-bind="enable: history().length, click: showClearHistoryModal.bind($data)">
-        <i class="fa fa-fw fa-calendar-times-o"></i> ${ I18n('Clear') }
-      </button>
-    </div>
-    <div class="btn-group">
-      <button class="btn btn-mini btn-editor" data-bind="enable: history().length, click: exportHistory.bind($data)">
-        <i class="fa fa-fw fa-download"></i> ${ I18n('Export') }
-      </button>
-      <button class="btn btn-mini btn-editor" data-bind="click: importHistory.bind($data)">
-        <i class="fa fa-fw fa-upload"></i> ${ I18n('import') }
-      </button>
-    </div>
-  </div>
-</div>
-
-<div class="snippet-tab-body">
-  <!-- ko if: loadingHistory -->
-    <div>
-      <h1 class="empty"><i class="fa fa-spinner fa-spin"></i> ${ I18n('Loading...') }</h1>
-    </div>
-  <!-- /ko -->
-
-  <!-- ko ifnot: loadingHistory -->
-    <!-- ko if: !history().length -->
-      <!-- ko ifnot: historyFilter -->
-        <div class="margin-top-10 margin-left-10" style="font-style: italic">${ I18n("No queries to be shown.") }</div>
-      <!-- /ko -->
-      <!-- ko if: historyFilter -->
-        <div class="margin-top-10 margin-left-10" style="font-style: italic">${ I18n('No queries found for') } <strong data-bind="text: historyFilter"></strong>.</div>
-      <!-- /ko -->
-    <!-- /ko -->
-
-    <!-- ko if: history().length -->
-      <table class="table table-condensed margin-top-10 history-table">
-        <tbody data-bind="foreach: history">
-          <tr data-bind="
-              click: function() {
-                $parent.openNotebook(uuid);
-              },
-              css: {
-                'highlight': uuid === $parent.currentNotebook.uuid(),
-                'pointer': uuid !== $parent.currentNotebook.uuid()
-              }
-            ">
-            <td style="width: 100px" class="muted" data-bind="style: { 'border-top-width': $index() === 0 ? '0' : ''}">
-              <span data-bind="momentFromNow: { data: lastExecuted, interval: 10000, titleFormat: 'LLL' }"></span>
-            </td>
-            <td style="width: 25px" class="muted" data-bind="style: { 'border-top-width': $index() === 0 ? '0' : ''}">
-              <!-- ko switch: status -->
-                <!-- ko case: 'running' -->
-                <div class="history-status" data-bind="tooltip: { title: '${ I18n("Query running") }', placement: 'bottom' }"><i class="fa fa-fighter-jet fa-fw"></i></div>
-                <!-- /ko -->
-                <!-- ko case: 'streaming' -->
-                <div class="history-status" data-bind="tooltip: { title: '${ I18n("Query streaming") }', placement: 'bottom' }"><i class="fa fa-fighter-jet fa-fw"></i></div>
-                <!-- /ko -->
-                <!-- ko case: 'failed' -->
-                <div class="history-status" data-bind="tooltip: { title: '${ I18n("Query failed") }', placement: 'bottom' }"><i class="fa fa-exclamation fa-fw"></i></div>
-                <!-- /ko -->
-                <!-- ko case: 'available' -->
-                <div class="history-status" data-bind="tooltip: { title: '${ I18n("Result available") }', placement: 'bottom' }"><i class="fa fa-check fa-fw"></i></div>
-                <!-- /ko -->
-                <!-- ko case: 'expired' -->
-                <div class="history-status" data-bind="tooltip: { title: '${ I18n("Result expired") }', placement: 'bottom' }"><i class="fa fa-unlink fa-fw"></i></div>
-                <!-- /ko -->
-              <!-- /ko -->
-            </td>
-            <td style="width: 25px" class="muted" data-bind="
-                ellipsis: {
-                  data: name,
-                  length: 30
-                },
-                style: {
-                  'border-top-width': $index() === 0 ? '0' : ''
-                }
-              "></td>
-            <td data-bind="
-                style: {
-                  'border-top-width': $index() === 0 ? '0' : ''
-                },
-                click: function() {
-                  $parent.openNotebook(uuid)
-                },
-                clickBubble: false
-              "><div data-bind="highlight: { value: query, dialect: $parent.dialect }"></div></td>
-          </tr>
-        </tbody>
-      </table>
-    <!-- /ko -->
-
-    <!-- ko component: {
-      name: '${ PAGINATOR_COMPONENT }',
-      params: {
-        currentPage: historyCurrentPage,
-        totalPages: historyTotalPages,
-        onPageChange: onPageChange
-      }
-    } --><!-- /ko -->
-  <!-- /ko -->
-</div>
-`;
-
-const QUERIES_PER_PAGE = 50;
-
-const AVAILABLE_INTERVAL = 60000 * 5;
-const STARTING_RUNNING_INTERVAL = 30000;
-
-const trimEllipsis = str => str.substring(0, 1000) + (str.length > 1000 ? '...' : '');
-
-class QueryHistory extends DisposableComponent {
-  constructor(params, element) {
-    super();
-    this.currentNotebook = params.currentNotebook;
-    this.dialect = params.dialect;
-    this.openFunction = params.openFunction;
-    this.element = element;
-
-    this.loadingHistory = ko.observable(true);
-    this.history = ko.observableArray();
-    this.historyFilter = ko.observable('').extend({ rateLimit: 900 });
-
-    this.historyCurrentPage = ko.observable(1);
-    this.totalHistoryCount = ko.observable(0);
-    this.historyTotalPages = ko.pureComputed(() =>
-      Math.max(1, Math.ceil(this.totalHistoryCount() / QUERIES_PER_PAGE))
-    );
-
-    this.refreshStatusFailed = false;
-
-    this.addDisposalCallback(() => {
-      this.refreshStatusFailed = true; // Cancels the status check intervals
-    });
-
-    let fetchTimeout = -1;
-    const throttledFetch = () => {
-      window.clearTimeout(fetchTimeout);
-      fetchTimeout = window.setTimeout(this.fetchHistory.bind(this), 10);
-    };
-
-    this.subscribe(this.historyFilter, () => {
-      if (this.historyCurrentPage() !== 1) {
-        this.historyCurrentPage(1);
-      } else {
-        throttledFetch();
-      }
-    });
-
-    this.onPageChange = throttledFetch;
-
-    this.subscribe(ADD_TO_HISTORY_EVENT, this.addHistoryRecord.bind(this));
-
-    throttledFetch();
-  }
-
-  async clearHistory() {
-    hueAnalytics.log('notebook', 'clearHistory');
-
-    apiHelper
-      .clearNotebookHistory({
-        notebookJson: await this.currentNotebook.toContextJson(),
-        docType: this.dialect()
-      })
-      .then(() => {
-        this.history.removeAll();
-        this.totalHistoryCount(0);
-        this.historyFilter('');
-        huePubSub.publish(HISTORY_CLEARED_EVENT);
-      })
-      .fail(xhr => {
-        if (xhr.status !== 502) {
-          $(document).trigger('error', xhr.responseText);
-        }
-      });
-
-    const $modal = $(this.element).find('.clear-history-modal');
-    $modal.modal('hide');
-  }
-
-  async exportHistory() {
-    const historyResponse = await apiHelper.getHistory({ type: this.dialect(), limit: 500 });
-
-    if (historyResponse && historyResponse.history) {
-      window.location.href =
-        window.HUE_BASE_URL +
-        '/desktop/api2/doc/export?history=true&documents=[' +
-        historyResponse.history.map(historyDoc => historyDoc.id).join(',') +
-        ']';
-    }
-  }
-
-  addHistoryRecord(historyRecord) {
-    this.history.unshift({
-      url: historyRecord.url,
-      query: trimEllipsis(historyRecord.statement),
-      lastExecuted: historyRecord.lastExecuted,
-      status: ko.observable(historyRecord.status),
-      name: historyRecord.name,
-      uuid: historyRecord.uuid
-    });
-    this.totalHistoryCount(this.totalHistoryCount() + 1);
-  }
-
-  async fetchHistory() {
-    this.loadingHistory(true);
-
-    try {
-      const historyData = await apiHelper.getHistory({
-        type: this.dialect(),
-        limit: QUERIES_PER_PAGE,
-        page: this.historyCurrentPage(),
-        docFilter: this.historyFilter()
-      });
-
-      if (historyData && historyData.history) {
-        this.history(
-          historyData.history.map(historyRecord => ({
-            url: historyRecord.absoluteUrl,
-            query: trimEllipsis(historyRecord.data.statement),
-            lastExecuted: historyRecord.data.lastExecuted,
-            status: ko.observable(historyRecord.data.status),
-            name: historyRecord.name,
-            uuid: historyRecord.uuid
-          }))
-        );
-      } else {
-        this.history([]);
-      }
-
-      this.totalHistoryCount(historyData.count);
-    } catch (err) {
-      this.history([]);
-      this.totalHistoryCount(0);
-    }
-
-    this.loadingHistory(false);
-    this.refreshStatus(
-      [ExecutionStatus.starting, ExecutionStatus.running, ExecutionStatus.streaming],
-      STARTING_RUNNING_INTERVAL
-    );
-    this.refreshStatus([ExecutionStatus.available], AVAILABLE_INTERVAL);
-  }
-
-  importHistory() {
-    huePubSub.publish(SHOW_EVENT, { importedCallback: this.fetchHistory.bind(this) });
-  }
-
-  openNotebook(uuid) {
-    if (window.getSelection().toString() === '' && uuid !== this.currentNotebook.uuid()) {
-      this.openFunction(uuid);
-    }
-  }
-
-  async refreshStatus(statusesToRefresh, interval) {
-    const statusIndex = {};
-    statusesToRefresh.forEach(status => {
-      statusIndex[status] = true;
-    });
-
-    const items = this.history()
-      .filter(item => statusIndex[item.status()])
-      .slice(0, 25);
-
-    const refreshStatusForItem = async item => {
-      if (this.refreshStatusFailed) {
-        return;
-      }
-      try {
-        const response = await apiHelper.checkStatus({
-          notebookJson: JSON.stringify({ uuid: item.uuid }),
-          silenceErrors: true
-        });
-        if (response.status === -3) {
-          item.status(ExecutionStatus.expired);
-        } else if (response.status !== 0) {
-          item.status(ExecutionStatus.failed);
-        } else if (response.query_status.status) {
-          item.status(response.query_status.status);
-        }
-      } catch (err) {
-        items.length = 0;
-        this.refreshStatusFailed = true;
-      } finally {
-        if (items.length) {
-          await sleep(1000);
-          await refreshStatusForItem(items.pop());
-        } else if (!this.refreshStatusFailed) {
-          await sleep(interval);
-          this.refreshStatus(statusesToRefresh, interval);
-        }
-      }
-    };
-
-    if (items.length) {
-      await refreshStatusForItem(items.pop());
-    } else if (!this.refreshStatusFailed) {
-      await sleep(interval);
-      this.refreshStatus(statusesToRefresh, interval);
-    }
-  }
-
-  showClearHistoryModal() {
-    const $modal = $(this.element).find('.clear-history-modal');
-    $modal.modal('show');
-  }
-}
-
-componentUtils.registerComponent(
-  NAME,
-  {
-    createViewModel: (params, componentInfo) =>
-      new QueryHistory(params, componentInfo.element.parentElement)
-  },
-  TEMPLATE
-);

+ 0 - 28
desktop/core/src/desktop/js/apps/editor/components/ko.queryHistory.test.js

@@ -1,28 +0,0 @@
-// 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 { koSetup } from 'jest/koTestUtils';
-import { NAME } from './ko.queryHistory';
-
-describe('ko.queryHistory.js', () => {
-  const setup = koSetup();
-
-  it('should render component', async () => {
-    const element = await setup.renderComponent(NAME, {});
-
-    expect(element.innerHTML).toMatchSnapshot();
-  });
-});

+ 2 - 1
desktop/core/src/desktop/js/apps/editor/notebook.js

@@ -25,7 +25,6 @@ import hueUtils from 'utils/hueUtils';
 import sessionManager from 'apps/editor/execution/sessionManager';
 import sessionManager from 'apps/editor/execution/sessionManager';
 
 
 import Snippet, { STATUS as SNIPPET_STATUS } from 'apps/editor/snippet';
 import Snippet, { STATUS as SNIPPET_STATUS } from 'apps/editor/snippet';
-import { HISTORY_CLEARED_EVENT } from 'apps/editor/components/ko.queryHistory';
 import { UPDATE_SAVED_QUERIES_EVENT } from 'apps/editor/components/ko.savedQueries';
 import { UPDATE_SAVED_QUERIES_EVENT } from 'apps/editor/components/ko.savedQueries';
 import {
 import {
   ASSIST_DB_PANEL_IS_READY_EVENT,
   ASSIST_DB_PANEL_IS_READY_EVENT,
@@ -33,6 +32,8 @@ import {
   ASSIST_SET_DATABASE_EVENT
   ASSIST_SET_DATABASE_EVENT
 } from 'ko/components/assist/events';
 } from 'ko/components/assist/events';
 
 
+const HISTORY_CLEARED_EVENT = 'query.history.cleared';
+
 export default class Notebook {
 export default class Notebook {
   constructor(vm, notebookRaw) {
   constructor(vm, notebookRaw) {
     this.parentVm = vm;
     this.parentVm = vm;

+ 2 - 2
desktop/core/src/desktop/js/apps/editor/snippet.js

@@ -22,11 +22,11 @@ import { markdown } from 'markdown';
 import 'apps/editor/components/ko.executableLogs';
 import 'apps/editor/components/ko.executableLogs';
 import 'apps/editor/components/ko.snippetEditorActions';
 import 'apps/editor/components/ko.snippetEditorActions';
 import 'apps/editor/components/resultChart/ko.resultChart';
 import 'apps/editor/components/resultChart/ko.resultChart';
-import 'apps/editor/components/ko.queryHistory';
 
 
 import './components/ExecutableActionsKoBridge.vue';
 import './components/ExecutableActionsKoBridge.vue';
 import './components/ExecutableProgressBarKoBridge.vue';
 import './components/ExecutableProgressBarKoBridge.vue';
 import './components/EditorResizerKoBridge.vue';
 import './components/EditorResizerKoBridge.vue';
+import './components/QueryHistoryTableKoBridge.vue';
 import './components/aceEditor/AceEditorKoBridge.vue';
 import './components/aceEditor/AceEditorKoBridge.vue';
 import './components/executionAnalysis/ExecutionAnalysisPanelKoBridge.vue';
 import './components/executionAnalysis/ExecutionAnalysisPanelKoBridge.vue';
 import './components/presentationMode/PresentationModeKoBridge.vue';
 import './components/presentationMode/PresentationModeKoBridge.vue';
@@ -52,7 +52,6 @@ import {
   ACTIVE_STATEMENT_CHANGED_EVENT,
   ACTIVE_STATEMENT_CHANGED_EVENT,
   REFRESH_STATEMENT_LOCATIONS_EVENT
   REFRESH_STATEMENT_LOCATIONS_EVENT
 } from 'ko/bindings/ace/aceLocationHandler';
 } from 'ko/bindings/ace/aceLocationHandler';
-import { ADD_TO_HISTORY_EVENT } from 'apps/editor/components/ko.queryHistory';
 import { findEditorConnector, getLastKnownConfig } from 'config/hueConfig';
 import { findEditorConnector, getLastKnownConfig } from 'config/hueConfig';
 import { cancelActiveRequest } from 'api/apiUtils';
 import { cancelActiveRequest } from 'api/apiUtils';
 import { getOptimizer } from 'catalog/optimizer/optimizer';
 import { getOptimizer } from 'catalog/optimizer/optimizer';
@@ -66,6 +65,7 @@ import {
 window.SqlExecutable = SqlExecutable;
 window.SqlExecutable = SqlExecutable;
 window.Executor = Executor;
 window.Executor = Executor;
 
 
+const ADD_TO_HISTORY_EVENT = 'query.history.add';
 export const CURRENT_QUERY_TAB_SWITCHED_EVENT = 'current.query.tab.switched';
 export const CURRENT_QUERY_TAB_SWITCHED_EVENT = 'current.query.tab.switched';
 
 
 export const DIALECT = {
 export const DIALECT = {

File diff suppressed because it is too large
+ 0 - 0
desktop/libs/notebook/src/notebook/static/notebook/css/editor2.css


+ 4 - 0
desktop/libs/notebook/src/notebook/static/notebook/less/editor2.less

@@ -244,6 +244,10 @@
               overflow: auto;
               overflow: auto;
             }
             }
           }
           }
+
+          .execution-history-tab-panel {
+            .fillAbsolute();
+          }
         }
         }
       }
       }
 
 

+ 12 - 8
desktop/libs/notebook/src/notebook/templates/editor2.mako

@@ -968,15 +968,19 @@
         </ul>
         </ul>
         <div class="editor-bottom-tab-content tab-content">
         <div class="editor-bottom-tab-content tab-content">
           <div class="tab-pane" id="queryHistory" data-bind="css: { 'active': currentQueryTab() === 'queryHistory' }">
           <div class="tab-pane" id="queryHistory" data-bind="css: { 'active': currentQueryTab() === 'queryHistory' }">
-            <div class="editor-bottom-tab-panel">
-              <!-- ko component: {
-                name: 'query-history',
-                params: {
-                  currentNotebook: parentNotebook,
-                  openFunction: parentVm.openNotebook.bind(parentVm),
-                  dialect: dialect
+            <div class="execution-history-tab-panel">
+              <query-history-table-ko-bridge style="width:100%; height: 100%; position: relative;" data-bind="
+                vueEvents: {
+                  'history-entry-clicked': function (event) {
+                    if (window.getSelection().toString() === '' && event.detail.uuid !== parentNotebook.uuid()) {
+                      parentVm.openNotebook(event.detail.uuid);
+                    }
+                  }
+                },
+                vueKoProps: {
+                  'connector-observable': connector
                 }
                 }
-              } --><!-- /ko -->
+              "></query-history-table-ko-bridge>
             </div>
             </div>
           </div>
           </div>
 
 

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