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

[editor] First version of new presentation mode in editor v2

Johan Ahlen 4 жил өмнө
parent
commit
bef7f753cd

+ 2 - 2
desktop/core/src/desktop/js/apps/editor/components/ExecutableActions.vue

@@ -96,7 +96,7 @@
     @Prop()
     executable?: SqlExecutable;
     @Prop()
-    beforeExecute?: () => Promise<void>;
+    beforeExecute?: (executable: Executable) => Promise<void>;
 
     subTracker = new SubscriptionTracker();
 
@@ -158,7 +158,7 @@
         return;
       }
       if (this.beforeExecute) {
-        await this.beforeExecute();
+        await this.beforeExecute(this.executable);
       }
       await this.executable.reset();
       this.executable.execute();

+ 9 - 4
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceEditorKoBridge.vue

@@ -37,6 +37,7 @@
 
 <script lang="ts">
   import { Ace } from 'ext/ace';
+  import { noop } from 'utils/hueUtils';
   import Vue from 'vue';
   import Component from 'vue-class-component';
   import { Prop } from 'vue-property-decorator';
@@ -80,9 +81,12 @@
 
         this.editorId = this.idObservable();
         if (!this.editorId) {
-          this.subTracker.whenDefined<string>(this.idObservable).then(id => {
-            this.editorId = id;
-          });
+          this.subTracker
+            .whenDefined<string>(this.idObservable)
+            .then(id => {
+              this.editorId = id;
+            })
+            .catch(noop);
         }
 
         this.cursorPosition = this.cursorPositionObservable();
@@ -91,7 +95,8 @@
             .whenDefined<Ace.Position>(this.cursorPositionObservable)
             .then(position => {
               this.cursorPosition = position;
-            });
+            })
+            .catch(noop);
         }
         this.initialized = true;
       }

+ 0 - 8
desktop/core/src/desktop/js/apps/editor/components/executionAnalysis/ExecutionAnalysisPanel.vue

@@ -126,11 +126,3 @@
     }
   }
 </script>
-
-<style lang="scss" scoped>
-  .execution-analysis {
-    position: relative;
-    height: 100%;
-    width: 100%;
-  }
-</style>

+ 39 - 0
desktop/core/src/desktop/js/apps/editor/components/presentationMode/PresentationMode.scss

@@ -0,0 +1,39 @@
+@import '../../../../components/styles/colors.scss';
+@import '../../../../components/styles/mixins.scss';
+
+.presentation-mode {
+  @include fillAbsolute;
+  @include flexRowLayout;
+
+  .presentation-mode-header {
+    @include flexRowLayoutRow(0 0 auto);
+
+    padding: 10px;
+
+    .presentation-mode-close-button {
+      position: absolute;
+      top: 10px;
+      right: 10px;
+    }
+  }
+
+  .presentation-mode-body {
+    @include flexRowLayoutRow(0 1 100%);
+
+    overflow: auto;
+
+    .presentation-mode-statement {
+      margin: 10px;
+
+      .presentation-mode-sql-text {
+        margin-bottom: 5px;
+      }
+
+      .presentation-mode-result-panel {
+        position: relative;
+        max-height: 400px;
+        overflow: hidden;
+      }
+    }
+  }
+}

+ 108 - 0
desktop/core/src/desktop/js/apps/editor/components/presentationMode/PresentationMode.vue

@@ -0,0 +1,108 @@
+<!--
+  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="presentation-mode">
+    <div class="presentation-mode-header">
+      <h2 v-if="title">{{ title }}</h2>
+      <h4 v-if="description">{{ description }}</h4>
+      <hue-button class="presentation-mode-close-button" :borderless="true" @click="$emit('close')">
+        <i class="fa fa-times fa-fw" />
+      </hue-button>
+    </div>
+    <div class="presentation-mode-body">
+      <div
+        v-for="{ header, statement, executable } of presentationStatements"
+        :key="executable.getKey()"
+        class="presentation-mode-statement"
+      >
+        <h2 v-if="header">{{ header }}</h2>
+        <h2 v-else>{{ I18n('Add -- comments on top of the SQL statement to display a title') }}</h2>
+
+        <SqlText class="presentation-mode-sql-text" :dialect="dialect" :value="statement" />
+        <ExecutableActions :executable="executable" :before-execute="beforeExecute" />
+        <div class="presentation-mode-result-panel">
+          <ResultTable :executable="executable" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+
+  import './PresentationMode.scss';
+  import ExecutableActions from 'apps/editor/components/ExecutableActions.vue';
+  import ResultTable from 'apps/editor/components/result/ResultTable.vue';
+  import Executor from 'apps/editor/execution/executor';
+  import SqlExecutable from 'apps/editor/execution/sqlExecutable';
+  import HueButton from 'components/HueButton.vue';
+  import SqlText from 'components/SqlText.vue';
+  import I18n from 'utils/i18n';
+
+  interface PresentationStatement {
+    executable: SqlExecutable;
+    statement: string;
+    header?: string;
+  }
+
+  const headerRegEx = /--(.*)$/m;
+
+  @Component({
+    components: { HueButton, ResultTable, ExecutableActions, SqlText },
+    methods: { I18n }
+  })
+  export default class PresentationMode extends Vue {
+    @Prop()
+    executor!: Executor;
+    @Prop({ required: false })
+    title?: string;
+    @Prop({ required: false })
+    description?: string;
+
+    get presentationStatements(): PresentationStatement[] {
+      return (this.executor.executables as SqlExecutable[]).map(executable => {
+        let statement = executable.parsedStatement.statement.trim();
+        let header: string | undefined = undefined;
+        const headerMatch = statement.match(headerRegEx);
+        if (headerMatch && headerMatch.index === 0) {
+          header = headerMatch[1].trim();
+          statement = statement.replace(headerMatch[0], '').trim();
+        }
+
+        return {
+          executable,
+          statement,
+          header
+        };
+      });
+    }
+
+    get dialect(): string {
+      return this.executor.connector().dialect as string;
+    }
+
+    async beforeExecute(executable: SqlExecutable): Promise<void> {
+      this.$emit('before-execute', executable);
+      this.executor.activeExecutable = executable;
+    }
+  }
+</script>

+ 93 - 0
desktop/core/src/desktop/js/apps/editor/components/presentationMode/PresentationModeKoBridge.vue

@@ -0,0 +1,93 @@
+<!--
+  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>
+  <PresentationMode
+    v-if="executor"
+    :executor="executor"
+    :title="title"
+    :description="description"
+    @before-execute="onBeforeExecute"
+    @close="onClose"
+  />
+</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 PresentationMode from './PresentationMode.vue';
+  import Executable from 'apps/editor/execution/executable';
+  import Executor from 'apps/editor/execution/executor';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+
+  @Component({
+    components: { PresentationMode }
+  })
+  export default class PresentationModeKoBridge extends Vue {
+    @Prop()
+    executor: Executor | null = null;
+    @Prop()
+    titleObservable: KnockoutObservable<string | undefined>;
+    @Prop()
+    descriptionObservable: KnockoutObservable<string | undefined>;
+
+    title: string | null = null;
+    description: string | null = null;
+
+    initialized = false;
+
+    subTracker = new SubscriptionTracker();
+
+    updated(): void {
+      if (!this.initialized) {
+        this.title = this.titleObservable();
+        this.subTracker.subscribe(this.titleObservable, (title?: string) => {
+          this.title = title;
+        });
+
+        this.description = this.descriptionObservable();
+        this.subTracker.subscribe(this.descriptionObservable, (description?: string) => {
+          this.description = description;
+        });
+        this.initialized = true;
+      }
+    }
+
+    destroyed(): void {
+      this.subTracker.dispose();
+    }
+
+    onBeforeExecute(executable: Executable): void {
+      this.$el.dispatchEvent(
+        new CustomEvent<Executable>('before-execute', { bubbles: true, detail: executable })
+      );
+    }
+
+    onClose(): void {
+      this.$el.dispatchEvent(
+        new CustomEvent<void>('close', { bubbles: true })
+      );
+    }
+  }
+
+  export const COMPONENT_NAME = 'presentation-mode-ko-bridge';
+  wrap(COMPONENT_NAME, PresentationModeKoBridge);
+</script>

+ 0 - 115
desktop/core/src/desktop/js/apps/editor/notebook.js

@@ -27,7 +27,6 @@ import sessionManager from 'apps/editor/execution/sessionManager';
 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 SqlExecutable from './execution/sqlExecutable';
 import {
   ASSIST_DB_PANEL_IS_READY_EVENT,
   ASSIST_IS_DB_PANEL_READY_EVENT,
@@ -54,10 +53,6 @@ export default class Notebook {
 
     this.isPresentationModeDefault = ko.observable(!!notebookRaw.isPresentationModeDefault);
     this.isPresentationMode = ko.observable(false);
-    this.isPresentationModeInitialized = ko.observable(false);
-    this.isPresentationMode.subscribe(this.onPresentationModeChange.bind(this));
-    this.presentationSnippets = ko.observable({});
-    this.prePresentationModeSnippet = undefined;
 
     this.isHidingCode = ko.observable(!!notebookRaw.isHidingCode);
 
@@ -111,18 +106,6 @@ export default class Notebook {
       notebookRaw.snippets.forEach(snippetRaw => {
         this.addSnippet(snippetRaw);
       });
-      if (
-        typeof notebookRaw.presentationSnippets != 'undefined' &&
-        notebookRaw.presentationSnippets != null
-      ) {
-        // Load
-        $.each(notebookRaw.presentationSnippets, (key, snippet) => {
-          snippet.status = 'ready'; // Protect from storm of check_statuses
-          const _snippet = new Snippet(vm, this, snippet);
-          _snippet.init();
-          this.presentationSnippets()[key] = _snippet;
-        });
-      }
     }
 
     huePubSub.subscribe(HISTORY_CLEARED_EVENT, () => {
@@ -426,100 +409,6 @@ export default class Notebook {
     });
   }
 
-  onPresentationModeChange(isPresentationMode) {
-    if (isPresentationMode) {
-      hueAnalytics.convert('editor', 'presentation');
-    }
-
-    // Problem with headers / row numbers redraw on full screen results
-    huePubSub.publish('editor.presentation.operate.toggle', isPresentationMode);
-    const newSnippets = [];
-
-    if (isPresentationMode) {
-      const sourceSnippet = this.snippets()[0];
-      this.prePresentationModeSnippet = sourceSnippet;
-      const statementKeys = {};
-
-      const database = sourceSnippet.database();
-
-      sourceSnippet.executor.executables.forEach(executable => {
-        const sqlStatement = executable.parsedStatement.statement;
-        const statementKey = sqlStatement.hashCode() + database;
-
-        let presentationSnippet;
-
-        if (!this.presentationSnippets()[statementKey]) {
-          const titleLines = [];
-          const statementLines = [];
-          sqlStatement
-            .trim()
-            .split('\n')
-            .forEach(line => {
-              if (line.trim().startsWith('--') && statementLines.length === 0) {
-                titleLines.push(line.substr(2));
-              } else {
-                statementLines.push(line);
-              }
-            });
-          presentationSnippet = new Snippet(this.parentVm, this, {
-            connector: sourceSnippet.connector(),
-            statement_raw: statementLines.join('\n'),
-            database: database,
-            name: titleLines.join('\n')
-          });
-          presentationSnippet.variableSubstitutionHandler =
-            sourceSnippet.variableSubstitutionHandler;
-          presentationSnippet.executor.variableSubstitionHandler =
-            sourceSnippet.variableSubstitutionHandler;
-          window.setTimeout(() => {
-            const executableRaw = executable.toJs();
-            const reattachedExecutable = SqlExecutable.fromJs(
-              presentationSnippet.executor,
-              executableRaw
-            );
-            reattachedExecutable.result = executable.result;
-            presentationSnippet.executor.executables = [reattachedExecutable];
-            presentationSnippet.activeExecutable(reattachedExecutable);
-          }, 1000); // TODO: Make it possible to set activeSnippet on Snippet creation
-          presentationSnippet.init();
-          this.presentationSnippets()[statementKey] = presentationSnippet;
-        } else {
-          presentationSnippet = this.presentationSnippets()[statementKey];
-        }
-        statementKeys[statementKey] = true;
-        newSnippets.push(presentationSnippet);
-      });
-
-      Object.keys(this.presentationSnippets()).forEach(key => {
-        // Dead statements
-        if (!statementKeys[key]) {
-          this.presentationSnippets()[key].executor.executables.forEach(executable => {
-            executable.cancelBatchChain();
-          });
-          delete this.presentationSnippets()[key];
-        }
-      });
-    } else {
-      newSnippets.push(this.prePresentationModeSnippet);
-    }
-    this.parentVm.editorMode(!isPresentationMode);
-    this.snippets(newSnippets);
-
-    newSnippets.forEach(snippet => {
-      huePubSub.publish('editor.redraw.data', { snippet: snippet });
-      if (this.isPresentationMode()) {
-        window.setTimeout(() => {
-          snippet.executor.executables.forEach(executable => {
-            executable.notify();
-            if (executable.result) {
-              executable.result.notify();
-            }
-          });
-        }, 1000); // TODO: Make it possible to set activeSnippet on Snippet creation
-      }
-    });
-  }
-
   async toContextJson() {
     return JSON.stringify({
       id: this.id(),
@@ -548,10 +437,6 @@ export default class Notebook {
       name: this.name(),
       onSuccessUrl: this.onSuccessUrl(),
       parentSavedQueryUuid: this.parentSavedQueryUuid(),
-      presentationSnippets: Object.keys(this.presentationSnippets()).reduce((result, key) => {
-        result[key] = this.presentationSnippets()[key].toJs();
-        return result;
-      }, {}),
       pubSubUrl: this.pubSubUrl(),
       result: {}, // TODO: Moved to executor but backend requires it
       sessions: await sessionManager.getAllSessions(),

+ 1 - 13
desktop/core/src/desktop/js/apps/editor/snippet.js

@@ -29,6 +29,7 @@ import './components/ExecutableActionsKoBridge.vue';
 import './components/EditorResizerKoBridge.vue';
 import './components/aceEditor/AceEditorKoBridge.vue';
 import './components/executionAnalysis/ExecutionAnalysisPanelKoBridge.vue';
+import './components/presentationMode/PresentationModeKoBridge.vue';
 import './components/result/ResultTableKoBridge.vue';
 
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
@@ -407,19 +408,6 @@ export default class Snippet {
           } else {
             this.statementsList([]);
           }
-          if (!this.parentNotebook.isPresentationModeInitialized()) {
-            if (this.parentNotebook.isPresentationModeDefault()) {
-              // When switching to presentation mode, the snippet in non presentation mode cannot get status notification.
-              // On initiailization, status is set to loading and does not get updated, because we moved to presentation mode.
-              this.status(STATUS.ready);
-            }
-            // Changing to presentation mode requires statementsList to be initialized. statementsList is initialized asynchronously.
-            // When presentation mode is default, we cannot change before statementsList has been calculated.
-            // Cleaner implementation would be to make toggleEditorMode statementsList asynchronous
-            // However this is currently impossible due to delete _notebook.presentationSnippets()[key];
-            this.parentNotebook.isPresentationModeInitialized(true);
-            this.parentNotebook.isPresentationMode(this.parentNotebook.isPresentationModeDefault());
-          }
         }
       },
       this.parentVm.huePubSubId

+ 4 - 0
desktop/core/src/desktop/js/utils/hueUtils.ts

@@ -603,6 +603,10 @@ export const defer = (callback: () => void): void => {
   sleep(0).finally(callback);
 };
 
+export const noop = (): void => {
+  // noop
+};
+
 export default {
   bootstrapRatios,
   changeURL,

+ 156 - 208
desktop/libs/notebook/src/notebook/templates/editor2.mako

@@ -299,43 +299,6 @@
 
 <script type="text/html" id="editor-code-editor">
   <div style="width: 100%; height: 100%; position: relative;" class="editor-drop-target">
-##     <!-- ko if: statementType() == 'file' -->
-##       <div class="margin-top-10">
-##         <label class="pull-left" style="margin-top: 6px;margin-right: 10px;">${_('Query File')}</label>
-##         <input type="text" class="pull-left input-xxlarge filechooser-input" data-bind="value: statementPath, valueUpdate: 'afterkeydown', filechooser: statementPath, filechooserOptions: { skipInitialPathIfEmpty: true, linkMarkup: true }" placeholder="${ _('Path to file, e.g. /user/hue/sample.sql') }"/>
-##         <!-- ko if: statementPath() -->
-##           <div class="inline-block" style="margin-top: 4px">
-##             <a data-bind="hueLink: '/filebrowser/view=' + statementPath()" title="${ _('Open') }">
-##               <i class="fa fa-external-link-square"></i>
-##             </a>
-##           </div>
-##           <a class="btn" data-bind="click: function() { getExternalStatement(); }"><i class="fa fa-lg fa-refresh"></i></a>
-##         <!-- /ko -->
-##       </div>
-##       <div class="clearfix margin-bottom-20"></div>
-##     <!-- /ko -->
-##
-##     <!-- ko if: statementType() == 'document' -->
-##       <div class="margin-top-10">
-##         <!-- ko if: associatedDocumentLoading -->
-##           <i class="fa fa-spinner fa-spin muted"></i>
-##         <!-- /ko -->
-##         <label class="pull-left" style="margin-top: 6px;margin-right: 10px;" data-bind="visible: !associatedDocumentLoading()">${_('Document')}</label>
-##         <div class="selectize-wrapper" style="width: 300px;" data-bind="visible: !associatedDocumentLoading()">
-##           <select placeholder="${ _('Search your documents...') }" data-bind="documentChooser: { loading: associatedDocumentLoading, value: associatedDocumentUuid, document: associatedDocument, type: dialect }"></select>
-##         </div>
-##         <!-- ko if: associatedDocument() -->
-##           <div class="pull-left" style="margin-top: 4px">
-##             <a data-bind="hueLink: associatedDocument().absoluteUrl" title="${ _('Open') }">
-##               <i class="fa fa-external-link-square"></i>
-##             </a>
-##             <span data-bind='text: associatedDocument().description' style="padding: 3px; margin-top: 2px" class="muted"></span>
-##           </div>
-##         <!-- /ko -->
-##       </div>
-##       <div class="clearfix margin-bottom-20"></div>
-##     <!-- /ko -->
-
     <ace-editor-ko-bridge style="width:100%; height: 100%; position: relative;" data-bind="
       vueEvents: {
         'ace-created': function (event) {
@@ -800,7 +763,7 @@
 </script>
 
 <script type="text/html" id="editor-navbar">
-  <div class="navbar hue-title-bar" data-bind="visible: ! $root.isPresentationMode()">
+  <div class="navbar hue-title-bar">
     <div class="navbar-inner">
       <div class="container-fluid">
         <!-- ko template: { name: 'editor-menu-buttons' } --><!-- /ko -->
@@ -885,203 +848,188 @@
       </div>
     </div>
   </div>
-
-  <!-- ko if: $root.isPresentationMode() -->
-  <a class="hueAnchor collapse-results" href="javascript:void(0)" title="${ _('Exit presentation') }" data-bind="click: function(){ $root.selectedNotebook().isPresentationMode(false); }">
-    <i class="fa fa-times fa-fw"></i>
-  </a>
-  <!-- /ko -->
-
-  <div class="player-toolbar margin-top-10" data-bind="visible: $root.isPresentationMode()" style="display: none">
-    <!-- ko if: $root.isPresentationMode() -->
-    <!-- ko if: $root.selectedNotebook() -->
-    <!-- ko if: $root.selectedNotebook().name() || $root.selectedNotebook().description() -->
-    <h2 class="margin-left-30 margin-right-10 inline padding-left-5" data-bind="text: $root.selectedNotebook().name"></h2>
-    <h2 class="muted inline" data-bind="text: $root.selectedNotebook().description"></h2>
-    <div class="clearfix"></div>
-    <!-- /ko -->
-
-    <!-- ko template: { name: 'editor-menu-buttons' } --><!-- /ko -->
-
-    <div class="margin-left-30 margin-top-10 padding-left-5 margin-bottom-20">
-      <!-- ko template: { name: 'editor-execution-actions' } --><!-- /ko -->
-      <!-- ko if: selectedNotebook().prePresentationModeSnippet -->
-      <!-- ko template: { if: $root.isPresentationMode(), name: 'snippet-variables', data: selectedNotebook().prePresentationModeSnippet }--><!-- /ko -->
-      <!-- /ko -->
-    </div>
-
-    <!-- /ko -->
-    <!-- /ko -->
-  </div>
 </script>
 
 <div id="editorComponents" class="editor" data-bind="css: { 'editor-bottom-expanded': bottomExpanded, 'editor-top-expanded': topExpanded }">
   <!-- ko template: { name: 'editor-modals' } --><!-- /ko -->
+  <!-- ko if: !isPresentationModeEnabled() || !isPresentationMode() -->
   <div class="editor-nav-bar">
     <!-- ko template: { name: 'editor-navbar' } --><!-- /ko -->
   </div>
+  <!-- /ko -->
   <div class="editor-app" data-bind="with: firstSnippet">
-    <div class="editor-top">
-      <div class="editor-top-actions">
-        <!-- ko template: { name: 'editor-query-redacted' } --><!-- /ko -->
-        <!-- ko template: { name: 'editor-longer-operation' } --><!-- /ko -->
-        <div class="editor-top-right-actions">
-          <!-- ko template: { name: 'snippet-header-database-selection' } --><!-- /ko -->
-          <button title="${ _('Expand editor') }" data-bind="toggle: $root.topExpanded">
-            <i class="fa" data-bind="css: { 'fa-expand': !$root.topExpanded(), 'fa-compress': $root.topExpanded() }"></i>
-          </button>
+    <!-- ko if: $parent.isPresentationModeEnabled() && $parent.isPresentationMode() -->
+      <presentation-mode-ko-bridge style="width:100%; height: 100%; position: relative;" data-bind="
+        vueEvents: {
+          'before-execute': function (event) {
+            activeExecutable(event.detail);
+          },
+          'close': function () {
+            parentNotebook.isPresentationMode(false);
+          }
+        },
+        vueKoProps: {
+          executor: executor,
+          titleObservable: parentNotebook.name,
+          descriptionObservable: parentNotebook.description
+        }
+      "></presentation-mode-ko-bridge>
+    <!-- /ko -->
+    <!-- ko ifnot: $parent.isPresentationModeEnabled() && $parent.isPresentationMode() -->
+      <div class="editor-top">
+        <div class="editor-top-actions">
+          <!-- ko template: { name: 'editor-query-redacted' } --><!-- /ko -->
+          <!-- ko template: { name: 'editor-longer-operation' } --><!-- /ko -->
+          <div class="editor-top-right-actions">
+            <!-- ko template: { name: 'snippet-header-database-selection' } --><!-- /ko -->
+            <button title="${ _('Expand editor') }" data-bind="toggle: $root.topExpanded">
+              <i class="fa" data-bind="css: { 'fa-expand': !$root.topExpanded(), 'fa-compress': $root.topExpanded() }"></i>
+            </button>
+          </div>
+        </div>
+        <div class="editor-settings-drawer">
+          <!-- ko template: 'editor-snippet-settings' --><!-- /ko -->
+        </div>
+  ##      <!-- ko template: { name: 'editor-optimizer-alerts' } --><!-- /ko -->
+        <div class="editor-code-editor">
+          <!-- ko template: { name: 'editor-code-editor' } --><!-- /ko -->
+        </div>
+  ##      <!-- ko template: { name: 'snippet-variables' }--><!-- /ko -->
+  ##      <!-- ko template: { name: 'editor-executable-snippet-body' } --><!-- /ko -->
+        <div class="editor-execute-status">
+          <!-- ko template: { name: 'editor-snippet-execution-status' } --><!-- /ko -->
+        </div>
+        <div class="editor-execute-actions">
+          <!-- ko template: { name: 'editor-execution-controls' } --><!-- /ko -->
         </div>
       </div>
-      <div class="editor-settings-drawer">
-        <!-- ko template: 'editor-snippet-settings' --><!-- /ko -->
-      </div>
-##      <!-- ko template: { name: 'editor-optimizer-alerts' } --><!-- /ko -->
-      <div class="editor-code-editor">
-        <!-- ko template: { name: 'editor-code-editor' } --><!-- /ko -->
-      </div>
-##      <!-- ko template: { name: 'snippet-variables' }--><!-- /ko -->
-##      <!-- ko template: { name: 'editor-executable-snippet-body' } --><!-- /ko -->
-      <div class="editor-execute-status">
-        <!-- ko template: { name: 'editor-snippet-execution-status' } --><!-- /ko -->
-      </div>
-      <div class="editor-execute-actions">
-        <!-- ko template: { name: 'editor-execution-controls' } --><!-- /ko -->
-      </div>
-##      <!-- ko component: { name: 'executable-logs', params: {
-##        activeExecutable: activeExecutable,
-##        showLogs: showLogs,
-##        resultsKlass: resultsKlass,
-##        isPresentationMode: parentNotebook.isPresentationMode,
-##        isHidingCode: parentNotebook.isHidingCode
-##      }} --><!-- /ko -->
-    </div>
-    <div class="editor-divider"></div>
-    <div class="editor-bottom">
-      <ul class="editor-bottom-tabs nav nav-tabs">
-        <li data-bind="click: function() { currentQueryTab('queryHistory'); }, css: { 'active': currentQueryTab() == 'queryHistory' }">
-          <a class="inactive-action" style="display:inline-block" href="#queryHistory" data-toggle="tab">${_('Query History')}</a>
-        </li>
-        <li data-bind="click: function(){ currentQueryTab('savedQueries'); }, css: { 'active': currentQueryTab() == 'savedQueries' }">
-          <a class="inactive-action" style="display:inline-block" href="#savedQueries" data-toggle="tab">${_('Saved Queries')}</a>
-        </li>
-        <li data-bind="click: function() { currentQueryTab('queryResults'); }, css: {'active': currentQueryTab() == 'queryResults'}">
-          <a class="inactive-action" style="display:inline-block" href="#queryResults" data-toggle="tab">${_('Results')}
-##          <!-- ko if: result.rows() != null  -->
-##          (<span data-bind="text: result.rows().toLocaleString() + (dialect() == 'impala' && result.rows() == 1024 ? '+' : '')" title="${ _('Number of rows') }"></span>)
-##          <!-- /ko -->
-          </a>
-        </li>
-        <li data-bind="click: function() { currentQueryTab('queryChart'); }, css: {'active': currentQueryTab() == 'queryChart'}">
-          <a class="inactive-action" style="display:inline-block" href="#queryChart" data-toggle="tab">${_('Chart')}</a>
-        </li>
-        <!-- ko if: explanation -->
-        <li data-bind="click: function() { currentQueryTab('queryExplain'); }, css: {'active': currentQueryTab() == 'queryExplain'}"><a class="inactive-action" href="#queryExplain" data-toggle="tab">${_('Explain')}</a></li>
-        <!-- /ko -->
-        <!-- ko foreach: pinnedContextTabs -->
-        <li data-bind="click: function() { $parent.currentQueryTab(tabId) }, css: { 'active': $parent.currentQueryTab() === tabId }">
-          <a class="inactive-action" data-toggle="tab" data-bind="attr: { 'href': '#' + tabId }">
-            <i class="snippet-icon fa" data-bind="css: iconClass"></i> <span data-bind="text: title"></span>
-            <div class="inline-block inactive-action margin-left-10 pointer" data-bind="click: function () { $parent.removeContextTab($data); }">
-              <i class="snippet-icon fa fa-times"></i>
-            </div>
-          </a>
-        </li>
-        <!-- /ko -->
-
-        <li data-bind="click: function(){ currentQueryTab('executionAnalysis'); }, css: {'active': currentQueryTab() == 'executionAnalysis'}">
-          <a class="inactive-action" href="#executionAnalysis" data-toggle="tab">${_('Execution Analysis')}</a>
-        </li>
+      <div class="editor-divider"></div>
+      <div class="editor-bottom">
+        <ul class="editor-bottom-tabs nav nav-tabs">
+          <li data-bind="click: function() { currentQueryTab('queryHistory'); }, css: { 'active': currentQueryTab() == 'queryHistory' }">
+            <a class="inactive-action" style="display:inline-block" href="#queryHistory" data-toggle="tab">${_('Query History')}</a>
+          </li>
+          <li data-bind="click: function(){ currentQueryTab('savedQueries'); }, css: { 'active': currentQueryTab() == 'savedQueries' }">
+            <a class="inactive-action" style="display:inline-block" href="#savedQueries" data-toggle="tab">${_('Saved Queries')}</a>
+          </li>
+          <li data-bind="click: function() { currentQueryTab('queryResults'); }, css: {'active': currentQueryTab() == 'queryResults'}">
+            <a class="inactive-action" style="display:inline-block" href="#queryResults" data-toggle="tab">${_('Results')}
+  ##          <!-- ko if: result.rows() != null  -->
+  ##          (<span data-bind="text: result.rows().toLocaleString() + (dialect() == 'impala' && result.rows() == 1024 ? '+' : '')" title="${ _('Number of rows') }"></span>)
+  ##          <!-- /ko -->
+            </a>
+          </li>
+          <li data-bind="click: function() { currentQueryTab('queryChart'); }, css: {'active': currentQueryTab() == 'queryChart'}">
+            <a class="inactive-action" style="display:inline-block" href="#queryChart" data-toggle="tab">${_('Chart')}</a>
+          </li>
+          <!-- ko if: explanation -->
+          <li data-bind="click: function() { currentQueryTab('queryExplain'); }, css: {'active': currentQueryTab() == 'queryExplain'}"><a class="inactive-action" href="#queryExplain" data-toggle="tab">${_('Explain')}</a></li>
+          <!-- /ko -->
+          <!-- ko foreach: pinnedContextTabs -->
+          <li data-bind="click: function() { $parent.currentQueryTab(tabId) }, css: { 'active': $parent.currentQueryTab() === tabId }">
+            <a class="inactive-action" data-toggle="tab" data-bind="attr: { 'href': '#' + tabId }">
+              <i class="snippet-icon fa" data-bind="css: iconClass"></i> <span data-bind="text: title"></span>
+              <div class="inline-block inactive-action margin-left-10 pointer" data-bind="click: function () { $parent.removeContextTab($data); }">
+                <i class="snippet-icon fa fa-times"></i>
+              </div>
+            </a>
+          </li>
+          <!-- /ko -->
 
-        <li class="editor-bottom-tab-actions">
-          <button data-bind="toggle: $root.bottomExpanded">
-            <i class="fa" data-bind="css: { 'fa-expand': !$root.bottomExpanded(), 'fa-compress': $root.bottomExpanded() }"></i>
-          </button>
-        </li>
-      </ul>
-      <div class="editor-bottom-tab-content tab-content">
-        <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
-              }
-            } --><!-- /ko -->
+          <li data-bind="click: function(){ currentQueryTab('executionAnalysis'); }, css: {'active': currentQueryTab() == 'executionAnalysis'}">
+            <a class="inactive-action" href="#executionAnalysis" data-toggle="tab">${_('Execution Analysis')}</a>
+          </li>
+
+          <li class="editor-bottom-tab-actions">
+            <button data-bind="toggle: $root.bottomExpanded">
+              <i class="fa" data-bind="css: { 'fa-expand': !$root.bottomExpanded(), 'fa-compress': $root.bottomExpanded() }"></i>
+            </button>
+          </li>
+        </ul>
+        <div class="editor-bottom-tab-content tab-content">
+          <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
+                }
+              } --><!-- /ko -->
+            </div>
           </div>
-        </div>
 
-        <div class="tab-pane" id="savedQueries" data-bind="css: { 'active': currentQueryTab() === 'savedQueries' }" style="overflow: hidden">
-          <div class="editor-bottom-tab-panel">
-            <!-- ko component: {
-              name: 'saved-queries',
-              params: {
-                currentNotebook: parentNotebook,
-                openFunction: parentVm.openNotebook.bind(parentVm),
-                dialect: dialect,
-                currentTab: currentQueryTab
-              }
-            } --><!-- /ko -->
+          <div class="tab-pane" id="savedQueries" data-bind="css: { 'active': currentQueryTab() === 'savedQueries' }" style="overflow: hidden">
+            <div class="editor-bottom-tab-panel">
+              <!-- ko component: {
+                name: 'saved-queries',
+                params: {
+                  currentNotebook: parentNotebook,
+                  openFunction: parentVm.openNotebook.bind(parentVm),
+                  dialect: dialect,
+                  currentTab: currentQueryTab
+                }
+              } --><!-- /ko -->
+            </div>
           </div>
-        </div>
 
-        <div class="tab-pane" id="queryResults" data-bind="css: {'active': currentQueryTab() == 'queryResults'}">
-          <div class="execution-results-tab-panel">
-            <result-table-ko-bridge class="table-results-bridge" data-bind="vueKoProps: {
-                executableObservable: activeExecutable
-              }"></result-table-ko-bridge>
+          <div class="tab-pane" id="queryResults" data-bind="css: {'active': currentQueryTab() == 'queryResults'}">
+            <div class="execution-results-tab-panel">
+              <result-table-ko-bridge class="table-results-bridge" data-bind="vueKoProps: {
+                  executableObservable: activeExecutable
+                }"></result-table-ko-bridge>
+            </div>
           </div>
-        </div>
 
-        <div class="tab-pane" id="queryChart" data-bind="css: {'active': currentQueryTab() == 'queryChart'}">
-          <div class="editor-bottom-tab-panel editor-chart-panel">
-            <!-- ko component: { name: 'snippet-result-chart', params: {
-              activeExecutable: activeExecutable,
-              editorMode: parentVm.editorMode,
-              id: id,
-              isPresentationMode: parentNotebook.isPresentationMode,
-              resultsKlass: resultsKlass
-            }} --><!-- /ko -->
+          <div class="tab-pane" id="queryChart" data-bind="css: {'active': currentQueryTab() == 'queryChart'}">
+            <div class="editor-bottom-tab-panel editor-chart-panel">
+              <!-- ko component: { name: 'snippet-result-chart', params: {
+                activeExecutable: activeExecutable,
+                editorMode: parentVm.editorMode,
+                id: id,
+                isPresentationMode: parentNotebook.isPresentationMode,
+                resultsKlass: resultsKlass
+              }} --><!-- /ko -->
+            </div>
           </div>
-        </div>
 
-        <!-- ko if: explanation -->
-        <div class="tab-pane" id="queryExplain" data-bind="css: {'active': currentQueryTab() == 'queryExplain'}">
-          <div class="editor-bottom-tab-panel">
-            <div style="width: 100%; height: 100%; overflow-y: auto;">
-              <pre class="no-margin-bottom" data-bind="text: explanation"></pre>
+          <!-- ko if: explanation -->
+          <div class="tab-pane" id="queryExplain" data-bind="css: {'active': currentQueryTab() == 'queryExplain'}">
+            <div class="editor-bottom-tab-panel">
+              <div style="width: 100%; height: 100%; overflow-y: auto;">
+                <pre class="no-margin-bottom" data-bind="text: explanation"></pre>
+              </div>
             </div>
           </div>
-        </div>
-        <!-- /ko -->
+          <!-- /ko -->
 
-        <div class="tab-pane" id="executionAnalysis" data-bind="css: {'active': currentQueryTab() == 'executionAnalysis'}">
-          <div class="execution-analysis-tab-panel">
-            <execution-analysis-panel-ko-bridge class="execution-analysis-bridge" data-bind="
-              vueKoProps: {
-                executableObservable: activeExecutable
-              },
-              vueEvents: {
-                'execution-error': function () { currentQueryTab('executionAnalysis') }
-              }
-            "></execution-analysis-panel-ko-bridge>
+          <div class="tab-pane" id="executionAnalysis" data-bind="css: {'active': currentQueryTab() == 'executionAnalysis'}">
+            <div class="execution-analysis-tab-panel">
+              <execution-analysis-panel-ko-bridge class="execution-analysis-bridge" data-bind="
+                vueKoProps: {
+                  executableObservable: activeExecutable
+                },
+                vueEvents: {
+                  'execution-error': function () { currentQueryTab('executionAnalysis') }
+                }
+              "></execution-analysis-panel-ko-bridge>
+            </div>
           </div>
-        </div>
 
-        <!-- ko foreach: pinnedContextTabs -->
-        <div class="tab-pane" data-bind="attr: { 'id': tabId }, css: {'active': $parent.currentQueryTab() === tabId }">
-          <div class="editor-bottom-tab-panel">
-            <div style="display: flex; flex-direction: column; margin-top: 10px; overflow: hidden; height: 100%; position: relative;" data-bind="template: 'context-popover-contents'"></div>
+          <!-- ko foreach: pinnedContextTabs -->
+          <div class="tab-pane" data-bind="attr: { 'id': tabId }, css: {'active': $parent.currentQueryTab() === tabId }">
+            <div class="editor-bottom-tab-panel">
+              <div style="display: flex; flex-direction: column; margin-top: 10px; overflow: hidden; height: 100%; position: relative;" data-bind="template: 'context-popover-contents'"></div>
+            </div>
           </div>
+          <!-- /ko -->
         </div>
-        <!-- /ko -->
       </div>
-    </div>
-
-    <div class="hoverMsg hide">
-      <p class="hoverText">${_('Drop a SQL file here')}</p>
-    </div>
+      <div class="hoverMsg hide">
+        <p class="hoverText">${_('Drop a SQL file here')}</p>
+      </div>
+    <!-- /ko -->
   </div>
 </div>