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

HUE-9125 [about] Create a component for the configuration page with filter

Johan Ahlen 5 роки тому
батько
коміт
68e70a6b96

+ 15 - 0
desktop/core/src/desktop/js/api/apiHelper.js

@@ -29,6 +29,7 @@ const SAMPLE_API_PREFIX = '/notebook/api/sample/';
 const EXECUTE_API_PREFIX = '/notebook/api/execute/';
 const DOCUMENTS_API = '/desktop/api2/doc/';
 const DOCUMENTS_SEARCH_API = '/desktop/api2/docs/';
+const GET_HUE_CONFIG_URL = '/desktop/api2/get_hue_config';
 const FETCH_CONFIG = '/desktop/api2/get_config/';
 const HDFS_API_PREFIX = '/filebrowser/view=' + encodeURIComponent('/');
 const ADLS_API_PREFIX = '/filebrowser/view=' + encodeURIComponent('adl:/');
@@ -2905,6 +2906,20 @@ class ApiHelper {
     return self.simpleGet(url, undefined, options);
   }
 
+  async fetchHueConfigAsync(options) {
+    return new Promise((resolve, reject) => {
+      $.get(GET_HUE_CONFIG_URL)
+        .done(response => {
+          if (!response && response.status === -1) {
+            reject(response.message);
+          } else {
+            resolve(response);
+          }
+        })
+        .fail(reject);
+    });
+  }
+
   getClusterConfig(data) {
     return $.post(FETCH_CONFIG, data);
   }

+ 217 - 0
desktop/core/src/desktop/js/apps/about/components/ko.hueConfigTree.js

@@ -0,0 +1,217 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import componentUtils from 'ko/components/componentUtils';
+import I18n from 'utils/i18n';
+import DisposableComponent from 'ko/components/DisposableComponent';
+
+export const NAME = 'hue-config-tree';
+
+// prettier-ignore
+const TEMPLATE = `
+<script id="config-values-template" type="text/html">
+  <tr>
+    <th>
+    <!-- ko if: config.is_anonymous -->
+      <i>(${ I18n('default section') })</i>
+    <!-- /ko -->
+    <!-- ko ifnot: config.is_anonymous -->
+      <span data-bind="text: config.key"></span>
+    <!-- /ko -->
+    </th>
+    <td data-bind="css: { 'border-top': depth === 1 ? '0' : null }">
+      <!-- ko if: typeof config.values !== 'undefined' -->
+        <!-- ko if: config.help || !config.values.length -->
+        <i data-bind="text: config.help || '${ I18n('No help available.') }'"></i>
+        <!-- /ko -->
+        <table class="table table-condensed recurse">
+          <tbody>
+            <!-- ko foreach: config.values -->
+              <!-- ko template: { name: 'config-values-template', data: { config: $data, depth: $parent.depth + 1 } } --><!-- /ko -->
+            <!-- /ko -->
+          </tbody>
+        </table>
+        <!-- /ko -->
+      <!-- ko if: typeof config.value !== 'undefined' -->
+        <code data-bind="text: config.value"></code><br/>
+        <i data-bind="text: config.help || '${ I18n('No help available.') }'"></i>
+        <span class="muted">${ I18n('Default:') } <i data-bind="text: config.default"></i></span>
+      <!-- /ko -->
+    </td>
+  </tr>
+</script>
+
+<div class="container-fluid">
+  <!-- ko hueSpinner: { spin:  loading, center: true, size: 'large' } --><!-- /ko -->
+  <!-- ko ifnot: loading -->
+    <!-- ko if: errorMessage -->
+      <div class="alert alert-error" style="margin: 20px" data-bind="text: errorMessage"></div>
+    <!-- /ko -->
+    <!-- ko ifnot: errorMessage -->
+      <div class="row-fluid">
+        <div class="card card-home">
+          <div class="pull-right muted margin-top-30">
+            ${ I18n('Configuration files located in') } <code style="color: #0B7FAD" data-bind="text: configDir"></code>
+          </div>
+          <h1 class="inline-block margin-top-20">
+            ${ I18n('Sections') }
+          </h1>
+          <form class="form-inline" autocomplete="off">
+            <input class="inline-autocomp-input" type="text" ${ window.PREVENT_AUTOFILL_INPUT_ATTRS } placeholder="${ I18n('Filter...') }" data-bind="textInput: filter, clearable: { value: filter }">
+          </form>
+          <div class="card-body clearfix">
+            <div class="span2">
+              <ul class="nav nav-pills nav-stacked" data-bind="foreach: filteredSections">
+                <li data-bind="
+                    css: { 'active': $data.key === $parent.selectedKey() },
+                    click: function () { $parent.selectedKey($data.key) }
+                  ">
+                  <a href="javascript: void(0);" data-bind="text: key"></a>
+                </li>
+              </ul>
+            </div>
+            <div class="span10" data-bind="with: selectedConfig">
+              <div class="tab-content">
+                <div class="tab-pane active">
+                  <!-- ko if: values.length -->
+                  <table class="table table-condensed recurse">
+                    <tbody data-bind="foreach: values">
+                      <!-- ko template: { name: 'config-values-template', data: { config: $data, depth: 1 } } --><!-- /ko -->
+                    </tbody>
+                  </table>
+                  <!-- /ko -->
+                  <!-- ko ifnot: values.length -->
+                    <i>${ I18n('Empty configuration section') }</i>
+                  <!-- /ko -->
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="card card-home" style="margin-top: 50px">
+          <h2 class="card-heading simple">${ I18n('Installed Applications') }</h2>
+          <div class="card-body" data-bind="foreach: apps">
+            <!-- ko if: has_ui -->
+              <a href="javascript: void(0);" data-bind="hueLink: display_name"><span class="badge badge-info" data-bind="text: name"></span></a>
+            <!-- /ko -->
+            <!-- ko ifnot: has_ui -->
+              <span class="badge" title="${ I18n('This app does not have a UI') }" data-bind="text: name"></span>
+            <!-- /ko -->
+          </div>
+        </div>
+      </div>
+    <!-- /ko -->
+  <!-- /ko -->
+</div>
+`;
+
+const filterConfig = (config, lowerCaseFilter) => {
+  if (
+    (config.value && config.value.indexOf(lowerCaseFilter) !== -1) ||
+    (!config.is_anonymous && config.key.indexOf(lowerCaseFilter) !== -1)
+  ) {
+    return config;
+  }
+
+  if (config.values) {
+    const values = [];
+    config.values.forEach(val => {
+      const filtered = filterConfig(val, lowerCaseFilter);
+      if (filtered) {
+        values.push(filtered);
+      }
+    });
+    if (values.length) {
+      return {
+        key: config.key,
+        is_anonymous: config.is_anonymous,
+        help: config.help,
+        values: values
+      };
+    }
+  }
+};
+
+class HueConfigTree extends DisposableComponent {
+  constructor(params) {
+    super();
+    this.loading = ko.observable(true);
+
+    this.filter = ko.observable().extend({ throttle: 500 });
+
+    this.errorMessage = ko.observable();
+    this.apps = ko.observableArray();
+    this.config = ko.observableArray();
+    this.configDir = ko.observable();
+    this.selectedKey = ko.observable();
+
+    this.filteredSections = ko.pureComputed(() => {
+      if (!this.filter()) {
+        return this.config();
+      }
+      const lowerCaseFilter = this.filter().toLowerCase();
+
+      const foundConfigs = [];
+      let selectedFound = false;
+      this.config().forEach(config => {
+        const filtered = filterConfig(config, lowerCaseFilter);
+        if (filtered) {
+          foundConfigs.push(filtered);
+          if (this.selectedKey() && this.selectedKey() === filtered.key) {
+            selectedFound = true;
+          }
+        }
+      });
+
+      if (!selectedFound) {
+        if (foundConfigs.length) {
+          this.selectedKey(foundConfigs[0].key);
+        } else {
+          this.selectedKey(undefined);
+        }
+      }
+
+      return foundConfigs;
+    });
+
+    this.selectedConfig = ko.pureComputed(() =>
+      this.filteredSections().find(section => section.key === this.selectedKey())
+    );
+
+    this.load();
+  }
+
+  async load() {
+    this.loading(true);
+    this.errorMessage(undefined);
+    try {
+      const response = await apiHelper.fetchHueConfigAsync();
+      this.config(response.config);
+      this.configDir(response.conf_dir);
+      this.apps(response.apps);
+      this.selectedKey(this.config().length ? this.config()[0].key : undefined);
+    } catch (err) {
+      this.errorMessage(err);
+    }
+    this.loading(false);
+  }
+}
+
+componentUtils.registerComponent(NAME, HueConfigTree, TEMPLATE);

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

@@ -168,6 +168,9 @@ import 'ko/components/ko.shareGistModal';
 import 'ko/components/ko.sidebar';
 import 'ko/components/ko.sqlColumnsTable';
 
+// TODO: Move to about app when it has it's own webpack entry
+import 'apps/about/components/ko.hueConfigTree';
+
 import 'ko/extenders/ko.maxLength';
 import 'ko/extenders/ko.numeric';
 import 'ko/extenders/ko.toJson';

+ 8 - 132
desktop/core/src/desktop/templates/dump_config.mako

@@ -18,7 +18,6 @@
 import logging
 import sys
 
-from desktop.lib.conf import BoundContainer, is_anonymous
 from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 
@@ -27,10 +26,7 @@ LOG = logging.getLogger(__name__)
 
 <%namespace name="layout" file="about_layout.mako" />
 
-% if not is_embeddable:
-  ${ commonheader(_('Configuration'), "about", user, request, "70px") | n,unicode }
-% endif
-  ${ layout.menubar(section='dump_config') }
+${ layout.menubar(section='dump_config') }
 
 <style type="text/css">
   .card-heading .pull-right {
@@ -39,132 +35,12 @@ LOG = logging.getLogger(__name__)
   }
 </style>
 
-<div class="container-fluid">
-  <div class="row-fluid">
-    <div class="card card-home">
-      <div class="pull-right muted">
-        ${_('Configuration files located in')} <code style="color: #0B7FAD">${conf_dir}</code>
-      </div>
-      <h1 class="margin-top-20 margin-bottom-30">
-        ${_('Sections')}
-      </h1>
-      <div class="card-body clearfix">
-        <div class="span2">
-          <ul class="nav nav-pills nav-stacked">
-            % for obj in top_level:
-              <li
-                % if loop.first:
-                    class="active"
-                % endif
-              >
-                <a href="#${ obj.config.key }Conf" data-toggle="tab">${ obj.config.key }</a>
-              </li>
-            % endfor
-          </ul>
-        </div>
-        <div class="span10">
-          ${showTopLevel(top_level)}
-        </div>
-      </div>
-    </div>
-
-    <div class="card card-home" style="margin-top: 50px">
-      <h2 class="card-heading simple">${_('Installed Applications')}</h2>
-      <div class="card-body">
-      % for app in apps:
-        % if app.menu_index != 999:
-          <a href="/${app.display_name}"><span class="badge badge-info">${app.name}</span></a>
-        % else:
-          <span class="badge" title="${ _('This app does not have a UI') }">${app.name}</span>
-        % endif
-      % endfor
-      </div>
-    </div>
-
-  </div>
-
-  <%def name="showTopLevel(config_obj, depth=0)">
-    <div class="tab-content">
-      % for v in config_obj:
-      <%
-          # Don't recurse into private variables.
-          if v.config.private and not show_private:
-              continue
-      %>
-      <div id="${v.config.key}Conf" class="tab-pane
-        % if loop.first:
-            active
-        % endif
-        ">
-        ${ recurse(v, depth + 1) }
-      </div>
-      % endfor
-    </div>
-  </%def>
-
-    <%def name="recurseList(config_obj, depth=0)">
-      <table class="table table-condensed recurse">
-      % for v in config_obj:
-        <%
-          # Don't recurse into private variables.
-          if v.config.private and not show_private:
-            continue
-        %>
-        ${ recurse(v, depth + 1) }
-      % endfor
-      </table>
-    </%def>
-
-    <%def name="recurse(config_obj, depth=0)">
-        <tr>
-         % if depth > 1:
-          <th>
-          % if is_anonymous(config_obj.config.key):
-            <i>(default section)</i>
-          % else:
-            ${ config_obj.config.key }
-          % endif
-          </th>
-         % endif
-         % if depth == 1:
-            <td style="border-top:0">
-         % else:
-            <td>
-         % endif
-          % if isinstance(config_obj, BoundContainer):
-            % if config_obj.config.help or not config_obj.get().values():
-            <i>${ config_obj.config.help or _('No help available.') }</i>
-            % endif
-            ${ recurseList(config_obj.get().values(), depth + 1) }
-          % else:
-            <code>
-            % if 'password' in config_obj.config.key:
-              ${ "*" * 10 }
-            % else:
-              ${ config_obj.get_raw() if sys.version_info[0] > 2 else str(config_obj.get_raw()).decode('utf-8', 'replace') }
-            % endif
-              <%
-                config_str = None
-                try:
-                  config_str = str(config_obj.get_raw()) if sys.version_info[0] > 2 else str(config_obj.get_raw()).decode('utf-8', 'replace')
-                except:
-                  LOG.exception("Potential misconfiguration. Error value of key '%s' in configuration." % config_obj.grab_key)
-              %>
-              % if config_str == '':
-              &nbsp;
-              % endif
-            </code><br/>
-              % if config_obj.config.help:
-                <i>${ config_obj.config.help or _('No help available.') }</i>
-              % endif
-            <span class="muted">${ _('Default:') } <i>${ config_obj.config.default if sys.version_info[0] > 2 else str(config_obj.config.default).decode('utf-8', 'replace') }</i></span>
-          % endif
-          </td>
-        </tr>
-    </%def>
-
+<div id="aboutConfiguration">
+  <!-- ko component: { name: 'hue-config-tree' } --><!-- /ko -->
 </div>
 
-%if not is_embeddable:
-${ commonfooter(request, messages) | n,unicode }
-%endif
+<script type="text/javascript">
+  $(document).ready(function () {
+    ko.applyBindings({}, $('#aboutConfiguration')[0]);
+  });
+</script>

+ 1 - 24
desktop/core/src/desktop/views.py

@@ -281,33 +281,10 @@ def status_bar(request):
 
 
 def dump_config(request):
-  show_private = False
-  conf_dir = os.path.realpath(os.getenv("HUE_CONF_DIR", get_desktop_root("conf")))
-
   if not is_admin(request.user):
     return HttpResponse(_("You must be a superuser."))
 
-  if request.GET.get("private"):
-    show_private = True
-
-  app_modules = appmanager.DESKTOP_MODULES
-  config_modules = GLOBAL_CONFIG.get().values()
-  if ENABLE_CONNECTORS.get():
-    app_modules = [app_module for app_module in app_modules if app_module.name == 'desktop']
-    config_modules = [config_module for config_module in config_modules if config_module.config.key == 'desktop']
-
-  apps = sorted(app_modules, key=lambda app: app.name)
-  apps_names = [app.name for app in apps]
-  top_level = sorted(config_modules, key=lambda obj: apps_names.index(obj.config.key))
-
-  return render("dump_config.mako", request, {
-      'show_private': show_private,
-      'top_level': top_level,
-      'conf_dir': conf_dir,
-      'is_embeddable': request.GET.get('is_embeddable', False),
-      'apps': apps
-    }
-  )
+  return render("dump_config.mako", request, {})
 
 
 @access_log_level(logging.WARN)