Reference Source

js/runtime/ui.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'
import DefaultRenderingPlugin from '../../../node_modules/devapt-core-common/dist/js/default_plugins/rendering_default_plugin'
import RenderingBuilder       from '../../../node_modules/devapt-core-common/dist/js/rendering/rendering_builder'
import RenderingPlugin        from '../../../node_modules/devapt-core-common/dist/js/plugins/rendering_plugin'
import Stream                 from '../../../node_modules/devapt-core-common/dist/js/messaging/stream'

// BROWSER IMPORTS
import UIFactory      from './ui_factory'
import UIRendering    from './ui_rendering'
import Page           from '../components/page'
import LayoutSimple   from '../base/layout_simple'
import DisplayCommand from '../commands/display_command'


const context = 'browser/runtime/ui'



/**
 * @file UI interaction class.
 * @author Luc BORIES
 * @license Apache-2.0
 */
export default class UI extends Loggable
{
	/**
	 * Create a UI instance.
	 * 
	 * 	API:
	 * 		->constructor(arg_runtime, arg_store)
	 * 
	 * 		->is_loaded():boolean - Test if UI is loaded and is ready to process display commands.
	 * 		
	 * 		->create_display_command(arg_cmd_settings):DisplayCommand - Create a DisplayCommand instance.
	 * 		->pipe_display_command(arg_display_command):nothing - Append a display command to the UI commands pipe.
	 * 
	 * 		->get_current_layout():Layout - Get current application layout.
	 * 		->get_resource_description_resolver():function - Get a resolver function to find UI component description.
	 * 		->get_rendering_function_resolver():function - Get a resolver function to find UI rendering function.
	 * 		->get_rendering_class_resolver():Class - Get a resolver function to find UI component class.
	 * 		
	 * 		->load():nothing - Load plugins.
	 * 
	 * 		->get(arg_name):Component - Get a UI component by its name.
	 * 		->create(arg_name):Promise - Create a UI component.
	 * 		->create_local(arg_name, arg_component_desc):Component - Create a UI component.
	 * 		
	 * 		->register_rendering_plugin(arg_plugin_class):nothing - Register a browser rendering plugin.
	 * 		->request_middleware(arg_middleware, arg_svc_route):Promise - Request server about middleware rendering.
	 * 
	 * @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 = true

		this._runtime = arg_runtime
		this.store = arg_store

		this.classes = {}
		this.classes.RenderingPlugin = RenderingPlugin
		this.classes.DefaultRenderingPlugin = DefaultRenderingPlugin
		
		this._ui_factory = new UIFactory(arg_runtime, arg_store)
		this._ui_rendering = new UIRendering(arg_runtime, this)
		this._ui_builder = new RenderingBuilder(arg_runtime, this._ui_rendering._assets_urls_templates, undefined)
		// this._ui_layout = undefined
		this._ui_layout = new LayoutSimple(this._runtime, {name:'main layout', type:'simple'})
		this._ui_layout._ui = this

		this._rendering_plugins = []
		this._rendering_plugins_counter = 0
		this._rendering_plugins_map = {}
		this._ordered_used_plugins_name = []

		this.page = {
			menubar:undefined,
			header:undefined,
			breadcrumbs:undefined,
			content:undefined,
			footer:undefined
		}

		this.body_page = new Page()

		this._display_command_waiting = []
		this._display_command_timer = undefined
		this._display_commands_pipe = new Stream()
		this._display_commands_pipe.subscribe(
			(cmd)=>{
				console.log(context + ':pipe_display_command.subscribe:do', cmd.get_name(), cmd)
				cmd.do()
			}
		)

		// this.enable_trace()
	}



	/**
	 * Test if UI is loaded and is ready to process display commands.
	 * 
	 * @returns {boolean} - true:UI is ready to process display commands,false:UI isn't ready.
	 */
	is_loaded()
	{
		return this._rendering_plugins_counter > 0 && this._rendering_plugins_counter == this._rendering_plugins.length
	}


	/**
	 * Create a DisplayCommand instance.
	 * 
	 * @param {object} arg_cmd_settings - command settings.
	 * 
	 * @returns {DisplayCommand}
	 */
	create_display_command(arg_cmd_settings)
	{
		return new DisplayCommand(this._runtime, arg_cmd_settings)
	}



	/**
	 * Append a display command to the UI commands pipe.
	 * If UI isn't ready to process display command, delay append.
	 * Commands are pushed into a stream.
	 * 
	 * @param {DisplayCommand} arg_display_command - display command to pipe.
	 * 
	 * @returns {nothing}
	 */
	pipe_display_command(arg_display_command)
	{
		if ( ! (T.isObject(arg_display_command) && arg_display_command.is_display_command) )
		{
			console.warn(context + ':pipe_display_command:bad display command', arg_display_command)
			return
		}

		if ( ! this.is_loaded() )
		{
			console.log(context + ':pipe_display_command:UI is not loaded')

			this._display_command_waiting.push(arg_display_command)

			if (! this._display_command_timer)
			{
				console.log(context + ':pipe_display_command:create a timer')

				const max_loops = 100
				let loop_counter = 0
				const finished_cb = ()=>{
					++loop_counter
					if (loop_counter > max_loops)
					{
						console.log(context + ':pipe_display_command:UI is not loaded, delay of 50ms, max loops is reached=', max_loops)
						return
					}

					if (! this.is_loaded())
					{
						console.log(context + ':pipe_display_command:UI is not loaded, delay of 50ms')

						this._display_command_timer = setTimeout(finished_cb, 50)
						return
					}

					console.log(context + ':pipe_display_command:UI is loaded')
					this._display_command_timer = undefined
					let cmd = this._display_command_waiting.shift()
					while(cmd)
					{
						console.log(context + ':pipe_display_command:shift cmd=', cmd.get_name(), cmd)
						this._display_commands_pipe.push(cmd)
						cmd = this._display_command_waiting.shift()
					}
				}

				this._display_command_timer = setTimeout(finished_cb, 50)
			}
			return
		}

		console.log(context + ':pipe_display_command:UI is already loaded', arg_display_command.get_name(), arg_display_command)
		this._display_commands_pipe.push(arg_display_command)
	}



	/**
	 * Get current application layout.
	 * 
	 * @returns {Layout}
	 */
	get_current_layout()
	{
		return this._ui_layout
	}

	
	
	/**
	 * Get a resolver function to find UI component description.
	 * 
	 * @returns {function} - (string)=>Immutable.Map|undefined.
	 */
	get_resource_description_resolver()
	{
		return (arg_name, arg_collection=undefined)=>{
			if ( T.isString(arg_collection) )
			{
				const result = this._ui_factory.find_component_desc(this.store.get_state(), arg_name, [arg_collection])
				// console.log('get_resource_description_resolver:name=%s,collection=%s', arg_name, arg_collection, result)
				return result.toJS()
			}

			const collections = ['views', 'menubars']
			let index = 0
			while(collections.length > index)
			{
				const result = this._ui_factory.find_component_desc(this.store.get_state(), arg_name, [collections[index]])
				if (result)
				{
					// console.log('get_resource_description_resolver:name=%s,collection=%s', arg_name, collections[index], result)
					return result.toJS()
				}
				index++
			}
			return undefined
		}
	}

	
	
	/**
	 * Get a resolver function to find UI rendering function.
	 * 
	 * @returns {function} - (string)=>function.
	 */
	get_rendering_function_resolver()
	{
		return (arg_type)=>{
			// console.log(context + ':get_rendering_function_resolver():type=' + arg_type, this._ordered_used_plugins_name, this._rendering_plugins_map)

			let not_found = true
			let index = 0
			let plugin = undefined
			let f = undefined
			while(this._ordered_used_plugins_name.length > index && not_found)
			{
				const plugin_name = this._ordered_used_plugins_name[index]
				if (plugin_name in this._rendering_plugins_map)
				{
					plugin = this._rendering_plugins_map[plugin_name]
					
					f = plugin.find_rendering_function(arg_type)
					if ( T.isFunction(f) )
					{
						not_found = false
					}

					// console.log(context + ':get_rendering_function_resolver():type=' + arg_type + ' iterate on plugin=%s, found=%b, function=', plugin.get_name(), not_found, f)
				}
				index++
			}

			if (! f)
			{
				console.warn(context + ':get_rendering_function_resolver:resolver:type not found for [%s] in plugins [%s]', arg_type, this._ordered_used_plugins_name.toString())
			}

			return f
		}
	}

	
	
	/**
	 * Get a resolver function to find UI component class.
	 * 
	 * @returns {Class} - (string)=>Class.
	 */
	get_rendering_class_resolver()
	{
		return (arg_type)=>{
			// console.log(context + ':get_rendering_class_resolver():type=' + arg_type, this._ordered_used_plugins_name, this._rendering_plugins_map)
			
			let not_found = true
			let index = 0
			let plugin = undefined
			let c = undefined
			while(this._ordered_used_plugins_name.length > index && not_found)
			{
				const plugin_name = this._ordered_used_plugins_name[index]
				if (plugin_name in this._rendering_plugins_map)
				{
					plugin = this._rendering_plugins_map[plugin_name]

					c = plugin.get_feature_class(arg_type)
					if ( T.isFunction(c) )
					{
						not_found = false
					}
					
					// console.log(context + ':get_rendering_class_resolver():type=' + arg_type + ' iterate on plugin=%s, found=%b, class=', plugin.get_name(), not_found, c)
				}
				index++
			}
			return c
		}
	}


	/**
	 * Load plugins.
	 * 
	 * @returns {nothing}
	 */
	load()
	{
		// this._ui_layout = new LayoutSimple(this._runtime, {name:'main layout', type:'simple'})

		// LOAD PLUGINS CLASSES
		// console.log('LOAD PLUGINS CLASSES:name=DefaultRenderingPlugin')
		this.register_rendering_plugin(DefaultRenderingPlugin)
		this._rendering_plugins_counter += 1

		const plugins_urls = this.store.get_state().get('plugins_urls', fromJS({})).toJS()
		const ordered_plugins = this.store.get_state().get('used_plugins', fromJS([])).toJS()
		this._ordered_used_plugins_name = ordered_plugins

		_.forEach(ordered_plugins,
			(plugin_name)=>{
				if (plugin_name in plugins_urls)
				{
					this._rendering_plugins_counter += 1

					const url = plugins_urls[plugin_name]
					const url_src = this._ui_rendering.get_asset_url('plugins/' + url, 'script', this._runtime.get_session_credentials())
					
					// console.log('LOAD PLUGINS CLASSES:name=%s,url=%s', plugin_name, url_src)

					this._ui_rendering.create_dom_url_element(document.body, 'script', 'js-' + plugin_name, url_src, 'text/javascript')
				}
			}
		)
	}
	
	
	
	/**
	 * Get a UI component by its name.
	 * 
	 * @param {string} arg_name - component name.
	 * 
	 * @returns {Component}
	 */
	get(arg_name)
	{
		return this._ui_factory.get(arg_name)
	}
	
	
	
	/**
	 * Test a UI component by its name.
	 * 
	 * @param {string} arg_name - component name.
	 * 
	 * @returns {boolean}
	 */
	has(arg_name)
	{
		return this._ui_factory.has(arg_name)
	}
	
	
	
	/**
	 * Create a UI component.
	 * 
	 * @param {string} arg_name - component name.
	 * 
	 * @returns {Promise} - Promise of a Component instance.
	 */
	create(arg_name)
	{
		const component_promise = this._ui_factory.create(arg_name)
		return component_promise
	}
	
	
	
	/**
	 * Create a UI component.
	 * 
	 * @param {string} arg_name - component name.
	 * @param {object} arg_component_desc - component description.
	 * 
	 * @returns {Component} - Component instance.
	 */
	create_local(arg_name, arg_component_desc)
	{
		const component = this._ui_factory.create_local(arg_name, arg_component_desc)
		return component
	}



	/**
	 * Register a browser rendering plugin.
	 * 
	 * @param{RenderingPlugin} arg_plugin - rendering plugin.
	 * 
	 * @returns {nothing}
	 */
	register_rendering_plugin(arg_plugin_class)
	{
		if ( ! arg_plugin_class)
		{
			console.warn(context + ':register_rendering_browser:bad plugin class', arg_plugin_class)
			return
		}

		const manager= {
			is_plugins_manager:true
		}
		const plugin = new arg_plugin_class(this._runtime, manager)
		plugin.find_rendering_function = (type)=>{
			const f =  arg_plugin_class.find_rendering_function(type)
			return f
		}

		console.log(context + ':register_rendering_browser:plugin=' + plugin.get_name())
		
		this._rendering_plugins.push(plugin)
		this._rendering_plugins_map[plugin.get_name()] = plugin
	}



	/**
	 * Request server about middleware rendering.
	 * 
	 * @param {string} arg_middleware - middleware name.
	 * @param {string} arg_svc_route  - requested route.
	 * 
	 * @returns {Promise} - Promise of a RenderingResult instance.
	 */
	request_middleware(arg_middleware, arg_svc_route)
	{
		this.enter_group('request_middleware:middleware=' + arg_middleware + ' route=' + arg_svc_route)

		const promise = this._runtime.register_service(arg_middleware)
		.then(
			(service)=>{
				// console.log(context + ':render_with_middleware:get rendering for ' + arg_cmd.url)
				return service.get( { route:arg_svc_route } )
			},
			
			(reason)=>{
				console.error(context + ':render_with_middleware:error 0', reason)
			}
		)
		.then(
			(stream)=>{
				// console.log(context + ':render_with_middleware:get listen stream for ' + arg_cmd.url)
				return new Promise(
					function(resolve, reject)
					{
						stream.subscribe(
							(response)=>{
								resolve(response.datas)
							}
						)
						stream.on_error(
							(reason)=>{
								reject(reason)
							}
						)
					}
				)
			},

			(reason)=>{
				console.error(context + ':render_with_middleware:error 1 for ' + arg_svc_route, reason)
			}
		)
		.catch(
			(reason)=>{
				console.error(context + ':render_with_middleware:error for ' + arg_svc_route, reason)
			}
		)

		this.leave_group('request_middleware:async')
		return promise
	}
}