webui: API browser

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

https://fedorahosted.org/freeipa/ticket/3129

Reviewed-By: Martin Kosek <mkosek@redhat.com>
Reviewed-By: Tomas Babej <tbabej@redhat.com>
This commit is contained in:
Petr Vobornik
2015-06-05 19:03:46 +02:00
committed by Tomas Babej
parent 392809f984
commit 2a976334c2
7 changed files with 1016 additions and 1 deletions

View File

@@ -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",

View File

@@ -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; }

View File

@@ -24,6 +24,7 @@ define([
'./plugins/sync_otp',
'./plugins/login',
'./plugins/login_process',
'./plugins/api_browser',
// entities
'./aci',
'./automember',

View File

@@ -206,6 +206,12 @@ var nav = {};
}
]
},
{
name: 'apibrowser',
label: 'API browser',
facet: 'apibrowser',
args: { 'type': 'command' }
},
{ entity: 'config' }
]
}

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});