Pārlūkot izejas kodu

HUE-9004 [editor] Migrate status bar to new executor for notebook 2

Johan Ahlen 6 gadi atpakaļ
vecāks
revīzija
74881f6fdb

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

@@ -0,0 +1,81 @@
+// 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 ko from 'knockout';
+
+import 'ko/bindings/ko.publish';
+
+import componentUtils from 'ko/components/componentUtils';
+import { EXECUTION_STATUS } from 'apps/notebook2/execution/executableStatement';
+import huePubSub from 'utils/huePubSub';
+import { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
+import DisposableComponent from 'ko/components/DisposableComponent';
+
+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) {
+    super();
+    this.snippet = params.snippet;
+
+    this.status = ko.observable();
+    this.progress = ko.observable(0);
+
+    this.progressClass = ko.pureComputed(() => {
+      if (this.status() === EXECUTION_STATUS.failed) {
+        return 'progress-danger';
+      }
+      if (
+        this.progress() === 0 &&
+        (this.status() === EXECUTION_STATUS.running || this.status() === EXECUTION_STATUS.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() === EXECUTION_STATUS.failed) {
+        return '100%';
+      }
+      return Math.max(2, this.progress()) + '%';
+    });
+
+    const updateSub = huePubSub.subscribe(EXECUTOR_UPDATED_EVENT, executorUpdate => {
+      if (this.snippet.executor() === executorUpdate.executor) {
+        this.status(executorUpdate.executable.status);
+        this.progress(executorUpdate.executable.progress);
+      }
+    });
+
+    this.addDisposalCallback(() => {
+      updateSub.remove();
+    });
+  }
+}
+
+componentUtils.registerComponent(NAME, ExecutableProgressBar, TEMPLATE);

+ 4 - 4
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetExecuteActions.js

@@ -25,16 +25,16 @@ import { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
 export const NAME = 'snippet-execute-actions';
 
 const TEMPLATE = `
-<div class="snippet-execute-actions">
+<div class="snippet-execute-actions" data-test="${NAME}">
   <div class="btn-group">
     <!-- ko if: status() !== '${EXECUTION_STATUS.running}' -->
-    <button class="btn btn-primary btn-mini btn-execute" data-bind="click: execute"><i class="fa fa-play fa-fw"></i> ${I18n(
+    <button class="btn btn-primary btn-mini btn-execute" data-test="execute" data-bind="click: execute"><i class="fa fa-play fa-fw"></i> ${I18n(
       'Execute'
     )}</button>
     <!-- /ko -->
     <!-- ko if: status() === '${EXECUTION_STATUS.running}' -->
     <!-- ko ifnot: stopping -->
-    <button class="btn btn-primary btn-mini btn-execute" data-bind="click: stop"><i class="fa fa-stop fa-fw"></i> ${I18n(
+    <button class="btn btn-primary btn-mini btn-execute" data-test="stop" data-bind="click: stop"><i class="fa fa-stop fa-fw"></i> ${I18n(
       'Stop'
     )}</button>
     <!-- /ko -->
@@ -54,7 +54,7 @@ class SnippetExecuteActions {
     this.stopping = ko.observable(false);
     this.snippet = params.snippet;
     this.status = ko.observable(EXECUTION_STATUS.ready);
-    const updateSub = huePubSub.subscribe('hue.executor.updated', executorUpdate => {
+    const updateSub = huePubSub.subscribe(EXECUTOR_UPDATED_EVENT, executorUpdate => {
       if (this.snippet.executor() === executorUpdate.executor) {
         this.status(executorUpdate.executable.status);
       }

+ 64 - 0
desktop/core/src/desktop/js/apps/notebook2/components/spec/ko.executableProgressBar.spec.js

@@ -0,0 +1,64 @@
+import huePubSub from 'utils/huePubSub';
+import { koSetup } from 'spec/jasmineSetup';
+import { NAME } from '../ko.executableProgressBar';
+import { EXECUTION_STATUS } from 'apps/notebook2/execution/executableStatement';
+import { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
+
+describe('ko.executableProgressBar.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    const element = await setup.renderComponent(NAME, {});
+
+    expect(element.querySelector('[data-test="' + NAME + '"]')).toBeTruthy();
+  });
+
+  it('should reflect progress updates', async () => {
+    const mockExecutor = {};
+    const snippet = {
+      executor: () => mockExecutor
+    };
+    const wrapper = await setup.renderComponent(NAME, {
+      snippet: snippet
+    });
+
+    // Progress should be 2% initially
+    expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('2%');
+
+    huePubSub.publish(EXECUTOR_UPDATED_EVENT, {
+      executor: mockExecutor,
+      executable: {
+        status: EXECUTION_STATUS.running,
+        progress: 10
+      }
+    });
+    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 mockExecutor = {};
+    const snippet = {
+      executor: () => mockExecutor
+    };
+    const wrapper = await setup.renderComponent(NAME, {
+      snippet: snippet
+    });
+
+    expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('2%');
+    expect(wrapper.querySelector('[data-test="' + NAME + '"].progress-danger')).toBeFalsy();
+
+    huePubSub.publish(EXECUTOR_UPDATED_EVENT, {
+      executor: mockExecutor,
+      executable: {
+        status: EXECUTION_STATUS.failed,
+        progress: 10
+      }
+    });
+    await setup.waitForKoUpdate();
+
+    expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('100%');
+    expect(wrapper.querySelector('[data-test="' + NAME + '"].progress-danger')).toBeTruthy();
+  });
+});

+ 8 - 8
desktop/core/src/desktop/js/apps/notebook2/components/spec/ko.snippetExecuteActions.spec.js

@@ -10,10 +10,10 @@ describe('ko.snippetExecuteActions.js', () => {
   it('should render component', async () => {
     const element = await setup.renderComponent(NAME, {});
 
-    expect(element.querySelector('.snippet-execute-actions')).toBeTruthy();
+    expect(element.querySelector('[data-test="' + NAME + '"]')).toBeTruthy();
   });
 
-  it('should handle play and stop clicks', async () => {
+  it('should handle execute and stop clicks', async () => {
     let executeCalled = false;
     let cancelCalled = false;
     const mockExecutor = {
@@ -33,9 +33,9 @@ describe('ko.snippetExecuteActions.js', () => {
 
     // Click play
     expect(executeCalled).toBeFalsy();
-    expect(wrapper.querySelector('.fa-play')).toBeTruthy();
-    expect(wrapper.querySelector('.fa-stop')).toBeFalsy();
-    wrapper.querySelector('.fa-play').click();
+    expect(wrapper.querySelector('[data-test="execute"]')).toBeTruthy();
+    expect(wrapper.querySelector('[data-test="stop"]')).toBeFalsy();
+    wrapper.querySelector('[data-test="execute"]').click();
 
     expect(executeCalled).toBeTruthy();
     huePubSub.publish(EXECUTOR_UPDATED_EVENT, {
@@ -49,9 +49,9 @@ describe('ko.snippetExecuteActions.js', () => {
 
     // Click stop
     expect(cancelCalled).toBeFalsy();
-    expect(wrapper.querySelector('.fa-play')).toBeFalsy();
-    expect(wrapper.querySelector('.fa-stop')).toBeTruthy();
-    wrapper.querySelector('.fa-stop').click();
+    expect(wrapper.querySelector('[data-test="execute"]')).toBeFalsy();
+    expect(wrapper.querySelector('[data-test="stop"]')).toBeTruthy();
+    wrapper.querySelector('[data-test="stop"]').click();
 
     expect(cancelCalled).toBeTruthy();
   });

+ 4 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/executableStatement.js

@@ -25,6 +25,7 @@ import sessionManager from 'apps/notebook2/execution/sessionManager';
  */
 export const EXECUTION_STATUS = {
   available: 'available',
+  failed: 'failed',
   success: 'success',
   expired: 'expired',
   running: 'running',
@@ -81,6 +82,7 @@ export class ExecutableStatement {
       let statusCheckCount = 0;
       let checkStatusTimeout = -1;
 
+      // TODO: Switch to async/await when we have cancellable Promise (not $.deferred)
       const checkStatus = () =>
         new Promise((statusResolve, statusReject) => {
           statusCheckCount++;
@@ -152,6 +154,8 @@ export class ExecutableStatement {
               });
           })
           .fail(error => {
+            this.status = EXECUTION_STATUS.failed;
+            notifyUpdates(this);
             reject(error);
           });
       });

+ 1 - 1
desktop/core/src/desktop/js/apps/notebook2/execution/spec/executableStatementSpec.js

@@ -99,7 +99,7 @@ describe('executableStatement.js', () => {
       .execute()
       .then(fail)
       .catch(() => {
-        expect(subject.status).toEqual(EXECUTION_STATUS.running);
+        expect(subject.status).toEqual(EXECUTION_STATUS.failed);
         done();
       });
 

+ 1 - 0
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -19,6 +19,7 @@ import ko from 'knockout';
 import komapping from 'knockout.mapping';
 import { markdown } from 'markdown';
 
+import 'apps/notebook2/components/ko.executableProgressBar';
 import 'apps/notebook2/components/ko.snippetEditorActions';
 import 'apps/notebook2/components/ko.snippetExecuteActions';
 

+ 31 - 0
desktop/core/src/desktop/js/ko/components/DisposableComponent.js

@@ -0,0 +1,31 @@
+// 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.
+
+export default class DisposableComponent {
+  constructor() {
+    this.disposals = [];
+  }
+
+  addDisposalCallback(callback) {
+    this.disposals.push(callback);
+  }
+
+  dispose() {
+    while (this.disposals.length) {
+      this.disposals.pop()();
+    }
+  }
+}

+ 3 - 3
desktop/core/src/desktop/js/ko/components/ko.sessionPanel.js

@@ -26,10 +26,10 @@ import I18n from 'utils/i18n';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
 
 export const NAME = 'session-panel';
-export const SHOW_EVENT_NAME = 'session.panel.show';
+export const SESSION_PANEL_SHOW_EVENT = 'session.panel.show';
 
 const TEMPLATE = `
-<div class="session-panel" data-bind="slideVisible: visible">
+<div class="session-panel" data-test="${NAME}" data-bind="slideVisible: visible">
   <div class="margin-top-10 padding-left-10 padding-right-10">
     <h4 class="margin-bottom-10"><i class="fa fa-cogs"></i> ${I18n('Sessions')}</h4>
     <div class="session-panel-content">
@@ -133,7 +133,7 @@ class SessionPanel {
     this.visible = ko.observable(false);
     this.sessions = ko.observableArray();
     this.activeTypeFilter = undefined;
-    huePubSub.subscribe(SHOW_EVENT_NAME, type => this.showSessions(type));
+    huePubSub.subscribe(SESSION_PANEL_SHOW_EVENT, type => this.showSessions(type));
   }
 
   /**

+ 4 - 4
desktop/core/src/desktop/js/ko/components/spec/ko.sessionPanel.spec.js

@@ -1,7 +1,7 @@
 import huePubSub from 'utils/huePubSub';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
 import { koSetup } from 'spec/jasmineSetup';
-import { NAME, SHOW_EVENT_NAME } from '../ko.sessionPanel';
+import { NAME, SESSION_PANEL_SHOW_EVENT } from '../ko.sessionPanel';
 
 describe('ko.sessionPanel.js', () => {
   const setup = koSetup();
@@ -9,18 +9,18 @@ describe('ko.sessionPanel.js', () => {
   it('should render component', async () => {
     const element = await setup.renderComponent(NAME, {});
 
-    expect(element.querySelector('.session-panel')).toBeTruthy();
+    expect(element.querySelector('[data-test="' + NAME + '"]')).toBeTruthy();
   });
 
   it('should be visible on publish event', async () => {
     const wrapper = await setup.renderComponent(NAME, {});
-    const sessionPanelElement = wrapper.querySelector('.session-panel');
+    const sessionPanelElement = wrapper.querySelector('[data-test="' + NAME + '"]');
     spyOn(sessionManager, 'getAllSessions').and.returnValue(Promise.resolve([]));
 
     // Initially hidden
     expect(sessionPanelElement.style['display']).toEqual('none');
 
-    huePubSub.publish(SHOW_EVENT_NAME, 'impala');
+    huePubSub.publish(SESSION_PANEL_SHOW_EVENT, 'impala');
     await setup.waitForKoUpdate();
 
     // Visible after pubsub

+ 1 - 0
desktop/core/src/desktop/js/spec/jasmineSetup.js

@@ -61,6 +61,7 @@ export const koSetup = () => {
     ko.components.defaultLoader.loadTemplate = originalLoadTemplate;
     wrapper.parentNode.removeChild(wrapper);
     ko.cleanNode(wrapper);
+    expect(window.document.querySelectorAll('[data-test]').length).toEqual(0);
   });
 
   return {

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
desktop/libs/notebook/src/notebook/static/notebook/css/notebook2.css


+ 5 - 0
desktop/libs/notebook/src/notebook/static/notebook/less/notebook2.less

@@ -593,6 +593,7 @@
   }
 
   .progress-snippet {
+    width: 100%;
     background-image: none;
     box-shadow: none;
   }
@@ -680,6 +681,10 @@
     background-color: @cui-green-400;
   }
 
+  .progress-running .bar {
+    background-color: @hue-primary-color-dark;
+  }
+
   .progress-warning .bar, .progress .bar-warning {
     background-color: @cui-orange-200;
   }

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

@@ -1585,13 +1585,7 @@
     <div class="snippet-execution-status" data-bind="clickForAceFocus: ace">
       <a class="inactive-action pull-left snippet-logs-btn" href="javascript:void(0)" data-bind="visible: status() === 'running' && errors().length == 0, click: function() { hideFixedHeaders(); $data.showLogs(!$data.showLogs());}, css: {'blue': $data.showLogs}" title="${ _('Toggle Logs') }"><i class="fa fa-fw" data-bind="css: { 'fa-caret-right': !$data.showLogs(), 'fa-caret-down': $data.showLogs() }"></i></a>
       <div class="snippet-progress-container" data-bind="visible: status() != 'canceled' && status() != 'with-optimizer-report'">
-        <div class="progress-snippet progress" data-bind="css: {
-        'progress-starting': progress() == 0 && (status() == 'running' || status() == 'starting'),
-        'progress-warning': progress() > 0 && progress() < 100,
-        'progress-success': progress() == 100,
-        'progress-danger': progress() == 0 && errors().length > 0}" style="background-color: #FFF; width: 100%">
-          <div class="bar" data-bind="style: {'width': (errors().length > 0 ? 100 : Math.max(2, progress())) + '%'}"></div>
-        </div>
+        <!-- ko component: { name: 'executable-progress-bar', params: { snippet: $data } } --><!-- /ko -->
       </div>
       <div class="snippet-error-container alert alert-error" style="margin-bottom: 0" data-bind="visible: errors().length > 0">
         <ul class="unstyled" data-bind="foreach: errors">

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels