From 2a976334c2160c91a61fb0c477777e7adbbd3150 Mon Sep 17 00:00:00 2001 From: Petr Vobornik Date: Fri, 5 Jun 2015 19:03:46 +0200 Subject: webui: API browser First part of API browser - displaying metadata in more consumable way. https://fedorahosted.org/freeipa/ticket/3129 Reviewed-By: Martin Kosek Reviewed-By: Tomas Babej --- 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 83c24c5ef..3a7c2ebc2 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 066e074a0..7778f6bf4 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 9b290ab0e..f05e8213c 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 c35445f12..120cba37d 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 000000000..88dbe59c7 --- /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 000000000..149a22fff --- /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 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 = $('
', { '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 = $('
', { 'class': 'details-view' }); + var row = $('
', { 'class': 'row' }); + this.sidebar_el = $('
', { 'class': 'sidebar-pf sidebar-pf-left col-sm-4 col-md-3 col-sm-pull-8 col-md-pull-9' }); + this.details_el = $('
', { '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 = $('
', { 'class': 'item-select' }); + + $('
', { 'class': 'nav-category' }). + append($('

', { + '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 = $('
', { 'class': 'default-view' }); + $('

', { text: "API Browser" }).appendTo(this.default_view_el); + var commands = $('
').appendTo(this.default_view_el); + $('

').append($('', { + href: "#/p/apibrowser/type=command", + text: "Browse Commands" + })).appendTo(commands); + var objects = $('

').appendTo(this.default_view_el); + $('

').append($('', { + 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 000000000..b40a183a3 --- /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 = $('

', { '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 = $('', { + 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 = $('', { + 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 = $('', { + href: "#" + navigation.create_hash(facet, { + type: 'param', + name: name + }), + text: text || param_name + }); + return link; + }, + + + render_title: function(type, text) { + var title = $('

', { 'class': 'api-title' }); + $('', { + 'class': 'api-title-type', + text: type + }).appendTo(title); + $('', { + 'class': 'api-title-text', + text: text + }).appendTo(title); + return title; + }, + + render_doc: function(text) { + return $('

', { text: text }); + }, + + render_section_header: function(text, link) { + return $('

', { + text: text, + id: link + }); + }, + + render_value_container: function() { + return $('
', { + 'class': 'properties' + }); + }, + + render_value: function(label, value_node, container) { + if (!text) return $(''); + + var row = $('
', { + 'class': 'row' + }); + $('
', { + 'class': 'col-sm-4 prop-label', + text: label + }).appendTo(row); + $('
', { + '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 = $('
'); + for (var i=0,l=flags.length; i', { + '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 = $('

', { + 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 = $('
'); + for (i=0, l=item.methods.length; i0) { + 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 0) { + this.render_section_header('Arguments').appendTo(this.el); + for (i=0, l=item.takes_args.length; i 0) { + var options = []; + var common_options = []; + + for (i=0, l=item.takes_options.length; i -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 0) { + this.render_section_header('Output Params').appendTo(this.el); + var out_params_cnt = $('
'); + for (i=0, l=item.output_params.length; i0) { + 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 = $('
').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 = $('', { + 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 -- cgit