source: SRUAggregator/trunk/src/main/resources/assets/js/main.jsx @ 5900

Last change on this file since 5900 was 5900, checked in by emanuel.dima@uni-tuebingen.de, 9 years ago
  1. alpha 9: corpus view UI improvements, bug fixes
File size: 18.5 KB
Line 
1/** @jsx React.DOM */
2(function() {
3"use strict";
4
5var PT = React.PropTypes;
6
7var SearchBox = window.MyAggregator.SearchBox;
8var CorpusSelection = window.MyAggregator.CorpusSelection;
9var HitNumber = window.MyAggregator.HitNumber;
10var Results = window.MyAggregator.Results;
11var CorpusView = window.MyAggregator.CorpusView;
12var Modal = window.MyReact.Modal;
13var ErrorPane = window.MyReact.ErrorPane;
14
15var multipleLanguageCode = "mul"; // see ISO-693-3
16
17var layers = [
18        {
19                id: "sampa",
20                name: "Phonetics Resources",
21                searchPlaceholder: "stA:z",
22                searchLabel: "SAMPA query",
23                searchLabelBkColor: "#eef",
24        },
25        {
26                id: "text",
27                name: "Text Resources",
28                searchPlaceholder: "Elephant",
29                searchLabel: "Search text",
30                searchLabelBkColor: "#fed",
31        },
32];
33var layerMap = {
34        sampa: layers[0],
35        text: layers[1],
36};
37
38function Corpora(corpora, updateFn) {
39        var that = this;
40        this.corpora = corpora;
41        this.update = function() {
42                updateFn(that);
43        };
44       
45        var sortFn = function(x, y) {
46                var r = x.institution.name.localeCompare(y.institution.name);
47                if (r !== 0) {
48                        return r;
49                }
50                var t1 = x.title ? x.title : x.displayName;
51                var t2 = y.title ? y.title : y.displayName;
52                return t1.toLowerCase().localeCompare(t2.toLowerCase());
53        };
54
55        this.recurse(function(corpus) { corpus.subCorpora.sort(sortFn); });
56        this.corpora.sort(sortFn);
57
58        this.recurse(function(corpus, index) {
59                corpus.visible = true; // visible in the corpus view
60                corpus.selected = true; // selected in the corpus view
61                corpus.expanded = false; // not expanded in the corpus view
62                corpus.priority = 1; // priority in corpus view
63                corpus.index = index;
64        });
65}
66
67Corpora.prototype.recurseCorpus = function(corpus, fn) {
68        if (false === fn(corpus)) {             
69                // no recursion
70        } else {
71                this.recurseCorpora(corpus.subCorpora, fn);
72        }
73};
74
75Corpora.prototype.recurseCorpora = function(corpora, fn) {
76        var recfn = function(corpus, index){
77                if (false === fn(corpus)) {
78                        // no recursion
79                } else {
80                        corpus.subCorpora.forEach(recfn);
81                }
82        };
83        corpora.forEach(recfn);
84};
85
86Corpora.prototype.recurse = function(fn) {
87        this.recurseCorpora(this.corpora, fn);
88};
89
90Corpora.prototype.getLanguageCodes = function() {
91        var languages = {};
92        this.recurse(function(corpus) {
93                corpus.languages.forEach(function(lang) {
94                        languages[lang] = true;
95                });
96                return true;
97        });
98        return languages;
99};
100
101Corpora.prototype.isCorpusVisible = function(corpus, layerId, languageCode) {
102        if (layerId !== "text") {
103                return false;
104        }
105        // yes for any language
106        if (languageCode === multipleLanguageCode) {
107                return true;
108        }
109        // yes if the corpus is in only that language
110        if (corpus.languages && corpus.languages.length === 1 && corpus.languages[0] === languageCode) {
111                return true;
112        }       
113
114        // ? yes if the corpus also contains that language
115        if (corpus.languages && corpus.languages.indexOf(languageCode) >=0) {
116                return true;
117        }
118
119        // ? yes if the corpus has no language
120        // if (!corpus.languages || corpus.languages.length === 0) {
121        //      return true;
122        // }
123        return false;
124};
125
126Corpora.prototype.setVisibility = function(layerId, languageCode) {
127        // top level
128        this.corpora.forEach(function(corpus) {
129                corpus.visible = this.isCorpusVisible(corpus, layerId, languageCode);
130                this.recurseCorpora(corpus.subCorpora, function(c) { c.visible = corpus.visible; });
131        }.bind(this));
132};
133
134Corpora.prototype.getSelectedIds = function() {
135        var ids = [];
136        this.recurse(function(corpus) {
137                if (corpus.visible && corpus.selected) {
138                        ids.push(corpus.id);
139                        return false; // top-most collection in tree, don't delve deeper
140                }
141                return true;
142        });
143
144        // console.log("ids: ", ids.length, {ids:ids});
145        return ids;
146};
147
148Corpora.prototype.getSelectedMessage = function() {
149        var selected = this.getSelectedIds().length;
150        if (this.corpora.length === selected) {
151                return "All available collections";
152        } else if (selected === 1) {
153                return "1 selected collection";
154        }
155        return selected+" selected collections";
156};
157
158
159var Main = React.createClass({
160        getInitialState: function () {
161                return {
162                        navbarCollapse: false,
163                        navbarPageFn: this.renderAggregator,
164                        errorMessages: [],
165
166                        corpora: new Corpora([], this.updateCorpora),
167                        languageMap: {},
168                };
169        },
170
171        componentDidMount: function() {
172                this.refreshCorpora();
173                this.refreshLanguages();
174        },
175
176        error: function(errObj) {
177                var err = "";
178                if (typeof errObj === 'string' || errObj instanceof String) {
179                        err = errObj;
180                } else if (typeof errObj === 'object' && errObj.statusText) {
181                        console.log("ERROR: jqXHR = ", errObj);
182                        err = errObj.statusText;
183                } else {
184                        return;
185                }
186
187                var that = this;
188                var errs = this.state.errorMessages.slice();
189                errs.push(err);
190                this.setState({errorMessages: errs});
191
192                setTimeout(function() {
193                        var errs = that.state.errorMessages.slice();
194                        errs.shift();
195                        that.setState({errorMessages: errs});
196                }, 10000);
197        },
198       
199        ajax: function(ajaxObject) {
200                var that = this;
201                if (!ajaxObject.error) {
202                        ajaxObject.error = function(jqXHR, textStatus, error) {
203                                if (jqXHR.readyState === 0) {
204                                        that.error("Network error, please check your internet connection");
205                                } else if (jqXHR.responseText) {
206                                        that.error(jqXHR.responseText + " ("+error+")");
207                                } else  {
208                                        that.error(error + " ("+textStatus+")");
209                                }
210                                console.log("ajax error, jqXHR: ", jqXHR);
211                        };
212                }
213                jQuery.ajax(ajaxObject);
214        },
215
216        refreshCorpora: function() {
217                this.ajax({
218                        url: 'rest/corpora',
219                        success: function(json, textStatus, jqXHR) {
220                                this.setState({corpora : new Corpora(json, this.updateCorpora)});
221                        }.bind(this),
222                });
223        },
224
225        refreshLanguages: function() {
226                this.ajax({
227                        url: 'rest/languages',
228                        success: function(json, textStatus, jqXHR) {
229                                this.setState({languageMap : json});
230                        }.bind(this),
231                });
232        },
233
234        updateCorpora: function(corpora) {
235                this.setState({corpora:corpora});
236        },
237
238        renderAggregator: function() {
239                return <AggregatorPage ajax={this.ajax} corpora={this.state.corpora} languageMap={this.state.languageMap} />;
240        },
241
242        renderStatistics: function() {
243                return <StatisticsPage ajax={this.ajax} />;
244        },
245
246        renderHelp: function() {
247                return <HelpPage />;
248        },
249
250        toggleCollapse: function() {
251                this.setState({navbarCollapse: !this.state.navbarCollapse});
252        },
253
254        setNavbarPageFn: function(pageFn) {
255                this.setState({navbarPageFn:pageFn});
256        },
257
258        renderCollapsible: function() {
259                var classname = "navbar-collapse collapse " + (this.state.navbarCollapse?"in":"");
260                return (
261                        <div className={classname}>
262                                <ul className="nav navbar-nav">
263                                        <li className={this.state.navbarPageFn === this.renderAggregator ? "active":""}>
264                                                <a className="link" tabIndex="-1"
265                                                        onClick={this.setNavbarPageFn.bind(this, this.renderAggregator)}>Aggregator</a>
266                                        </li>
267                                        <li className={this.state.navbarPageFn === this.renderStatistics ? "active":""}>
268                                                <a className="link" tabIndex="-1"
269                                                        onClick={this.setNavbarPageFn.bind(this, this.renderStatistics)}>Statistics</a>
270                                        </li>
271                                        <li className={this.state.navbarPageFn === this.renderHelp ? "active":""}>
272                                                <a className="link" tabIndex="-1"
273                                                        onClick={this.setNavbarPageFn.bind(this, this.renderHelp)}>Help</a>
274                                        </li>
275                                </ul>
276                                <ul id="CLARIN_header_right" className="nav navbar-nav navbar-right">
277                                        <li className="unauthenticated">
278                                                <a href="login" tabIndex="-1"><span className="glyphicon glyphicon-log-in"></span> LOGIN</a>
279                                        </li>
280                                </ul>
281                        </div>
282                );
283        },
284
285        render: function() {
286                return  (
287                        <div>
288                                <div className="container">
289                                        <div className="beta-tag">
290                                                <span>BETA</span>
291                                        </div>
292                                </div>
293                       
294                                <div className="navbar navbar-default navbar-static-top" role="navigation">
295                                        <div className="container">
296                                                <div className="navbar-header">
297                                                        <button type="button" className="navbar-toggle" onClick={this.toggleCollapse}>
298                                                                <span className="sr-only">Toggle navigation</span>
299                                                                <span className="icon-bar"></span>
300                                                                <span className="icon-bar"></span>
301                                                                <span className="icon-bar"></span>
302                                                        </button>
303                                                        <a className="navbar-brand" href="#" tabIndex="-1"><header>Federated Content Search</header></a>
304                                                </div>
305                                                {this.renderCollapsible()}
306                                        </div>
307                                </div>
308
309                                <ErrorPane errorMessages={this.state.errorMessages} />
310
311                                <div id="push">
312                                        <div className="container">
313                                                {this.state.navbarPageFn()}
314                                        </div>
315                                        <div className="top-gap" />
316                                </div>
317                        </div>
318                );
319        }
320});
321
322var AggregatorPage = React.createClass({
323        propTypes: {
324                ajax: PT.func.isRequired,
325                corpora: PT.object.isRequired,
326                languageMap: PT.object.isRequired,
327        },
328
329        mixins: [React.addons.LinkedStateMixin],
330        timeout: 0,
331        nohits: {
332                requests: [],
333                results: [],
334        },
335        anyLanguage: [multipleLanguageCode, "Any Language"],
336
337        getInitialState: function () {
338                return {
339                        searchLayerId: "text",
340                        language: this.anyLanguage,
341                        numberOfResults: 10,
342
343                        searchId: null,
344                        hits: this.nohits,
345                };
346        },
347
348        search: function(query) {
349                // console.log(query);
350                if (!query) {
351                        this.setState({ hits: this.nohits, searchId: null });
352                        return;                 
353                }
354                this.props.ajax({
355                        url: 'rest/search',
356                        type: "POST",
357                        data: {
358                                layer: this.state.searchLayerId,
359                                language: this.state.language[0],
360                                query: query,
361                                numberOfResults: this.state.numberOfResults,
362                                corporaIds: this.props.corpora.getSelectedIds(),
363                        },
364                        success: function(searchId, textStatus, jqXHR) {
365                                // console.log("search ["+query+"] ok: ", searchId, jqXHR);
366                                this.setState({searchId : searchId});
367                                this.timeout = 250;
368                                setTimeout(this.refreshSearchResults, this.timeout);
369                        }.bind(this),
370                });
371        },
372
373        refreshSearchResults: function() {
374                if (!this.state.searchId) {
375                        return;
376                }
377                this.props.ajax({
378                        url: 'rest/search/'+this.state.searchId,
379                        success: function(json, textStatus, jqXHR) {
380                                if (json.requests.length > 0) {
381                                        if (this.timeout < 10000) {
382                                                this.timeout = 1.5 * this.timeout;
383                                        }
384                                        setTimeout(this.refreshSearchResults, this.timeout);
385                                        // console.log("new search in: " + this.timeout+ "ms");
386                                } else {
387                                        // console.log("search ended");
388                                }
389                                this.setState({hits:json});
390                                // console.log("hits:", json);
391                        }.bind(this),
392                });
393        },
394
395        setLanguage: function(languageObj) {
396                this.props.corpora.setVisibility(this.state.searchLayerId, languageObj[0]);
397                this.setState({language: languageObj});
398                this.props.corpora.update();
399        },
400
401        setLayer: function(layerId) {
402                this.props.corpora.setVisibility(layerId, this.state.language[0]);
403                this.props.corpora.update();
404                this.setState({searchLayerId: layerId});
405        },
406
407        setNumberOfResults: function(e) {
408                var n = e.target.value;
409                if (n < 10) n = 10;
410                if (n > 250) n = 250;
411                this.setState({numberOfResults: n});
412                e.preventDefault();
413                e.stopPropagation();
414        },
415
416        stop: function(e) {
417                e.preventDefault();
418                e.stopPropagation();
419        },
420
421        toggleCorpusSelection: function(e) {
422                $(this.refs.corporaModal.getDOMNode()).modal();
423                e.preventDefault();
424                e.stopPropagation();
425        },
426
427        renderAggregator: function() {
428                var layer = layerMap[this.state.searchLayerId];
429                return  (
430                        <div className="top-gap">
431                                <div className="row">
432                                        <div className="aligncenter" style={{marginLeft:16, marginRight:16}}>
433                                                <div className="input-group">
434                                                        <span className="input-group-addon" style={{backgroundColor:layer.searchLabelBkColor}}>
435                                                                {layer.searchLabel}
436                                                        </span>
437
438                                                        <SearchBox search={this.search} placeholder={layer.searchPlaceholder} />
439                                                        <div className="input-group-btn">
440                                                                <button className="btn btn-default input-lg" type="button" onClick={this.search}>
441                                                                        <i className="glyphicon glyphicon-search"></i>
442                                                                </button>
443                                                        </div>
444                                                </div>
445                                        </div>
446                                </div>
447
448                                <div className="wel" style={{marginTop:20}}>
449                                        <div className="aligncenter" >
450                                                <form className="form-inline" role="form">
451
452                                                        <div className="input-group" style={{marginRight:10}}>
453                                                                <span className="input-group-addon nobkg">Search in</span>
454                                                                        <button type="button" className="btn btn-default" onClick={this.toggleCorpusSelection}>
455                                                                                {this.props.corpora.getSelectedMessage()} <span className="caret"/>
456                                                                        </button>
457                                                        </div>
458
459                                                        <div className="input-group" style={{marginRight:10}}>
460                                                               
461                                                                <span className="input-group-addon nobkg" >of</span>
462                                                               
463                                                                <div className="input-group-btn">
464                                                                        <button className="form-control btn btn-default"
465                                                                                        aria-expanded="false" data-toggle="dropdown">
466                                                                                {this.state.language[1]} <span className="caret"/>
467                                                                        </button>
468                                                                        <ul ref="languageDropdownMenu" className="dropdown-menu">
469                                                                                <li key={this.anyLanguage[0]}> <a tabIndex="-1" href="#"
470                                                                                                onClick={this.setLanguage.bind(this, this.anyLanguage)}>
471                                                                                        {this.anyLanguage[1]}</a>
472                                                                                </li>
473                                                                                {       _.pairs(this.props.languageMap).sort(function(l1, l2){
474                                                                                                return l1[1].localeCompare(l2[1]);
475                                                                                        }).map(function(l) {
476                                                                                                var desc = l[1] + " [" + l[0] + "]";
477                                                                                                return <li key={l[0]}> <a tabIndex="-1" href="#"
478                                                                                                        onClick={this.setLanguage.bind(this, l)}>{desc}</a></li>;
479                                                                                        }.bind(this))
480                                                                                }
481                                                                        </ul>
482                                                                </div>
483
484                                                                <div className="input-group-btn">
485                                                                        <ul ref="layerDropdownMenu" className="dropdown-menu">
486                                                                                {       layers.map(function(l) {
487                                                                                                return <li key={l.id}> <a tabIndex="-1" href="#"
488                                                                                                        onClick={this.setLayer.bind(this, l.id)}> {l.name} </a></li>;
489                                                                                        }.bind(this))
490                                                                                }
491                                                                        </ul>                                                           
492                                                                        <button className="form-control btn btn-default"
493                                                                                        aria-expanded="false" data-toggle="dropdown" >
494                                                                                {layer.name} <span className="caret"/>
495                                                                        </button>
496                                                                </div>
497
498                                                        </div>
499
500                                                        <div className="input-group">
501                                                                <span className="input-group-addon nobkg">and show up to</span>
502                                                                <div className="input-group-btn">
503                                                                        <input type="number" className="form-control input" min="10" max="250" step="5"
504                                                                                onChange={this.setNumberOfResults} value={this.state.numberOfResults}
505                                                                                onKeyPress={this.stop}/>
506                                                                </div>
507                                                                <span className="input-group-addon nobkg">hits</span>
508                                                        </div>
509                                                </form>
510                                        </div>
511                                </div>
512
513                    <Modal ref="corporaModal" title="Collections">
514                                        <CorpusView corpora={this.props.corpora} languageMap={this.props.languageMap} />
515                    </Modal>
516
517                                <div className="top-gap">
518                                        <Results requests={this.state.hits.requests} results={this.state.hits.results} />
519                                </div>
520                        </div>
521                        );
522        },
523        render: function() {
524                return this.renderAggregator();
525        }
526});
527
528var StatisticsPage = React.createClass({
529        propTypes: {
530                ajax: PT.func.isRequired,
531        },
532
533        getInitialState: function () {
534                return {
535                        searchStats: {},
536                        lastScanStats: {},
537                };
538        },
539
540        componentDidMount: function() {
541                this.refreshStats();
542        },
543
544        refreshStats: function() {
545                this.props.ajax({
546                        url: 'rest/statistics',
547                        success: function(json, textStatus, jqXHR) {
548                                this.setState({
549                                        searchStats: json.searchStats,
550                                        lastScanStats: json.lastScanStats,
551                                });
552                                console.log("stats:", json);
553                        }.bind(this),
554                });
555        },
556
557        listItem: function(it) {
558                return <li>     {it[0]}:
559                                        { typeof(it[1]) === "object" ?
560                                                <ul>{_.pairs(it[1]).map(this.listItem)}</ul> :
561                                                it[1]
562                                        }
563                                </li>;
564        },
565
566        // renderEndpoint: function(endp) {
567        //      return <li>
568        //                              <ul>
569        //                                      <li>endpoint: {endp[0]}</li>
570        //                              <li>numberOfRequests: {endp[1].numberOfRequests}</li>
571        //                              <li>avgQueueTime: {endp[1].avgQueueTime}</li>
572        //                              <li>maxQueueTime: {endp[1].maxQueueTime}</li>
573        //                              <li>avgExecutionTime: {endp[1].avgExecutionTime}</li>
574        //                              <li>maxExecutionTime: {endp[1].maxExecutionTime}</li>
575        //                                      <li>errors
576        //                                              <ul>
577        //                                                      { _.pairs(object).map(endp[1].errors, function(e) { return <li>{e[0]}:{e[1]}</li>; }) }
578        //                                              </ul>
579        //                                      </li>
580        //                              </ul>
581        //                      </li>;
582        // },
583        // renderInstitution: function(instname, instendps) {
584        //      return  <li>
585        //                              <ul>
586        //                                      <li>{instname}</li>
587        //                                      <li>
588        //                                              <ul>{_.pairs(object).map(instendps, this.renderEndpoint)}</ul>
589        //                                      </li>
590 //                                     </ul>
591 //                             </li>;
592        // },
593
594        renderStatistics: function(stats) {
595                return <ul>{_.pairs(stats).map(this.listItem)}</ul>;
596        },
597
598        render: function() {
599                return  (
600                        <div>
601                                <div className="top-gap">
602                                        <h1>Statistics</h1>
603                                        <h2>Last Scan</h2>
604                                        {this.renderStatistics(this.state.lastScanStats)}
605                                        <h2>Search</h2>
606                                        {this.renderStatistics(this.state.searchStats)}
607                                </div>
608                        </div>
609                        );
610        },
611});
612
613var HelpPage = React.createClass({
614        openHelpDesk: function() {
615                window.open('http://support.clarin-d.de/mail/form.php?queue=Aggregator',
616                        '_blank', 'height=560,width=370');
617        },
618
619        render: function() {
620                return  (
621                        <div>
622                                <div className="top-gap">
623                                        <h3>Performing search in FCS corpora</h3>
624                                        <p>To perform simple keyword search in all CLARIN-D Federated Content Search centers
625                                        and their corpora, go to the search field at the top of the page,
626                                        enter your query, and click 'search' button or press the 'Enter' key.</p>
627                                       
628                                        <h3>Search Options - adjusting search criteria</h3>
629                                        <p>To select specific corpora based on their name or language and to specify
630                                        number of search results (hits) per corpus per page, click on the 'Search options'
631                                        link. Here, you can filter resources based on the language, select specific resources,
632                                        set the maximum number of hits.</p>
633
634                                        <h3>Search Results - inspecting search results</h3>
635                                        <p>When the search starts, the 'Search results' page is displayed
636                                        and its content starts to get filled with the corpora responses.
637                                        To save or process the displayed search result, in the 'Search results' page,
638                                        go to the menu and select either 'Export to Personal Workspace',
639                                        'Download' or 'Use WebLicht' menu item. This menu appears only after
640                                        all the results on the page have been loaded. To get the next hits from each corpus,
641                                        click the 'next' arrow at the bottom of 'Search results' page.</p>
642
643
644                                        <h3>More help</h3>
645                                        <p>More detailed information on using FCS Aggregator is available
646                                        at the Aggegator wiki page. If you still cannot find an answer to your question,
647                                        or if want to send a feedback, you can write to Clarin-D helpdesk: </p>
648                                        <button type="button" className="btn btn-default btn-lg" onClick={this.openHelpDesk} >
649                                                <span className="glyphicon glyphicon-question-sign" aria-hidden="true"></span>
650                                                &nbsp;HelpDesk
651                                        </button>                                       
652                                </div>
653                        </div>
654                );
655        }
656});
657
658var _ = _ || {
659        keys: function() {
660                var ret = [];
661                for (var x in o) {
662                        if (o.hasOwnProperty(x)) {
663                                ret.push(x);
664                        }
665                }
666                return ret;
667        },
668
669        pairs: function(o){
670                var ret = [];
671                for (var x in o) {
672                        if (o.hasOwnProperty(x)) {
673                                ret.push([x, o[x]]);
674                        }
675                }
676                return ret;
677        },
678};
679
680
681React.render(<Main />, document.getElementById('reactMain') );
682})();
Note: See TracBrowser for help on using the repository browser.