This is just a first draft of implementation of https://fedorahosted.org/freeipa/ticket/4345

It introduces a `freeipa/extend` module which should serve as a more stable API for Web UI plugins. I think it requires further discussion - what to have there, the level of abstraction...

Other patches fixes navigation/router to support custom URL path patterns.

The usage of facet.create_hash(router) method in patch #649 is questionable.

The WIP patch contains an example of a plugin which uses this functionality.

--
Petr Vobornik
From c7582a3fe8d26b51028e1da24642f2456ae0f7f4 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 16 May 2014 18:32:17 +0200
Subject: [PATCH] webui: plugin API

new `extend` module should serve as a stable API for plugin authors.
It should expose the most commonly used global calls.

https://fedorahosted.org/freeipa/ticket/4345
---
 install/ui/src/freeipa/extend.js | 76 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 76 insertions(+)
 create mode 100644 install/ui/src/freeipa/extend.js

diff --git a/install/ui/src/freeipa/extend.js b/install/ui/src/freeipa/extend.js
new file mode 100644
index 0000000000000000000000000000000000000000..e081aaad60a448d213abe57fc19836d6439def65
--- /dev/null
+++ b/install/ui/src/freeipa/extend.js
@@ -0,0 +1,76 @@
+/*  Authors:
+ *    Petr Vobornik <pvobo...@redhat.com>
+ *
+ * Copyright (C) 2014 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+define([
+    'dojo/_base/lang',
+    './jquery',
+    './phases',
+    './app_container',
+    'exports'
+],function(lang, $, phases, app, extend) {
+
+/**
+ * Extension interface
+ *
+ * This class provides interface for plugins and tries to hide underlying functionality
+ *
+ * @class extend
+ * @singleton
+ */
+lang.mixin(extend, {
+    /**
+     * Adds element to utility section
+     *
+     * This method doesn't do any correction. Expended root node type to add is
+     * by default <li>.
+     *
+     * Preferred phase: any after `init`
+     *
+     * @param {HTMLElement|jQuery} element Element to add to utility section
+     * @return {HTMLElement} Utility node
+     */
+    add_menu_utility: function(element) {
+
+        // Should we check if we are in good stage or atleast report that app doesn't exist yet?
+
+        var $utility = $(app.app.app_widget.nav_util_tool_node);
+        $utility.prepend(element);
+        return $utility.eq(0);
+    },
+
+    /**
+     * Add route to router
+     *
+     * Handle should expect an `event` param. Check `dojo/router` documentation
+     * for more details
+     *
+     * @param {String|RegExp} route A string or regular expression that should
+     *                              be used for  matching hash changes.
+     * @param {Function} handler A callback function which is executed on
+     *                           match. It's context is the router.
+     */
+    add_route: function(route, handler) {
+        var router = app.app.router;
+        router.register_route(route, handler);
+    }
+});
+
+    return extend;
+});
\ No newline at end of file
-- 
1.9.0

From 3c79ff2d2d51ade7d2807ad90f5ddadfb77741ed Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 16 May 2014 18:25:04 +0200
Subject: [PATCH] webui: add parent link to widgets in ContainerMixin

Standard facets sets `facet` attribute to widgets. This one adds
similar, more generic `parent` attribute which should be used for going through
the hierarchy up to top.
---
 install/ui/src/freeipa/widget.js                 | 1 +
 install/ui/src/freeipa/widgets/ContainerMixin.js | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index 212fc1c445e05b5c20c2ccfc7ad4beae4fc5c15e..75bacf0c287750ff0e52e7da20ac804d310e8327 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -5665,6 +5665,7 @@ exp.activity_widget = IPA.activity_widget = function(spec) {
 exp.pre_op = function(spec, context) {
 
     if (context.facet) spec.facet = context.facet;
+    if (context.parent) spec.parent = context.parent;
     if (context.entity) spec.entity = context.entity;
     return spec;
 };
diff --git a/install/ui/src/freeipa/widgets/ContainerMixin.js b/install/ui/src/freeipa/widgets/ContainerMixin.js
index 906d26266da915ce1128cf12891eaf4a08d75c06..82214b94fd5d41cd2763e12757225c82806d4b2b 100644
--- a/install/ui/src/freeipa/widgets/ContainerMixin.js
+++ b/install/ui/src/freeipa/widgets/ContainerMixin.js
@@ -147,6 +147,8 @@ define(['dojo/_base/declare',
             this.widgets = ordered_map();
             var builder_spec = spec.widget_builder || widget_mod.widget_builder;
             this.widget_builder = builder.build(null, builder_spec);
+            this.widget_builder.widget_options =  this.widget_builder.widget_options || {};
+            this.widget_builder.widget_options.parent = this;
         }
     });
 
-- 
1.9.0

From b6994fcd86bf11acf88ff371fed160a407410c84 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 16 May 2014 18:22:56 +0200
Subject: [PATCH] webui: create hash for generic facet

Router is not able to create hash from facet state for custom
routes/facets. This patch allows facets to have their own hash
serialization logic.

Also fixes state serialization issue for non-entity facets.
---
 install/ui/src/freeipa/navigation/Router.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/install/ui/src/freeipa/navigation/Router.js b/install/ui/src/freeipa/navigation/Router.js
index a3b2a6791a290c099fbcab4d2184e0c742c01e54..fd5478559632d9056bb4f47c3fe049bd69bab4fb 100644
--- a/install/ui/src/freeipa/navigation/Router.js
+++ b/install/ui/src/freeipa/navigation/Router.js
@@ -294,7 +294,7 @@ define(['dojo/_base/declare',
          * Creates hash of general facet.
          */
         _create_facet_hash: function(facet, state) {
-            var args = ioquery.objectToQuery(state.args || {});
+            var args = ioquery.objectToQuery(state || {});
             var path = [this.route_prefix, 'p', facet.name];
 
             if (!IPA.is_empty(args)) path.push(args);
@@ -309,7 +309,8 @@ define(['dojo/_base/declare',
          * @param {Object} state
          */
         create_hash: function(facet, state) {
-            if (facet.entity) return this._create_entity_facet_hash(facet, state);
+            if (facet.create_hash) return facet.create_hash(router);
+            else if (facet.entity) return this._create_entity_facet_hash(facet, state);
             else return this._create_facet_hash(facet, state);
         },
 
-- 
1.9.0

From 08654334c15db1a7e1b699fa8dadf5c33950ac05 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 16 May 2014 18:19:19 +0200
Subject: [PATCH] webui: support standalone facets in navigation module

One can access standard standalone facets with:
  `navigation.show('facet_name')`
and completely custom facets with low level call:
  `navigation.show_generic('/custom/hash', facet)``
---
 install/ui/src/freeipa/navigation.js | 22 ++++++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

diff --git a/install/ui/src/freeipa/navigation.js b/install/ui/src/freeipa/navigation.js
index ef0a0bf3f02bed9b3f158deda8aad56f13887244..7cf309f158839ecf55fae40a21e70c83384725d3 100644
--- a/install/ui/src/freeipa/navigation.js
+++ b/install/ui/src/freeipa/navigation.js
@@ -77,7 +77,7 @@ define([
          * type.
          *
          * When facet is defined as a string it has to be registered in
-         * facet register. //FIXME: not yet implemented
+         * facet register.
          *
          * When it's an object (Facet) and has an entity set it will be
          * dealt as entity facet.
@@ -98,9 +98,7 @@ define([
             var facet = params.facet;
 
             if (typeof facet === 'string') {
-                // FIXME: doesn't work at the moment
-                throw 'Not yet supported';
-                //facet = IPA.get_facet(facet);
+                nav.navigate_to_facet(facet, params.args);
             }
 
             if (!facet) throw 'Argument exception: missing facet';
@@ -141,6 +139,22 @@ define([
         },
 
         /**
+         * Uses lower level access
+         *
+         * `experimental`
+         *
+         * Navigates to generic page by changing hash.
+         *
+         * @param  {string} hash Hash of the change
+         * @param  {Object} [facet] Facet we are navigating to. Usually used for
+         *                          notification purposes
+         */
+        show_generic: function(hash, facet) {
+            var nav = get_router();
+            nav.navigate_to_hash(hash, facet);
+        },
+
+        /**
          * Show default facet
          * @method show_default
          */
-- 
1.9.0

From 062c68f895190c14af61da78e7b94cc8106a4805 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 16 May 2014 18:15:37 +0200
Subject: [PATCH] webui: fix excessive registration of state change event
 listeners

`Facet` descendants don't have `container` attribute as opposite to
`facet.facet`. Therefore the registration will happen on every facet
visit.
---
 install/ui/src/freeipa/Application_controller.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/install/ui/src/freeipa/Application_controller.js b/install/ui/src/freeipa/Application_controller.js
index c166e36ee29db21c780e4e635caf90fdd87d7c17..9e7f59393042ef1cc2088222d049e7d4379eb5c5 100644
--- a/install/ui/src/freeipa/Application_controller.js
+++ b/install/ui/src/freeipa/Application_controller.js
@@ -315,7 +315,7 @@ define([
             if (menu_item) this.menu.select(menu_item);
 
             // show facet
-            if (!facet.container) {
+            if (!facet.container_node) {
                 facet.container_node = container.widget.content_node;
                 on(facet, 'facet-state-change', lang.hitch(this, this.on_facet_state_changed));
             }
-- 
1.9.0

From 18fa8f8838335e4603e3f849c467574efd10256e Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 16 May 2014 17:54:11 +0200
Subject: [PATCH] API browser plugin

This is a crude example of a plugin which:
- adds custom dropdown menu to utility area of navigation
- have it's own structure of hash part of URL - custom route+handler
- have a custom facet with custom widget

NOTE: this is ONLY an EXAMPLE. Never add plugins which should be part of
core server to this directory.
---
 install/ui/src/plugins/apibrowser/apibrowser.js | 243 ++++++++++++++++++++++++
 install/ui/src/plugins/apibrowser/jsl.conf      | 128 +++++++++++++
 2 files changed, 371 insertions(+)
 create mode 100644 install/ui/src/plugins/apibrowser/apibrowser.js
 create mode 100644 install/ui/src/plugins/apibrowser/jsl.conf

diff --git a/install/ui/src/plugins/apibrowser/apibrowser.js b/install/ui/src/plugins/apibrowser/apibrowser.js
new file mode 100644
index 0000000000000000000000000000000000000000..94e98a81ebd2ad1bdf3d0e5a0fd7a14208bdef71
--- /dev/null
+++ b/install/ui/src/plugins/apibrowser/apibrowser.js
@@ -0,0 +1,243 @@
+
+define([
+    'dojo/_base/lang',
+    'dojo/_base/declare',
+    'dojo/dom-construct',
+    'dojo/on',
+    'freeipa/jquery',
+    'freeipa/json2',
+    'freeipa/extend',
+    'freeipa/navigation',
+    'freeipa/phases',
+    'freeipa/reg',
+    'freeipa/facets/Facet',
+    'freeipa/widgets/DropdownWidget'
+],function(lang, declare, construct, on, $, JSON, extend, navigation, phases, reg, Facet, DropdownWidget) {
+
+/**
+ * Widget which display facet's public state.
+ *
+ * Public state usually corresponds to hash part of URL (facet doesn't care,
+ * it's handle by different subsystem.
+ *
+ * The simplest widget just has to implement `create` or `render` method and
+ * set couple properties (handled by mixing-in spec)
+ *
+ * @class
+ */
+var StateWidget = declare([], {
+
+    init: function() {
+        // not really nice code. It should be injected into the widget.
+        var facet = this.facet || this.parent;
+        if (facet) {
+            on(facet, 'facet-state-change', lang.hitch(this, this.on_state_change));
+        }
+    },
+
+    on_state_change: function(event) {
+        if (!this.dom_node) return;
+        this.render_hash();
+    },
+
+    render: function() {
+        this.dom_node = construct.create('div', {
+            'class': this['class']
+        });
+
+        if (this.container_node) {
+            construct.place(this.dom_node, this.container_node);
+        }
+        this.render_hash();
+        return this.dom_node;
+    },
+
+    render_hash: function() {
+        construct.empty(this.dom_node);
+        this.dom_node.innerHTML = JSON.stringify(this.parent.state.clone());
+    },
+
+    constructor: function(spec) {
+        spec = spec || {};
+        declare.safeMixin(this, spec);
+    }
+
+});
+
+/**
+ * BrowserFacet
+ *
+ * We require custom facet because of non-standard URL hash part.
+ *
+ * @class
+ */
+var BrowserFacet = declare([Facet], {
+
+    /**
+     * Custom serialization of facet state into URL hash.
+     *
+     * This method might be removed and moved to different component since
+     * it should not be Facet's responsibility.
+     *
+     * @return {string} hash
+     */
+    create_hash: function(router) {
+
+        var state = this.state.clone();
+        var path = [router.route_prefix, 'api'];
+        if (state.uri) {
+            path.push(state.uri);
+        }
+        if (state.urlparam) {
+            path.push(state.urlparam);
+        }
+
+        var hash = path.join('/');
+        window.console.log(hash);
+        return hash;
+    }
+});
+
+
+
+/**
+ * API Browser plugin
+ *
+ * Browse API
+ *
+ * @class apibrowser
+ * @singleton
+ */
+var apibrowser = {
+
+    menu_dropdown: null,
+
+    facet_spec: {
+        name: 'apibrowser',
+
+        // use:
+        // - `main` to show navigation on the top
+        // - `simple` to fill entire page
+        preferred_container: 'main',
+
+        'class': 'container-fluid', // for styling
+        widgets: [
+            // some widgets
+            {
+                $type: 'html',
+                name: 'html',
+                html: '<h1>API Browser</h1>'
+            },
+            {
+                $type: 'api_state',
+                name: 'state'
+            }
+        ]
+    },
+
+    /**
+     * Create and add dropdown menu to utility area of navigation
+     */
+    create_menu_dropdown: function() {
+
+        this.menu_dropdown = new DropdownWidget({
+            el_type:  'li',
+            name: 'api-browser-menu',
+            right_aligned: true,
+            // can be uncommented if you don't want to implement `create_menu_toggle`
+            // toggle_text: 'API Browser ',
+            // toggle_class: '',
+            // toggle_icon: 'fa fa-angle-down',
+            items: [
+                {
+                    name: 'browser',
+                    label: 'Browser',
+                    icon: 'fa-user',
+                    data: '/api/foo'
+                },
+                {
+                    'class': 'divider'
+                },
+                {
+                    name: 'about',
+                    label: 'Browser 2',
+                    icon: 'fa-question',
+                    data: '/api/foo/bar/abc=baz'
+                }
+            ]
+        });
+        on(this.menu_dropdown, 'item-click', lang.hitch(this, this.on_dropdown_click));
+
+        this.menu_dropdown.set('toggle_content', this.create_menu_toggle());
+        var el = this.menu_dropdown.render();
+
+        extend.add_menu_utility(el);
+    },
+
+    /**
+     * This is optional but it's a way to add icon before text
+     */
+    create_menu_toggle: function() {
+        var nodes = [];
+        nodes.push(construct.create('span', { 'class': 'fa fa-star' }));
+        nodes.push(construct.create('span', { innerHTML: ' API browser' }));
+        nodes.push(construct.create('b', { 'class': 'caret' }));
+        return nodes;
+    },
+
+    on_dropdown_click: function (item) {
+
+        // you can make decision on item properties
+        //
+        // by default just pass hash to router
+
+        var facet = reg.facet.get('apibrowser');
+        navigation.show_generic(item.data, facet);
+        // there is a bug that it doesn't collapse the menu on small screens
+    },
+
+    /**
+     * Handler of our custom route
+     */
+    route_handler: function(event, router) {
+        if (router.check_clear_ignore()) return;
+
+        // always get apibrowser facet
+        var facet = reg.facet.get('apibrowser');
+        facet.reset_state(event.params);
+        router.show_facet(facet);
+    },
+
+    register: function () {
+        var fa = reg.facet;
+        var w = reg.widget;
+
+        fa.register({
+            type: 'apibrowser',
+            factory: BrowserFacet,
+            spec: this.facet_spec
+        });
+
+        w.register('api_state', StateWidget);
+        w.register_post_op('api_state', function(obj, spec, context) {
+            obj.init();
+            return obj;
+        });
+    },
+
+    initialize: function () {
+        this.create_menu_dropdown();
+        var self = this;
+
+        // add route to router
+        extend.add_route('/api/*urlparam', function(event) {
+            self.route_handler(event, this);
+        });
+    }
+};
+
+phases.on('registration', lang.hitch(apibrowser, apibrowser.register));
+phases.on('init', lang.hitch(apibrowser, apibrowser.initialize), 20);
+
+    return apibrowser;
+});
\ No newline at end of file
diff --git a/install/ui/src/plugins/apibrowser/jsl.conf b/install/ui/src/plugins/apibrowser/jsl.conf
new file mode 100644
index 0000000000000000000000000000000000000000..ef43a7cdb5a7ae7745c45ade516bfe7fb9f46267
--- /dev/null
+++ b/install/ui/src/plugins/apibrowser/jsl.conf
@@ -0,0 +1,128 @@
+#
+# Configuration File for JavaScript Lint 0.3.0
+# Developed by Matthias Miller (http://www.JavaScriptLint.com)
+#
+# This configuration file can be used to lint a collection of scripts, or to enable
+# or disable warnings for scripts that are linted via the command line.
+#
+
+### Warnings
+# Enable or disable warnings based on requirements.
+# Use "+WarningName" to display or "-WarningName" to suppress.
+#
++no_return_value              # function {0} does not always return a value
++duplicate_formal             # duplicate formal argument {0}
++equal_as_assign              # test for equality (==) mistyped as assignment (=)?{0}
++var_hides_arg                # variable {0} hides argument
++redeclared_var               # redeclaration of {0} {1}
++anon_no_return_value         # anonymous function does not always return a value
++missing_semicolon            # missing semicolon
++meaningless_block            # meaningless block; curly braces have no impact
++comma_separated_stmts        # multiple statements separated by commas (use semicolons?)
++unreachable_code             # unreachable code
++missing_break                # missing break statement
++missing_break_for_last_case  # missing break statement for last case in switch
++comparison_type_conv         # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==)
++inc_dec_within_stmt          # increment (++) and decrement (--) operators used as part of greater statement
++useless_void                 # use of the void type may be unnecessary (void is always undefined)
++multiple_plus_minus          # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs
++use_of_label                 # use of label
+-block_without_braces         # block statement without curly braces
++leading_decimal_point        # leading decimal point may indicate a number or an object member
++trailing_decimal_point       # trailing decimal point may indicate a number or an object member
++octal_number                 # leading zeros make an octal number
++nested_comment               # nested comment
++misplaced_regex              # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma
++ambiguous_newline            # unexpected end of line; it is ambiguous whether these lines are part of the same statement
++empty_statement              # empty statement or extra semicolon
+-missing_option_explicit      # the "option explicit" control comment is missing
++partial_option_explicit      # the "option explicit" control comment, if used, must be in the first script tag
++dup_option_explicit          # duplicate "option explicit" control comment
++useless_assign               # useless assignment
++ambiguous_nested_stmt        # block statements containing block statements should use curly braces to resolve ambiguity
++ambiguous_else_stmt          # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent)
++missing_default_case         # missing default case in switch statement
++duplicate_case_in_switch     # duplicate case in switch statements
++default_not_at_end           # the default case is not at the end of the switch statement
++legacy_cc_not_understood     # couldn't understand control comment using /*@keyword@*/ syntax
++jsl_cc_not_understood        # couldn't understand control comment using /*jsl:keyword*/ syntax
++useless_comparison           # useless comparison; comparing identical expressions
++with_statement               # with statement hides undeclared variables; use temporary variable instead
++trailing_comma_in_array      # extra comma is not recommended in array initializers
++assign_to_function_call      # assignment to a function call
++parseint_missing_radix       # parseInt missing radix parameter
+
+
+### Output format
+# Customize the format of the error message.
+#    __FILE__ indicates current file path
+#    __FILENAME__ indicates current file name
+#    __LINE__ indicates current line
+#    __ERROR__ indicates error message
+#
+# Visual Studio syntax (default):
++output-format __FILE__(__LINE__): __ERROR__
+# Alternative syntax:
+#+output-format __FILE__:__LINE__: __ERROR__
+
+
+### Context
+# Show the in-line position of the error.
+# Use "+context" to display or "-context" to suppress.
+#
++context
+
+
+### Semicolons
+# By default, assignments of an anonymous function to a variable or
+# property (such as a function prototype) must be followed by a semicolon.
+#
++lambda_assign_requires_semicolon
+
+
+### Control Comments
+# Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for
+# the /*@keyword@*/ control comments and JScript conditional comments. (The latter is
+# enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason,
+# although legacy control comments are enabled by default for backward compatibility.
+#
++legacy_control_comments
+
+
+### JScript Function Extensions
+# JScript allows member functions to be defined like this:
+#     function MyObj() { /*constructor*/ }
+#     function MyObj.prototype.go() { /*member function*/ }
+#
+# It also allows events to be attached like this:
+#     function window::onload() { /*init page*/ }
+#
+# This is a Microsoft-only JavaScript extension. Enable this setting to allow them.
+#
+-jscript_function_extensions
+
+
+### Defining identifiers
+# By default, "option explicit" is enabled on a per-file basis.
+# To enable this for all files, use "+always_use_option_explicit"
+#-always_use_option_explicit
++always_use_option_explicit
+
+# Define certain identifiers of which the lint is not aware.
+# (Use this in conjunction with the "undeclared identifier" warning.)
+#
+# Common uses for webpages might be:
++define window
++define document
++define alert
++define define
++define require
+
+### Files
+# Specify which files to lint
+# Use "+recurse" to enable recursion (disabled by default).
+# To add a set of files, use "+process FileName", "+process Folder\Path\*.js",
+# or "+process Folder\Path\*.htm".
+#
+
++process ./*.js
\ No newline at end of file
-- 
1.9.0

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to