js/runtime/ui_factory.js
// NPM IMPORTS
import assert from 'assert'
// import _ from 'lodash'
import { fromJS } from 'immutable'
// COMMON IMPORTS
import T from '../../../node_modules/devapt-core-common/dist/js/utils/types'
import Loggable from '../../../node_modules/devapt-core-common/dist/js/base/loggable'
// BROWSER IMPORTS
import Component from '../base/component'
import Container from '../base/container'
import Table from '../components/table'
import LogsTable from '../components/logs_table'
import AttributesTable from '../components/attributes_table'
import Dock from '../components/dock'
import DockItem from '../components/dock_item'
import Tabs from '../components/tabs'
import Tree from '../components/tree'
import TableTree from '../components/table_tree'
import Topology from '../components/topology'
import RecordsTable from '../components/records_table'
import InputField from '../components/input-field'
const context = 'browser/runtime/ui_factory'
const DEBUG_TRACE_FIND_STATE=false
/**
* @file UI factory class.
* @author Luc BORIES
* @license Apache-2.0
*/
export default class UIFactory extends Loggable
{
/**
* Create a UI factory instance.
*
* API:
* ->constructor(arg_runtime, arg_store)
* ->get(arg_name):Component - Get a UI component by its name.
* ->create(arg_name):Component - Create a UI component.
* ->find_component_desc(arg_state, arg_name, arg_state_path = []):Immutable.Map|undefined - Find a UI component state.
*
* @param {object} arg_runtime - client runtime.
* @param {object} arg_store - UI components state store.
*
* @returns {nothing}
*/
constructor(arg_runtime, arg_store)
{
super(context, arg_runtime.get_logger_manager())
this.is_ui_factory = true
this._runtime = arg_runtime
this._store = arg_store
this._cache = {}
this._state_by_path = {}
// this.update_trace_enabled()
}
/**
* Get a UI component by its name.
*
* @param {string} arg_name - component name.
*
* @returns {Component}
*/
get(arg_name)
{
if (arg_name in this._cache)
{
return this._cache[arg_name]
}
return this.create_local(arg_name)
}
/**
* Test a UI component by its name.
*
* @param {string} arg_name - component name.
*
* @returns {boolean}
*/
has(arg_name)
{
return (arg_name in this._cache)
}
/**
* Create a UI component with local cache and state.
*
* @param {string} arg_component_name - component name.
* @param {object} arg_component_desc - component description (optional, default:undefined).
*
* @returns {Component} - Component instance
*/
create_local(arg_component_name, arg_component_desc=undefined)
{
this.enter_group('create_local:component name=' + arg_component_name)
let state_path = undefined
let component_desc = arg_component_desc ? ( arg_component_desc.toJS ? arg_component_desc : fromJS(arg_component_desc) ) : undefined
// SEARCH DESCRIPTION INTO CACHE
if (! component_desc)
{
// GET APPLICATION STATE AND INIT APPLICATION STATE PATH
const current_app_state = this._store.get_state()
state_path = ['views']
this.debug('create_local:search in views for ' + arg_component_name)
component_desc = this.find_component_desc(current_app_state, arg_component_name, state_path)
if (! component_desc)
{
this.debug('create_local:search in menubars for ' + arg_component_name)
state_path = ['menubars']
component_desc = this.find_component_desc(current_app_state, arg_component_name, state_path)
if (! component_desc)
{
this.debug('create_local:state not found in views/menubars for ' + arg_component_name)
this.debug('create_local:state_path', state_path)
this.leave_group('create_local:component description not found name=' + arg_component_name)
return undefined
}
}
}
const component = this.create_instance(arg_component_name, component_desc, state_path)
this.leave_group('create_local:component created for name=' + arg_component_name)
return component
}
/**
* Create a UI component.
*
* @param {string} arg_component_name - component name.
* @param {object} arg_component_desc - component description.
* @param {array} arg_state_path - component state path.
*
* @returns {Component|undefined} - Component instance
*/
create_instance(arg_component_name, arg_component_desc, arg_state_path)
{
this.enter_group('create_instance:component name=' + arg_component_name)
const mix = this.create_instance_mix(arg_component_name, arg_component_desc, arg_state_path)
this.leave_group('create_instance:component created for name=' + arg_component_name)
return mix.component
}
/**
* Create a UI component.
*
* @param {string} arg_component_name - component name.
* @param {object} arg_component_desc - component description.
* @param {array} arg_state_path - component state path.
*
* @returns {Promise} - Component instance
*/
create_instance_promise(arg_component_name, arg_component_desc, arg_state_path)
{
this.enter_group('create_instance_promise:component name=' + arg_component_name)
const mix = this.create_instance_mix(arg_component_name, arg_component_desc, arg_state_path)
this.leave_group('create_instance_promise:component created for name=' + arg_component_name)
return mix.promise
}
/**
* Create a UI component.
*
* @param {string} arg_component_name - component name.
* @param {object} arg_component_desc - component description.
* @param {array} arg_state_path - component state path.
*
* @returns {object} - { component:Component, promise:Promise}
*/
create_instance_mix(arg_component_name, arg_component_desc, arg_state_path)
{
this.enter_group('create_instance_mix:component name=' + arg_component_name)
// REGISTER COMPONENT APPLICATION STATE PATH
const state_path = arg_state_path ? arg_state_path : ( arg_component_desc.get('type') == 'menubar' ? ['menubars', arg_component_name] : ['views', arg_component_name] )
this._state_by_path[arg_component_name] = state_path
this._state_by_path[arg_component_name].push('state')
// GET COMPONENT TYPE
const type = arg_component_desc.has('browser_type') ? arg_component_desc.get('browser_type') : arg_component_desc.get('type')
assert( T.isString(type), context + ':create:bad component desctription type string for ' + arg_component_name)
// GET COMPONENT STATE
let comp_state = arg_component_desc.get('state')
comp_state = comp_state.set('name', arg_component_name)
comp_state = comp_state.set('type', type)
comp_state = comp_state.set('state_path', fromJS(state_path) )
// console.log('ui:create:path,state:', state_path, comp_state)
// CREATE COMPONENT INSTANCE
let component_class = this._runtime.ui().get_rendering_class_resolver()(type)
if (!component_class)
{
component_class = this.get_component_class(type)
}
if ( ! component_class)
{
const msg = 'create:error:bad found component class for ' + arg_component_name + ' type=' + type
this.error(msg)
this.leave_group(msg)
return { component:undefined, promise:Promise.reject(context + msg) }
}
// CREATE COMPONENT
const component = new component_class(this._runtime, comp_state)
this._cache[arg_component_name] = component
// LOAD COMPONENT AND INIT BINDINGS
let promise = component.render()
.then(
()=>{
return component.load()
},
(reason)=>{
this.error(reason)
}
)
.then(
()=>{
component.init_bindings()
},
(reason)=>{
this.error(reason)
}
)
this.leave_group('create_instance_mix:component created for name=' + arg_component_name)
return { component:component, promise:promise }
}
/**
* Create a UI component.
*
* @param {string} arg_component_name - component name.
*
* @returns {Promise} - promise of Component instance
*/
create(arg_component_name, arg_component_desc=undefined)
{
this.enter_group('create:component name=' + arg_component_name)
// SEARCH DESCRIPTION INTO CACHE
const component = this.create_local(arg_component_name, arg_component_desc)
if (component)
{
this.leave_group('create:async:found:component name=' + arg_component_name)
return Promise.resolve(component)
}
let component_desc_promise = arg_component_desc ? Promise.resolve(arg_component_desc) : undefined
// REQUEST DESCRIPTION FROM SERVER
if (! component_desc_promise)
{
component_desc_promise = this.request_component_desc(arg_component_name)
}
// DESCRIPTION PROMISE NOT FOUND
if (! component_desc_promise)
{
this.leave_group('create:error:bad promise for component name=' + arg_component_name)
return Promise.reject(context + ':create:bad promise')
}
const component_promise = component_desc_promise.then(
(component_desc)=>{
this.debug('create:found:component description for ' + arg_component_name)
// CHECK COMPONENT DESCRIPTION
if ( ! (T.isObject(component_desc) && component_desc.has && component_desc.get) )
{
this.error('create:found:bad Immutable component description for ' + arg_component_name)
this.leave_group('create:error bad description for ' + arg_component_name)
return Promise.reject(context + 'create:found:bad Immutable component description for ' + arg_component_name)
}
const promise = this.create_instance_promise(arg_component_name, component_desc)
return promise
},
(reason)=>{
this.error('create:error:promise exception for ' + arg_component_name + ' reason=' + reason)
this.leave_group('create:error promise exception for ' + arg_component_name + ' reason=' + reason)
return Promise.reject(context + 'create:error promise exception for ' + arg_component_name + ' reason=' + reason)
}
)
this.leave_group('create:async:component name=' + arg_component_name)
return component_promise
}
/**
* Create a UI component.
*
* @param {string} arg_name - component name.
*
* @returns {Component}
*/
get_component_class(arg_type)
{
switch(arg_type.toLocaleLowerCase())
{
case 'component': return Component
case 'container': return Container
case 'dock': return Dock
case 'dockitem':
case 'dock_item':
case 'dock-item': return DockItem
case 'input-field':
case 'inputfield': return InputField
case 'logstable': return LogsTable
case 'attributestable': return AttributesTable
case 'table': return Table
case 'tabs': return Tabs
case 'tabletree': return TableTree
case 'topology': return Topology
case 'recordstable': return RecordsTable
case 'tree': return Tree
case 'button':
case 'script':
case 'menubar':
case 'hbox':
case 'vbox':
case 'list':
case 'page':
default: return Component
}
// return undefined
}
/**
* Find UI component description.
*
* @param {Immutable.Map} arg_state - registry global state object.
* @param {string} arg_name - component name.
* @param {array} arg_state_path - state path (optional, default=[])
*
* @returns {Immutable.Map|undefined} - component state object.
*/
find_component_desc(arg_state, arg_name, arg_state_path = [])
{
const js_state = arg_state && arg_state.toJS ? arg_state.toJS() : arg_state
DEBUG_TRACE_FIND_STATE && this.debug('ui.find_component_desc for ' + arg_name, arg_state_path, js_state)
if (! arg_state)
{
console.error('state is undefined for ' + arg_name)
return undefined
}
if (! T.isFunction(arg_state.get) )
{
// GLOBAL STATE IS NOT AN IMMUTABLE.MAP
console.error(context + ':find_component_desc:state is not an Immutable for ' + arg_name, arg_state)
this.error(context + ':find_component_desc:state is not an Immutable for ' + arg_name, arg_state)
return undefined
}
// FOUND ON ROOT
if ( arg_state.has('name') )
{
const name = arg_state.get('name').toString()
// arg_state_path.push(name)
if ( name == arg_name )
{
DEBUG_TRACE_FIND_STATE && this.debug('ui.find_component_desc FOUND 1 for ' + arg_name, [])
return arg_state
}
}
// LOOKUP ON VIEWS CHILDREN
let children_key = 'children'
if (arg_state_path.length == 1 && arg_state_path[0] == 'views')
{
children_key = 'views'
arg_state_path.pop()
}
if (arg_state_path.length == 1 && arg_state_path[0] == 'menubars')
{
children_key = 'menubars'
arg_state_path.pop()
}
if (arg_state_path.length == 1 && arg_state_path[0] == 'menus')
{
children_key = 'menus'
arg_state_path.pop()
}
if (arg_state_path.length == 1 && arg_state_path[0] == 'models')
{
children_key = 'models'
arg_state_path.pop()
}
if ( arg_state.has(children_key) )
{
arg_state_path.push(children_key)
if ( arg_state.hasIn( [children_key, arg_name] ) )
{
arg_state_path.push(arg_name)
DEBUG_TRACE_FIND_STATE && this.debug('ui.find_component_desc FOUND 2 for ' + arg_name, arg_state_path)
return arg_state.getIn( [children_key, arg_name] )
}
let result = undefined
arg_state.get(children_key).forEach(
(child_state, key) => {
if (! result)
{
DEBUG_TRACE_FIND_STATE && this.debug('ui.find_component_desc loop on child ' + key + ' for ' + arg_name, arg_state_path)
result = this.find_component_desc(child_state, arg_name, arg_state_path)
if (result)
{
DEBUG_TRACE_FIND_STATE && this.debug('ui.find_component_desc FOUND 3 for ' + arg_name, arg_state_path)
return result
}
}
}
)
if (result)
{
DEBUG_TRACE_FIND_STATE && this.debug('ui.find_component_desc FOUND 4 for ' + arg_name, arg_state_path, result && result.toJS ? result.toJS() : result)
return result
}
arg_state_path.pop() // CHILDREN
}
if ( arg_state.has('name') )
{
arg_state_path.pop() // NAME
}
// console.error('state not found for ' + arg_name)
return undefined
}
request_component_desc(arg_component_name)
{
this.enter_group('request_component_desc')
this.leave_group('request_component_desc:async')
// TODO SET RESOURCES SVC NAME IN SETTINGS OR FIND IT BY ITS TYPE
return this._runtime.register_service('resources_svc')
.then(
(service)=>{
// console.log(context + ':request_component_desc:get service for ' + arg_view_name)
return service.get( {collection:'*', 'resource':arg_component_name} )
},
(reason)=>{
console.error(context + ':request_component_desc:error 1 for ' + arg_component_name, reason)
}
)
.then(
(stream)=>{
// console.log(context + ':request_component_desc:get listen stream for ' + arg_view_name)
return new Promise(
function(resolve, reject)
{
stream.subscribe(
(response)=>{
resolve(response)
}
)
stream.on_error(
(reason)=>{
reject(reason)
}
)
}
)
},
(reason)=>{
console.error(context + ':request_component_desc:error 2 for ' + arg_component_name, reason)
}
)
.then(
(response)=>{
// console.log(context + ':request_component_desc:get response for ' + arg_view_name, response)
if (response.result == 'done')
{
// console.log(context + ':request_component_desc:dispatch ADD_JSON_RESOURCE action for ' + arg_view_name)
const action = { type:'ADD_JSON_RESOURCE', resource:arg_component_name, collection:'views', json:response.datas }
this._store.dispatch(action)
return response.datas
}
return undefined
},
(reason)=>{
console.error(context + ':request_component_desc:error 3 for ' + arg_component_name, reason)
}
)
.catch(
(reason)=>{
console.error(context + ':request_component_desc:an error occured [' + reason + ']')
return undefined
}
)
}
}