Imported Upstream version 4.6.2
This commit is contained in:
79
install/ui/doc/guides/debugging_web_ui/README.md
Normal file
79
install/ui/doc/guides/debugging_web_ui/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Debugging Web UI - FreeIPA 3.2 and later
|
||||
|
||||
## Introduction
|
||||
|
||||
Since version 3.2 (not released yet) FreeIPA Web UI uses [AMD modules](http://dojotoolkit.org/documentation/tutorials/1.8/modules/). The change also lead to introduction of JavaScript builder&compiler. Code is build into one JavaScript file by Dojo builder and further compiled by UglifyJS compiler. Overall it makes UI faster but it made debugging of production version more complicated -- such code is quite hard to read.
|
||||
|
||||
This document tries to describe how to report runtime error in FreeIPA Web UI.
|
||||
|
||||
## Browser developer tools
|
||||
|
||||
The main prerequisite is to know how to inspect code and thrown JS errors in browser developer tools. This document will deal only with Chrome/Chromium developer tools because, in authors option, they are the most advanced (compared to Internet Explorer 9, Firefox, Opera).
|
||||
|
||||
Chrome Developer tools are [documented](https://developers.google.com/chrome-developer-tools).
|
||||
|
||||
## JavaScript console
|
||||
|
||||
[JavaScript console][1] can be opened by `ctrl + shift + j` keyboard shortcut, or by `F12` and choosing `console` tab in developer tools.
|
||||
|
||||
Error reporting perspective, Console serves for displaying errors. If there is a unexpected error it usually indicates a bug. The error message can be expanded, it will display a call stack with source file names, line number and a link to a [Script Panel][2]. The top line is usually the source of the error.
|
||||
|
||||
### Expected errors
|
||||
|
||||
Web UI needs to have established session. If it doesn't have one, first JSON-RPC command to FreeIPA API will fail with HTTP 401 Unauthorized error. Web UI will try to authenticate by Kerberos ticket. If user doesn't have a valid ticket it will raise another error.
|
||||
|
||||
The output looks like this:
|
||||
|
||||
POST https://vm-061.idm.lab.eng.brq.redhat.com/ipa/session/json 401 (Unauthorized) jquery.js:4
|
||||
GET https://vm-061.idm.lab.eng.brq.redhat.com/ipa/session/login_kerberos?_=1363177904558 401 (Unauthorized) jquery.js:4
|
||||
|
||||
Please ignore these two errors.
|
||||
|
||||
## Script panel
|
||||
|
||||
[Script panel][2] provides graphical interface to a debugger. One can inspect and step through the code.
|
||||
|
||||
### Compiled code
|
||||
|
||||
If the code is compiled, it is usually one or several dense lines. Such code is completely unreadable -> unusable for error reporting. One should use `Pretty print` feature executed by button with `{}` icon located on the bottom. Pretty print will add proper formatting to the code. It may be required to reload the page and reproduce the bug again to have console link to new line number. At this point the Error console contains error with usable line numbers which are helpful for developers.
|
||||
|
||||
### Debugging with source codes
|
||||
|
||||
Even better reporting is when one have and can use source codes. The compiled Web UI layer is located in `/usr/share/ipa/ui/js/freeipa/app.js` file. One can copy files from source git repository in `install/ui/src/freeipa/` directory to the `/usr/share/ipa/ui/js/freeipa/` directory (in will replace the `app.js` file). By doing that, next reload of Web UI will use source files (clearing browser cache may be required). After that all JavaScript errors will contain proper source code name and line number.
|
||||
|
||||
A tool was made to made this task easier. It's located in git repo at `install/ui/util/sync.sh`. One can copy the files form dev machine to test machine by:
|
||||
|
||||
sync.sh --host root@test.machine -fc
|
||||
|
||||
Notes:
|
||||
|
||||
* `root` is user with write rights to `/usr/share/ipa/ui/js/freeipa/` directory
|
||||
* `test.machine` is name of test machine
|
||||
* `-f` option is shortcut for `--freeipa`
|
||||
* `-c` option is shortcut for `--clean` - wipes the content of destination directory
|
||||
* working directory is `install/ui/util` in git directory
|
||||
* without using `--host` option it can also work on local computer (not much tested)
|
||||
|
||||
If one did not backup the original app.js file, he can make new one and sync it there by:
|
||||
|
||||
|
||||
make-ui.sh
|
||||
sync.sh --host root@test.machine -fcC
|
||||
|
||||
Notes:
|
||||
|
||||
* `-C` is shortcut for `--compiled` and means that the compiled version, made by `make-ui.sh`, will be copied.
|
||||
* `make-ui.sh` requires to have Java installed in order to work (uses Rhino)
|
||||
|
||||
## Conclusion
|
||||
|
||||
While reporting an UI bug it's good to check if there is some JavaScript error and if so, send a call stack with line numbers, preferably the ones by using source codes. If source codes are not available, pretty print function should be used and send also code (~15 lines on both sides) around the bug.
|
||||
|
||||
The most valuable information in order of preference are:
|
||||
|
||||
- steps to reproduce
|
||||
- JavaScript error text with call stack with line numbers and source code names
|
||||
- sreenshots
|
||||
|
||||
[1]: https://developers.google.com/chrome-developer-tools/docs/console
|
||||
[2]: https://developers.google.com/chrome-developer-tools/docs/scripts
|
||||
270
install/ui/doc/guides/navigation/README.md
Normal file
270
install/ui/doc/guides/navigation/README.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Navigation refactoring
|
||||
|
||||
## Introduction
|
||||
|
||||
Navigation code undertook complete rewrite. Previous navigation served for multiple purposes. These purposes were so tight together that it created several limitations for future enhancements of the framework. New implementation splits the code to several more or less independent components with given responsibilities. First part of this document describes old issues, second new implementation.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **navigation**: covers whole functionality of changing pages, updating of visual representation and hash.
|
||||
- **menu**: only the visual representation on the page
|
||||
- **hash**: part of the URL after `#` sign
|
||||
- **router**: component which matches hashes to facets
|
||||
- **facet**: 'page' of the application
|
||||
|
||||
## Problems of previous implementation
|
||||
|
||||
### Global state
|
||||
|
||||
The change of facet state was done by changing hash part of URL. The change was handled by navigation, it caused the same chain of commands as showing a different page. Basically a facet told navigation to tell it that it needs to change it state, which is redundant. The only thing needed is to announce: ``I'm changing my state''. Navigation can then just update hash.
|
||||
|
||||
The process was as follows:
|
||||
|
||||
1. `IPA.nav.push_state(state)`
|
||||
2. `IPA.nav.update()`
|
||||
3. `entity.display(facet_node)`
|
||||
4. check if to reload facet
|
||||
5. `facet.show()`
|
||||
6. several calls of `IPA.nav.get_state(key)`
|
||||
7. do the action
|
||||
|
||||
Page change was the same with some additional steps.
|
||||
|
||||
Using global values also prevents using more facets on a one page. Such feature is not required yet but it is an unnecessary limitation which can be easily
|
||||
avoided.
|
||||
|
||||
### Entity structure
|
||||
|
||||
Menu specification and UI structure required an entity to be specified for every page. `entity.display(facet_node)` served for switching facets. The `display` method was basically doing part of a work of application controller. An artificial entity would have to be created for pages without any entity.
|
||||
|
||||
### Fixed dirty check
|
||||
|
||||
`IPA.nav.push_state(state)` method contained a code which checks facet if it has some unsaved changes (is dirty) and displays a ``dirty dialog''. The type of the dialog was set in the method and therefore it was the same for each facet. It would create limitations for non-CRUD facets.
|
||||
|
||||
### DOM representation bound with rest of the functionality
|
||||
|
||||
This was the biggest issue. DOM representation (menu) was required for switching facets. Navigation used a concept of tabs. A tab was a node in navigation tree structure. Each leaf tab represented an entity or entity's facet. So one couldn't have a working navigation without existing menu or a facet without corresponding menu item.
|
||||
|
||||
### Difficult extensibility
|
||||
|
||||
It was very difficult to add item on particular position. The tree-like structure of tabs was hard-coded and initialized on init. Additional changes weren't supported. Navigation didn't offer any method which would add new item on certain position. Partial update of menu wasn't possible. Recreation wasn't tested.
|
||||
|
||||
## New implementation
|
||||
|
||||
Rewrite of the navigation addresses all mentioned issues. The old `IPA.navigation` class was split into server smaller ones with limited responsibilities:
|
||||
|
||||
* `navigation.menu_spec`
|
||||
* `navigation.Menu`
|
||||
* `navigation.Router`
|
||||
* `Application_controller`
|
||||
* `widgets.App`
|
||||
* `widgets.Menu`
|
||||
|
||||
### Menu
|
||||
|
||||
Menu is implemented in two classes: `navigation.Menu` and `widgets.Menu`.
|
||||
|
||||
#### `navigation.Menu`
|
||||
|
||||
Is a data model of a menu. It contains object store of menu items. It provides array of currently selected items and events which can be used for observation of changes.
|
||||
|
||||
New items can be added by simple method.
|
||||
|
||||
#### `widgets.Menu`
|
||||
|
||||
Is the HTML representation of a menu - a widget. It uses `navigation.Menu` as it's data model. It listens to data model's `selected` event to update selection state and observers data.models object store to reflect it's changes. Hence, new items are immediately rendered and removed are destroyed.
|
||||
|
||||
Menu offers `item-select` event. Other components should listed to it and do appropriate work.
|
||||
|
||||
Menu widget doesn't change it's data model in any way. It just observes.
|
||||
|
||||
### Router
|
||||
|
||||
`navigation.Router` is applications router. It is an extension of dojo router. It adds a support for facets. It provides API for route registration, hash generation and update, and navigation to facets.
|
||||
|
||||
IMPORTANT: Facets widgets and other parts of application shouldn't use router directly. They should use navigation proxy(`navigation`) instead.
|
||||
|
||||
#### Route registration
|
||||
|
||||
Router has `register_route(routes, handler)` method for registering of routes. It's a wrapper of Dojo's router's `register` method. The difference is that this method can register multiple routes for one handler and that the handler is bound to router object so it can has it reference.
|
||||
|
||||
Standard implementation has handler for entity pages and for standalone pages. Standalone pages are not supported yet because facet register (a map of facets) is not implemented yet.
|
||||
|
||||
Extensions can register their own routes+handlers to support new types of facets/hash representations.
|
||||
|
||||
Router maintains each route registration so they can be deleted if needed.
|
||||
|
||||
#### Route evaluation
|
||||
|
||||
Evaluation is done by dojo router component. When it matches a route, it calls corresponded handler.
|
||||
|
||||
### Handlers and showing a facet
|
||||
|
||||
Route handlers receive dojo router's event argument objects. Handler can inspect old hash, new hash and state pulled from the new hash.
|
||||
|
||||
Handler should decode a facet and it's state, set the state to the facet and call `show_facet(facet)`. It publishes `facet-show` event. This tells the application that we should change the facet but router doesn't care how it's done.
|
||||
|
||||
Handler should always change if `ignore_next` flag isn't set. It can use `check_clear_ignore` which does this check and cleans the flag. When the flag is set the handler shouldn't do anything. The flag is mainly used when updating hash of already displayed facet. User can then bookmark the facet state.
|
||||
|
||||
### Navigating to a facet
|
||||
|
||||
It's a opposite operation of hash change handling and facet show. Router has two build-in methods: `navigate_to_facet` and `navigate_to_entity_facet`. Their purpose is to create hash which can be matched by some route and then call `navigate_to_hash(hash, facet)`.
|
||||
|
||||
**navigate_to_hash**
|
||||
Tells application "I want to change facet" by raising `facet-change` global event. Listeners can inspect associated facet and hash, then set `navigation.canceled` property if they want to prevent the change. They should do it synchronously. When somebody cancels the change, navigation notifies it by raising `facet-change-canceled` global event. Otherwise the hash is updated.
|
||||
|
||||
#### Updating hash
|
||||
|
||||
Hash can be updated in two cases: navigating to a facet and updating a facet state. The difference is in setting `ignore_next` property. The former sets it, latter doesn't.
|
||||
|
||||
|
||||
### Navigation proxy
|
||||
|
||||
Implemented as singleton in `.navigation` module. It's purpose is to offer interface for navigating between pages to facet, widgets and other components.
|
||||
|
||||
Consumers don't have to worry about internal implementation => they are not bound to specific router implementation.
|
||||
|
||||
Exposed methods:
|
||||
|
||||
* `show`
|
||||
* `show_entity`
|
||||
* `show_default`
|
||||
|
||||
Check code for arguments documentation.
|
||||
|
||||
Extensions can add new methods.
|
||||
|
||||
### Application controller
|
||||
|
||||
Application controller (AC) ties all the components together. It basically contains most of the integration logic as navigation did before but it delegates most of the actions to other components.
|
||||
|
||||
#### Application initialization
|
||||
|
||||
AC initialization is done in 3 phases: `app-init`, `metadata`, `profile`. Creation of AC instance and phase registration is done in `./app` module.
|
||||
|
||||
**app-init phase**
|
||||
|
||||
* creates AC instance
|
||||
* create menu store
|
||||
* creates router
|
||||
* creates app widget
|
||||
* bounds menu widget with menu store
|
||||
* registers handlers for:
|
||||
* click in menu widget (`item-select` event)
|
||||
* select change in menu data model (`selected` event)
|
||||
* profile view (app widget event)
|
||||
* logout (app widget event)
|
||||
* `facet-show` (router)
|
||||
* `facet-change` (router)
|
||||
* `facet-change-canceled` (router)
|
||||
* subscribes to global events (topics):
|
||||
* `phase-error`
|
||||
* renders app widget
|
||||
|
||||
**metadata phase**
|
||||
basically calls `IPA.init` to get metadata and profile information.
|
||||
|
||||
**profile phase**
|
||||
chooses menu based on identity information obtained in metadata phase. Adds menu items to menu.
|
||||
|
||||
Starts the router. Start of the router causes hash evaluation. A facet is displayed when hash is specified. When no facet is displayed, AC navigates to profile's default page.
|
||||
|
||||
**phase error:**
|
||||
When some phase fail, error content is displayed. At the time we can't count that everything is initialized so tools for displaying the error are very limited - no metadata, no profile, possibly no app widget.
|
||||
|
||||
#### Facet changing
|
||||
|
||||
AC listens to `facet-change` event. It compares current and new facet and asks old one if it is dirty, when it is, AC obtains dirty dialog from current facet and displays it. Hence, facets can choose how the dirty dialog would look like. AC prevents the change when told by the dialog.
|
||||
|
||||
#### Facet showing
|
||||
|
||||
AC hides old facets and shows new one. If new doesn't have container AC sets it one and registers listener for facet's `facet-state-change` event. At this point it also tries to identify menu item with the facet and select it. Unlike the previous implementation, it also works when no menu item is matched.
|
||||
|
||||
#### Menu clicks
|
||||
|
||||
On menu click AC tries to match facet with menu item and navigate to it.
|
||||
|
||||
Menu item is not selected because the change can be prevented. Therefore selection is performed in facet show handler.
|
||||
|
||||
#### Facet state changing
|
||||
|
||||
AC listens to `facet-state-change` event. AC updates hash when displayed facet changes its state.
|
||||
|
||||
#### Multiple menu levels
|
||||
|
||||
AC expect that the menu will have 2-3 menu levels. Current used CSS doesn't automatically handle 3rd level. Hence, AC observes select state and sets special class on content node to inform CSS.
|
||||
|
||||
#### Further generalization
|
||||
|
||||
As a framework class AC should be further generalized, mostly to get rid of IPA specific functions (app widget and its handlers, profiles,...).
|
||||
|
||||
|
||||
## How does it all work together?
|
||||
|
||||
Now, when everything is described, we can proceed to method calls chains examples of most common use cases. It will explain what operations are executed for certain use cases.
|
||||
|
||||
### Loading Web UI
|
||||
|
||||
It just goes through `app-init`, `metadata`, `profiles` phases. At the end of profile phase a facet is selected based on hash, if there is no hash, default facet is selected. The difference is only lack of `navigate_to_xxx` call in the former case. Details will be described in following chapter.
|
||||
|
||||
### Navigation to a page
|
||||
|
||||
Initiated by:
|
||||
|
||||
// navigation is './navigation' module
|
||||
var state = { key1: val1, key2: val2 };
|
||||
navigation.show(target_facet, state);
|
||||
|
||||
1. proxy gets reference to navigation of current AC: `nav`
|
||||
2. proxy calls `nav.navigate_to_facet(facet, state);`
|
||||
3. `navigate_to_facet` creates hash, calls `navigate_to_hash(facet, hash)`
|
||||
4. `navigate_to_hash` ask app if it can change facet by raising `facet-change` event
|
||||
5. AC will responds to it and ask current facet if it's dirty, if so:
|
||||
a. navigation is canceled. Must be canceled because it has to be synchronous and it can't wait for user input.
|
||||
b. `facet-change-cancelled` event is raised, no build in listeners for it
|
||||
c. dirty dialog is displayed
|
||||
d. user selects action
|
||||
e. on confirmation AC calls `navigate_to_hash` with same params to proceed with the change
|
||||
6. hash is updated
|
||||
7. hash changed event is raised and then processed by router
|
||||
8. router matches hash to handler and calls handler
|
||||
9. handler decodes facet and state from hash
|
||||
10. handler sets state to facet, it will cause appropriate actions in facet
|
||||
11. handler raises `facet-show` event
|
||||
12. `facet-show` event is caught by AC (handler: `on_facet_show`)
|
||||
13. AC matches facet to menu item (`this._find_menu_item(facet)`)
|
||||
14. AC selects menu item (`this.menu.select(menu_item)`)
|
||||
15. menu store raises `selected` event
|
||||
a. menu widget redraw itself according to selection
|
||||
b. AC adds style for third level menu if needed (handler: `on_menu_select`)
|
||||
16. AC set container for facet if needed (usually for the first time)
|
||||
17. AC hides current facet (`current_facet.hide()`)
|
||||
18. AC shows new facet (`facet.show()`)
|
||||
* facet clears and refreshes when `needs_update()` is true (might be set by as a result of `facet.set_state(state)` call in hash handler
|
||||
|
||||
### Click on menu item
|
||||
|
||||
1. user clicks on menu item
|
||||
2. raises `item-select` event of menu widget
|
||||
3. AC catches it
|
||||
4. AC traverse menu items to get first one with facet or entity set
|
||||
5. AC calls `navigate_to_entity_facet` or `navigate_to_facet` based on the result of previous operation
|
||||
6. rest is same as in [previous use case](#navigation-to-a-page)
|
||||
|
||||
### Update of facet state
|
||||
|
||||
1. facet updates its state
|
||||
2. facet publishes `facet-state-change` event.
|
||||
3. AC catches it and calls `navigation.create_hash` and then `navigation.update_hash`
|
||||
4. facet internally responds to the change
|
||||
|
||||
As you can see it just updates the hash but no navigation is done. This feature makes facet independent on navigation breaks one-facet displayed limitation (there can be facet containing more facets).
|
||||
|
||||
### Set hash by hand in a browser when facet is dirty
|
||||
|
||||
Basically the same as navigation to a page, only from hash is updated step. This implementation, nor the previous one, doesn't check dirty state of currently displayed facet. Hence, facet is changed in both cases.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should facet state update be moved from navigation to app controller?
|
||||
- Add extensibility point to Menu Store for supporting initialization of different types of menu items? Extract entity & facet logic to it.
|
||||
125
install/ui/doc/guides/phases/README.md
Normal file
125
install/ui/doc/guides/phases/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Introduction
|
||||
|
||||
Declarative nature and support for extensibility of FreeIPA WebUI
|
||||
creates demand for structural initialization of the application. Phase
|
||||
components were created to solve this tasks.
|
||||
|
||||
## Components
|
||||
|
||||
Phases functionality consists of the two components/modules:
|
||||
`./_base/PhaseController` and `./phases`.
|
||||
|
||||
### PhaseController
|
||||
|
||||
Phase controller basically does all the work. It provides functionality for running and
|
||||
changing phases.
|
||||
|
||||
- define phases
|
||||
- controls instantiation of PhaseController
|
||||
- provides API for phase task registration
|
||||
|
||||
## Phase
|
||||
|
||||
Phase is a part of application runtime. Most of the phases are related
|
||||
to load and initialization process.
|
||||
|
||||
### Tasks
|
||||
|
||||
Phase consists of task. Each task can have a priority set, 10 is
|
||||
default. Task can by synchronous or asynchronous. When phase is started
|
||||
it runs all tasks sequentially sorted by priority. If the task is
|
||||
synchronous it waits for its completion before starting another task.
|
||||
Asynchronous task are just started. Phase is completed when all task
|
||||
finishes - waits for all asynchronous tasks.
|
||||
|
||||
Asynchronous task is a task which handler returns a promise.
|
||||
|
||||
### Phase task registration
|
||||
|
||||
Modules should register a phase task through `./phases` module.
|
||||
|
||||
phases.on('phase_name', handler, priority);
|
||||
|
||||
## Descriptions of FreeIPA phases
|
||||
|
||||
FreeIPA has following phases:
|
||||
|
||||
- customization
|
||||
- registration
|
||||
- init
|
||||
- metadata
|
||||
- post-metadata
|
||||
- profile
|
||||
- runtime
|
||||
- shutdown
|
||||
|
||||
### Resource load implicit phase
|
||||
|
||||
Each application needs to load its resources. This mainly means
|
||||
JavaScript files but also CSS files, images and fonts. Phases modules
|
||||
are part of that data and therefore are no initialized until loaded.
|
||||
Hence resources load is an implicit and a first phase.
|
||||
|
||||
FreeIPA Web UI uses AMD modules therefore resources have to be either
|
||||
declared in main HTML file’s header or in modules specification. The
|
||||
former one is obsolete and should be replace by the latter.
|
||||
|
||||
The main HTML file should require an application module. Application
|
||||
module is a module that should have dependencies required for the
|
||||
application to run. By specifying these dependencies we may control
|
||||
which modules/plugins and their dependencies get loaded. Currently it’s
|
||||
`freeipa/app`.
|
||||
|
||||
### Alternation phase
|
||||
|
||||
Serves for altering components specifications. This phase should be used
|
||||
only by plugins and configurable modules. Core modules shouldn’t use
|
||||
this phase.
|
||||
|
||||
### Registration phase
|
||||
|
||||
Modules should register widget, facet, entity final construct
|
||||
specifications.
|
||||
|
||||
### Init phase
|
||||
|
||||
Serves for initialization of core UI components: application controller,
|
||||
router, menu, application widget …
|
||||
|
||||
### Metadata phase
|
||||
|
||||
Metadata, configuration and user specific information should be loaded
|
||||
in this phase.
|
||||
|
||||
### Runtime phase
|
||||
|
||||
Phase where plugins can expect that application is completely
|
||||
initialized. Most of user interaction will happen here.
|
||||
|
||||
### Shutdown phase
|
||||
|
||||
Destroys session. Currently redirects to other page. In future may
|
||||
destroy all facets and entities and just show basic UI again.
|
||||
|
||||
## Adding a new phase
|
||||
|
||||
One can add a new phase at any time. It will be executed only if it’s
|
||||
position is after currently running phase.
|
||||
|
||||
|
||||
define(['./phases'], function(phases) {
|
||||
|
||||
// add 'new-phase' on the last position
|
||||
phases.add('new-phase');
|
||||
|
||||
// add 'pre-runtime' phase before 'runtime' phase
|
||||
phases.add('pre-runtime', { before: 'runtime' });
|
||||
|
||||
// add 'post-runtime' phase after 'runtime' phase
|
||||
phases.add('post-runtime', { after: 'runtime' });
|
||||
|
||||
// add '7th-phase' phase on exact position (whatever it is)
|
||||
// or on the last, if position is bigger than number of currently
|
||||
// registered phases
|
||||
phases.add('7th-phase', { position: 7 });
|
||||
});
|
||||
BIN
install/ui/doc/guides/phases/icon.png
Normal file
BIN
install/ui/doc/guides/phases/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
73
install/ui/doc/guides/plugins/README.md
Normal file
73
install/ui/doc/guides/plugins/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Plugins
|
||||
|
||||
This document tries to describe new plugin infrastructure of FreeIPA Web UI.
|
||||
|
||||
## Plugin location and structure
|
||||
|
||||
Plugin is registered by creating a directory `$plugin_name` in `/usr/share/ipa/ui/js/plugins/`. `$plugin_name` is name of the plugin, it has to start with a letter and may contain only ASCII alphanumeric character, underscore _ and dash - .
|
||||
|
||||
Plugin folder must contain `$plugin_name.js` file. It's have to be either AMD module or a layer built by Dojo builder.
|
||||
|
||||
## Plugin index - registration
|
||||
|
||||
Web UI consist only of static files. It can't examine plugins directory during runtime. Therefore it needs some configuration file with listed plugins to load. We can call this file a plugin index. It's located in `/usr/share/ipa/ui/js/freeipa/plugins.js`.
|
||||
|
||||
### Structure
|
||||
|
||||
Plugin index is a simple AMD module with array `plugins` containing plugin names.
|
||||
|
||||
define([], function() {
|
||||
return [
|
||||
'module-1',
|
||||
'module-2'
|
||||
];
|
||||
});
|
||||
|
||||
### Index regeneration
|
||||
|
||||
Plugin index is dynamically created by wsgi script: `/usr/share/ipa/wsgi/plugins.py`. Httpd rewrite rule hides this fact and makes sure that it can be access as at it's location.
|
||||
|
||||
## Load
|
||||
|
||||
Plugins have to be loaded before registration phase, that means right after implicit resource load phase. First we must load index and then plugins.
|
||||
|
||||
### Loading index
|
||||
|
||||
Plugin index is part of `freeipa` package of FreeIPA Web UI, therefore the easiest would be to add `./plugins` as a dependency in `./app` module. But that would be wrong. `./app` module is part of freeipa layer build and it would be included - it couldn't be updated later. Hence, it must be loaded dynamically.
|
||||
|
||||
### Loading plugins
|
||||
|
||||
From AMD's perspective plugin is a package. Loader can't load it because it doesn't know where it is located, so each plugin needs to be registered in a loader with:
|
||||
|
||||
{
|
||||
name: '$plugin_name',
|
||||
path: 'plugins/$plugin_name'
|
||||
}
|
||||
|
||||
Then, plugin may be dynamically loaded. Application should proceed to registration phase after all plugins are loaded.
|
||||
|
||||
## Extending UI
|
||||
|
||||
At this point plugins can do whatever they want to do. In future we should provide more or less stable plugin API. Consumers of the API would care only about the API and not the underlying functionality. It would allow to create some internal changes without breaking simple plugins.
|
||||
|
||||
There are 3 main extension use cases:
|
||||
|
||||
* adding completely new page
|
||||
* adding new UI fields to existing pages
|
||||
* by modifying page spec
|
||||
* by extending already created pages
|
||||
|
||||
Both cases of adding new UI fields are not straightforward.
|
||||
|
||||
### Modifying page spec
|
||||
|
||||
Spec of most pages are enclosed in function calls which are usually evaluated right before creating a page. All spec should be moved to static property of corresponding module. Spec should be completely declarative. It should no contain any calls dependent on already initialized app. Extensions should extend the spec at `alternation` phase.
|
||||
|
||||
### Modifying already created pages
|
||||
|
||||
Facets don't support content recreation. They expects that `create(container)` method is called only once. New rules for facets and widgets have to be set to support extensibility:
|
||||
|
||||
* Web UI components should not be directly depend on HTML representation of other components (widget/facets)
|
||||
* each widget should support destroying and recreating its HTML representation
|
||||
* widget should not modify it's container
|
||||
* communication should be done mostly by events and topics (global event)
|
||||
304
install/ui/doc/guides/registries/README.md
Normal file
304
install/ui/doc/guides/registries/README.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Components registration and build
|
||||
|
||||
This document contains design of component build system.
|
||||
|
||||
## Build system
|
||||
|
||||
Build system is the core of Web UI. Its task is to create instance from components definition (spec object).
|
||||
|
||||
The system consists of:
|
||||
|
||||
* registries
|
||||
* builders
|
||||
* component classes for registration
|
||||
* nested component specification
|
||||
|
||||
## Registries
|
||||
|
||||
We can have two types of registries:
|
||||
|
||||
* constructor registries
|
||||
* singleton registries
|
||||
|
||||
|
||||
## Constructor registry
|
||||
|
||||
Constructor registry holds information required for building an object. It's basically a map with component type as a key and `{factory, constructor, default specification}` as value. For successful registration one must provide a component type, and factory or a constructor. Default specification object is not required.
|
||||
|
||||
Both constructors and factories should not expect more than one parameter. The only param which they will receive is a specification object.
|
||||
|
||||
## Singleton registry
|
||||
|
||||
Singleton registry is a object which stores `[component_name, instance]` pairs. The difference between singleton registry and normal JavaScript object is that the registry is supposed to create the instance if it doesn't exist when requested.
|
||||
|
||||
To accomplish such task registry has to have a factory or a component constructor and a specification object available. This values might be stored in a constructor registry. Therefore singleton registry might internally contain constructor registries.
|
||||
|
||||
## Build
|
||||
|
||||
The build process has a general rule: Each component handles the build of its children. It's expected that the component knows what type of children it's supposed to contain and therefore it knows how to build them. Usually that means that it knows which builder to use.
|
||||
|
||||
To allow smooth transition, we need to support building object by using various methods: registry, factory and constructor. When using registry, we may also want to allow applying some defaults. Therefore spec object may contain following build related properties to tell how it should be build. Each build property should have `$` prefix to distinguish it from spec properties.
|
||||
|
||||
* `$type`: string - component type
|
||||
* `$factory`: factory function which expects this spec. obj. as param
|
||||
* `$ctor`: constructor function which expects this spec. obj. as param
|
||||
* `$mixim_spec`: boolean indicating that multiple specification objects can be mixed in.
|
||||
* `$pre_ops`: operations which modify spec before instantiation
|
||||
* `$post_ops`: operations which modify object after instantiation
|
||||
|
||||
|
||||
## Builder
|
||||
|
||||
We can see that the build process is moderately complex and therefore it should not be done by parent components. It's a task for a builder.
|
||||
|
||||
Additionally builder must deal with following conditions:
|
||||
|
||||
* to have or to not have associated constructor registry
|
||||
* handle only string or a function as a spec object
|
||||
* handle already built object
|
||||
* handle array of spec
|
||||
|
||||
**Builders tasks**
|
||||
|
||||
1. get factory or constructor function
|
||||
2. make copy of spec object
|
||||
3. remove reserved properties from spec object
|
||||
4. call factory|constructor with a copy of spec object as param
|
||||
5. return built instance
|
||||
|
||||
### Get factory or constructor function
|
||||
|
||||
The flow depends on:
|
||||
|
||||
* type of spec object (object, string, function)
|
||||
* presence of Constructor registry
|
||||
|
||||
If more than one specification is defined (type, constructor, factory) the order of precedence is:
|
||||
|
||||
1. factory
|
||||
2. constructor
|
||||
3. type (registry contain spec)
|
||||
a. factory
|
||||
b. constructor
|
||||
|
||||
|
||||
#### Distinguishing between methods
|
||||
|
||||
Methods are distinguished by type of spec object or it's properties.
|
||||
|
||||
**Type**
|
||||
|
||||
* spec is string
|
||||
* spec is Object and `spec.type` is string
|
||||
* spec in registry is evaluated the same way except that it shouldn't contain `type` property.
|
||||
|
||||
**Factory**
|
||||
|
||||
* spec is function
|
||||
* spec is Object and spec.factory is function
|
||||
|
||||
**Constructor**
|
||||
|
||||
* spec is function and with extend method (indicates that it's dojo class)
|
||||
* spec is Object and `spec.constructor` is function
|
||||
|
||||
Builder must have a reference to Constructor registry for `Type` method to be able to build the object, otherwise it doesn't have constructor or factory available, unless builder has defaults set.
|
||||
|
||||
### Builder defaults
|
||||
|
||||
Builder can have default values for factory or constructor. It's useful for builders which specializes in building one object type. Ie. widget builder might have `text_widget` as a default.
|
||||
|
||||
### String modes
|
||||
|
||||
Builder supports two behaviors when spec is a string: `type` and `property`.
|
||||
|
||||
**type**
|
||||
|
||||
String specifies object type - default
|
||||
|
||||
**property**
|
||||
|
||||
String represents a value of spec property. Spec property is specified by `string_property` builder property. One has to make sure that builder has default factory or a constructor set when using this mode.
|
||||
|
||||
### Conflicts of specification objects
|
||||
|
||||
When spec is an Object and Constructor registry has also defined default specification object, only the supplied spec is used. This behavior might be overridden by setting `mixim_spec` attribute of supplied spec to `true`. In such case supplied spec is mixed in a default spec. Setting `mixim_spec` in default spec doesn't have any effect, it's ignored.
|
||||
|
||||
### Spec modification - pre_ops
|
||||
|
||||
pre_ops' purpose is to modify spec object. Pre_ops can be specified at locations ordered by execution order as follows:
|
||||
|
||||
* builder: applies to all objects
|
||||
* construct registry: applies to objects with given type
|
||||
* spec: applies only to the one object
|
||||
|
||||
#### Types of operations
|
||||
|
||||
Pre_op can be function, object or diff object.
|
||||
|
||||
**Function**
|
||||
|
||||
Takes two params: `spec` and `context`. Has to return `spec`. It can replace the spec by other object but it has make sure that it returns something. Returned spec will replace the previous one.
|
||||
|
||||
**Object**
|
||||
|
||||
Plain object is mixed in into spec object
|
||||
|
||||
**Diff object**
|
||||
|
||||
Diff object is applied on spec by `spec_mod` utility. Control properties are removed and then remains are mixed in into spec as if it was just plain object.
|
||||
|
||||
### Object modification - post_ops
|
||||
|
||||
post_ops are similar to pre_ops but their purpose is to modify the created object. Post_ops can only be functions and objects, no diff objects. Functions receive `obj`, `spec` and `context` as params and have to return `obj`.
|
||||
|
||||
|
||||
## Global registries for builders and registries
|
||||
|
||||
Web UI has several object types: entities, facets, widgets, fields, actions, validators, entity policies, facet policies. Each object type requires its builder, construct registry and some also a singleton registry. Two umbrella pseudo-registries were created to avoid creation of twice as many modules and also to allow redefinition of the registries: `reg` and `builder`.
|
||||
|
||||
**`reg`**
|
||||
is a registry of construct registries or singleton registries. Difference between normal registry is that one can access its registries directly.
|
||||
|
||||
**`builder`**
|
||||
is a registry of builders. Each object type may have its own builder with different defaults, pre_ops, post_ops and construct registry. Construct registry is usually the one registered in `reg` under the same type name.
|
||||
Builder also serves as general build interface so one don't have to obtain the builder first. It's interface is:
|
||||
|
||||
var new_obj = builder.build(object_type, spec, context, overrides);
|
||||
|
||||
`builder` contains general builder - a builder without any defaults. It can be used when there is a need to use builder logic but it doesn't require any defaults:
|
||||
|
||||
var new_obj = builder.build('', spec, context, overrides);
|
||||
|
||||
## Examples
|
||||
|
||||
For demonstration purposes examples will use action as a framework object
|
||||
|
||||
### Registration of builder and registry to global registries
|
||||
|
||||
|
||||
// './facet_registry.js'
|
||||
define(['./builder', './reg'], function(builder, reg) {
|
||||
|
||||
var exp = {};
|
||||
|
||||
var exp.action = function(spec) {
|
||||
//fac definition
|
||||
};
|
||||
|
||||
// Registration of Action builder and registry into global registries
|
||||
// ./builder can create new builder just by calling `get` with
|
||||
// not-yet-registered type
|
||||
exp.action_builder = builder.get('action');
|
||||
|
||||
// setting default factory
|
||||
exp.action_builder.factory = exp.action;
|
||||
|
||||
// registration of construct registry - builder by default creates its
|
||||
// own construct registry
|
||||
reg.set('action', exp.action_builder.registry);
|
||||
|
||||
return exp;
|
||||
});
|
||||
|
||||
|
||||
#### Singleton registry
|
||||
|
||||
Some object types, like entities, are singletons. It's type corresponds to instance. For these types the registration is slightly different:
|
||||
|
||||
define(['./_base/Singleton_registry','./builder', './reg'],
|
||||
function(Singleton_registry, builder, reg) {
|
||||
|
||||
var exp = {};
|
||||
// module definition ...
|
||||
|
||||
// registries definition
|
||||
var registry = new Singleton_registry();
|
||||
reg.set('entity', registry);
|
||||
builder.set('entity', registry.builder);
|
||||
registry.builder.factory = exp.entity;
|
||||
|
||||
return exp;
|
||||
});
|
||||
|
||||
|
||||
### New action registration
|
||||
|
||||
define([
|
||||
'./phases',
|
||||
'./reg'
|
||||
], function(phases, reg) {
|
||||
var exp = {};
|
||||
exp.custom_action = function(spec) { /* definition */ };
|
||||
|
||||
exp.registry = function() {
|
||||
var a = reg.action; // action construct registry
|
||||
a.register('custom', exp.custom_action);
|
||||
};
|
||||
|
||||
// register in registration phase
|
||||
phases.on('registration', exp.registry);
|
||||
return exp;
|
||||
});
|
||||
|
||||
|
||||
### Build of new action
|
||||
|
||||
define([
|
||||
'./builder'
|
||||
], function(builder) {
|
||||
var action = builder.build('action', {
|
||||
$type = 'custom'
|
||||
/* other spec properties */
|
||||
}); // no context and no overrides
|
||||
|
||||
// simplified:
|
||||
action = builder.build('action', 'custom');
|
||||
});
|
||||
|
||||
|
||||
### Raw usage of Singleton registry without using global registries
|
||||
|
||||
It's only example. One should use global registries for facets and entities.
|
||||
|
||||
#### Definition
|
||||
|
||||
// './facet_registry.js'
|
||||
define(['./_base/Singleton_registry'], function(Singleton_registry) {
|
||||
|
||||
return new Singleton_registry();
|
||||
});
|
||||
|
||||
#### Usage
|
||||
|
||||
define(['./facet_registry', './ipa'], function(registry, IPA) {
|
||||
|
||||
var my_facet = function(spec) {
|
||||
|
||||
spec = spec || {};
|
||||
|
||||
var that = IPA.facet(spec);
|
||||
return that;
|
||||
};
|
||||
|
||||
// optional, but for facets preferable
|
||||
var default_spec = { /* some properties */ };
|
||||
|
||||
// register factory as construct specification
|
||||
registry.register('foo_facet', my_facet, default_spec);
|
||||
|
||||
|
||||
// we may define other instance with the same factory
|
||||
var other_spec = { /* other properties */ };
|
||||
|
||||
// register with spec object
|
||||
registry.register({
|
||||
type: 'other_facet',
|
||||
factory: my_facet,
|
||||
spec: other_spec
|
||||
});
|
||||
|
||||
// obtaining instance
|
||||
var foo_facet = registry.get('foo_facet');
|
||||
var other_facet = registry.get('other_facet');
|
||||
});
|
||||
Reference in New Issue
Block a user