diff --git a/Gruntfile.js b/Gruntfile.js index 6d90f5bd..78cd2d76 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -100,6 +100,8 @@ module.exports = function (grunt) { jsDir + '/utilities.js', jsDir + '/MinervaModel.js', jsDir + '/MinervaCollection.js', + jsDir + '/models/DatasetModel.js', + jsDir + '/models/SourceModel.js', jsDir + '/models/**/*.js', jsDir + '/collections/**/*.js', jsDir + '/views/**/*.js' diff --git a/development.md b/development.md new file mode 100644 index 00000000..d390d93e --- /dev/null +++ b/development.md @@ -0,0 +1,150 @@ +# Minerva developers guide + +## Glossary + +### Source + +A source produces data. A source itself cannot be visualized, but a source +can create a dataset that can be visualized, or it can be the input to an analysis, +which then creates a dataset that can be visualized. + +### Dataset + +A dataset contains data and can be visualized, either on a map or through some other means. + +### Analysis + +An analysis creates a dataset, but running some client side or server side process, and +potentially using datasets and sources as inputs. + +## Adding a source + +I think it's easier to work on the backend first, as you can test the api +independently of the client. We'll work through an example using the +Elasticsearch source. + +### Source api + +Create an endpoint to create the source, copy server/rest/wms_source.py to +server/rest/elasticsearch_source.py and modify accordingly. + +Important points are + + * use the access decorator to ensure only logged in users can call the endpoint + * create minerva_metadata with the correct `source_type` + * save the source using the superclass method createSource + * return the document corresponding to the new source + * here we store the authentication credentials after encryption + * set the description object to display the params correctly on the swagger api page + +Add the endpoint to server/loader.py + + info['apiRoot'].minerva_source_elasticsearch = elasticsearch_source.ElasticsearchSource() + +You should now be able to see your endpoint through the swagger api page, and +test it there. Usually + + http://localhost:8080/api + +### Testing the source api + +Create a test, copy plugin_tests/wms_test.py to plugin_tests/elasticsearch_test.py. + +Add the test to plugin.cmake + + add_python_test(elasticsearch PLUGIN minerva) + +Run `cmake PATH_TO_GIRDER_DIR` again in your build directory to pick up the new test. + +Now you should see the new test in your build directory + + ctest -N | grep minerva + Test #110: server_minerva.dataset + Test #111: server_minerva.source + Test #112: server_minerva.session + Test #113: server_minerva.analysis + Test #114: server_minerva.geonames + Test #115: server_minerva.s3_dataset + Test #116: server_minerva.import_analyses + Test #117: server_minerva.contour_analysis + Test #118: server_minerva.wms + Test #119: server_minerva.elasticsearch + Test #120: server_minerva.geojson + Test #121: server_minerva.mean_contour_analysis + Test #122: pep8_style_minerva_constants + Test #123: pep8_style_minerva_geonames + Test #124: pep8_style_minerva_rest + Test #125: pep8_style_minerva_utility + Test #126: pep8_style_minerva_bsve + Test #127: pep8_style_minerva_jobs + Test #128: jshint_minerva + Test #129: jsstyle_minerva + Test #130: web_client_minerva + +You can run the test, with extra verbosity + + ctest -R server_minerva.elasticsearch -VV + +Also check your python style, and fix any errors + + ctest -R pep8_style_minerva_rest -VV + +### Add the source to client side collection + +Add the new source type to web/external/js/collections/SourceCollection.js, +this will prevent mysterious backbone errors later on like + + `a.on is not a function` + +You're welcome. + +### Add a new source model + +Add a new model like web_external/js/models/ElasticsearchSourceModel.js. + +### Add the source to AddSourceWidget + +Add your new source type as an option in web_external/templates/widgets/addSourceWidget.jade +and deal with the new option in the `submit #m-add-source-form` event handler in +web_external/js/views/widgets/AddSourceWidget.js by creating a new add widget specific +to your source, e.g. `AddElasticsearchSourceWidget`. + +Test that when you click on the add new source icon in the source panel, your +new source type is displayed as an option. + +Create the widget to add your new source type, e.g. in + + web_external/js/views/widgets/AddElasticsearchSourceWidget.js + web_external/templates/widgets/addElasticsearchSourceWidget.jade + +### Display the new source in the source panel + +Update the necessary in + + web_external/templates/body/sourcePanel.jade + web_external/stylesheets/body/sourcePanel.styl + +### Add an action to the source displayed in the source panel + +If it makes sense for your source to have an action, as when there is a natural +path to create a dataset from your source, add an action to the source displayed +in the source panel. + +E.g., a WMS source naturally creates datasets by exposing a set of WMS layers +and allowing one or more to be created as a dataset. An Elasticsearch source naturally +creates datasets by running an analysis which is a search query, resulting in a +JSON dataset with a default visualization as GeoJson. + +Add an event handler for your source icon in web_external/js/views/body/SourcePanel.js . + +Add the widget constructed and rendered by the event handler + + web_external/js/views/widgets/ElasticsearchWidget.js + web_external/templates/widgets/elasticsearchWidget.jade + +### Comply with javascript styles + +Because it's the law of the land. + + ctest -R jshint_minerva -VV + ctest -R jsstyle_minerva -VV diff --git a/plugin.cmake b/plugin.cmake index e79493f8..d576aae5 100644 --- a/plugin.cmake +++ b/plugin.cmake @@ -23,6 +23,7 @@ add_python_test(s3_dataset PLUGIN minerva) add_python_test(import_analyses PLUGIN minerva) add_python_test(contour_analysis PLUGIN minerva) add_python_test(wms PLUGIN minerva) +add_python_test(elasticsearch PLUGIN minerva) add_python_test(geojson PLUGIN minerva) diff --git a/plugin_tests/elasticsearch_test.py b/plugin_tests/elasticsearch_test.py new file mode 100644 index 00000000..e5d09d01 --- /dev/null +++ b/plugin_tests/elasticsearch_test.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################### +# Copyright Kitware Inc. +# +# Licensed under the Apache License, Version 2.0 ( the "License" ); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################### + +import os + +# Need to set the environment variable before importing girder +os.environ['GIRDER_PORT'] = os.environ.get('GIRDER_TEST_PORT', '20200') # noqa + +from tests import base + + +def setUpModule(): + """ + Enable the minerva plugin and start the server. + """ + base.enabledPlugins.append('jobs') + base.enabledPlugins.append('romanesco') + base.enabledPlugins.append('gravatar') + base.enabledPlugins.append('minerva') + base.startServer(False) + + +def tearDownModule(): + """ + Stop the server. + """ + base.stopServer() + + +class ElasticsearchTestCase(base.TestCase): + + """ + Tests of the minerva source API endpoints. + """ + + def setUp(self): + """ + Set up the test case with a user + """ + super(ElasticsearchTestCase, self).setUp() + + self._user = self.model('user').createUser( + 'minervauser', 'password', 'minerva', 'user', + 'minervauser@example.com') + + def testCreateElasticsearchSource(self): + """ + Test the minerva Elasticsearch source API endpoints. + """ + + path = '/minerva_source_elasticsearch' + name = 'testElasticsearch' + username = '' + password = '' + baseURL = 'http://elasticsearch.com' + index = 'myindex' + params = { + 'name': name, + 'username': username, + 'password': password, + 'index': index, + 'baseURL': baseURL + } + response = self.request(path=path, method='POST', params=params, user=self._user) + self.assertStatusOk(response) + elasticsearchSource = response.json + print(response.json) + minerva_metadata = elasticsearchSource['meta']['minerva'] + self.assertEquals(elasticsearchSource['name'], name, 'incorrect elasticsearch source name') + self.assertEquals(minerva_metadata['source_type'], 'elasticsearch', 'incorrect elasticsearch source type') + self.assertEquals(minerva_metadata['elasticsearch_params']['base_url'], baseURL, 'incorrect elasticsearch source baseURL') + self.assertEquals(minerva_metadata['elasticsearch_params']['index'], index, 'incorrect elasticsearch source index') + + return elasticsearchSource diff --git a/server/jobs/elasticsearch_worker.py b/server/jobs/elasticsearch_worker.py new file mode 100644 index 00000000..09bd096f --- /dev/null +++ b/server/jobs/elasticsearch_worker.py @@ -0,0 +1,111 @@ +import json +import os +import shutil +import sys +import tempfile +import traceback + +from elasticsearch import Elasticsearch + +from girder.constants import AccessType +from girder.utility import config +from girder.utility.model_importer import ModelImporter +from girder.plugins.jobs.constants import JobStatus +from girder.plugins.minerva.utility.dataset_utility import \ + jsonArrayHead + +from girder.plugins.minerva.utility.minerva_utility import decryptCredentials + +import girder_client + + +def run(job): + job_model = ModelImporter.model('job', 'jobs') + job_model.updateJob(job, status=JobStatus.RUNNING) + + try: + kwargs = job['kwargs'] + # TODO better to create a job token rather than a user token? + token = kwargs['token'] + datasetId = str(kwargs['dataset']['_id']) + + # connect to girder and upload the file + # TODO will probably have to change this from local to romanesco + # so that can work on worker machine + # at least need host connection info + girderPort = config.getConfig()['server.socket_port'] + client = girder_client.GirderClient(port=girderPort) + client.token = token['_id'] + + # Get datasource + source = client.getItem(kwargs['params']['sourceId']) + esUrl = 'https://%s@%s' % (decryptCredentials( + source['meta']['minerva']['elasticsearch_params']['credentials']), + source['meta']['minerva']['elasticsearch_params']['host_name']) + es = Elasticsearch([esUrl]) + + # TODO sleeping in async thread, probably starving other tasks + # would be better to split this into two or more parts, creating + # additional jobs as needed + searchResult = es.search( + index=source['meta']['minerva']['elasticsearch_params']['index'], + body=json.loads(kwargs['params']['searchParams'])) + + # write the output to a json file + tmpdir = tempfile.mkdtemp() + outFilepath = tempfile.mkstemp(suffix='.json', dir=tmpdir)[1] + writer = open(outFilepath, 'w') + writer.write(json.dumps(searchResult)) + writer.close() + + # rename the file so it will have the right name when uploaded + # could probably be done post upload + outFilename = 'search.json' + humanFilepath = os.path.join(tmpdir, outFilename) + shutil.move(outFilepath, humanFilepath) + + client.uploadFileToItem(datasetId, humanFilepath) + + # TODO some stuff here using models will only work on a local job + # will have to be rewritten using girder client to work in romanesco + # non-locally + + user_model = ModelImporter.model('user') + user = user_model.load(job['userId'], force=True) + item_model = ModelImporter.model('item') + # TODO only works locally + dataset = item_model.load(datasetId, level=AccessType.WRITE, user=user) + metadata = dataset['meta'] + minerva_metadata = metadata['minerva'] + + # TODO only works locally + file_model = ModelImporter.model('file') + existing = file_model.findOne({ + 'itemId': dataset['_id'], + 'name': outFilename + }) + if existing: + minerva_metadata['original_files'] = [{ + '_id': existing['_id'], + 'name': outFilename + }] + else: + raise (Exception('Cannot find file %s in dataset %s' % + (outFilename, datasetId))) + + jsonRow = jsonArrayHead(humanFilepath, limit=1)[0] + minerva_metadata['json_row'] = jsonRow + + shutil.rmtree(tmpdir) + + metadata['minerva'] = minerva_metadata + # TODO only works locally + item_model.setMetadata(dataset, metadata) + # TODO only works locally + job_model.updateJob(job, status=JobStatus.SUCCESS) + except Exception: + t, val, tb = sys.exc_info() + log = '%s: %s\n%s' % (t.__name__, repr(val), traceback.extract_tb(tb)) + # TODO only works locally + job_model.updateJob(job, status=JobStatus.ERROR, log=log) + raise diff --git a/server/loader.py b/server/loader.py index a2c0e387..f2521e0f 100644 --- a/server/loader.py +++ b/server/loader.py @@ -28,8 +28,7 @@ from girder.plugins.minerva.rest import \ analysis, dataset, s3_dataset, session, shapefile, geocode, source, \ - wms_dataset, wms_source, geojson_dataset -from girder.plugins.minerva.constants import PluginSettings + wms_dataset, wms_source, geojson_dataset, elasticsearch_source from girder.plugins.minerva.utility.minerva_utility import decryptCredentials @@ -187,8 +186,17 @@ def load(info): info['apiRoot'].minerva_analysis = analysis.Analysis() info['apiRoot'].minerva_session = session.Session() info['apiRoot'].minerva_dataset_s3 = s3_dataset.S3Dataset() + info['apiRoot'].minerva_source = source.Source() + info['apiRoot'].minerva_source_wms = wms_source.WmsSource() info['apiRoot'].minerva_dataset_wms = wms_dataset.WmsDataset() + info['apiRoot'].minerva_dataset_geojson = geojson_dataset.GeojsonDataset() + + info['apiRoot'].minerva_source_elasticsearch = \ + elasticsearch_source.ElasticsearchSource() + info['apiRoot'].minerva_query_elasticsearch = \ + elasticsearch_source.ElasticsearchQuery() + info['serverRoot'].wms_proxy = WmsProxy() diff --git a/server/rest/elasticsearch_source.py b/server/rest/elasticsearch_source.py new file mode 100644 index 00000000..303ed239 --- /dev/null +++ b/server/rest/elasticsearch_source.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################### +# Copyright Kitware Inc. +# +# Licensed under the Apache License, Version 2.0 ( the "License" ); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################### + +from girder.api import access +from girder.api.describe import Description +from girder.api.rest import getUrlParts, Resource + +from girder.plugins.minerva.rest.source import Source +from girder.plugins.minerva.utility.minerva_utility import \ + encryptCredentials, findDatasetFolder, updateMinervaMetadata + + +class ElasticsearchSource(Source): + + def __init__(self): + self.resourceName = 'minerva_source_elasticsearch' + self.route('POST', (), self.createElasticsearchSource) + + @access.user + def createElasticsearchSource(self, params): + name = params['name'] + baseURL = params['baseURL'] + parsedUrl = getUrlParts(baseURL) + hostName = parsedUrl.netloc + index = params['index'] + username = params['username'] if 'username' in params else None + password = params['password'] if 'password' in params else None + minerva_metadata = { + 'source_type': 'elasticsearch', + 'elasticsearch_params': { + 'index': index, + 'base_url': baseURL, + 'host_name': hostName + } + } + if username and password: + enc_creds = encryptCredentials("{}:{}".format( + username, password)) + minerva_metadata['elasticsearch_params']['credentials'] = enc_creds + desc = 'elasticsearch source for %s' % name + return self.createSource(name, minerva_metadata, desc) + createElasticsearchSource.description = ( + Description('Create a source from an external elasticsearch server.') + .responseClass('Item') + .param('name', 'The name of the elasticsearch source', required=True) + .param('baseURL', 'URL of the elasticsearch service', required=True) + .param('index', 'Index of interest', required=True) + .param('username', 'Elasticsearch username', required=False) + .param('password', 'Elasticsearch password', required=False) + .errorResponse('Write permission denied on the source folder.', 403)) + + +class ElasticsearchQuery(Resource): + + def __init__(self): + self.resourceName = 'minerva_query_elasticsearch' + self.route('POST', (), self.queryElasticsearch) + + @access.user + def queryElasticsearch(self, params): + """ + Creates a local job to run the elasticsearch_worker, the job will store + the results of the elastic search query in a dataset. + """ + currentUser = self.getCurrentUser() + datasetName = params['datasetName'] + elasticsearchParams = params['searchParams'] + + datasetFolder = findDatasetFolder(currentUser, currentUser) + dataset = (self.model('item').createItem( + datasetName, + currentUser, + datasetFolder, + 'created by elasticsearch query')) + + user, token = self.getCurrentUser(returnToken=True) + kwargs = { + 'params': params, + 'user': currentUser, + 'dataset': dataset, + 'token': token, + 'sourceId': params['sourceId'] + } + + job = self.model('job', 'jobs').createLocalJob( + title='elasticsearch: %s' % datasetName, + user=currentUser, + type='elasticsearch', + public=False, + kwargs=kwargs, + module='girder.plugins.minerva.jobs.elasticsearch_worker', + async=True) + + minerva_metadata = { + 'dataset_type': 'json', + 'source_id': params['sourceId'], + 'source': 'elasticsearch', + 'elasticsearch_params': elasticsearchParams + } + updateMinervaMetadata(dataset, minerva_metadata) + + self.model('job', 'jobs').scheduleJob(job) + + return job + + queryElasticsearch.description = ( + Description('Query an elasticsearch source.') + .param('sourceId', 'Item id of the elasticsearch source to query') + .param('datasetName', 'The name of the resulting dataset') + .param('query', 'JSON Body of an elasticsearch query')) diff --git a/web_external/js/collections/SourceCollection.js b/web_external/js/collections/SourceCollection.js index 5759f0fb..83d1d714 100644 --- a/web_external/js/collections/SourceCollection.js +++ b/web_external/js/collections/SourceCollection.js @@ -4,17 +4,19 @@ minerva.collections.SourceCollection = minerva.collections.MinervaCollection.ext if (attrs.meta && ('minerva' in attrs.meta)) { if (attrs.meta.minerva.source_type === 'wms') { return new minerva.models.WmsSourceModel(attrs, options); + } else if (attrs.meta.minerva.source_type === 'elasticsearch') { + return new minerva.models.ElasticsearchSourceModel(attrs, options); } - } else { - console.error('Source collection includes unknown source type'); - console.error(attrs); - girder.events.trigger('g:alert', { - icon: 'cancel', - text: 'Unknown source type in collection.', - type: 'error', - timeout: 4000 - }); } + + console.error('Source collection includes unknown source type'); + console.error(attrs); + girder.events.trigger('g:alert', { + icon: 'cancel', + text: 'Unknown source type in collection.', + type: 'error', + timeout: 4000 + }); }, path: 'minerva_source', diff --git a/web_external/js/models/ElasticsearchSourceModel.js b/web_external/js/models/ElasticsearchSourceModel.js new file mode 100644 index 00000000..15a44415 --- /dev/null +++ b/web_external/js/models/ElasticsearchSourceModel.js @@ -0,0 +1,19 @@ +minerva.models.ElasticsearchSourceModel = minerva.models.SourceModel.extend({ + + createSource: function (params) { + girder.restRequest({ + path: '/minerva_source_elasticsearch', + type: 'POST', + data: params, + error: null // ignore default error behavior (validation may fail) + }).done(_.bind(function (resp) { + this.set(resp); + this.trigger('m:sourceReceived'); + }, this)).error(_.bind(function (err) { + this.trigger('m:error', err); + }, this)); + + return this; + } + +}); diff --git a/web_external/js/views/body/SourcePanel.js b/web_external/js/views/body/SourcePanel.js index 95fadd7a..04953d2a 100644 --- a/web_external/js/views/body/SourcePanel.js +++ b/web_external/js/views/body/SourcePanel.js @@ -4,7 +4,8 @@ minerva.views.SourcePanel = minerva.View.extend({ 'click .m-add-source': 'addSourceDialog', 'click .m-display-wms-layers-list': 'displayWmsLayersList', 'click .m-icon-info': 'displaySourceInfo', - 'click .m-delete-source': 'deleteSource' + 'click .m-delete-source': 'deleteSource', + 'click .m-display-elasticsearch-query': 'displayElasticsearchQuery' }, addSourceDialog: function () { @@ -51,6 +52,20 @@ minerva.views.SourcePanel = minerva.View.extend({ source.destroy(); }, + displayElasticsearchQuery: function (evt) { + var el = $(evt.currentTarget); + var source = this.sourceCollection.get(el.attr('cid')); + if (!this.elasticsearchWidget) { + this.elasticsearchWidget = new minerva.views.ElasticsearchWidget({ + el: $('#g-dialog-container'), + source: source, + collection: this.datasetCollection, + parentView: this + }); + } + this.elasticsearchWidget.render(); + }, + initialize: function (settings) { this.session = settings.session; this.sourceCollection = settings.sourceCollection; diff --git a/web_external/js/views/widgets/AddElasticsearchSourceWidget.js b/web_external/js/views/widgets/AddElasticsearchSourceWidget.js new file mode 100644 index 00000000..fac21d07 --- /dev/null +++ b/web_external/js/views/widgets/AddElasticsearchSourceWidget.js @@ -0,0 +1,34 @@ +/** +* This widget displays a form for adding an Elasticsearch source. +*/ +minerva.views.AddElasticsearchSourceWidget = minerva.View.extend({ + + events: { + 'submit #m-add-elasticsearch-source-form': function (e) { + e.preventDefault(); + var params = { + name: this.$('#m-elasticsearch-name').val(), + baseURL: this.$('#m-elasticsearch-uri').val(), + index: this.$('#m-elasticsearch-index').val(), + username: this.$('#m-elasticsearch-username').val(), + password: this.$('#m-elasticsearch-password').val() + }; + var elasticsearchSource = new minerva.models.ElasticsearchSourceModel({}); + elasticsearchSource.on('m:sourceReceived', function () { + this.$el.modal('hide'); + this.collection.add(elasticsearchSource); + }, this).createSource(params); + } + }, + + initialize: function (settings) { + this.collection = settings.collection; + }, + + render: function () { + var modal = this.$el.html(minerva.templates.addElasticsearchSourceWidget({})); + modal.trigger($.Event('ready.girder.modal', {relatedTarget: modal})); + return this; + } + +}); diff --git a/web_external/js/views/widgets/AddSourceWidget.js b/web_external/js/views/widgets/AddSourceWidget.js index d4335206..cd02c2f0 100644 --- a/web_external/js/views/widgets/AddSourceWidget.js +++ b/web_external/js/views/widgets/AddSourceWidget.js @@ -17,6 +17,14 @@ minerva.views.AddSourceWidget = minerva.View.extend({ collection: this.collection, parentView: this.parentView }).render(); + } else if (sourceType === 'm-elasticsearch-source') { + this.elasticsearchSourceWidget = new minerva.views.AddElasticsearchSourceWidget({ + el: container, + title: 'Enter Elasticsearch Source details', + noParent: true, + collection: this.collection, + parentView: this.parentView + }).render(); } else { console.error('Unknown source type'); } diff --git a/web_external/js/views/widgets/AddWmsSourceWidget.js b/web_external/js/views/widgets/AddWmsSourceWidget.js index d267e3d1..c17ea7c9 100644 --- a/web_external/js/views/widgets/AddWmsSourceWidget.js +++ b/web_external/js/views/widgets/AddWmsSourceWidget.js @@ -1,5 +1,5 @@ /** -* This widget displays a form for adding WMS services +* This widget displays a form for adding a WMS source. */ minerva.views.AddWmsSourceWidget = minerva.View.extend({ diff --git a/web_external/js/views/widgets/ElasticsearchWidget.js b/web_external/js/views/widgets/ElasticsearchWidget.js new file mode 100644 index 00000000..ac82f0c3 --- /dev/null +++ b/web_external/js/views/widgets/ElasticsearchWidget.js @@ -0,0 +1,60 @@ +/** +* This widget is used to display and run an Elasticsearch query. +*/ +minerva.views.ElasticsearchWidget = minerva.View.extend({ + + events: { + 'submit #m-elasticsearch-form': function (e) { + e.preventDefault(); + this.$('.g-validation-failed-message').text(''); + + var searchParams = this.$('#m-elasticsearch-dataset-params').val(); + var datasetName = this.$('#m-elasticsearch-dataset-name').val(); + + if (!datasetName || datasetName === '') { + this.$('.g-validation-failed-message').text('Dataset name is required'); + return; + } + + try { + // parse for side effect of validation + JSON.parse(searchParams); + this.$('.g-validation-failed-message').text(''); + } catch (err) { + this.$('.g-validation-failed-message').text('Search Params must be valid JSON'); + return; + } + + this.$('button.m-run-elasticsearch-query').addClass('disabled'); + + var data = { + datasetName: datasetName, + searchParams: searchParams, + sourceId: this.source.get('id') + }; + + girder.restRequest({ + path: 'minerva_query_elasticsearch', + type: 'POST', + data: data + }).done(_.bind(function () { + girder.events.trigger('m:job.created'); + this.$el.modal('hide'); + }, this)); + + } + }, + + initialize: function (settings) { + this.source = settings.source; + }, + + render: function () { + var modal = this.$el.html(minerva.templates.elasticsearchWidget({ + })).girderModal(this).on('ready.girder.modal', _.bind(function () { + }, this)); + modal.trigger($.Event('ready.girder.modal', {relatedTarget: modal})); + + return this; + } +}); diff --git a/web_external/stylesheets/body/sourcePanel.styl b/web_external/stylesheets/body/sourcePanel.styl index f20c3ee0..891df0aa 100644 --- a/web_external/stylesheets/body/sourcePanel.styl +++ b/web_external/stylesheets/body/sourcePanel.styl @@ -14,6 +14,9 @@ .m-display-wms-layers-list float: left + .m-display-elasticsearch-query + float: left + .m-delete-source float right diff --git a/web_external/templates/body/sourcePanel.jade b/web_external/templates/body/sourcePanel.jade index ca8c32da..b37c7799 100644 --- a/web_external/templates/body/sourcePanel.jade +++ b/web_external/templates/body/sourcePanel.jade @@ -11,10 +11,14 @@ - var attributes = {'cid': source.cid} if source.getSourceType() === 'wms' //- list icon to open the list of WMS layers - - var classes = 'icon-list m-icon-enabled m-display-wms-layers-list' + - var classes = 'icon-layers m-icon-enabled m-display-wms-layers-list' i(class=classes)&attributes(attributes) - //- trash icon to delete source - - var classes = 'icon-trash m-icon-enabled m-delete-source' - i(title='delete source', class=classes)&attributes(attributes) - //- info icon for minerva metadata display - i(title='display source info').icon-info-circled.m-icon-info.m-icon-enabled&attributes(attributes) + else if source.getSourceType() === 'elasticsearch' + //- list icon to run an elasticsearch query + - var classes = 'icon-search m-icon-enabled m-display-elasticsearch-query' + i(class=classes)&attributes(attributes) + //- trash icon to delete source + - var classes = 'icon-trash m-icon-enabled m-delete-source' + i(title='delete source', class=classes)&attributes(attributes) + //- info icon for minerva metadata display + i(title='display source info').icon-info-circled.m-icon-info.m-icon-enabled&attributes(attributes) diff --git a/web_external/templates/widgets/addElasticsearchSourceWidget.jade b/web_external/templates/widgets/addElasticsearchSourceWidget.jade new file mode 100644 index 00000000..e544858d --- /dev/null +++ b/web_external/templates/widgets/addElasticsearchSourceWidget.jade @@ -0,0 +1,30 @@ +.modal-dialog + .modal-content + form#m-add-elasticsearch-source-form.modal-form(role="form") + .modal-header + button.close(data-dismiss="modal", aria-hidden="true", type="button") × + h4.modal-title + | Enter Elasticsearch Service details + + .modal-body + .form-group + label.control-label(for="m-elasticsearch-name") Name + input.input-sm#m-elasticsearch-name.form-control(type="text", placeholder="Enter Elasticsearch name") + .form-group + label.control-label(for="m-elasticsearch-uri") Elasticsearch baseURL + input.input-sm#m-elasticsearch-uri.form-control(type="text", placeholder="http://elasticsearch.com") + .form-group + label.control-label(for="m-elasticsearch-index") Elasticsearch index + input.input-sm#m-elasticsearch-index.form-control(type="text", placeholder="index") + .form-group + label.control-label(for="m-elasticsearch-username") Username + input.input-sm#m-elasticsearch-username.form-control(type="text", placeholder="username") + .form-group + label.control-label(for="m-elasticsearch-password") Password + input.input-sm#m-elasticsearch-password.form-control(type="password", placeholder="password") + + .modal-footer + a.btn.btn-small.btn-default(data-dismiss="modal") Cancel + button.m-add-service-button.btn.btn-small.btn-primary + i.icon-plus-squared + | Add Elasticsearch Source diff --git a/web_external/templates/widgets/addSourceWidget.jade b/web_external/templates/widgets/addSourceWidget.jade index b36ed19e..80fee72c 100644 --- a/web_external/templates/widgets/addSourceWidget.jade +++ b/web_external/templates/widgets/addSourceWidget.jade @@ -12,6 +12,11 @@ label i.icon-layers.m-add-source-type-icon | WMS Source + .radio + input#m-elasticsearch-source(type="radio", name="m-source-add-method") + label + i.icon-search.m-add-source-type-icon + | Elasticsearch Source .modal-footer a.btn.btn-small.btn-default(data-dismiss="modal") Cancel button.m-add-source-button.btn.btn-small.btn-primary diff --git a/web_external/templates/widgets/addWmsSourceWidget.jade b/web_external/templates/widgets/addWmsSourceWidget.jade index f1fd9d50..743c1656 100644 --- a/web_external/templates/widgets/addWmsSourceWidget.jade +++ b/web_external/templates/widgets/addWmsSourceWidget.jade @@ -24,4 +24,4 @@ a.btn.btn-small.btn-default(data-dismiss="modal") Cancel button.m-add-service-button.btn.btn-small.btn-primary i.icon-plus-squared - | Add WMS Service + | Add WMS Source diff --git a/web_external/templates/widgets/elasticsearchWidget.jade b/web_external/templates/widgets/elasticsearchWidget.jade new file mode 100644 index 00000000..e5937eef --- /dev/null +++ b/web_external/templates/widgets/elasticsearchWidget.jade @@ -0,0 +1,20 @@ +.modal-dialog + .modal-content + form#m-elasticsearch-form.modal-form(role="form") + .modal-header + button.close(data-dismiss="modal", aria-hidden="true", type="button") × + h4.modal-title + | Elasticsearch Query + .modal-body + .form-group.m-elasticsearch-dataset-name-group + label.control-label(for='m-elasticsearch-dataset-name') Output dataset name + input.input-sm#m-elasticsearch-dataset-name.form-control(type="text") + .form-group.m-elasticsearch-dataset-params-group + label.control-label(for="m-elasticsearch-dataset-params") JSON search params + textarea#m-elasticsearch-dataset-params.form-control(rows=20) + .g-validation-failed-message + .modal-footer + a.btn.btn-small.btn-default(data-dismiss="modal") Cancel + button.m-run-elasticsearch-query.btn.btn-small.btn-primary(type="submit") + i.icon-play-circled + | Run