Sfoglia il codice sorgente

[editor] Create an ExecutableProgressBar Vue component

Johan Ahlen 4 anni fa
parent
commit
aa8fc560e8

+ 83 - 0
desktop/core/src/desktop/js/apps/editor/components/ExecutableProgressBar.test.ts

@@ -0,0 +1,83 @@
+// 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 { nextTick } from 'vue';
+import { shallowMount, mount } from '@vue/test-utils';
+import Executable, {
+  EXECUTABLE_UPDATED_EVENT,
+  ExecutionStatus
+} from 'apps/editor/execution/executable';
+import ExecutableProgressBar from './ExecutableProgressBar.vue';
+import huePubSub from 'utils/huePubSub';
+
+describe('ExecutableProgressBar.vue', () => {
+  it('should render', () => {
+    const wrapper = shallowMount(ExecutableProgressBar);
+    expect(wrapper.element).toMatchSnapshot();
+  });
+
+  it('should be 100% and have .progress-danger when failed', async () => {
+    const mockExecutable = {
+      id: 'some-id',
+      progress: 0,
+      status: ExecutionStatus.ready
+    };
+
+    const { element } = mount(ExecutableProgressBar, {
+      propsData: {
+        executable: <Executable>mockExecutable
+      }
+    });
+
+    const progressDiv = element.querySelector('.executable-progress-bar') as HTMLElement;
+    expect(progressDiv).toBeTruthy();
+    expect(progressDiv.style['width']).toEqual('2%');
+    expect(element.querySelector('.executable-progress-bar.progress-failed')).toBeFalsy();
+
+    mockExecutable.status = ExecutionStatus.failed;
+    mockExecutable.progress = 10;
+    huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
+    await nextTick();
+
+    expect(progressDiv.style['width']).toEqual('100%');
+    expect(element.querySelector('.executable-progress-bar.progress-failed')).toBeTruthy();
+  });
+
+  it('should reflect progress updates', async () => {
+    const mockExecutable = {
+      status: ExecutionStatus.ready,
+      progress: 0
+    };
+
+    const { element } = mount(ExecutableProgressBar, {
+      propsData: {
+        executable: <Executable>mockExecutable
+      }
+    });
+
+    // Progress should be 2% initially
+    const progressDiv = element.querySelector('.executable-progress-bar') as HTMLElement;
+    expect(progressDiv).toBeTruthy();
+    expect(progressDiv.style['width']).toEqual('2%');
+
+    mockExecutable.status = ExecutionStatus.running;
+    mockExecutable.progress = 10;
+    huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
+    await nextTick();
+
+    expect(progressDiv.style['width']).toEqual('10%');
+  });
+});

+ 165 - 0
desktop/core/src/desktop/js/apps/editor/components/ExecutableProgressBar.vue

@@ -0,0 +1,165 @@
+<!--
+  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="executable-progress-container">
+    <div v-if="visible" class="executable-progress">
+      <div
+        class="executable-progress-bar"
+        :class="progressClass"
+        :style="{ width: progressBarWidth, height: progressBarHeight }"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType, ref, toRefs, watch } from 'vue';
+
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import Executable, { EXECUTABLE_UPDATED_EVENT, ExecutionStatus } from '../execution/executable';
+
+  export default defineComponent({
+    name: 'ExecutableProgressBar',
+    props: {
+      executable: {
+        type: Object as PropType<Executable>,
+        default: undefined
+      }
+    },
+    setup(props) {
+      const subTracker = new SubscriptionTracker();
+      const { executable } = toRefs(props);
+
+      const progress = ref(0);
+      const status = ref<ExecutionStatus>(ExecutionStatus.ready);
+      const progressBarHeight = ref('100%');
+
+      let hideTimeout = -1;
+
+      const updateFromExecutable = (updated?: Executable) => {
+        window.clearTimeout(hideTimeout);
+        progress.value = (updated && updated.progress) || 0;
+        status.value = (updated && updated.status) || ExecutionStatus.ready;
+        if (progress.value === 100) {
+          hideTimeout = window.setTimeout(() => {
+            progressBarHeight.value = '0';
+          }, 2000);
+        } else {
+          progressBarHeight.value = '100%';
+        }
+      };
+
+      watch(
+        executable,
+        newVal => {
+          updateFromExecutable(newVal as Executable);
+        },
+        { immediate: true }
+      );
+
+      subTracker.subscribe(EXECUTABLE_UPDATED_EVENT, updated => {
+        if (executable.value && executable.value.id === updated.id) {
+          updateFromExecutable(updated);
+        }
+      });
+
+      return { subTracker, progress, status, progressBarHeight };
+    },
+    computed: {
+      visible(): boolean {
+        return this.status !== ExecutionStatus.canceled;
+      },
+      progressBarWidth(): string {
+        return this.status === ExecutionStatus.failed ? '100%' : `${Math.max(2, this.progress)}%`;
+      },
+      progressClass(): string {
+        if (this.status === ExecutionStatus.failed) {
+          return 'progress-failed';
+        }
+        if (
+          this.progress === 0 &&
+          (this.status === ExecutionStatus.running ||
+            this.status === ExecutionStatus.streaming ||
+            this.status === ExecutionStatus.starting)
+        ) {
+          return 'progress-starting';
+        }
+        if (0 < this.progress && this.progress < 100) {
+          return 'progress-running';
+        }
+        if (this.progress === 100) {
+          return 'progress-success';
+        }
+        return '';
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import '../../../components/styles/colors.scss';
+  @import '../../../components/styles/mixins.scss';
+
+  .executable-progress-container {
+    height: 3px;
+    overflow: hidden;
+    margin-bottom: 2px;
+    padding: 0 5px;
+
+    .executable-progress {
+      width: 100%;
+      height: 100%;
+      position: relative;
+
+      .executable-progress-bar {
+        background-color: $fluid-white;
+        @include ease-transition(height);
+
+        @include keyframes(pulsate) {
+          0% {
+            margin-left: 0;
+          }
+          50% {
+            margin-left: 30px;
+          }
+          100% {
+            margin-left: 0;
+          }
+        }
+
+        &.progress-starting {
+          background-color: $hue-primary-color-dark;
+          @include animation('pulsate 1s infinite');
+        }
+
+        &.progress-running {
+          background-color: $hue-primary-color-dark;
+        }
+
+        &.progress-success {
+          background-color: $fluid-green-400;
+        }
+
+        &.progress-failed {
+          background-color: $fluid-red-400;
+        }
+      }
+    }
+  }
+</style>

+ 60 - 0
desktop/core/src/desktop/js/apps/editor/components/ExecutableProgressBarKoBridge.vue

@@ -0,0 +1,60 @@
+<!--
+  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>
+  <ExecutableProgressBar :executable="executable" />
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType, ref, toRefs } from 'vue';
+
+  import ExecutableProgressBar from './ExecutableProgressBar.vue';
+  import SqlExecutable from 'apps/editor/execution/sqlExecutable';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import { wrap } from 'vue/webComponentWrap';
+
+  const ExecutableProgressBarKoBridge = defineComponent({
+    name: 'ExecutableProgressBarKoBridge',
+    components: {
+      ExecutableProgressBar
+    },
+    props: {
+      executableObservable: {
+        type: Object as PropType<KnockoutObservable<SqlExecutable | undefined>>,
+        default: undefined
+      }
+    },
+    setup(props) {
+      const subTracker = new SubscriptionTracker();
+      const { executableObservable } = toRefs(props);
+
+      const executable = ref<SqlExecutable | null>(null);
+
+      subTracker.trackObservable(executableObservable, executable);
+
+      return {
+        executable
+      };
+    }
+  });
+
+  export const COMPONENT_NAME = 'executable-progress-bar-ko-bridge';
+  wrap(COMPONENT_NAME, ExecutableProgressBarKoBridge);
+
+  export default ExecutableProgressBarKoBridge;
+</script>

+ 16 - 0
desktop/core/src/desktop/js/apps/editor/components/__snapshots__/ExecutableProgressBar.test.ts.snap

@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ExecutableProgressBar.vue should render 1`] = `
+<div
+  class="executable-progress-container"
+>
+  <div
+    class="executable-progress"
+  >
+    <div
+      class="executable-progress-bar"
+      style="width: 2%; height: 100%;"
+    />
+  </div>
+</div>
+`;

+ 0 - 102
desktop/core/src/desktop/js/apps/editor/components/ko.executableProgressBar.js

@@ -1,102 +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 'ko/bindings/ko.publish';
-
-import componentUtils from 'ko/components/componentUtils';
-import { EXECUTABLE_UPDATED_EVENT, ExecutionStatus } from 'apps/editor/execution/executable';
-import DisposableComponent from 'ko/components/DisposableComponent';
-import { sleep } from 'utils/hueUtils';
-
-export const NAME = 'executable-progress-bar';
-
-const TEMPLATE = `
-<div class="progress-snippet progress" data-test="${NAME}" data-bind="css: progressClass" style="background-color: #FFF;">
-  <div class="bar" data-test="bar" data-bind="style: { 'width': progressWidth }"></div>
-</div>
-`;
-
-class ExecutableProgressBar extends DisposableComponent {
-  constructor(params, element) {
-    super();
-    this.activeExecutable = params.activeExecutable;
-
-    this.status = ko.observable();
-    this.progress = ko.observable(0);
-
-    this.progressClass = ko.pureComputed(() => {
-      if (this.status() === ExecutionStatus.failed) {
-        return 'progress-danger';
-      }
-      if (
-        this.progress() === 0 &&
-        (this.status() === ExecutionStatus.running ||
-          this.status() === ExecutionStatus.streaming ||
-          this.status() === ExecutionStatus.starting)
-      ) {
-        return 'progress-starting';
-      }
-      if (0 < this.progress() && this.progress() < 100) {
-        return 'progress-running';
-      }
-      if (this.progress() === 100) {
-        return 'progress-success';
-      }
-    });
-
-    this.progressWidth = ko.pureComputed(() => {
-      if (this.status() === ExecutionStatus.failed) {
-        return '100%';
-      }
-      return Math.max(2, this.progress()) + '%';
-    });
-
-    this.subscribe(EXECUTABLE_UPDATED_EVENT, async executable => {
-      if (this.activeExecutable() === executable) {
-        this.status(executable.status);
-        this.progress(executable.progress);
-        if (executable.progress === 100) {
-          await sleep(2000);
-          $(element).parent().find('.progress-snippet').animate(
-            {
-              height: '0'
-            },
-            100
-          );
-        } else {
-          $(element).parent().find('.progress-snippet').css('height', '');
-        }
-      }
-    });
-
-    this.subscribe(this.activeExecutable, executable => {
-      this.status(executable.status);
-      this.progress(executable.progress);
-    });
-  }
-}
-
-componentUtils.registerComponent(
-  NAME,
-  {
-    createViewModel: (params, componentInfo) =>
-      new ExecutableProgressBar(params, componentInfo.element)
-  },
-  TEMPLATE
-);

+ 0 - 84
desktop/core/src/desktop/js/apps/editor/components/ko.executableProgressBar.test.js

@@ -1,84 +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 huePubSub from 'utils/huePubSub';
-import { koSetup } from 'jest/koTestUtils';
-import { NAME } from './ko.executableProgressBar';
-import { EXECUTABLE_UPDATED_EVENT, ExecutionStatus } from 'apps/editor/execution/executable';
-
-describe('ko.executableProgressBar.js', () => {
-  const setup = koSetup();
-
-  it('should render component', async () => {
-    const mockExecutable = {
-      status: ExecutionStatus.ready,
-      progress: 0
-    };
-    const activeExecutable = () => mockExecutable;
-    activeExecutable.prototype.subscribe = () => {};
-    const element = await setup.renderComponent(NAME, {
-      activeExecutable: activeExecutable
-    });
-
-    expect(element.querySelector('[data-test="' + NAME + '"]')).toBeTruthy();
-  });
-
-  it('should reflect progress updates', async () => {
-    const mockExecutable = {
-      status: ExecutionStatus.ready,
-      progress: 0
-    };
-    const activeExecutable = () => mockExecutable;
-    activeExecutable.prototype.subscribe = () => {};
-
-    const wrapper = await setup.renderComponent(NAME, {
-      activeExecutable: activeExecutable
-    });
-
-    // Progress should be 2% initially
-    expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('2%');
-
-    mockExecutable.status = ExecutionStatus.running;
-    mockExecutable.progress = 10;
-    huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
-    await setup.waitForKoUpdate();
-
-    expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('10%');
-  });
-
-  it('should be 100% and have .progress-danger when failed', async () => {
-    const mockExecutable = {
-      status: ExecutionStatus.ready,
-      progress: 0
-    };
-    const activeExecutable = () => mockExecutable;
-    activeExecutable.prototype.subscribe = () => {};
-    const wrapper = await setup.renderComponent(NAME, {
-      activeExecutable: activeExecutable
-    });
-
-    expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('2%');
-    expect(wrapper.querySelector('[data-test="' + NAME + '"].progress-danger')).toBeFalsy();
-
-    mockExecutable.status = ExecutionStatus.failed;
-    mockExecutable.progress = 10;
-    huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
-    await setup.waitForKoUpdate();
-
-    expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('100%');
-    expect(wrapper.querySelector('[data-test="' + NAME + '"].progress-danger')).toBeTruthy();
-  });
-});

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

@@ -20,12 +20,12 @@ import komapping from 'knockout.mapping';
 import { markdown } from 'markdown';
 
 import 'apps/editor/components/ko.executableLogs';
-import 'apps/editor/components/ko.executableProgressBar';
 import 'apps/editor/components/ko.snippetEditorActions';
 import 'apps/editor/components/resultChart/ko.resultChart';
 import 'apps/editor/components/ko.queryHistory';
 
 import './components/ExecutableActionsKoBridge.vue';
+import './components/ExecutableProgressBarKoBridge.vue';
 import './components/EditorResizerKoBridge.vue';
 import './components/aceEditor/AceEditorKoBridge.vue';
 import './components/executionAnalysis/ExecutionAnalysisPanelKoBridge.vue';

+ 4 - 1
desktop/libs/notebook/src/notebook/templates/editor2.mako

@@ -912,7 +912,10 @@
           "></variable-substitution-ko-bridge>
   ##      <!-- ko template: { name: 'editor-executable-snippet-body' } --><!-- /ko -->
         <div class="editor-execute-status">
-          <!-- ko template: { name: 'editor-snippet-execution-status' } --><!-- /ko -->
+          <executable-progress-bar-ko-bridge data-bind="vueKoProps: {
+            'executable-observable': activeExecutable
+          }"></executable-progress-bar-ko-bridge>
+  ##      <!-- ko template: { name: 'editor-snippet-execution-status' } --><!-- /ko -->
         </div>
         <div class="editor-execute-actions">
           <!-- ko template: { name: 'editor-execution-controls' } --><!-- /ko -->