Ver Fonte

[editor] Replace the execute actions with a Vue components in editor v2

This also introduces a skeleton SqlEditor Web Component
Johan Ahlen há 5 anos atrás
pai
commit
8486b13e8e

+ 39 - 26
desktop/core/src/desktop/js/apps/notebook2/components/ko.executableActions.test.js → desktop/core/src/desktop/js/apps/notebook2/components/ExecutableActions.test.ts

@@ -3,31 +3,37 @@
 // 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
+// '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,
+// 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 huePubSub from 'utils/huePubSub';
-import { koSetup } from 'jest/koTestUtils';
-import { NAME } from './ko.executableActions';
+import Vue from 'vue';
+import { mount, shallowMount } from '@vue/test-utils';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
-import { sleep } from 'utils/hueUtils';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
+import ExecutableActions from './ExecutableActions.vue';
 
-describe('ko.executableActions.js', () => {
-  const setup = koSetup();
+/* eslint-disable @typescript-eslint/no-empty-function */
 
-  it('should render component', async () => {
+describe('ExecutableActions.vue', () => {
+  it('should render', () => {
+    const wrapper = shallowMount(ExecutableActions);
+    expect(wrapper.element).toMatchSnapshot();
+  });
+
+  it('should show execute once the session is loaded', async () => {
     const spy = spyOn(sessionManager, 'getSession').and.returnValue(
       Promise.resolve({ type: 'foo' })
     );
+
     const mockExecutable = {
       cancel: () => {},
       cancelBatchChain: () => {},
@@ -44,14 +50,17 @@ describe('ko.executableActions.js', () => {
       },
       status: EXECUTION_STATUS.ready
     };
-    const activeExecutable = () => mockExecutable;
-    activeExecutable.prototype.subscribe = () => {};
-    const element = await setup.renderComponent(NAME, {
-      activeExecutable: activeExecutable
+
+    const wrapper = shallowMount(ExecutableActions, {
+      propsData: {
+        executable: mockExecutable
+      }
     });
 
+    await Vue.nextTick();
+
     expect(spy).toHaveBeenCalled();
-    expect(element.querySelector('[data-test="' + NAME + '"]')).toBeTruthy();
+    expect(wrapper.element).toMatchSnapshot();
   });
 
   it('should handle execute and stop clicks', async () => {
@@ -77,38 +86,42 @@ describe('ko.executableActions.js', () => {
       executor: {
         defaultLimit: () => {},
         connector: () => ({
-          id: 'foo'
+          id: 'foo',
+          type: 'foo'
         })
       },
       status: EXECUTION_STATUS.ready
     };
-    const activeExecutable = () => mockExecutable;
-    activeExecutable.prototype.subscribe = () => {};
-    const wrapper = await setup.renderComponent(NAME, {
-      activeExecutable: activeExecutable
+
+    const wrapper = mount(ExecutableActions, {
+      propsData: {
+        executable: mockExecutable
+      }
     });
 
     expect(spy).toHaveBeenCalled();
 
+    await Vue.nextTick();
+
     // Click play
     expect(executeCalled).toBeFalsy();
-    expect(wrapper.querySelector('[data-test="execute"]')).toBeTruthy();
-    expect(wrapper.querySelector('[data-test="stop"]')).toBeFalsy();
-    wrapper.querySelector('[data-test="execute"]').click();
+    expect(wrapper.get('button').text()).toContain('Execute');
+    wrapper.get('button').trigger('click');
 
-    await sleep(0);
+    await Vue.nextTick();
 
     expect(executeCalled).toBeTruthy();
     mockExecutable.status = EXECUTION_STATUS.running;
     huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
 
-    await setup.waitForKoUpdate();
+    await Vue.nextTick();
 
     // Click stop
     expect(cancelCalled).toBeFalsy();
-    expect(wrapper.querySelector('[data-test="execute"]')).toBeFalsy();
-    expect(wrapper.querySelector('[data-test="stop"]')).toBeTruthy();
-    wrapper.querySelector('[data-test="stop"]').click();
+    expect(wrapper.get('button').text()).toContain('Stop');
+    wrapper.get('button').trigger('click');
+
+    await Vue.nextTick();
 
     expect(cancelCalled).toBeTruthy();
   });

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

@@ -0,0 +1,210 @@
+<!--
+  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="snippet-execute-actions">
+    <hue-button
+      v-if="loadingSession"
+      key="loading-button"
+      small="true"
+      :disabled="disabled"
+      :title="I18n('Creating session')"
+    >
+      <i class="fa fa-fw fa-spinner fa-spin" /> {{ I18n('Loading') }}
+    </hue-button>
+
+    <hue-button
+      v-if="showExecute"
+      key="execute-button"
+      small="true"
+      primary="true"
+      :disabled="disabled"
+      @click="execute"
+    >
+      <i class="fa fa-play fa-fw" /> {{ I18n('Execute') }}
+    </hue-button>
+
+    <hue-button v-if="showStop && !stopping" key="stop-button" small="true" @click="stop">
+      <i class="fa fa-stop fa-fw" />
+      <span v-if="waiting">{{ I18n('Stop batch') }}</span>
+      <span v-else>{{ I18n('Stop') }}</span>
+    </hue-button>
+
+    <hue-button v-if="showStop && stopping" key="stopping-button" small="true">
+      <i class="fa fa-fw fa-spinner fa-spin" /> {{ I18n('Stopping') }}
+    </hue-button>
+
+    <form autocomplete="off" class="inline-block margin-left-10">
+      <input
+        v-model="limit"
+        class="input-small limit-input"
+        type="number"
+        autocorrect="off"
+        autocomplete="do-not-autocomplete"
+        autocapitalize="off"
+        spellcheck="false"
+        :placeholder="I18n('Limit')"
+        @change="$emit('limit-changed', limit)"
+      />
+    </form>
+  </div>
+</template>
+
+<script lang="ts">
+  import SqlExecutable from 'apps/notebook2/execution/sqlExecutable';
+  import HueButton from 'components/HueButton.vue';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import huePubSub from 'utils/huePubSub';
+  import I18n from 'utils/i18n';
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop, Watch } from 'vue-property-decorator';
+
+  import { Session } from 'apps/notebook2/execution/apiUtils';
+  import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+  import sessionManager from 'apps/notebook2/execution/sessionManager';
+
+  export const EXECUTE_ACTIVE_EXECUTABLE_EVENT = 'executable.active.executable';
+  const WHITE_SPACE_REGEX = /^\s*$/;
+
+  @Component({
+    components: { HueButton },
+    methods: { I18n }
+  })
+  export default class ExecutableActions extends Vue {
+    @Prop()
+    executable?: SqlExecutable;
+    @Prop()
+    beforeExecute?: () => Promise<void>;
+
+    subTracker = new SubscriptionTracker();
+
+    loadingSession = true;
+    lastSession: Session | null = null;
+    partOfRunningExecution = false;
+    limit: number | null = null;
+    stopping = false;
+    status: string = EXECUTION_STATUS.ready;
+    hasStatement = false;
+
+    mounted(): void {
+      this.subTracker.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
+        if (this.executable === executable) {
+          this.updateFromExecutable(executable);
+        }
+      });
+
+      this.subTracker.subscribe(EXECUTE_ACTIVE_EXECUTABLE_EVENT, executable => {
+        if (this.executable === executable) {
+          this.execute();
+        }
+      });
+    }
+
+    destroyed(): void {
+      this.subTracker.dispose();
+    }
+
+    get waiting(): boolean {
+      return !!(this.executable && this.executable.isReady() && this.partOfRunningExecution);
+    }
+
+    get disabled(): boolean {
+      return this.loadingSession || !this.executable || !this.hasStatement;
+    }
+
+    get showExecute(): boolean {
+      return (
+        !!this.executable &&
+        !this.waiting &&
+        !this.loadingSession &&
+        this.status !== EXECUTION_STATUS.running &&
+        this.status !== EXECUTION_STATUS.streaming
+      );
+    }
+
+    get showStop(): boolean {
+      return (
+        this.status === EXECUTION_STATUS.running ||
+        this.status === EXECUTION_STATUS.streaming ||
+        this.waiting
+      );
+    }
+
+    async execute(): Promise<void> {
+      huePubSub.publish('hue.ace.autocompleter.hide');
+      if (!this.executable) {
+        return;
+      }
+      if (this.beforeExecute) {
+        await this.beforeExecute();
+      }
+      await this.executable.reset();
+      this.executable.execute();
+    }
+
+    async stop(): Promise<void> {
+      if (this.stopping || !this.executable) {
+        return;
+      }
+      this.stopping = true;
+      await this.executable.cancelBatchChain(true);
+      this.stopping = false;
+    }
+
+    @Watch('executable', { immediate: true })
+    executableChanged(): void {
+      if (this.executable) {
+        this.updateFromExecutable(this.executable);
+      }
+    }
+
+    updateFromExecutable(executable: SqlExecutable): void {
+      const waitForSession =
+        !this.lastSession || this.lastSession.type !== executable.executor.connector().type;
+      this.status = executable.status;
+      this.hasStatement =
+        !executable.parsedStatement ||
+        !WHITE_SPACE_REGEX.test(executable.parsedStatement.statement);
+      this.partOfRunningExecution = executable.isPartOfRunningExecution();
+      this.limit = (executable.executor.defaultLimit && executable.executor.defaultLimit()) || null;
+      if (waitForSession) {
+        this.loadingSession = true;
+        this.lastSession = null;
+        sessionManager.getSession({ type: executable.executor.connector().id }).then(session => {
+          this.lastSession = session;
+          this.loadingSession = false;
+        });
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .snippet-execute-actions {
+    display: inline-block;
+
+    .limit-input {
+      border-radius: 2px;
+      height: 13px;
+      width: 50px;
+      margin: 0 5px;
+      padding: 5px 6px;
+    }
+  }
+</style>

+ 76 - 0
desktop/core/src/desktop/js/apps/notebook2/components/SqlEditor.vue

@@ -0,0 +1,76 @@
+<!--
+  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>
+    <executable-actions
+      :executable="executable"
+      :before-execute="beforeExecute"
+      @limit-changed="limitChanged"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+  import ExecutableActions from './ExecutableActions.vue';
+  import SqlExecutable from 'apps/notebook2/execution/sqlExecutable';
+  import HueButton from 'components/HueButton.vue';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import I18n from 'utils/i18n';
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+  import { wrap } from 'vue/webComponentWrapper';
+
+  @Component({
+    components: { HueButton, ExecutableActions },
+    methods: { I18n }
+  })
+  export default class SqlEditor extends Vue {
+    @Prop()
+    executableObservable?: KnockoutObservable<SqlExecutable | undefined>;
+    @Prop()
+    beforeExecute?: () => Promise<void>;
+
+    subTracker = new SubscriptionTracker();
+    initialized = false;
+    executable: SqlExecutable | null = null;
+
+    updated(): void {
+      if (!this.initialized && this.executableObservable) {
+        this.executable = this.executableObservable() || null;
+        this.subTracker.subscribe(this.executableObservable, (executable: SqlExecutable) => {
+          this.executable = executable;
+        });
+      }
+    }
+
+    destroyed(): void {
+      this.subTracker.dispose();
+    }
+
+    limitChanged(limit: number): void {
+      if (this.executable && this.executable.executor.defaultLimit) {
+        this.executable.executor.defaultLimit(limit);
+      }
+    }
+  }
+
+  export const COMPONENT_NAME = 'sql-editor';
+  wrap(COMPONENT_NAME, SqlEditor);
+</script>

+ 78 - 0
desktop/core/src/desktop/js/apps/notebook2/components/__snapshots__/ExecutableActions.test.ts.snap

@@ -0,0 +1,78 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ExecutableActions.vue should render 1`] = `
+<div
+  class="snippet-execute-actions"
+>
+  <hue-button-stub
+    disabled="true"
+    small="true"
+    title="Creating session"
+  >
+    <i
+      class="fa fa-fw fa-spinner fa-spin"
+    />
+     Loading
+  
+  </hue-button-stub>
+   
+  <!---->
+   
+  <!---->
+   
+  <!---->
+   
+  <form
+    autocomplete="off"
+    class="inline-block margin-left-10"
+  >
+    <input
+      autocapitalize="off"
+      autocomplete="do-not-autocomplete"
+      autocorrect="off"
+      class="input-small limit-input"
+      placeholder="Limit"
+      spellcheck="false"
+      type="number"
+    />
+  </form>
+</div>
+`;
+
+exports[`ExecutableActions.vue should show execute once the session is loaded 1`] = `
+<div
+  class="snippet-execute-actions"
+>
+  <!---->
+   
+  <hue-button-stub
+    primary="true"
+    small="true"
+  >
+    <i
+      class="fa fa-play fa-fw"
+    />
+     Execute
+  
+  </hue-button-stub>
+   
+  <!---->
+   
+  <!---->
+   
+  <form
+    autocomplete="off"
+    class="inline-block margin-left-10"
+  >
+    <input
+      autocapitalize="off"
+      autocomplete="do-not-autocomplete"
+      autocorrect="off"
+      class="input-small limit-input"
+      placeholder="Limit"
+      spellcheck="false"
+      type="number"
+    />
+  </form>
+</div>
+`;

+ 0 - 179
desktop/core/src/desktop/js/apps/notebook2/components/ko.executableActions.js

@@ -1,179 +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 * as ko from 'knockout';
-
-import componentUtils from 'ko/components/componentUtils';
-import DisposableComponent from 'ko/components/DisposableComponent';
-import huePubSub from 'utils/huePubSub';
-import I18n from 'utils/i18n';
-import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
-import sessionManager from 'apps/notebook2/execution/sessionManager';
-
-export const NAME = 'executable-actions';
-
-export const EXECUTE_ACTIVE_EXECUTABLE_EVENT = 'executable.active.executable';
-
-// prettier-ignore
-const TEMPLATE = `
-<div class="snippet-execute-actions" data-test="${ NAME }">
-  <div class="btn-group">
-    <!-- ko if: showLoading -->
-    <button class="btn btn-primary btn-mini btn-execute disable-feedback" disabled title="${ I18n('Creating session') }">
-      <i class="fa fa-fw fa-spinner fa-spin"></i> ${ I18n('Loading') }
-    </button>
-    <!-- /ko -->
-    <!-- ko if: showExecute -->
-    <button class="btn btn-primary btn-mini btn-execute disable-feedback" data-test="execute" data-bind="click: execute, disable: disabled">
-      <i class="fa fa-fw fa-play"></i> ${ I18n('Execute') }
-    </button>
-    <!-- /ko -->
-    <!-- ko if: showStop -->
-      <!-- ko ifnot: stopping -->
-      <button class="btn btn-danger btn-mini btn-execute disable-feedback" data-test="stop" data-bind="click: stop">
-        <i class="fa fa-stop fa-fw"></i>
-        <!-- ko ifnot: waiting -->
-          ${ I18n('Stop') }
-        <!-- /ko -->
-        <!-- ko if: waiting -->
-          ${ I18n('Stop batch') }
-        <!-- /ko -->
-      </button>
-      <!-- /ko -->
-      <!-- ko if: stopping -->
-      <button class="btn btn-primary btn-mini btn-execute disable-feedback disabled">
-        <i class="fa fa-fw fa-spinner fa-spin"></i> ${ I18n('Stopping') }
-      </button>
-      <!-- /ko -->
-    <!-- /ko -->
-  </div>
-  <form autocomplete="off" class="inline-block margin-left-10">
-    <input class="input-small limit-input" type="number" ${ window.PREVENT_AUTOFILL_INPUT_ATTRS } placeholder="${ I18n('Limit') }" data-bind="textInput: limit, autogrowInput: { minWidth: 50, maxWidth: 80, comfortZone: 25 }">
-  </form>
-</div>
-`;
-
-const WHITE_SPACE_REGEX = /^\s*$/;
-
-class ExecutableActions extends DisposableComponent {
-  constructor(params) {
-    super();
-    this.stopping = ko.observable(false);
-    this.activeExecutable = params.activeExecutable;
-    this.status = ko.observable(EXECUTION_STATUS.ready);
-    this.partOfRunningExecution = ko.observable(false);
-    this.beforeExecute = params.beforeExecute;
-
-    this.lastSession = ko.observable();
-
-    this.limit = ko.observable();
-
-    this.subscribe(this.limit, newVal => {
-      if (this.activeExecutable()) {
-        this.activeExecutable().executor.defaultLimit(newVal);
-      }
-    });
-
-    this.waiting = ko.pureComputed(
-      () =>
-        this.activeExecutable() &&
-        this.activeExecutable().isReady() &&
-        this.partOfRunningExecution()
-    );
-
-    this.showLoading = ko.pureComputed(() => !this.lastSession());
-
-    this.showExecute = ko.pureComputed(
-      () =>
-        !this.showLoading() &&
-        this.status() !== EXECUTION_STATUS.running &&
-        this.status() !== EXECUTION_STATUS.streaming &&
-        !this.waiting()
-    );
-
-    this.showStop = ko.pureComputed(
-      () =>
-        this.status() === EXECUTION_STATUS.running ||
-        this.status() === EXECUTION_STATUS.streaming ||
-        this.waiting()
-    );
-
-    this.disabled = ko.pureComputed(() => {
-      const executable = this.activeExecutable();
-
-      return (
-        !this.lastSession() ||
-        !executable ||
-        (executable.parsedStatement && WHITE_SPACE_REGEX.test(executable.parsedStatement.statement))
-      );
-    });
-
-    this.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
-      if (this.activeExecutable() === executable) {
-        this.updateFromExecutable(executable);
-      }
-    });
-
-    this.subscribe(EXECUTE_ACTIVE_EXECUTABLE_EVENT, executable => {
-      if (this.activeExecutable() === executable) {
-        this.execute();
-      }
-    });
-
-    this.subscribe(this.activeExecutable, this.updateFromExecutable.bind(this));
-
-    if (this.activeExecutable()) {
-      this.updateFromExecutable(this.activeExecutable());
-    }
-  }
-
-  updateFromExecutable(executable) {
-    const waitForSession =
-      !this.lastSession() || this.lastSession().type !== executable.executor.connector().type;
-    this.status(executable.status);
-    this.partOfRunningExecution(executable.isPartOfRunningExecution());
-    this.limit(executable.executor.defaultLimit());
-    if (waitForSession) {
-      this.lastSession(undefined);
-      sessionManager
-        .getSession({ type: executable.executor.connector().id })
-        .then(this.lastSession);
-    }
-  }
-
-  async stop() {
-    if (this.stopping() || !this.activeExecutable()) {
-      return;
-    }
-    this.stopping(true);
-    await this.activeExecutable().cancelBatchChain(true);
-    this.stopping(false);
-  }
-
-  async execute() {
-    huePubSub.publish('hue.ace.autocompleter.hide');
-    if (this.beforeExecute) {
-      await this.beforeExecute();
-    }
-    const executable = this.activeExecutable();
-    if (executable) {
-      await executable.reset();
-      executable.execute();
-    }
-  }
-}
-
-componentUtils.registerComponent(NAME, ExecutableActions, TEMPLATE);

+ 2 - 2
desktop/core/src/desktop/js/apps/notebook2/execution/executor.ts

@@ -30,7 +30,7 @@ export default class Executor {
   compute: () => Compute;
   namespace: () => Namespace;
   database: () => string;
-  defaultLimit?: () => number;
+  defaultLimit?: KnockoutObservable<number>;
   isSqlEngine?: boolean;
   isOptimizerEnabled?: boolean;
   executables: Executable[] = [];
@@ -42,7 +42,7 @@ export default class Executor {
     compute: () => Compute;
     namespace: () => Namespace;
     database: () => string;
-    defaultLimit?: () => number;
+    defaultLimit?: KnockoutObservable<number>;
     isSqlEngine?: boolean;
     snippet?: Snippet;
     isOptimizerEnabled?: boolean;

+ 3 - 2
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -19,13 +19,14 @@ import * as ko from 'knockout';
 import komapping from 'knockout.mapping';
 import { markdown } from 'markdown';
 
-import 'apps/notebook2/components/ko.executableActions';
 import 'apps/notebook2/components/ko.executableLogs';
 import 'apps/notebook2/components/ko.executableProgressBar';
 import 'apps/notebook2/components/ko.snippetEditorActions';
 import 'apps/notebook2/components/ko.snippetResults';
 import 'apps/notebook2/components/ko.queryHistory';
 
+import './components/SqlEditor.vue';
+
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
 import apiHelper from 'api/apiHelper';
 import Executor from 'apps/notebook2/execution/executor';
@@ -44,7 +45,7 @@ import {
   ACTIVE_STATEMENT_CHANGED_EVENT,
   REFRESH_STATEMENT_LOCATIONS_EVENT
 } from 'ko/bindings/ace/aceLocationHandler';
-import { EXECUTE_ACTIVE_EXECUTABLE_EVENT } from 'apps/notebook2/components/ko.executableActions';
+import { EXECUTE_ACTIVE_EXECUTABLE_EVENT } from './components/ExecutableActions.vue';
 import { ADD_TO_HISTORY_EVENT } from 'apps/notebook2/components/ko.queryHistory';
 import { findEditorConnector, getLastKnownConfig } from 'utils/hueConfig';
 import { cancelActiveRequest } from 'api/apiUtils';

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

@@ -42,5 +42,7 @@
 </script>
 
 <style lang="scss" scoped>
-  // TODO: Scope the button styles.
+  .btn {
+    border-radius: 3px;
+  }
 </style>

+ 52 - 0
desktop/core/src/desktop/js/components/utils/SubscriptionTracker.ts

@@ -0,0 +1,52 @@
+// 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 huePubSub from 'utils/huePubSub';
+
+export default class SubscriptionTracker {
+  disposals: (() => void)[] = [];
+
+  subscribe(
+    subscribable: string | KnockoutSubscribable<unknown>,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    callback: (...args: any[]) => any
+  ): void {
+    if (typeof subscribable === 'string') {
+      const pubSub = huePubSub.subscribe(subscribable, callback);
+      this.disposals.push(() => {
+        pubSub.remove();
+      });
+    } else if (subscribable.subscribe) {
+      const sub = subscribable.subscribe(callback);
+      this.disposals.push(() => {
+        sub.dispose();
+      });
+    }
+  }
+
+  dispose(): void {
+    while (this.disposals.length) {
+      try {
+        const disposeFn = this.disposals.pop();
+        if (disposeFn) {
+          disposeFn();
+        }
+      } catch (err) {
+        console.warn(err);
+      }
+    }
+  }
+}

+ 7 - 0
desktop/core/src/desktop/js/ko/bindings/ko.vue.js

@@ -16,6 +16,13 @@
 
 import * as ko from 'knockout';
 
+ko.bindingHandlers.vueKoProps = {
+  init: (element, valueAccessor) => {
+    const data = valueAccessor();
+    Object.assign(element, data);
+  }
+};
+
 ko.bindingHandlers.vueProps = {
   init: (element, valueAccessor) => {
     const data = valueAccessor();

+ 5 - 7
desktop/core/src/desktop/js/ko/components/contextPopover/ko.quickQueryContext.js

@@ -19,9 +19,10 @@ import * as ko from 'knockout';
 import { MULTI_NAME as SIMPLE_ACE_MULTI } from 'ko/components/simpleAceEditor/ko.simpleAceEditor';
 import { CONTEXT_SELECTOR_COMPONENT } from 'ko/components/ko.contextSelector';
 import { HUE_DROP_DOWN_COMPONENT } from 'ko/components/ko.dropDown';
-import { NAME as EXECUTABLE_ACTIONS } from 'apps/notebook2/components/ko.executableActions';
 import { SIMPLE_RESULT_GRID_COMPONENT } from 'apps/notebook2/components/resultGrid/ko.simpleResultGrid';
 
+import 'apps/notebook2/components/SqlEditor.vue';
+
 import componentUtils from 'ko/components/componentUtils';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import Executor from 'apps/notebook2/execution/executor';
@@ -83,12 +84,9 @@ const TEMPLATE = `
           }
         }
       "></div>
-      <div data-bind="
-        component: {
-          name: '${ EXECUTABLE_ACTIONS }',
-          params: { activeExecutable: $parent.activeExecutable }
-        }
-      "></div>
+      <sql-editor data-bind="vueKoProps: {
+          executableObservable: $parent.activeExecutable
+        }"></sql-editor>
       <div data-bind="
         component: {
           name: '${ SIMPLE_RESULT_GRID_COMPONENT }',

+ 1 - 1
desktop/core/src/desktop/js/sql/reference/apiUtils.ts

@@ -17,7 +17,7 @@
 import { simplePostAsync } from 'api/apiUtils';
 import { AUTOCOMPLETE_API_PREFIX } from 'api/urls';
 import { UdfArgument, UdfDetails } from 'sql/reference/types';
-import { Connector } from 'types/types';
+import { Connector } from 'types/config';
 import I18n from 'utils/i18n';
 
 export interface ApiUdf {

+ 6 - 1
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -1237,7 +1237,12 @@
 
   <script type ="text/html" id="snippet-execution-controls${ suffix }">
     <div class="snippet-actions clearfix">
-      <div class="pull-left" data-bind="component: { name: 'executable-actions', params: { activeExecutable: activeExecutable, beforeExecute: beforeExecute } }"></div>
+      <div class="pull-left">
+        <sql-editor data-bind="vueKoProps: {
+          executableObservable: activeExecutable,
+          beforeExecute: beforeExecute
+        }"></sql-editor>
+      </div>
       <!-- ko if: isSqlDialect() && !$root.isPresentationMode() -->
       <div class="pull-right" data-bind="component: { name: 'snippet-editor-actions', params: { snippet: $data } }"></div>
       <!-- /ko -->