For those of you who don't want to try the patches:
* https://pvoborni.fedorapeople.org/images/api-user-show.png
* https://pvoborni.fedorapeople.org/images/api-user-add.png

On 07/01/2015 09:35 AM, Martin Kosek wrote:
On 06/30/2015 06:35 PM, Petr Vobornik wrote:
First part of API Browser - displaying the metadata in more consumable way.

Second part, how to use it in different languages will be written as wiki pages
first.

The browser could be later enhanced with more infos and tooltips.

Patch 886 extends backend to send more metadata.
Patch 887,888,889 are webui fixes and prerequisites
Patch 890 is the API browser

Thanks, this is a very good start. I looked at a VM with the patches and have
couple usability suggestions:

1) It was hard for me to find where the API Browser is. But "IPA Server" looks
as a good tab where it should be though.

could be moved to "Help" tab when it's introduced. For that we need at least one more link.


2) I have strong doubts about the "Objects" tab, this is only understandable to
users knowledgeable about FreeIPA framework internals. Common API user who just
want to consume the API and not know about the internals will not know what
this is.

What I would do is make "API Browser" directly clickable so that it opens the
Commands tab. This is what most people will use. Other tabs may be stacked on
the left just like with Staged or Deleted users. For now, I would hide Objects
as I think it would cause more confusion. If we want to show it, there should
be some introduction what it is good for and maybe limitation of showed fields
to only those that has any value for the consumers.

fixed, there is only "API Browser" and no submenu


3) In Commands tab, we will some more explanatory what the attributes of Param
needs and probably hide some. For example "exclude" is not needed for consumers.


Attributes as follows were kept: label, type, default, default_from, values, minlength, maxlength, pattern, minvalue, maxvalue, precision, cli_name, option_group

4) Many attributes have "autofill": True. I wonder how usable it is without
knowing the actual default for the attribute. Can we show the default?

default_from now contains list of attrs which are used for the default value, e.g.:
  default value created from: givenname, sn



5) I would hide "Output Params" all together given we don't have them set up
correctly in FreeIPA framework and they may rather confuse people, with having
all the HBAC or SUDO with User objects.


Removed from metadata

I may think about it more, there were just my couple first thoughts. Others may
have different opinions here.

Martin


Other changes:
* cli options are shown with dashes as in CLI
* required and multivalued were changed into tags next to option name. 'flags' which were shown as the tags are not displayed anymore


updated patches attached.

--
Petr Vobornik
From 92cc50ebfce0aede3d823b9df5f52ea83e5aa368 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 5 Jun 2015 19:03:46 +0200
Subject: [PATCH] webui: API browser

First part of API browser - displaying metadata in more consumable way.

https://fedorahosted.org/freeipa/ticket/3129
---
 install/ui/doc/categories.json                     |   4 +-
 install/ui/less/widgets.less                       |  14 +
 install/ui/src/freeipa/app.js                      |   1 +
 install/ui/src/freeipa/navigation/menu_spec.js     |   6 +
 install/ui/src/freeipa/plugins/api_browser.js      | 106 +++++
 install/ui/src/freeipa/widgets/APIBrowserWidget.js | 383 ++++++++++++++++
 install/ui/src/freeipa/widgets/browser_widgets.js  | 503 +++++++++++++++++++++
 7 files changed, 1016 insertions(+), 1 deletion(-)
 create mode 100644 install/ui/src/freeipa/plugins/api_browser.js
 create mode 100644 install/ui/src/freeipa/widgets/APIBrowserWidget.js
 create mode 100644 install/ui/src/freeipa/widgets/browser_widgets.js

diff --git a/install/ui/doc/categories.json b/install/ui/doc/categories.json
index 83c24c5efa7f8e52188a30452a9b3119d831592b..3a7c2ebc2d6cdee34d48cc72f94ce845bd73d7e4 100644
--- a/install/ui/doc/categories.json
+++ b/install/ui/doc/categories.json
@@ -39,7 +39,8 @@
                 "classes": [
                     "facet.facet",
                     "facets.Facet",
-                    "*_facet"
+                    "*_facet",
+                    "*Facet"
                 ]
             },
             {
@@ -254,6 +255,7 @@
                     "stageuser",
                     "topology",
                     "user",
+                    "plugins.api_browser",
                     "plugins.load",
                     "plugins.login",
                     "plugins.login_process",
diff --git a/install/ui/less/widgets.less b/install/ui/less/widgets.less
index 066e074a044fc25f88ca8060c6ead2edd9d11cb0..7778f6bf46b3bbebf99fff4a7799fe4b0b090385 100644
--- a/install/ui/less/widgets.less
+++ b/install/ui/less/widgets.less
@@ -117,5 +117,19 @@
     padding-left: 10px
 }
 
+.apibrowser {
+    .item-select input[type=text] {
+        width: 100%;
+        padding-left: 5px;
+    }
+    .label {
+        margin-left: 5px; // spacing between param flags
+    }
+    .prop-label {
+        text-align: right;
+        font-weight: 300;
+    }
+}
+
 // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=409254
 tbody:empty { display: none; }
\ No newline at end of file
diff --git a/install/ui/src/freeipa/app.js b/install/ui/src/freeipa/app.js
index 9b290ab0eee216f8b8adb3181a1b3e7ac22fb351..f05e8213c0b17e21515fdfce5ab496516a02692e 100644
--- a/install/ui/src/freeipa/app.js
+++ b/install/ui/src/freeipa/app.js
@@ -24,6 +24,7 @@ define([
     './plugins/sync_otp',
     './plugins/login',
     './plugins/login_process',
+    './plugins/api_browser',
     // entities
     './aci',
     './automember',
diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js
index c35445f12e36c5dc49033723d8efdcaed7331002..120cba37dca7aa355bdb94b1ef16615b95afeb28 100644
--- a/install/ui/src/freeipa/navigation/menu_spec.js
+++ b/install/ui/src/freeipa/navigation/menu_spec.js
@@ -206,6 +206,12 @@ var nav = {};
                         }
                     ]
                 },
+                {
+                    name: 'apibrowser',
+                    label: 'API browser',
+                    facet: 'apibrowser',
+                    args: { 'type': 'command' }
+                },
                 { entity: 'config' }
             ]
         }
diff --git a/install/ui/src/freeipa/plugins/api_browser.js b/install/ui/src/freeipa/plugins/api_browser.js
new file mode 100644
index 0000000000000000000000000000000000000000..88dbe59c7358fd0c8c3fe41ff1b7140ce3086f7e
--- /dev/null
+++ b/install/ui/src/freeipa/plugins/api_browser.js
@@ -0,0 +1,106 @@
+//
+// Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+//
+
+define(['dojo/_base/declare',
+        'dojo/_base/lang',
+        'dojo/on',
+        '../facets/Facet',
+        '../phases',
+        '../reg',
+        '../widget',
+        '../widgets/APIBrowserWidget',
+        '../builder'
+       ],
+
+    function(declare, lang, on, Facet, phases, reg, widget,
+        APIBrowserWidget, builder) {
+
+
+var plugins = {}; // dummy namespace object
+
+/**
+ * API browser plugin
+ *
+ * @class
+ * @singleton
+ */
+plugins.api_browser = {};
+
+plugins.api_browser.facet_spec = {
+    name: 'apibrowser',
+    'class': 'apibrowser container-fluid',
+    widgets: [
+        {
+            $type: 'activity',
+            name: 'activity',
+            text: 'Working',
+            visible: false
+        },
+        {
+            $type: 'apibrowser',
+            name: 'apibrowser'
+        }
+    ]
+};
+
+/**
+ * API browser facet
+ * @class
+ */
+plugins.api_browser.APIBrowserFacet = declare([Facet], {
+
+    init: function(spec) {
+        this.inherited(arguments);
+        var browser = this.get_widget('apibrowser');
+
+        on(this, 'show', lang.hitch(this, function(args) {
+
+            var state = this.get_state();
+            var t = state.type;
+            var n = state.name;
+
+            if (t && n) {
+                browser.show_item(t, n);
+                return;
+            } else if (t) {
+                if (t == 'command') {
+                    browser.show_default_command();
+                    return;
+                } else {
+                    browser.show_default_object();
+                    return;
+                }
+            }
+            browser.show_default();
+            return;
+        }));
+
+        // Reflect item change in facet state and therefore URL hash
+        browser.watch('current', lang.hitch(this, function(name, old, value) {
+            var state = {};
+            if (value.type && value.name) {
+                state = { type: value.type, name: value.name };
+            }
+            this.set_state(state);
+        }));
+    }
+});
+
+phases.on('registration', function() {
+
+    var fa = reg.facet;
+    var w = reg.widget;
+
+    w.register('apibrowser', APIBrowserWidget);
+
+    fa.register({
+        type: 'apibrowser',
+        factory: plugins.api_browser.APIBrowserFacet,
+        spec: plugins.api_browser.facet_spec
+    });
+});
+
+return plugins.api_browser;
+
+});
\ No newline at end of file
diff --git a/install/ui/src/freeipa/widgets/APIBrowserWidget.js b/install/ui/src/freeipa/widgets/APIBrowserWidget.js
new file mode 100644
index 0000000000000000000000000000000000000000..149a22fff4902a0f3ffb57d2b0e1aa904c973174
--- /dev/null
+++ b/install/ui/src/freeipa/widgets/APIBrowserWidget.js
@@ -0,0 +1,383 @@
+//
+// Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+//
+
+define([
+    'dojo/_base/declare',
+    'dojo/_base/lang',
+    'dojo/on',
+    'dojo/Evented',
+    'dojo/Stateful',
+    '../jquery',
+    '../ipa',
+    '../metadata',
+    '../reg',
+    '../text',
+    '../util',
+    './ListViewWidget',
+    './browser_widgets'
+], function(declare, lang, on, Evented, Stateful, $, IPA, metadata, reg, text,
+    util, ListViewWidget, browser_widgets) {
+
+var widgets = {};
+
+/**
+ * API browser widget
+ *
+ * Consists of two parts: command browser and details.
+ *
+ * Command browser consist of:
+ * - filter
+ * - list view with commands
+ *
+ * Details could be:
+ * - command
+ * - object
+ * - param
+ *
+ * @class
+ */
+widgets.APIBrowserWidget = declare([Stateful, Evented], {
+
+    // widgets
+    filter_w: null,
+    list_w: null,
+    object_detail_w: null,
+    command_detail_w: null,
+    param_detail_w: null,
+    current_details_w: null, // Current details widget, one of the three above
+
+    // nodes
+    container_node: null,
+    el: null,
+    default_view_el: null,
+    details_view_el: null,
+    current_view: null, // either default_view_el or details_view_el
+    filter_el: null,
+    list_el: null,
+    sidebar_el: null,
+    details_el: null,
+
+    /**
+     * Currently displayed item or view
+     *
+     * Monitor this property to reflect the change of item
+     *
+     * @property {Object}
+     */
+    current: {},
+
+
+    metadata_map: {
+        'object': '@mo:',
+        'command': '@mc:',
+        'param': '@mo-param:'
+    },
+
+    _to_list: function(objects) {
+        var names = [];
+        for (name in objects) {
+            if (objects.hasOwnProperty(name)) {
+                names.push(name);
+            }
+        }
+        names.sort();
+        var new_objects = [];
+        var o;
+        for (var i=0,l=names.length; i<l; i++) {
+            o = objects[names[i]];
+            if (!o.name) o.name = names[i];
+            if (o.only_webui) continue;
+            new_objects.push(o);
+        }
+        return new_objects;
+    },
+
+    _get_commands: function() {
+        var commands = metadata.get('@m:commands');
+        commands = this._to_list(commands);
+        return [{
+            name: "commands",
+            label: "Commands",
+            items: commands
+        }];
+    },
+
+    _get_objects: function() {
+        var objects = metadata.get('@m:objects');
+        objects = this._to_list(objects);
+        return [{
+            name: "commands",
+            label: "Objects",
+            items: objects
+        }];
+    },
+
+    _get_params: function(name) {
+        var object = metadata.get('@mo:'+name);
+        var params = object.takes_params;
+        return [{
+            name: object.name,
+            label:object.label_singular + ' params',
+            items: params
+        }];
+    },
+
+    _get_list: function(type, name, filter) {
+
+        var groups = null;
+        if (type === 'object') {
+            groups = this._get_objects();
+        } else if (type === 'command') {
+            groups = this._get_commands();
+        } else if (type === 'param') {
+            var parts = name.split(':');
+            groups = this._get_params(parts[0]);
+        }
+
+        if (filter) {
+            filter = filter.toLowerCase();
+            var new_groups = [];
+            for (var i=0,l=groups.length; i<l; i++) {
+                var filtered_list = [];
+                var items = groups[i].items;
+                for (var j=0,m=items.length; j<m; j++) {
+                    var item = items[j];
+                    if (item.name.match(filter) ||
+                        (item.label && item.label.toLowerCase().match(filter))) {
+                        filtered_list.push(item);
+                    }
+                }
+                groups[i].items = filtered_list;
+                if (filtered_list.length > 0) {
+                    new_groups.push(groups[i]);
+                }
+            }
+            return new_groups;
+        } else {
+            return groups;
+        }
+    },
+
+    /**
+     * Search metadata for object of given type and name. Display it if found.
+     * Display default view otherwise.
+     *
+     * Supported types and values:
+     * - 'object', value is object name, e.g., 'user'
+     * - 'command', value is command name, e.g., 'user_show'
+     * - 'param', value is tuple 'object_name:param_name', e.g., 'user:cn'
+     *
+     * @param  {string} type Type of the object
+     * @param  {string} name Object identifier
+     */
+    show_item: function (type, name) {
+        var item;
+        if (!this.metadata_map[type]) {
+            IPA.notify("Invalid object type requested: "+type, 'error');
+            this.show_default();
+        } else {
+            item = metadata.get(this.metadata_map[type] + name);
+            if (!item) {
+                IPA.notify("Requested "+ type +" does not exist: " + name, 'error');
+                this.show_default();
+                return;
+            }
+        }
+        this._set_item(type, item, name);
+    },
+
+    /**
+     * Show default view.
+     *
+     * For now a fallback if item is not found. Later could be extended to
+     * contain help info how to use the API.
+     */
+    show_default: function() {
+        // switch view
+        if (this.current_view !== this.default_view_el) {
+            this.el.empty();
+            this.el.append(this.default_view_el);
+            this.current_view = this.default_view_el;
+        }
+        this.set('current', {
+            view: 'default'
+        });
+    },
+
+    /**
+     * Shows default command
+     */
+    show_default_command: function() {
+        this.show_item('command', 'user_show'); // TODO: change
+    },
+
+    /**
+     * Shows default object
+     */
+    show_default_object: function() {
+        this.show_item('object', 'user'); // TODO: change
+    },
+
+    /**
+     * Show item
+     *
+     * @param {string} type Type of item
+     * @param {Object} item The item
+     * @param {string} name Name of the item
+     */
+    _set_item: function(type, item, name) {
+
+        // get widget
+        var widget = null;
+        if (type === 'object') {
+            widget = this.object_detail_w;
+        } else if (type === 'command') {
+            widget = this.command_detail_w;
+        } else if (type === 'param') {
+            widget = this.param_detail_w;
+        } else {
+            IPA.notify("Invalid type", 'error');
+            this.show_default();
+        }
+
+        // switch view
+        if (!this.details_view_el) {
+            this._render_details_view();
+        }
+        if (this.current_view !== this.details_view_el) {
+            this.el.empty();
+            this.el.append(this.details_view_el);
+            this.current_view = this.details_view_el;
+        }
+
+        // switch widget
+        if (!widget.el) widget.render();
+        if (this.current_details_w !== widget) {
+            this.details_el.empty();
+            this.details_el.append(widget.el);
+        }
+
+        // set list
+        var list = this._get_list(type, name, this.current.filter);
+        this.list_w.set('groups', list);
+        this.list_w.select(item);
+
+        // set item
+        widget.set('item', item);
+        this.set('current', {
+            item: item,
+            type: type,
+            name: name,
+            filter: this.current.filter,
+            view: 'details'
+        });
+
+        // update sidebar
+        $(window).trigger('resize');
+
+        $('html, body').animate({
+            scrollTop: 0
+        }, 500);
+    },
+
+    render: function() {
+        this.el = $('<div/>', { 'class': this.css_class });
+        this._render_default_view().appendTo(this.el);
+        if (this.container_node) {
+            this.el.appendTo(this.container_node);
+        }
+        return this.el;
+    },
+
+    _render_details_view: function() {
+        this.details_view_el = $('<div/>', { 'class': 'details-view' });
+        var row = $('<div/>', { 'class': 'row' });
+        this.sidebar_el = $('<div/>', { 'class': 'sidebar-pf sidebar-pf-left col-sm-4 col-md-3 col-sm-pull-8 col-md-pull-9' });
+        this.details_el = $('<div/>', { 'class': 'col-sm-8 col-md-9 col-sm-push-4 col-md-push-3' });
+        this.details_el.appendTo(row);
+        this.sidebar_el.appendTo(row);
+        row.appendTo(this.details_view_el);
+        this._render_select().appendTo(this.sidebar_el);
+        return this.details_view_el;
+    },
+
+    _render_select: function() {
+        var el  = $('<div/>', { 'class': 'item-select' });
+
+        $('<div/>', { 'class': 'nav-category' }).
+        append($('<h2/>', {
+            'class': 'item-select',
+            text: 'Browse'
+        })).
+        appendTo(el);
+
+        this.filter_el = this.filter_w.render();
+        this.list_el = this.list_w.render();
+        this.filter_el.appendTo(el);
+        this.list_el.appendTo(el);
+        return el;
+    },
+
+    _render_default_view: function() {
+        this.default_view_el = $('<div/>', { 'class': 'default-view' });
+        $('<h1/>', { text: "API Browser" }).appendTo(this.default_view_el);
+        var commands = $('<div/>').appendTo(this.default_view_el);
+        $('<p/>').append($('<a/>', {
+            href: "#/p/apibrowser/type=command",
+            text: "Browse Commands"
+        })).appendTo(commands);
+        var objects = $('<div/>').appendTo(this.default_view_el);
+        $('<p/>').append($('<a/>', {
+            href: "#/p/apibrowser/type=object",
+            text: "Browse Objects"
+        })).appendTo(commands);
+        return this.default_view_el;
+    },
+
+    _apply_filter: function(filter) {
+        var current = this.current;
+        current.filter = filter;
+        var list = this._get_list(current.type, current.name, current.filter);
+        this.list_w.set('groups', list);
+        this.list_w.select(current.item);
+        // reset min height so that PatternFly can set proper min height
+        this.sidebar_el.css({'min-height': 0});
+        this.details_el.css({'min-height': 0});
+        $(window).trigger('resize');
+    },
+
+    _item_selected: function(item) {
+        var t = this.current.type;
+        var n = item.name;
+        if (t == 'param') {
+            var obj = this.current.name.split(':')[0];
+            n = [obj, n].join(':');
+        }
+        this.show_item(t, n);
+    },
+
+    _init_widgets: function() {
+        this.filter_w = new browser_widgets.FilterWidget();
+        this.filter_w.watch('filter', lang.hitch(this, function(name, old, value) {
+            this._apply_filter(value);
+        }));
+
+        this.list_w = new ListViewWidget();
+        this.object_detail_w = new browser_widgets.ObjectDetailWidget();
+        this.command_detail_w = new browser_widgets.CommandDetailWidget();
+        this.param_detail_w = new browser_widgets.ParamDetailWidget();
+
+        on(this.list_w, 'item-click', lang.hitch(this, function(args) {
+            this._item_selected(args.context);
+        }));
+    },
+
+    constructor: function(spec) {
+        lang.mixin(this, spec);
+        this._init_widgets();
+    }
+});
+
+    return widgets.APIBrowserWidget;
+});
\ No newline at end of file
diff --git a/install/ui/src/freeipa/widgets/browser_widgets.js b/install/ui/src/freeipa/widgets/browser_widgets.js
new file mode 100644
index 0000000000000000000000000000000000000000..b40a183a3896568b1a7e0eb087cbe3c2d1316fea
--- /dev/null
+++ b/install/ui/src/freeipa/widgets/browser_widgets.js
@@ -0,0 +1,503 @@
+//
+// Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+//
+
+//
+// Contains API browser widgets
+//
+
+define([
+    'dojo/_base/declare',
+    'dojo/_base/lang',
+    'dojo/on',
+    'dojo/Evented',
+    'dojo/Stateful',
+    '../jquery',
+    '../ipa',
+    '../metadata',
+    '../navigation',
+    '../reg',
+    '../text',
+    '../util'
+], function(declare, lang, on, Evented, Stateful, $, IPA, metadata, navigation,
+     reg, text, util) {
+
+var widgets = { browser_widgets: {} }; //namespace
+
+var apibrowser_facet = 'apibrowser';
+
+/**
+ * Browser Widget Base
+ *
+ * Candidate for a base class for all widgets
+ *
+ * @class
+ */
+widgets.browser_widgets.Base = declare([Stateful, Evented], {
+
+    // nodes
+    el: null,
+
+    /**
+     * Render widget's HTML
+     * @return {jQuery} base node
+     */
+    render: function() {
+        this.el = $('<div/>', { 'class': this.css_class });
+        this.render_content();
+        return this.el;
+    },
+
+    /**
+     * Should be overridden
+     */
+    render_content: function() {
+    },
+
+    constructor: function(spec) {
+        lang.mixin(this, spec);
+    }
+});
+
+/**
+ * Detail Base
+ *
+ * A base class for showing details of various API objects
+ *
+ * @class
+ * @extends {widgets.browser_widgets.Base}
+ */
+widgets.browser_widgets.DetailBase = declare([widgets.browser_widgets.Base], {
+
+    /**
+     * Item to be displayed
+     * @property {Object}
+     */
+    item: null,
+
+    common_options: [
+        'all', 'rights', 'raw', 'version', 'addattr', 'setattr', 'delattr',
+        'getattr', 'timelimit', 'sizelimit', 'pkey_only'
+    ],
+
+    _itemSetter: function(value) {
+        this.item = value;
+        if (this.el) {
+            this.render_content();
+        }
+    },
+
+    _get_object: function(obj_name) {
+        var obj = metadata.get('@mo:' + obj_name);
+        if (!obj || obj.only_webui) return null;
+        return obj;
+    },
+
+    _get_command_object: function(command_name) {
+        var obj_name = command_name.split('_')[0];
+        var obj = this._get_object(obj_name);
+        return obj;
+    },
+
+    _get_objectparam: function(command_name, param_name) {
+        var obj = this._get_command_object(command_name);
+        if (!obj) return null;
+        var param = metadata.get('@mo-param:' + obj.name + ':' + param_name);
+        return param;
+    },
+
+    _get_cli_option: function(name) {
+        if (!name) return name;
+        return '--' + name.replace('_', '-');
+    },
+
+    render_object_link: function(obj_name, text) {
+        var facet = reg.facet.get(apibrowser_facet);
+        var link = $('<a/>', {
+            href: "#" + navigation.create_hash(facet, {
+                type: 'object',
+                name: obj_name
+            }),
+            text: text || obj_name
+        });
+        return link;
+    },
+
+    render_command_link: function(command_name, text) {
+        var facet = reg.facet.get(apibrowser_facet);
+        var link = $('<a/>', {
+            href: "#" + navigation.create_hash(facet, {
+                type: 'command',
+                name: command_name
+            }),
+            text: text || command_name
+        });
+        return link;
+    },
+
+    render_param_link: function(obj_name, param_name, text) {
+        var name = obj_name + ':' + param_name;
+        var facet = reg.facet.get(apibrowser_facet);
+        var link = $('<a/>', {
+            href: "#" + navigation.create_hash(facet, {
+                type: 'param',
+                name: name
+            }),
+            text: text || param_name
+        });
+        return link;
+    },
+
+
+    render_title: function(type, text) {
+        var title = $('<h1/>', { 'class': 'api-title' });
+        $('<span/>', {
+            'class': 'api-title-type',
+            text: type
+        }).appendTo(title);
+        $('<span/>', {
+            'class': 'api-title-text',
+            text: text
+        }).appendTo(title);
+        return title;
+    },
+
+    render_doc: function(text) {
+        return $('<p/>', { text: text });
+    },
+
+    render_section_header: function(text, link) {
+        return $('<h2/>', {
+            text: text,
+            id: link
+        });
+    },
+
+    render_value_container: function() {
+        return $('<div/>', {
+            'class': 'properties'
+        });
+    },
+
+    render_value: function(label, value_node, container) {
+        if (!text) return $('');
+
+        var row = $('<div/>', {
+            'class': 'row'
+        });
+        $('<div/>', {
+            'class': 'col-sm-4 prop-label',
+            text: label
+        }).appendTo(row);
+        $('<div/>', {
+            'class': 'col-sm-8 prop-value'
+        }).append(value_node).appendTo(row);
+
+        if (container) {
+            container.append(row);
+        }
+        return row;
+    },
+
+    render_text_all: function(label, text, container) {
+        if (text === null || text === undefined) return $('');
+        var node = document.createTextNode(text);
+        return this.render_value(label, node, container);
+    },
+
+    render_text: function(label, text, container) {
+        if (!text) return $('');
+        var node = document.createTextNode(text);
+        return this.render_value(label, node, container);
+    },
+
+    render_array: function(label, value, container) {
+        if (!value || value.length === 0) return $('');
+        var text = value.join(', ');
+        return this.render_text(label, text, container);
+    },
+
+    render_object: function(label, obj, container) {
+        if (obj === undefined || obj === null) return $('');
+        var text = JSON.stringify(obj);
+        return this.render_text(label, text, container);
+    },
+
+    render_command_object_link: function(label, command_name, container) {
+        var obj = this._get_command_object(command_name);
+        if (!obj) return $('');
+        var link = this.render_object_link(obj.name, obj.label_singular);
+        return this.render_value(label, link, container);
+    },
+
+
+    render_flags: function(flags, cnt) {
+        if (!flags) return null;
+        if (!cnt) cnt = $('<div/>');
+        for (var i=0,l=flags.length; i<l; i++) {
+            $('<span/>', {
+                'class': 'label label-default',
+                text: flags[i]
+            }).appendTo(cnt);
+        }
+        return cnt;
+    },
+
+    render_param: function(param, is_arg, container) {
+        var prop_cnt = this.render_value_container();
+        var header = $('<h3/>', {
+            text: param.name
+        });
+        header.appendTo(prop_cnt);
+        this.render_param_properties(param, is_arg, prop_cnt, header);
+        if (container) {
+            container.append(prop_cnt);
+        }
+        return prop_cnt;
+    },
+
+    render_param_properties: function(param, is_arg, container, flags_container) {
+
+        var flags = [];
+        if (param.required) flags.push('required');
+        if (param.multivalue) flags.push('multivalued');
+        //if (param.primary_key) flags.push('primary key');
+
+        this.render_doc(param.doc).appendTo(container);
+        this.render_flags(flags, flags_container);
+        if (param.label && param.label[0] !== '<') {
+            this.render_text("label", param.label, container);
+        }
+        this.render_text("type", param.type, container);
+        this.render_text_all("default value", param['default'], container);
+        this.render_array("default value created from", param['default_from'], container);
+        if (param.values) {
+            this.render_array("possible values", param.values, container);
+        }
+
+        // Str values
+        this.render_text("minimum length", param.minlength, container);
+        this.render_text("maximum length", param.maxlength, container);
+        this.render_text("pattern", param.pattern, container);
+
+        // Int, Decimal
+        this.render_text("minimum value", param.minvalue, container);
+        this.render_text("maximum value", param.maxvalue, container);
+        this.render_text("precision", param.precision, container);
+
+        // CLI
+        if (!is_arg) {
+            this.render_text("CLI option name", this._get_cli_option(param.cli_name), container);
+        }
+
+        this.render_text("option_group", param.option_group, container);
+    }
+});
+
+var base = widgets.browser_widgets.DetailBase;
+
+/**
+ * Object detail
+ * @class
+ * @extends {widgets.browser_widgets.DetailBase
+ */
+widgets.browser_widgets.ObjectDetailWidget = declare([base], {
+
+    render_content: function() {
+        var link, obj;
+        this.el.empty();
+        if (!this.item) {
+            this.el.append('No object selected');
+            return;
+        }
+        var item = this.item;
+        this.render_title('Object: ', item.name).appendTo(this.el);
+        if (item.doc) this.render_doc(item.doc).appendTo(this.el);
+        if (item.parent_object) {
+            obj = this._get_object(item.parent_object);
+            if (obj) {
+                link = this.render_object_link(item.parent_object, obj.label_singular);
+                this.render_value('parent_object', link, this.el);
+            }
+        }
+        //this.render_text("parent_object", item.parent_object, this.el);
+        this.render_text("label", item.label, this.el);
+        this.render_text("label_singular", item.label_singular, this.el);
+        this.render_text("container_dn", item.container_dn, this.el);
+        this.render_text("object_class", item.object_class, this.el);
+        this.render_text("object_class_config", item.object_class_config, this.el);
+        this.render_text("object_name", item.object_name, this.el);
+        this.render_text("object_name_plural", item.object_name_plural, this.el);
+        this.render_text("uuid_attribute", item.uuid_attribute, this.el);
+        this.render_text("rdn_attribute", item.rdn_attribute, this.el);
+        this.render_text("bindable", item.bindable, this.el);
+        this.render_array("aciattrs", item.aciattrs, this.el);
+        this.render_text("can_have_permissions", item.can_have_permissions, this.el);
+        this.render_array("default_attributes", item.default_attributes, this.el);
+        this.render_array("hidden_attributes", item.hidden_attributes, this.el);
+        this.render_object("attribute_members", item.attribute_members, this.el);
+        this.render_object("relationships", item.relationships, this.el);
+
+        if (item.methods) {
+            this.render_section_header('Methods').appendTo(this.el);
+            var cnt = $('<div/>');
+            for (i=0, l=item.methods.length; i<l; i++) {
+                var method_name = item.methods[i];
+                if (i>0) {
+                    cnt.append(', ');
+                }
+                var command_name = item.name + '_' + method_name;
+                link = this.render_command_link(command_name, method_name);
+                cnt.append(link);
+            }
+            this.render_value('', cnt, this.el);
+        }
+
+        if (item.takes_params) {
+            this.render_section_header('Params').appendTo(this.el);
+            for (var i=0,l=item.takes_params.length; i<l; i++) {
+                var opt = item.takes_params[i];
+                this.render_param(opt, true).appendTo(this.el);
+            }
+        }
+    }
+});
+
+/**
+ * Command Detail
+ * @class
+ * @extends {widgets.browser_widgets.DetailBase
+ */
+widgets.browser_widgets.CommandDetailWidget = declare([base], {
+
+    render_content: function() {
+        var i = 0, l = 0;
+        this.el.empty();
+        if (!this.item) {
+            this.el.append('No command selected');
+            return;
+        }
+        var item = this.item;
+        var obj = this._get_command_object(item.name);
+        this.render_title('Command: ', item.name).appendTo(this.el);
+        if (item.doc) this.render_doc(item.doc).appendTo(this.el);
+        this.render_command_object_link('object', item.name, this.el);
+        if (item.takes_args && item.takes_args.length > 0) {
+            this.render_section_header('Arguments').appendTo(this.el);
+            for (i=0, l=item.takes_args.length; i<l; i++) {
+                var arg = item.takes_args[i];
+                this.render_param(arg, true).appendTo(this.el);
+            }
+        }
+        if (item.takes_options && item.takes_options.length > 0) {
+            var options = [];
+            var common_options = [];
+
+            for (i=0, l=item.takes_options.length; i<l; i++) {
+                var opt = item.takes_options[i];
+                if (opt.include && opt.include.indexOf('server') === -1) continue;
+                if (opt.exclude && opt.exclude.indexOf('server') > -1) continue;
+                if (this.common_options.indexOf(opt.name) > -1) {
+                    common_options.push(opt);
+                } else {
+                    options.push(opt);
+                }
+            }
+
+            if (options.length) {
+                this.render_section_header('Options').appendTo(this.el);
+            }
+            for (i=0, l=options.length; i<l; i++) {
+                this.render_param(options[i], false).appendTo(this.el);
+            }
+
+            if (common_options.length) {
+                this.render_section_header('Common Options').appendTo(this.el);
+            }
+            for (i=0, l=common_options.length; i<l; i++) {
+                this.render_param(common_options[i], false).appendTo(this.el);
+            }
+        }
+        if (item.output_params && item.output_params.length > 0) {
+            this.render_section_header('Output Params').appendTo(this.el);
+            var out_params_cnt = $('<div/>');
+            for (i=0, l=item.output_params.length; i<l; i++) {
+                var param_name = item.output_params[i];
+                var param = this._get_objectparam(item.name, param_name);
+                if (i>0) {
+                    out_params_cnt.append(', ');
+                }
+                if (!param) {
+                    out_params_cnt.append(param_name);
+                } else {
+                    var link = this.render_param_link(obj.name, param_name);
+                    out_params_cnt.append(link);
+                }
+            }
+            out_params_cnt.appendTo(this.el);
+        }
+    }
+});
+
+/**
+ * Param Detail
+ * @class
+ * @extends {widgets.browser_widgets.DetailBase
+ */
+widgets.browser_widgets.ParamDetailWidget = declare([base], {
+
+    render_content: function() {
+        this.el.empty();
+        if (!this.item) {
+            this.el.append('No param selected');
+            return;
+        }
+        var item = this.item;
+        this.render_title('Param: ', item.name).appendTo(this.el);
+        var flags = $('<div/>').appendTo(this.el);
+        this.render_param_properties(item, this.el, flags);
+    }
+});
+
+/**
+ * Filter input
+ *
+ * @class
+ * @extends {widgets.browser_widgets.DetailBase
+ */
+widgets.browser_widgets.FilterWidget = declare([widgets.browser_widgets.Base], {
+
+    /**
+     * Filter text
+     * @property {String}
+     */
+    filter: '',
+
+    _filter_el: null,
+
+    _filterSetter: function(value) {
+        this.filter = value;
+        if (this.el) {
+            this._filter_el.val(value);
+        }
+    },
+
+    render_content: function() {
+        this.el.empty();
+        this._filter_el = $('<input/>', {
+            type: 'text',
+            name: 'filter',
+            placeholder: 'type to filter...',
+            title: 'accepts case insensitive regular expression'
+        });
+        this._filter_el.bind('input', lang.hitch(this, function() {
+            var filter = this._filter_el.val();
+            this.set('filter', filter);
+        }));
+        this._filter_el.appendTo(this.el);
+    }
+});
+
+
+    return widgets.browser_widgets;
+});
\ No newline at end of file
-- 
2.4.3

From 0693d06557a75ce08cf05d8435c2c73c54901822 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 26 Jun 2015 10:34:37 +0200
Subject: [PATCH] webui: menu and navigation fixes

fixes:

1. When navigation is initiated from clicking and a link with hash, update
of facet state causes that subsequent click on a link with hash will be
ignored. Caused by a code which prevents infinite loop because of facet
state update. Now hash update is done only if it was really changed.

2. registered correct handler for standalone pages

3. fix selection of menu item where the items differ only in args. Chooses
the item with the most similar state to current facet.

https://fedorahosted.org/freeipa/ticket/3129
---
 install/ui/src/freeipa/Application_controller.js | 31 ++++++++++++++++++++++--
 install/ui/src/freeipa/navigation/Router.js      |  2 +-
 install/ui/src/freeipa/navigation/routing.js     |  4 +--
 3 files changed, 32 insertions(+), 5 deletions(-)

diff --git a/install/ui/src/freeipa/Application_controller.js b/install/ui/src/freeipa/Application_controller.js
index ca08770fe347213e6525c74b80da1eeca67fe009..69b9cd1c3c5873e10729f808ca3ec27688a1ecf2 100644
--- a/install/ui/src/freeipa/Application_controller.js
+++ b/install/ui/src/freeipa/Application_controller.js
@@ -379,9 +379,36 @@ define([
                 items = this.menu.query({ parent: null });
             }
 
-            // select first
             if (items.total) {
-                return items[0];
+                if (items.total === 1) return items[0];
+
+                // select the menu item with the most similar state as the facet
+                var best = items[0];
+                var best_score = 0;
+                var item, i, j, l, score;
+                var state = facet.state;
+                for (i=0, l=items.total; i<l; i++) {
+                    item = items[i];
+                    score = 0;
+                    if (item.pkeys && facet.get_pkeys) {
+                        var pkeys = facet.get_pkeys();
+                        for (j=0, j=item.pkeys.length; j<l; j++) {
+                            if (pkeys.indexOf(item.pkeys[j]) > -1) score++;
+                        }
+                    }
+                    if (item.args) {
+                        for (var name in item.args) {
+                            if (!item.args.hasOwnProperty(name)) continue;
+                            if (state[name] == item.args[name]) score++;
+                        }
+                    }
+                    if (score > best_score) {
+                        best_score = score;
+                        best = item;
+                    }
+                }
+
+                return best;
             }
         },
 
diff --git a/install/ui/src/freeipa/navigation/Router.js b/install/ui/src/freeipa/navigation/Router.js
index a65c60fd3644c8e89bfebd7174fd8724d42d1ab3..5523993f4ef13ac02f3ccb1f801a3200412711f0 100644
--- a/install/ui/src/freeipa/navigation/Router.js
+++ b/install/ui/src/freeipa/navigation/Router.js
@@ -66,7 +66,6 @@ define(['dojo/_base/declare',
          */
         ignore_next: false,
 
-
         /**
          * Register a route-handler pair to a dojo.router
          * Handler will be run in context of this object
@@ -111,6 +110,7 @@ define(['dojo/_base/declare',
          * @param {boolean} Whether to suppress following hash change handler
          */
         update_hash: function(hash, ignore_change) {
+            if (window.location.hash === "#" + hash) return;
             this.ignore_next = !!ignore_change;
             router.go(hash);
         },
diff --git a/install/ui/src/freeipa/navigation/routing.js b/install/ui/src/freeipa/navigation/routing.js
index 6e18b0228fc8bf2459499ef21dbdcaa0f0a22bd8..89a323dd5fd4f08ed692093f2b84c152c2a55a45 100644
--- a/install/ui/src/freeipa/navigation/routing.js
+++ b/install/ui/src/freeipa/navigation/routing.js
@@ -166,7 +166,7 @@ var routing = {
      */
     navigate_to_facet: function(facet, options) {
         var hash = this.create_hash(facet, options);
-        return this.router.navigate_to_hash(hash);
+        return this.router.navigate_to_hash(hash, facet);
     },
 
     update_hash: function(facet, options) {
@@ -494,7 +494,7 @@ routing.init = function(router) {
     var entity_n = new routing.EntityNavigator();
     this.add_hash_creator(generic_hc);
     this.add_hash_creator(entity_hc);
-    this.add_route(this.routes, generic_rh);
+    this.add_route(this.page_routes, generic_rh);
     this.add_route(this.entity_routes, entity_rh);
     this.add_navigator(generic_n);
     this.add_navigator(entity_n);
-- 
2.4.3

From cebd400a81f6ebf3b6bc996fa50fc60e83e6fea5 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 26 Jun 2015 10:33:58 +0200
Subject: [PATCH] webui: fix webui specific metadata

Mark all Web UI specific metadata so they could be filtered out
in the API Browser.

Fix cert name.

https://fedorahosted.org/freeipa/ticket/3129
---
 install/ui/src/freeipa/certificate.js | 3 ++-
 install/ui/src/freeipa/topology.js    | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index ccaa25807175be9cb917928d5795d76a456b6fc5..182ec7e66238375eb3140a449bd79c46d7937841 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -970,7 +970,7 @@ exp.create_cert_metadata = function() {
         'show',
         'status'
     ];
-    entity.name = "certificate";
+    entity.name = "cert";
     entity.object_name = "certificate";
     entity.object_name_plural = "certificates";
     entity.parent_object = "";
@@ -978,6 +978,7 @@ exp.create_cert_metadata = function() {
     entity.rdn_attribute = "";
     entity.relationships = {};
     entity.takes_params = lang.clone(entity.takes_options);
+    entity.only_webui = true;
 
     get_param(entity.takes_params, 'subject').flags = ['no_update'];
     var reason = get_param(entity.takes_params, 'revocation_reason');
diff --git a/install/ui/src/freeipa/topology.js b/install/ui/src/freeipa/topology.js
index 9ca31741f18e47e567c3e0a4248f5beaad77ccd1..4d585947bf0574ac74bbd94e0d3069701cba85d0 100644
--- a/install/ui/src/freeipa/topology.js
+++ b/install/ui/src/freeipa/topology.js
@@ -261,6 +261,7 @@ topology.domainlevel_metadata = function(spec, context) {
         name: 'domainlevel',
         label: text.get('@i18n:objects.domainlevel.label'),
         label_singular: text.get('@i18n:objects.domainlevel.label_singular'),
+        only_webui: true,
         takes_params: [
             {
                 'class': "Int",
-- 
2.4.3

From af88dea9e15212275c34601e13c69c04e0b6740d Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 5 Jun 2015 15:55:11 +0200
Subject: [PATCH] webui: ListViewWidget

A widget for rendering a list of groups of items. Intended to be
used in sidebar. Plan is to serve also as a base for FacetGroupsWidget.

https://fedorahosted.org/freeipa/ticket/3129
---
 install/ui/src/freeipa/widgets/ListViewWidget.js | 233 +++++++++++++++++++++++
 1 file changed, 233 insertions(+)
 create mode 100644 install/ui/src/freeipa/widgets/ListViewWidget.js

diff --git a/install/ui/src/freeipa/widgets/ListViewWidget.js b/install/ui/src/freeipa/widgets/ListViewWidget.js
new file mode 100644
index 0000000000000000000000000000000000000000..d378e2b1ef293882bfb10d90688d73a8a7a7bffa
--- /dev/null
+++ b/install/ui/src/freeipa/widgets/ListViewWidget.js
@@ -0,0 +1,233 @@
+//
+// Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+//
+
+define([
+    'dojo/_base/declare',
+    'dojo/_base/lang',
+    'dojo/on',
+    'dojo/Evented',
+    'dojo/Stateful',
+    '../jquery',
+    '../ipa',
+    '../reg',
+    '../text',
+    '../util'
+], function(declare, lang, on, Evented, Stateful, $, IPA, reg, text, util) {
+
+var widgets = {};
+
+/**
+ * Widget for rendering a list of groups of items
+ * @class
+ */
+widgets.ListViewWidget = declare([Stateful, Evented], {
+
+    /**
+     * List of groups
+     * @type {Object[]}
+     */
+    groups: null,
+
+    /**
+     * Should be visible
+     * @type {Boolean}
+     */
+    visible: true,
+
+    /**
+     * Selected item
+     * @property {Object}
+     */
+    selected_item: null,
+
+    // behavior
+    select_on_click: true,
+
+    /**
+     * Raised when menu item is clicked
+     * @event item-click
+     */
+
+    // nodes
+    el: null,
+    group_els: null,
+    item_els: null,
+
+    // html and styling
+    css_class: '',
+    group_el_type: '<div/>',
+    group_class: '',
+    group_label_el_type: '<div/>',
+    group_label_class: 'nav-category',
+    group_label_title_el_type: '<h2/>',
+    group_label_title_class: '',
+    item_cont_el_type: '<div/>',
+    item_cont_class: '',
+    item_list_el_type: '<ul/>',
+    item_list_class: 'nav nav-pills nav-stacked',
+    item_el_type: '<li/>',
+    item_class: 't',
+    item_link_class: 'item-link',
+    selected_class: 'active',
+
+    _groupsSetter: function(value) {
+        this.groups = value;
+        this.render_groups();
+    },
+
+    _get_group_items: function(group) {
+        return group.items;
+    },
+
+    render: function() {
+
+        this.el = $('<div/>', { 'class': this.css_class });
+        this.render_groups();
+        return this.el;
+    },
+
+    render_groups: function() {
+        if (!this.el) return;
+
+        this.group_els = {};
+        this.item_els = {};
+
+        this.el.empty();
+        if (!this.groups) return;
+
+        for (var i=0; i<this.groups.length; i++) {
+            var group = this.groups[i];
+            var items = this._get_group_items(group);
+            if (items.length) {
+                var group_el = this.render_group(group);
+                this.el.append(group_el);
+            }
+        }
+    },
+
+    render_group: function(group) {
+
+        var gr = this.group_els[group.name] = { item_els: {}};
+
+        gr.group_el = $(this.group_el_type, {
+            'class': this.group_class,
+            name: group.name
+        });
+
+        gr.label_el = $(this.group_label_el_type, {
+            'class': this.group_label_class
+        }).appendTo(gr.group_el);
+
+        gr.label_title_el = $(this.group_label_title_el_type, {
+            'class': this.group_label_title_class,
+            text: ' '
+        }).appendTo(gr.label_el);
+
+
+        var item_cont = $(this.item_cont_el_type, { 'class': this.item_cont_class });
+        var item_list = $(this.item_list_el_type, { 'class': this.item_list_class });
+        item_list.appendTo(item_cont);
+        var items = this._get_group_items(group);
+        for (var i=0,l=items.length; i<l; i++) {
+            var item = items[i];
+            var item_el = this.item_els[item.name] = this.render_item(item);
+            item_list.append(item_el);
+        }
+        gr.group_el.append(item_cont);
+
+        return gr.group_el;
+    },
+
+    render_item: function(item) {
+        var self = this;
+        var el = $(this.item_el_type, {
+            name: item.name,
+            'class': this.item_class,
+            click: function() {
+                if (item.disabled || el.hasClass('entity-facet-disabled')) {
+                    return false;
+                }
+                self.on_click(item);
+                return false;
+            }
+        });
+
+        $('<a/>', {
+            text: item.label || item.name || '',
+            'class': 'item-link',
+            href: this.create_item_link(item),
+            name: item.name,
+            title: item.title
+        }).appendTo(el);
+
+        return el;
+    },
+
+    create_item_link: function(item) {
+        return '#';
+    },
+
+    on_click: function(item) {
+        this.emit('item-click', { source: this, context: item });
+        if (this.select_on_click) {
+            this.select(item);
+        }
+    },
+
+    update_group: function(group) {
+        if (!this.group_els[group.name]) return;
+        var label_el = this.group_els[group.name].label_title_el;
+        label_el.text(group.label);
+        if (group.title) label_el.attr('title', group.title);
+    },
+
+    update_item: function(item) {
+        var item_el = this.item_els[item.name];
+        var label_el = $('a', item_el);
+        label_el.text(item.label);
+        if (item.title) label_el.attr('title', item.title);
+    },
+
+    select: function(item) {
+        if (!this.el) return;
+        var cls = this.selected_class;
+        var item_el = this.item_els[item.name];
+
+        this.el.find(this.item_class).removeClass(cls);
+        this.el.find(this.item_link_class).removeClass(cls);
+
+        if (!item_el) return; // return if can't select
+
+        item_el.addClass(cls);
+        item_el.find(this.item_link_class).addClass(cls);
+        this.set('selected_item', item);
+        this.emit('select', { source: this, context: item });
+    },
+
+    select_first: function() {
+        if (!this.el) return;
+        this.el.find(this.item_link_class).removeClass(this.selected_class);
+        this.el.find(this.item_class).removeClass(this.selected_class);
+        var first = this.el.find(this.item_link_class + ':first');
+        first.addClass(this.selected_class);
+        first.parent().addClass(this.selected_class);
+    },
+
+    set_visible: function(visible) {
+        this.set('visible', visible);
+        this._apply_visible();
+    },
+
+    _apply_visible: function() {
+        if (!this.el) return;
+        this.el.css('display', this.visible ? '' : 'none');
+    },
+
+    constructor: function(spec) {
+        lang.mixin(this, spec);
+    }
+});
+
+    return widgets.ListViewWidget;
+});
\ No newline at end of file
-- 
2.4.3

From 476ff61a6349a681bc389286298847845e6648b0 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 5 Jun 2015 15:11:54 +0200
Subject: [PATCH] include more information in metadata

added to commands: doc, proper args, NO_CLI

added to options: default_from, cli_name, cli_short_name and others

https://fedorahosted.org/freeipa/ticket/3129
---
 ipalib/frontend.py         | 36 ++++++++++++++++++------------------
 ipalib/parameters.py       | 25 ++++++-------------------
 ipalib/plugins/baseldap.py | 33 ---------------------------------
 3 files changed, 24 insertions(+), 70 deletions(-)

diff --git a/ipalib/frontend.py b/ipalib/frontend.py
index 19190c37878fc50f70113e10b4af0bdd7183f2a1..c35cc0c85fc6d4ddf459aef2de292f3681ba1475 100644
--- a/ipalib/frontend.py
+++ b/ipalib/frontend.py
@@ -35,6 +35,7 @@ from errors import (ZeroArgumentError, MaxArgumentError, OverlapError,
     VersionError, OptionError, InvocationError,
     ValidationError, ConversionError)
 from ipalib import messages
+from ipalib.util import json_serialize
 from textwrap import wrap
 
 
@@ -1050,31 +1051,16 @@ class Command(HasParam):
 
     # list of attributes we want exported to JSON
     json_friendly_attributes = (
-        'name', 'takes_args',
+        'name', 'doc', 'NO_CLI'
     )
 
-    # list of options we want only to mention their presence and not to write
-    # their attributes
-    json_only_presence_options = (
-        'all', 'raw', 'attrs', 'addattr', 'delattr', 'setattr', 'version',
-    )
-
-    def get_json_options(self):
-        """
-        Get only options we want exported to JSON
-        """
-        for option in self.get_options():
-            if option.name not in self.json_only_presence_options:
-                yield option
-            else:
-                yield { 'name': option.name }
-
     def __json__(self):
         json_dict = dict(
             (a, getattr(self, a)) for a in self.json_friendly_attributes
         )
 
-        json_dict['takes_options'] = list(self.get_json_options())
+        json_dict['takes_args'] = list(self.get_args())
+        json_dict['takes_options'] = list(self.get_options())
 
         return json_dict
 
@@ -1211,6 +1197,20 @@ class Object(HasParam):
                 return 1
             return 2
 
+    json_friendly_attributes = (
+        'name', 'takes_params',
+    )
+
+    def __json__(self):
+        json_dict = dict(
+            (a, json_serialize(getattr(self, a)))
+            for a in self.json_friendly_attributes
+        )
+        if self.primary_key:
+            json_dict['primary_key'] = self.primary_key.name
+        json_dict['methods'] = [m for m in self.methods]
+        return json_dict
+
 
 class Attribute(Plugin):
     """
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index 7fa55fd6a6854ffa97da211ca5ef04b7ad974dc4..6cc6f8c9244abb9e895782f40cbdde63b2144d22 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -240,6 +240,9 @@ class DefaultFrom(ReadOnly):
         except StandardError:
             pass
 
+    def __json__(self):
+        return self.keys
+
 
 def parse_param_spec(spec):
     """
@@ -917,30 +920,14 @@ class Param(ReadOnly):
     def sort_key(self, value):
         return value
 
-    json_exclude_attrs = (
-        'alwaysask', 'autofill', 'cli_name', 'cli_short_name', 'csv',
-        'sortorder', 'falsehoods', 'truths', 'version',
-    )
-
     def __json__(self):
         json_dict = {}
-        for (a, k, d) in self.kwargs:
-            if a in self.json_exclude_attrs:
-                continue
-            if k in (callable, DefaultFrom):
-                continue
-            elif isinstance(getattr(self, a), frozenset):
-                json_dict[a] = [k for k in getattr(self, a, [])]
-            else:
-                val = getattr(self, a, '')
-                if val is None or val is False:
-                    # ignore False and not set because lack of their presence is
-                    # the information itself
-                    continue;
-                json_dict[a] = json_serialize(val)
+        for key in self.__kw:
+            json_dict[key] = json_serialize(self.__kw[key])
         json_dict['class'] = self.__class__.__name__
         json_dict['name'] = self.name
         json_dict['type'] = self.type.__name__
+        json_dict['flags'] = json_serialize([f for f in self.flags])
         return json_dict
 
 
diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py
index 2eab69f3decb3359e82d30a0a3a595e81a6d9bc3..a2dbbbfc28a8f281ae88d4cdc573eccb5d7307c0 100644
--- a/ipalib/plugins/baseldap.py
+++ b/ipalib/plugins/baseldap.py
@@ -1299,17 +1299,6 @@ class LDAPCreate(BaseLDAPCommand, crud.Create):
     def interactive_prompt_callback(self, kw):
         return
 
-    # list of attributes we want exported to JSON
-    json_friendly_attributes = (
-        'takes_args',
-    )
-
-    def __json__(self):
-        json_dict = dict(
-            (a, getattr(self, a)) for a in self.json_friendly_attributes
-        )
-        json_dict['takes_options'] = list(self.get_json_options())
-        return json_dict
 
 class LDAPQuery(BaseLDAPCommand, crud.PKQuery):
     """
@@ -1321,17 +1310,6 @@ class LDAPQuery(BaseLDAPCommand, crud.PKQuery):
         for arg in super(LDAPQuery, self).get_args():
             yield arg
 
-    # list of attributes we want exported to JSON
-    json_friendly_attributes = (
-        'takes_args',
-    )
-
-    def __json__(self):
-        json_dict = dict(
-            (a, getattr(self, a)) for a in self.json_friendly_attributes
-        )
-        json_dict['takes_options'] = list(self.get_json_options())
-        return json_dict
 
 class LDAPMultiQuery(LDAPQuery):
     """
@@ -2131,17 +2109,6 @@ class LDAPSearch(BaseLDAPCommand, crud.Search):
     def interactive_prompt_callback(self, kw):
         return
 
-    # list of attributes we want exported to JSON
-    json_friendly_attributes = (
-        'takes_args',
-    )
-
-    def __json__(self):
-        json_dict = dict(
-            (a, getattr(self, a)) for a in self.json_friendly_attributes
-        )
-        json_dict['takes_options'] = list(self.get_json_options())
-        return json_dict
 
 class LDAPModReverseMember(LDAPQuery):
     """
-- 
2.4.3

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to