فهرست منبع

HUE-8981 [editor] Extract session handling from the editor view model

Johan Ahlen 6 سال پیش
والد
کامیت
22cf8ddbe9

+ 27 - 3
desktop/core/src/desktop/js/api/apiHelper.js

@@ -1887,6 +1887,28 @@ class ApiHelper {
     return this.simplePost('/notebook/api/create_session', data);
   }
 
+  /**
+   * Creates a new session
+   *
+   * @param {Object} options
+   * @param {String} options.type
+   * @param {Array<SessionProperty>} [options.properties] - Default []
+   * @return {Promise}
+   */
+  createSessionV2(options) {
+    const data = {
+      session: JSON.stringify({ type: options.type, properties: options.properties || [] })
+    };
+    return this.simplePost('/notebook/api/create_session', data, options);
+  }
+
+  closeSession(options) {
+    const data = {
+      session: JSON.stringify(options.session)
+    };
+    return this.simplePost('/notebook/api/close_session', data, options);
+  }
+
   checkStatus(options) {
     const data = {
       notebook: options.notebookJson
@@ -1959,10 +1981,11 @@ class ApiHelper {
   /**
    *
    * @param {ExecutableStatement} executable
+   * @param {Session} [session]
    *
    * @return {{snippet: string, notebook: string}}
    */
-  static adaptExecutableToNotebook(executable) {
+  static adaptExecutableToNotebook(executable, session) {
     const statement = executable.getStatement();
     const snippet = {
       type: executable.sourceType,
@@ -1985,7 +2008,7 @@ class ApiHelper {
       id: executable.notebookId,
       name: '',
       isSaved: false,
-      sessions: executable.sessions || []
+      sessions: session ? [session] : []
     };
 
     return {
@@ -2016,6 +2039,7 @@ class ApiHelper {
    * @param {Object} options
    * @param {boolean} [options.silenceErrors]
    * @param {ExecutableStatement} options.executable
+   * @param {Session} options.session
    *
    * @return {Promise<ExecutionHandle>}
    */
@@ -2024,7 +2048,7 @@ class ApiHelper {
     const url = EXECUTE_API_PREFIX + executable.sourceType;
     const deferred = $.Deferred();
 
-    this.simplePost(url, ApiHelper.adaptExecutableToNotebook(executable), options)
+    this.simplePost(url, ApiHelper.adaptExecutableToNotebook(executable, options.session), options)
       .done(response => {
         if (response.handle) {
           deferred.resolve(response.handle);

+ 0 - 7
desktop/core/src/desktop/js/apps/notebook/editorViewModel.js

@@ -297,13 +297,6 @@ class EditorViewModel {
       }
     });
 
-    self.isContextPanelVisible = ko.observable(false);
-    self.isContextPanelVisible.subscribe(newValue => {
-      huePubSub.publish('context.panel.visible', newValue);
-    });
-
-    huePubSub.subscribe('context.panel.visible.editor', self.isContextPanelVisible);
-
     huePubSub.subscribe(
       'get.active.snippet.type',
       callback => {

+ 41 - 53
desktop/core/src/desktop/js/apps/notebook2/editorViewModel.js

@@ -34,7 +34,6 @@ class EditorViewModel {
     console.log('Notebook 2 enabled.');
 
     self.editorId = editorId;
-    self.sessionProperties = options.session_properties || [];
     self.snippetViewSettings = options.snippetViewSettings;
     self.notebooks = notebooks;
 
@@ -171,44 +170,16 @@ class EditorViewModel {
       }
     });
 
-    const withActiveSnippet = function(callback) {
-      const notebook = self.selectedNotebook();
-      let foundSnippet;
-      if (notebook) {
-        if (notebook.snippets().length === 1) {
-          foundSnippet = notebook.snippets()[0];
-        } else {
-          notebook.snippets().every(snippet => {
-            if (snippet.inFocus()) {
-              foundSnippet = snippet;
-              return false;
-            }
-            return true;
-          });
-        }
-      }
-      if (foundSnippet) {
-        callback(foundSnippet);
-      }
-    };
-
     huePubSub.subscribe('assist.highlight.risk.suggestions', () => {
       if (self.isRightPanelAvailable() && !self.isRightPanelVisible()) {
         self.isRightPanelVisible(true);
       }
     });
 
-    self.isContextPanelVisible = ko.observable(false);
-    self.isContextPanelVisible.subscribe(newValue => {
-      huePubSub.publish('context.panel.visible', newValue);
-    });
-
-    huePubSub.subscribe('context.panel.visible.editor', self.isContextPanelVisible);
-
     huePubSub.subscribe(
       'get.active.snippet.type',
       callback => {
-        withActiveSnippet(activeSnippet => {
+        this.withActiveSnippet(activeSnippet => {
           if (callback) {
             callback(activeSnippet.type());
           } else {
@@ -222,7 +193,7 @@ class EditorViewModel {
     huePubSub.subscribe(
       'save.snippet.to.file',
       () => {
-        withActiveSnippet(activeSnippet => {
+        this.withActiveSnippet(activeSnippet => {
           const data = {
             path: activeSnippet.statementPath(),
             contents: activeSnippet.statement()
@@ -245,7 +216,7 @@ class EditorViewModel {
     huePubSub.subscribe(
       'sql.context.pin',
       contextData => {
-        withActiveSnippet(activeSnippet => {
+        this.withActiveSnippet(activeSnippet => {
           contextData.tabId = 'context' + activeSnippet.pinnedContextTabs().length;
           activeSnippet.pinnedContextTabs.push(contextData);
           activeSnippet.currentQueryTab(contextData.tabId);
@@ -257,7 +228,7 @@ class EditorViewModel {
     huePubSub.subscribe(
       'assist.database.set',
       databaseDef => {
-        withActiveSnippet(activeSnippet => {
+        this.withActiveSnippet(activeSnippet => {
           activeSnippet.handleAssistSelection(databaseDef);
         });
       },
@@ -267,7 +238,7 @@ class EditorViewModel {
     huePubSub.subscribe(
       'assist.database.selected',
       databaseDef => {
-        withActiveSnippet(activeSnippet => {
+        this.withActiveSnippet(activeSnippet => {
           activeSnippet.handleAssistSelection(databaseDef);
         });
       },
@@ -277,12 +248,6 @@ class EditorViewModel {
     self.availableSnippets = komapping.fromJS(options.languages);
 
     self.editorMode = ko.observable(options.mode === 'editor');
-
-    // Only Spark
-    // Could filter out the ones already selected + yarn only or not
-    self.availableSessionProperties = ko.pureComputed(() =>
-      self.sessionProperties.filter(item => item.name !== '')
-    );
   }
 
   changeURL(url) {
@@ -334,18 +299,6 @@ class EditorViewModel {
     };
   }
 
-  getSessionProperties(name) {
-    const self = this;
-    let result = null;
-    self.sessionProperties.some(prop => {
-      if (prop.name === name) {
-        result = prop;
-        return true;
-      }
-    });
-    return result;
-  }
-
   getSnippetName(snippetType) {
     const self = this;
     const availableSnippets = self.availableSnippets();
@@ -535,7 +488,8 @@ class EditorViewModel {
     const self = this;
     let hasContent = snippet.statement_raw().length > 0;
     if (!hasContent) {
-      snippet.properties().forEach(value => {
+      Object.keys(snippet.properties()).forEach(key => {
+        const value = snippet.properties()[key];
         hasContent = hasContent || (ko.isObservable(value) && value().length > 0);
       });
     }
@@ -592,6 +546,17 @@ class EditorViewModel {
     });
   }
 
+  showSessionPanel() {
+    this.withActiveSnippet(
+      snippet => {
+        huePubSub.publish('session.panel.show', snippet.type());
+      },
+      () => {
+        huePubSub.publish('session.panel.show');
+      }
+    );
+  }
+
   toggleEditing() {
     const self = this;
     self.isEditing(!self.isEditing());
@@ -668,6 +633,29 @@ class EditorViewModel {
       self.toggleEditorMode();
     }
   }
+
+  withActiveSnippet(callback, notFoundCallback) {
+    const notebook = this.selectedNotebook();
+    let foundSnippet;
+    if (notebook) {
+      if (notebook.snippets().length === 1) {
+        foundSnippet = notebook.snippets()[0];
+      } else {
+        notebook.snippets().every(snippet => {
+          if (snippet.inFocus()) {
+            foundSnippet = snippet;
+            return false;
+          }
+          return true;
+        });
+      }
+    }
+    if (foundSnippet) {
+      callback(foundSnippet);
+    } else if (notFoundCallback) {
+      notFoundCallback();
+    }
+  }
 }
 
 export default EditorViewModel;

+ 24 - 21
desktop/core/src/desktop/js/apps/notebook2/execution/executableStatement.js

@@ -18,7 +18,7 @@ import apiHelper from 'api/apiHelper';
 import { ExecutionResult } from 'apps/notebook2/execution/executionResult';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
-
+import sessionManager from 'apps/notebook2/execution/sessionManager';
 /**
  *
  * @type {{running: string, canceling: string, canceled: string, expired: string, waiting: string, success: string, ready: string, available: string, closed: string, starting: string}}
@@ -58,7 +58,6 @@ class ExecutableStatement {
     this.sourceType = options.sourceType;
     this.parsedStatement = options.parsedStatement;
     this.statement = options.statement;
-    this.sessions = options.sessions;
     this.handle = {
       statement_id: 0 // TODO: Get rid of need for initial handle in the backend
     };
@@ -133,25 +132,29 @@ class ExecutableStatement {
       this.progress = 0;
 
       notifyUpdates(this);
-      this.lastCancellable = apiHelper
-        .executeStatement({
-          executable: this
-        })
-        .done(handle => {
-          this.handle = handle;
-
-          checkStatus()
-            .then(() => {
-              this.result = new ExecutionResult(this);
-              resolve(this.result);
-            })
-            .catch(error => {
-              reject(error);
-            });
-        })
-        .fail(error => {
-          reject(error);
-        });
+
+      sessionManager.getSession({ type: this.sourceType }).then(session => {
+        this.lastCancellable = apiHelper
+          .executeStatement({
+            executable: this,
+            session: session
+          })
+          .done(handle => {
+            this.handle = handle;
+
+            checkStatus()
+              .then(() => {
+                this.result = new ExecutionResult(this);
+                resolve(this.result);
+              })
+              .catch(error => {
+                reject(error);
+              });
+          })
+          .fail(error => {
+            reject(error);
+          });
+      });
     });
   }
 

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

@@ -17,6 +17,10 @@
 import { EXECUTION_STATUS, ExecutableStatement } from './executableStatement';
 import sqlStatementsParser from 'parse/sqlStatementsParser';
 import huePubSub from 'utils/huePubSub';
+import sessionManager from './sessionManager';
+
+// TODO: Remove, debug var
+window.sessionManager = sessionManager;
 
 const EXECUTION_FLOW = {
   step: 'step',
@@ -34,7 +38,6 @@ class Executor {
    * @param {ContextCompute} options.compute
    * @param {ContextNamespace} options.namespace
    * @param {EXECUTION_FLOW} [options.executionFlow] (default EXECUTION_FLOW.batch)
-   * @param {Session[]} [options.sessions]
    * @param {string} options.statement
    * @param {string} [options.database]
    */
@@ -73,7 +76,6 @@ class Executor {
                 compute: options.compute,
                 namespace: options.namespace,
                 database: database,
-                sessions: options.sessions,
                 parsedStatement: parsedStatement
               })
             );
@@ -87,9 +89,7 @@ class Executor {
     huePubSub.subscribe('hue.executable.updated', executable => {
       if (
         executable === this.currentExecutable ||
-        this.executed.some(executed => {
-          executed === executable;
-        })
+        this.executed.some(executed => executed === executable)
       ) {
         huePubSub.publish('hue.executor.updated', {
           executable: executable,

+ 82 - 5
desktop/core/src/desktop/js/apps/notebook2/execution/sessionManager.js

@@ -14,16 +14,93 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-const knownSessions = {};
+import apiHelper from '../../../api/apiHelper';
 
 class SessionManager {
   constructor() {
-    console.warn('Not implemented yet.');
+    this.knownSessions = {};
   }
+  /**
+   * @typedef SessionProperty
+   * @property {Array<*>} defaultValue
+   * @property {String} help_text
+   * @property {String} key
+   * @property {Boolean} multiple
+   * @property {String} nice_name
+   * @property {Array<*>} options
+   * @property {String} type
+   * @property {Array<*>} value
+   */
 
-  getSessions(interpreter) {
-    return knownSessions[interpreter];
+  /**
+   * @typedef Session
+   * @property {Object.<string, string>} configuration
+   * @property {string} http_addr
+   * @property {number} id
+   * @property {Array<SessionProperty>} properties
+   * @property {boolean} reuse_session
+   * @property {string} session_id
+   * @property {string} type
+   */
+
+  /**
+   * Gets an existing session or creates a new one if there is no session.
+   *
+   * @param {Object} options
+   * @param {String} options.type
+   * @param {Array<SessionProperty>} [options.properties] - Default []
+   *
+   * @return {Promise<Session>}
+   */
+  async getSession(options) {
+    if (!this.knownSessions[options.type]) {
+      this.knownSessions[options.type] = new Promise((resolve, reject) => {
+        apiHelper
+          .createSessionV2({
+            type: options.type,
+            properties: options.properties
+          })
+          .then(response => {
+            resolve(response.session);
+          })
+          .fail(reject);
+      });
+
+      // Sessions that fail
+      this.knownSessions[options.type].catch(() => {
+        delete this.knownSessions[options.type];
+      });
+    }
+    return this.knownSessions[options.type];
+  }
+
+  async getAllSessions() {
+    const promises = Object.keys(this.knownSessions).map(key => this.knownSessions[key]);
+    return Promise.all(promises);
+  }
+
+  async restartSession(session) {
+    await this.closeSession(session);
+    return this.getSession({ type: session.type, properties: session.properties });
+  }
+
+  hasSession(type) {
+    return !!this.knownSessions[type];
+  }
+
+  async closeSession(session) {
+    if (!this.hasSession(session.type)) {
+      return;
+    }
+    delete this.knownSessions[session.type];
+    try {
+      await apiHelper.closeSession({ session: session, silenceErrors: true });
+    } catch (err) {
+      console.warn(err);
+    }
   }
 }
 
-export default SessionManager;
+const instance = new SessionManager();
+
+export default instance;

+ 1 - 186
desktop/core/src/desktop/js/apps/notebook2/notebook.js

@@ -23,7 +23,6 @@ import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 
-import Session from 'apps/notebook2/session';
 import { Snippet, STATUS as SNIPPET_STATUS } from 'apps/notebook2/snippet';
 
 const NOTEBOOK_MAPPING = {
@@ -97,10 +96,6 @@ class Notebook {
 
     self.snippets = ko.observableArray();
     self.selectedSnippet = ko.observable(vm.editorType()); // Aka selectedSnippetType
-    self.creatingSessionLocks = ko.observableArray();
-    self.sessions = komapping.fromJS(notebook.sessions || [], {
-      create: value => new Session(vm, value.data)
-    });
     self.directoryUuid = ko.observable(notebook.directoryUuid);
     self.dependents = komapping.fromJS(notebook.dependents || []);
     self.dependentsCoordinator = ko.pureComputed(() =>
@@ -255,47 +250,14 @@ class Notebook {
     huePubSub.publish('assist.is.db.panel.ready');
   }
 
-  addSession(session) {
-    const self = this;
-    const toRemove = self.sessions().filter(s => s.type() === session.type());
-
-    toRemove.forEach(s => {
-      self.sessions.remove(s);
-    });
-
-    self.sessions.push(session);
-  }
-
-  addSnippet(snippet, skipSession) {
+  addSnippet(snippet) {
     const self = this;
     const newSnippet = new Snippet(self.parentVm, self, snippet);
     self.snippets.push(newSnippet);
-
-    if (self.getSession(newSnippet.type()) == null && typeof skipSession == 'undefined') {
-      window.setTimeout(() => {
-        newSnippet.status(SNIPPET_STATUS.loading);
-        self.createSession(new Session(self.parentVm, { type: newSnippet.type() }));
-      }, 200);
-    }
-
     newSnippet.init();
     return newSnippet;
   }
 
-  authSession() {
-    const self = this;
-    self.createSession(
-      new Session(self.parentVm, {
-        type: self.parentVm.authSessionType(),
-        properties: [
-          { name: 'user', value: self.parentVm.authSessionUsername() },
-          { name: 'password', value: self.parentVm.authSessionPassword() }
-        ]
-      }),
-      self.parentVm.authSessionCallback() // On new session we don't automatically execute the snippet after the aut. On session expiration we do or we refresh assist DB when login-in.
-    );
-  }
-
   cancelExecutingAll() {
     const self = this;
     const index = self.executingAllIndex();
@@ -348,99 +310,6 @@ class Notebook {
     });
   }
 
-  closeAndRemoveSession(session) {
-    const self = this;
-    self.closeSession(session, false, () => {
-      self.sessions.remove(session);
-    });
-  }
-
-  closeSession(session, silent, callback) {
-    $.post(
-      '/notebook/api/close_session',
-      {
-        session: komapping.toJSON(session)
-      },
-      data => {
-        if (!silent && data && data.status !== 0 && data.status !== -2 && data.message) {
-          $(document).trigger('error', data.message);
-        }
-
-        if (callback) {
-          callback();
-        }
-      }
-    ).fail(xhr => {
-      if (!silent && xhr.status !== 502) {
-        $(document).trigger('error', xhr.responseText);
-      }
-    });
-  }
-
-  createSession(session, callback, failCallback) {
-    const self = this;
-    if (self.creatingSessionLocks().indexOf(session.type()) !== -1) {
-      // Create one type of session max
-      return;
-    } else {
-      self.creatingSessionLocks.push(session.type());
-    }
-
-    let compute = null;
-    self.getSnippets(session.type()).forEach((snippet, index) => {
-      snippet.status(SNIPPET_STATUS.loading);
-      if (index === 0) {
-        compute = snippet.compute();
-      }
-    });
-
-    const fail = function(message) {
-      self.getSnippets(session.type()).forEach(snippet => {
-        snippet.status('failed');
-      });
-      $(document).trigger('error', message);
-      if (failCallback) {
-        failCallback();
-      }
-    };
-
-    apiHelper
-      .createSession({
-        notebookJson: komapping.toJSON(self.getContext(), NOTEBOOK_MAPPING),
-        sessionJson: komapping.toJSON(session), // e.g. {'type': 'pyspark', 'properties': [{'name': driverCores', 'value', '2'}]}
-        clusterJson: komapping.toJSON(compute ? compute : '')
-      })
-      .then(data => {
-        if (data.status === 0) {
-          komapping.fromJS(data.session, {}, session);
-          if (self.getSession(session.type()) == null) {
-            self.addSession(session);
-          } else {
-            const _session = self.getSession(session.type());
-            komapping.fromJS(data.session, {}, _session);
-          }
-          self.getSnippets(session.type()).forEach(snippet => {
-            snippet.status('ready');
-          });
-          if (callback) {
-            setTimeout(callback, 500);
-          }
-        } else if (data.status === 401) {
-          $(document).trigger('showAuthModal', { type: session.type(), message: data.message });
-        } else {
-          fail(data.message);
-        }
-      })
-      .fail(xhr => {
-        if (xhr.status !== 502) {
-          fail(xhr.responseText);
-        }
-      })
-      .always(() => {
-        self.creatingSessionLocks.remove(session.type());
-      });
-  }
-
   executeAll() {
     const self = this;
     if (self.isExecutingAll() || self.snippets().length === 0) {
@@ -501,25 +370,11 @@ class Notebook {
       uuid: self.uuid,
       parentSavedQueryUuid: self.parentSavedQueryUuid,
       isSaved: self.isSaved,
-      sessions: self.sessions,
       type: self.type,
       name: self.name
     };
   }
 
-  getSession(session_type) {
-    const self = this;
-    let found = undefined;
-    self.sessions().every(session => {
-      if (session.type() === session_type) {
-        found = session;
-        return false;
-      }
-      return true;
-    });
-    return found;
-  }
-
   getSnippets(type) {
     const self = this;
     return self.snippets().filter(snippet => snippet.type() === type);
@@ -660,37 +515,6 @@ class Notebook {
     }
   }
 
-  restartSession(session, callback) {
-    const self = this;
-    if (session.restarting()) {
-      return;
-    }
-    session.restarting(true);
-    const snippets = self.getSnippets(session.type());
-
-    snippets.forEach(snippet => {
-      snippet.status(SNIPPET_STATUS.loading);
-    });
-
-    self.closeSession(session, true, () => {
-      self.createSession(
-        session,
-        () => {
-          snippets.forEach(snippet => {
-            snippet.status(SNIPPET_STATUS.ready);
-          });
-          session.restarting(false);
-          if (callback) {
-            callback();
-          }
-        },
-        () => {
-          session.restarting(false);
-        }
-      );
-    });
-  }
-
   save(callback) {
     const self = this;
     hueAnalytics.log('notebook', 'save');
@@ -786,15 +610,6 @@ class Notebook {
       });
   }
 
-  saveDefaultUserProperties(session) {
-    const self = this;
-    apiHelper.saveConfiguration({
-      app: session.type(),
-      properties: session.properties,
-      userId: self.parentVm.userId
-    });
-  }
-
   saveScheduler() {
     const self = this;
     if (

+ 11 - 17
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -29,7 +29,7 @@ import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 import { NOTEBOOK_MAPPING } from 'apps/notebook2/notebook';
 import Result from 'apps/notebook2/result';
-import Session from 'apps/notebook2/session';
+import sessionManager from 'apps/notebook2/execution/sessionManager';
 
 // TODO: Remove. Temporary here for debug
 window.ExecutableStatement = ExecutableStatement;
@@ -172,11 +172,15 @@ class Snippet {
 
     self.id = ko.observable(snippet.id || hueUtils.UUID());
     self.name = ko.observable(snippet.name || '');
-    self.type = ko.observable(snippet.type || TYPE.hive);
-    self.type.subscribe(() => {
-      self.status(STATUS.ready);
+    self.type = ko.observable();
+    self.type.subscribe(newValue => {
+      // TODO: Add session disposal for ENABLE_NOTEBOOK_2
+      // Pre-create a session to speed up execution
+      sessionManager.getSession({ type: newValue }).then(() => {
+        self.status(STATUS.ready);
+      });
     });
-
+    self.type(snippet.type || TYPE.hive);
     self.isBatchable = ko.pureComputed(
       () =>
         self.type() === TYPE.hive ||
@@ -1385,8 +1389,7 @@ class Snippet {
       sourceType: this.type(),
       namespace: this.namespace(),
       statement: this.statement(),
-      isSqlEngine: this.isSqlDialect(),
-      sessions: komapping.toJS(this.parentNotebook.sessions)
+      isSqlEngine: this.isSqlDialect()
     });
 
     this.executor.executeNext().then(executionResult => {
@@ -1853,16 +1856,7 @@ class Snippet {
   handleAjaxError(data, callback) {
     const self = this;
     if (data.status === -2) {
-      // Session expired
-      const existingSession = self.parentNotebook.getSession(self.type());
-      if (existingSession) {
-        self.parentNotebook.restartSession(existingSession, callback);
-      } else {
-        self.parentNotebook.createSession(
-          new Session(self.parentVm, { type: self.type() }),
-          callback
-        );
-      }
+      // TODO: Session expired, check if handleAjaxError is used for ENABLE_NOTEBOOK_2
     } else if (data.status === -3) {
       // Statement expired
       self.status(STATUS.expired);

+ 3 - 1
desktop/core/src/desktop/js/hue.js

@@ -122,7 +122,9 @@ $(document).ready(() => {
   ko.applyBindings(sidePanelViewModel, $('.left-panel')[0]);
   ko.applyBindings(sidePanelViewModel, $('#leftResizer')[0]);
   ko.applyBindings(sidePanelViewModel, $('.right-panel')[0]);
-  ko.applyBindings(sidePanelViewModel, $('.context-panel')[0]);
+  if (!window.ENABLE_NOTEBOOK_2) {
+    ko.applyBindings(sidePanelViewModel, $('.context-panel')[0]);
+  }
 
   const topNavViewModel = new TopNavViewModel(onePageViewModel);
   ko.applyBindings(topNavViewModel, $('.top-nav')[0]);

+ 187 - 0
desktop/core/src/desktop/js/ko/components/ko.sessionPanel.js

@@ -0,0 +1,187 @@
+// 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 komapping from 'knockout.mapping';
+
+import componentUtils from './componentUtils';
+import huePubSub from 'utils/huePubSub';
+import I18n from 'utils/i18n';
+import sessionManager from 'apps/notebook2/execution/sessionManager';
+import apiHelper from '../../api/apiHelper';
+
+const TEMPLATE = `
+<div class="session-panel" 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">
+      <!-- ko ifnot: sessions().length -->
+      ${I18n('There are currently no information about the sessions.')}
+      <!-- /ko -->
+      
+      <!-- ko if: sessions().length -->
+      <div class="row-fluid">
+        <div class="span11">
+          <form class="form-horizontal session-config">
+            <fieldset>
+              <!-- ko foreach: sessions -->
+              <div>
+                <h4 style="clear:left; display: inline-block">
+                  <span data-bind="text: session.type"></span>
+                  <!-- ko if: typeof session.session_id != 'undefined' && session.session_id -->
+                    <span data-bind="text: session.session_id"></span>
+                  <!-- /ko -->
+                </h4>
+                <div class="session-actions">
+                  <a class="inactive-action pointer" title="${I18n(
+                    'Re-create session'
+                  )}" rel="tooltip" data-bind="click: $parent.restartSession.bind($parent)">
+                    <i class="fa fa-refresh" data-bind="css: { 'fa-spin': loading }"></i> ${I18n(
+                      'Re-create'
+                    )}
+                  </a>
+                  <a class="inactive-action pointer margin-left-10" title="${I18n(
+                    'Close session'
+                  )}" rel="tooltip" data-bind="click: $parent.closeSession.bind($parent)">
+                    <i class="fa fa-times"></i> ${I18n('Close')}
+                  </a>
+                  <!-- ko if: window.USE_DEFAULT_CONFIGURATION -->
+                    <a class="inactive-action pointer margin-left-10" title="${I18n(
+                      'Save session settings as default'
+                    )}" rel="tooltip" data-bind="click: saveDefaultUserProperties"><i class="fa fa-save"></i> ${I18n(
+  'Set as default settings'
+)}</a>
+                  <!-- /ko -->
+                  <!-- ko if: session.type === 'impala' && typeof session.http_addr != 'undefined' -->
+                    <a class="margin-left-10" data-bind="attr: { 'href': session.http_addr }" target="_blank">
+                      <span data-bind="text: session.http_addr.replace(/^(https?):\\/\\//, '')"></span> <i class="fa fa-external-link"></i>
+                    </a>
+                  <!-- /ko -->
+                </div>
+                <!-- ko if: window.USE_DEFAULT_CONFIGURATION -->
+                <div style="width:100%;">
+                  <!-- ko component: { name: 'property-selector', params: { properties: properties } } --><!-- /ko -->
+                </div>
+                <!-- /ko -->
+              </div>
+              <!-- /ko -->
+            </fieldset>
+          </form>
+        </div>
+      </div>
+      <!-- /ko -->
+    </div>
+    <a class="pointer demi-modal-chevron" style="position: absolute; bottom: 0" data-bind="toggle: visible"><i class="fa fa-chevron-up"></i></a>
+  </div>
+</div>
+`;
+
+class EditableProperty {
+  constructor(property) {
+    this.defaultValue = ko.observableArray(property.defaultValue);
+    this.help_text = ko.observable(property.help_text);
+    this.key = ko.observable(property.key);
+    this.multiple = ko.observable(property.multiple);
+    this.nice_name = ko.observable(property.nice_name);
+    this.type = ko.observable(property.type);
+    this.value = ko.observableArray(property.value);
+  }
+
+  getClean() {
+    return komapping.toJS(this);
+  }
+}
+
+class EditableSession {
+  constructor(session) {
+    this.session = session;
+    this.loading = ko.observable(false);
+    this.properties = ko.observableArray(
+      this.session.properties.map(property => new EditableProperty(property))
+    );
+  }
+
+  saveDefaultUserProperties() {
+    apiHelper.saveConfiguration({
+      app: this.session.type,
+      properties: this.properties().map(property => property.getClean()),
+      userId: window.LOGGED_USER_ID
+    });
+  }
+}
+
+class SessionPanel {
+  constructor(params) {
+    this.visible = ko.observable(false);
+    this.sessions = ko.observableArray();
+    this.activeTypeFilter = undefined;
+
+    huePubSub.subscribe('session.panel.show', type => this.showSessions(type));
+  }
+
+  /**
+   *
+   * @param {String} typeFilter
+   * @return {Promise<void>}
+   */
+  async showSessions(typeFilter) {
+    this.activeTypeFilter = typeFilter;
+    let sessions = [];
+    if (typeFilter && sessionManager.hasSession(typeFilter)) {
+      sessions.push(await sessionManager.getSession({ type: typeFilter }));
+    } else {
+      sessions = await sessionManager.getAllSessions();
+    }
+    this.sessions(
+      sessions
+        .map(session => new EditableSession(session))
+        .sort((a, b) => a.session.type.localeCompare(b.session.type))
+    );
+    this.visible(true);
+  }
+
+  /**
+   *
+   * @param {EditableSession} editableSession
+   * @return {Promise<void>}
+   */
+  async restartSession(editableSession) {
+    editableSession.loading(true);
+    const session = editableSession.session;
+    // Apply any changed properties
+    session.properties = editableSession
+      .properties()
+      .map(editableProperty => editableProperty.getClean());
+    await sessionManager.restartSession(session);
+    await this.showSessions(this.activeTypeFilter);
+  }
+
+  /**
+   *
+   * @param {EditableSession} editableSession
+   * @return {Promise<void>}
+   */
+  async closeSession(editableSession) {
+    editableSession.loading(true);
+    const session = editableSession.session;
+    await sessionManager.closeSession(session);
+    await this.showSessions(this.activeTypeFilter);
+  }
+
+  saveDefaultUserProperties(session) {}
+}
+
+componentUtils.registerComponent('session-panel', SessionPanel, TEMPLATE);

+ 1 - 0
desktop/core/src/desktop/js/ko/ko.all.js

@@ -160,6 +160,7 @@ import 'ko/components/ko.navTags';
 import 'ko/components/ko.performanceGraph';
 import 'ko/components/ko.pollingCatalogEntriesList';
 import 'ko/components/ko.sentryPrivileges';
+import 'ko/components/ko.sessionPanel';
 import 'ko/components/ko.sidebar';
 import 'ko/components/ko.sqlColumnsTable';
 

+ 0 - 1
desktop/core/src/desktop/js/onePageViewModel.js

@@ -294,7 +294,6 @@ class OnePageViewModel {
       huePubSub.publish('hue.datatable.search.hide');
       huePubSub.publish('hue.scrollleft.hide');
       huePubSub.publish('context.panel.visible', false);
-      huePubSub.publish('context.panel.visible.editor', false);
       if (app === 'filebrowser') {
         $(window).unbind('hashchange.fblist');
       }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


+ 36 - 0
desktop/core/src/desktop/static/desktop/less/hue4.less

@@ -729,6 +729,42 @@ a:visited {
 
 /* context panel */
 
+.session-panel {
+  display: none;
+  position: absolute;
+  top: 50px;
+  width: 90%;
+  margin-left: 5%;
+  background: @cui-white;
+  z-index: 401;
+  outline: none !important;
+  padding: 10px 0;
+  max-height: 80%;
+  .hue-box-shadow-bottom;
+
+  .session-panel-content {
+    overflow-y: auto;
+    margin-bottom: 20px;
+  }
+
+  .nav-tabs {
+    padding-left: 10px;
+  }
+
+  .tab-content {
+    padding: 12px;
+    min-height: calc(100% - 90px);
+  }
+
+  .tab-pane {
+    height: 100%;
+  }
+
+  .demi-modal-chevron {
+    margin-left: -10px;
+  }
+}
+
 /* TODO: Positioning and clean-up */
 .context-panel {
   position: fixed;

+ 14 - 2
desktop/core/src/desktop/templates/global_js_constants.mako

@@ -19,7 +19,7 @@
 
   from desktop import conf
   from desktop.auth.backend import is_admin;
-  from desktop.conf import APP_SWITCHER_ALTUS_BASE_URL, APP_SWITCHER_MOW_BASE_URL, DISPLAY_APP_SWITCHER, IS_K8S_ONLY, IS_MULTICLUSTER_ONLY, USE_NEW_SIDE_PANELS, VCS
+  from desktop.conf import APP_SWITCHER_ALTUS_BASE_URL, APP_SWITCHER_MOW_BASE_URL, DISPLAY_APP_SWITCHER, IS_K8S_ONLY, IS_MULTICLUSTER_ONLY, USE_DEFAULT_CONFIGURATION, USE_NEW_SIDE_PANELS, VCS
   from desktop.models import hue_version, _get_apps, get_cluster_config
 
   from beeswax.conf import LIST_PARTITIONS_LIMIT
@@ -66,7 +66,6 @@
   window.KNOX_BASE_PATH = window._KNOX_BASE_PATH.indexOf('KNOX_BASE_PATH_KNOX') < 0 ? window._KNOX_BASE_PATH_KNOX : '';
   window.KNOX_BASE_URL = window._KNOX_BASE_URL.indexOf('KNOX_BASE_URL') < 0 ? window._KNOX_BASE_URL : '';
 
-
   window.HAS_GIT = ${ len(VCS.keys()) } > 0;
   window.HAS_MULTI_CLUSTER = '${ get_cluster_config(user)['has_computes'] }' === 'True';
 
@@ -74,6 +73,8 @@
 
   window.DROPZONE_HOME_DIR = '${ user.get_home_directory() if not user.is_anonymous() else "" }';
 
+  window.USE_DEFAULT_CONFIGURATION = '${ USE_DEFAULT_CONFIGURATION.get() }' === 'True';
+
   window.USER_HAS_METADATA_WRITE_PERM = '${ user.has_hue_permission(action="write", app="metadata") }' === 'True';
 
   window.ENABLE_NEW_CREATE_TABLE = '${ hasattr(ENABLE_NEW_CREATE_TABLE, 'get') and ENABLE_NEW_CREATE_TABLE.get()}' === 'True';
@@ -164,6 +165,7 @@
     'Clear the query history': '${ _('Clear the query history') }',
     'Click for more details': '${ _('Click for more details') }',
     'Close': '${_('Close')}',
+    'Close session': '${_('Close session')}',
     'Cluster': '${ _('Cluster') }',
     'Clusters': '${ _('Clusters') }',
     'CodeGen': '${ _('CodeGen') }',
@@ -173,9 +175,11 @@
     'Columns': '${ _('Columns') }',
     'Compilation': '${ _('Compilation') }',
     'Compute': '${ _('Compute') }',
+    'Connect': '${ _('Connect') }',
     'condition': '${ _('condition') }',
     'Confirm History Clearing': '${ _('Confirm History Clearing') }',
     'Confirm the deletion?': '${ _('Confirm the deletion?') }',
+    'Connect to the data source': '${ _('Connect to the data source') }',
     'Could not find details for the function': '${_('Could not find details for the function')}',
     'Could not find': '${_('Could not find')}',
     'CPU': '${ _('CPU') }',
@@ -356,6 +360,7 @@
     'Owner': '${ _('Owner') }',
     'Partition key': '${ _('Partition key') }',
     'Partitions': '${ _('Partitions') }',
+    'Password': '${ _('Password') }',
     'Perform incremental metadata update.': '${ _('Perform incremental metadata update.') }',
     'Permissions': '${ _('Permissions') }',
     'Pig Design': '${_('Pig Design')}',
@@ -375,6 +380,8 @@
     'Query requires a select or aggregate.': '${ _('Query requires a select or aggregate.') }',
     'Query running': '${ _('Query running') }',
     'queued': '${ _('queued') }',
+    'Re-create': '${ _('Re-create') }',
+    'Re-create session': '${ _('Re-create session') }',
     'Refresh': '${ _('Refresh') }',
     'Remove': '${ _('Remove') }',
     'Replace the editor content...': '${ _('Replace the editor content...') }',
@@ -389,6 +396,7 @@
     'Samples': '${ _('Samples') }',
     'Save': '${ _('Save') }',
     'Save changes': '${ _('Save changes') }',
+    'Save session settings as default': '${ _('Save session settings as default') }',
     'Schedule': '${ _('Schedule') }',
     'Search Dashboard': '${_('Search Dashboard')}',
     'Search data and saved documents...': '${ _('Search data and saved documents...') }',
@@ -399,7 +407,9 @@
     'Selected entry': '${_('Selected entry')}',
     'Sentry will recursively delete the SERVER or DATABASE privileges you marked for deletion.': '${ _('Sentry will recursively delete the SERVER or DATABASE privileges you marked for deletion.') }',
     'Server': '${ _('Server') }',
+    'Sessions': '${ _('Sessions') }',
     'Set as default application': '${_('Set as default application')}',
+    'Set as default settings': '${_('Set as default settings')}',
     'Shell Script': '${_('Shell Script')}',
     'Show 50 more...': '${_('Show 50 more...')}',
     'Show advanced': '${_('Show advanced')}',
@@ -435,6 +445,7 @@
     'The upload has been canceled': '${ _('The upload has been canceled') }',
     'There are no stats to be shown': '${ _('There are no stats to be shown') }',
     'There are no terms to be shown': '${ _('There are no terms to be shown') }',
+    'There is currently no information about the sessions.': '${ _('There is currently no information about the sessions.') }',
     'This field does not support stats': '${ _('This field does not support stats') }',
     'This will sync missing tables.': '${ _('This will sync missing tables.') }',
     'Timeline': '${ _('Timeline') }',
@@ -450,6 +461,7 @@
     'Upload file': '${_('Upload file')}',
     'uploaded successfully': '${ _('uploaded successfully') }',
     'used by': '${ _('used by') }',
+    'Username': '${ _('Username') }',
     'Value': '${ _('Value') }',
     'Values': '${ _('Values') }',
     'variable': '${ _('variable') }',

+ 8 - 3
desktop/core/src/desktop/templates/hue.mako

@@ -27,6 +27,7 @@
   from filebrowser.conf import SHOW_UPLOAD_BUTTON
   from indexer.conf import ENABLE_NEW_INDEXER
   from metadata.conf import has_optimizer, OPTIMIZER
+  from notebook.conf import ENABLE_NOTEBOOK_2
 
   from desktop.auth.backend import is_admin
   from webpack_loader.templatetags.webpack_loader import render_bundle
@@ -228,8 +229,10 @@ ${ hueIcons.symbols() }
         onPosition: function() { huePubSub.publish('split.draggable.position') }
       }"><div class="resize-bar"></div></div>
 
-
       <div class="page-content">
+        <!-- ko if: window.ENABLE_NOTEBOOK_2 -->
+        <!-- ko component: 'session-panel' --><!-- /ko -->
+        <!-- /ko -->
         <!-- ko hueSpinner: { spin: isLoadingEmbeddable, center: true, size: 'xlarge', blackout: true } --><!-- /ko -->
         <div id="embeddable_editor" class="embeddable"></div>
         <div id="embeddable_notebook" class="embeddable"></div>
@@ -291,6 +294,7 @@ ${ hueIcons.symbols() }
           }
         }" style="display: none;"></div>
 
+      %if not ENABLE_NOTEBOOK_2.get():
       <div class="context-panel" data-bind="slideVisible: contextPanelVisible">
         <div class="margin-top-10 padding-left-10 padding-right-10">
           <h4 class="margin-bottom-30"><i class="fa fa-cogs"></i> ${_('Session')}</h4>
@@ -302,12 +306,13 @@ ${ hueIcons.symbols() }
             <!-- /ko -->
 
             <!-- ko ifnot: sessionsAvailable() && templateApp() -->
-            ${_('There are currently no information about the sessions.')}
+            ${_('There is currently no information about the sessions.')}
             <!-- /ko -->
           </div>
         </div>
-        <a class="pointer demi-modal-chevron" style="position: absolute; bottom: 0" data-bind="click: function () { huePubSub.publish('context.panel.visible.editor', false); }"><i class="fa fa-chevron-up"></i></a>
+        <a class="pointer demi-modal-chevron" style="position: absolute; bottom: 0" data-bind="click: function () { huePubSub.publish('context.panel.visible', false); }"><i class="fa fa-chevron-up"></i></a>
       </div>
+      %endif
     </div>
   </div>
 </div>

+ 0 - 1
desktop/libs/notebook/src/notebook/api.py

@@ -88,7 +88,6 @@ def create_notebook(request):
 def create_session(request):
   response = {'status': -1}
 
-  notebook = json.loads(request.POST.get('notebook', '{}'))
   session = json.loads(request.POST.get('session', '{}'))
 
   properties = session.get('properties', [])

+ 1 - 16
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -291,7 +291,7 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
         </li>
         <!-- /ko -->
         <li>
-          <a class="pointer" data-bind="css: {'active': $root.isContextPanelVisible }, click: function() { $root.isContextPanelVisible(!$root.isContextPanelVisible()); }">
+          <a class="pointer" data-bind="publish: {'context.panel.visible': true}">
             <i class="fa fa-fw fa-cogs"></i> ${ _('Session') }
           </a>
         </li>
@@ -497,21 +497,6 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
       </div>
     </div>
   % endif
-
-
-  <div class="context-panel" data-bind="css: {'visible': isContextPanelVisible}" style="${ 'height: 100%' if not is_embeddable else '' }">
-    <ul class="nav nav-tabs">
-      <li class="active"><a href="#sessionsTab" data-toggle="tab">${_('Sessions')}</a></li>
-    </ul>
-
-    <div class="tab-content" style="border: none">
-      <div class="tab-pane active" id="sessionsTab">
-        <div class="row-fluid">
-          <div class="span12" data-bind="template: { name: 'notebook-session-config-template${ suffix }', data: $root }"></div>
-        </div>
-      </div>
-    </div>
-  </div>
 </script>
 
 <script type="text/html" id="notebook-session-config-template${ suffix }">

+ 3 - 88
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -284,15 +284,15 @@
           <li class="divider"></li>
           <!-- ko if: $root.canSave -->
           <li>
-            <a class="share-link pointer" data-bind="click: prepareShareModal,
+            <a href="javascript:void(0)" class="share-link" data-bind="click: prepareShareModal,
               css: {'isShared': isShared()}">
               <i class="fa fa-fw fa-users"></i> ${ _('Share') }
             </a>
           </li>
           <!-- /ko -->
           <li>
-            <a class="pointer" data-bind="css: {'active': $root.isContextPanelVisible }, click: function() { $root.isContextPanelVisible(!$root.isContextPanelVisible()); }">
-              <i class="fa fa-fw fa-cogs"></i> ${ _('Session') }
+            <a href="javascript:void(0)" data-bind="click: showSessionPanel">
+              <i class="fa fa-fw fa-cogs"></i> ${ _('Sessions') }
             </a>
           </li>
         </ul>
@@ -479,91 +479,6 @@
         % endif
       </div>
     </div>
-
-      % if not is_embeddable:
-        <!-- ko if: isRightPanelAvailable -->
-        <div class="resizer" data-bind="visible: isRightPanelVisible, splitDraggable : { isRightPanel: true, appName: 'notebook', leftPanelVisible: isLeftPanelVisible, rightPanelVisible: isRightPanelVisible, rightPanelAvailable: isRightPanelAvailable, onPosition: function(){ huePubSub.publish('split.draggable.position') } }" style="display: none;"><div class="resize-bar">&nbsp;</div></div>
-        <!-- /ko -->
-        <div class="assist-container right-panel" data-bind="visible: isRightPanelVisible() && isRightPanelAvailable()" style="display:none;">
-          <a title="${_('Toggle Assist')}" class="pointer hide-assist-right" data-bind="click: function() { isRightPanelVisible(false); huePubSub.publish('assist.set.manual.visibility'); }">
-            <i class="fa fa-chevron-right"></i>
-          </a>
-          <div class="assist" data-bind="component: {
-          name: 'right-assist-panel',
-          params: {
-            rightAssistAvailable: isRightPanelAvailable
-          }
-        }" >
-          </div>
-        </div>
-      % endif
-
-
-    <div class="context-panel" data-bind="css: {'visible': isContextPanelVisible}" style="${ 'height: 100%' if not is_embeddable else '' }">
-      <ul class="nav nav-tabs">
-        <li class="active"><a href="#sessionsTab" data-toggle="tab">${_('Sessions')}</a></li>
-      </ul>
-
-      <div class="tab-content" style="border: none">
-        <div class="tab-pane active" id="sessionsTab">
-          <div class="row-fluid">
-            <div class="span12" data-bind="template: { name: 'notebook-session-config-template${ suffix }', data: $root }"></div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </script>
-
-  <script type="text/html" id="notebook-session-config-template${ suffix }">
-    <!-- ko with: selectedNotebook() -->
-    <form class="form-horizontal session-config">
-      <fieldset>
-        <!-- ko ifnot: sessions().length -->
-        <p>${ _('There are currently no active sessions, please reload the page.') }</p>
-        <!-- /ko -->
-
-        <!-- ko foreach: sessions -->
-        <h4 style="clear:left; display: inline-block">
-          <span data-bind="text: $parents[1].getSnippetName(type())"></span>
-          <!-- ko if: typeof session_id != 'undefined' && session_id -->
-          <span data-bind="text: session_id"></span>
-          <!-- /ko -->
-        </h4>
-        <div class="session-actions">
-          <a class="inactive-action pointer" title="${ _('Recreate session') }" rel="tooltip" data-bind="click: function() { $parent.restartSession($data) }">
-            <i class="fa fa-refresh" data-bind="css: { 'fa-spin': restarting }"></i> ${ _('Recreate') }
-          </a>
-          <a class="inactive-action pointer margin-left-10" title="${ _('Close session') }" rel="tooltip" data-bind="click: function() { $parent.closeAndRemoveSession($data) }">
-            <i class="fa fa-times"></i> ${ _('Close') }
-          </a>
-          % if conf.USE_DEFAULT_CONFIGURATION.get():
-            <a class="inactive-action pointer margin-left-10" title="${ _('Save session settings as default') }" rel="tooltip" data-bind="click: function() { $parent.saveDefaultUserProperties($data) }"><i class="fa fa-save"></i> ${ _('Set as default settings') }</a>
-          % endif
-          <!-- ko if: type() == 'impala' && typeof http_addr != 'undefined' -->
-          <a class="margin-left-10" data-bind="attr: {'href': http_addr()}" target="_blank">
-            <span data-bind="text: http_addr().replace(/^(https?):\/\//, '')"></span> <i class="fa fa-external-link"></i>
-          </a>
-          <!-- /ko -->
-        </div>
-        % if conf.USE_DEFAULT_CONFIGURATION.get():
-          <div style="width:100%;">
-            <!-- ko component: { name: 'property-selector', params: { properties: properties } } --><!-- /ko -->
-          </div>
-        % endif
-        <div style="clear:both; padding-left: 120px;">
-          <!-- ko if: availableNewProperties().length -->
-          <a class="pointer" style="padding:5px;" data-bind="click: selectedSessionProperty() && function() {
-                      properties.push(ko.mapping.fromJS({'name': selectedSessionProperty(), 'value': ''}));
-                      selectedSessionProperty('');
-                     }" style="margin-left:10px;vertical-align: text-top;">
-          </a>
-          <!-- /ko -->
-        </div>
-        <!-- /ko -->
-        <br/>
-      </fieldset>
-    </form>
-    <!-- /ko -->
   </script>
 
   <script type="text/html" id="snippetIcon${ suffix }">

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است