소스 검색

+ BiAuditGraph::raportView

Piotr Labudda 7 년 전
부모
커밋
e5bccf81db
2개의 변경된 파일483개의 추가작업 그리고 17개의 파일을 삭제
  1. 215 17
      SE/se-lib/Route/UrlAction/BiAuditGraph.php
  2. 268 0
      SE/se-lib/Route/UrlAction/BiAuditGraph.php.network-graph.js

+ 215 - 17
SE/se-lib/Route/UrlAction/BiAuditGraph.php

@@ -1,6 +1,7 @@
 <?php
 
 Lib::loadClass('Api_WfsNs');
+Lib::loadClass('Response');
 
 class Route_UrlAction_BiAuditGraph extends RouteBase {
 
@@ -85,6 +86,191 @@ class Route_UrlAction_BiAuditGraph extends RouteBase {
 		UI::alert('danger', $e->getMessage());
 		UI::dol();
 	}
+
+	function raportOutputStatsAction() {
+		session_write_close();
+		$args = [];
+		$args['raportId'] = V::get('raportId', 0, $_GET, 'int');
+		$args['outputFormat'] = V::get('outputFormat', '', $_GET, 'word');
+		try {
+			if (!$args['raportId']) throw new Exception("Wrong param raportId");
+		} catch (\Exception $e) {
+			switch ($args['outputFormat']) {
+				case 'json': return $this->sendJsonErrorMsg($e->getMessage());
+				case 'xml': return $this->sendXmlErrorMsg($e->getMessage());
+				case 'html':
+				default: {
+					return $this->sendHtmlErrorMsg($e->getMessage());
+				}
+			}
+		}
+
+		$stats = $this->getOutputStats($args['raportId']);
+
+		switch ($args['outputFormat']) {
+			case 'json': return $this->sendJsonStats($args['raportId'], $stats);
+			case 'xml': return $this->sendXmlStats($args['raportId'], $stats);
+			case 'html':
+			default: {
+				return $this->sendHtmlStats($args['raportId'], $stats);
+			}
+		}
+	}
+	function getOutputStats($raportId) {
+		$matrix = $this->getRaportOutputMatrix($raportId);
+		$listObjectsNs = $this->_getListPointChildNs();
+		$listPointChildAlias = $this->_getListPointChildAlias();
+
+		$listPathLength = array_reduce($matrix, function ($ret, $item) {
+			$ret[ $item['pk_Path'] ] = 1 + V::get($item['pk_Path'], 0, $ret);
+			return $ret;
+		}, []);
+		$listPathPk = array_keys($listPathLength);
+		$pointOccurs = array_reduce($matrix, function ($ret, $item) use ($listPointChildAlias) {
+			foreach ($listPointChildAlias as $alias) {
+				$pkField = "pk_{$alias}";
+				if ($item[$pkField]) {
+					$featureId = "{$alias}.{$item[$pkField]}";
+					$ret[$featureId] = 1 + V::getValue($ret[$featureId], 0);
+				}
+			}
+			return $ret;
+		}, []);
+		asort($pointOccurs);
+		DBG::log($pointOccurs, 'array', '$pointOccurs');
+
+		$getPointFeatureIdFromPath = function ($item) {
+			$listPointChildAlias = $this->_getListPointChildAlias();
+			return array_reduce($listPointChildAlias, function ($ret, $objAlias) use ($item) {
+				$pk = V::get("pk_{$objAlias}", 0, $item);
+				if ($pk) $ret = "{$objAlias}.{$pk}";
+				return $ret;
+			}, null);
+		};
+		$prevItem = [];
+		$edges = array_reduce($matrix, function ($ret, $item) use (&$prevItem, $getPointFeatureIdFromPath) {
+			if (!empty($prevItem) && $prevItem['pk_Path'] === $item['pk_Path']) {
+				$ret[] = [ 'source' => $getPointFeatureIdFromPath($prevItem), 'target' => $getPointFeatureIdFromPath($item), 'pathId' => $item['pk_Path'] ];
+				$prevItem = array_merge([], $item);
+				return $ret;
+			} else {
+				$prevItem = array_merge([], $item);
+				return $ret;
+			}
+		}, []);
+
+		DBG::log("edges - done");
+		$prevFeaturesOnPath = [];
+		$prevPathId = 0;
+		$allToAllEdges = array_reduce($matrix, function ($ret, $item) use (&$prevFeaturesOnPath, &$prevPathId, $getPointFeatureIdFromPath) {
+			$curFeatureId = $getPointFeatureIdFromPath($item);
+			if (!empty($prevFeaturesOnPath) && $prevPathId === $item['pk_Path']) {
+				foreach ($prevFeaturesOnPath as $idx => $prevFeatureId) {
+					$ret[] = [ 'source' => $prevFeatureId, 'target' => $curFeatureId, 'pathId' => $item['pk_Path'], 'length' => (1 + $idx) ];
+				}
+				array_unshift($prevFeaturesOnPath, $curFeatureId);
+				$prevPathId = $item['pk_Path'];
+				return $ret;
+			} else {
+				$prevFeaturesOnPath = [ $curFeatureId ];
+				$prevPathId = $item['pk_Path'];
+				return $ret;
+			}
+		}, []);
+		DBG::log("edges_all_to_all - done");
+
+		$stats = [];
+		// $stats['path_length'] = $listPathLength;
+		$stats['path_total'] = count($listPathLength);
+		$stats['point_total'] = count($matrix);
+		$stats['point_occurs'] = $pointOccurs;
+		$stats['edges_all_to_all'] = $allToAllEdges;
+		$stats['edges'] = $edges;
+		$stats['nodes'] = array_map([ $this, 'getFeature' ], array_keys($pointOccurs));
+		return $stats;
+	}
+
+	function getFeature($featureId) {
+		list($objectName, $primaryKey) = explode(".", $featureId);
+		if (!$objectName) throw new Exception("Wrong featureId '{$featureId}' - missing objectName");
+		if (!$primaryKey) throw new Exception("Wrong featureId '{$featureId}' - missing primaryKey");
+		$tableName = $objectName;
+		$item = DB::getPDO()->fetchFirst(" select * from `{$tableName}` where id = :id ", [ ':id' => $primaryKey ]);
+		$getLabel = [ $this, '_getFeatureLabel' ];
+		return array_merge($item, [
+			'@label' => $getLabel($objectName, $item), // TODO: label depend on table / later format from xsd
+			'@object' => $objectName,
+			'@primaryKey' => $primaryKey,
+		]);
+	}
+	function _getFeatureLabel($objectName, $item) {
+		$primaryKey = $item['@primaryKey'];
+		switch ($objectName) {
+			case 'BI_audit_ENERGA_RUM_KONTRAHENCI': return V::get('Pelna_nazwa_kontrahenta', $primaryKey, $item);
+			case 'BI_audit_MSIG': return V::get('nazwa', $primaryKey, $item);
+			case 'BI_audit_MSIG_person': return V::getValue(
+				implode(" ", array_filter([
+					V::get('imiona', '', $item),
+					V::get('nazwisko', '', $item)
+				], 'strlen'))
+				, "{$objectName} Nr {$primaryKey}"
+			);
+			case 'BI_audit_CEIDG': return V::get('firma', $primaryKey, $item);
+			case 'BI_audit_KRS': return V::get('nazwa', $primaryKey, $item);
+			case 'BI_audit_KRS_person': return V::getValue(
+				implode(" ", array_filter([
+					V::get('imiona', '', $item),
+					V::get('nazwisko', '', $item)
+				], 'strlen'))
+				, "{$objectName} Nr {$primaryKey}"
+			);
+			case 'BI_audit_MSIG_company': return V::get('nazwa', $primaryKey, $item);
+			case 'BI_audit_KRS_company': return V::get('nazwa', $primaryKey, $item);
+			case 'TERYT_adresy': return "TERYT({$item['TERYT_SYM']}/{$item['TERYT_SYM_UL']})";
+			case 'BI_audit_MSIG_address': return V::getValue(
+				implode(", ", array_filter([
+					implode(" ", array_filter([
+						V::get('A_kod', '', $item),
+						V::get('S_miejscowosc', '', $item)
+					], 'strlen')),
+					implode(" ", array_filter([
+						V::get('A_nrDomu', '', $item),
+						V::get('A_nrDomu', '', $item),
+						V::get('A_nrLokalu', '', $item)
+					], 'strlen'))
+				], 'strlen'))
+				, "{$objectName} Nr {$primaryKey}"
+			);
+
+			default: return "TODO: {$objectName} Nr {$primaryKey}";
+		}
+	}
+	function sendJsonStats($raportId, $stats) {
+		Response::sendJson([
+			'type' => "success",
+			'msg' => "OK",
+			'body' => $stats
+		]);
+		die();
+	}
+	function sendXmlStats($raportId, $stats) {
+		print_r($stats);
+	}
+	function sendHtmlStats($raportId, $stats) {
+		UI::gora();
+		DBG::nicePrint($stats, "\$stats");
+		$dbgNode = function ($item) {
+			return [
+				'object' => $item['@object'],
+				'primaryKey' => $item['@primaryKey'],
+				'label' => $item['@label'],
+			];
+		};
+		UI::table([ 'rows' => array_map($dbgNode, $stats['nodes']) ]);
+		UI::dol();
+	}
+
+
 	function raportView($args) {
 		$raportId = V::get('raportId', 0, $args, 'int');
 		$featureId = V::get('featureId', '', $args, '');
@@ -92,12 +278,16 @@ class Route_UrlAction_BiAuditGraph extends RouteBase {
 		echo $h('div', [ 'id' => "bi_audit_raport-network_graph" ], "TODO: loading data...");
 		echo $h('script', ['src'=>"static/vendor.js?v=a76e2988", 'type'=>"text/javascript"]);
 		echo $h('script', [ 'src' => "static/visjs/vis.min.js" ]);
+		$stats = $this->getOutputStats($raportId);
+		echo UI::h('link', [ 'href' => 'static/react-bootstrap-typeahead.min.css', 'type' => 'text/css', 'rel' => 'stylesheet' ]);
 		UI::inlineJS(__FILE__ . '.network-graph.js', [
 			'HTML_ID_REF_GRAPH' => 'bi_audit_raport-network_graph',
 			// 'TYPENAME' => Api_WfsNs::typeName($namespace),
 			// 'PRIMARY_KEY' => $primaryKey,
 			// 'WFS_URL' => Router::getRoute('WfsBiAudit')->getLink(),
-			'API_URL' => $this->getLink('getRaportNetworkGraphDataAjax'),
+			'RAPORT_ID' => $raportId,
+			'API_URL' => $this->getLink('raportOutputStats', [ 'outputFormat' => "json", 'raportId' => $raportId ]),
+			'STATS' => $stats,
 			'DBG' => (V::get('DBG', 0, $_GET, 'int') > 0),
 		]);
 
@@ -106,16 +296,7 @@ class Route_UrlAction_BiAuditGraph extends RouteBase {
 	function getRaportNetworkGraphDataAjaxAction() {
 
 	}
-	function getRaportOutputMatrix($raportId) {
-		// Main Raport Acl NS: default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA
-		// - Found Paths in: default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row
-		// - Path Points in: default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object
-		$mainNs = [
-			'Raport' => 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA',
-			'Path' => 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row',
-			'Point' => 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object',
-		];
-
+	function _getListPointChildNs() {
 		$listObjectsNs = [];
 		$listObjectsNs[] = 'default_db__x3A__BI_audit_CEIDG:BI_audit_CEIDG';
 		$listObjectsNs[] = 'default_db__x3A__BI_audit_CEIDG_pelnomocnicy:BI_audit_CEIDG_pelnomocnicy';
@@ -133,6 +314,25 @@ class Route_UrlAction_BiAuditGraph extends RouteBase {
 		$listObjectsNs[] = 'default_db__x3A__BI_audit_MSIG_company:BI_audit_MSIG_company';
 		$listObjectsNs[] = 'default_db__x3A__BI_audit_MSIG_person:BI_audit_MSIG_person';
 		$listObjectsNs[] = 'default_db__x3A__TERYT_adresy:TERYT_adresy';
+		return $listObjectsNs;
+	}
+	function _getListPointChildAlias() {
+		return array_map(function ($objNs) {
+			// return str_replace([ 'BI_audit_' ], '', substr($objNs, strrpos($objNs, ':') + 1));
+			return substr($objNs, strrpos($objNs, ':') + 1);
+		}, $this->_getListPointChildNs());
+	}
+	function getRaportOutputMatrix($raportId) {
+		// Main Raport Acl NS: default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA
+		// - Found Paths in: default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row
+		// - Path Points in: default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object
+		$mainNs = [
+			'Raport' => 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA',
+			'Path' => 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row',
+			'Point' => 'default_db__x3A__BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object:BI_audit_ENERGA_RUM_KONTRAHENCI_POWIAZANIA_row_object',
+		];
+
+		$listObjectsNs = $this->_getListPointChildNs();
 
 		$outRefTables = array_merge(
 			[ [ 'label' => 'Raport_to_Path', 'table' => ACL::getRefTable(Api_WfsNs::toNamespace($mainNs['Raport']), $mainNs['Path']) ] ],
@@ -153,16 +353,14 @@ class Route_UrlAction_BiAuditGraph extends RouteBase {
 		$refTables = array_combine( array_map(V::makePick('label'), $outRefTables), array_map(V::makePick('table'), $outRefTables) );
 		DBG::log($refTables, 'array', '$refTables');
 
-		$fromPointRefTableAliases = array_map(function ($objNs) {
-			return str_replace([ 'BI_audit_' ], '', substr($objNs, strrpos($objNs, ':') + 1));
-		}, array_merge($listObjectsNs));
-		DBG::log($fromPointRefTableAliases, 'array', '$fromPointRefTableAliases');
+		$listPointChildAlias = $this->_getListPointChildAlias();
+		DBG::log($listPointChildAlias, 'array', '$listPointChildAlias');
 		$sqlSelect = implode("\n , ", array_merge(
 			array_map(function ($tbl, $key) {
 				return "t_{$key}.ID as pk_{$key}";
 			}, $mainTables, array_keys($mainTables)),
-			array_map(function ($tbl, $key) use ($fromPointRefTableAliases) {
-				return "t_{$key}.ID as pk_{$fromPointRefTableAliases[$key]}";
+			array_map(function ($tbl, $key) use ($listPointChildAlias) {
+				return "t_{$key}.ID as pk_{$listPointChildAlias[$key]}";
 			}, $refFromPointTables, array_keys($refFromPointTables))
 		));
 		$sqlTablesFrom = [];

+ 268 - 0
SE/se-lib/Route/UrlAction/BiAuditGraph.php.network-graph.js

@@ -0,0 +1,268 @@
+var DBG = DBG || false;
+var DBG1 = true;
+
+if ('undefined' === typeof HTML_ID_REF_GRAPH) throw "Missing HTML_ID_REF_GRAPH";
+if (!RAPORT_ID) throw "Missing RAPORT_ID";
+if (!API_URL) throw "Missing API_URL";
+if (!global.p5VendorJs) throw "Missing p5VendorJs";
+if (!global.vis) throw "Missing vis";
+if (!global.p5VendorJs.Typeahead) throw "Missing Typeahead";
+
+var createReactClass = global.p5VendorJs.createReactClass;
+var h = global.p5VendorJs.React.createElement;
+var React = global.p5VendorJs.React;
+var ReactDOM = global.p5VendorJs.ReactDOM;
+var Redux = global.p5VendorJs.Redux;
+var ReduxThunk = global.p5VendorJs.ReduxThunk;
+var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js
+var Typeahead = global.p5VendorJs.Typeahead;
+
+var mapStatsNodeToGraphNode = function (node) {
+	return {
+		id: node['@object'] + '.' + node['@primaryKey'],
+		label: node['@label'],
+		value: 1,
+		level: 0,
+	}
+}
+
+{ // TODO: mv to another file
+	function getinitialState() {
+		var nodes = (STATS && STATS.nodes) ? STATS.nodes : [];
+		var edges = (STATS && STATS.edges) ? STATS.edges : [];
+		DBG1 && console.log('DBG: STATS', STATS);
+		return {
+			nodes: nodes.map(mapStatsNodeToGraphNode),
+			edges: edges,
+			isLoading: false,
+			sentRequestId: 0,
+			receivedRequestId: 0,
+		}
+	}
+	function networkGraphStore(state, action) {
+		var prevState = state || getinitialState();
+		DBG1 && console.log('DBG: store', { prevState, action, actionType: action.type });
+		switch (action.type) {
+			case 'SET_SENT_REQUEST_ID': return Object.assign(prevState, {
+				sentRequestId: action.sentRequestId,
+				isLoading: true
+			});
+			case 'SET_RESPONSE': return Object.assign(prevState, {
+				receivedRequestId: action.requestId,
+				nodes: action.body.nodes.map(mapStatsNodeToGraphNode),
+				edges: action.body.edges,
+				isLoading: (prevState.sentRequestId > action.requestId) ? true : false,
+			});
+			default: return prevState;
+		}
+	}
+
+	function createNetworkGraphStoreActions() {
+		var setSentRequestId = function (sentRequestId) {
+			return { type: 'SET_SENT_REQUEST_ID', sentRequestId: sentRequestId };
+		}
+		var setResponse = function (response) {
+			return Object.assign(response, { type: 'SET_RESPONSE' });
+		}
+
+		var fetchData = function (raportId, params) {
+			return function (dispatch, getState) {
+				var state = getState();
+				var this__requestId = state.sentRequestId + 1;
+				dispatch(setSentRequestId(this__requestId));
+
+				var reqPromise = window.fetch(API_URL, {
+					method: 'GET',
+					credentials: 'same-origin',
+				}).then(function (response) {
+					return response.json();
+				});
+				return reqPromise.then(function (response) {
+					var items = response;
+					var state = getState();
+
+					if (state.receivedRequestId > this__requestId) {
+						DBG1 && console.log('DBG: skiped response', { 'state.receivedRequestId': state.receivedRequestId, this__requestId });
+						return;
+					}
+
+					dispatch(
+						setResponse({
+							requestId: this__requestId,
+							msg: "Pobrano dane",
+							body: response.body
+						})
+					);
+				}).catch(function (e) {
+					p5UI__notifyAjaxCallback({type: 'error', msg: 'Wystąpił błąd #GS1: ' + e});
+					// dispatch( setErrorMsg(e) ); // TODO: show error with msg and refresh button
+				});
+			}
+		}
+
+		return {
+			fetchData: fetchData,
+		}
+	}
+}
+
+var defaultOptions = {
+	nodes: {
+		shape: 'dot',
+		scaling: {
+			min: 20, max: 30,
+			label: {
+				min: 8, max: 16, drawThreshold: 9, maxVisible: 20
+			}
+		},
+		font: { color: "#666", size: 10, face: 'Helvetica Neue, Helvetica, Arial' }
+		// font: "8px Helvetica Neue, Helvetica, Arial"
+	},
+	interaction: {
+		hover: true,
+		hoverConnectedEdges: false,
+		selectConnectedEdges: true,
+	},
+};
+var TMP_COUNTER = 1;
+
+var p5UI__NetworkGraph = createReactClass({
+	_network: null,
+	_visOutputRef: React.createRef(),
+	_nodes: new vis.DataSet(),
+	_edges: new vis.DataSet(),
+	getStateFromStore: function () {
+		var state = this.props.store.getState();
+		return {
+			isLoading: state.isLoading,
+			receivedRequestId: state.receivedRequestId, // to force render after udpate nodes, edges
+		}
+	},
+	getInitialState: function () {
+		return {
+			initialized: false,
+			isLoading: false,
+			receivedRequestId: null,
+		};
+	},
+	setOutputRef: function (elem) {
+		this._visOutputRef = elem;
+	},
+	componentDidMount: function () {
+		var data = { nodes: this._nodes, edges: this._edges };
+		this._network = new vis.Network(this._visOutputRef, data, defaultOptions);
+	  // bindNetwork();
+	  this.setState({ initialized: true });
+		this._unsubscribe = this.props.store.subscribe(this._storeUpdated)
+	},
+	_storeUpdated: function () {
+		this.setState( this.getStateFromStore() )
+	},
+
+	testOnClick1: function () {
+		TMP_COUNTER++;
+		this._nodes.add({ id: TMP_COUNTER, label: "Node " + TMP_COUNTER, value: TMP_COUNTER, level: 0 });
+	},
+	testOnClick2: function () {
+		TMP_COUNTER++;
+		var ids = this._nodes.getIds();
+		var totalNodes = ids.length
+		if (totalNodes < 3) return;
+		var fromIdx = Math.floor(Math.random() * totalNodes);
+		var toIdx = Math.floor(Math.random() * totalNodes);
+		this._edges.add({ from: ids[fromIdx], to: ids[toIdx] });
+	},
+	testOnClick3: function () {
+		this.props.store.dispatch( this.props.storeActions.fetchData( this.props.raportId ) );
+	},
+
+	shouldComponentUpdate: function (nextProps, nextState) {
+		if (this.state.receivedRequestId !== nextState.receivedRequestId) { // add missing nodes
+			var state = this.props.store.getState();
+			DBG1 && console.warn("TODO: update nodes, edges", { state });
+			// this._edges.add( edge_or_edges_array )
+			if (state.nodes && state.nodes.length) {
+				var __nodes = this._nodes;
+				state.nodes.forEach(function (node) {
+					__nodes.add(node);
+				})
+			}
+			// edge: { from: page, to: subpageID, color:getEdgeColor(level), level: level, selectionWidth:2, hoverWidth:0 }
+			if (state.edges && state.edges.length) {
+				var __edges = this._edges;
+				// state.edges.forEach(function (edge) {
+				state.edges.slice(0, 30).forEach(function (edge) {
+					__edges.add({
+						from: edge.source,
+						to: edge.target,
+					});
+				})
+			}
+		}
+
+		if (this.props.selected.length != nextProps.selected.length) {
+			var edges = []; // TODO: by nodes
+			this._network.setData({ nodes: nextProps.selected, edges: edges });
+		}
+		return false;
+	},
+	render: function () {
+		DBG1 && console.log('DBG:render');
+		return h('div', {}, [
+			h('div', {
+				ref: this.setOutputRef,
+				style: {
+					'min-height': 600,
+					'height': 600,
+				},
+			}),
+			h('div', {}, [
+				h('button', { onClick: this.testOnClick1 }, [ "TEST 1 ", h('small', [], "(+ node)") ]),
+				h('button', { onClick: this.testOnClick2 }, [ "TEST 2 ", h('small', [], "(+ edge)") ]),
+				h('button', { onClick: this.testOnClick3 }, [ "TEST 3 ", h('small', [], "(+ fetch)") ]),
+			]),
+		]);
+	}
+});
+
+var p5UI__PanelNetworkGraph = createReactClass({
+	getInitialState: function () {
+		return {
+			selected: [],
+		}
+	},
+	handleSelectFeatures: function (selected) {
+		DBG1 && console.log('DBG:typeahead selected:', { selected })
+		this.setState({ selected: selected });
+	},
+	render: function () {
+		var state = this.props.store.getState()
+		DBG1 && console.log('DBG: state', state);
+		var options = state.nodes || [];
+		return h(React.Fragment, {}, [
+			h(Typeahead, {
+				labelKey: "label",
+				multiple: true,
+				options: options,
+				placeholder: "Wybierz",
+				bsSize: 'large',
+				onChange: this.handleSelectFeatures,
+			}),
+			h(p5UI__NetworkGraph, {
+				raportId: this.props.raportId,
+				store: this.props.store,
+				storeActions: this.props.storeActions,
+				selected: this.state.selected,
+			})
+		])
+	}
+})
+
+ReactDOM.render(
+	h(p5UI__PanelNetworkGraph, {
+		raportId: RAPORT_ID,
+		store: createStoreWithThunkMiddleware(networkGraphStore),
+		storeActions: createNetworkGraphStoreActions(),
+	}),
+	document.getElementById(HTML_ID_REF_GRAPH)
+);