Переглянути джерело

[jb] Various mini Job Browser improvements (#3654)

* [jb] Initialize the mini job browser on first open instead of when the document is 'ready'

* [jb] The mini job browser should not update based on URL hash or when the JB main page is loaded

* [frontend] Fix an issue where knockout components under test have their view models created twice
Johan Åhlén 1 рік тому
батько
коміт
57523a1b9e

+ 0 - 5
apps/jobbrowser/src/jobbrowser/templates/mini_job_browser.mako

@@ -20,7 +20,6 @@ from desktop.conf import CUSTOM, IS_K8S_ONLY
 from desktop.views import commonheader, commonfooter
 from desktop.webpack_utils import get_hue_bundles
 from jobbrowser.conf import MAX_JOB_FETCH, ENABLE_QUERY_BROWSER
-from webpack_loader.templatetags.webpack_loader import render_bundle
 
 if sys.version_info[0] > 2:
   from django.utils.translation import gettext as _
@@ -54,10 +53,6 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
   <link rel="stylesheet" href="${ static('desktop/css/bootstrap-spinedit.css') }">
   <link rel="stylesheet" href="${ static('desktop/css/bootstrap-slider.css') }">
   
-  % for bundle in get_hue_bundles('miniJobBrowser'):
-    ${ render_bundle(bundle) | n,unicode }
-  % endfor
-  
   <script src="${ static('desktop/ext/js/bootstrap-datepicker.min.js') }" type="text/javascript" charset="utf-8"></script>
   <script src="${ static('desktop/ext/js/bootstrap-timepicker.min.js') }" type="text/javascript" charset="utf-8"></script>
   <script src="${ static('desktop/js/bootstrap-spinedit.js') }" type="text/javascript" charset="utf-8"></script>

+ 2 - 24
desktop/core/src/desktop/js/apps/jobBrowser/miniApp.js → desktop/core/src/desktop/js/apps/jobBrowser/miniJobBrowser.js

@@ -27,7 +27,7 @@ import './eventListeners';
 import JobBrowserViewModel from './knockout/JobBrowserViewModel';
 import Job from './knockout/Job';
 
-$(document).ready(() => {
+export const initializeMiniJobBrowser = () => {
   const jobBrowserViewModel = new JobBrowserViewModel(true);
   const openJob = id => {
     if (jobBrowserViewModel.job() == null) {
@@ -39,31 +39,9 @@ $(document).ready(() => {
 
   ko.applyBindings(jobBrowserViewModel, $('#jobbrowserMiniComponents')[0]);
 
-  huePubSub.subscribe(
-    'app.gained.focus',
-    app => {
-      if (app === 'jobbrowser') {
-        huePubSub.publish('graph.draw.arrows');
-        loadHash();
-      }
-    },
-    'jobbrowser'
-  );
-
-  const loadHash = () => {
-    if (window.location.pathname.indexOf('jobbrowser') > -1) {
-      jobBrowserViewModel.load();
-    }
-  };
-
-  window.onhashchange = () => {
-    loadHash();
-  };
-
   const configUpdated = clusterConfig => {
     jobBrowserViewModel.appConfig(clusterConfig && clusterConfig['app_config']);
     jobBrowserViewModel.clusterType(clusterConfig && clusterConfig['cluster_type']);
-    loadHash();
   };
 
   huePubSub.subscribe('cluster.config.set.config', configUpdated);
@@ -96,4 +74,4 @@ $(document).ready(() => {
       huePubSub.publish('open.link', '/jobbrowser/#!' + jobBrowserViewModel.interface());
     }
   });
-});
+};

+ 4 - 4
desktop/core/src/desktop/js/jest/koTestUtils.js

@@ -56,10 +56,6 @@ export const koSetup = () => {
               const origSetTimeout = window.setTimeout;
               if (instantTimeout) {
                 window.setTimeout = cb => cb();
-                comp.createViewModel(data, { element: element });
-                window.setTimeout = origSetTimeout;
-              } else {
-                comp.createViewModel(data, { element: element });
               }
 
               ko.applyBindings(
@@ -69,6 +65,10 @@ export const koSetup = () => {
                 },
                 wrapper
               );
+
+              if (instantTimeout) {
+                window.setTimeout = origSetTimeout;
+              }
             } else {
               reject('no createViewModel function found on component ' + name);
             }

+ 37 - 29
desktop/core/src/desktop/js/ko/components/ko.jobBrowserLinks.js

@@ -20,6 +20,8 @@ import * as ko from 'knockout';
 import componentUtils from './componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
+import { initializeMiniJobBrowser } from '../../apps/jobBrowser/miniJobBrowser';
+import DisposableComponent from './DisposableComponent';
 
 export const NAME = 'hue-job-browser-links';
 
@@ -52,25 +54,30 @@ const TEMPLATE = `
 
 const JB_CHECK_INTERVAL_IN_MILLIS = 30000;
 
-class JobBrowserPanel {
+class JobBrowserPanel extends DisposableComponent {
   constructor(params) {
-    const self = this;
-
+    super();
     const $container = $('body');
-    const $jobsPanel = $('#jobsPanel');
-    const $toggle = $('.btn-toggle-jobs-panel');
+
+    this.initialized = false;
 
     const reposition = function () {
+      const $jobsPanel = $('#jobsPanel');
+      const $toggle = $('.btn-toggle-jobs-panel');
       $jobsPanel.css('top', $toggle.offset().top + $toggle.height() + 15 + 'px');
       $jobsPanel.css('left', $container.offset().left + $container.width() - 630 + 'px');
     };
 
-    huePubSub.subscribe('hide.jobs.panel', () => {
+    super.subscribe('hide.jobs.panel', () => {
       $(window).off('resize', reposition);
-      $jobsPanel.hide();
+      $('#jobsPanel').hide();
     });
 
-    huePubSub.subscribe('show.jobs.panel', options => {
+    super.subscribe('show.jobs.panel', options => {
+      if (!this.initialized) {
+        this.initialized = true;
+        initializeMiniJobBrowser();
+      }
       if (window.IS_K8S_ONLY) {
         huePubSub.publish('context.selector.set.cluster', 'impala');
       }
@@ -78,7 +85,7 @@ class JobBrowserPanel {
       huePubSub.publish('hide.history.panel');
       $(window).on('resize', reposition);
       reposition();
-      $jobsPanel.show();
+      $('#jobsPanel').show();
       huePubSub.publish('mini.jb.navigate', {
         section: options && options.interface ? options.interface : 'jobs',
         compute: options && options.compute
@@ -88,26 +95,26 @@ class JobBrowserPanel {
       }
     });
 
-    huePubSub.subscribe('toggle.jobs.panel', () => {
-      if ($jobsPanel.is(':visible')) {
+    super.subscribe('toggle.jobs.panel', () => {
+      if ($('#jobsPanel').is(':visible')) {
         huePubSub.publish('hide.jobs.panel');
       } else {
         huePubSub.publish('show.jobs.panel');
       }
     });
 
-    self.jobCounts = ko.observable({ yarn: 0, schedules: 0, history: 0 });
-    self.jobCount = ko.pureComputed(() => {
+    this.jobCounts = ko.observable({ yarn: 0, schedules: 0, history: 0 });
+    this.jobCount = ko.pureComputed(() => {
       let total = 0;
-      Object.keys(self.jobCounts()).forEach(value => {
-        total += self.jobCounts()[value];
+      Object.keys(this.jobCounts()).forEach(value => {
+        total += this.jobCounts()[value];
       });
       return total;
     });
-    self.onePageViewModel = params.onePageViewModel;
+    this.onePageViewModel = params.onePageViewModel;
 
     let lastYarnBrowserRequest = null;
-    const checkYarnBrowserStatus = function () {
+    const checkYarnBrowserStatus = () => {
       return $.post('/jobbrowser/jobs/', {
         format: 'json',
         state: 'running',
@@ -116,8 +123,8 @@ class JobBrowserPanel {
         .done(data => {
           if (data != null && data.jobs != null) {
             huePubSub.publish('jobbrowser.data', data.jobs);
-            self.jobCounts()['yarn'] = data.jobs.length;
-            self.jobCounts.valueHasMutated();
+            this.jobCounts()['yarn'] = data.jobs.length;
+            this.jobCounts.valueHasMutated();
           }
         })
         .fail(response => {
@@ -125,7 +132,7 @@ class JobBrowserPanel {
         });
     };
     let lastScheduleBrowserRequest = undefined;
-    const checkScheduleBrowserStatus = function () {
+    const checkScheduleBrowserStatus = () => {
       return $.post(
         '/scheduler/api/schedule/list',
         {
@@ -140,14 +147,14 @@ class JobBrowserPanel {
         data => {
           if (data != null && data.total != null) {
             huePubSub.publish('jobbrowser.schedule.data', data.apps);
-            self.jobCounts()['schedules'] = data.total;
-            self.jobCounts.valueHasMutated();
+            this.jobCounts()['schedules'] = data.total;
+            this.jobCounts.valueHasMutated();
           }
         }
       );
     };
     let lastHistoryBrowserRequest = null;
-    const checkHistoryBrowserStatus = function () {
+    const checkHistoryBrowserStatus = () => {
       return $.post('/jobbrowser/api/jobs/history', {
         interface: ko.mapping.toJSON('history'),
         filters: ko.mapping.toJSON([
@@ -159,8 +166,8 @@ class JobBrowserPanel {
       })
         .done(data => {
           if (data != null && data.apps != null) {
-            self.jobCounts()['history'] = data.apps.length;
-            self.jobCounts.valueHasMutated();
+            this.jobCounts()['history'] = data.apps.length;
+            this.jobCounts.valueHasMutated();
           }
         })
         .fail(response => {
@@ -192,7 +199,8 @@ class JobBrowserPanel {
         });
     };
 
-    // Load the mini jobbrowser
+    // Load the mini jobbrowser DOM and insert it in the #mini_jobrowser element
+    // Any script or styles are also inserted in the header and thus loaded async.
     $.ajax({
       url: '/jobbrowser/apps?is_embeddable=true&is_mini=true',
       beforeSend: function (xhr) {
@@ -210,9 +218,9 @@ class JobBrowserPanel {
     if (!window.IS_K8S_ONLY) {
       checkJobBrowserStatusIdx = window.setTimeout(checkJobBrowserStatus, 10);
 
-      huePubSub.subscribe('check.job.browser', checkYarnBrowserStatus);
-      huePubSub.subscribe('check.schedules.browser', checkScheduleBrowserStatus);
-      huePubSub.subscribe('check.history.browser', checkHistoryBrowserStatus);
+      super.subscribe('check.job.browser', checkYarnBrowserStatus);
+      super.subscribe('check.schedules.browser', checkScheduleBrowserStatus);
+      super.subscribe('check.history.browser', checkHistoryBrowserStatus);
     }
   }
 }

+ 41 - 1
desktop/core/src/desktop/js/ko/components/ko.jobBrowserLinks.test.js

@@ -17,19 +17,59 @@
 import $ from 'jquery';
 import { koSetup } from 'jest/koTestUtils';
 import { NAME } from './ko.jobBrowserLinks';
+import huePubSub from 'utils/huePubSub';
+
+import { initializeMiniJobBrowser } from 'apps/jobBrowser/miniJobBrowser';
+jest.mock('apps/jobBrowser/miniJobBrowser');
 
 describe('ko.jobBrowserLinks.js', () => {
   const setup = koSetup();
+  let fakeMiniPanel;
 
-  it('should render component', async () => {
+  beforeEach(() => {
     jest.spyOn($, 'post').mockImplementation(url => {
       if (url === '/jobbrowser/jobs/') {
         return $.Deferred().resolve({ jobs: [] }).promise();
       }
     });
 
+    // This element gets created during runtime via an ajax call that gets the raw
+    // html contents including scripts and styles.
+    fakeMiniPanel = document.createElement('div');
+    fakeMiniPanel.setAttribute('id', 'jobsPanel');
+    document.body.appendChild(fakeMiniPanel);
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
+    fakeMiniPanel.remove();
+  });
+
+  it('should render component', async () => {
     const element = await setup.renderComponent(NAME, {});
 
     expect(element.innerHTML).toMatchSnapshot();
   });
+
+  it('should initialize the mini job browser on first open', async () => {
+    window.HAS_JOB_BROWSER = true;
+    window.getLastKnownConfig = () => ({});
+    jest.spyOn($, 'post').mockImplementation(url => {
+      if (url === '/jobbrowser/jobs/') {
+        return Promise.resolve({ jobs: [] }).promise();
+      }
+    });
+    await setup.renderComponent(NAME, {});
+    expect(initializeMiniJobBrowser.mock.calls).toHaveLength(0);
+
+    // First open
+    huePubSub.publish('show.jobs.panel');
+    expect(initializeMiniJobBrowser.mock.calls).toHaveLength(1);
+
+    huePubSub.publish('hide.jobs.panel');
+
+    // Second open
+    huePubSub.publish('show.jobs.panel');
+    expect(initializeMiniJobBrowser.mock.calls).toHaveLength(1);
+  });
 });

+ 1 - 5
webpack.config.js

@@ -37,11 +37,7 @@ const config = {
       import: './desktop/core/src/desktop/js/apps/storageBrowser/app.js',
       dependOn: 'hue'
     },
-    jobBrowser: { import: './desktop/core/src/desktop/js/apps/jobBrowser/app.js', dependOn: 'hue' },
-    miniJobBrowser: {
-      import: './desktop/core/src/desktop/js/apps/jobBrowser/miniApp.js',
-      dependOn: 'hue'
-    }
+    jobBrowser: { import: './desktop/core/src/desktop/js/apps/jobBrowser/app.js', dependOn: 'hue' }
   },
   mode: 'development',
   module: {